Ran Wei/Git Series
中文
Git and Version Control — Ran Wei

Collaborating with pull requests

Work in a team the way most projects do: a branch for each change, a pull request to propose it, a review before it is merged, and a fork when you cannot push to the project yourself.

3 hours3 study sessions6 command sessionsGit 2.50.1 (Apple Git-155)

By the end you can

  • Follow the feature-branch workflow: branch, commit, push, propose, merge, tidy up.
  • Open a pull request on GitHub, and link it to an issue.
  • Review someone else's branch with git log main..branch and git diff main...branch, and answer a review with new commits.
  • Bring a branch up to date with main, and clean up merged branches locally and on the server.
  • Contribute to a project you cannot push to, through a fork and an upstream remote.

Before you start

Module 6: remotes, push, fetch and pull, and a GitHub account.

Contents

Study plan

3 hours

Times are estimates and include typing the commands. Progress is saved in this browser and shared by both language editions.

Session 160 min
Proposing a change
Session 260 min
Review and merge
Session 360 min
Forks and practice
1

How teams share work

In Module 6, two people pushed straight to main, and one push was rejected because the other had got there first. That works for two careful people, but not for a team. Most teams follow a few simple rules instead:

  • main always works. Nobody commits to it directly.
  • Every change, however small, is made on its own branch, called a feature branch.
  • When a change is ready, its author proposes it with a pull request: a page on GitHub that shows the branch’s commits and changes, and invites others to review them.
  • After review, the pull request is merged into main, and the branch is deleted.

This is the feature-branch workflow. Its strength is that every change is looked at before it reaches main, and that several people can work at once without stepping on each other.

The life of a pull request 1 Branchgit switch -c2 Commitgit add, git commit3 Pushgit push -u origin4 Pull requestproposed on GitHub5 Reviewcomments and fixes6 Mergeinto main on GitHub7 Tidy upgit pull, git branch -dEach change travels on its own branch, and reaches main only after review
Figure 7.1

The life of a pull request: a branch is created, committed to and pushed; a pull request proposes it; review may add commits; the branch is merged into main on GitHub, and everyone tidies up.

There are two ways to share a project on GitHub. In a shared repository, the team members have permission to push branches to the same repository, and pull requests go from one branch of it to another. Companies and small teams usually work this way. In open-source projects, most contributors cannot push to the project at all. Each makes a fork, a personal copy of the repository on GitHub, pushes to it, and opens pull requests from the fork to the original. Section 7 covers forks; until then, everyone shares one repository.

GitHub also has issues: a list of bugs, ideas and tasks for each repository. A pull request often says which issue it resolves, and GitHub can close the issue automatically when the pull request is merged.

2

A branch for each change

Alex wants to add a soup recipe. Instead of committing on main, Alex starts a branch, commits on it, and pushes the branch to the shared repository with git push -u, exactly as in Module 6’s Exercise 4.

The sessions in this module use the stand-in server from Module 6, with Alex and Sam each in their own clone. To follow along, use your GitHub repository from Module 6: your own recipes folder plays Alex. For Sam, point Sam’s existing clone at GitHub: in Sam’s window, run git remote set-url origin with the GitHub address, then git pull. On GitHub, both people are you. That works for almost everything in this module: you can open, comment on and merge your own pull requests, but GitHub does not let a pull request’s author approve it or request changes, so submit your practice review with Comment.

Try itWork on a feature branch
git switch -c add-soups
Output
Switched to a new branch 'add-soups'
Create or edit soup.md so that it contains:
# Tomato soup
git add soup.md
git commit -m "Add a tomato soup recipe"
Output
[add-soups 22abb2e] Add a tomato soup recipe
 1 file changed, 1 insertion(+)
 create mode 100644 soup.md
git push -u origin add-soups
Output
To /srv/git/recipes.git
 * [new branch]      add-soups -> add-soups
branch 'add-soups' set up to track 'origin/add-soups'.
git branch -vv
Output
* add-soups 22abb2e [origin/add-soups] Add a tomato soup recipe
  main      8bf3c2d [origin/main] Add a pancake recipe
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

The push creates the branch add-soups on the server, and -u makes origin/add-soups its upstream. git branch -vv shows both local branches with their upstreams: add-soups with the new commit, main unchanged at 8bf3c2d.

