Visual walkthrough — Git operations — branch, merge, rebase, cherry-pick, stash, bisect
We only need one starting fact, and we will draw it first: a Git history is a Directed Acyclic Graph of commits. Let us make sure those five words mean something concrete before we do anything with them.
Step 1 — What a "commit" and a "graph" actually are
WHAT. A commit is a photograph of your whole project at one instant, plus a note saying "the photo taken just before me was that one". That backward-pointing note is called a parent link. Draw commits as circles and parent links as arrows pointing backward in time.
WHY draw arrows backward? Because a commit can never know its future — it is frozen the moment it is created. It can only remember its past. So every arrow points from a newer commit to an older one. This is exactly what "Directed" means: arrows have a direction. "Acyclic" means you can never follow arrows and loop back to where you started (you can't be your own ancestor). "Graph" just means dots-and-arrows.
PICTURE. Four commits . Each circle is a snapshot; each amber arrow is a parent link. A branch is the cyan sticky-note — just a label — parked on the newest commit.

Everything below is built only from circles, backward-arrows, and sticky-notes.
Step 2 — What "two branches diverged" looks like
WHAT. Two sticky-notes start on the same commit, then each grows its own new commits. Call the notes main and feature.
WHY this is the whole problem. The moment the two chains share a common start but then differ, there is no single "latest" photo any more — there are two. Any tool that wants to combine them must first answer: where did they last agree? That shared commit is the object we will lean on for the rest of the derivation.
PICTURE. From commit (the fork point), main grows and feature grows . Follow both chains backward with your finger: they meet at .

Step 3 — The three photos we compare (setting up the merge rule)
WHAT. Zoom into one single line of one file — say line 10 of app.js. That one line has three versions we care about:
- = the line at the merge base (what both sides started from).
- = the line on our branch (
main). O for "ours". - = the line on their branch (
feature). T for "theirs".
WHY only these three? Because a merge asks a purely local question for every line: "given where we both started (), and where each of us ended ( and ), what is the correct combined line?" No other information can help — history further back is already summarised inside .
WHY do we need at all — why not just compare and ? This is the key insight. If we only saw , we could not tell who changed it. Maybe I changed it and you left it alone — then my version wins. Maybe you changed it and I left it alone — then your version wins. The base is the referee that reveals who moved. This is why the algorithm is called a three-way merge: it needs three photos, not two.
PICTURE. Three stacked cards labelled , , , each showing the text of line 10.

Step 4 — Deriving the merge rule by exhausting every case
WHAT. We now decide the output for every possible combination of "did we move?" and "did they move?". "We moved" simply means . "They moved" means . Two yes/no questions give exactly four boxes — and we will fill in all four so no scenario can surprise you.
WHY a truth table. Because "did we move? / did they move?" are the only two facts that exist, listing their 2×2 combinations guarantees completeness. This is the derivation: not a guess, but the only four possibilities.
PICTURE. A 2×2 grid. Rows = "we changed the line? (yes/no)". Columns = "they changed the line? (yes/no)". Each cell is filled with the correct output and colour-coded (cyan = automatic, amber = conflict).

Reading the four boxes one at a time:
- Neither moved ( and ): the line never changed. Output . Trivial.
- Only they moved (, ): I left it alone, they edited it → take theirs, output .
- Only we moved (, ): they left it alone, I edited it → take ours, output .
- Both moved ( and ):
- If we happened to make the identical edit (): no disagreement, output .
- If we made different edits (): Git has no rule to pick a winner → CONFLICT.
Step 5 — Fast-forward: the degenerate merge (a case we must not skip)
WHAT. Suppose only one side moved at all — feature grew new commits but main stayed exactly on the fork point . Then there is nothing to reconcile: main's latest commit is the merge base.
WHY this is a separate case. In the grid of Step 4, every line would fall into row "only they changed" (or "neither"). There is no divergence, so building a new merge commit would be pointless ceremony. Git instead just slides the main sticky-note forward onto feature's tip. This is a fast-forward — it is a merge that creates zero new commits.
PICTURE. main on ; feature on down a straight chain . The merge simply peels the main note off and re-parks it on (dashed amber arrow = the pointer moving).

Step 6 — Rebase: the same commits, re-photographed on a new base
WHAT. A merge records that two lines came together. Rebase does the opposite — it rewrites history to pretend the divergence never happened. It lifts your feature commits off the old base and re-applies each one as a brand-new commit on top of main's tip.
WHY new commits (new names)? A commit's identity is computed from its content and its parent. When we replant so its parent is now (instead of ), its parent changed → it is a different object → it gets a new SHA. So , . Same changes, new identities, producing one straight line.
HOW (the four internal steps): ① find merge base . ② save the diff of each feature commit after . ③ move the feature note to main's tip. ④ replay each saved diff in order as
PICTURE. Left half: the diverged shape from Step 2. Right half: feature's commits transplanted onto the tip of main as a single straight cyan line; the old ghosted out, new solid.

Step 7 — Bisect: why tests, derived by halving
WHAT. Somewhere along a straight chain of commits, a bug appeared. Every commit before the culprit is good; the culprit and everything after it is bad. We want the first bad commit. Testing all commits one by one is wasteful — binary search does far better.
WHY binary search applies here. Binary search needs one thing: a list that is sorted / monotone. Our commits are monotone in exactly the right way — once broken, it stays broken going forward. So the chain looks like good good good ... good BAD BAD ... BAD, a single flip-point. That flip is precisely what binary search hunts.
HOW. Mark one endpoint good, the other bad. Git checks out the midpoint. You test it:
- midpoint good → culprit is in the newer half → discard the older half.
- midpoint bad → culprit is in the older half → discard the newer half.
Either answer throws away half the suspects.
WHY the count is . Start with suspects. Each test halves the range: Ask: how many halvings turn into ? That means , i.e. , i.e. . Since must be a whole number of tests we round up: .
PICTURE. A bar of commits (green good on the left, red bad on the right). Test 1 halves 8→4, test 2 halves 4→2, test 3 halves 2→1, arriving at the first red commit — 3 tests for 8 commits, and . ✓

The one-picture summary
Below, all three results share one diagram. Read left-to-right: two branches diverge from base (Step 2). Merge joins them with a two-parent commit (Steps 3–4). Rebase instead straightens them into one line of new commits (Step 6). And along any straight chain, bisect finds the flip-point in halvings (Step 7). One base, three destinies.

Recall Feynman: the whole walkthrough in plain words
Picture your project as a line of photos, each remembering the one before it. Two friends grab the same photo and each add their own photos — now the line splits into a Y. The last photo they shared is the base, the referee. To combine their work, Git looks at each line of the file three ways — how it looked at the base, how you left it, how they left it — and asks two questions: did you touch it? did they touch it? If only one of you touched it, that person's version wins automatically. If you both touched the same line but differently, Git can't guess, so it hands it back to you — that's a conflict, and it's the only case that ever causes one. If only one side added photos at all, Git doesn't even build a joining photo; it just slides the sticky-note forward (fast-forward). Rebase is the tidy friend: instead of a Y, they peel off their photos and re-take them on top of yours so it looks like one straight line — but they're new photos with new names, so never do it to photos you've already shared. Finally, if a bug crept in somewhere along a straight line, don't check every photo — check the middle one. Good? The bug is in the newer half. Bad? It's in the older half. Each check throws away half, so a thousand photos take only about ten checks. That "keep halving" trick is why it's logarithmic.
Recall Quick self-test
A conflict happens in exactly which case of the three-way merge? ::: When AND AND — both sides changed the same region differently. Why does rebase produce new SHAs? ::: A commit's identity depends on its parent; rebase changes each commit's parent, so each becomes a new object. How many bisect tests for 1024 suspect commits? ::: . What is a fast-forward merge, in one line? ::: Moving a branch pointer forward with no new commit, because the target is a direct ancestor (no divergence).
Prerequisites & links: Directed Acyclic Graph · Three-way merge algorithm · Binary Search · Diff and patch (Myers algorithm) · Immutability and content-addressable storage · Conflict resolution · Trunk-based vs GitFlow · back to parent topic.