The entire merge driver idea is doomed for reasons outlined in my answer to Git merge strategy for a specific file depending on rebase / merge.1 There's a different possibility that can be made to work, but it's ugly. In fact, in the end, it's horribly ugly, and probably still a bad idea. Your best bet is probably, instead, to use Git hooks (post-checkout
and post-merge
, specifically) to manipulate some untracked and ignored file in the work-tree instead.
(Note, however, that since I don't know what you really want to have in your files, I do not even have a good starting point for proposing these as a solution.)
Discussion
It's worth remembering here, before we even start with the idea of having a file whose contents are handled specially, just how Git works with files. Files, in Git, aren't really all that important. What matters in Git is the commit. A commit stores files, so files come along for the ride, but it's the commit itself that is the key—and the way a commit stores files is a little peculiar, which begins to matter at this point.
The way a commit stores files is by building, and then referring-to, a tree object. A tree object is essentially a list of <mode, name, hash> tuples:
$ git ls-tree HEAD
[lots of snippages here]
100644 blob acf853e0299463a12212e9ed5f35d7f4a9d289af .gitattributes
040000 tree 7ba15927519648dbc42b15e61739cbf5aeebf48b .github
100644 blob 0d77ea5894274c43c4b348c8b52b8e665a1a339e .gitignore
...
100755 blob 54cbfecc5ab0531513ff9e069be55d74339ad427 git-bisect.sh
100644 blob 09b0102cae8c8c0e39dc239003ca599a896730cf git-compat-util.h
100755 blob d13f02da95f3b9b3921c3ccff9e3b6a7511cd666 git-cvsexportcommit.perl
...
100644 blob 2d41fffd4c618b5d7b816146d9df684b195535e3 xdiff-interface.h
040000 tree 77abde3699bc6874e10f1c17f4b97c219492542f xdiff
100644 blob d594cba3fc9d82d94b9277e886f2bee265e552f6 zlib.c
The string in the middle here (blob
or tree
) is derived from the mode at the front: 100644
or 100755
is a blob, 040000
is a tree, and there are a bunch of less-common special cases.
The file isn't quite stored in Git. Instead, the file's contents appear in the blob object at the listed hash ID. We can see that blob object directly:
$ git cat-file -p 54cbfecc5ab0531513ff9e069be55d74339ad427
#!/bin/sh
USAGE='[help|start|bad|good|new|old|terms|skip|next|reset|visualize|view|replay|log|run]'
LONG_USAGE='git bisect help
print this long help message.
... [lots more, snipped]
The git cat-file -p
command extracts the object, taking out of Git's frozen internal compressed format and turning it into readable text. So the blob object has the contents of the git bisect
shell script, and the tree object tells Git that in this particular commit, the blob object should be expanded into useful text form, in the work-tree, under the name git-bisect.sh
.
It's this expanding into useful text form process where we can make something interesting happen. We can do this with a .gitattributes
filter driver, rather than a merge driver. The merge driver isn't used in critical cases, where we would want it used. The filter driver is always used when extracting a file into the work-tree.
1If you read through the linked question's answer and reason out what's going on, you will see that it would be possible for Git to make this approach work, perhaps by having another per-file attribute such as always-merge
. But Git doesn't have this today, at least.
Filter drivers
Filter drivers come in two forms, which Git calls smudge filters and clean filters. These operate right at the interface between work-tree, where your files have a useful-to-you-and-the-computer format, and the index, which is where Git stores the file's name and the hash ID of a compressed, ready-to-go snapshot of that file (always ready for the next commit, but initially the same as the current commit).
The purpose of a smudge filter is to take the de-compressed, but not yet ready-for-use, text of a file and convert it to ready-to-use, work-tree form. The purpose of a clean filter is to take the work-tree form of a file and remove any work-tree-specific data, so that the file is ready to compressed into the Git-only internal form. The git checkout
command—along with a few other commands that can get frozen Git-only objects out—uses the smudge filter. The git add
command uses the clean filter to strip out "dirty stuff" that the smudge filter put in.
So, now we can see how we could make the work-tree copy of some file depend on the current branch: we just write a smudge filter that does this. We probably should also write a clean filter that takes out the branch-specific stuff, which will let Git compress the file better, but I'll leave that to you.
To define a smudge filter, we need an entry in some .gitconfig
or .git/config
configuration file. For instance, if we want to run source code through some sor of pretty-printing filter:
[filter "pretty-printer-for-XYZ-language"]
smudge = xyz prettyprint --stdin
(assuming the command that pretty-prints a source file is xyz prettyprint
and that it needs --stdin
to read from standard input). Then we tell Git, through .gitattributes
, to apply this filter to *.xyz
files:
*.xyz filter=pretty-printer-for-XYZ-language
The filter needs only read stdin and write stdout: Git arranges for the filter's stdin to come from the uncompressed but "clean" file's content as it appears in the blob object, with the filter's stdout going to the temporary file that will, at the end of this process, become the appropriate file in the work-tree.
For instance, if somefile.xyz
in the tree object has some blob hash, Git will read the blob, write the contents into the filter's stdin, read the filter's stdout, and write those contents to somefile.xyz
. There are a few important things to realize here though:
- The filter has no direct access to the name
somefile.xyz
. You can tell Git to produce the name as an argument, via a %f
directive, but remember that the filter must still read stdin and write stdout. (If you rewrite your filter as a "long running filter process" for efficiency, the filter must obey the packet protocol described in the documentation, which also provides the file's path-name.)
- Smudge filters run before
git checkout
updates HEAD
. As with point 1, smudge filters have no direct access to what's going on: they don't know that Git is in the middle of a git checkout otherbranch
, for instance.
Point 2 here is in bold because it's the biggest stumbling block here. It's possible to use the current process tree to find the Git command that invoked the filter, and use whatever OS facilities there are to find the command-line arguments. It would be very helpful if Git set up an environment variable before starting such filters, indicating what's happening: is this filter being run on behalf of a switch to new branch operation, or is it being run due to a git checkout -- path/to/file
or git checkout --ours -- path/to/file
index extraction, for instance? But, alas, Git doesn't do that either.