Reputation: 129
I want to merge 2 branches with different configuration files, so that configuration files stayed untouched even if there is no conflict. As I understood from here and here, I need to configure custom merge strategy not merge driver (I had configured merge driver and was working fine when I had conflicted merges)
I am trying to set up my merge strategy with guidance of this.
I created git-merge-mystrategy.sh in my projects root directory (the place where my .gitattributes is placed) with this code in it:
merge-file -q --ours "$2" "$1" "$3";
in .gitattributes I have:
pom.xml merge=ours
When I run:
git merge --strategy mystrategy develop
I get:
Could not find merge strategy 'mystrategy'.
Available strategies are: octopus ours recursive resolve subtree.
I am possibly missing this part from here: create an executable git-merge-mystrategy in your path.
Could you please give me a bit more detailes how to create custom merge strategy?
Upvotes: 3
Views: 2094
Reputation: 490068
As Edward Thomson said in his answer to a different question, and you guessed, you have to put that git-merge-mystrategy.sh
into a file that Git can find via $PATH
. That file must be executable (chmod +x
on Unix/Linux, I have no idea what on Windows) and found via the name git-merge-mystrategy
if you run it as git merge -s mystrategy
. (You could run git merge -s mystrategy.sh
to search $PATH
for git-merge-mystrategy.sh
.)
The larger problem is that a correct merge strategy is not easy to write. This:
merge-file -q --ours "$2" "$1" "$3";
is not going to work. Your strategy will be invoked with particular arguments computed by git merge
, but none are filenames. I'll illustrate this with a non-functional merge strategy script here.
If you run:
git merge --strategy mystrategy develop
your script will be invoked with four (!) arguments. If you run:
git merge -s mystrategy -Xa -Xkcd=12 --find-renames=35 develop
your script will get seven arguments:
$ cat ~/scripts/git-merge-silly
#! /bin/sh
echo "I got $# arguments..."
for i do
printf "%s\n" "$i"
done
read junk
$ chmod +x ~/scripts/git-merge-silly
$ git merge -s silly -Xa -Xkcd=12 -X find-renames=35 b1
I got 7 arguments...
--a
--kcd=12
--find-renames=35
4f95ecf496de9dfe175e7ed4dd97972adf0ca625
--
HEAD
439e327bc8f8e78d74b27ae89f433451eec09111
(at this point the script is reading the variable junk
from stdin, so everything pauses, and I can hit CTRL+C to interrupt it and stop the bogus merge).
Here is what the argument are, and what they mean:
--whatever
: this passes through an extended strategy option, -X whatever
. As you can see, there is no vetting of these arguments: they are really just arbitrary strings (what would the -Xkcd
option mean?).--
: this is the merge base, or the merge bases if there are more than one, from the heads to be merged.--
: this separates the merge bases from the strings specifying which heads are to be merged.HEAD
: this is always the next argument; it's compiled into git merge
. This is the local head and for a strategy like -s ours
(or -s theirs
, if it existed) it has a bit of extra significance, but for most merge strategies, you should treat it the same as the remaining arguments.git merge ... develop
, Git has resolved develop
to a specific commit ID that is the tip commit of your branch named develop
, and that commit is the remote HEAD.As a merge strategy, your job is now to:
It's perhaps simplest to just quote the git merge
source here:
At this point, we need a real merge. No matter what strategy we use, it would operate on the index, possibly affecting the working tree, and when resolved cleanly, have the desired tree in the index -- this means that the index must be in sync with the head commit. The strategies are responsible to ensure this.
and, just a bit later:
The backend exits with 1 when conflicts are left to be resolved, with 2 when it does not handle the given merge at all.
If you supply multiple -s
options on the command line, the git merge
code will (in modern Git) run git stash
for you and then do a git reset --hard
and git stash apply
between each strategy-attempt, until one either succeeds completely by exiting with 0 status—in which case Git takes its result—or it runs out of strategies to attempt. Having run out of strategies, if some of them exited 1 instead of 2, Git chooses the one that produced the "best" result, where "best" is a somewhat odd measure: the smallest numerical sum of changed files (vs the HEAD commit) and unmerged files is considered the "best" result. (It seems to me that perhaps unmerged files should be more heavily weighted, here.) It will then re-run that best merge after yet another hard-reset.
Anyway, the point of all this is that you have a lot of work to do in a merge strategy. It's wise to make sure the index and work-tree are in a sensibly recoverable state, before proceeding, and to reject the merge (with "please commit or stash your changes") if not. Similarly, if there are multiple merge bases, you can reject the attempt entirely using exit 2
, but otherwise you must do something about the multiple merge bases. (The resolve
strategy chooses one of them and ignores the rest, which is a valid option, though not always the best one. The recursive
strategy merges the merge bases, and uses the resulting commit as the new merge base.)
Then, for each head—your own regular HEAD
head plus each remote head—you must find the changes. The usual recursive and resolve strategies, which refuse to run unless there are exactly two heads, run git diff
with rename detection set to 50% by default, allowing -X
arguments to change this, so as to detect file creation, deletion, and renaming in each head when compared with the base. They then merge these at a high level—or fail to merge them, leaving the high-level conflict in the index—and for each file-pair identified in base-to-each-head, merge those files at a lower level using git merge-file
.
In your strategy, you would need to do all of this same work, except that for one particular file, you would use git merge-file -q --ours
in the way you want here.
Upvotes: 6