Git & GitHub
Remotes, Pull Requests and Code Review
Remotes, fetch versus pull, upstream tracking, forks, and the pull request flow with review habits.
On this page
In one line: a remote is just another copy of your repository, usually on GitHub. You send commits with push, get new ones with fetch (safe) or pull (fetch plus merge), and on GitHub you ask for your work to be joined through a pull request that others review.
So far everything lived on one computer. Real work is shared. This lesson covers the Git side (clone, remote, fetch, pull, push, upstream tracking, forks) with real runs, and then the GitHub side (the pull request flow, writing a good description and reviewing), which I describe from the GitHub docs but did not run.
The review part is about code and the Git tools. For the human side of reviewing (tone, size, what to look for), read Team Lead Lesson 3.
What I ran / checked. Git commands ran with git version 2.43.0 on Linux. GitHub itself was not reachable for pushes, so I used a local bare repository as the "remote". A bare repository is a repository with no working files, only the history, and it is what a server keeps. git push and git fetch to a folder path behave like they do to a GitHub URL, but everything that is special to GitHub (pull requests, reviews, forks as a button, branch protection, Actions) was not run. I describe those from the github/docs repository (and from general knowledge for details I did not find there, such as the "Compare & pull request" banner and the Fixes #123 keyword): content/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository.md, content/pull-requests/reference/pull-request-merges.md and content/repositories/.../about-code-owners.md. For the fork simulation I used two more local bare repositories. For repeatable output I used the names "Ada Lovelace" and "Bob" and fixed dates; your hashes will differ.
Words used in this lesson
- A remote is another copy of the repository, kept on a server or in another folder. Its short name is often
origin. originis the default name Git gives to the place you cloned from.- To clone is to copy a whole repository, with all history, and set up
origin. - A remote-tracking branch is your local, read-only note of where a remote branch was the last time you talked to it. It looks like
origin/main. - Upstream (tracking) branch is the remote branch that your local branch is linked to. It decides what a plain
git pushandgit pulldo. - To fetch is to download new commits and update
origin/*. Your own branches and files do not change. - To pull is
fetchfollowed by a merge (or rebase) into your current branch. - To push is to send your commits to a remote branch.
- A fork is your own copy of someone else's repository on GitHub.
- A pull request (PR) is a request on GitHub to merge one branch into another, with a page for discussion and review.
- A review is when another person reads your change and approves it or asks for changes.
Remotes
A remote is only a name plus a URL. I start with a bare repository as the "server" and a normal repository for Ada:
git -c init.defaultBranch=main init --bare -q origin.gitgit -c init.defaultBranch=main init -q ada && cd adaecho "# Notes" > README.md && git add . && git commit -qm "Add README"git remote -v(The git remote -v printed nothing: a new repository has no remotes.) Add one and push:
git remote add origin ../origin.gitgit remote -vgit push origin maingit push -u origin maingit branch -vvorigin ../origin.git (fetch)origin ../origin.git (push)To ../origin.git * [new branch] main -> mainEverything up-to-datebranch 'main' set up to track 'origin/main'.* main 80dc38d [origin/main] Add READMEWith GitHub the URL would look like https://github.com/you/repo.git or git@github.com:you/repo.git (the second form uses an SSH key). Everything else is the same. The -u (long form --set-upstream) tells Git to link local main to origin/main. git branch -vv shows the link in brackets. The link is stored in the repository config:
[remote "origin"] url = ../origin.git fetch = +refs/heads/*:refs/remotes/origin/*[branch "main"] remote = origin merge = refs/heads/mainClone
Bob joins the project. He does not set up a remote by hand. git clone copies everything and sets up origin:
git clone origin.git bobcd bobgit branch -aCloning into 'bob'...done.* main remotes/origin/HEAD -> origin/main remotes/origin/mainBob has a local main and a remote-tracking branch remotes/origin/main, and main is already linked to it. Clone also sets origin/HEAD, the remote's default branch.
Fetch, pull and push
Bob makes a commit and pushes. Because main has an upstream, plain git push is enough:
echo "bob line" >> README.md && git commit -qam "Bob adds a line"git pushTo .../origin.git 80dc38d..322fd66 main -> mainAda does not know yet. Her origin/main is only a note from her last talk with the server:
git statusgit fetchgit statusOn branch mainYour branch is up to date with 'origin/main'.nothing to commit, working tree cleanFrom ../origin 80dc38d..322fd66 main -> origin/mainOn branch mainYour branch is behind 'origin/main' by 1 commit, and can be fast-forwarded. (use "git pull" to update your local branch)nothing to commit, working tree cleanThe first status says "up to date" because Git never contacts the server by itself. After git fetch it knows Bob's commit exists. Fetch is safe: it only updates origin/*. It never touches her files or her branch. She can look before she decides:
git log --oneline --all --graph --decorategit diff main origin/main --statgit merge origin/main* 322fd66 (origin/main) Bob adds a line* 80dc38d (HEAD -> main) Add README README.md | 1 + 1 file changed, 1 insertion(+)Updating 80dc38d..322fd66Fast-forward README.md | 1 + 1 file changed, 1 insertion(+)That is exactly what git pull does, in one step:
git pull = git fetch + git merge origin/main (or a rebase, if you configure it)| Command | Changes my branch? | Safe to run any time? |
|---|---|---|
git fetch | No (only origin/*) | Yes |
git pull | Yes (merge or rebase) | Mostly, but may stop on a conflict |
git push | No (changes the remote) | Rejected if the remote moved |
The rejected push
Now both edit and Bob pushes first. Ada has an unpushed commit and tries to push:
! [rejected] main -> main (fetch first)error: failed to push some refs to '../origin.git'hint: Updates were rejected because the remote contains work that you do nothint: have locally. This is usually caused by another repository pushing tohint: the same ref. If you want to integrate the remote changes, usehint: 'git pull' before pushing again.This is not a bug. It protects Bob's work: Git only lets a push fast-forward the remote branch. The fix is to bring Bob's commit into Ada's history first. Then I ran git pull, and this Git version stopped:
hint: You have divergent branches and need to specify how to reconcile them.hint: You can do so by running one of the following commands sometime beforehint: your next pull:hint:hint: git config pull.rebase false # mergehint: git config pull.rebase true # rebasehint: git config pull.ff only # fast-forward onlyfatal: Need to specify how to reconcile divergent branches.The histories have diverged (each side has a commit the other lacks), so Git asks which strategy you want. I chose merge:
git config pull.rebase falsegit pull --no-editgit pushgit log --oneline --graphMerge made by the 'ort' strategy. bob.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 bob.txtTo ../origin.git 25f3b1d..5216c83 main -> main* 5216c83 Merge branch 'main' of ../origin|\| * 25f3b1d Bob adds bob.txt* | ab8a3f9 Ada line 2|/* 322fd66 Bob adds a line* 80dc38d Add README(I removed trailing spaces from the graph lines.) Rebase (pull.rebase true) would give a straight line instead of a merge commit. Many teams prefer that; lesson 5 compares them. If you want to be safe, git pull --ff-only refuses to do anything except a fast-forward, so you can never get a surprise merge.
New branches and upstream tracking
git switch -c feature/greetingecho hi > greet.txt && git add greet.txt && git commit -qm "Add greeting"git pushfatal: The current branch feature/greeting has no upstream branch.To push the current branch and set the remote as upstream, use git push --set-upstream origin feature/greetingGit does not guess where a new branch should go. Do what it says once:
git push -u origin feature/greetinggit branch -vv * [new branch] feature/greeting -> feature/greetingbranch 'feature/greeting' set up to track 'origin/feature/greeting'.* feature/greeting 5ad154f [origin/feature/greeting] Add greeting main 5216c83 [origin/main] Merge branch 'main' of ../originFrom now on, plain git push and git pull on this branch know where to go. Git also has a setting push.autoSetupRemote that does this for you the first time.
To delete the remote branch (for example after the PR is merged):
git push origin --delete feature/greetinggit fetch --prune - [deleted] feature/greetinggit fetch --prune also removes your stale origin/* notes for branches that no longer exist on the server.
Forks
On GitHub you often cannot push to a project (you are not a member). So you make a fork: your own copy under your account. You push to your fork, then open a PR from the fork to the original ("upstream") repository. You keep two remotes:
origin: your fork (you can push)upstream: the original project (you only fetch)
I simulated this with two bare repositories on disk (upstream.git for the project, fork.git for my fork). GitHub's Fork button did not run here.
git clone -q fork.git mine && cd minegit remote add upstream ../upstream.gitgit remote -vorigin .../fork.git (fetch)origin .../fork.git (push)upstream ../upstream.git (fetch)upstream ../upstream.git (push)The maintainers then commit to the project. I bring that into my fork:
git fetch upstreamgit merge --ff-only upstream/maingit push origin mainFrom ../upstream * [new branch] main -> upstream/main M.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 M.txt 5216c83..7b62051 main -> main(I show only the last lines of the merge and push output.) That is the standard way to keep a fork current: fetch from upstream, fast-forward your main, push it to origin. For your own work, make a branch from the updated main. Never commit on your fork's main, or it will drift from the project.
The pull request flow on GitHub
Not run. This is from the GitHub docs and from how teams work.
- Branch. From an up-to-date
main, make a branch with a clear name:git switch -c fix/cart-rounding. - Commit in small steps (lesson 1).
- Push the branch:
git push -u origin fix/cart-rounding. GitHub shows a banner offering "Compare & pull request". - Open the PR. Choose the base branch (where the change goes, usually
main) and the compare branch (your branch). Write the title and description. You can open it as a draft if it is not ready; code owners are not asked to review a draft until you mark it ready. - Automatic checks run (tests, lint) if the repository has them (lesson 5).
- Review. People comment on lines, ask for changes or approve.
- Update. You push more commits to the same branch and the PR updates by itself.
- Merge with one of three methods, then delete the branch.
The GitHub docs describe the three merge methods like this:
| Method | Result | Choose when |
|---|---|---|
| Merge commit | Keeps every commit and adds a merge commit | History of the branch is meaningful |
| Squash and merge | All commits become one commit on the base branch | One logical change, lots of "fix typo" commits |
| Rebase and merge | Each commit is added without a merge commit; linear history | Commits are already clean |
The same docs warn that squash merging works best for short-lived branches: if you keep working on the same branch after a squash merge, later PRs can contain commits that were already squashed, and conflicts become likelier. That is one more reason to delete the branch after merging and start the next change from a fresh main.
A PR from a fork
The steps are the same, except the compare side is your-name:branch on your fork and the base is the project's main. Usually there is a checkbox "Allow edits by maintainers". A repository can have a PR template (.github/pull_request_template.md) that pre-fills the description. The GitHub docs list the places it can live: the root, docs/ or .github/; it is used when it is on the default branch.
Writing a good PR description
The reviewer has not been inside your head for three days. Give them the context. A simple template:
## WhatRound discount amounts to two decimals on the order page.## WhyOrders showed 89.99999999999999 (see issue #123).## Howround() on the final amount only, so rates still add up exactly.## How I tested it- New unit test for 0%, 10% and 33% discounts- Checked the order page by hand with a 3-item cart## Notes for reviewers- Please look at cart.py:discount(); the rest is test code.- Not in this PR: changing the tax rounding (separate ticket).Good habits:
- Small. A PR you can review in 15 minutes gets a real review. A 1,500-line PR gets "looks good".
- One topic. If the title needs "and", split it.
- Say what you did not do and what you want help with.
- Link the issue. Writing
Fixes #123in the description closes the issue when the PR merges into the default branch. - Show it (screenshot, before and after, or the output of a command) if it changes something visible.
- Self-review first. Read your own diff in the PR page before asking anyone. You will find leftovers.
- Keep CI green. Do not ask for review on a red build unless you say why.
Reviewing a PR
When you are the reviewer:
- Read the description first, then the tests, then the code. Tests tell you what the author thinks the code should do.
- Check the big things first: is this the right change? Does it break something? Are there missing tests, security risks, or data changes?
- Be clear which comments block. Many teams mark small ideas as
nit:(optional). - Ask questions instead of giving orders: "What happens when the list is empty?"
- Run it when the change is risky.
git fetch origin pull/123/head:pr-123(GitHub offers this special ref; I did not run it) or check out the branch. - Decide: Comment (no verdict), Approve, or Request changes.
When you are the author: answer every comment (fix it, or explain why not), push the fix, then mark the conversation resolved. Do not take comments personally; they are about the code. For tone, size and speed of reviews on a team, see Team Lead Lesson 3.
Never put secrets in a PR. Even if you remove them in a later commit, they stay in the branch history and in the PR page. If it happens, rotate the secret first.
Try it
Use two folders on your own computer. No GitHub account needed.
- Make a bare repository with
git -c init.defaultBranch=main init --bare shared.git. Clone it twice intoaliceandbob. (Git will warn that you cloned an empty repository. That is fine.) - In
alice, create a commit and push it withgit push -u origin main. If you get an error about the branch name, checkgit branchand use that name. - In
bob, rungit pull. What happens? - Now both of them commit different files. Bob pushes first. Alice pushes second. What error does Alice get and how does she fix it?
- Alice runs
git fetchinstead ofgit pull. Usegit log --oneline --all --graphto see the difference betweenmainandorigin/main. - Make a branch
feature/xin Alice's clone. Push it without-u. Read the error and fix it. - Write a PR description for a change you made, using the What/Why/How/Testing template.
Answers
git clone shared.git aliceandgit clone shared.git bob. The warning "You appear to have cloned an empty repository" is expected.- After the push,
git branch -vvshows[origin/main]. - Bob gets Alice's commit. If his own branch is empty (nothing committed), pull just creates his
mainfrom the remote. - Alice's push is rejected with
! [rejected] main -> main (fetch first). She runsgit pull(merge, rebase or--ff-only, depending on config; she may be asked to choose), tests the result, thengit push. origin/mainis ahead (or has a different commit) whilemainstays where it was, and her files did not change.fatal: The current branch feature/x has no upstream branch.and Git printsgit push --set-upstream origin feature/x. Use that, orgit push -u origin feature/x.- Example: see the template above. A good answer has all four parts and states what the change does not cover.
Interview questions
What is the difference between git fetch and git pull? (Must know)
fetch downloads new commits and updates my remote-tracking branches like origin/main. It never changes my own branches or files, so it is always safe. pull is fetch followed by a merge (or a rebase if I configure that) into the current branch, so it can change my files and stop on a conflict. I often fetch first, look at git log main..origin/main, and then decide.
Why was my push rejected? (Must know)
The remote branch has commits that I do not have. Git only allows a push that fast-forwards the remote, so nobody's work gets lost silently. I fetch, bring the remote work into my branch (merge or rebase), run the tests, and push again. I do not use --force to "fix" it, because that would delete my teammate's commits from the remote branch.
What is origin? What is origin/main? (Must know)
origin is the default name of the remote I cloned from. origin/main is a remote-tracking branch: my local record of where main was on origin at the last fetch. It is read-only, and it only moves when I fetch, pull or push. That is why git status can say "up to date" while the server has something new.
What does git push -u do? (Must know)
It pushes the branch and also sets the upstream, so my local branch is linked to the remote one. After that, git push and git pull work without arguments, and git status can tell me how many commits I am ahead or behind. I can check with git branch -vv.
What is a fork, and how is it different from a clone and a branch? (Must know)
A clone is a copy on my computer. A branch is a label inside one repository. A fork is a GitHub feature: a full copy of a repository under my account on the server. I use a fork when I cannot push to the original. I clone my fork (origin), add the original as upstream, push branches to my fork and open a PR from the fork to the original.
How do you keep a fork up to date? (Senior signal)
git fetch upstream, then on main git merge --ff-only upstream/main (or rebase), then git push origin main. I never commit directly on my fork's main; I make a branch for each change. If --ff-only fails, it means I committed on main by accident, and I need to move that work to a branch first.
What makes a good pull request? (Must know)
It is small, on one topic, with a title and description that explain what, why and how I tested it. It links the issue. The checks are green and I reviewed my own diff first. It says what is not included. The aim is that a reviewer can understand it in about 15 minutes without asking me for context.
Follow-up: "What do you do with a 2,000-line change?" I split it into steps that each work alone (for example, refactor first, then the feature) and open them in order, or I stack small PRs. If the change is truly big, I talk to the reviewer first and add a design note.
How do you review a pull request? (Senior signal)
I read the description and the tests first. Then I check the design and risk (is this the right change, what can break, security, data), then the details. I separate blocking comments from optional ones (nit:), ask questions instead of ordering, and run the branch when the change is risky. I always say something good as well; reviews are about the code, not the person. For team practice I follow my team's agreed rules.
Squash, merge commit or rebase and merge? Which would you pick? (Senior signal)
It depends on what the team wants from history. Squash gives one commit per PR, a clean main and easy reverts, at the cost of losing the small steps. A merge commit keeps all steps and a record of the branch. Rebase and merge gives a linear history with the original commits, and it only makes sense if the commits are already clean. I would ask the team, and I would choose one and use it everywhere. For short-lived branches I like squash.
A teammate says "my branch is behind origin". What does that mean and what do you do? (Trap)
It means origin/<branch> (as of their last fetch) has commits that their local branch lacks. They should run git fetch first because the information can be old, then git pull (or git pull --rebase). If they also have local commits, the branch is diverged, not just behind, and they need to merge or rebase. They must not force push to "fix" it.
Key takeaways
- A remote is a name plus a URL.
git clonesets uporigin.git remote -vlists them. origin/mainis your local note of the remote.git fetchupdates the note and is always safe.git pullis fetch plus merge or rebase.- A rejected push means the remote has work you lack. Integrate first, then push. Do not force.
git push -u origin branchlinks a branch to its upstream once.- Fork when you cannot push to the project. Keep
origin(fork) andupstream(project). Fetch fromupstreamto stay current. - A good PR is small, has one topic, explains what/why/how tested, and links the issue.
- Reviews are about the code. Say which comments block, and ask questions.
- GitHub features (PRs, forks, reviews) are described here from the docs; I ran only the Git part against local bare repositories.
What's next
Next: Undoing things safely, Lesson 4. Everyone makes mistakes. You will learn restore, amend, reset, revert, the reflog, stash, cherry-pick and bisect, with a table that says which command to use. Practise now: after your next real PR, run git fetch --prune and git branch -vv and delete the local branches that are gone on the remote.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Git, Team Workflow and Monorepos · Full-Stack Questions
Part of the study guide
Related series