Reputation: 68698
While using git, suppose there is a local commit on main
and a new remote commit on origin/main
and I enter:
$ git pull --rebase
git will then rewind my local commit, apply the remote commit, and then try to merge my local commit.
Now suppose the merge fails, and there is a conflict in one binary file foo
:
You are currently rebasing branch 'main'.
(fix conflicts and then run "git rebase --continue")
Changes to be committed:
a
b
c
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: foo
and I want to either:
or
What command do I run for (1) ?
$ git take-remote-version-??? foo
$ git rebase --continue
What command do I run for (2) ?
$ git take-local-version-??? foo
$ git rebase --continue
Upvotes: 2
Views: 4036
Reputation: 30287
in general, the way you want to solve it (say, forget about the changes from one branch) is not really correct (you should go through them to see what's going on). Having said that, the way you get a file as it is in one of the 2 branches that is being merged/rebased, is like this:
git show :2:path-to-file > path-to-file # get this file as it is on _my_ branch
git show :3:path-to-file2 > path-to-file2 # get this file as it is on _the other_ branch/revision
git add path-to-file path-to-file2
git rebase --continue
Upvotes: 2
Reputation: 489388
eftshift0's answer shows one way to pick out the --ours
and --theirs
versions, using the numeric conventions to specify a particular index copy of some file. I've tried that while writing this up and they are not working; I could have sworn they did work at some point, but now --ours
and --theirs
seem to be required. You can (still?) use git show
, though: git show :1:path
, git show :2:path
, and git show :3:path
. Combine this with shell style redirection to replace the working tree copy, e.g., git show :3:path > path
.
There are several things to know before you proceed, though. The first and most important is that during a git rebase
, the --ours
and --theirs
checkout options are very misleading. The --ours
flag can mean their version and the --theirs
flag always means our version. (That's one reason to use the :2:
and :3:
spellings instead: at least they aren't misnamed!)
The reason this is the case is that Git is allowing the implementation behind rebase to show through. Many people call these "leaky abstractions", and there is an MIT CSAIL lecture on Git and version control that notes this as well. As Joel Spolsky notes, all abstractions tend to leak. Git, however, embraces the leaks, shouting them from rooftops, as it were.
This means that you need to know the following things:
git rebase
is, in effect (and sometimes in reality), mostly a series of git cherry-pick
commands.git cherry-pick
uses Git's merge engine.I'm going to omit most of what you need to know about part 4 here because it's long (but see, e.g., this answer). Also, you need to know most of this just to use Git at all. People try to skip over it because it's messy and complicated, and that's what leads to xkcd #1597. Let's instead jump straight to a description of Git's merge engine.
Suppose we have a series of commits that we might draw like this, with newer commits towards the right:
I--J <-- branch1 (HEAD)
/
...--G--H
\
K--L <-- branch2
Here we have two branches—more precisely, two branch names—branch1
and branch2
, which select commits J
and L
respectively. We have run git switch branch1
(or git checkout branch1
in older Git versions), so that our index and working tree are filled in from commit J
. We now run git merge branch2
, directing Git to look at commit L
.
The three commits that are inputs to this particular merge are:
Commit H
: this is the one Git calls the merge base. It is the best shared commit that is on both branches. We can see, visually, from the drawing, how it's on both branches: it's where they diverge. Why it's the best is mostly because it's the newest. Internally in Git, it's because it is the Lowest Common Ancestor of the Directed Acyclic Graph, but it's obvious by eyeball that it's "newer" than commit G
.
Commit J
: that's the one we have checked out right now. That's our commit. Or, in git merge
terms, it's --ours
.
Commit L
: that's the one we told git merge
to merge. That's their commit: --theirs
.
Git's merge engine now goes on to run two git diff
s, to compare the snapshot in commit H
to the ones in J
and L
. This produces two sets of operations: make these changes to this file and so on. The merge engine combines these two sets of changes, applying the combined changes to the snapshot in commit H
. That way, we keep our changes—the changes that we made to get from H
to J
—and add their changes from H
-vs-L
.
If something goes wrong in the combining process, Git halts in the middle of the merge, leaving us with a mess. This mess is in both Git's index, and our working tree. Our job is to clean up the visible mess in our working tree, and then run git add
to clean up Git's index too. Git itself really only cares about the stuff that goes into its index, but we use our working trees to make that stuff. So we care about fixing up the working tree files.
Anyway, having fixed up the working tree and run git add
, we run git merge --continue
or git commit
to get Git to finish the merge. Git then makes the new merge commit M
, which we can draw like this:
I--J
/ \
...--G--H M <-- branch1 (HEAD)
\ /
K--L <-- branch2
Like every commit, merge commit M
has a snapshot. The snapshot in M
is the one we proposed when we cleaned up the mess and ran git add
. Like every commit, merge commit M
has some parent commits. The thing that makes M
a merge commit is that it has two parents: J
, where we were when we ran git merge
in the first place, and L
, from the argument we gave to git merge
.
So, when we're doing the "clean up the mess" part, the --ours
and --theirs
flags make perfect sense. They refer to the version of some file that came out of commit J
—"ours"—or L
.
When we use git rebase
, we don't have a simple fork into two branches that we need to merge. Instead, we typically have a situation more like this:
I--J <-- our-branch (HEAD)
/
...--G--H--K--L <-- main
Our goal is to take the two existing commits I-J
and copy them to new and improved commits, which we'll call I'
and J'
to keep track of what they got copied from. What makes them new is that we're about to make them now. What makes them improved is that we'll have a different source base ("rebase") on which we make some change(s), and that the parent of I'
will be L
, with the parent of J'
being I'
, so that we get this:
I--J [abandoned]
/
...--G--H--K--L <-- main
\
I'-J' <-- our-branch (HEAD)
By making the name our-branch
select commit J'
, we abandon the original commits in favor of the new-and-improved ones. But there's the small matter of making these new-and-improved commits. That's where git rebase
does most of its work, but here, git rebase
fobs that work off on git cherry-pick
.
The rebase operation starts by listing the hash IDs of commits to copy, then doing a detached-HEAD switch / checkout of the place the copies are to go. In this case, that's commit L
, so that we have:
I--J <-- our-branch
/
...--G--H--K--L <-- main, HEAD
Then, rebase runs git cherry-pick hash-of-I
, to copy commit I
to the new commit I'
. The end result, if all goes well, will be this:
I--J <-- our-branch
/
...--G--H--K--L <-- main
\
I' <-- HEAD
If things don't go so well ... er, let's come back to that in a moment. 😀
Having successfully copied I
to I'
, git rebase
repeats the cherry-pick for commit J
:
I--J <-- our-branch
/
...--G--H--K--L <-- main
\
I'-J' <-- HEAD
That's the last of the commits to copy, in this example. If there were more commits to copy—say, ten commits, or a thousand, or whatever—git rebase
would keep on going, one commit at a time, using git cherry-pick
to copy the commits. Eventually it copies the last one, and that's when it stops the cherry-picking and moves the branch name:
I--J [abandoned]
/
...--G--H--K--L <-- main
\
I'-J' <-- our-branch (HEAD)
and that's the result we wanted, so git rebase
is now finished.
Just as with git merge
, the cherry-pick action can fail. When it does, Git stops in the middle of whatever it was doing. Cherry-picks can happen through use of the git cherry-pick
command, or through use of git rebase
. Git saves what it was doing somewhere,1 then ends the in-progress operation for now. You're now back at your shell prompt. It is now your job to resolve the mess, then tell Git to get back to work, with git rebase --continue
or git cherry-pick --continue
.
But—here's the important part for us here—a cherry-pick, no matter what caused it, actually uses Git's merge engine, just like git merge
. These repeated cherry-picks are actually mini-merges, that happen over and over again. And—since we're merging—there is an ours, theirs, and merge-base commit and the --ours
, --theirs
, and numeric git show :1:
style stuff refer to those.
But: What are we merging? That's where a diagram comes in handy. Let's go back to our first cherry-pick, that tries to copy I
to I'
:
I--J <-- our-branch
/
...--G--H--K--L <-- main, HEAD
Since HEAD
points to commit L
, the --ours
commit is commit L
. We had git rebase
run git cherry-pick hash-of-I
, so --theirs
is commit I
.
But commit L
is not our commit, in our own heads. Commit L
is their commit. When we ran git rebase
, commit J
was the current commit, not commit L
. Git has just slapped us in the face with one of its leaky abstractions. The fact is, we're in the middle of a rebase-initiated cherry-pick, and Git insists on proving it to us. So if we want our version of some file from commit I
, we need to run git checkout --theirs -- filename
. We want the one from commit I
, not the one from commit L
. If we want their version of the file, from commit L
, we must use --ours
.
This is documented in [the git checkout
documentation]. Let's look, though, at what happens if we get commit I
copied, and commit J
is the one that has an issue:
I--J <-- our-branch
/
...--G--H--K--L <-- main
\
I' <-- HEAD
Now --ours
means commit I'
, as that's where HEAD
points. The --theirs
option means commit J
, so --theirs
is ours. But --ours
is ... also ours? Whose commit is commit I'
?
This is what makes the ours/theirs flags to git checkout
and git restore
confusing during a rebase. The idea that some commit is "ours" and some commit is "theirs" works fine for git merge
. It works, but is backwards, for the first cherry-pick of a rebase. After that, the whole idea is just crazy. The commits are all ours, at this point. It makes more sense just to use the numbers.
1The somewhere has moved around, over the years. Fancy prompts for bash and zsh and the like have, built into them, knowledge about how to find out about a suspended merge, rebase, or cherry-pick, and will set your shell prompt to tell you that you are in the middle of an unfinished operation, including which one it is. The git status
command also knows where to look, and will tell you this—at least, in modern Git. Back in the Git 1.5 days, git status
was not as nice as it is now.
git add
When using git checkout --ours
or git checkout --theirs
, what you're doing is telling Git: copy, from your index, one of the three versions of some file, to my working tree. These three versions are from the merge base commit, from the HEAD
commit, and from the other commit: numbers 1, 2, and 3. The --ours
flag means to retrieve version #2, from HEAD
; the --theirs
flag means to retrieve version #3, from the other commit that's not the merge base. (There is no flag for version #1.)
Your job, during one of these in-progress merges that has not yet finished, is to clean up Git's index for Git. You'll do that with your working tree files, because the Git-ified files inside the index are unusable. But when you run git checkout --ours
or git checkout --theirs
, that doesn't affect Git's index.
So, if you used git checkout --ours somefile
and git checkout --theirs anotherfile
, you've replaced the working tree copy of these two files. But that's just for you. You still need to run git add somefile anotherfile
, to tell Git: Copy the working tree version of these files back into Git's index. It's the copy-back-into-index step that resolves the files. Git takes the expanded entries, that held three versions of some file, and collapses then down to one normal, un-expanded index entry. The resolved file from your working tree goes into that normal slot, in Git's index. That's how Git remembers that you told it that the file was ready to go into the next commit.
There are other constructs, such as git checkout commit-hash -- path
, that cause Git to overwrite the entry in Git's index, as well as overwriting the normal everyday copy of the file named path
. These particular constructs have the side effect of marking the file resolved, because any time you write a new copy to Git's index, that automatically erases any expanded-to-three-index-slots-for-one-file versions. Since the expanded state is how Git remembers that there was some problem merging some file, erasing the expanded state resolves the conflict.
Upvotes: 3