Name the branch after the change, short and specific, as Module 4 advised: add-soups, fix-oven-temperature. The name appears on the pull request, so others read it too.

3

Opening a pull request

A pull request is a GitHub feature, not a Git command, so this section has no session. Follow the steps on your own repository; GitHub Docs’ Creating a pull request shows the current pages.

  1. Open your repository on GitHub. Shortly after a push, GitHub offers a Compare & pull request button for the branch. Otherwise, go to the Pull requests tab and choose New pull request.
  2. Choose the base branch, the one the change should go into (main), and the compare branch, the one with the change (add-soups). GitHub shows the commits and the changes that the pull request would bring.
  3. Write a title that says what the change does, such as “Add a tomato soup recipe”, and a description that says why, and anything a reviewer should know.
  4. If the change resolves an issue, write Closes # followed by the issue number in the description, for example Closes #4. When the pull request is merged into the default branch, GitHub closes the issue. Linking a pull request to an issue lists the other keywords.
  5. Choose Create pull request.

The pull request’s page has three tabs: Conversation, for the description and comments; Commits, listing the branch’s commits; and Files changed, showing the diff in the same + and - form as git diff. The pull request follows the branch: when you push more commits to add-soups, they appear in it automatically.

Tip

Keep pull requests small. A pull request that does one thing is reviewed quickly and carefully; one that changes thirty files waits for days and gets a quick “looks good”. If a change is large, split it into several branches that build on each other.

4

Reviewing a pull request

Sam is asked to review. Most reviews happen on GitHub: in the Files changed tab, you can comment on any line, and then submit the review with Comment, Approve or Request changes. GitHub Docs’ Reviewing proposed changes in a pull request shows the steps.

Sometimes a reviewer wants the branch on their own computer: to try the change, or to read it in their own editor. In the session, Sam fetches, switches to add-soups, and lists what the branch adds.

Try itLook at a branch before it is merged
Now acting as Sam Lee, in Sam's own folder.
git fetch
Output
From /srv/git/recipes
 * [new branch]      add-soups  -> origin/add-soups
git switch add-soups
Output
Switched to a new branch 'add-soups'
branch 'add-soups' set up to track 'origin/add-soups'.
git log --oneline main..add-soups
Output
22abb2e Add a tomato soup recipe
git diff main...add-soups
Output
diff --git a/soup.md b/soup.md
new file mode 100644
index 0000000..4ad1865
--- /dev/null
+++ b/soup.md
@@ -0,0 +1 @@
+# Tomato soup
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

git fetch reports the new branch: * [new branch] add-soups -> origin/add-soups. Sam has no local add-soups yet, but git switch add-soups notices that origin/add-soups exists, creates a local branch from it, and sets it up to track it, all in one step.

Two commands then show what the branch adds:

  • git log --oneline main..add-soups lists the commits on add-soups that are not on main, here the one soup commit. Two dots, as in Module 6, mean “reachable from the right-hand side, but not from the left”.
  • git diff main...add-soups, with three dots, shows the changes made on add-soups since it split from main, ignoring anything that has happened on main since. That is exactly what GitHub shows under Files changed.

Suppose that Sam’s review asks for the ingredients. Alex answers by changing the file on the same branch, committing and pushing again. Nothing else is needed: the pull request picks up the new commit. Sam then pulls to get it.

Try itAnswer a review with another commit
Create or edit soup.md so that it contains:
# Tomato soup

- 1 kg tomatoes
- 1 onion
git add soup.md
git commit -m "List the soup ingredients"
Output
[add-soups b9835b7] List the soup ingredients
 1 file changed, 3 insertions(+)
git push
Output
To /srv/git/recipes.git
   22abb2e..b9835b7  add-soups -> add-soups
Now acting as Sam Lee, in Sam's own folder.
git pull
Output
From /srv/git/recipes
   22abb2e..b9835b7  add-soups  -> origin/add-soups
Updating 22abb2e..b9835b7
Fast-forward
 soup.md | 3 +++
 1 file changed, 3 insertions(+)
git log --oneline main..add-soups
Output
b9835b7 List the soup ingredients
22abb2e Add a tomato soup recipe
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

Alex’s push moves add-soups on the server from 22abb2e to b9835b7, and Sam’s git pull fast-forwards Sam’s copy of the branch. git log --oneline main..add-soups now lists both commits of the pull request.

Note — Fix or rewrite?

