Possible to resolve Git conflict on single file using Ours / Theirs?
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
orgit rev-parse @
to get its hash ID). - One is the commit you just named. If you ran
git merge otherbranch
, you can rungit 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 rangit merge a123456
, the other commit, the one for--theirs
, is hash IDa123456
. - 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).
Edit, Jun 2022: Since I wrote the above, Git acquired a new pair of commands, git switch
and git restore
, that split up the multiple different jobs that git checkout
used to combine. If your Git is version 2.23 or later, you might wish to use git restore
rather than git checkout
for extracting individual files. (When using the old git checkout
, it's too easy to invoke it the wrong way: even experienced Git users will sometimes mess up here.) Still, the --ours
and --theirs
flags to git restore
work the same way as with git checkout
: they select slots 2 and 3 respectively, always, even during a rebase (see the Postscript below).
Postscript
The above describes the merge-as-a-verb process invoked by the git merge
command. Git's internal merge engines—"merge as a verb"—are also used by git cherry-pick
, git revert
, and git rebase
(among others). When using these, the "merge base" commit—the entity that fills slot 1—is picked via a different process, and the "ours" and "theirs" commits—the things that go into slots 2 and 3, as named by, e.g., --ours
with git checkout
or git restore
for instance, aren't always straightforward. I will leave the details for other StackOverflow questions-and-answers, as this is plenty to digest.