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:
- A shared repository. Alex starts the project and shares it; Sam clones it.
- Two features at once. Alex adds a soup recipe and Sam a bread recipe, each on a branch, each proposed in a pull request.
- A conflict. Alex’s pull request is merged first, and Sam’s now conflicts with it. Sam brings
maininto the branch and resolves the conflict. - Review and merge. Alex reviews Sam’s pull request and merges it, and everyone tidies up.
- 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.
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.
mkdir cookbook
cd cookbook
git init
Initialized empty Git repository in /home/alex/cookbook/.git/
README.md so that it contains:# Family cookbook
Our family's recipes, collected by Alex and Sam.
index.md so that it contains:# Recipes
- [Pancakes](pancakes.md)
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"
[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
Initialized empty Git repository in /srv/git/cookbook.git/
git remote add origin /srv/git/cookbook.git
git push -u origin main
To /srv/git/cookbook.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
git clone /srv/git/cookbook.git
Cloning into 'cookbook'...
done.
cd cookbook
git log --oneline
db5abad Start the family cookbook
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.
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.
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.
git switch -c add-soup
Switched to a new branch 'add-soup'
soup.md so that it contains:# Tomato soup
- 1 kg tomatoes
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"
[add-soup 7c82c56] Add a tomato soup recipe
2 files changed, 4 insertions(+)
create mode 100644 soup.md
git push -u origin add-soup
To /srv/git/cookbook.git
* [new branch] add-soup -> add-soup
branch 'add-soup' set up to track 'origin/add-soup'.
git switch -c add-bread
Switched to a new branch 'add-bread'
bread.md so that it contains:# Soda bread
- 450 g flour
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"
[add-bread 0070943] Add a soda bread recipe
2 files changed, 4 insertions(+)
create mode 100644 bread.md
git push -u origin add-bread
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
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.
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.
git fetch
From /srv/git/cookbook
db5abad..c9c95dc main -> origin/main
git merge origin/main
Auto-merging index.md
CONFLICT (content): Merge conflict in index.md
Automatic merge failed; fix conflicts and then commit the result.
cat index.md
# Recipes
- [Pancakes](pancakes.md)
<<<<<<< HEAD
- [Soda bread](bread.md)
=======
- [Tomato soup](soup.md)
>>>>>>> origin/main
index.md so that it contains:# Recipes
- [Pancakes](pancakes.md)
- [Soda bread](bread.md)
- [Tomato soup](soup.md)
git add index.md
git commit
[add-bread 702089a] Merge remote-tracking branch 'origin/main' into add-bread
git push
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
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.
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.
git switch main
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
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
Deleted branch add-bread (was 702089a).
git fetch --prune
From /srv/git/cookbook
- [deleted] (none) -> origin/add-bread
cat index.md
# 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
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) and7c82c56(soup); - the merge of pull request #1;
- Sam’s merge of
mainintoadd-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.
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.
git log --oneline --graph
* 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
To /srv/git/cookbook.git
* [new tag] v1.0 -> v1.0
git tag
v1.0
git show v1.0 --no-patch
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
git fetch
From /srv/git/cookbook
c9c95dc..0dba4f9 main -> origin/main
* [new tag] v1.0 -> v1.0
git log --oneline --graphshows 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.0sends the tag:* [new tag] v1.0 -> v1.0.git taglists the tags.git show v1.0 --no-patchshows 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.
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.
Exercises
These extension tasks continue the project. Do them in your cookbook, on GitHub.
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:
git switch -c fix-pancakes
Switched to a new branch 'fix-pancakes'
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"
[fix-pancakes ff0ea75] Add the salt the pancakes were missing
1 file changed, 1 insertion(+)
git push -u origin fix-pancakes
To /srv/git/cookbook.git
* [new branch] fix-pancakes -> fix-pancakes
branch 'fix-pancakes' set up to track 'origin/fix-pancakes'.
git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
git pull
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
To /srv/git/cookbook.git
* [new tag] v1.0.1 -> v1.0.1
git tag
v1.0
v1.0.1
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.
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.mdafter each merge, so recipe branches never touch it. - Keep
index.mdin alphabetical order, so that different recipes usually go on different lines (though neighbouring lines can still conflict). - Merge
maininto a branch just before opening its pull request, so that whoever is second sees the other change while it is small. - Generate
index.mdfrom the recipe files with a script, instead of editing it by hand.
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:
git log --oneline -S "Soda bread"
0070943 Add a soda bread recipe
git log -n 1 --format="%an, %s" 0070943
Sam Lee, Add a soda bread recipe
-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.
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:
git tag -a v2.0 -m "Family cookbook 2.0"
git push origin v2.0
To /srv/git/cookbook.git
* [new tag] v2.0 -> v2.0
git tag -d v2.0
Deleted tag 'v2.0' (was 2f63022)
git push origin --delete v2.0
To /srv/git/cookbook.git
- [deleted] v2.0
git tag
v1.0
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.
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.
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.
Self-check quiz
main and the conflict is resolved.v1.0 and run git push. Why is the tag not on GitHub?--tags.v1.4.2 is numbered:Further reading
- Scott Chacon and Ben Straub, Pro Git, section 2.6: Tagging, and the reference page for git tag.
- GitHub Docs, About rulesets and About merge conflicts.
- Where to go next, when you need them:
- submodules, for a repository inside another;
- Git LFS, for large files such as videos;
- hooks, for scripts that run on commit or push;
- git bisect, for finding the commit that introduced a bug;
- git worktree, for several branches checked out at once;
- signing your work, with GitHub’s commit signature verification.