Merging

git merge brings the work from one branch into another. Depending on how the branches have moved, Git either simply moves the branch label forward (a fast-forward) or creates a merge commit that ties the two lines of history together.

A fast-forward merge

Sam creates a branch for desserts and makes two commits on it, while main stays where it was:

$
git switch -c desserts
echo "Brownies: chocolate, butter, sugar" > brownies.txt
git add . && git commit -m "Add brownie recipe"
echo "Tiramisu: mascarpone, coffee, sponge" > tiramisu.txt
git add . && git commit -m "Add tiramisu recipe"
Output
Switched to a new branch 'desserts'
[desserts 25437c2] Add brownie recipe
 1 file changed, 1 insertion(+)
 create mode 100644 brownies.txt
[desserts 005385a] Add tiramisu recipe
 1 file changed, 1 insertion(+)
 create mode 100644 tiramisu.txt

To merge, switch to the branch that should receive the work, then name the branch to merge in:

$
git switch main
git merge desserts
Output
Switched to branch 'main'
Updating 90c0089..005385a
Fast-forward
 brownies.txt | 1 +
 tiramisu.txt | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 brownies.txt
 create mode 100644 tiramisu.txt

"Fast-forward" means no new commit was needed. main hadn't moved since desserts was created, so the desserts commits already follow on from it; Git just slid the main label forward:

$
git log --oneline --graph
Output
* 005385a (HEAD -> main, desserts) Add tiramisu recipe
* 25437c2 Add brownie recipe
* 90c0089 Add omelette recipe
* c4f5e47 Add pancake recipe

A three-way merge

Now both branches get new commits. Sam starts a soups branch, and meanwhile a fix lands on main:

$
git switch -c soups
echo "Tomato soup: tomatoes, onion, stock" > tomato-soup.txt
git add . && git commit -m "Add tomato soup"
git switch main
echo "Omelette: eggs, butter, chives" > omelette.txt
git commit -am "Add chives to omelette"
Output
Switched to a new branch 'soups'
[soups 3fd4708] Add tomato soup
 1 file changed, 1 insertion(+)
 create mode 100644 tomato-soup.txt
Switched to branch 'main'
[main 726f0fb] Add chives to omelette
 1 file changed, 1 insertion(+), 1 deletion(-)
$
git log --oneline --graph --all
Output
* 726f0fb (HEAD -> main) Add chives to omelette
| * 3fd4708 (soups) Add tomato soup
|/
* 005385a (desserts) Add tiramisu recipe
* 25437c2 Add brownie recipe
* 90c0089 Add omelette recipe
* c4f5e47 Add pancake recipe

The branches have diverged. A fast-forward is impossible, so Git performs a three-way merge: it compares both branch tips with their common ancestor and combines the changes, then records the result in a new merge commit that has two parents.

$
git merge soups
Output
Merge made by the 'ort' strategy.
 tomato-soup.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 tomato-soup.txt

Without --no-edit, Git opens your editor so you can adjust the default message "Merge branch 'soups'"; accepting it is normal.

$
git log --oneline --graph
Output
*   96670ee (HEAD -> main) Merge branch 'soups'
|\
| * 3fd4708 (soups) Add tomato soup
* | 726f0fb Add chives to omelette
|/
* 005385a (desserts) Add tiramisu recipe
* 25437c2 Add brownie recipe
* 90c0089 Add omelette recipe
* c4f5e47 Add pancake recipe

"ort" in the output is the name of Git's default merge strategy; you can ignore it.

Forcing a merge commit

Some teams like every feature to show up as a merge commit, even when a fast-forward is possible, so the history records which commits belonged together. --no-ff does that:

$
git switch -c breads
echo "Soda bread: flour, buttermilk, soda" > soda-bread.txt
git add . && git commit -m "Add soda bread"
git switch main
git merge --no-ff --no-edit breads
git log --oneline --graph -4
Output
Switched to a new branch 'breads'
[breads 60fdcc6] Add soda bread
 1 file changed, 1 insertion(+)
 create mode 100644 soda-bread.txt
Switched to branch 'main'
Merge made by the 'ort' strategy.
 soda-bread.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 soda-bread.txt
*   f76132b (HEAD -> main) Merge branch 'breads'
|\
| * 60fdcc6 (breads) Add soda bread
|/
*   96670ee Merge branch 'soups'
|\
| * 3fd4708 (soups) Add tomato soup

The opposite, --ff-only, merges only if a fast-forward is possible and refuses otherwise — useful when you want to be sure nothing unexpected gets merged.

Squash merges

git merge --squash branch takes all the changes from a branch and stages them as one set, without making a merge commit. You then commit them yourself as a single ordinary commit. The branch's individual commits don't appear in main's history. Hosting services offer the same thing as "Squash and merge" on pull requests (lesson 36).

After merging

A merged branch has done its job. Delete it to keep the branch list tidy — -d succeeds because its work is in main:

$
git branch --merged
Output
  breads
  desserts
* main
  soups
$
git branch -d desserts soups breads
Output
Deleted branch desserts (was 005385a).
Deleted branch soups (was 3fd4708).
Deleted branch breads (was 60fdcc6).

Merging tips

  • Start clean. Commit or stash your changes before merging; Git refuses to merge over uncommitted changes in files it needs to touch.
  • Merge often. The longer branches live apart, the more they diverge and the more likely conflicts become. Bringing main into your feature branch now and then (git merge main while on the feature branch) keeps it current.
  • Undoing. Right after a merge that you haven't pushed, git reset --hard HEAD~1 (or ORIG_HEAD, which Git sets to where you were before the merge) takes it back. After pushing, use git revert -m 1 (lesson 19).

When both branches change the same lines, Git can't combine them automatically — that's a merge conflict, the topic of the next lesson.

Try it yourself

Create two branches from main. Merge the first with a fast-forward. Then commit on both main and the second branch and merge again — check with git log --oneline --graph that you got a merge commit with two parents.