变基时如何处理与给定策略的特定合并冲突?

How do I process a specific merge conflict with a given strategy when rebasing?

说我正在执行一个交互式 git 变基,通过例如整理我的存储库重新排列、分离或压缩提交。

git rebase -i HEAD~100

还要说我预计会遇到许多合并冲突,其中所需的行为是像将 -s recursive -X theirs 传递给 git rebase 一样解决,但我也希望在其中仍然存在一些冲突做别的事情,我需要根据具体情况决定如何进行。

当 rebase 遇到合并冲突时,它会让您进入一个 shell 环境,其中正在进行部分完成的合并。有没有一种方法可以让我放弃这个部分完成的合并,并根据我想要的策略重新运行 合并,仅针对这一次提交,而不会破坏我正在进行的 rebase?

我可以大致像这样使用的一些命令:

git rebase --retry --step -s recursive -X theirs

所需的命令应仅在变基的单个步骤上运行,仅应用一次提交,覆盖默认合并过程,并且如有必要,丢弃正在进行的部分完成的合并并使用相同的输入文件重复它。是否存在这样的命令?

首先,简短的旁注:

Say also that I expect to encounter many merge conflicts where the desired behavior is to resolve as though passing -s recursive -X theirs

这里的 -s recursive 是现代 rebase 的默认值(尽管它可能很快就会成为 -s ort,作为 ort 策略,即将替代 recursive,几乎可以用于一般用途)。你可以在这里使用-X theirs

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?

是的。恐怕这个答案有点长,因为这里有很多背景知识。我怀疑您已经知道其中的一些(甚至可能全部),但如果是这样,请原谅我多嘴多舌。我试着至少按部分组织这个。

背景

这在过去并没有得到很好的记录,现在 git rebase 是一个 C 程序,很难看出(这里没有双关语C/see 同音词)它是如何所有作品。但是这里有一个通用公式。 任何 rebase,无论是否交互,都通过执行以下步骤来工作:

  • 枚举要复制的提交(如果有的话)。保存当前 b运行ch 名称,或者记住当前位置是一个“分离的 HEAD”。
  • 使用 git checkout --detachgit switch --detach 或等效方法获取所需目标 (--onto) 提交的分离头检出。
  • 使用git cherry-pick 或等价物逐一复制原始列表中的每个提交。 (请注意,每个 cherry-pick 都是一种稍微扭曲的合并形式,因此您希望向合并策略添加 -X 扩展策略参数。)
  • 一旦复制了所有提交,使用保存的 b运行ch 名称强制 b运行ch 名称指向现在 HEAD 的任何提交。如果没有保存 b运行ch 名称(启动时分离 HEAD),则在这一步什么也不做(保持分离 HEAD 模式)。

这里涉及一些小的附加工作,例如更新 reflogs,将原始 b运行ch 的存储哈希 ID 保存在 ORIG_HEAD 中,以及创建和清理一个目录来保存在合并冲突等情况下重新设置状态。但这四个步骤是变基的核心。

Interactive rebase 增加了一个额外的皱纹,最近,还有新功能:不仅仅是列出对 cherry-pick 的提交,然后立即进行,交互式rebase 向 you 提供列表,其中每个 cherry-pick 都是指令 sheet 中的命令行。您可以编辑指令 sheet,然后才将 sheet 交给执行者,执行者一次执行一条指令。

除了基本的 pick(随机选择)指令外,您还有一些替代方法,例如 squashfixuprewordedit,和exec;在新奇的 --rebase-merges 模式下,有指令允许 Git 到 运行 git resetgit merge 并保存生成的提交的哈希 ID 通过涉及的各个步骤,这导致能够采用包括原始合并提交的提交序列,并重新执行运行 git merge命令。1

尽管如此,在所有这些情况下,我们都保留了原始 rebase 的核心:list commits;分离头;复制提交,一个接一个;移动 b运行ch 名称并重新附加 HEAD。而且,无论合并的 类型 或要使用的后端如何,2 任何一个“复制提交”步骤都是可能的因合并冲突而失败。这正是您的段落的用武之地。


1不可能使用git cherry-pick重新执行合并,所以一个标准的rebase drops合并提交。在这个答案中,我不会详细说明 Git 如何选择 提交复制 以进行变基(这变得复杂),但使用 --rebase-merges抑制合并提交的删除:现在使用指令 sheet 中的 merge 命令“复制”合并提交。不过这里有一些缺陷:Git 永远不会保存原始合并的 -s-X 选项,因此稍后重新执行的合并不知道使用 -s ours-X theirs 或任何合适的内容。

2后端负责每次commit的拷贝。在糟糕的过去,唯一可用的后端使用 git format-patchgit am。这个后端今天仍然存在,但今天默认后端使用 git cherry-pick;交互式后端总是使用 git cherry-pick,而 git rebase -s recursivegit rebase -kgit rebase -mgit rebase -p 切换到交互式后端,即使实际上不是交互式工作.一些 rebase 选项仍然仅由基于 git am 的后端实现。


合并时的状态冲突

