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

Capstone: a team project

Put the whole series to work: two people, two clones and one shared repository build a family cookbook from the first commit to a tagged release, with branches, pull requests, a conflict and a review on the way.

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

By the end you can

  • Set up a shared repository for a team, with each member working in their own clone.
  • Develop two features in parallel on branches, each proposed in a pull request.
  • Resolve a conflict in a pull request by bringing main into the branch.
  • Review, merge and tidy up, so that everyone's main stays in step.
  • Tag a release with an annotated tag, push it, and publish it on GitHub.

Before you start

Modules 1 to 8, and a GitHub account (Module 6). A friend with a GitHub account is welcome, but not required.

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
Starting together
Session 260 min
Integrating and releasing
Session 360 min
Going further
1

The project

This module puts the whole series to work on one small project, from the first commit to a published release. Alex and Sam build a family cookbook: a README, an index of recipes, and one file per recipe. The commands are the ones you already know. What is new is doing all of it together, in order, the way a team works.

The project has five milestones:

  1. A shared repository. Alex starts the project and shares it; Sam clones it.
  2. Two features at once. Alex adds a soup recipe and Sam a bread recipe, each on a branch, each proposed in a pull request.
  3. A conflict. Alex’s pull request is merged first, and Sam’s now conflicts with it. Sam brings main into the branch and resolves the conflict.
  4. Review and merge. Alex reviews Sam’s pull request and merges it, and everyone tidies up.
  5. A release. Alex tags the result as version 1.0 and publishes it.

The team follows the rules from Module 7: nobody commits to main directly, every change goes through a branch and a pull request, and someone other than the author reviews and merges it.

Do the project on GitHub. There are two ways:

  • With a friend. Each of you has a GitHub account and a computer. One plays Alex and owns the repository; the other plays Sam.
  • On your own. You play both people, with two folders and two terminal windows, as in Module 6. On GitHub, both people are you. GitHub does not let anyone approve their own pull request, so where the project says approve, leave a review comment instead, then merge.

The sessions show every step with the stand-in server from Module 6. Where a step happens on GitHub instead, such as creating the repository, opening a pull request or merging one, the text says how.

2

Milestone 1: a shared repository

Alex creates the project: a new folder, git init, three files and a first commit. Start in your home folder (cd ~), not inside recipes. Then Alex puts it on the server and pushes main, and Sam clones it.

On GitHub:

  • Alex creates an empty repository called cookbook, as in Module 6, and pushes to its address instead of /srv/git/cookbook.git.
  • With a friend, Alex then invites Sam to the repository, following Inviting collaborators to a personal repository. Once Sam accepts, Sam can push branches to it.
  • On your own, you need no invitation. Clone the repository into Sam’s folder, and set Sam’s name and email in that clone as in Module 6.
Try itStart the project and share it
mkdir cookbook
cd cookbook
git init
Output
Initialized empty Git repository in /home/alex/cookbook/.git/
Create or edit README.md so that it contains:
# Family cookbook

Our family's recipes, collected by Alex and Sam.
Create or edit index.md so that it contains:
# Recipes

- [Pancakes](pancakes.md)
Create or edit pancakes.md so that it contains:
# Pancakes

- 200 g flour
- 2 eggs
- 300 ml milk
git add README.md index.md pancakes.md
git commit -m "Start the family cookbook"
Output
[main (root-commit) db5abad] Start the family cookbook
 3 files changed, 11 insertions(+)
 create mode 100644 README.md
 create mode 100644 index.md
 create mode 100644 pancakes.md
git init --bare /srv/git/cookbook.git
Output
Initialized empty Git repository in /srv/git/cookbook.git/
git remote add origin /srv/git/cookbook.git
git push -u origin main
Output
To /srv/git/cookbook.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.
Now acting as Sam Lee, in Sam's own folder.
git clone /srv/git/cookbook.git
Output
Cloning into 'cookbook'...
done.
cd cookbook
git log --oneline
Output
db5abad Start the family cookbook
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.

