Skip to content

Merge strategies

  • Lesson 9
  • 40 min
  • Module 3: Collaborate through reviews
Lesson 9 of 15 · Git and Team Development EssentialsMerge strategies

What you will be able to do

Choose how a pull request goes into main, and know what happens to its commits. After this lesson, you can say what history a merge commit, a squash and a rebase each keep or rewrite.

Before you startWhat to know and to have readyAbout 40 minutes

What you need to know

What you need on your computer

  • The folder ~/projects/nextia-learning-labs. You cloned it in Pull-request sequence: Set up the practice lab. If it is missing, do step 2 of that section.

  • Reset the practice lab, so that your output is the same as this lesson. In ~/projects, run:

    Terminal window
    sh nextia-learning-labs/C14/setup.sh m03 --reset
  • Then go to ~/projects/c14-lab/ticket-notes. git status shows On branch main and working tree clean. The branch add-faq, which you merge in three ways, is in your clone. The script made it.

  • The merge, squash and rebase in this lesson make new commits. Your new hashes are different from the ones here. The hashes of the setup commits, such as 6b53600, are the same.

Amira’s branch add-faq adds a new file, faq.txt, in two commits. She made the branch from e058b3f. After that, Sam added a commit to main:

Terminal (Amira, ticket-notes)
git log --oneline --graph main add-faq
Output (first lines)
* 2e9e37c (HEAD -> main, origin/main, origin/HEAD) Add the on-call number
| * 6c3828f (origin/add-faq, add-faq) Add a network question to the FAQ
| * 6b53600 Add FAQ
|/
* e058b3f Add ticket 103

main and add-faq have diverged: each has a commit that the other does not have. A hosted service offers three ways to get add-faq into main. All three give the same files. They give different history.

  • Merge commit. Git makes one new commit with two parents: the newest commit of main and the newest commit of the branch. The branch commits keep their hashes and become part of main. You did this with git merge --no-ff in the first lesson of this module.
  • Squash. To squash is to combine all the changes of a branch into one new commit on the target branch. The new commit has one parent. The branch commits do not become part of main.
  • Rebase. To rebase is to copy the commits of a branch onto a new starting commit, one at a time. Each copy is a new commit with a new hash. Then main moves forward to the last copy, with a fast-forward.
Four small histories. Start: main has e058b3f then 2e9e37c. add-faq starts from e058b3f and has 6b53600 then 6c3828f. Merge commit: main gets a new commit, 47b3604, with two parents, 2e9e37c and 6c3828f, so the branch commits are part of main. Squash: main gets one new commit, edddd6f, after 2e9e37c. 6b53600 and 6c3828f are not in main. Rebase: main gets two new commits, c8c08d3 and da418c8, after 2e9e37c. They are copies of 6b53600 and 6c3828f with new hashes. The old commits are not in main.
The same start, three results. Filled circles are new commits. Dashed circles are commits that do not become part of main.

Why does a rebase change the hashes? A commit hash is calculated from everything in the commit, and that includes the hash of its parent. A copy on a new parent is a different commit, so it gets a different hash, even when its changes are the same.

Worked example: try all three side by side

Section titled “Worked example: try all three side by side”

Amira wants to see each result without changing main or add-faq. So she uses throwaway branches: new branch labels that she deletes after the test. A branch costs almost nothing, and the real branches do not move.

She makes a copy of main called try-merge, and merges add-faq into it:

Terminal (Amira, ticket-notes)
git switch -c try-merge main
git merge --no-ff --no-edit add-faq
git log --oneline --graph -5
Output
Switched to a new branch 'try-merge'
Merge made by the 'ort' strategy.
faq.txt | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 faq.txt
* 47b3604 (HEAD -> try-merge) Merge branch 'add-faq' into try-merge
|\
| * 6c3828f (origin/add-faq, add-faq) Add a network question to the FAQ
| * 6b53600 Add FAQ
* | 2e9e37c (origin/main, origin/HEAD, main) Add the on-call number
|/
* e058b3f Add ticket 103

git switch -c try-merge main makes the new branch at main, not at the branch that she is on. The message says into try-merge because the target is not main. On main, it says only Merge branch 'add-faq'.

The history keeps everything: both FAQ commits, with their original hashes, and the point where the branch joined.

