Say I am performing an interactive git rebase, to tidy up my repository by e.g. rearranging, separating, or squashing commits.
git rebase -i HEAD~100
Say also that I expect to encounter many merge conflicts where the desired behavior is to resolve as though passing -s recursive -X theirs
to git rebase
, but also that there will still be some conflicts where I will want to do something else, and I need to decide case by case how to proceed.
When rebase hits a merge conflict, it drops you into a shell in an environment where there is an ongoing partially completed merge. Is there a way I can discard this partially completed merge, and re-run the merge with my desired strategy, for just this one commit, without breaking my ongoing rebase?
Some command which I can use roughly like this:
git rebase --retry --step -s recursive -X theirs
The desired command should operate only on a single step of the rebase, applying just a single commit, overriding the default merge process, and if necessary, discarding an ongoing partially completed merge and repeating it with the same input files. Does any such command exist?
First, a short side note:
Say also that I expect to encounter many merge conflicts where the desired behavior is to resolve as though passing
-s recursive -X theirs
The -s recursive
here is the default for modern rebase (although it will probably soon be -s ort
, as the ort
strategy, which is the upcoming replacement for recursive
, is almost ready for general use). You can just use -X theirs
here.
When rebase hits a merge conflict, it drops you into a shell in an environment where there is an ongoing partially completed merge. Is there a way I can discard this partially completed merge, and re-run the merge with my desired strategy, for just this one commit, without breaking my ongoing rebase?
Yes. I'm afraid this answer is a bit long, as there is a lot of background to know here. I suspect you already know some of it (or maybe even all), but if so, forgive the loquaciousness. I've tried to organize this by section, at least.
This hasn't been very well documented in the past, and now that git rebase
is a C program, it's harder to see (no pun intended on the C/see homonym here) how it all works. But there's a general formula here. Any rebase, interactive or not, works by doing the following steps:
git checkout --detach
or git switch --detach
or equivalent to get a detached-HEAD check-out of the desired target (--onto
) commit.git cherry-pick
or equivalent to copy each commit in the original list, one by one. (Note that each cherry-pick is a slightly twisted form of merge, hence your desire to add -X
extended-strategy arguments to the merge strategy.)HEAD
. If there was no saved branch name (detached HEAD at startup), do nothing at this step (remain in detached-HEAD mode).There's some minor additional work involved here, such as updating reflogs, saving the original branch's stored hash ID in ORIG_HEAD
, and making and cleaning up a directory in which to save the rebase state in the case of merge conflicts and the like. But those four steps are the heart of rebasing.
Interactive rebase adds a big extra wrinkle, and lately, new capabilities as well: rather than just listing the commits to cherry-pick, then immediately going at it, the interactive rebase presents you with the list, in which each cherry-pick is a command line in an instruction sheet. You get to edit the instruction sheet, and only then turn over the sheet to the executor, who executes each instruction, one at a time.
Besides the basic pick
(do-a-cherry-pick) instruction, you get some alternatives, such as squash
and fixup
, reword
, edit
, and exec
; and in the newfangled --rebase-merges
mode, there are instructions that allow Git to run git reset
and git merge
and to save the hash IDs of commits generated by the various steps involved, which results in the ability to take a commit sequence that includes original merge commits, and re-run git merge
commands.1
In all of these cases, though, we preserve the heart of the original rebase: list commits; detach HEAD; copy commits, one by one; move branch name and re-attach HEAD. And, regardless of the type of merge, or the back-end to be used,2 it's possible for any one "copy commit" step to fail with a merge conflict. And that's just where your paragraph comes in.
1It's impossible to use git cherry-pick
to re-perform a merge, so a standard rebase drops merge commits. I won't, in this answer, go into details about how Git chooses which commits to copy for a rebase (this gets complicated), but using --rebase-merges
suppresses the dropping of merge commits: the merge commits are now "copied" using merge
commands in the instruction sheet. There's a defect of sorts here though: Git never saves the original merge's -s
and -X
options, so the re-performed merge later does not know to use -s ours
or -X theirs
or whatever was appropriate.
2The back end is responsible for the copying of each commit. In the bad old days, the one back-end available used git format-patch
and git am
. This back-end still exists today, but today the default back-end uses git cherry-pick
; the interactive back end always used git cherry-pick
, and git rebase -s recursive
, git rebase -k
, git rebase -m
, and git rebase -p
switched to the interactive back end, even if not actually working interactively. Some of the rebase options are still implemented only by the git am
-based back end.
When you do hit a merge conflict, the state, inside Git, at this point is:
A git am
or git cherry-pick
hit a conflict. This command has terminated, but it has left a merge conflict behind. This merge conflict is where all Git merge conflicts are: in Git's index.
The rebase command that ran the above command has also terminated. It has left behind some "resume state" so that you can run git rebase --continue
. This state is in the rebase temporary directory (inside the .git
directory; the precise location depends on whether you're in an added work-tree, and your Git version).
You are in detached HEAD state, and the current commit is the last commit that was successfully copied (or the --onto
target if this is the first commit and it has yet to be successfully copied).
Your job at this point is to resolve the merge conflict. You can do this any way you like. When you run git rebase --continue
, Git expects the index and working tree to be ready to complete this copy, by running git commit
to commit the cherry-pick. You may, optionally, run git commit
yourself; if you do, Git may be able to guess that you committed the copy, and will proceed to the next commit it should copy, or you can explicitly tell Git to proceed anyway, with git rebase --skip
.3
In your case, you can start with:
git reset --hard
to clean up the index and reset the working tree. Then, do whatever you like to set up the index: for instance,
git cherry-pick -n -X theirs <hash>
The -n
here is to make sure that git cherry-pick
does not go on to make the commit itself. If it does, that's not too big a problem: just use git rebase --skip
instead of git rebase --continue
. I tested this with Git 2.27 and it did not auto-detect the need to skip.
I did find that the commit being cherry-picked (with the failure) is stored in REBASE_HEAD
at this point. In older versions of interactive rebase, which actually ran git cherry-pick
directly—the current one has it built in and thus can make a few internal alterations—it would be in CHERRY_PICK_HEAD
. You can also find its hash ID (abbreviated) in git status
output. So the command sequence I actually used was:
$ git status
interactive rebase in progress; onto 7753c04
Last commands done (2 commands done):
pick 2cd436b 1
pick a50fcb5 4
Next commands to do (2 remaining commands):
pick 7bcbde0 2
pick d162089 3
(use "git rebase --edit-todo" to view and edit)
You are currently rebasing branch 'master' on '7753c04'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: afile
no changes added to commit (use "git add" and/or "git commit -a")
$ git reset --hard
HEAD is now at 2cd436b 1
$ git cherry-pick -n -X theirs REBASE_HEAD
Auto-merging afile
$ git rebase --continue
hint: Waiting for your editor to close the file...
(and at this point my editor was open on the commit message). I wrote that out, and:
[detached HEAD 8c95399] 4
1 file changed, 2 insertions(+)
Auto-merging afile
CONFLICT (content): Merge conflict in afile
error: could not apply 7bcbde0... 2
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 7bcbde0... 2
$ git reset --hard
HEAD is now at 8c95399 4
$ git rev-parse REBASE_HEAD
7bcbde00fb66f08d46b1abc5f718c88d144179c8
$ git cherry-pick -n -X theirs REBASE_HEAD
Auto-merging afile
$ git rebase --continue
hint: Waiting for your editor to close the file...
Writing that out and exiting my editor then finished my rebase testing:
[detached HEAD 5ce59c6] 2
1 file changed, 1 deletion(-)
Successfully rebased and updated refs/heads/master.
3I don't think this "I'll figure out that you did a commit, so I'll --skip
for you" part was ever explicitly called out, so there might be versions of Git where it doesn't work. I recall seeing it happen, but in testing just now, it didn't happen—but that might have to do with the particular case I'm testing here.
edit
commandWhen you change pick
to edit
in the interactive instructions, this tells the rebase code that, after successfully copying the commit, it should pause, much as it would after a failed copy that leaves conflicts in the index. But this stops after the successful copy, so the state is different:
git rebase --continue
so that it can read the saved state and continue.If you want to make changes to the copy that Git just made, you can do so:
git reset
and/or git add
and/or any other Git commands you like, to update your index.git commit --amend
. This shoves the current (HEAD
) commit aside, making instead a new commit whose parent(s) is/are the parent(s) of the current commit. HEAD
then becomes this new commit. Since you're in detached HEAD state, the previously-current commit's hash ID is only to be found in the HEAD
reflog.You can use this to "split" a commit that has too much stuff in it. For instance, suppose you made a commit that fixes two bugs, but you realize afterwards that it would be better to make two separate commits. It's no longer the most recent commit, where you updated the documentation. So, you can run git rebase -i HEAD~2
to redo the last two commits. Change the first one from pick
to edit
and write out the instruction sheet. Then:
# HEAD commit fixed two bugs, one in file-one.py and one in file-two.py.
# Get file-two.py from HEAD~ into the index, leaving the fix in the
# working tree.
git restore --source=HEAD~ file-two.py -S
git commit --amend --edit
and fix up the commit message to say that we are fixing just the one file, then:
git add file-two.py
git commit
and write a new commit message about fixing just this one file. Then:
git rebase --continue
to copy the remaining commit with the documentation update.
The things to keep in mind are:
git cherry-pick
, so that's what rebase does now. Another way is git format-patch ... | git am
, so the old rebase did that; this method has the advantage of being able to copy multiple commits, but the disadvantage of not being able to copy an "empty" (no-diff) commit.The reason for the work stoppage determines the precise state: it's always detached-HEAD, but sometimes it's in the middle of a conflict, and sometimes it's because you said edit
. If it's in the middle of a conflict, the copy that was supposed to happen is also still in progress, so there's no actual copy yet; if not, the copy did happen, and then rebase stopped.