Merge strategies
- Lesson 9
- 40 min
- Module 3: Collaborate through reviews
Lesson 9 of 15 · Git and Team Development EssentialsMerge strategies
1. Understand a repository
2. Work with branches and remotes
3. Collaborate through reviews
4. Resolve and recover
5. Release and hotfix practice
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 ready
What you need to know
- How to review and merge a pull request, from Pull-request sequence: 8. Merge and Read and review diffs: Two dots or three dots.
- What a merge commit with two parents is, from Synchronize deliberately: Both people commit at the same time.
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 statusshowsOn branch mainandworking tree clean. The branchadd-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.
One branch, three ways into main
Section titled “One branch, three ways into main”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:
git log --oneline --graph main add-faq* 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 103main 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
mainand the newest commit of the branch. The branch commits keep their hashes and become part ofmain. You did this withgit merge --no-ffin 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
mainmoves forward to the last copy, with a fast-forward.
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.
1. Merge commit
Section titled “1. Merge commit”She makes a copy of main called try-merge, and merges add-faq into it:
git switch -c try-merge maingit merge --no-ff --no-edit add-faqgit log --oneline --graph -5Switched 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 103git 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.
2. Squash
Section titled “2. Squash”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:
git switch -c try-squash maingit merge --squash add-faqgit status --shortSwitched to a new branch 'try-squash'Automatic merge went well; stopped before committing as requestedSquash commit -- not updating HEADA faq.txtShe commits the changes as one commit, with a message for the whole PR:
git commit -m "Add an FAQ with two questions"git log --oneline --graph -3[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 103The 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.
3. Rebase
Section titled “3. Rebase”First, Amira writes down the hashes of the branch commits:
git log --oneline main..add-faq6c3828f Add a network question to the FAQ6b53600 Add FAQShe 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:
git switch -c try-rebase add-faqgit rebase maingit log --oneline --graph -4Switched 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 103The 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:
git log --oneline main..add-faqgit log --oneline main..try-rebase6c3828f Add a network question to the FAQ6b53600 Add FAQda418c8 Add a network question to the FAQc8c08d3 Add FAQTo 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.
git switch -c try-ff maingit merge --ff-only try-rebaseSwitched to a new branch 'try-ff'Updating 2e9e37c..da418c8Fast-forward faq.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 faq.txtThe fast-forward worked, because after the rebase the copies start from the newest commit of main. No merge commit was made.
4. Same files, different history
Section titled “4. Same files, different history”All three results contain the same files. git diff --stat between them prints nothing:
git diff --stat try-merge try-squashgit diff --stat try-merge try-ffNo output means no difference. Only the history is different.
5. Delete the throwaway branches
Section titled “5. Delete the throwaway branches”Amira switches back to main. git branch -d refuses to delete try-merge, because its merge commit is not in main:
git switch maingit branch -d try-mergeSwitched to branch 'main'Your branch is up to date with 'origin/main'.error: the branch 'try-merge' is not fully mergedhint: 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:
git branch -D try-merge try-squash try-rebase try-ffDeleted 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.
Compare the three methods
Section titled “Compare the three methods”| 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.
-
In
~/projects/c14-lab/ticket-notes, rungit status. The output showsOn branch mainandworking tree clean. -
Run
git log --oneline main..add-faq. Write down the two hashes:6c3828fand6b53600. -
Try a merge commit:
Terminal window git switch -c try-merge maingit merge --no-ff --no-edit add-faq -
Run
git log --oneline --graph -5. The newest commit has two parents, and6b53600and6c3828fare in the graph. -
Try a squash:
Terminal window git switch -c try-squash maingit merge --squash add-faqgit commit -m "Add an FAQ with two questions" -
Run
git log --oneline --graph -3. The history is a straight line. The FAQ commits are not in it. -
Try a rebase on a copy of the branch:
Terminal window git switch -c try-rebase add-faqgit rebase mainThe output ends with
Successfully rebased and updated refs/heads/try-rebase. -
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. -
Fast-forward a copy of
mainto the rebased commits:Terminal window git switch -c try-ff maingit merge --ff-only try-rebaseThe output includes
Fast-forward. -
Compare the files of the results:
Terminal window git diff --stat try-merge try-squashgit diff --stat try-merge try-ffThere is no output: the files are the same.
-
Switch to
main, and delete the throwaway branches:Terminal window git switch maingit branch -D try-merge try-squash try-rebase try-ff -
Run
git log --oneline --graph --all -6.mainis still at2e9e37c, andadd-faqis still at6c3828f.
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:
- How many new commits does
mainget? - Do the hashes
e34484fand5a15d07appear in the history ofmain? - Can a reader of
mainstill 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
git switch -c try-merge maingit merge --no-ff --no-edit origin/add-ticket-104git log --oneline main..try-merge5c41513 Merge remote-tracking branch 'origin/add-ticket-104' into try-merge5a15d07 Improve README wordinge34484f Add ticket 104git switch -c try-squash maingit merge --squash origin/add-ticket-104git commit -m "Add ticket 104"git log --oneline main..try-squashgit show --stat HEAD37a47a9 Add ticket 104 README.md | 2 +- scratch.txt | 2 ++ tickets.txt | 1 + 3 files changed, 4 insertions(+), 1 deletion(-)git switch -c try-rebase origin/add-ticket-104git rebase maingit log --oneline main..try-rebase9586a0f Improve README wording651a164 Add ticket 104Then 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:
git branch -d add-faqerror: the branch 'add-faq' is not fully mergedhint: 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:
-
Check that the content is in
main. Compare the file on both branches:Terminal window git diff add-faq main -- faq.txtNo output means that
faq.txtis the same on both branches. Compare only the files that the branch changed.maincan have other new changes that the branch does not have. -
If there is no output, delete the branch with
git branch -D add-faq. -
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:
On branch add-faqYour branch and 'origin/add-faq' have diverged,and have 3 and 2 different commits each, respectively.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 behindhint: 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-leaseOutput 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:
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.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
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.
6b53600and6c3828fbecome part ofmainwith 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.
maingets one new commit with one parent and one message.6b53600and6c3828fand their separate messages are not inmain. - Rebase: rewrites the branch commits. Git makes copies with the same changes and messages, but new hashes, on top of the newest
main. Thenmainmoves forward with a fast-forward, and no merge commit is made.
In all three cases, the files in main are the same.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
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-leaseonly on a branch that only you use, and never onmain. - After a squash merge,
git branch -drefuses. Check the content withgit 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