tags:

views:

486

answers:

3

I have a number of remote repositories that I want to merge together. Some of the subtrees in those repositories are unique to the remote (they contain data that is host-specific), other subtrees contain data that is (supposed to be) common across all remotes.

What I want to do, essentially, is run "git pull " for each remote. This will fast-forward the local master branch along the tracking branch for the remote master for host-specific files that have changed on the remote, and will do nothing for the common files because they will not have changed.

A change in a common file (call it F, with the change being F') shouldn't be a problem, even if it only happens on one remote at first. git-merge will Do The Right Thing and give me a copy of F' in my composite workspace, which is what I want. The problem comes if the same common file changes in a different way on another remote (call it F"). git-merge will give me a composite of F' and F", which isn't what I want. All I want is F".

When I worked with ClearCase we called this a copy-merge. The result of the merge was always an exact copy of the contributor. This sounds a lot like "git merge -s theirs" would be, except that it doesn't exist.

I wondering whether I can cook something up with "git-read-tree -m --trivial" to get the fast-forward merges out of the way, then do some magic with git-merge and a custom mergetool that simply copies the $REMOTE file to $MERGED. But even with that I don't see how I can stop git-merge from compositing F' and F" if it things the merge is trivial.

I've read the link http://stackoverflow.com/questions/173919/git-merge-s-ours-what-about-their on this site, and the post by Junio Hamano it references explaining why "git merge -s theirs" is such a bad idea, but this isn't the case for me. I do value the old history, but I need to jump ship and follow the change on the remote site when one happens. No new work is done on the local site. It simply needs to form a composite of all the remote sites, taking the latest "common" file from the last polled remote when one changes.

Thanks in advance for any help you can give me.

A: 

For the specific files/trees you want to copy-merge, you could setting up a gitattributes value like the one I mention in this SO question, defining a custom merge driver.
The script associated with the merge attribute would ensure always keeping the remote file as the merge result (see this SO answer for an illustration, albeit for the opposite scenario -- keeping the local version).

echo * merge=keepTheir > dirWithCopyMerge\.gitattributes
git config merge.keepTheir.name "always keep theirduring merge"
git config merge.keepTheir.driver "keepTheir.sh %O %A %B"

By setting a .gitattribute on top of the sub-tree you want to be copy-merged, with '* merge=keepTheir' as its content, you effectively attribute a custom merge driver to all files of that subtree (note the use of the '*' wildcard here).

With keepTheir.sh as:

mv -f $3 $2
exit 0

you do not have to modify any "default" merge driver, and you apply your own only on the files you want.

VonC
kbro
@kbro: "since I want it to happen for every file, doing it with attributes is a bit ungainly". I would say: quite "gainly" actually; a '*' would be enough. See my completed answer.
VonC
Will the '*' wildcard glob fully on U*IX systems, or should there be an extra attribute for '.*' assuming I want to copy-merge those files as well? Or is there a way to glob both dot-files and non-dot-files? Sad that I don't know!
kbro
+3  A: 

Many thanks to @VonC for suggesting the use of the merge=custom-driver attribute in the .gitattributes file. While this will work, I'm reluctant to pollute my workspace with .git files, and while I could use *$GIT_DIR/info/attributes* to avoid the pollution, I'm bothered by the need for 2 rules to catch dot-files and non-dot files.

After a bit of experimentation I managed to get a solution with the merge.default configuration variable (mentioned in the gitattributes(5) manpage) working. The trick I missed was that merge.default takes the name of a custom driver you have defined previously; you don't give it the custom command directly. Here's what works for me...

First define your copy-merge custom driver. You can use shell commands directly; there's no need for an external script (just make sure you get your shell meta-character quoting right):

git config merge.copy-merge.name   'Copy Merge'
git config merge.copy-merge.driver 'mv %B %A'

Note that mv returns 0 on success, 1 on failure, meeting the criteria for reporting merge "success" back to git.

Now tell git that ALL merges are copy-merges:

git config merge.default copy-merge

Hey Presto! Job done. git merge <branch> will now copy-merge everything so the branch you're on contains exact copies of all files on <branch>. QED.

If you want to do a non-copy-merge then simply reset the default merge driver:

git config --unset merge.default

If you do want to be more selective then leave merge.default unset and use attributes as @VonC says:

cd path/to/copy-merge/in
echo '* merge=copy-merge'  >  .gitattributes
echo '.* merge=copy-merge' >> .gitattributes

Do this at the top of every subtree you want to copy-merge in. If there's a sub-subtree that you DON'T want to copy-merge in, you can turn it off again:

cd path/to/copy-merge/in/path/to/normal-merge/in
echo '* merge'  >  .gitattributes
echo '.* merge' >> .gitattributes

WARNING: littering your working tree with lots of .gitattributes files is bound to lead to confusion, especially if you also use things like "*.bin -merge" in other directories to force all merges of .bin files to fail with conflicts. It may be better to use *$GIT_DIR/info/attributes* for this sort of thing, as it has the highest precedence.

kbro
You have so many subtrees you do not want the .gitattributes files? And I do not understand the "is bound to lead to confusion, especially if you also use things like "*.bin -merge" part. Anyway, interesting feedback. +1
VonC
For my purposes I only have one subtree - the entire repo! But I'm against .gitattributes files on principle as they pollute the workspace - I worked with CVS and Subversion for too long to want to put tool-specific cr*p in amongst my files :-)
kbro
The confusion I talk about could arise if you've already used .gitattributes to set up custom merge actions for other filetypes, and then you want to set things up for a copy-merge. Given the precedence rules for .gitattributes files, putting one in your top-level directory won't override the setting in any lower-level .gitattributes files. You'll therefore get copy-merge for previously default-merged files, but you'll get the previous override action for ones you've already defined. Definitely confusing.
kbro
The only way round this, if you want copy-merge throughout the repository (which I do) is to set the merge attribute in $GIT_DIR/info/attributes, as this overrides *everything*.Note that setting the merge.default config variable doesn't guarantee a complete copy-merge as this only catches files for which the merge attribute is *unspecified*.So @VonC was right after all - use attributes.But I was sorta right too, saying .gitattributes wasn't the best place to do it :-)
kbro
A: 

In version 1.7.1 of Git, you can pass a "theirs" strategy to merge with and "-Xtheirs" argument.

git merge -Xtheirs otherBranch

Not sure if that applies to what you are trying to do, but it's probably worth a shot.

anotherAlan
I think that should be git merge -s recursive -X theirs otherBranch
kbro