Skip to content

14.1.2 — Merging, Rebasing and Getting Out of Trouble

Everything here is the model from Chapter 14.1.1 applied: objects are immutable, a branch is a pointer, and the reflog remembers where the pointer was. Once those three hold, the commands stop needing memorisation.

1. Merging

A three-way merge uses three commits: the tip of each branch, and the merge base — their most recent common ancestor.

        A---B---C   feature
       /
  D---E---F---G     main

     merge base

For each file, Git compares both tips against the base. Changed on one side only — take that change. Changed on both sides in different places — take both. Changed on both sides in the same place — conflict.

A fast-forward happens when one branch is strictly ahead: nothing to merge, so Git moves the pointer. No merge commit exists, so the branch leaves no trace in history — which is sometimes what you want and sometimes loses the information that a set of commits belonged together. --no-ff forces a merge commit.

The strategy, and what ort is. Git's default merge strategy is ort — "Ostensibly Recursive's Twin" — which replaced recursive as the default in Git 2.34 (2021). It is a rewrite rather than a new algorithm:

  • Much faster, especially on large repositories and on rebases with many commits, partly because it can merge without touching the working directory.
  • Better rename handling. Because renames are detected rather than recorded (Chapter 14.1.1), a rename on one side and an edit on the other is exactly the case that used to go wrong.
  • Fewer edge-case bugs, particularly around directory renames and file-to-directory changes.

You will rarely name it explicitly, and knowing what it is answers the question when it appears in output. The other strategies: ours (take our side entirely — useful for recording that a branch was considered and rejected) and octopus (several branches at once, and it refuses conflicts).

Resolving a conflict:

<<<<<<< HEAD
const timeout = 5000;
||||||| merge base                 ← only with conflictstyle = zdiff3
const timeout = 3000;
=======
const timeout = 10000;
>>>>>>> feature

The middle section is the reason to set zdiff3 (Chapter 14.1.1). Without it you see two versions and must guess who changed what; with it you see the original, so "they raised it from 3000, we raised it further" is immediately visible and the correct resolution is usually obvious.

Then: edit, remove the markers, git add, git commit. git merge --abort returns to before you started. And enable rerere so a conflict resolved once is replayed on the next rebase of the same branch.

2. Rebasing

Rebase replays your commits onto a new base, creating new commits with new hashes — the originals are unchanged and simply unreferenced.

before:  A---B---C   feature          after:          A'--B'--C'   feature
        /                                            /
   D---E---F---G  main                     D---E---F---G  main

Read what that means: C and C' have the same content and different identities. Nothing was modified; new objects were written and the branch pointer moved.

Interactive rebase is where the real value is:

bash
git rebase -i main
pick   a1b2c3  Add refund endpoint
squash d4e5f6  Fix typo                   # merge into the previous commit
fixup  789abc  Fix typo again             # same, but discard this message
reword 0f1e2d  Add refund handling        # change the message
edit   3c4d5e  Add validation             # stop here to amend the commit
drop   6f7a8b  Debug logging              # remove it

Reordering the lines reorders the commits.

--autosquash makes this routine. Commit a fix as git commit --fixup <hash>, and git rebase -i --autosquash places it under its target already marked fixup. With rebase.autosquash = true set, cleaning a branch before review is two commands.

The golden rule, and its real boundary. "Never rebase public history" is usually stated too broadly. The accurate version: rebasing rewrites hashes, so anyone who has those commits now has a divergent history.

  • Your own feature branch, before or during review — rebase freely. Force-push with --force-with-lease, which refuses if the remote moved since you last fetched, so you cannot overwrite a colleague's push.
  • A shared main branch — never. Everyone's history diverges and the recovery is manual for each person.
  • A branch someone else is actively working on — coordinate first.

3. Merge, rebase or squash

HistoryBisectConflictsTrace to PR
Merge commitExact, branchyGoodOnceYes
RebaseLinearBestPossibly per commitWeaker
Squash mergeOne commit per PRCoarserOnceExcellent

Merge preserves what happened, including the branching. Main becomes hard to read on a busy repository.

