skip to content

When can integrating a version-control branch just move a pointer instead of creating a two-parent commit?

level: middleimportance: should knowfreq 53%

answer

  1. Ask whether both lines actually moved
  2. One side must have stayed still
  3. The base equals the target's tip
  4. Nothing to combine, so nothing recorded
  5. Divergence forces a commit with two parents

basics

~20 s

Only when the target has recorded nothing since the two lines parted — its tip is an ancestor of the source's tip. Then there is nothing to combine and the name simply advances. If both lines moved, a commit with two parents is required.

solid answer

~50 s

The deciding question is whether the two lines have actually **diverged**. If the target's tip is an ancestor of the source's tip, then the merge base *is* the target's tip: one side changed nothing, so there is no content to reconcile and the target name can simply be advanced onto the source's tip. That is a **fast-forward** — it creates no commit at all and leaves a straight line, with no record in the graph that the work ever sat on a separate line. If instead each tip has commits the other cannot reach, the histories have genuinely diverged and the result must be recorded as a new commit with **two parents**, one on each line. That commit is the only place the graph says these two lines came back together, and it is what lets you later ask which commits arrived as one unit.

code

pseudocode · 11 lines
pseudocode
case 1 -- only the feature line moved
  integration line:  A -- B -- C
  feature line:      A -- B -- C -- D -- E
  C is an ancestor of E, so the base IS C.
  the integration name simply advances: C -> E. no commit written.

case 2 -- both lines moved
  integration line:  A -- B -- C -- F
  feature line:      A -- B -- C -- D -- E
  F cannot reach E and E cannot reach F. the base is C.
  a new commit M is written with two parents, F and E.

go deeper

for a junior

Know that integrating sometimes writes a commit and sometimes just moves a name, and that the name can only move when the other line has not advanced. That distinction alone is what is expected here.

for a middle

State the condition precisely — the target's tip is an ancestor of the source's tip, so the base is the target's tip and nothing needs combining. Then say what each option leaves behind in the graph.

for a senior

Argue the consequence rather than the mechanics: a junction is the only thing that later answers "which commits landed together", which is what reverting and reviewing by unit of work depend on.

for a principal

Own it as a readability and recoverability policy for the whole estate, and note that how often the cheap case is even available is a signal of how far lines are drifting before they are brought back.

## The condition, stated precisely Integration has exactly one cheap case and one general case, and the test between them is about **reachability**, not about how much changed. - **Cheap case:** the target's tip is reachable from the source's tip by walking parent links. Equivalently, the merge base of the two lines *is* the target's tip. The target has recorded nothing since the lines parted. - **General case:** each tip has at least one commit the other cannot reach. The lines have diverged, and no pointer move can express the result. In the cheap case there is nothing to decide. Base, yours and theirs collapse: your side equals the base in every region, so every region resolves to the other side. The correct result is already sitting in the source's tip, and integration reduces to updating one name — the **fast-forward**. No commit is written, no content is combined, and the operation costs what creating a branch costs. In the general case both sides moved, the three-way comparison actually has work to do, and the result is a state that does not exist anywhere in the graph yet. A new commit has to be written to hold it, and that commit records **both** tips as parents, because both are genuinely its ancestors. This is the only kind of commit with more than one parent, and it is what makes the history a graph rather than a list. ## What each leaves in the history | | Pointer advanced | Two-parent commit | |---|---|---| | New commit written | none | one | | Shape of the graph | straight line | a visible junction | | Record that a separate line existed | none | preserved | | "Which commits landed together?" | unanswerable | answerable at the junction | | Reverting the whole integration | pick the commits by hand | one commit to undo | | Reading the line as a story | uniform, undifferentiated | grouped by junction | The row that decides most arguments is the third. A fast-forward is not a *smaller* record of the same event; it is **no record of the event**. Afterwards, the graph cannot distinguish work that was developed on a separate line and brought back from work that was recorded directly on the target in the first place. That is sometimes exactly what you want — a one-commit fix should not leave a junction for the rest of time — and sometimes it destroys the grouping that makes the history navigable. ## Common misreadings 1. **"It fast-forwarded, so there were no changes."** Wrong: a fast-forward can bring in a hundred commits. What it means is that the *target* had no changes, not the source. 2. **"A two-parent commit means someone had to resolve something."** Wrong: divergence, not contested regions, is what forces the commit. Two lines can touch completely different files, resolve every region automatically, and still require a two-parent commit because both had moved. 3. **"Fast-forwarding loses commits."** Wrong: every commit on the source is preserved exactly as recorded. What is lost is the *grouping* — the marker saying these arrived as one unit. 4. **"The system decides which to do."** Half right. The system determines whether the cheap case is *possible*; whether to take it when it is possible is a team choice about the history they want to be able to read. ## Why the choice keeps coming up Because the two options answer different questions well. A straight line is easy to read linearly and easy to search commit by commit. A graph with junctions is easy to read *by unit of work*: you can point at one commit and say everything that came in with it. Teams that review and revert by unit of work want the junction. Teams that want the line to read as one continuous narrative prefer the pointer to advance. Neither is a correctness question, and an interviewer asking it is usually checking that you know the difference is about **what the history can later be asked**, not about which is tidier. One durable consequence is worth stating: the cheap case is not always available, and it becomes unavailable the moment anyone records anything on the target. A line of work that sat untouched for weeks will almost never be able to fast-forward, because the target moved underneath it. The availability of the cheap case is therefore a rough measure of how much the two lines have drifted — which is a fact about how the team works, not about the tooling.

  • A colleague says the integration fast-forwarded, so the branch contained nothing. Are they right?
    No. A fast-forward says the *target* recorded nothing since the lines parted, not the source. The source can carry any number of commits; they are all preserved and the name simply advances onto its tip. What the fast-forward tells you is that nothing had to be reconciled, because one side never moved.
  • Two lines touched entirely different files. Can that still require a commit with two parents?
    Yes. What forces the commit is divergence — each tip holding commits the other cannot reach — not whether any region was contested. Every region may resolve automatically and the combined state still exists nowhere in the graph, so a commit must be written to hold it, and it honestly has two ancestors.

saying these in an interview costs you the question

  • Thinks a fast-forward means the branch had no commits
  • Says a two-parent commit only happens after a contested region
  • Believes fast-forwarding discards the branch's commits
  • Cannot state the ancestor condition for the cheap case
  • Assumes the shape choice is purely cosmetic tidiness