Everything here comes from earlier modules. git add names the three files at once. The first commit is a root commit, and git push -u creates main on the server and makes it the upstream. Sam’s clone then has the same single commit, db5abad.

Tip

Teams often ask GitHub to enforce the rules. A ruleset or branch protection on main can require a pull request, and an approving review, before anything is merged. About protected branches explains the options. It is optional for this project, and only for a team: on your own, a required approval would block every merge, because nobody can approve their own pull request. On a free account, GitHub enforces these rules only in public repositories.

3

Milestone 2: two features at once

Alex and Sam now work at the same time, each on their own branch. Each adds a recipe file and a line for it in index.md, under the pancakes. Then each pushes the branch and opens a pull request on GitHub, as in Module 7: Alex’s becomes pull request #1, Sam’s #2. Write a title and a short description for each.

Try itTwo features at once
git switch -c add-soup
Output
Switched to a new branch 'add-soup'
Create or edit soup.md so that it contains:
# Tomato soup

- 1 kg tomatoes
Create or edit index.md so that it contains:
# Recipes

- [Pancakes](pancakes.md)
- [Tomato soup](soup.md)
git add soup.md index.md
git commit -m "Add a tomato soup recipe"
Output
[add-soup 7c82c56] Add a tomato soup recipe
 2 files changed, 4 insertions(+)
 create mode 100644 soup.md
git push -u origin add-soup
Output
To /srv/git/cookbook.git
 * [new branch]      add-soup -> add-soup
branch 'add-soup' set up to track 'origin/add-soup'.
Now acting as Sam Lee, in Sam's own folder.
git switch -c add-bread
Output
Switched to a new branch 'add-bread'
Create or edit bread.md so that it contains:
# Soda bread

- 450 g flour
Create or edit index.md so that it contains:
# Recipes

- [Pancakes](pancakes.md)
- [Soda bread](bread.md)
git add bread.md index.md
git commit -m "Add a soda bread recipe"
Output
[add-bread 0070943] Add a soda bread recipe
 2 files changed, 4 insertions(+)
 create mode 100644 bread.md
git push -u origin add-bread
Output
To /srv/git/cookbook.git
 * [new branch]      add-bread -> add-bread
branch 'add-bread' set up to track 'origin/add-bread'.
* 0070943 (HEAD -> add-bread, origin/add-bread) Add a soda bread recipe
* db5abad (origin/main, origin/HEAD, main) Start the family cookbook
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.

Each person’s commit touches two files: the new recipe and index.md. The graph shows Sam’s repository. Sam’s branch, add-bread, is one commit ahead of main. Sam’s repository has not fetched Alex’s branch, so it does not appear.

Both changes add a line to index.md in the same place, straight after the pancakes. Neither person knows yet, but that makes a conflict certain, as Module 5 explained.

4

Milestone 3: a conflict

Sam reviews pull request #1, approves it and merges it with Merge pull request, then deletes the branch with Delete branch. Now main has the soup, and Sam’s pull request #2 no longer merges cleanly. GitHub shows it on the pull request’s page: This branch has conflicts that must be resolved.

The fix belongs on the branch, so it is Sam’s job, as the branch’s author: bring the new main into add-bread, resolve the conflict, and push. That is Module 7’s “keeping a branch up to date”, with Module 5’s conflict resolution in the middle.

To follow along on your own, merge pull request #1 on GitHub first. Then work in Sam’s folder, on add-bread.

Try itBring a branch up to date, and resolve the conflict
Now acting as Sam Lee, in Sam's own folder.
git fetch
Output
From /srv/git/cookbook
   db5abad..c9c95dc  main       -> origin/main
git merge origin/main
Output
Auto-merging index.md
CONFLICT (content): Merge conflict in index.md
Automatic merge failed; fix conflicts and then commit the result.
cat index.md
Output
# Recipes

- [Pancakes](pancakes.md)
<<<<<<< HEAD
- [Soda bread](bread.md)
=======
- [Tomato soup](soup.md)
>>>>>>> origin/main
Create or edit index.md so that it contains:
# Recipes