Answer a review by adding commits, not by amending the ones already pushed: other people may have fetched them, and new commits let reviewers see exactly what changed since their last look. If you want a tidier history before merging, Module 8 shows how to squash commits, and GitHub can squash a pull request when it merges it.

5

Merging and tidying up

Once the pull request is approved, someone with permission merges it with the Merge pull request button. By default GitHub makes a merge commit on main, exactly like git merge --no-ff: a commit with two parents, even if a fast-forward would have been possible, so that the history shows the whole branch as one unit. Its message reads “Merge pull request #1 from” followed by the branch. GitHub then offers a Delete branch button, which deletes the branch on GitHub; your local branch stays. Merging a pull request describes the button and its other options, Squash and merge and Rebase and merge.

In the session, Sam has merged the branch on the server and deleted it there, as the buttons would. Alex now brings main up to date and tidies up.

Try itTidy up after a merge
git switch main
Output
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
git pull
Output
From /srv/git/recipes
   8bf3c2d..4088575  main       -> origin/main
Updating 8bf3c2d..4088575
Fast-forward
 soup.md | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 soup.md
git branch -d add-soups
Output
Deleted branch add-soups (was 22abb2e).
git fetch --prune
Output
From /srv/git/recipes
 - [deleted]         (none)     -> origin/add-soups
git branch -a
Output
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main
*   4088575 (HEAD -> main, origin/main, origin/HEAD) Merge branch 'add-soups'
|\
| * 22abb2e Add a tomato soup recipe
|/
* 8bf3c2d Add a pancake recipe
* 907a979 Add a README
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.
  • git switch main says that Alex’s main is up to date with origin/main, but that refers to Alex’s last fetch. git pull then fetches the merge and fast-forwards main to it.
  • git branch -d add-soups deletes Alex’s local branch. It succeeds without complaint, because main now contains its commits.
  • git fetch --prune removes remote-tracking branches whose branch no longer exists on the server: - [deleted] (none) -> origin/add-soups. Without --prune, origin/add-soups would stay in Alex’s repository indefinitely.
  • git branch -a confirms the result: only main and origin/main remain.

The graph shows the merge commit joining the soup commit into main. In the session it says Merge branch 'add-soups', because Sam merged with git merge --no-ff; on GitHub the message names the pull request instead.

Tip

Run git config --global fetch.prune true once, and every git fetch and git pull will prune automatically.

6

Keeping a branch up to date

While you work on a branch, main moves on: other pull requests are merged. Before you ask for a review, and again if GitHub says that the branch is out of date or has conflicts, bring the latest main into your branch. Fetch, then merge origin/main into your branch, and push.

In the session, Alex is working on add-breads, which is already pushed, and Sam’s README change has meanwhile reached main. To follow along, push a branch with a commit, then, on GitHub or as Sam, add a commit to main.

Try itBring a branch up to date with main
git fetch
Output
From /srv/git/recipes
   8bf3c2d..76cf385  main       -> origin/main
git status
Output
On branch add-breads
Your branch is up to date with 'origin/add-breads'.

nothing to commit, working tree clean
* 76cf385 (origin/main, origin/HEAD) Say what the notes are for
| * 4354065 (HEAD -> add-breads, origin/add-breads) Add a soda bread recipe
|/
* 8bf3c2d (main) Add a pancake recipe
* 907a979 Add a README
git merge origin/main
Output
Merge made by the 'ort' strategy.
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
git push
Output
To /srv/git/recipes.git
   4354065..930b2c7  add-breads -> add-breads
*   930b2c7 (HEAD -> add-breads, origin/add-breads) Merge remote-tracking branch 'origin/main' into add-breads
|\
| * 76cf385 (origin/main, origin/HEAD) Say what the notes are for
* | 4354065 Add a soda bread recipe
|/
* 8bf3c2d (main) Add a pancake recipe
* 907a979 Add a README
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

Look at git status after the fetch: Your branch is up to date with ‘origin/add-breads’. That is true, but beside the point: a branch’s status compares it with its own upstream, not with main. The first graph shows the real situation: origin/main has a commit that add-breads lacks.

git merge origin/main merges it in, with a merge commit whose message is Merge remote-tracking branch 'origin/main' into add-breads, and git push updates the pull request. The second graph shows add-breads with both lines of history. Merging origin/main rather than your local main saves you from updating main first.

