⚉ DWIM Atom feed

DWIM — Trying to make the computer Do What I Mean

Fast-Forward and parent reversal

Consider a feature branch which has conflicts against the mainline branch. The developer merges from upstream in order to bring in those changes and resolve the conflicts; the branch is then pushed to the server, where the web interface offers a "merge" button for this branch.

If the mainline branch has not moved since the developer merged it into the feature branch, the server has two options to perform the merge: it can create a new commit on top of the mainlie branch in which it merges the feature branch into the mainline branch, or it can perform a fast-forward "merge", making the mainline branch point to the same commit as the feature branch.

These are the two modes of operation of the git merge command, and it's what is generally expected when a program or system implements merge functionality in git. However, the second option can cause issues in the scenario mentioned above.

The history fo the parents is significant during a merge. The first parent is the commit into which changes are being introduced. The second parent is the commit being brought in as a merge. So the Git system knows which direction merges went in. In this case we have a merge from the mainline into the feature branch.

By making the tip of the feature branch now also become the tip of the mainline branch, we would make this feature branch in essence the main branch. This means that the first parent of this commit is the tip of the feature branch, and the incoming changes are from the mainline branch. This also means that all those commits which were made on the mainline branch between the branch point and the fast-forward "merge" will now look like they've been done in a side branch (interestingly in one which bears the name of the mainline branch).

In the scenario above, after merging into the feature branch, the branches look like

-----x----x----x mainline
      \         \
       x--x---x--x feature

and if we perform a full merge, the history becomes

-----x----x----x---x mainline
      \         \ /
       x--x---x--x feature

and looking at the log from the mainline's point of view everything is as expected. If we however perform a fast-foward (what git merge would do by default) we get something different

------x--x---x---x mainline, feature
       \        /

By making the tip of the feature branch become the tip of the mainline, those three commits we saw in the mainline have now become part of a side of the history, and those commits building up the feature branch now form part of the mainline history.

A look though git log would usually show the same commits, as it orders primarily by time so you would only miss the extra commit made by the no-ff variant. However, if you look at git log --graph, you'll see that the mainline branch now has those small commits from the feature branch, instead of bringing in those changes.

If you use git log --first-parent to filter out side history and figure out what was done directly on the mainline (e.g. what was merged, what was a hotfix made directly there), you'll now see those commits from the feature branch as part of mainline's direct history, and it will hide those commits which were in fact made directly on the mainline and would have shown up as such before the fast-forward "merge".

Carlos Martín Nieto <cmn@dwim.me>