For the most part when I’m designing a development pipeline along the lines of Vincent Driessen’s somewhat debated Git branching model, I often want to synchronize all the code from one branch with another except a configuration file which specifies directory paths, hosts and other branch-specific items.

Motivation

Naturally I want my dev code to write to non-production locations and my production code to write to production pipelines, so how does one avoid manually resolving the config file every time a development feature is to be integrated?

One way is to set up a custom Git merge driver to handle this specific file. I’ll be walking through my own usage but great general posts about this topic can by found on Stack Overflow by user Von C here and here.

Setup

I’ll use a toy branch structure:

       c1 (tst)
      /
     /
-- c0 (master)
     \
      \
       c2 --- c3 (dev)

Let’s say on each branch you’ve got a code_library/ directory along with a config file in the top directory. The goal will be to merge the dev changes onto the tst branch without altering the config file as it is committed on `tst.

Git Attributes

Git attributes (documentation here) allow us to change what happens to specific files for specific actions. In this case we’ll be altering what happens when a merge operates on the config file between two branches.

It should be noted that .gitattributes files can be present in any directory and that the “deepest” .gitattributes file set the attributes for any files, .e.g. a top level attribute file will ignore the file if it sees it has already been assigned attribute(s). The sole exception is that .git/info/attributes will override any directives set inside individual .gitattribues files.

To set an attribute for our config file:

echo config merge=keep-branch-config >> .gitattributes

Add it and commit:

git add -A
git commit -m "prepare branch with .gitattributes merge strategy to ignore conflicting config"

This should be done for all branches just in case the dev branch needs to pull in a change made to master.

Configure Custom Driver

Establishing the custom driver need only be done once per repository. We set a descriptor to explain what the driver is doing and then point it to a “shell script”:

git config merge.keep-branch-config.name "always keep branch config during merge"
git config merge.keep-branch-config.driver "true"

This configures the driver to run the command true whenever that merge is called, which does nothing and returns a successful exit code. Alternatively, you can add a script to further document the reason for the driver, but you also need to ensure that the location of said shell script needs to be added to your PATH variable.

Merge Branch

Now you’re all set! Merge away between the branches and watch the copy of config of the branch you’re on always be preserved. To pull all the changes from the dev branch to the tst branch but preserve the config you would merge with a theirs strategy.

git checkout tst
git merge -X theirs dev

The repo now looks like this:

-- c0 (master)
   | \
   |  \
   |   c1 ------ c4 (tst)
    \           /
     \         /
      c2 --- c3 (dev)

You now can pull changes into tst whenever you want from dev without ever changing the config.

Caveat

All seems right with the world, until you try to merge the changes back into tst:

git checkout master
git merge -X theirs tst

To your horror the config file is overwritten! Why?

The reason is the custom merge driver we initialized is only called upon when changes have been made to both files. Git is smart enough to see that the changes to the tst branch’s config were “downstream” of the latest commit for the config on master.

Unfortunately I haven’t been able to find an automatic way to force this. The best I can come up with is to pause the merge before the commit, unstage and revert the changes to the config before continuing:

git checkout master
git merge --no-ff --no-commit tst
git reset config
# Unstaged changes after reset:
# M       config
git checkout -- config
git commit -m "merging with changes from dev branch"

Sure this may be annoying to have to do “by hand”, but the alternative of editing the config files by hand is in my opinion far worse.