If the merge stops with a conflict, resolve it as in Module 5, commit and push. GitHub’s Update branch button, where it appears, does the same merge on GitHub. Module 8 shows rebasing, an alternative that some teams prefer.

7

Contributing through a fork

To contribute to a project you cannot push to, such as most open-source projects, you work through a fork:

  1. On the project’s GitHub page, choose Fork. GitHub creates your own copy of the repository, under your account.
  2. Clone your fork. In your clone, origin is your fork, which you can push to.
  3. Add the original project as a second remote, by convention called upstream, so that you can fetch its new commits.
  4. Work on a branch as usual, push it to origin, and open a pull request from your fork’s branch to the original project’s main. GitHub offers this when you open a pull request on either repository.
  5. To keep your fork’s main up to date, fetch upstream, merge upstream/main into your main, and push it to origin. GitHub’s Sync fork button does the same on GitHub.
Contributing through a fork GitHubThe projectupstream: you cannot push hereYour forkorigin: your copy on GitHubYour cloneon your computerForkpull requestgit clonegit pushgit fetch upstream
Figure 7.2

Working through a fork. You cannot push to the original project, upstream. You push to your fork, origin, and open pull requests from it; you fetch new work from upstream.

In the session, the server holds the original repository and Sam’s fork of it, sam-recipes.git, which Sam can push to. Sam clones the fork and adds upstream. Then Alex, who maintains the original, adds a cake recipe, and Sam brings it into the fork.

Try itContribute through a fork
Now acting as Sam Lee, in Sam's own folder.
git clone /srv/git/sam-recipes.git
Output
Cloning into 'sam-recipes'...
done.
cd sam-recipes
git remote add upstream /srv/git/recipes.git
git remote -v
Output
origin	/srv/git/sam-recipes.git (fetch)
origin	/srv/git/sam-recipes.git (push)
upstream	/srv/git/recipes.git (fetch)
upstream	/srv/git/recipes.git (push)
Back to Alex Smith, in Alex's own folder.
Create or edit cake.md so that it contains:
# Lemon cake
git add cake.md
git commit -m "Add a lemon cake recipe"
Output
[main 6e8609b] Add a lemon cake recipe
 1 file changed, 1 insertion(+)
 create mode 100644 cake.md
git push
Output
To /srv/git/recipes.git
   8bf3c2d..6e8609b  main -> main
Now acting as Sam Lee, in Sam's own folder.
git fetch upstream
Output
From /srv/git/recipes
 * [new branch]      main       -> upstream/main
git merge upstream/main
Output
Updating 8bf3c2d..6e8609b
Fast-forward
 cake.md | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 cake.md
git push
Output
To /srv/git/sam-recipes.git
   8bf3c2d..6e8609b  main -> main
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

git remote -v now lists two remotes: origin, Sam’s fork, and upstream, the original. git fetch upstream brings in the original’s main as upstream/main, git merge upstream/main fast-forwards Sam’s main to it, and git push updates the fork, because Sam’s main still pushes to origin.

To practise on GitHub, fork the practice repository octocat/Spoon-Knife, as GitHub Docs’ Fork a repository suggests, then clone your fork and add https://github.com/octocat/Spoon-Knife.git as upstream.

8

Exercises

Exercise 1★★★conceptual4 min

Put the steps in order. Arrange these steps of the feature-branch workflow: merge the pull request; git push -u origin fix-typo; git switch -c fix-typo; open a pull request; git switch main and git pull; commit the fix; answer the review with another commit; git branch -d fix-typo.

Show solution

git switch -c fix-typo; commit the fix; git push -u origin fix-typo; open a pull request; answer the review with another commit (and push it); merge the pull request; git switch main and git pull; git branch -d fix-typo.

Exercise 2★★★conceptual5 min

Two dots or three? main has gained two commits since add-soups split from it, and add-soups has one commit of its own. What does git log --oneline main..add-soups list? What does git diff main...add-soups show, and why would git diff main add-soups, without dots, mislead a reviewer?

Show solution

main..add-soups lists the one commit on add-soups that main does not have. main...add-soups shows only the changes made on add-soups since the split, which is what the pull request would add. git diff main add-soups compares the two newest commits directly, so main’s two new commits would appear as if add-soups removed them, although the branch never touched them.

Exercise 3★★★coding6 min

