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.

A note on terminology.  I have looked for the most official term for merges that are not fast-forward merges; these are the classical merges that create a merge commit.  The closest I have found is "true merge" as a git-merge reference section header.  Some people use the term "3-way merge" for all merges that create a merge commit, but 3-way merges are an algorithm for producing merge output where you also look at the most recent common ancestor of the two things you are merging, thus the name having "3" in it.  True merges do not have to use the 3-way merge algorithm (see merge strategies).  Thus, I will be using "true merge" or "non-fast-forward merge" to refer to merges that create a merge commit.

Fast-Forward Merge 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 non-fast-forward 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 true merges (merges that create a merge commit).  True 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