Search code examples
gitmergedrivergit-mergegit-merge-conflict

How to make a Git custom merge driver automatically stage resolved files?


I've been trying to set up a custom merge driver for merge conflicts in Git and have it mostly working, but can't figure out why it doesn't automatically stage successfully-resolved conflicts like the built-in text driver does.

This is my custom driver definition, it's relatively simple:

[merge "auto-pro"]
    name = Auto-merge as much as possible and don't add conflict markers
    driver = git merge-file -p %A %O %B > %P || (git merge-file --ours -p %A %O %B > %P && false)
    recursive = binary

The idea is that it tries to do a normal-ish merge using git merge-file first. If that fails, it leaves conflict markers in the file and returns a non-zero exit code, which leads to the next statement getting executed and running git merge-file again, but with the --ours flag to suppress conflict markers for chunks that can't be auto-merged.

This all works pretty much as expected, except that in the "successful auto merge" case (ie, the case where git merge-file only runs once and returns an exit code of 0), the successfully merged file doesn't end up staged for commit. Why? If I switch back to the default text driver and redo the merge, it successfully merges and automatically stages the resolved file. How can I get my custom driver to behave like this?

Note: I already tried adding git add the driver command, but it barfs an error because the merge process is already holding the lock on the Git index.


Solution

  • Finally figured out my problem here. I didn't read the docs for custom merge drivers closely enough and it turns out the driver is supposed to write out the results of the merge to the %A file, not the %P file.

    However, simply changing the driver to direct the output of git merge-file to %A instead of %P was not working for some reason. Best guess is that it's because I'm on Windows and the merge-file command was holding open a lock on %A and preventing it from being directly re-written. Storing the merge result in a temp variable before flushing it to disk solved the problem, which seems to support that theory.

    Here is my final merge driver implementation, which works as desired:

    [merge "auto-pro"]
        name = Auto-merge as much as possible and don't add conflict markers
        driver = ~/.git/pro-merge-driver.sh %A %O %B %P
        recursive = binary
    

    Contents of ~/.git/pro-merge-driver.sh:

    #!/bin/bash
    
    if AUTO_MERGE_RESULT=`git merge-file -p $1 $2 $3`; then
        echo "Auto-merge: $4"
        echo "$AUTO_MERGE_RESULT" > $1
        exit 0
    else
        echo "Conflict: $4"
        OURS_MERGE_RESULT=`git merge-file --ours -p $1 $2 $3`
        echo "$OURS_MERGE_RESULT" > $1
        exit 1
    fi