Add to a colleague’s branch. Alex’s pull request for add-soups is missing a note on how many the soup serves. As Sam, add that to the branch, so that the pull request includes it, and let Alex get the change. To follow along, push a branch from Alex’s window first.

Show solution

Fetch the branch, switch to it, commit and push; Alex then pulls:

SolutionAdd a commit to a colleague's branch
Now acting as Sam Lee, in Sam's own folder.
git fetch
Output
From /srv/git/recipes
 * [new branch]      add-soups  -> origin/add-soups
git switch add-soups
Output
Switched to a new branch 'add-soups'
branch 'add-soups' set up to track 'origin/add-soups'.
Create or edit soup.md so that it contains:
# Tomato soup

Serves four.
git commit -am "Say how many the soup serves"
Output
[add-soups 2f70ec5] Say how many the soup serves
 1 file changed, 2 insertions(+)
git push
Output
To /srv/git/recipes.git
   22abb2e..2f70ec5  add-soups -> add-soups
Back to Alex Smith, in Alex's own folder.
git pull
Output
From /srv/git/recipes
   22abb2e..2f70ec5  add-soups  -> origin/add-soups
Updating 22abb2e..2f70ec5
Fast-forward
 soup.md | 2 ++
 1 file changed, 2 insertions(+)
git log --oneline -n 2
Output
2f70ec5 Say how many the soup serves
22abb2e Add a tomato soup recipe
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

Anyone with push access can add commits to a pull request’s branch. Agree on it first, so that two people do not edit the same lines at once.

Exercise 4★★★conceptual4 min

Write the pull request. Your branch adds a soup recipe, which issue #4 asked for. Write a title and a description, so that merging the pull request closes the issue.

Show solution

For example, title: Add a tomato soup recipe. Description: Adds soup.md with the ingredients and method we used last week. Closes #4. Any wording works, as long as the description contains Closes #4 (or Fixes #4, or Resolves #4).

Exercise 5★★★coding6 min

A branch that will not delete. Your pull request was merged with Squash and merge: GitHub added one new commit to main containing all the branch’s changes, and deleted the branch. After git pull and git fetch --prune, git branch -d add-soups refuses, saying that the branch is not fully merged. Check that nothing would be lost, and delete the branch. To follow along, merge a pull request on GitHub with Squash and merge.

Show solution

Squashing copies the branch’s changes into a new commit, so main never contains the branch’s own commits, and -d cannot tell that the work is in main. Compare the contents instead:

SolutionTidy up after a squash merge
git switch main
Output
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
git pull
Output
From /srv/git/recipes
   8bf3c2d..b200d8c  main       -> origin/main
Updating 8bf3c2d..b200d8c
Fast-forward
 soup.md | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 soup.md
git fetch --prune
Output
From /srv/git/recipes
 - [deleted]         (none)     -> origin/add-soups
git branch -d add-soups
Output
error: the branch 'add-soups' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D add-soups'
hint: Disable this message with "git config set advice.forceDeleteBranch false"
git diff main add-soups
git branch -D add-soups
Output
Deleted branch add-soups (was 22abb2e).
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

git diff main add-soups prints nothing: the files are identical, so git branch -D loses nothing. Prune first: while origin/add-soups still exists in your repository, -d checks against it and deletes the branch with only a warning.

Exercise 6★★★conceptual5 min

Fork or branch? For each situation, would you push a branch to the repository, or work through a fork?

  1. You are a member of the team that owns the repository.
  2. You found a typo in the documentation of a popular open-source library.
  3. You want to experiment with a project’s code without anyone seeing it, and you may never contribute it.
Show solution
  1. A branch: you can push to the repository, and pull requests between its branches keep things simple.
  2. A fork: you cannot push to the library’s repository, so you push to your fork and open a pull request from it.
  3. A clone: it stays on your computer, needs no account, and nobody sees it. A fork would not do: a fork of a public repository is public.
9

Self-check quiz

1
What is a pull request?
2
A reviewer asks for a change. How do you update your pull request?
3
What does git diff main...add-soups show?
4
A branch was merged and deleted on GitHub. Which command removes origin/add-soups from your repository?
5
git status on your feature branch says Your branch is up to date with ‘origin/add-breads’. Does that mean your branch has everything on main?
6
In a fork setup, what is upstream usually?
7
Your pull request’s description says Closes #12. What happens when it is merged into the default branch?
10

Further reading