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.
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.
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 (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
To set an attribute for our
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
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
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
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.
All seems right with the world, until you try to merge the changes back into
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
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.