Merge, rebase, squash: combining work, and fixing conflicts
The four ways to bring a branch into main, the history each leaves behind, and when teams use each. How to read and resolve a conflict calmly, and copy one commit with cherry-pick.
By the end of this lesson you'll be able to
0 of 6 completeFour ways to combine a branch
Every method below ends with the same files on main. What differs is the history left behind,
and history is what you read when something breaks:
| Method | History on main | Use when |
|---|---|---|
| Merge commit | every branch commit, plus a merge commit | you want the true record of how work happened |
| Rebase, then fast-forward | branch commits, replayed in a straight line | each commit is meaningful and you want a linear log |
| Squash | one commit per branch | branches are messy; main should read one change per pull request |
| Fast-forward | branch commits, no extra commit | main hadn't moved |
Fast-forward or merge commit
You saw both in the Branches lesson. Two options control them:
$ git merge --no-ff feature/x # always make a merge commit, even when a fast-forward is possible $ git merge --ff-only feature/x # only fast-forward; refuse if a merge commit would be needed
git merge --no-ff feature/xgitversion control
--no-ff keeps a visible "this feature was merged here" commit that you can revert as one unit.
--ff-only is a safe default for updating a local branch: it never creates surprise merge commits.
Rebase: replay your commits
git rebase main takes the commits on your branch that aren't on main, sets them aside, moves your
branch to the tip of main, and replays them one by one:
$ git log --oneline --graph --all * fbf0837 wip * 2022fd0 Return an empty list from search * 0d86374 Add search stub | * d45f583 Add VERSION |/ * 18269df Merge branch 'feature/discount' $ git rebase main Successfully rebased and updated refs/heads/feature/search. $ git log --oneline --graph -4 * 735332d wip * fa49403 Return an empty list from search * d9fd28b Add search stub * d45f583 Add VERSION
git log --oneline --graph --allgitversion controlloghistory
The branch now starts after Add VERSION, and merging it into main would be a fast-forward. Look at
the IDs: 0d86374 became d9fd28b. Rebase creates new commits. The old ones still exist for a
while, but your branch no longer points at them.
Squash: one commit per change
A branch often ends with commits like "wip", "fix typo", "address review". --squash brings in all
the changes but none of those commits, so you write one good commit:
$ git switch main $ git merge --squash feature/search Squash commit -- not updating HEAD search.py | 2 ++ 1 file changed, 2 insertions(+) $ git status --short # the changes are staged, nothing is committed yet A search.py $ git commit -m "Add search"
git switch maingitversion control
git status --shortstatuswhat changed
git commit -m "Add search"commitsave staged changes
On GitHub you rarely type this: the pull request's Squash and merge button does it. The cost is
that main loses the branch's individual commits, so git bisect (the History lesson) can only narrow
a bug down to the whole pull request. Small pull requests make that cost tiny.
Handling conflicts
A conflict happens when both sides changed the same lines and Git can't know which is right. It stops and asks you:
$ git merge feature/discount Auto-merging config.py CONFLICT (content): Merge conflict in config.py Automatic merge failed; fix conflicts and then commit the result. $ git status --short UU config.py $ cat config.py <<<<<<< HEAD PRICE = 120 ======= PRICE = 90 >>>>>>> feature/discount TAX = 0.18
git merge feature/discountgitversion control
git status --shortstatuswhat changed
cat config.pycatprint a file
Resolving is always the same four steps:
- Understand both changes. Here main raised the price and the branch lowered it. Ask whoever made them if the right answer isn't obvious; never just pick a side to make the conflict go away.
- Edit the file to what it should finally say, and delete all three marker lines.
- Mark it resolved:
git add config.py. - Finish:
git commitfor a merge,git rebase --continuefor a rebase.
$ git diff # during a conflict: both sides, side by side $ git merge --abort # changed your mind: back to before the merge $ git rebase --abort # same, for a rebase $ git checkout --ours config.py # take your side for this whole file $ git checkout --theirs config.py
git diffgitversion control
During a rebase "ours" and "theirs" swap places: Git is replaying your commits onto their
branch, so HEAD is main. It confuses everyone once.
Two settings make conflicts easier:
$ git config --global merge.conflictStyle zdiff3 # also show the original lines both sides changed $ git config --global rerere.enabled true # remember how you resolved a conflict and reuse it
git config --global merge.conflictStyle zdiff3gitversion controlconfigread or set settings--globalfor your user, in every repo
With zdiff3 a third block, starting ||||||| and the ID of the common ancestor commit, shows what the line was before either change: PRICE = 100. Seeing that both sides moved away from 100 makes the right answer much easier to work out. In VS
Code, the merge editor shows the same three panes.
Cherry-pick: one commit
cherry-pick copies the change from one commit onto your current branch, as a new commit. The classic
use: a fix landed on main, and a release branch needs only that fix:
$ git switch release/1.0 $ git cherry-pick 2cb5a0e [release/1.0 ec15333] Add search 1 file changed, 2 insertions(+) $ git cherry-pick -x 2cb5a0e # -x adds "(cherry picked from commit …)" to the message
git switch release/1.0gitversion control
Use it for single commits. If you find yourself cherry-picking ten commits, the branches are set up wrong; a merge is probably what you need.
Resolve a real conflict, then land a clean branch
Two branches change the same price on main. Merge them, resolve the conflict to the price the business agreed (110), and verify. Then rebase a second feature branch onto the updated main and squash it into a single commit.
main with the agreed price and one 'Add search' commitReveal one safe solution
$ git init shop && cd shop $ printf 'PRICE = 100\nTAX = 0.18\n' > config.py && git add . && git commit -m "Add pricing config" $ git switch -c feature/discount && sed -i 's/PRICE = 100/PRICE = 90/' config.py && git commit -am "Lower the price to 90" $ git switch main && sed -i 's/PRICE = 100/PRICE = 120/' config.py && git commit -am "Raise the price to 120" $ git merge feature/discount # CONFLICT $ printf 'PRICE = 110\nTAX = 0.18\n' > config.py # the agreed price, no markers $ git add config.py && git commit --no-edit $ git switch -c feature/search $ echo "def search(q): return []" > search.py && git add . && git commit -m "Add search stub" $ echo "# TODO: real search" >> search.py && git commit -am "wip" $ git rebase main # already on main's tip: nothing to replay over $ git switch main && git merge --squash feature/search && git commit -m "Add search" $ git branch -D feature/search feature/discount $ git log --oneline --graph * 728e61d Add search * 2278f46 Merge branch 'feature/discount' |\ | * 6286003 Lower the price to 90 * | f3a8a78 Raise the price to 120 |/ * 48f3407 Add pricing config $ grep -c '<<<<<<<' config.py # 0: no markers left behind 0
git init shop && cd shopgitversion controlinitcreate a repository here&&run the next command only if this one workedcdchange directory
printf 'PRICE = 100\nTAX = 0.18\n' > config.py && git add . && git commit -m "Add pricing config">write output to a file, replacing itaddstage changes.this foldercommitsave staged changes
echo "def search(q): return []" > search.py && git add . && git commit -m "Add search stub"echoprint text
echo "# TODO: real search" >> search.py && git commit -am "wip">>append output to a file
git log --oneline --graphloghistory
grep -c '<<<<<<<' config.pygrepfind lines that match a pattern-ccount matching lines
git branch -D is needed for feature/search because a squash merge copies the changes without
recording the branch's commits as merged. Your commit IDs will differ from these.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
When does git merge create a fast-forward instead of a merge commit?
Official reading for this lesson
Or get every quiz answer right and it completes itself.