Merging branches¶
Each branch has a purpose: developing a feature, fixing a bug, testing an idea. Once that task is complete, its changes are incorporated into the production version of the project, usually the main branch. This is called merging.
Source and destination¶
A merge always involves two branches:
| Role | Meaning | Example |
|---|---|---|
| Source | The branch to merge from: the one with the finished work | monitoring |
| Destination | The branch to merge into: the one that receives the work | main |
The latest commits of the two branches are the parent commits of the merge.
git merge¶
git merge always merges into the current branch. So the procedure is:
- Switch to the destination branch.
- Run
git mergewith the source branch.
git switch main
git merge monitoring
There is no destination argument
Every branch given to git merge is a source: git merge monitoring main, run from another branch, merges both monitoring and main into the branch you are on, and leaves main unchanged. To merge into main, be on main (git merge documentation).
Depending on the history of the two branches, Git merges in one of two ways.
Fast-forward merge¶
Since monitoring was created, main has not received any new commit. The history is still linear: monitoring is simply main plus one commit.
$ git switch main
Already on 'main'
$ git merge monitoring
Updating 112c587..eeb2e47
Fast-forward
inventory.csv | 1 +
services.md | 1 +
2 files changed, 2 insertions(+)
Reading the output¶
| Line | Meaning |
|---|---|
Updating 112c587..eeb2e47 |
main moves from its latest commit (112c587) to the latest commit of monitoring (eeb2e47) |
Fast-forward |
The merge type: no new commit was needed |
inventory.csv \| 1 + |
Per-file summary: one line added, as in git diff --stat |
2 files changed, 2 insertions(+) |
Total of the changes brought into main |
In a fast-forward, Git only moves the main label forward to the last commit of the source branch. No merge commit is created, and the history stays a straight line:
flowchart LR
subgraph before["Before the merge"]
direction LR
b1["112c587<br/>Create homelab inventory"] --> b2["eeb2e47<br/>Add monitoring server"]
bm(["main"]) --> b1
bmon(["monitoring"]) --> b2
end
subgraph after["After the fast-forward"]
direction LR
a1["112c587<br/>Create homelab inventory"] --> a2["eeb2e47<br/>Add monitoring server"]
am(["main"]) --> a2
amon(["monitoring"]) --> a2
end
before ~~~ after
$ git log --oneline --graph --all
* eeb2e47 Add monitoring server
* 112c587 Create homelab inventory
Three-way merge¶
Next, Sam documents the nightly backups on a new branch, backups. Meanwhile, a UPS is added to the inventory directly on main. Both branches now have a commit the other does not have: the history has diverged.
%%{init: {"gitGraph": {"showBranches": true, "rotateCommitLabel": false, "mainBranchName": "main"}, "themeVariables": {"git0": "#43a047", "git1": "#1e88e5", "git2": "#fb8c00", "gitBranchLabel0": "#ffffff", "gitBranchLabel1": "#ffffff", "gitBranchLabel2": "#ffffff", "gitInv0": "#ffffff", "commitLabelFontSize": "13px"}}}%%
gitGraph TB:
commit id: "112c587 Create homelab inventory"
commit id: "eeb2e47 Add monitoring server"
branch backups
commit id: "6f673d0 Document nightly backups"
checkout main
commit id: "49500df Add UPS to inventory"
merge backups id: "d88fecf Merge branch 'backups'"
main cannot simply move forward, since that would lose the UPS commit. Git performs a three-way merge: it compares the two branch tips with their common ancestor (eeb2e47), combines the changes, and records the result in a new merge commit.
$ git switch main
Switched to branch 'main'
$ git merge backups
Merge made by the 'ort' strategy.
backups.md | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 backups.md
Because a new commit is created, Git opens the text editor with a prepared message, Merge branch 'backups'. Save and close it to finish the merge, or skip the editor with git merge --no-edit backups. 'ort' is the name of Git's default merge strategy; create mode 100644 backups.md means that backups.md is a new file.
The graph shows the two lines of history joining:
$ git log --oneline --graph
* d88fecf Merge branch 'backups'
|\
| * 6f673d0 Document nightly backups
* | 49500df Add UPS to inventory
|/
* eeb2e47 Add monitoring server
* 112c587 Create homelab inventory
A merge commit is the only kind of commit with two parents. git log shows them on a Merge: line:
$ git log -1
commit d88fecfea962f512358d7cf72a9993e7bb072a7a
Merge: 49500df 6f673d0
Author: Sam Rivera <[email protected]>
Date: Mon Apr 6 10:06:00 2026 +0200
Merge branch 'backups'
Fast-forward or three-way?¶
| Situation | Merge type | New commit? |
|---|---|---|
| The destination has no new commits since the source branched off | Fast-forward | No, the destination label moves forward |
| Both branches have new commits | Three-way merge | Yes, a merge commit with two parents |
Git chooses automatically. git merge --no-ff <branch> forces a merge commit even when a fast-forward is possible, which keeps a visible trace of the branch in the history. git merge --ff-only <branch> does the opposite: it merges only if a fast-forward is possible, and refuses otherwise.
When the same lines were changed on both sides, the three-way merge cannot decide on its own and stops with a conflict: see Resolving merge conflicts.
Cleaning up merged branches¶
Once merged, a branch has served its purpose. git branch --merged lists the branches whose commits are all included in the current branch:
$ git branch --merged
backups
* main
monitoring
These are safe to delete with -d, which accepts several names:
$ git branch -d monitoring backups
Deleted branch monitoring (was eeb2e47).
Deleted branch backups (was 6f673d0).
The commits stay in the history of main: only the labels are removed.
Summary¶
| Command | Result |
|---|---|
git switch main then git merge monitoring |
Merge monitoring (source) into main (destination) |
git merge --no-edit backups |
Merge, accepting the default merge commit message |
git merge --no-ff monitoring |
Always create a merge commit, even if a fast-forward is possible |
git merge --ff-only monitoring |
Merge only if a fast-forward is possible |
git log --oneline --graph |
Display the history with its branches and merges |
git branch --merged |
List the branches already merged into the current branch |
Common mistakes¶
- Running
git mergefrom the wrong branch. The current branch is the destination. Checkgit branchfirst. - Expecting
git merge source destinationto merge intodestination. All arguments are sources; the destination is always the current branch. - Being surprised by an editor during a merge. A three-way merge creates a commit and asks for its message. Save and close, or use
--no-edit. - Keeping merged branches forever. Delete them with
git branch -donce merged.
Hands-on labs¶
Three labs, from guided to more autonomous. Each one has its own setup, so you can do them in any order and repeat them as often as you like. Type the commands rather than pasting them, and read every output before moving on: the goal is to build reflexes, not to reach the end.
Lab 1: fast-forward and three-way merges¶
Objective: perform both kinds of merge, recognise them in the output and in the graph, then clean up.
Prerequisites and initial state: Git installed. The setup creates ~/git-practice/merge with one commit on main.
Setup:
mkdir -p ~/git-practice/merge && cd ~/git-practice/merge
git init -q -b main
git config user.name "Practice User"
git config user.email "[email protected]"
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\n' > inventory.csv
git add . && git commit -q -m "Create inventory"
Tasks:
- Create a branch
nas, add the linenas,192.168.1.20,storagetoinventory.csv, and commit with the messageAdd NAS. - Merge
nasintomain. Which kind of merge happens? - Create a branch
docsfrommain, create a fileREADME.mdcontaining# Homelab, and commit it with the messageAdd README. - Back on
main, add the lineweb01,192.168.1.30,webtoinventory.csvand commit with the messageAdd web server. - Merge
docsintomainwithout opening an editor. Which kind of merge happens this time? - List the merged branches and delete them.
Expected result and verification:
- Task 2 prints
Fast-forward. - Task 5 prints
Merge made by the 'ort' strategy. git log --oneline --graphshowsMerge branch 'docs'at the top, with two lines of history joining below it.git log -1shows aMerge:line with two hashes.git branchfinally lists only* main, andmaincontainsREADME.mdand an inventory with three machines.
Solution
# 1. Work on a branch
git switch -c nas
echo "nas,192.168.1.20,storage" >> inventory.csv
git commit -am "Add NAS"
# 2. main has not moved: fast-forward
git switch main
git merge nas # Updating ... Fast-forward
# 3. Second branch
git switch -c docs
echo "# Homelab" > README.md
git add README.md
git commit -m "Add README"
# 4. main moves on in the meantime
git switch main
echo "web01,192.168.1.30,web" >> inventory.csv
git commit -am "Add web server"
# 5. Diverged history: three-way merge with a merge commit
git merge --no-edit docs # Merge made by the 'ort' strategy.
git log --oneline --graph
git log -1 # Merge: <parent 1> <parent 2>
# 6. Clean up
git branch --merged # docs, * main, nas
git branch -d nas docs
- At task 2,
mainwas still at the commitnasstarted from, so Git only moved the label. - At task 5, each branch had a commit the other did not have, so Git created a merge commit with two parents.
- The two changes touched different files, so the merge needed no help. The next page covers what happens when they touch the same lines.
Clean up when you are done: rm -rf ~/git-practice/merge.
Lab 2: which way round?¶
Objective: see that the direction of a merge matters, by merging main into a branch first, then the branch into main.
Prerequisites and initial state: Git installed. The setup creates ~/git-practice/direction: main and backups have each received one commit since they split.
Setup:
mkdir -p ~/git-practice/direction && cd ~/git-practice/direction
git init -q -b main
git config user.name "Practice User"
git config user.email "[email protected]"
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\n' > inventory.csv
git add . && git commit -q -m "Create inventory"
git switch -q -c backups
printf '#!/bin/sh\nrsync -a /srv/data /mnt/backup\n' > backup.sh
git add . && git commit -q -m "Add backup script"
git switch -q main
echo "nas,192.168.1.20,storage" >> inventory.csv
git commit -q -am "Add NAS"
Tasks: before each merge, write down your prediction: fast-forward or three-way?
- Display the graph of all branches.
- Your work on
backupsis not finished, but you want the NAS in it to test the script. Bringmain's changes intobackups, without opening an editor. - Display the graph. On which branch is the merge commit?
- The script is ready. Merge
backupsintomain. - Display the graph again, and delete
backups.
Expected result and verification:
- Task 2 is a three-way merge (
Merge made by the 'ort' strategy.): both branches had moved on. - Task 3:
Merge branch 'main' into backupsis onbackups;mainhas not changed. - Task 4 is a fast-forward:
mainis now an ancestor ofbackups, so Git only moves the label. - Task 5:
mainandbackupspoint to the same merge commit, andgit branch -d backupssucceeds.
Solution
# 1. Diverged history
git log --oneline --graph --all
# 2. The destination is the current branch: switch to backups first
git switch backups
git merge --no-edit main # Merge made by the 'ort' strategy.
# 3. Only backups moved
git log --oneline --graph --all # Merge branch 'main' into backups
# 4. Now main is behind backups: fast-forward
git switch main
git merge backups # Fast-forward
# 5. Clean up
git log --oneline --graph --all
git branch -d backups
git merge Xalways means "bring X into the current branch". Running it from the wrong branch merges in the wrong direction.- Merging
maininto a long-lived branch keeps it up to date and lets you solve problems on the branch, before they reachmain. - After task 2, every commit of
mainwas already insidebackups. Nothing had to be combined in task 4, hence the fast-forward.
Clean up when you are done: rm -rf ~/git-practice/direction.
Lab 3: a branch on a branch¶
Objective: merge a branch that was created from another branch, and see what --merged reports afterwards.
Prerequisites and initial state: Git installed. The setup creates ~/git-practice/stacked with one commit on main.
Setup:
mkdir -p ~/git-practice/stacked && cd ~/git-practice/stacked
git init -q -b main
git config user.name "Practice User"
git config user.email "[email protected]"
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\n' > inventory.csv
git add . && git commit -q -m "Create inventory"
Tasks:
- Create a branch
monitoring, addmon01,192.168.1.50,monitoringto the inventory, and commit with the messageAdd monitoring server. - From
monitoring, create a branchalerts. Create a filealerts.mdcontaining# Alerts, and commit it with the messageAdd alert rules. - Go back to
main, and list the branches that are already merged into it. - Merge only
alertsintomain. Predict: which commits willmainreceive? - List the merged branches again, and delete every branch except
mainwith the safe option.
Expected result and verification:
- Task 3:
git branch --mergedlists only* main. - Task 4 is a fast-forward, and
git log --onelineonmainshowsAdd alert rules,Add monitoring server,Create inventory. - Task 5:
git branch --mergedlistsalerts,* main, andmonitoring;git branch -d alerts monitoringsucceeds.
Solution
# 1. First branch
git switch -c monitoring
echo "mon01,192.168.1.50,monitoring" >> inventory.csv
git commit -am "Add monitoring server"
# 2. Second branch, created from the first one
git switch -c alerts
echo "# Alerts" > alerts.md
git add alerts.md
git commit -m "Add alert rules"
# 3. Nothing merged yet
git switch main
git branch --merged # * main
# 4. alerts contains monitoring's commit too
git merge alerts # Fast-forward
git log --oneline # 3 commits
# 5. Both branches are now merged
git branch --merged # alerts, * main, monitoring
git branch -d alerts monitoring
- A branch contains the whole chain of commits leading to its tip. Merging
alertsbringsAdd monitoring serveralong, even thoughmonitoringwas never merged by name. - This is why
git branch --mergedreportsmonitoringas merged: all its commits are now inmain. - Stacking branches is handy, but merging the top one merges everything below it.
Clean up when you are done: rm -rf ~/git-practice/stacked.