She makes a second copy of main. git merge --squash puts all the changes of add-faq into the staging area, but it does not commit:

Terminal (Amira, ticket-notes)
git switch -c try-squash main
git merge --squash add-faq
git status --short
Output
Switched to a new branch 'try-squash'
Automatic merge went well; stopped before committing as requested
Squash commit -- not updating HEAD
A faq.txt

She commits the changes as one commit, with a message for the whole PR:

Terminal (Amira, ticket-notes)
git commit -m "Add an FAQ with two questions"
git log --oneline --graph -3
Output
[try-squash edddd6f] Add an FAQ with two questions
1 file changed, 5 insertions(+)
create mode 100644 faq.txt
* edddd6f (HEAD -> try-squash) Add an FAQ with two questions
* 2e9e37c (origin/main, origin/HEAD, main) Add the on-call number
* e058b3f Add ticket 103

The history is a straight line. edddd6f contains the changes of both FAQ commits, but it has one parent, 2e9e37c. The commits 6b53600 and 6c3828f are not part of this history, and their separate messages are gone from it.

First, Amira writes down the hashes of the branch commits:

Terminal (Amira, ticket-notes)
git log --oneline main..add-faq
Output
6c3828f Add a network question to the FAQ
6b53600 Add FAQ

She makes a copy of add-faq this time, because a rebase changes the branch that you are on. Then she rebases the copy onto main:

Terminal (Amira, ticket-notes)
git switch -c try-rebase add-faq
git rebase main
git log --oneline --graph -4
Output
Switched to a new branch 'try-rebase'
Successfully rebased and updated refs/heads/try-rebase.
* da418c8 (HEAD -> try-rebase) Add a network question to the FAQ
* c8c08d3 Add FAQ
* 2e9e37c (origin/main, origin/HEAD, main) Add the on-call number
* e058b3f Add ticket 103

The two commits have the same messages and the same changes. But now they start from 2e9e37c, and they have new hashes: c8c08d3 and da418c8. The originals still exist on add-faq:

Terminal (Amira, ticket-notes)
git log --oneline main..add-faq
git log --oneline main..try-rebase
Output
6c3828f Add a network question to the FAQ
6b53600 Add FAQ
da418c8 Add a network question to the FAQ
c8c08d3 Add FAQ

To finish, the target branch moves forward to the copies. She makes a copy of main called try-ff, and merges with --ff-only. This option merges only when Git can do a fast-forward. Otherwise it stops, and changes nothing.

Terminal (Amira, ticket-notes)
git switch -c try-ff main
git merge --ff-only try-rebase
Output
Switched to a new branch 'try-ff'
Updating 2e9e37c..da418c8
Fast-forward
faq.txt | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 faq.txt

The fast-forward worked, because after the rebase the copies start from the newest commit of main. No merge commit was made.

All three results contain the same files. git diff --stat between them prints nothing:

Terminal (Amira, ticket-notes)
git diff --stat try-merge try-squash
git diff --stat try-merge try-ff

No output means no difference. Only the history is different.

Amira switches back to main. git branch -d refuses to delete try-merge, because its merge commit is not in main:

Terminal (Amira, ticket-notes)
git switch main
git branch -d try-merge
Output
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
error: the branch 'try-merge' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D try-merge'
hint: Disable this message with "git config set advice.forceDeleteBranch false"

These branches are tests, so she deletes them with -D. -D deletes a branch even when its commits are not merged anywhere:

Terminal (Amira, ticket-notes)
git branch -D try-merge try-squash try-rebase try-ff
Output
Deleted branch try-merge (was 47b3604).
Deleted branch try-squash (was edddd6f).
Deleted branch try-rebase (was da418c8).
Deleted branch try-ff (was da418c8).

main and add-faq did not change during the test.

Merge commit Squash Rebase, then fast-forward
New commits on main 1 merge commit 1 commit 1 copy for each branch commit
Branch commits Kept, with the same hashes Not in main. Their changes are inside the new commit. Rewritten: copies with new hashes
Shape of the history Shows where the branch started and joined A straight line A straight line
Separate commit messages Kept Replaced by one message Kept
Button on GitHub Create a merge commit Squash and merge Rebase and merge
When teams use it When each commit matters, and the team wants to see each PR as a unit When the branch has many small or “fix” commits, and one commit for each PR is clearer When the team wants a straight history, and each commit is clean

