Search code examples
gitgithubsystemconceptual

Why does git only allow fast-forward merges when pushing?


So I know that when you do a git push, git tries to integrate changes from your local branch to the corresponding remote branch. However, if the branches have diverged, git won't allow you to push, saying

hint: Updates were rejected because the remote contains work that you do not have locally.

When a user pushes successfully, he expects that the remote branch he just pushed mirrors his own. If we allowed 3 way merges when pushing (only if there are NO conflicts), then the remote branch obviously wouldn't match his own because it has commits our local copy doesn't know about, as well as a merge commit. However, couldn't git just do a pull after a push if it required a 3 way merge? What are the downsides to allowing a 3 way merge when pushing in this fashion, and why did the creators of Git decide to only allow fast forward commits?

Here's a diagram to illustrate the situation: https://i.sstatic.net/DDqWu.jpg


Solution

  • As you observe, the fact is that git push never does a merge. (Another way to put this—I think perhaps a better one—is that Git's "fast-forward merge" is not a merge at all.)

    As matt noted, when you use GitHub's Pull Request feature,1 GitHub makes available, to the person operating the target repository, a one-button-click method to really, actually do a merge using the commits you git push-ed, provided this merge can be done automatically. But git push never even checks to see if a merge is possible: it just says that a merge, or some other action, would be required, to add your new commits to the target repository without dropping some commit(s) from the target branch.

    The definition of fast-forward is:2

    • Let Cold be the commit hash ID currently identified by name N.
    • Let Cnew be the commit hash ID you propose N should identify.
    • The operation is a fast forward if and only if ColdCnew.

    That funny symbol ≼ (Unicode PRECEDES OR EQUAL TO, U+227c) that looks like a curvy less-than-or-equal-to is what the Git command git merge-base --is-ancestor tests, i.e., that the old commit's hash ID is a predecessor of, or is the same commit as, the new commit's. To put it yet another way, the old commit will still be reachable from the new commit: see Think Like (a) Git.

    Merge commits have the peculiar property of having more than one parent commit hash ID, making more than one sub-graph of the overall commit DAG reachable. Hence a simple way to keep some old sub-graph reachable while adding a new graph fragment is to add a merge commit. The git merge command can add such a commit, but in the process, it does something arguably useful with the three commit snapshots that are inputs to the merge.

    As bk2204 noted, git push could detect the push failure and automatically invoke git fetch and git merge on your end. You could write a script yourself to do the same thing. But this isn't always the right thing to do, so it would need yet more configuration knobs. I myself hold the opinion that git pull, which runs fetch and then merge for you, already does too much, and that such a control knob for git push would be far too much.

    Anyway, you could write this yourself. (It would perhaps help if git push reserved a particular exit status for "rejected due to non-fast-forward", perhaps, but there's yet another problem here: you can git push multiple names in one go, and can have some succeed and others fail, each for different reasons.) Feel free to write it yourself—Git is, after all, a big box of tools, rather than one specific solution—but remember that some merges fail with merge conflicts. Your command would need to limit the internal push step to push only the current branch.


    1This really is a GitHub feature, not part of base Git. Other web-hosting sites do provide a feature with the same name and very similar operation. Obviously there's a strong desire for it. But Git itself still does not have it, for at least technical reasons. In particular, who should be the author and committer of a merge commit? GitHub's answer is: wait until someone clicks the button, then use that person's information. That person is usually not the person who made the pull request in the first place, but often not one particular person either: there may be a whole group of people allowed to approve PRs. In any case, there is always a human in the loop.

    2Well, this is a definition. There are other equivalent ones you could formulate.