- [Pancakes](pancakes.md)
- [Soda bread](bread.md)
- [Tomato soup](soup.md)
git add index.md
git commit
Output
[add-bread 702089a] Merge remote-tracking branch 'origin/main' into add-bread
git push
Output
To /srv/git/cookbook.git
   0070943..702089a  add-bread -> add-bread
*   702089a (HEAD -> add-bread, origin/add-bread) Merge remote-tracking branch 'origin/main' into add-bread
|\
| *   c9c95dc (origin/main, origin/HEAD) Merge pull request #1 from alex/add-soup
| |\
| | * 7c82c56 Add a tomato soup recipe
| |/
* / 0070943 Add a soda bread recipe
|/
* db5abad (main) Start the family cookbook
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 brings in the merged main. git merge origin/main stops with a conflict in index.md. The markers show Sam’s line, HEAD, which is the branch Sam is on, against the soup line from origin/main. Both lines belong in index.md. Sam keeps both, in alphabetical order, and removes the markers. Then git add, git commit (accepting the ready-made message Merge remote-tracking branch 'origin/main' into add-bread) and git push.

The graph shows the result: add-bread now contains both the bread commit and everything on main, including Alex’s merged pull request. On GitHub, the pull request updates by itself, and the conflict warning disappears.

5

Milestone 4: review and merge

Alex reviews pull request #2 in the Files changed tab, which now shows only Sam’s changes: the new bread file and the resolved index.md. Alex approves it, merges it, and deletes the branch with Delete branch. Then both people bring their main up to date and delete the merged branches, as in Module 7.

Try itMerge the second pull request and tidy up
Now acting as Sam Lee, in Sam's own folder.
git switch main
Output
Switched to branch 'main'
Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)
git pull
Output
From /srv/git/cookbook
   c9c95dc..0dba4f9  main       -> origin/main
Updating db5abad..0dba4f9
Fast-forward
 bread.md | 3 +++
 index.md | 2 ++
 soup.md  | 3 +++
 3 files changed, 8 insertions(+)
 create mode 100644 bread.md
 create mode 100644 soup.md
git branch -d add-bread
Output
Deleted branch add-bread (was 702089a).
git fetch --prune
Output
From /srv/git/cookbook
 - [deleted]         (none)     -> origin/add-bread
cat index.md
Output
# Recipes

- [Pancakes](pancakes.md)
- [Soda bread](bread.md)
- [Tomato soup](soup.md)
*   0dba4f9 (HEAD -> main, origin/main, origin/HEAD) Merge pull request #2 from alex/add-bread
|\
| *   702089a Merge remote-tracking branch 'origin/main' into add-bread
| |\
| |/
|/|
* |   c9c95dc Merge pull request #1 from alex/add-soup
|\ \
| * | 7c82c56 Add a tomato soup recipe
|/ /
| * 0070943 Add a soda bread recipe
|/
* db5abad Start the family cookbook
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.

Sam switches to main, pulls the merge of pull request #2, deletes the local branch and prunes origin/add-bread. cat index.md shows all three recipes.

The graph is the most tangled in this series, so read it from the bottom:

  • the first commit, db5abad;
  • the two feature commits, 0070943 (bread) and 7c82c56 (soup);
  • the merge of pull request #1;
  • Sam’s merge of main into add-bread;
  • at the top, the merge of pull request #2.

Every line of work is there, and every merge says where it came from. Such a history is normal for a team that merges; Module 8 showed how rebasing can keep it straighter.

6

Milestone 5: a release

The cookbook is ready for its first version. A tag gives a commit a permanent name, such as v1.0, that does not move as new work arrives, unlike a branch.

Git has two kinds of tag:

  • a lightweight tag, made with git tag <name>, is just a name for a commit, like a branch that never moves;
  • an annotated tag, made with git tag -a <name> -m "<message>", is an object of its own (Module 9). It records who made it, when, and why.

Use annotated tags for releases.

Tags are not pushed by git push; push each one by name, as in git push origin v1.0. Many projects number their releases with Semantic Versioning: v1.0.1 for a fix, v1.1.0 for a new feature that changes nothing existing, and v2.0.0 for a change that breaks something people rely on.