Rebase gives a linear history that reads as a sequence of intentional changes, and it is the best case for bisect (section 6) because every commit is a real state. The cost: conflicts may recur per commit, and the shape of the original work is lost.

Squash merge collapses a pull request into one commit. Excellent when branches are short-lived and each pull request is one logical change, which is exactly trunk-based development (Chapter 13.7). Its costs are real: a 40-file commit is harder to bisect into, and authorship of individual commits is folded away.

The pragmatic default most teams land on: rebase your branch onto main to keep it current, then squash-merge the pull request. Linear history, one commit per reviewed change, and no merge-commit noise.

4. Undoing things

revert is the safe one. It creates a new commit that undoes an earlier one. History is unchanged, so it is the only correct choice for anything already pushed.

bash
git revert <hash>
git revert -m 1 <merge-hash>     # (1)

(1) Reverting a merge needs -m to say which parent is "mainline" — usually 1, the branch you merged into. And there is a trap worth knowing: after reverting a merge, re-merging that branch brings in nothing, because Git considers those commits already merged. You must revert the revert before merging again. This surprises people badly during a release rollback.

reset moves the branch pointer, per Chapter 14.1.1's three modes. Fine locally, and a rewrite if the commits were pushed.

amend rewrites the last commit — new hash, so the same push rules apply.

cherry-pick copies a commit onto the current branch, creating a new commit with the same change. Right for backporting a fix to a release branch, and -x records the original hash in the message, which is what lets someone later see where it came from.

5. The everyday operations

bash
# Branches
git switch -c feature/refunds         # create and switch   (modern; checkout -b still works)
git switch -                          # previous branch
git branch -m old new                 # rename locally
git push origin :old new              # delete old on the remote, push new
git branch -d done                    # delete merged
git branch -D abandoned               # delete regardless   ← reflog still has it

# Files
git restore file.js                   # discard working changes   (modern; checkout -- )
git restore --staged file.js          # unstage, keep changes
git restore --source=HEAD~2 file.js   # this file as it was two commits ago

# Stash
git stash push -u -m "wip refunds"    # -u includes untracked files
git stash list
git stash apply stash@{0}             # keep it in the list
git stash pop                         # apply and drop

# Remotes
git fetch --all --prune
git remote -v
git push -u origin HEAD               # push current branch and set upstream
git push --force-with-lease           # the only acceptable force

# Finding things
git log -S "chargeback" --oneline     # commits that added or removed this string
git log -L 40,60:server.js            # the history of these lines
git blame -w -C server.js             # ignore whitespace, follow moved code
git worktree add ../hotfix main       # a second working directory, same repository

Four of these deserve a note.

switch and restore exist because checkout did too many things — changing branches and discarding file changes are unrelated operations with very different consequences. The newer commands separate them, and the split is worth adopting.

stash -u matters because a plain stash leaves untracked files behind, which is how a "clean" stash still breaks the build after switching branches.

git log -S (the pickaxe) is the underused one. "When did this string appear or disappear" answers most archaeology questions in seconds — far better than scrolling a log.

git worktree gives a second checked-out directory sharing one repository, so an urgent fix does not require stashing everything and switching branches. It is the correct answer to "I need to look at main without losing my place."

6. bisect

A binary search for the commit that introduced a bug — and the most powerful debugging tool in Git.

bash
git bisect start
git bisect bad                    # current commit is broken
git bisect good v1.4.0            # this release was fine
# Git checks out a midpoint; test it; then:
git bisect good     |     git bisect bad
# … about log₂(n) steps …
git bisect reset

Over a thousand commits that is about ten tests.

And it automates:

bash
git bisect run ./check.sh         # exit 0 = good, non-zero = bad

Write the check as a script and walk away. This is the single highest-leverage Git command for a regression whose cause is not obvious, and it works on performance regressions too — make the script assert a threshold. Chapter 14.6 places it in the wider debugging method.

Its one prerequisite: commits must individually build and run. That is a real argument for the linear, atomic history that rebasing produces.