当您遇到合并冲突时,Git 内的状态此时为:

  • git amgit cherry-pick 命中冲突。此命令 已终止 ,但它留下了合并冲突。此合并冲突是所有 Git 合并冲突所在的位置:在 Git 的索引中。

  • 运行上述命令的rebase命令也已终止。它留下了一些“恢复状态”,以便您可以 运行 git rebase --continue。此状态位于 rebase 临时目录中(在 .git 目录中;精确位置取决于您是否在添加的工作树中,以及您的 Git 版本)。

  • 你处于detached HEAD状态,当前提交是最后一次成功复制的提交(或者--onto目标,如果这个是第一次提交,尚未成功复制)。

此时你的工作是解决合并冲突。您可以按照自己喜欢的方式执行此操作。当您 运行 git rebase --continue、Git 期望索引和工作树准备好完成此副本时,通过 运行ning git commit 提交 cherry-pick .您可以选择 运行 git commit 自己;如果你这样做,Git可能能够猜测你提交了副本,并将继续进行下一次它应该复制的提交,或者你可以明确告诉Git 继续,git rebase --skip.3

对于你的情况,你可以从:

开始
git reset --hard

清理索引并重置工作树。然后,随心所欲地设置索引:例如,

git cherry-pick -n -X theirs <hash>

这里的-n是为了确保git cherry-pick不会继续进行提交本身。如果是,那不是什么大问题:只需使用 git rebase --skip 而不是 git rebase --continue。我用 Git 2.27 测试了这个,它没有自动检测是否需要跳过。

此时我确实发现被挑选的提交(失败)存储在 REBASE_HEAD 中。在旧版本的交互式 rebase 中,实际上直接 运行 git cherry-pick——当前版本内置了它,因此可以进行一些内部更改——它会在 CHERRY_PICK_HEAD 中。您还可以在 git status 输出中找到它的哈希 ID(缩写)。所以我实际使用的命令序列是:

$ 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... 

(此时我的编辑器已打开提交消息)。我写出来了,并且:

[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... 

写完并退出我的编辑器然后完成我的 rebase 测试:

[detached HEAD 5ce59c6] 2
 1 file changed, 1 deletion(-)
Successfully rebased and updated refs/heads/master.

3我不认为这个“我会弄清楚你做了提交,所以我会 --skip 为你”部分是明确的叫出来,所以可能有 Git 的版本不起作用。我记得看到它发生了,但在刚才的测试中,它并没有发生——但这可能与我在这里测试的特定情况有关。


使用edit命令

当您在交互式指令中将 pick 更改为 edit 时,这会告诉 rebase 代码,在成功复制提交之后,它应该暂停,就像在失败的复制之后一样在索引中留下冲突。但是这样就停止了after复制成功,所以状态不一样了:

  • 您仍处于分离的 HEAD 状态,但当前提交 是刚刚制作的副本Git。
  • rebase 命令已终止,但仍像往常一样留下状态,希望您 运行 git rebase --continue 以便它可以读取保存的状态并继续。

如果您想对Git刚刚制作的副本进行更改,您可以这样做:

  • 根据需要更新您的工作树 and/or 使用 git reset and/or git add and/or 您喜欢的任何其他 Git 命令来更新你的指数。
  • 运行git commit --amend。这将当前 (HEAD) 提交推到一边,取而代之的是创建一个新提交,其父级 is/are 当前提交的父级。 HEAD 然后成为这个新的提交。由于您处于分离的 HEAD 状态,因此只能在 HEAD reflog 中找到先前当前提交的哈希 ID。

您可以使用它来“拆分”包含太多内容的提交。例如,假设您进行了修复 两个 错误的提交,但之后您意识到最好进行两次单独的提交。它不再是您更新文档的 most 最近提交。因此,您可以 运行 git rebase -i HEAD~2 重做最后两次提交。将第一个从 pick 更改为 edit 并写出指令 sheet。那么:

# 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

并修改提交消息,说明我们只修复了一个文件,然后:

git add file-two.py
git commit

并写一条关于只修复这个文件的新提交消息。那么:

git rebase --continue

将剩余的提交与文档更新一起复制。

结论

要记住的事情是:

  • 从根本上说,Rebase 是通过将旧的提交(旧的和糟糕的?)复制到新的(新的和改进的?)提交来工作的。
  • 复制一次提交的常用方法是使用git cherry-pick,所以现在rebase就是这么做的。另一种方式是 git format-patch ... | git am,所以旧的 rebase 就是这样做的;此方法的优点是能够复制多个提交,但缺点是无法复制“空”(无差异)提交。
  • 我们找到提交的方式是从b运行ch名称开始,然后向后工作,因此,将一些提交复制到新的和改进的提交,我们需要 Git 移动一个 b运行ch 名称。因此,Rebase 通过移动 b运行ch 名称来结束——要移动的名称是我们开始时 were 的 b运行ch。在内部,rebase 使用分离头模式。
  • 任何时候 any rebase 停止,它都处于这种内部分离 HEAD 模式。你的工作是修复任何需要修复的东西,然后继续变基。

停工的原因决定了准确的状态:一直是detached-HEAD,但有时是在冲突中,有时是因为你说了edit。如果它处于冲突之中,那么应该发生的复制也仍在进行中,因此还没有实际的 copy;如果没有,复制 did 发生,然后 rebase 停止。