Git & GitHub Module 4 – Branching and merging
Merge branches: fast-forward and three-way
Predict when git merge fast-forwards and when it makes a merge commit, find the merge base, choose --ff-only or --no-ff, and read history with --first-parent.
What you will learn
- Predict whether git merge will fast-forward or create a merge commit
- Find the merge base of two branches and explain how a three-way merge uses it
- Choose --ff-only, --no-ff or the default for a team history
- Read a merge commit's parents and follow main-line history with --first-parent
Before you start
What you’ll learn · Before you start
What you will learn
- Predict whether git merge will fast-forward or create a merge commit
- Find the merge base of two branches and explain how a three-way merge uses it
- Choose --ff-only, --no-ff or the default for a team history
- Read a merge commit's parents and follow main-line history with --first-parent
Before you start
On this page
git merge <branch> brings the commits of another branch into the branch you are on. First Git looks at how the two
histories relate. If your branch has no commits of its own since the other branch started, your branch’s commit is
an ancestor of the other branch’s, and Git does a fast-forward: it moves your branch to the other branch’s
commit and creates nothing. If both branches have new commits, Git does a three-way merge: it finds the merge
base, the newest commit both branches share, compares it with each side to see what each side changed, combines the
changes and records the result in a merge commit with two parents. This lesson runs both kinds on a canteen-menu
repository, then shows the rule that combines changes line by line, the options that force or forbid a
fast-forward, and how to read a history that contains merges.
Fast-forward: when your branch has not moved
Meera keeps the canteen’s menu in Git. She adds two dishes on a branch, add-poha, while nobody touches main:
from gitsession import session
session(r"""
git config set --global user.name "Meera Iyer"
git config set --global user.email "meera@example.com"
git init -q -b main canteen
cd canteen
printf 'Idli: 30\nDosa: 50\n' > menu.txt
git add menu.txt
git commit -q -m "Start the canteen menu"
git switch -q -c add-poha
printf 'Idli: 30\nDosa: 50\nPoha: 25\n' > menu.txt
git commit -q -am "Add poha"
printf 'Idli: 30\nDosa: 50\nPoha: 25\nUpma: 25\n' > menu.txt
git commit -q -am "Add upma"
git switch -q main
# main has not moved since add-poha began, so main is an ancestor of add-poha
git merge-base --is-ancestor main add-poha && echo "main is an ancestor of add-poha"
git log --oneline --graph --all
git merge add-poha
git log --oneline --decorate
""") Output
$ git config set --global user.name "Meera Iyer" $ git config set --global user.email "meera@example.com" $ git init -q -b main canteen $ cd canteen $ printf 'Idli: 30\nDosa: 50\n' > menu.txt $ git add menu.txt $ git commit -q -m "Start the canteen menu" $ git switch -q -c add-poha $ printf 'Idli: 30\nDosa: 50\nPoha: 25\n' > menu.txt $ git commit -q -am "Add poha" $ printf 'Idli: 30\nDosa: 50\nPoha: 25\nUpma: 25\n' > menu.txt $ git commit -q -am "Add upma" $ git switch -q main # main has not moved since add-poha began, so main is an ancestor of add-poha $ git merge-base --is-ancestor main add-poha && echo "main is an ancestor of add-poha" main is an ancestor of add-poha $ git log --oneline --graph --all * 39e3cec Add upma * 417b2a9 Add poha * 0994c2f Start the canteen menu $ git merge add-poha Updating 0994c2f..39e3cec Fast-forward menu.txt | 2 ++ 1 file changed, 2 insertions(+) $ git log --oneline --decorate 39e3cec (HEAD -> main, add-poha) Add upma 417b2a9 Add poha 0994c2f Start the canteen menu
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fast_forward.py
git merge-base --is-ancestor main add-poha exits with status 0 when its first commit is an ancestor of the second,
so the echo after && ran: every commit main has is also in add-poha’s history, and the graph is one straight
line. There is nothing to combine, so git merge add-poha printed Updating 0994c2f..39e3cec and Fast-forward:
it moved main from the old commit to the new one, updated the files, and showed which files changed. Afterwards
main and add-poha name the same commit, 39e3cec. No commit was made, and the history is exactly the commits
that add-poha already had.
A fast-forward moves main along add-poha's commits
Text description of the diagram
The diagram shows the three commits of the fast-forward session, newest at the top.
- Commit 39e3cec, "Add upma", has 417b2a9, "Add poha", as its parent, and 417b2a9 has 0994c2f, "Start the canteen menu", as its parent.
- Before the merge, main held 0994c2f, an ancestor of everything add-poha holds.
- After git merge add-poha, main holds 39e3cec, the same commit as add-poha.
No commit was created: the branch name simply moved forward along commits that already existed.
Three-way merge: when both branches have moved
Now the work overlaps in time. A teammate adds chai on add-chai while Meera raises the price of idli directly on
main:
from gitsession import session
session(r"""
git config set --global user.name "Meera Iyer"
git config set --global user.email "meera@example.com"
git init -q -b main canteen
cd canteen
printf 'Idli: 30\nDosa: 50\n' > menu.txt
git add menu.txt
git commit -q -m "Start the canteen menu"
git switch -q -c add-chai
printf 'Chai: 15\n' > drinks.txt
git add drinks.txt
git commit -q -m "Add chai"
git switch -q main
printf 'Idli: 35\nDosa: 50\n' > menu.txt
git commit -q -am "Raise the price of idli"
# Both branches moved on from the commit where add-chai began
git log --oneline --graph --all
git merge-base main add-chai
git merge add-chai
git log --oneline --graph
# A merge commit names two parents: first the branch you were on, then the one you merged
git cat-file -p HEAD
cat menu.txt drinks.txt
git merge add-chai
""") Output
$ git config set --global user.name "Meera Iyer" $ git config set --global user.email "meera@example.com" $ git init -q -b main canteen $ cd canteen $ printf 'Idli: 30\nDosa: 50\n' > menu.txt $ git add menu.txt $ git commit -q -m "Start the canteen menu" $ git switch -q -c add-chai $ printf 'Chai: 15\n' > drinks.txt $ git add drinks.txt $ git commit -q -m "Add chai" $ git switch -q main $ printf 'Idli: 35\nDosa: 50\n' > menu.txt $ git commit -q -am "Raise the price of idli" # Both branches moved on from the commit where add-chai began $ git log --oneline --graph --all * c3a9acb Add chai | * e432682 Raise the price of idli |/ * 0994c2f Start the canteen menu $ git merge-base main add-chai 0994c2f448f87f606624b3df119ba4378063e220 $ git merge add-chai Merge made by the 'ort' strategy. drinks.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 drinks.txt $ git log --oneline --graph * b572a03 Merge branch 'add-chai' |\ | * c3a9acb Add chai * | e432682 Raise the price of idli |/ * 0994c2f Start the canteen menu # A merge commit names two parents: first the branch you were on, then the one you merged $ git cat-file -p HEAD tree 4709c445789aa1d1a6457f1765d2662b699e779a parent e43268254a64ce725352e2594fc9c26f86fa7bf4 parent c3a9acb48581d30d0c06f2c990c9ace6cb9fd296 author Meera Iyer <meera@example.com> 1767240000 +0530 committer Meera Iyer <meera@example.com> 1767240000 +0530 Merge branch 'add-chai' $ cat menu.txt drinks.txt Idli: 35 Dosa: 50 Chai: 15 $ git merge add-chai Already up to date.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 three_way.py
The graph forks at 0994c2f, and git merge-base main add-chai prints that commit’s full ID: it is the newest
commit both branches contain, the point where they went separate ways. Neither tip is an ancestor of the other, so a
fast-forward is impossible. Git compared three versions of the project, the merge base and the two tips, which is
where the name three-way comes from. Each side had changed a different file, so the combined result kept both
changes: menu.txt has the new idli price from main, and drinks.txt comes from add-chai.
Merge made by the 'ort' strategy. names the merge machinery. ort has been Git’s default strategy since Git 2.34,
when it replaced the older recursive, and since Git 2.50 the name recursive simply means ort. The result is
commit b572a03. git cat-file -p HEAD prints the raw commit, and it has two parent lines: first e432682, the
commit main held when the merge started, then c3a9acb, the tip of add-chai. Merging the same branch again prints
Already up to date., because add-chai’s tip is now an ancestor of main.
A three-way merge records a commit with two parents
Text description of the diagram
The diagram shows the four commits of the three-way merge session, newest at the top.
- The merge commit b572a03, "Merge branch 'add-chai'", is where main points after the merge.
- Its first parent is e432682, "Raise the price of idli", the commit main held before the merge.
- Its second parent is c3a9acb, "Add chai", the commit add-chai holds.
- Both parents have 0994c2f, "Start the canteen menu", as their parent: that commit is the merge base, the newest commit the two branches share.
Git compared the merge base with each parent to work out what each side changed, and recorded the combined result in the merge commit.
How Git combines two sets of changes
A three-way merge works region by region inside each file. For every part of a file it asks what the merge base said
and what each side says now. git merge-file performs the same kind of three-way merge on three plain files, which
makes the rule easy to watch:
from gitsession import session
session(r"""
# Three versions of one file: the merge base, and the two sides
printf 'Idli: 30\nDosa: 50\nVada: 20\n' > base.txt
printf 'Idli: 35\nDosa: 50\nVada: 20\n' > ours.txt
printf 'Idli: 30\nDosa: 50\nVada: 25\n' > theirs.txt
# Each side changed a different line: both changes are kept
git merge-file -p ours.txt base.txt theirs.txt
# Both sides changed the same line, in the same way: nothing to decide
printf 'Idli: 35\nDosa: 50\nVada: 25\n' > theirs.txt
git merge-file -p ours.txt base.txt theirs.txt
# Both sides changed the same line, in different ways: a conflict
printf 'Idli: 32\nDosa: 50\nVada: 20\n' > theirs.txt
! git merge-file -p ours.txt base.txt theirs.txt
""") Output
# Three versions of one file: the merge base, and the two sides $ printf 'Idli: 30\nDosa: 50\nVada: 20\n' > base.txt $ printf 'Idli: 35\nDosa: 50\nVada: 20\n' > ours.txt $ printf 'Idli: 30\nDosa: 50\nVada: 25\n' > theirs.txt # Each side changed a different line: both changes are kept $ git merge-file -p ours.txt base.txt theirs.txt Idli: 35 Dosa: 50 Vada: 25 # Both sides changed the same line, in the same way: nothing to decide $ printf 'Idli: 35\nDosa: 50\nVada: 25\n' > theirs.txt $ git merge-file -p ours.txt base.txt theirs.txt Idli: 35 Dosa: 50 Vada: 25 # Both sides changed the same line, in different ways: a conflict $ printf 'Idli: 32\nDosa: 50\nVada: 20\n' > theirs.txt $ git merge-file -p ours.txt base.txt theirs.txt <<<<<<< ours.txt Idli: 35 ======= Idli: 32 >>>>>>> theirs.txt Dosa: 50 Vada: 20 [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 line_rule.py
| Line | Merge base | Your side | Their side | Result |
|---|---|---|---|---|
| Idli | 30 | 35 | 30 | 35: only you changed it |
| Vada | 20 | 20 | 25 | 25: only they changed it |
| Idli | 30 | 35 | 35 | 35: both made the same change |
| Idli | 30 | 35 | 32 | A conflict: Git will not pick a price for you |
The merge base is what makes the first two rows possible. With only the two tips, Idli: 35 against Idli: 30 could
mean that you raised the price or that they lowered it; with the base, Git can see who changed the line. A conflict
happens only when both sides changed a common segment of lines in different ways. Then Git writes both versions into
the file between markers and stops, and git merge-file exits with the number of conflicts, 1 here.
The next lesson is about exactly that situation. Files that only one side changed are not merged line by line at all: Git takes that
side’s version whole.
Choosing –ff, –no-ff or –ff-only
The default, --ff, fast-forwards when it can and makes a merge commit when it must. Two options change that:
--no-ff makes a merge commit even when a fast-forward is possible, and --ff-only allows nothing but a
fast-forward:
from gitsession import session
session(r"""
git config set --global user.name "Meera Iyer"
git config set --global user.email "meera@example.com"
git init -q -b main canteen
cd canteen
printf 'Idli: 30\nDosa: 50\n' > menu.txt
git add menu.txt
git commit -q -m "Start the canteen menu"
git switch -q -c add-poha
printf 'Idli: 30\nDosa: 50\nPoha: 25\n' > menu.txt
git commit -q -am "Add poha"
git switch -q main
# A fast-forward was possible, but --no-ff records a merge commit anyway
git merge --no-ff add-poha
git log --oneline --graph
# add-chai starts from the first commit, so main and add-chai have diverged
git switch -q -c add-chai main~1
printf 'Chai: 15\n' > drinks.txt
git add drinks.txt
git commit -q -m "Add chai"
git switch -q main
! git merge --ff-only add-chai
git log --oneline -1
""") Output
$ git config set --global user.name "Meera Iyer" $ git config set --global user.email "meera@example.com" $ git init -q -b main canteen $ cd canteen $ printf 'Idli: 30\nDosa: 50\n' > menu.txt $ git add menu.txt $ git commit -q -m "Start the canteen menu" $ git switch -q -c add-poha $ printf 'Idli: 30\nDosa: 50\nPoha: 25\n' > menu.txt $ git commit -q -am "Add poha" $ git switch -q main # A fast-forward was possible, but --no-ff records a merge commit anyway $ git merge --no-ff add-poha Merge made by the 'ort' strategy. menu.txt | 1 + 1 file changed, 1 insertion(+) $ git log --oneline --graph * 0a62cb9 Merge branch 'add-poha' |\ | * 417b2a9 Add poha |/ * 0994c2f Start the canteen menu # add-chai starts from the first commit, so main and add-chai have diverged $ git switch -q -c add-chai main~1 $ printf 'Chai: 15\n' > drinks.txt $ git add drinks.txt $ git commit -q -m "Add chai" $ git switch -q main $ git merge --ff-only add-chai hint: Diverging branches can't be fast-forwarded, you need to either: hint: hint: git merge --no-ff hint: hint: or: hint: hint: git rebase hint: hint: Disable this message with "git config set advice.diverging false" fatal: Not possible to fast-forward, aborting. [exit status 128] $ git log --oneline -1 0a62cb9 Merge branch 'add-poha'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ff_options.py
With --no-ff, the poha work sits on its own side line and the merge commit marks where it joined main, although
main had not moved. With --ff-only, the diverged add-chai was refused: Git printed a hint, then
Not possible to fast-forward, aborting., exited with status 128 and left main exactly where it was.
| Criterion | --ff (the default) | --no-ff | --ff-only |
|---|---|---|---|
| Your branch has not moved | Moves your branch forward, no new commit | Makes a merge commit anyway | Moves your branch forward |
| Both branches have moved | Makes a merge commit | Makes a merge commit | Refuses and changes nothing |
| What the history shows | A feature can vanish into main's line | Every merged branch stays a visible side line | A straight line, unless you merge some other way |
| As a setting | merge.ff true, or not set | merge.ff false | merge.ff only |
| When to choose | Small projects where nobody reads the history by feature | Teams that want each feature to show, and to be undone as one commit | Updating a branch that must never receive a merge commit |
The trade-off is between a short history and a readable one. A fast-forwarded feature leaves no trace of where it
began or ended. A --no-ff merge keeps the feature’s commits together behind one merge commit, so the first-parent
history below lists one entry per feature, and the whole feature can be undone by reverting that single commit
(git revert -m 1 <merge>, covered with the other undo commands later in this track). --ff-only suits a branch
that should only ever follow another one, such as your local copy of the team’s main. To change the default for one
repository, set merge.ff with git config set.
Reading history with merges
Merge commits make a history branch out and back. Three views help to read it:
from gitsession import session
session(r"""
git config set --global user.name "Meera Iyer"
git config set --global user.email "meera@example.com"
git init -q -b main canteen
cd canteen
printf 'Idli: 30\nDosa: 50\n' > menu.txt
git add menu.txt
git commit -q -m "Start the canteen menu"
git switch -q -c add-poha
printf 'Idli: 30\nDosa: 50\nPoha: 25\n' > menu.txt
git commit -q -am "Add poha"
printf 'Idli: 30\nDosa: 50\nPoha: 25\nUpma: 25\n' > menu.txt
git commit -q -am "Add upma"
git switch -q main
git merge -q --no-ff add-poha
git switch -q -c add-chai
printf 'Chai: 15\n' > drinks.txt
git add drinks.txt
git commit -q -m "Add chai"
git switch -q main
printf 'Idli: 35\nDosa: 50\nPoha: 25\nUpma: 25\n' > menu.txt
git commit -q -am "Raise the price of idli"
git merge -q add-chai
git log --oneline --graph
# Follow only first parents: the history of main itself
git log --oneline --first-parent
git log --oneline --merges
# The newest merge's parents: HEAD^1, where main was, and HEAD^2, the tip of add-chai
git log -1 --format='%p'
git log --oneline -1 HEAD^1
git log --oneline -1 HEAD^2
""") Output
$ git config set --global user.name "Meera Iyer" $ git config set --global user.email "meera@example.com" $ git init -q -b main canteen $ cd canteen $ printf 'Idli: 30\nDosa: 50\n' > menu.txt $ git add menu.txt $ git commit -q -m "Start the canteen menu" $ git switch -q -c add-poha $ printf 'Idli: 30\nDosa: 50\nPoha: 25\n' > menu.txt $ git commit -q -am "Add poha" $ printf 'Idli: 30\nDosa: 50\nPoha: 25\nUpma: 25\n' > menu.txt $ git commit -q -am "Add upma" $ git switch -q main $ git merge -q --no-ff add-poha $ git switch -q -c add-chai $ printf 'Chai: 15\n' > drinks.txt $ git add drinks.txt $ git commit -q -m "Add chai" $ git switch -q main $ printf 'Idli: 35\nDosa: 50\nPoha: 25\nUpma: 25\n' > menu.txt $ git commit -q -am "Raise the price of idli" $ git merge -q add-chai $ git log --oneline --graph * fb5508f Merge branch 'add-chai' |\ | * d8add51 Add chai * | 6e8714c Raise the price of idli |/ * 5e784ea Merge branch 'add-poha' |\ | * 39e3cec Add upma | * 417b2a9 Add poha |/ * 0994c2f Start the canteen menu # Follow only first parents: the history of main itself $ git log --oneline --first-parent fb5508f Merge branch 'add-chai' 6e8714c Raise the price of idli 5e784ea Merge branch 'add-poha' 0994c2f Start the canteen menu $ git log --oneline --merges fb5508f Merge branch 'add-chai' 5e784ea Merge branch 'add-poha' # The newest merge's parents: HEAD^1, where main was, and HEAD^2, the tip of add-chai $ git log -1 --format='%p' 6e8714c d8add51 $ git log --oneline -1 HEAD^1 6e8714c Raise the price of idli $ git log --oneline -1 HEAD^2 d8add51 Add chai
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 first_parent.py
git log --graph draws every line. git log --first-parent follows only the first parent at each merge, which is
the commit the branch was on, so it shows the history of main itself: the start, the poha merge, the idli price
change and the chai merge, without the commits those merges brought in. git log --merges lists only merge commits.
The format %p prints a commit’s abbreviated parents in order, and the suffixes ^1 and ^2 name the first and the
second parent: HEAD^2 is d8add51, the tip that add-chai had.
Parent order is why it matters which branch you are on when you merge. Merge add-chai while on main, and main’s
old tip is the first parent; merge main while on add-chai, and the first-parent line is the feature’s own.
Merging two branches at once
git merge takes several branches. Git then uses the octopus strategy and makes one merge commit with a parent per
branch:
from gitsession import session
session(r"""
git config set --global user.name "Meera Iyer"
git config set --global user.email "meera@example.com"
git init -q -b main canteen
cd canteen
printf 'Idli: 30\nDosa: 50\n' > menu.txt
git add menu.txt
git commit -q -m "Start the canteen menu"
git switch -q -c add-chai
printf 'Chai: 15\n' > chai.txt
git add chai.txt
git commit -q -m "Add chai"
git switch -q -c add-lassi main
printf 'Lassi: 40\n' > lassi.txt
git add lassi.txt
git commit -q -m "Add lassi"
git switch -q main
printf 'Idli: 35\nDosa: 50\n' > menu.txt
git commit -q -am "Raise the price of idli"
# Two branches at once: one merge commit with three parents
git merge add-chai add-lassi
git log --oneline --graph
git log -1 --format='%p'
""") Output
$ git config set --global user.name "Meera Iyer" $ git config set --global user.email "meera@example.com" $ git init -q -b main canteen $ cd canteen $ printf 'Idli: 30\nDosa: 50\n' > menu.txt $ git add menu.txt $ git commit -q -m "Start the canteen menu" $ git switch -q -c add-chai $ printf 'Chai: 15\n' > chai.txt $ git add chai.txt $ git commit -q -m "Add chai" $ git switch -q -c add-lassi main $ printf 'Lassi: 40\n' > lassi.txt $ git add lassi.txt $ git commit -q -m "Add lassi" $ git switch -q main $ printf 'Idli: 35\nDosa: 50\n' > menu.txt $ git commit -q -am "Raise the price of idli" # Two branches at once: one merge commit with three parents $ git merge add-chai add-lassi Trying simple merge with add-chai Trying simple merge with add-lassi Merge made by the 'octopus' strategy. chai.txt | 1 + lassi.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 chai.txt create mode 100644 lassi.txt $ git log --oneline --graph *-. f21cc0b Merge branches 'add-chai' and 'add-lassi' |\ \ | | * 8142dd9 Add lassi | * | d3be88d Add chai | |/ * / e432682 Raise the price of idli |/ * 0994c2f Start the canteen menu $ git log -1 --format='%p' e432682 d3be88d 8142dd9
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 octopus.py
%p printed three parents: main’s old tip, then the two branches in the order they were named. The octopus
strategy refuses to go on when a merge needs manual resolution, so it fits only branches that change different
files. Most teams merge one branch at a time, which keeps each merge easy to review and to undo.
Keeping a long-running branch current, and backing out
A branch that lives for weeks falls behind main. Merging main into it (git switch add-chai, then
git merge main) brings the new work in and lets you test against it early, at the cost of one merge commit each
time. The first-parent view of the feature branch then skips everything those merges brought in from main, which is
the case the documentation of --first-parent describes. Rebasing is the other way to keep up, and this track
compares the two in its rewriting-history lessons.
Before each merge Git saves where your branch was in ORIG_HEAD. If a merge you have not shared with anyone turns out
to be wrong, git reset --hard ORIG_HEAD puts the branch, the index and your files back to that commit. It also
discards every uncommitted change, so commit or stash first. A merge that others already have is undone with
git revert instead, which adds a commit rather than rewriting history.
Practise: predict the merge
Exercise · Easy · Python
Predict what git merge will do
Before running git merge other, Git asks one question about the history: who is an ancestor of whom? Write merge_outcome(parents, current, other) that answers it the way git merge does with its default settings.
parentsis a dictionary from every commit of a repository to the list of its parents:[]for the first commit, one parent for an ordinary commit and two or more for a merge commit.currentis the commit your branch holds, andotheris the commit of the branch you merge.
Return one of three strings:
"already up to date"whenotheriscurrentitself or one of its ancestors: everything is already merged;"fast-forward"whencurrentis an ancestor ofother: Git only moves your branch forward;"merge commit"in every other case: both sides have commits of their own, so Git makes a merge commit.
A commit's ancestors are its parents, their parents and so on, through every parent of a merge commit, not only the first.
Starter code · outcome.py
def merge_outcome(parents, current, other):
"""Return "already up to date", "fast-forward" or "merge commit" for git merge other on current."""
# Replace this line with your code.
return "" The sample tests · test_outcome.py
from outcome import merge_outcome
# The canteen history of the lesson: three commits on one line, then a branch and a merge.
LINE = {"0994c2f": [], "417b2a9": ["0994c2f"], "39e3cec": ["417b2a9"]}
FORKED = {
"0994c2f": [],
"e432682": ["0994c2f"], # main: Raise the price of idli
"c3a9acb": ["0994c2f"], # add-chai: Add chai
"b572a03": ["e432682", "c3a9acb"], # the merge commit
}
def test_fast_forward():
"""moves forward when the current commit is an ancestor of the other"""
assert merge_outcome(LINE, "0994c2f", "39e3cec") == "fast-forward"
def test_already_merged():
"""has nothing to do when the other commit is an ancestor"""
assert merge_outcome(LINE, "39e3cec", "417b2a9") == "already up to date"
def test_same_commit():
"""has nothing to do when both branches hold the same commit"""
assert merge_outcome(LINE, "417b2a9", "417b2a9") == "already up to date"
def test_diverged():
"""makes a merge commit when both sides have commits of their own"""
assert merge_outcome(FORKED, "e432682", "c3a9acb") == "merge commit"
def test_second_parent_counts():
"""finds ancestors through the second parent of a merge"""
assert merge_outcome(FORKED, "b572a03", "c3a9acb") == "already up to date"
def test_forward_through_a_merge():
"""moves forward to a merge commit that contains the current commit"""
assert merge_outcome(FORKED, "c3a9acb", "b572a03") == "fast-forward" A hint
Write a helper that collects every commit reachable from a start commit: keep a list of commits to visit, take one off it, and add its parents unless you have seen them already. Then other in reachable(current) answers the first case and current in reachable(other) the second.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Run the sessions yourself
These sessions are Python programs that pass their commands to real Git and print each one after $, with what Git
printed below it. The helper, gitsession.py, gives every session an empty folder of its own, which also serves as its
home folder, so the identity set with git config set --global stays in that folder. Commit times are fixed too, so
the commit IDs you get match the ones on this page. Save the helper beside any session, such as three_way.py, and
python3 three_way.py replays it in a terminal on macOS or Linux that has Git and Python 3.
"""Plays a terminal session with real Git: each command is printed after "$ ", then everything it printed.
The session runs in a new, empty folder called learner/, which is also its home folder, so your own Git settings
change nothing, and GIT_CONFIG_NOSYSTEM=1 makes Git skip the computer's system-wide settings file too. Git never
looks for a repository above that folder, so a session cannot touch a repository you run it from. In the output,
"~" stands for that folder. Commit times are fixed, because a commit's ID covers the time it was made: with the
same files, message, name and time, every run makes the same commit IDs.
Each line of a script is one /bin/sh command (macOS and Linux), except "cd", which the session does itself. A line
that starts with "# " is printed as a comment. A command written as "! command" is meant to fail: its exit status
is printed and the session goes on. Any other command that fails stops the session with exit status 1.
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
HOME = Path("learner").resolve()
# One fixed moment in Git's own date format: seconds since 1970, then the time zone (India, UTC+05:30).
FIXED_TIME = "@1767240000 +0530"
def _environment():
# Git's own folder comes first, so every command runs that Git directly (on macOS, /usr/bin/git is a small
# launcher that looks Git up again on every call).
git_folder = subprocess.run(["git", "--exec-path"], capture_output=True, text=True, check=True).stdout.strip()
env = {
"PATH": os.pathsep.join([git_folder, os.environ.get("PATH", "/usr/bin:/bin")]),
"HOME": str(HOME),
"XDG_CONFIG_HOME": str(HOME / ".config"),
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_CEILING_DIRECTORIES": str(HOME.parent),
"GIT_AUTHOR_DATE": FIXED_TIME,
"GIT_COMMITTER_DATE": FIXED_TIME,
"LANG": "C",
"LC_ALL": "C",
"TERM": "dumb",
}
if "TMPDIR" in os.environ:
env["TMPDIR"] = os.environ["TMPDIR"]
return env
def _with_tilde(text):
"""Writes the home folder as "~", however the operating system spells its path."""
for spelling in sorted({str(HOME), os.path.realpath(HOME)}, key=len, reverse=True):
text = text.replace(spelling, "~")
return text
def session(script):
"""Runs the script's commands one by one, starting in an empty home folder."""
shutil.rmtree(HOME, ignore_errors=True)
HOME.mkdir()
env = _environment()
folder = HOME
for line in script.strip().splitlines():
command = line.strip()
if not command:
continue
if command.startswith("# "):
print(command)
continue
meant_to_fail = command.startswith("! ")
if meant_to_fail:
command = command[2:]
print("$", command)
if command.startswith("cd "):
folder = (folder / command[3:].strip()).resolve()
continue
done = subprocess.run(command, shell=True, cwd=folder, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print(_with_tilde(done.stdout), end="")
if done.returncode != 0:
print(f"[exit status {done.returncode}]")
if not meant_to_fail:
sys.exit(1)
elif meant_to_fail:
sys.exit(f"{command} was meant to fail, but it worked") Interview questions
What is a fast-forward merge, and when does Git make one?
A fast-forward is a merge that creates no commit. When the branch you are on has no commits of its own since the
other branch started, its tip is an ancestor of the other branch’s tip, so nothing needs combining: Git simply moves
your branch to the other branch’s commit and updates your files. git merge prints Fast-forward, and afterwards
both branches name the same commit. As soon as both branches have new commits, a fast-forward is impossible and Git
makes a merge commit with two parents instead. --no-ff makes a merge commit even when a fast-forward is possible,
which keeps a visible record of the branch, and --ff-only refuses any merge that would need a merge commit.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
git merge feature prints "Already up to date.", yet the change your teammate made on feature is not in your files. List the likely causes, and how you would check each one.
-
Mid to senior
In a merge commit, which parent comes first, and why does the order matter? Name two everyday commands whose result changes if you merge main into a feature branch and then fast-forward main to it, instead of merging the feature into main.
Key takeaways
git mergefast-forwards when your branch’s tip is an ancestor of the other branch’s tip: the branch moves and no commit is made.- When both branches have new commits, Git does a three-way merge from the merge base and records a merge commit with two parents.
- The merge base is the newest commit both branches share;
git merge-baseprints it, and--is-ancestortests ancestry. - Per region of a file: a change made by one side is taken, an identical change by both is taken once, and different changes to the same lines conflict.
--no-ffalways records a merge commit;--ff-onlyrefuses anything but a fast-forward;merge.ffsets the default.- A merge commit’s first parent is the branch you were on, so
git log --first-parentshows that branch’s own history.
References
- git-merge documentation (The Git project)
- git-merge-base documentation (The Git project)
- git-merge-file documentation (The Git project)
- git-log documentation (--first-parent, --merges, --graph) (The Git project)
- git-reset documentation (undo a merge with ORIG_HEAD) (The Git project)
- git-revert documentation (reverting a merge with --mainline) (The Git project)
- Git 2.34 release notes (ort becomes the default merge strategy) (The Git project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress