2022-05-24

Git Fast-Forward

Fast-forward merges have properties that are tightly tied together (basically they are equivalent)...

  • A fast-forward merge is exactly the sort of merge that doesn't create a merge commit (assuming that you aren't doing destructive operations on commit history).
  • A fast-forward merge is exactly the sort of merge that just updates the branch pointer. (Remember that `git pull` first does a `fetch` to get the remote commits before the merge happens, so a merge "just updating the branch pointer" can involve commits that didn't exist locally until the fetch.)
  • A merge is fast-forwardable exactly when the source commit history is a superset of the destination branch commit history.
  • A merge is fast-forwardable exactly when the destination commit is an ancestor of the source commit.

Example: The following picture shows a single repo.  On Monday, main branch pointed to commit C, thus having a commit history of  ABC (A is parent of B, B is parent of C).  Also on Monday, feature branch pointed to commit E, thus having a commit history of AB{C,D}E.  On Tuesday, a merge was done with source=feature and destination=main, resulting in main branch pointing to commit E, thus having commit history AB{C,D}E.  This merge on Tuesday was a fast-forward merge.  You might be curious about commit E.  Commit E is a merge commit created by a 3-way merge that happened before this story, but  main branch advancing from commit C to commit E is a fast-forward merge.

(Overly?) branch-oriented view of the repo over time.

The picture above is a bit weird in that it suggests that branches are independent creatures each with their own copy of commits, which is not the case for branches in the same repo.  If I were to alter the situation such that main was a local branch and that feature was a branch from some remote, then they truly are independent creatures that point to different copies of commits.

To make the fast-forward nature more obvious, the next picture shows the same situation more accurately (that branches are basically pointers to commits). The picture also shows the fast-forward nature of Tuesday's merge more clearly: main simply moved from C to E.  The Tuesday merge straightforwardly satisfies all the (equivalent) properties of a fast-forward merge listed at the top of this post, but perhaps seeing that the Tuesday merge simply moves a branch pointer from C to E will help people feel the fast-forwardness on a gut level.

More realistic and commit-graph-oriented view of the repo over time.

People like to talk about "linearity" when they discuss fast-forward merges, and seem to think that a nice always-one-child ("linear") commit history will stay "linear" if changes are brought in via fast-forward merges.  That is wrong.  I will try to rescue this paragraph from pedantry accusations by pointing out that this matters when people act on the mistaken idea that fast-forward merges will prevent "nonlinearity", and then are surprised and disappointed when a fast-forward merge is done that increases "nonlinearity".

Also, misconceptions about fast-forward merges makes it hard to discuss one of the true benefits of fast-forward merges: they will NOT create a never-before-seen repo state.  If the source of a fast-forward merge was tested/verified/buildable/whatever, then the end result of that fast-forward merge is content-identical and can also be considered tested/verified/buildable/whatever.  This desirable property of fast-forward merges does not hold for 3-way merges.  3-way merges can create a never-before-seen repo state that can contain bugs/build-errors/whatever that were not present in the parents of that merge.

Support from https://git-scm.com/docs on the properties of fast-forward merges...

  • https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddeffastforwardafast-forward
    • "A fast-forward is a special type of merge where you have a revision and you are "merging" another branch's changes that happen to be a descendant of what you have. In such a case, you do not make a new merge commit but instead just update your branch to point at the same revision as the branch you are merging. This will happen frequently on a remote-tracking branch of a remote repository."
  • https://git-scm.com/docs/git-push#_note_about_fast_forwards
    • "When an update changes a branch (or more in general, a ref) that used to point at commit A to point at another commit B, it is called a fast-forward update if and only if B is a descendant of A."
      • Other phrasing: merging two branches is fast-forwardable if the tip commit of destination branch is an ancestor of the tip commit of source branch
    • "In a fast-forward update from A to B, the set of commits that the original commit A built on top of is a subset of the commits the new commit B builds on top of."
      • Other phrasing: a merge is fast-forwardable if destination branch commits are a subset of source branch commits
  •  https://git-scm.com/docs/git-merge#Documentation/git-merge.txt---commit
    • "Note that fast-forward updates do not create a merge commit"
      • There is such thing as destructive updates, so a compatible rephrasing might be: an update is a fast-forward update if-and-only-if it is a non-destructive update that did not create a merge commit.
  • https://git-scm.com/docs/git-merge#Documentation/git-merge.txt---ff
    • "With --ff, when possible resolve the merge as a fast-forward (only update the branch pointer to match the merged branch; do not create a merge commit). When not possible (when the merged-in history is not a descendant of the current history), create a merge commit."
      • Notice how a fast-forward merge will "only update the branch pointer" (remember that `git pull` does a fetch to bring in commits before the merge) and "do not create a merge commit" go together conceptually.

No comments:

Post a Comment