Try itTag the first release
git log --oneline --graph
Output
*   0dba4f9 Merge pull request #2 from alex/add-bread
|\
| *   702089a Merge remote-tracking branch 'origin/main' into add-bread
| |\
| |/
|/|
* |   c9c95dc Merge pull request #1 from alex/add-soup
|\ \
| * | 7c82c56 Add a tomato soup recipe
|/ /
| * 0070943 Add a soda bread recipe
|/
* db5abad Start the family cookbook
git tag -a v1.0 -m "Family cookbook 1.0"
git push origin v1.0
Output
To /srv/git/cookbook.git
 * [new tag]         v1.0 -> v1.0
git tag
Output
v1.0
git show v1.0 --no-patch
Output
tag v1.0
Tagger: Alex Smith <alex@example.com>
Date:   Mon Jan 5 09:49:00 2026 +0000

Family cookbook 1.0

commit 0dba4f9bab9f4e6ee71716da3e6bd40bf54fb4fc
Merge: c9c95dc 702089a
Author: Alex Smith <alex@example.com>
Date:   Mon Jan 5 09:39:00 2026 +0000

    Merge pull request #2 from alex/add-bread
*   0dba4f9 (HEAD -> main, tag: v1.0, origin/main, origin/HEAD) Merge pull request #2 from alex/add-bread
|\
| *   702089a Merge remote-tracking branch 'origin/main' into add-bread
| |\
| |/
|/|
* |   c9c95dc Merge pull request #1 from alex/add-soup
|\ \
| * | 7c82c56 Add a tomato soup recipe
|/ /
| * 0070943 Add a soda bread recipe
|/
* db5abad Start the family cookbook
Now acting as Sam Lee, in Sam's own folder.
git fetch
Output
From /srv/git/cookbook
   c9c95dc..0dba4f9  main       -> origin/main
 * [new tag]         v1.0       -> v1.0
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 log --oneline --graph shows the history being released. git tag -a v1.0 -m "Family cookbook 1.0" tags the newest commit, the merge of pull request #2.
  • git push origin v1.0 sends the tag: * [new tag] v1.0 -> v1.0. git tag lists the tags.
  • git show v1.0 --no-patch shows the tag object (tagger, date and message), followed by the commit it names.
  • The graph shows the tag as a yellow label on that commit.
  • When Sam fetches, Git brings the new tag along: * [new tag] v1.0 -> v1.0.

To publish the release on GitHub, open the repository’s Releases page, choose Draft a new release, select the tag v1.0, and write a title and a short list of what is in it, then choose Publish release. Managing releases in a repository shows the steps. A release is a page for people who use the project, built on the tag that Git records.

Key idea

That is the whole cycle: branch, commit, push, pull request, review, merge, update, tag. Everything in this series serves one of those steps, and every Git project you join, however large, works this way.

7

Exercises

These extension tasks continue the project. Do them in your cookbook, on GitHub.

Exercise 1★★★coding10 min

A fix release. The pancake recipe has no salt. As Alex, fix it through a pull request, and release the fix as v1.0.1.

Show solution

Branch, commit, push and open a pull request. Once it is merged, update main, then tag and push the tag:

SolutionA fix release
git switch -c fix-pancakes
Output
Switched to a new branch 'fix-pancakes'
Create or edit pancakes.md so that it contains:
# Pancakes

- 200 g flour
- 2 eggs
- 300 ml milk
- a pinch of salt
git commit -am "Add the salt the pancakes were missing"
Output
[fix-pancakes ff0ea75] Add the salt the pancakes were missing
 1 file changed, 1 insertion(+)
git push -u origin fix-pancakes
Output
To /srv/git/cookbook.git
 * [new branch]      fix-pancakes -> fix-pancakes
branch 'fix-pancakes' set up to track 'origin/fix-pancakes'.
git switch main
Output
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
git pull
Output
From /srv/git/cookbook
   0dba4f9..458c69d  main       -> origin/main
Updating 0dba4f9..458c69d
Fast-forward
 pancakes.md | 1 +
 1 file changed, 1 insertion(+)
