Worked examples — Git operations — branch, merge, rebase, cherry-pick, stash, bisect
The scenario matrix
Every worked example below is tagged with the cell it covers. Think of each row as a "quadrant" of Git behaviour — just like signs of pick a quadrant for an angle.
| # | Case class | The deciding question | Degenerate / limiting edge |
|---|---|---|---|
| A | Fast-forward merge | Is the target a direct ancestor? (no divergence) | zero commits between = "already up to date" |
| B | Three-way merge, clean | Diverged, but different files/lines touched | both sides identical edit → auto-resolved |
| C | Three-way merge, CONFLICT | Diverged AND same region changed differently | one side deletes, other edits |
| D | Rebase (linearise) | Replay my commits on a new base | rebasing shared commits (forbidden) |
| E | Cherry-pick | Copy one commit's diff elsewhere | context differs → conflict |
| F | Stash | Shelve dirty tree to switch context | nothing to stash (clean tree) |
| G | Bisect | Binary search for first bad commit | , and the count |
| H | Word problem + exam twist | Combine several cells under time pressure | duplicate-change trap (pick then merge) |
Let me build the vocabulary once, then never assume it again.
Cell A — Fast-forward merge (no divergence)

- Locate the merge base . Since
featuregrew directly out of andmainnever moved, = the current tip ofmain. Why this step? Merge always starts by finding the last shared commit — that decides FF vs three-way. - Check ancestry. Is
main's tip an ancestor offeature's tip? Yes — look at the figure: is one straight chain. Why this step? If the target is a direct ancestor there is nothing to reconcile — no divergence means no new information to combine. - Slide the pointer. Git just moves
mainfrom forward to . No new commit is made. Why this step? Creating a merge commit would add a redundant node with only one meaningful parent — pointless.
Verify: commits on main after = commits before () plus = 5 commits, 0 merge commits. Number of new commits created by the merge = . ✓ (checked in VERIFY)
Cell B — Three-way merge, clean (diverged, no overlap)

- Find . The last commit both share (the fork point in the figure). Why this step? The Three-way merge algorithm compares and ; we need first.
- Apply the per-line rule for
style.css. Here (we changed it) but (they did not). Why this step? If only one side moved away from the base, that side's version is the unique answer. - Apply the rule for
app.js. Now , → result . Why this step? Symmetric to step 2 — the other side is the only one that changed. - Write the merge commit with two parents and . Why this step? Divergence really happened, so history records the join (unlike cell A).
Verify: each file falls in a single-changer branch of the piecewise rule, so CONFLICT is never reached. Files auto-merged = 2, conflicts = 0, merge commit parents = 2. ✓
Cell C — Three-way merge, CONFLICT (same region, both differ)
- Evaluate the rule. =
1, =2, =3. Test the CONFLICT clause: Why this step? A conflict is not random — it is exactly the cell where both sides moved the same region to different values. Git refuses to guess. - Read the markers. Git writes both versions between
<<<<<<</=======/>>>>>>>. Why this step? It hands you the raw data (Conflict resolution) so a human decides intent. - Resolve, then
git add app.js && git commit. Why this step? Staging tells Git "this region is settled"; the commit becomes the two-parent merge.
Verify: plug into the piecewise function → CONFLICT branch is the only true one. ✓
Cell D — Rebase to linearise (and the forbidden edge)

- Find merge base of
featureandmain(the fork commit). Why this step? Rebase only replays commits after the base — everything before is already shared. - Save the diffs of (patches relative to their parents). Why this step? Rebase re-applies changes, not whole snapshots — same idea as diffs.
- Move
featuretomain's tip , replay: , on top of . Why this step? This produces a linear history with new SHAs (new parents = new content-address, see Immutability and content-addressable storage).
Verify: commit count on feature stays (replayed 1-for-1); SHAs change because parent changed. New tip's ancestor count = base + main gain + feature commits = . If had 3 commits behind it (incl. itself), total = . ✓
Cell E — Cherry-pick one commit (and its context edge)
- Compute the patch . Why this step? Cherry-pick copies the diff of a single commit, nothing before it.
- Apply the patch on
main's tip, creating a new commit (new SHA). Why this step? Same change, different history location → content-address differs. - If surrounding code differs → conflict, resolve exactly like cell C.
Why this step? The patch's context lines may not match
main; then a human must place it.
Verify: commits copied = exactly ; main's commit count rises by . ✓
Cell F — Stash, including the empty (degenerate) case
git stashsaves dirty tree + index as a hidden commit on a stack, reverts to cleanHEAD. Why this step? Git blocks branch switches that would overwrite changes; stash = safe shelf.- Switch, fix, switch back, then
git stash pop=apply+drop. Why this step?popreapplies your work and removes the entry — one clean round-trip. - Degenerate case: if the tree was clean,
git stashreports "No local changes to save" and creates 0 stash entries. Why this step? There is literally nothing to shelve — the stack is untouched.
Verify: clean pop → stack goes entries. Empty-tree stash → stack stays at . ✓
Cell G — Bisect: binary search and the count

- Mark endpoints:
git bisect bad(now),git bisect good v1.0(known good). Why this step? Binary Search needs a range with a known-good low end and known-bad high end. - Git checks out the midpoint; you test and answer
good/bad. Each answer discards half the range. Why this step? Halving is why the cost is logarithmic, not linear. - Count the tests. Each test does . Halvings to reach 1: Why this step? , so 10 halvings cover 1000 commits.
- Degenerate : extra tests — the single suspect is the answer.
Verify: ; linear search would need up to . And . ✓
Cell H — Word problem + exam twist (combines cells)
- Cherry-pick → on
main(cell E). Ships only the fix, not the 40 commits. Why this step? You need the patch live without releasing unfinished work. - Later,
mainanddevhave diverged ( onmain; + 40 ondev) → three-way merge (cell B/C). Why this step? Both branches moved past their base → divergence → merge commit with 2 parents. - Twist resolved: the change from now exists as both and . Modern Git often patch-IDs them equal and skips a redraw, but if surrounding lines shifted you get a cell-C conflict on that region. Why this step? This is the duplicate-change trap — the reason cherry-pick + later merge is delicate.
Recall Which cell was each command?
cherry-pick ::: Cell E (copy one commit) merge dev into main ::: Cell B or C (three-way, divergence) the "appears twice" risk ::: the duplicate-change trap of Cell E
Verify: merge in step 2 has parents = ; patches carrying the same change = ( and ). ✓