None of the methods is always correct. Many teams choose one method for all their PRs, so that the history is consistent. Find out which method your team uses before you merge.

Guided practice: compare the methods yourself

Section titled “Guided practice: compare the methods yourself”

Use throwaway branches. Do not run these commands on main or add-faq themselves.

  1. In ~/projects/c14-lab/ticket-notes, run git status. The output shows On branch main and working tree clean.

  2. Run git log --oneline main..add-faq. Write down the two hashes: 6c3828f and 6b53600.

  3. Try a merge commit:

    Terminal window
    git switch -c try-merge main
    git merge --no-ff --no-edit add-faq
  4. Run git log --oneline --graph -5. The newest commit has two parents, and 6b53600 and 6c3828f are in the graph.

  5. Try a squash:

    Terminal window
    git switch -c try-squash main
    git merge --squash add-faq
    git commit -m "Add an FAQ with two questions"
  6. Run git log --oneline --graph -3. The history is a straight line. The FAQ commits are not in it.

  7. Try a rebase on a copy of the branch:

    Terminal window
    git switch -c try-rebase add-faq
    git rebase main

    The output ends with Successfully rebased and updated refs/heads/try-rebase.

  8. Run git log --oneline main..try-rebase. Compare the hashes with the ones from step 2. The messages are the same. The hashes are different.

  9. Fast-forward a copy of main to the rebased commits:

    Terminal window
    git switch -c try-ff main
    git merge --ff-only try-rebase

    The output includes Fast-forward.

  10. Compare the files of the results:

    Terminal window
    git diff --stat try-merge try-squash
    git diff --stat try-merge try-ff

    There is no output: the files are the same.

  11. Switch to main, and delete the throwaway branches:

    Terminal window
    git switch main
    git branch -D try-merge try-squash try-rebase try-ff
  12. Run git log --oneline --graph --all -6. main is still at 2e9e37c, and add-faq is still at 6c3828f.

Your turn: a branch with an unrelated commit

Section titled “Your turn: a branch with an unrelated commit”

Sam’s PR from the last lesson, add-ticket-104 → main, has two commits: e34484f Add ticket 104 and 5a15d07 Improve README wording.

First, predict for each method:

  1. How many new commits does main get?
  2. Do the hashes e34484f and 5a15d07 appear in the history of main?
  3. Can a reader of main still see that the README change was a separate commit?

Then check your predictions with throwaway branches. Use origin/add-ticket-104 as the branch to merge, and git log --oneline main..<throwaway branch> to see the result. Delete the throwaway branches at the end.

Show a model answer
Terminal (Amira, ticket-notes)
git switch -c try-merge main
git merge --no-ff --no-edit origin/add-ticket-104
git log --oneline main..try-merge
Output (end)
5c41513 Merge remote-tracking branch 'origin/add-ticket-104' into try-merge
5a15d07 Improve README wording
e34484f Add ticket 104
Terminal (Amira, ticket-notes)
git switch -c try-squash main
git merge --squash origin/add-ticket-104
git commit -m "Add ticket 104"
git log --oneline main..try-squash
git show --stat HEAD
Output (end)
37a47a9 Add ticket 104
README.md | 2 +-
scratch.txt | 2 ++
tickets.txt | 1 +
3 files changed, 4 insertions(+), 1 deletion(-)
Terminal (Amira, ticket-notes)
git switch -c try-rebase origin/add-ticket-104
git rebase main
git log --oneline main..try-rebase
Output (end)
9586a0f Improve README wording
651a164 Add ticket 104

Then git switch main and git branch -D try-merge try-squash try-rebase.

New commits e34484f and 5a15d07 in main? README change visible as its own commit?
Merge commit 3: both commits and the merge commit Yes Yes
Squash 1 No No. It is hidden inside “Add ticket 104”, with scratch.txt.
Rebase 2 copies No: copies with new hashes Yes

The squash result is the dangerous one here. A squash does not remove unrelated changes or accidental files. It hides them inside one commit with a message that does not mention them. A merge method never replaces a review: fix the PR first, as in the last lesson, then merge it.

Common mistake

After a squash merge, git branch -d refuses

Symptom: Amira squash-merges add-faq into main, pushes main, and deletes the remote branch with git push origin --delete add-faq. Then she deletes her local branch:

