3

When I tried to merge a source branch into target branch, git recognizes two files to be conflicting. But both these files correspond to different file paths in the source branch and target branch (The file has been moved and modified). I need to get the path of this conflicting file in the source branch.

Note that I can use git diff --name-only --diff-filter=U to list the files under merge conflict, but this lists the paths in the target branch (since I checkout target branch and pull the source branch into it).

Is there a command to list the path of conflicting files in the source branch?

Ganesa Vijayakumar
  • 2,422
  • 5
  • 28
  • 41
Goutam Lavudiya
  • 111
  • 1
  • 6

1 Answers1

1

When I tried to merge a "source branch" into "target branch", git recognizes two files to be conflicting. But both these files correspond to different file paths in the "source branch" and "target branch" (The file has been moved and modified).

To put this in a more concrete manner, I believe you did:

git checkout tgtbranch
git merge srcbranch

and got:

CONFLICT (rename/delete): orig-filename deleted in HEAD and renamed
to new-filename in A. Version A of new-filename left in tree.
Automatic merge failed; fix conflicts and then commit the result.

(Note that I reproduced this conflict with branches named A and B instead of srcbranch and tgtbranch respectively. Also, the CONFLICT message was all on one line. I split it into two for posting purposes only.)

I need to get the path of this conflicting file in the "source branch".

As you can see from the above, this information is available in the CONFLICT message that git merge prints.

Unfortunately, this information is not available, at least not directly, anywhere afterward. You must save the CONFLICT message. (See below for two additional alternatives, if you did not save the CONFLICT message.)

What is available is what git status prints:

$ git status --short
UA new-filename
$ git status
On branch B
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    added by them:   new-filename

no changes added to commit (use "git add" and/or "git commit -a")

We can also use git ls-files --stage to see what's in Git's index at this point:

$ git ls-files --stage
100644 4466f6b253fc4b67ec2749befdd29b7c1f573e36 0       README
100644 923932af0c5b6ddc43c116c648cd793759949b66 3       new-filename

Only the new file name exists!

What you can do if you did not save the CONFLICT line

Method 1: repeat the merge

One option, probably the easiest, is to repeat the merge. Of course, if you're working on resolving the merge, you probably don't want to mess with the current index and your work-tree, which makes this a bit tricky. You can:

  • clone the repository, so that you have a new clone with a new index and work-tree, or
  • use git worktree add, provided you have git worktree

and then do the re-merge in the new clone or work-tree. Use git checkout to check out the commit to which tgtbranch points in the original clone (if you re-cloned) or current clone (if you are using git worktree), then run git merge to merge the commit to which tgtbranch points in the original clone (if you re-cloned) or current clone (if you are using git worktree). That is:

$ git rev-parse tgtbranch
<hash>
$ git rev-parse srcbranch
<hash>
$ cd /tmp && git clone <path> reclone && cd reclone
$ git checkout <hash> && git merge <hash>

if you're using the re-clone procedure, or:

$ git worktree add --detach <path> tgtbranch
$ cd <path> && git merge srcbranch

if you are using the git worktree approach.

This merge will spit out the CONFLICT line. Save it! Then remove the new clone or added work-tree—its entire purpose was to obtain the CONFLICT line. Your problem is now solved: you have the original file name.

Method 2: use git diff against the merge base

The merge operation detected the rename in tgtbranch using git diff --find-renames. The threshold for rename-finding was set to whatever you supplied with your -X find-renames=number argument, or 50% if you did not specify a number.

This git diff was a diff between two specific commits: the merge base commit, which Git found automatically, and the tip commit of tgtbranch or srcbranch (whichever branch Git "saw" the rename in: one tip commit had a rename, the other had a delete, as compared to the merge base).

Find the merge base commit. There is one complication here, but it will show up immediately if the complication is there. Run:

git merge-base --all srcbranch tgtbranch

For instance, in my case, I get:

$ git merge-base --all A B
3a5c9ae0669e9969f8986810ea03c5283e0ac693

If this prints out just one commit hash, you're good. If it prints out more than one, you probably should go to method #1 instead.

Now that you know that there is a single merge base, you can run two git diff --find-renames commands. Add the --name-status option to limit the amount of output. You may also want to add --diff-filter=RD to show only renamed or deleted files. In my case, I left these options out because the rename or delete is all I did:

$ git diff --find-renames --name-status A...B
D       orig-filename
$ git diff --find-renames --name-status B...A
R064    orig-filename   new-filename

This shows the renamed/deleted file: the original name was orig-filename and the new name is new-filename.

This three-dot syntax, A...B (or in your case tgtname...srcname and srcname...tgtname), has one special meaning exclusive to git diff. In git diff—but not in other Git commands1—it means Find a merge base between the two specified commits, and use that as the left side of the diff. Use the right-hand commit specifier as the right side of the diff. Since there is only the one merge base, this diffs the merge base against the commit named by the right hand side argument.


1The special three-dot syntax applies to all the usual Git diff engines, including git diff-tree as well as the user-facing git diff.

There is a bug in the way this is implemented in many version of Git, in that if there is more than one merge base, the result is not predictable. So make sure there is only one merge base.


Side note

If we inspect the index file directly, in this unmerged state, we find a REUC record with the original file name:

$ od -c .git/index
0000000    D   I   R   C  \0  \0  \0 002  \0  \0  \0 002   ] 264 256   x
0000020    2 270   )   P   ] 264 256   x   2 227 221 220  \0  \0  \0   ~
0000040   \0 320   !   a  \0  \0 201 244  \0  \0 003 351  \0  \0 003 351
0000060   \0  \0  \0   W   D   f 366 262   S 374   K   g 354   '   I 276
0000100  375   қ  **   | 037   W   >   6  \0 006   R   E   A   D   M   E
0000120   \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000140   \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0 201 244
0000160   \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0 222   9   2 257
0000200   \f   [   m 334   C 301 026 306   H 315   y   7   Y 224 233   f
0000220    0  \f   n   e   w   -   f   i   l   e   n   a   m   e  \0  \0
0000240   \0  \0  \0  \0   T   R   E   E  \0  \0  \0 006  \0   -   1    
0000260    0  \n   R   E   U   C  \0  \0  \0   -   o   r   i   g   -   f
0000300    i   l   e   n   a   m   e  \0   1   0   0   6   4   4  \0   0
0000320   \0   0  \0 214 354   + 214 330 360 004   N   O 355  \b 243   '
0000340    4 331 225  \n   W   [ 037 026 216   K   } 367   ? 373 354 364
0000360    [   ^ 364   b 001 371   6   ,   3 370 320                    
0000373

The index begins with a DIRC magic number, as it must, then has ordinary index entries for both of my files (I have just the two as shown in the git ls-files --stage output). But then it has a TREE record, which we can see near offset 0000240, followed by a REUC record in the very next line. This REUC record is an "undo" record that allows git checkout -m to reproduce the merge conflict. And, as we can see, the undo record actually stores the original file name.

Git really should let us access this information, perhaps through git ls-files or—more conveniently—through git status. Alas, it does not.

torek
  • 448,244
  • 59
  • 642
  • 775
  • I get only `CONFLICT (content): ` in my merge conflict message. Also note that the merge commit message gets saved in `.git/MERGE_MSG` file, which does not contain the old file name for my case. – Goutam Lavudiya Oct 30 '19 at 11:00
  • `CONFLICT (content)` means there *isn't* an old file name; *no* rename was detected for this file. (The saved commit message never contains the old file name in rename cases, so that's not dispositive.) – torek Oct 30 '19 at 16:02