gitmergegitattributes

What exactly does "* -merge" in gitattributes effect?


I am a little mistrustful towards automatic merging, so I'd like to stop Git from trying anything of that kind when I issue git merge or git pull; instead, I'd like to have my mergetool opened.

For that reason, I have put * -merge into my .gitattributes file. As I understand the documentation, that should do what I want. From https://git-scm.com/docs/gitattributes (in the section "Performing a three-way merge", about the merge attribute):

Unset
Take the version from the current branch as the tentative merge result, and declare that the merge has conflicts. This is suitable for binary files that do not have a well-defined merge semantics.

However, that stanza in the .gitattributes file doesn't seem to effect anything. After having fetched the remote branch, a git merge still immediately opens the editor for the commit message, which means that Git has performed the necessary actions in the background.

Perhaps I misunderstand what * -merge should actually effect. Could somebody please elaborate a bit?

This question relates to two situations:

  1. The remote branch has diverged from the local branch, but the set of changed files is orthogonal; that is, a file that has been changed remotely has not been changed locally, and vice versa.

  2. The remote branch has diverged from the local branch, and there is at least one file which has been changed remotely and locally.

[ Side note: The reason for it not working as expected may be that my .gitattributes file does not get evaluated for some reason. But that's a different subject for a different question. I'd first like to know what I can expect from * -merge at all. ]


Solution

  • First: the documentation here is correct, it's just misleading. The reason your:

    *    -merge
    

    directive seems to have no effect is that git merge doesn't do what people think it does.

    The misleading part

    ... in the section "Performing a three-way merge", about the merge attribute ...

    The key is that this section discusses how Git performs a three-way merge, with the missing but implied phrase at the file level included here. To get to this point, Git has to have decided, before we get here, that a three-way merge between three input files is required.

    When you run:

    git merge <name-or-hash-ID>
    

    you're telling Git to locate some particular merge base commit B on its own, by providing it with a current or --ours or $LOCAL commit HEAD, which I will call L, and some other commit (--theirs or $REMOTE) R. Git uses the commit graph:

                 o--o--L   <-- our-branch (HEAD)
                /
    ...--o--o--B
                \
                 o--o--R   <-- their-branch
    

    to find B.

    The three commits B, L, and R all contain snapshots. For each file (after doing any rename detection so that we can identify renames if needed; for simplicity of discussion we'll assume no renames), there may be some path P that exists in one, two, or all three commits. For further simplicity, we can assume that P does in fact exist in all three commits. So there are now "three versions" of file P.

    There are now exactly three cases:

    1. All three versions match exactly (all blob hash IDs for PB, PL, and PR match). There is nothing to merge and Git takes any of the three versions (in practice, the --ours or PL file) as the final merged result.
    2. All three version are different: A three way merge is required, and * -merge will take effect now.
    3. Two versions match, one doesn't. No three-way merge is required and Git does not do one.

    Case three here is the one that is biting you. Git simply compares the three hash IDs. Which one is the odd man out? If it's the merge base version, Git uses either the L or R version (these are the same). If it's the L version, that's the one Git uses. If it's the R version, that's the one Git uses. In all three cases Git now has the merge result, which it puts into its index (staged for commit) and leaves in your working tree.

    Git only uses the merge driver when a three-way merge is required. To use the merge driver, Git consults the merge setting, and now the .gitattribute documentation section you're talking about comes into play. So only case 2 above is affected.

    Ideally, Git should at least have a way to override its case-3 action, falling back to case 2 and using the defined merge driver, for those cases where the L and R files differ. If Git had such a thing and you used it, it would fire on exactly the right set of files here. But Git doesn't have that.