简体   繁体   中英

Git: Manually merge changes in an undetected rename

This question uses Git 2.7.0.windows.1 , so some of the git commands might be outdated.

If my git merge command doesn't detect a renamed file, how can I tell git to manually merge the changes in two files that are supposed to be a single file, without starting the merge over and using a lower rename threshold?

Reproduction Steps:

git init
echo "//Hello world!" > hw.h
git add . && git commit -m "Initial commit"
git checkout -b someBranch
mv hw.h hw.hpp
echo "//Foobar" > hw.hpp
git add . && git commit -m "Change hw to HPP & change content"
git checkout master
echo "//Boofar" > hw.h
git add . && git commit -m "Change content of hw"
git merge -X rename-threshold=100% someBranch

You will get merge conflicts, but no conflicted chunks. That is, the only conflict/error you should get is:

CONFLICT (modify/delete): hw.h deleted in branchB and modified in HEAD. Version HEAD of hw.h left in tree.
Automatic merge failed; fix conflicts and then commit the result.

And git status --porcelain will show:

UD hw.h
A  hw.hpp

Normally, when merging, it's ideal to set the threshold for detecting renames low enough that renames are detected. Some people recommend 5% , for example. In my case, I'm doing a massive merge (>1000 files and about 9m LOC), and I had to raise the rename threshold high enough to avoid any "false positives"; literally one percent lower and I got a huge swath of falsely-detected renames (I know, duplicated code sucks). At the value I wound up using, I get only a small handful of missed renames, which seems like a better option.

TL;DR lowering the rename threshold is not an option for me; how can I, without starting the merge over, tell git to consider hw.h and hw.hpp to be a single file (with conflicts), rather than two files as shown above?

The tools for this are a bit klunky, but they are there.

You need to be sure that the merge itself stops before committing. In your case this happens automatically. For trickier merges, where Git thinks it's doing it correctly but is not, you would add --no-commit , but this then affects the next few steps. We'll ignore that problem for now.

Next, you need to get all three versions of the file in question. Since Git stopped with a conflict, we're in good shape: the three versions are all accessible through the index. Remember that the three versions we care about are merge base , --ours , and --theirs .

If Git had detected the rename correctly, all three versions would be in the index under a single name. Since it did not, they are not: we need two names. (With the "Git thinks it did the merge correctly" case, the merge base version of the file is not in the index at all, and we have to retrieve it some other way.) The two names in your case here are hw.h and hw.hpp , so now we do this:

$ git show :1:hw.h > hw.h.base    # extract base version
$ git show :2:hw.h > hw.h         # extract ours
$ mv hw.hpp hw.h.theirs           # move theirs into place

(The renaming is not strictly necessary, it's just to help keep it all straight and nicely illustrated.)

Now we want to merge the one file with git merge-file :

$ git merge-file hw.h hw.h.base hw.h.theirs

This uses your configured merge.conflictStyle so that what's in the merged file looks just as you would expect, except that the labels on the conflicted lines are a bit different. I have diff3 set, so I get:

$ cat hw.h
<<<<<<< hw.h
//Boofar
||||||| hw.h.base
//Hello world!
=======
//Foobar
>>>>>>> hw.h.theirs

You can now resolve this as usual, rm the extra .base and .theirs files, git add the final result, git rm --cached hw.hpp , and git commit . (It's up to you when to git rm --cached hw.hpp : it's safe to do this at any point in time before the commit, but once done you can no longer get "theirs" from the index; see below.)

Note that the "ours" and "theirs" versions are also available through git show HEAD:path and git show MERGE_HEAD:path . To get at the base version without the index, we would have to run git merge-base HEAD MERGE_HEAD to find its hash ID (and then assume there's a single merge base as well 1 ), and git show <hash>:path . This is what we must do if Git thinks it has done the merge correctly.

Note also that if you really want to—I imagine this would only be true if you wanted to use some other tool(s) you have, that require it—you can use git update-index to shuffle the entries around in the index, moving hw.hpp into slot-3 of hw.h so that it does show up as "theirs", and shows up that way in git status . For this particular example:

 $ printf '100644 bbda177a6ecfe285153467ff8fd332de5ecfb2f8 3\thw.h' |
     git update-index --index-info

The hash here came from git ls-files --stage and is the hash for hw.hpp . (You need a second step to remove the hw.hpp index entry.)


1 Use git merge-base --all to find all merge bases. If there is more than one, you can either pick one arbitrarily (this is what -s resolve does), or try to merge all the merge bases into a virtual merge base . To merge two merge bases, you find their own merge base, and merge two bases as if they are branch tips, using that merge base. Recurse and iterate as needed—this is what Git does with the default -s recursive strategy—until you have a single merge base version of the file.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM