Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Possible to resolve Git conflict on single file using Ours / Theirs?

I've found many instructions on Stack Overflow and elsewhere for resolving conflicts using the OURS/THEIRS dynamic, in cases where you simply want to overwrite one file with another (particularly binary files). However in almost every example I find, it's always been applied en masse to all conflicts, whereas I only want to apply it to a single conflicted file.

One supposed solution I found is to use the git mergetool command. However the mergetool is giving me issues, where I select "choose left" or "choose right" and nothing happens.

Regardless of mergetool though, I'd like to know anyway if there is a way to do this from the command line. I'm sure there is, and if anyone would let me know the command, or otherwise link me to the SO question I must not be finding, I'd appreciate it greatly.

I've also tried using...

git checkout --theirs PATH/TO/CONFLICTED/FILE

But then when I enter 'git status', it still shows the file as conflicted.

Thank you so much for your time.

like image 526
SemperCallide Avatar asked Jan 05 '18 14:01

SemperCallide


1 Answers

TL;DR

You will need to git add the final resolution, unless you use a different method to extract the "ours" or "theirs" version.

Long

Each merge tool is independent of Git (Git just runs them and lets them do their things) so for that particular sub-part of this question, you must consult the merge tool itself.

As for the git checkout --ours or git checkout --theirs, well, this is where what Git calls the index shows its full bit of complexity. Remember that the index, which is otherwise kind of mysterious and is also called the staging area and sometimes the cache, is essentially where you and Git build up the next commit you will make.

When you run:

git merge <commit-or-branch-specifier>

Git finds three commits:

  • One is your current commit, which is the one you're always working with at any given time, so that's not really special, except that you can refer to it by the name HEAD or the single character @ (e.g., git rev-parse HEAD or git rev-parse @ to get its hash ID).
  • One is the commit you just named. If you ran git merge otherbranch, you can run git rev-parse otherbranch to see what its commit hash ID is right now. (Branch names have the property of moving: that is, the commit identified by a branch name right now is not necessarily as the commit identified by that name yesterday, or tomorrow. This motion of branch names is how branches grow.) Of course if you ran git merge a123456, the other commit, the one for --theirs, is hash ID a123456.
  • The last commit is the merge base, which Git finds for you automatically. It finds this commit by using the parent linkages from your commit and the other commit, to work backwards through both branches until it finds the appropriate point where the two branches first come back together.

Having found the three commits, Git runs, in effect:

git diff --find-renames <merge-base> <ours>    # see what we changed
git diff --find-renames <merge-base> <theirs>  # see what they changed

The merge process—to merge as a verb, as it were—consists of finding these three commits, doing the diff, and combining the changes. You get a merge conflict when the two sets of changes affect the same lines.

In a file where there are no merge conflicts, Git puts the result into both your work-tree (as an ordinary file) and the index (as the special Git-form of the file, ready to be committed). So for unconflicted files, there is generally nothing else you need to do.

When there's a merge conflict, though, Git does two unusual things: first, it writes the merge-conflicted version into the work-tree, so that you can edit it as a plain file. Second, it writes into the index, not one version of the file, but all three: the merge base version, the "ours" version, and the "theirs" version.

Git calls these extra versions higher stages. Stage number is the merge base, and there's no --base option to access it, but you can use git show :1:path to see it. Stage number two is the "ours" version: there's --ours but you can also run git show :2:path to see it. Stage number 3 is the "theirs" version, available through git show :3:path. These three stages replace the normal stage-zero entry, which is now missing.

In fact, when you run git mergetool, what that does is find the three versions in the index, extract them into regular (non-Git-ified) files, and run the actual merge tool on those three files. The merge tool is assumed to Do The Right Thing (whatever that turns out to be) to combine the three files into one merged file, after which git mergetool can run git add on the result.

From the command line, though—which is how I do my merges—you can just edit the work-tree file, with its conflict markers, and figure out what the right result is. Write that out, git add the resulting file, and you're good, because git add notices that the file exists in the three-staged-versions form and erases those three versions, writing instead into stage number zero.

Once there's a stage zero (and no longer stages 1-3), the file is considered resolved.

Now, git checkout --ours -- path just tells Git: Take the stage-2 version out of the index and put it into the work-tree. The version with --theirs tells Git to take the stage-3 version instead. In both cases, the index, with its three staged versions, is left alone. This only extracts from the index, to the work-tree. (The -- here is just in case the path part is, say, a file named --theirs. If the file name doesn't resemble an option, you don't need the --. It's kind of a good habit to use the -- all the time, but most people don't.)

Since the index still has all three staged versions, the file is not yet resolved. Running git add takes the work-tree file and puts it in slot zero, wiping out the 1-through-3 entries, and now the file is resolved.

Curiously, running git checkout HEAD -- path or git checkout otherbranch -- path causes the file to become resolved. This is an artifact of Git letting the implementation dictate the interface: internally, when you use git checkout name -- path, Git has to first locate the Git form of the file in the given name (a commit hash or a name like HEAD or otherbranch). Then it has to copy that Git form into the index ... and this copying wipes out the slot 1-3 entries, writing into the normal slot-zero entry. Last, Git then extracts the (Git-form) file from index entry zero to the work-tree.

The side effect of this "write to index first, then extract from index to work-tree" is that if the file was in conflicted state—had stages 1-3 active—it's no longer conflicted! Hence:

git checkout --ours -- file

doesn't resolve the file (because it extracts from index slot 2), but:

git checkout HEAD -- file

does resolve the file (because it extracts from the current commit, going to index slot 0, wiping out 1-3; then extracts from the slot 0 entry it just wrote).

like image 153
torek Avatar answered Oct 24 '22 03:10

torek