How to split file into ours and theirs on conflict

Viewed 576

I there a way to get 2 copies of file where first will have remote content and the second with local content

3 Answers

I don't think that there is a git option to keep both versions of the file in two copies.

But you can easily acheive this goal with the following commands :

git merge the_branch
git checkout --theirs -- path/to/file ; mv path/to/file path/to/file.theirs
git checkout --ours -- path/to/file ; mv path/to/file path/to/file.ours
git checkout -m -- path/to/file

In the end you have three files :

  • file.theirs, with their version;
  • file.ours, with your version;
  • file, with both versions and conflicts marks.

TL;DR

Consider using git mergetool (though I never do it this way myself), or git show (I sometimes do this). Also consider setting merge.conflictStyle to diff3, which makes Git write, into the conflicted work-tree copy, the base version as well as the left and right side versions. I find that with this turned on, I almost never need to see the two inputs. (But only almost never.)

Long(ish)

In fact, the file is already present in all three versions, but they are all in the index, not in the work-tree. The trick is therefore to get them out of the index, where they're in the special Git-only format, into your work-tree where you can see and work on / with them.

Let's look briefly, yet in detail, at how git merge works when there are conflicts. For conflicts to occur, we must have done something like this:

$ git checkout ours
Switched to branch 'ours'
$ git merge theirs

Git will have looked at the commit graph and found that ours and theirs have diverged, but have a common merge base commit:1

          o--...--L   <-- ours (HEAD)
         /
...--o--B
         \
          o--...--R   <-- theirs

Commit L is the left side or local or --ours commit. Commit R is the right side or remote or --theirs commit. Commit B here is the merge base. Git then did, in effect, two git diff commands, one to find out what we changed since the merge base:

git diff --find-renames <hash-of-B> <hash-of-L>   # what we changed
git diff --find-renames <hash-of-B> <hash-of-R>   # what they changed

The merge then attempted to combine these two sets of changes, using the contents associated with commit B as the base for the combined changes. However, we changed some file, they changed the same file, and our changes collided with their changes, so that we got a merge conflict.

What Git has done, at this point, is to put all three copies of the file into the index, at nonzero staging slot numbers:

  • Stage 1 contains the merge base: file B:P, where B is the base commit and P is the path name of the file (as found in that commit anyway—we might have renamed the file!).
  • Stage 2 contains our version of the file: file L:P.
  • Stage 3 contains their version of the file: file R:P.

The work-tree contains Git's attempt to merge the two sets of changes, with conflict markers. Note that this version is not the same as any of the three input versions! Some part(s) of the merge may already be resolved. The default style for conflicting changes is merge, which shows only the left (B-vs-L) and right (B-vs-R) changes without showing you the section that was in B itself. In many cases that's enough, but when the changes are purely deletions, it's often very helpful to know what line(s) in B is / are being deleted, and that's not something you can just deduce. Setting the conflict style to diff3 makes Git record the B code section as well, in between the two change-sections.


1It's possible for there to be multiple merge base commits. In this case, Git defaults to constructing a new merge base commit by merging the merge bases. This process is a little bit messy, but fortunately it's rare, and even when it does happen, it rarely makes anything worse.

Extracting the three versions

  • You can extract any of the three versions using git checkout-index, although this command is a bit of a pain to use:

    git checkout-index --all -- path/to/file
    

    This writes all three to files with funky temporary names, which you then have to rename. (There are several variations on this theme but they're all a bit annoying.)

  • You can use git mergetool, which automatically extracts all three versions, then invokes your chosen merge tool command on all of them.

  • Or, you can manually extract one or both files. The method in DogEata's answer will work, but if you have no concerns over line-ending issues, this way is shorter:

    git show :1:path/to/file > path/to/file.base
    git show :2:path/to/file > path/to/file.ours
    git show :3:path/to/file > path/to/file.theirs
    

    This uses the git show command along with the gitrevisions syntax for accessing the index copies, showing them to standard output, and redirecting the output to new files.

Note that git add writes to slot zero

Git knows that a merge is in progress, and has not yet been finished, in several ways, but the most important is that there are these files in slots 1, 2, and/or 3 for some path in the index. When you've figured out the correct contents for that path, and written them to that path in your work-tree, you run:

git add path/to/file

to copy the file back into the index, taking the normal-format work-tree copy and compressing in into the special Git-only format that goes in the index.

If the file were in the index at slot zero, the way it normally is, that would just overwrite the old index copy with the fixed-up work-tree version. When there are multiple copies of the file in the index using the higher numbered slots, git add still writes to slot zero, but this time, it also removes the higher-numbered entries entirely. Now the file is resolved.

If you use git mergetool, the git mergetool command can automatically run git add for you, hiding the extra step. Some people find this especially convenient.

Cherry-picking ideas from the official documentation and the other comments here, I came up with an small function that may help you.

This script covers the case for rebase conflicts too, which for some reason the solution using git checkout --ours wasn't working on them.

In my specific use-case, I'm interested in getting all the files separate whenever I run the command, adapt as you may need.

Function made for fish:

function git-conflict-split
    for file in (git ls-files -u | cut -c 51- | sort -u)
        rm $file
        git checkout-index --stage 3 -- $file
        mv $file $file.THEIRS
        git checkout-index --stage 2 -- $file
        mv $file $file.OURS
        git checkout-index --stage 1 -- $file
        echo "meld $file.OURS $file $file.THEIRS"
    end
end
Related