7. When it has gone wrong

SituationFix
Bad reset --hardgit reflog, then git reset --hard HEAD@{n}
Deleted a branchgit reflog, then git branch name <hash>
Committed to the wrong branchgit reset --soft HEAD~1, switch, commit
Committed a secretRevoke it first (Chapter 8.6.1), then rewrite
Rebase went wronggit rebase --abort, or reflog to before it
Pushed a bad commitgit revert — do not rewrite shared history
Need to remove a file from all historygit filter-repo, then everyone re-clones
Lost commits, no reflog entrygit fsck --lost-found
Detached HEAD with commits on itgit branch rescue <hash> before switching away
Merge conflict chaosgit merge --abort and start again

The two rules that cover almost every case: reflog first, and revert rather than rewrite for anything pushed.

8. Commit hygiene

Atomic commits — one logical change each. Not "morning's work", and not a commit that cannot build. This is what makes review, revert and bisect all work.

Messages that help. A short imperative subject under about 50 characters, a blank line, then why — the code already says what.

Reject refunds outside the 30-day window

The refund endpoint accepted any order id, so support could
issue refunds on year-old orders by pasting a stale link.

Fixes #482

Conventional commitsfeat:, fix:, docs:, refactor:, chore:, with ! or a BREAKING CHANGE footer — are a machine-readable convention that lets tooling derive versions and changelogs automatically. Adopt it if you want that automation; it is overhead otherwise, and a team that adopts the format without the tooling has taken the cost and left the benefit.

And the point behind all of it: you write history for the person debugging at 3am, who is often you. git blame leading to a commit that says "fix stuff" is a dead end; one that explains the incident is an answer.

9. Workflows and large repositories

Trunk-based — short branches, merged within a day or two, feature flags for incomplete work. The right default, and Chapter 13.7 makes the delivery case.

GitHub flow — branch, pull request, review, merge, deploy. Trunk-based with a review step, and what most teams actually run.

GitFlowdevelop, release/*, hotfix/*. Designed for versioned software with scheduled releases, and a poor fit for continuous deployment, where it produces large risky merges.

For large repositories, three features matter:

  • --depth 1 — shallow clone, the right default in CI.
  • Partial clone (--filter=blob:none) — fetch file contents on demand, which makes a huge repository usable interactively.
  • Sparse checkout — check out only some directories, which is how monorepos stay workable.

Recall

  • A three-way merge compares both tips against the merge base; a conflict is only when both sides changed the same place. ort is the default strategy since Git 2.34 — faster, and notably better at rename-plus-edit cases.
  • Set conflictstyle = zdiff3 so a conflict shows the original text too, and rerere so a conflict resolved once replays on later rebases.
  • Rebase writes new commits with new hashes. Rebase your own branch freely and force-push with --force-with-lease; never rebase a shared main. Interactive rebase plus commit --fixup and --autosquash is how a branch gets cleaned before review.
  • Rebase onto main, then squash-merge the pull request is the common default: linear history, one commit per reviewed change.
  • revert for anything pushed. Reverting a merge needs -m 1, and re-merging that branch later requires reverting the revert — a real release-rollback trap.
  • switch and restore split what checkout conflated. stash -u includes untracked files. git log -S answers "when did this string appear" in seconds. git worktree gives a second directory instead of stashing everything.
  • git bisect run ./check.sh finds a regression in about log₂(n) tests, unattended — and it works for performance regressions. Its prerequisite is that every commit builds.
  • Reflog first; revert rather than rewrite for anything pushed. A committed secret is revoked before it is rewritten. Atomic commits with messages that explain why are what make review, revert and bisect all work.

Self-test: What are the three commits in a three-way merge? · What does zdiff3 add and why does it help? · Why is --force-with-lease safe where --force is not? · What must you do before re-merging a branch whose merge was reverted? · Which command finds when a string was introduced? · What does bisect require of your commit history?

Next: 14.2 covers the rest of the toolbox — how command-line tools work internally and how to build one worth using, diagrams as code, and the automation that removes the tasks nobody should be doing by hand.