2

I have 2 branches. In first file named index.html is edited and commited. In second this file edited and moved to other folder. Can I make GIT understand (while merging) that index.html from 1st branch is the same file "folder/index.html" form second branch. In my opinion the problem is that when I moved file into a new folder GIT first deleted it and then created it in new folder. So after merging I will have 1 file with both changes?

Evgenii
  • 422
  • 2
  • 14
  • Are the two versions of the file identical, except being in different folders? – mkrieger1 Dec 12 '17 at 16:30
  • What exactly are you doing (which commands are you running?), and what exactly is the problem you are encountering? – mkrieger1 Dec 12 '17 at 16:32
  • And what is the result you are trying to achieve? – mkrieger1 Dec 12 '17 at 16:35
  • Now, after merge I have 2 files index.html. One in old folder and the other in new folder. And I need to have the only one with changes from both branches. – Evgenii Dec 12 '17 at 16:39
  • It looks like you can try to tweak the `find-renames` option (`-X find-renames=25%` or whatever), but if that doesn’t work you’ll need to merge the files manually. Once you do there’s nothing for Git to remember; each commit is stored as its tree, not a list of changes, so once you have the merged file that’s all you need. – Daniel H Dec 12 '17 at 16:53

2 Answers2

2

Git should already know they are the same file. See the answer to this question here. Is it possible to move/rename files in git and maintain their history?. If you type git log on moved file you see the commit after the move. The following command should have the full history.

git log --follow folder/index.html
stoneg64
  • 106
  • 6
1

Edit: I now believe I misread the question this morning, and there is only one modified file in your branch, derived from a single source. In which case, the description below is accurate, but tells us that Git should find the renamed file on its own, as long as it's sufficiently similar. If not, you can add and adjust the -X find-renames=number argument to git merge. You may want to run some manual git diffs first, with --find-renames=number (or the shorter spelling, -Mnumber) to find out what numbers make Git see the merge-base to --ours change as a rename.


If you have two modified files in your own branch, both derived from a single source file, Git's git merge won't help you. You will have to merge this file on your own, perhaps using git merge-file. See the final section below for instructions.

There are three inputs to git merge

The fundamental problem here is that git merge looks at exactly three commits.1 We need names for these commits and Git is not terribly consistent about these names: sometimes they're --ours and --theirs for the two branch tip commits, and sometimes they're local and remote. I'll use L, for left or local or --ours, and R for right or remote or --theirs. The third commit is in some ways the most important, but Git finds this third commit on its own: we have no control over which commit it chooses. We choose only the L and R commits. The third commit is the merge base, and in short,2 it's where the two branches first come back together:

          o--...--o--o   <-- L
         /
...--o--*
         \
          o--o--...--o   <-- R

or:

...--o--o--o--*--o   <-- L
         \     \
          o--o--o--o   <-- R

In both of these graphs, the commit marked * is the merge base.


1For simplicity, we'll assume a normal merge here, of two heads, with a single merge base commit. Criss-cross merge cases, with two or more merge bases, and octopus merges that have more than two heads, complicate this a lot, but are not the case we care about anyway.

2This is a little too short, but suffices for most cases.


Git makes two diffs

Now that Git has found the merge base—the L commit is just our current branch tip, and the R commit is the one you specify when you run git merge—and has its three inputs, Git runs git diff twice:

git diff --find-renames base L   # what we did
git diff --find-renames base R   # what they did

The first git diff compares all the files stored in the merge-base commit—remember, each commit is a snapshot of all files—to all the files stored in L, to see what we changed in --ours. The --find-renames option makes this particular git diff check for files that were renamed, but does not make it check for files that were copied. The rename detector runs with the default "at least 50% similar" (you can find detailed descriptions of how this rename detector works in some of my other answers: diff rename, copy, and break detection and similarity index), but you can tweak the percentage with -X find-renames=n.

The second git diff does the same thing but with their R commit.

Then, git merge goes on to combine the two sets of differences.

If one or both of the two diffs detects that index.html in the base was renamed to some other path(s) in one or both of the L and R commits, Git will take one of the renames (and declare a high-level rename/rename conflict if both of these commits renamed the base file). Again, though, there's no option for turning on copy detection, and Git will just see any copy as a new file in one or both of L and R. This could result in a high-level add/add conflict, if it's added in both; or if it is just added in one commit, Git assumes it should be added in the new commit.

If there are no high-level conflicts and no low-level conflicts, git merge will normally make a new commit immediately. You can suppress this with --no-commit and do additional work to the work-tree. (The result is what some call an evil merge. You should be aware of this, and at least mark the commit somehow, e.g., in the commit message. Alternatively, make a normal merge, followed by a fixup commit. There are no perfect answers to this particular problem.)

Manual merge

To manually merge a copied file, extract the merge base version—you can find the merge base commit hash ID with git merge-base—and both tip versions into three ordinary files, then use git merge-file on it. Note that by default, git merge-file writes its output over one of the three inputs—this is likely what you will want, since the HEAD or --ours version is already in your work-tree, under the name you are using for it, so all you really have to do is extract the base and --theirs or R versions:

git show base-hash:base-path > newname.base
git show theirs:theirs-path > newname.theirs
git merge-file newname newname.base newname.theirs

then remove the .base and .theirs versions when you are done merging. (It's wise to inspect the resulting merge, as always, since Git just obeys simple line-at-a-time rules for combining diffs.)

torek
  • 448,244
  • 59
  • 642
  • 775