Git from First PrinciplesPost 6 of 16 · 16 of 16 written

6. Branching and integration: merge, rebase, cherry-pick

Git · 6 of 165 min readOct 2026

Integration answers one question: two lines of history split from a common ancestor, so how do you get one line containing both? Git gives two answers. Merge records the join; rebase replays one line on top of the other.

Start from the merge base

Every integration begins by finding the merge base, the best common ancestor of the two tips. Everything after it on each side is “their changes” and “your changes”.

git merge-base main feature    # the ancestor's hash
git log main..feature          # your side's commits
git log feature..main          # their side's commits

Fast-forward: nothing to combine

If main has not moved since feature split off, main is an ancestor of feature. Git simply slides the main pointer forward to feature’s commit. No new commit is created.

git merge --ff-only feature refuses unless a fast-forward is possible. It is a safe default whenever you only expect to catch up.

Three-way merge: both sides moved

When both sides have new commits, Git compares three snapshots: the base, yours (“ours”), and theirs. For each region of each file:

  • Changed only on one side: take that side.
  • Changed identically on both sides: take it once.
  • Changed differently on both sides in overlapping lines: conflict.

Why three versions and not two? With only the two tips, Git cannot tell who changed a line and who left it alone. The base answers that question. Git records the result as a merge commit with two parents, yours first, which is why HEAD^2 exists.

Conflicts, from first principles

A conflict means both sides changed the same region differently, and Git refuses to guess. It does three things, then stops:

  1. Writes the file to your working tree with conflict markers.
  2. Stores three versions in the index: stage 1 is the base, stage 2 is ours, stage 3 is theirs (git ls-files -u shows them).
  3. Marks the merge as in progress until you finish or abort.

Set this once; it adds the base to the markers and turns guessing into reasoning:

git config --global merge.conflictStyle zdiff3
<<<<<<< HEAD
lr = 3e-4
||||||| base
lr = 1e-3
=======
lr = 5e-4
>>>>>>> feature

Now you can see that both sides lowered the learning rate from 1e-3, to different values. The right answer is a decision, not a merge of text.

To resolve: run git status, edit each conflicted file into its correct final form and delete the markers, git add each file, then git commit (or git merge --continue). git merge --abort returns you to exactly where you were before. To take one side wholesale: git restore --ours <file> or --theirs.

Git merges text, not meaning. A merge with no conflicts can still be broken: one side renames a function while the other adds a call to the old name. Always run the tests after integrating.

Tip: git config --global rerere.enabled true makes Git remember how you resolved a conflict and reapply it if the same conflict returns. That helps when you rebase a long-lived branch repeatedly.

Rebase: replay your work on a new base

Rebase takes your commits since the merge base and replays them one by one on top of a new base. Each replayed commit is a new commit: same change, new parent, therefore new hash. Then your branch moves to the last new commit.

git switch feature
git rebase main

The result is linear history, as if you had started from today’s main. The old commits still exist, reachable through the reflog and ORIG_HEAD, until garbage collection.

Conflicts during a rebase arrive one replayed commit at a time. Resolve, git add, then git rebase --continue; or --skip that commit, or --abort everything. Warning: during a rebase, “ours” means the branch you are rebasing onto, and “theirs” means your own commit. It is reversed because rebase works by standing on main and cherry-picking your commits onto it.

Interactive rebase: edit your local history

git rebase -i HEAD~5 opens a to-do list of your last five commits. For each one you can pick, reword, edit, squash, fixup or drop it, and you can reorder the lines. This is how you turn a messy work session into clean, reviewable commits before sharing.

A useful pairing: commit a quick fix with git commit --fixup <sha>, later run git rebase -i --autosquash main, and Git folds each fix into its target automatically.

The golden rule of rewriting

Never rewrite commits that other people have already built on. Rebasing creates new hashes; your collaborators still hold the old ones. When they pull, Git sees two divergent histories carrying the same changes twice.

Rewriting your own unpushed commits, or your own feature branch that nobody else uses, is fine. After rewriting a branch you already pushed, push with --force-with-lease, never plain --force (Part 8).

[embedded content: the same diverged branches · before, after merge, after rebase]

Merge adds one commit and changes nothing that already exists. Rebase leaves the old D and E behind and builds new ones, which is why rebased commits get new hashes.

Merge or rebase?

Merge Rebase
History shape The true record, including where lines joined Linear, as if work happened in sequence
Existing commits Unchanged Replaced by new ones with new hashes
Conflicts Resolved once, in the merge commit Possibly once per replayed commit
Safe on shared branches Yes No, it rewrites history
Reading and bisecting Busier graph Cleaner and easier to follow

A common team pattern combines both. Rebase your own feature branch onto main to stay current; merge it into main through a pull request.

Squash merge

git merge --squash feature puts the combined change of the whole branch into the index as one change, without recording a merge. You then commit once. GitHub’s “Squash and merge” button does this.

main gets one tidy commit per feature, but the link to the branch’s own commits is lost. Git therefore does not know the branch was merged, so git branch -d refuses to delete it; use -D.

Cherry-pick

git cherry-pick <sha> takes the change one commit introduced (its diff against its parent) and applies it on top of HEAD as a new commit. Use it to carry a hotfix to a release branch, or to rescue one good commit from an abandoned experiment. A rebase is essentially an automated series of cherry-picks.

Check yourself

  • Why does Git need the base version to merge, and not just the two tips?
  • After git rebase main, why do your commits have new hashes when the changes are identical?
  • A merge reported no conflicts, yet the tests fail. How is that possible?