Terminal window
git branch -d add-faq
Output
error: the branch 'add-faq' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D add-faq'
hint: Disable this message with "git config set advice.forceDeleteBranch false"

Cause: -d checks if the commits of the branch are in main. After a squash, they are not: main has a new commit with the same changes, but 6b53600 and 6c3828f are not part of its history. Git cannot see that the changes are the same, so it refuses. git log --oneline main..add-faq still lists both commits.

Fix:

  1. Check that the content is in main. Compare the file on both branches:

    Terminal window
    git diff add-faq main -- faq.txt

    No output means that faq.txt is the same on both branches. Compare only the files that the branch changed. main can have other new changes that the branch does not have.

  2. If there is no output, delete the branch with git branch -D add-faq.

  3. If there is output, stop. Some work of the branch is not in main. Find out why before you delete anything.

Common mistake

After a rebase, git push is rejected

Symptom: Amira rebases add-faq itself onto main. The branch was pushed before. Now git status and git push show:

git status (first lines)
On branch add-faq
Your branch and 'origin/add-faq' have diverged,
and have 3 and 2 different commits each, respectively.
git push
To /Users/amira/projects/c14-lab/team-remote.git
! [rejected] add-faq -> add-faq (non-fast-forward)
error: failed to push some refs to '/Users/amira/projects/c14-lab/team-remote.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Cause: the remote still has the old commits, 6b53600 and 6c3828f. Her branch has the copies instead. A normal push must keep all the commits that are on the remote, so Git refuses it. Do not follow the hint here. A pull that merges would join the old commits and the copies, and each change would appear twice.

Fix: first, decide if other people use the branch.

  • If other people use the branch, or it is main: do not force the push. Ask the team. Usually you merge instead of rebase.

  • If only you use the branch: replace the remote branch with --force-with-lease:

    Terminal window
    git push --force-with-lease
    Output
    To /Users/amira/projects/c14-lab/team-remote.git
    + 6c3828f...68770b4 add-faq -> add-faq (forced update)

--force-with-lease replaces the remote branch only if it is still where you last fetched it. If a teammate pushed to the branch since then, Git refuses, and their commit is safe:

Output
To /Users/amira/projects/c14-lab/team-remote.git
! [rejected] add-faq -> add-faq (stale info)
error: failed to push some refs to '/Users/amira/projects/c14-lab/team-remote.git'

Then fetch, and look at their commit before you do anything else. Never use --force-with-lease or --force on main.

Check your understanding

Predict: Sam has add-faq in his clone, with 6b53600 and 6c3828f. Amira rebases add-faq and force-pushes it. Sam adds a commit to his add-faq and runs git push. What happens, and which rule did Amira break?

Show the answer

Sam’s push is rejected. The remote add-faq now has the rebased copies, and Sam’s branch still has the old commits, so his push is not a fast-forward. If he pulls and merges, his history contains both the old commits and the copies: the same changes twice. Amira broke the rule: do not rebase commits that other people already have.

Check your understanding

Completion check: explain what history each merge method preserves or rewrites. Use add-faq as the example.

Show the answer
  • Merge commit: preserves all of it. 6b53600 and 6c3828f become part of main with the same hashes, and a new merge commit with two parents records where the branch joined.
  • Squash: keeps the changes, but not the branch commits. main gets one new commit with one parent and one message. 6b53600 and 6c3828f and their separate messages are not in main.
  • Rebase: rewrites the branch commits. Git makes copies with the same changes and messages, but new hashes, on top of the newest main. Then main moves forward with a fast-forward, and no merge commit is made.

In all three cases, the files in main are the same.

Recap

  • A merge commit keeps every branch commit and its hash, and adds a commit with two parents.
  • A squash puts all the changes of a branch into one new commit. The branch commits do not become part of main.
  • A rebase copies the branch commits onto a new start. The copies have new hashes.
  • All three give the same files. They give different history.
  • Do not rebase commits that other people already have. Use --force-with-lease only on a branch that only you use, and never on main.
  • After a squash merge, git branch -d refuses. Check the content with git diff, then use -D.

This is the end of Module 3. You can now take a change through a pull request, review one, and choose how it goes into main. In Module 4, two people change the same line, and you resolve the conflict.

Next: Conflicts step by step