git tag -a v1.0.1 -m "Family cookbook 1.0.1"
git push origin v1.0.1
Output
To /srv/git/cookbook.git
 * [new tag]         v1.0.1 -> v1.0.1
git tag
Output
v1.0
v1.0.1
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.

In the session, Sam merges pull request #3, as the merge on GitHub would. A fix that changes nothing else gets the next patch number, v1.0.1.

Exercise 2★★★conceptual6 min

Avoid the conflict. The conflict in Milestone 3 was predictable: every new recipe adds a line in the same place in index.md. Suggest two ways the team could avoid such conflicts in future.

Show solution

Any two of these:

  • Agree that one person updates index.md after each merge, so recipe branches never touch it.
  • Keep index.md in alphabetical order, so that different recipes usually go on different lines (though neighbouring lines can still conflict).
  • Merge main into a branch just before opening its pull request, so that whoever is second sees the other change while it is small.
  • Generate index.md from the recipe files with a script, instead of editing it by hand.
Exercise 3★★★coding8 min

Who added this? A new member asks who added the soda bread to index.md, and in which commit. Find out with git log.

Show solution

git log -S lists the commits that added or removed a piece of text:

SolutionWho added this line?
git log --oneline -S "Soda bread"
Output
0070943 Add a soda bread recipe
git log -n 1 --format="%an, %s" 0070943
Output
Sam Lee, Add a soda bread 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.

-S "Soda bread" finds 0070943, the commit that added the text. --format then prints its author and message. git blame index.md gives a similar answer line by line: it shows, for every line, the last commit that changed it.

Exercise 4★★★coding8 min

A tag pushed by mistake. Alex tagged and pushed v2.0 by mistake. Remove the tag, locally and on the server. To practise, first tag and push v2.0, as the solution’s first two commands do.

Show solution

Delete the tag locally with git tag -d, and on the server with git push origin --delete:

SolutionDelete a tag pushed by mistake
git tag -a v2.0 -m "Family cookbook 2.0"
git push origin v2.0
Output
To /srv/git/cookbook.git
 * [new tag]         v2.0 -> v2.0
git tag -d v2.0
Output
Deleted tag 'v2.0' (was 2f63022)
git push origin --delete v2.0
Output
To /srv/git/cookbook.git
 - [deleted]         v2.0
git tag
Output
v1.0
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 who has already fetched v2.0 keeps their copy, because fetching never deletes tags. Tags are meant to be permanent, so when a released version turns out to be wrong, release a new one, such as v1.0.2, rather than moving or deleting a tag that others may have.

Exercise 5★★★conceptual6 min

Which kind of tag? Why does the project use annotated tags for releases rather than lightweight ones? When might a lightweight tag be enough?

Show solution

An annotated tag records who made the release, when, and a message, which git show and other tools display. It can also be signed. A lightweight tag is only a name for a commit: enough for a private bookmark, such as before-big-change, that you do not intend to share.

Exercise 6★★★conceptual7 min

Protect main. In Milestone 3, nothing stopped Sam from merging pull request #1 without reading it, or from pushing straight to main. What could the team set up on GitHub, and what would each person then have to do differently?

Show solution

A ruleset or branch protection on main that requires a pull request with at least one approving review. Pushes straight to main are then rejected, and the Merge pull request button stays disabled until someone other than the author approves. With classic branch protection, also turn on Do not allow bypassing the above settings, or the rules do not apply to the repository’s owner; a ruleset applies to everyone not on its bypass list. Everyone keeps working exactly as in this project: branch, push, pull request, review.

8

Self-check quiz

1
GitHub says that your pull request has conflicts that must be resolved. What do you do?
2
Who usually resolves a conflict in a pull request?
3
What is the difference between a tag and a branch?
4
You have tagged v1.0 and run git push. Why is the tag not on GitHub?
5
With Semantic Versioning, a release that only fixes a bug in v1.4.2 is numbered:
6
What does an annotated tag record that a lightweight tag does not?
7
Which order matches the cycle this project followed?
9

Further reading