变基时如何处理与给定策略的特定合并冲突?
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 --detach
或 git 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
(随机选择)指令外,您还有一些替代方法,例如 squash
和 fixup
、reword
、edit
,和exec
;在新奇的 --rebase-merges
模式下,有指令允许 Git 到 运行 git reset
和 git 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-patch
和 git am
。这个后端今天仍然存在,但今天默认后端使用 git cherry-pick
;交互式后端总是使用 git cherry-pick
,而 git rebase -s recursive
、git rebase -k
、git rebase -m
和 git rebase -p
切换到交互式后端,即使实际上不是交互式工作.一些 rebase 选项仍然仅由基于 git am
的后端实现。
合并时的状态冲突
当您遇到合并冲突时,Git 内的状态此时为:
git am
或 git 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 停止。
说我正在执行一个交互式 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 --detach
或git 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
(随机选择)指令外,您还有一些替代方法,例如 squash
和 fixup
、reword
、edit
,和exec
;在新奇的 --rebase-merges
模式下,有指令允许 Git 到 运行 git reset
和 git 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-patch
和 git am
。这个后端今天仍然存在,但今天默认后端使用 git cherry-pick
;交互式后端总是使用 git cherry-pick
,而 git rebase -s recursive
、git rebase -k
、git rebase -m
和 git rebase -p
切换到交互式后端,即使实际上不是交互式工作.一些 rebase 选项仍然仅由基于 git am
的后端实现。
合并时的状态冲突
当您遇到合并冲突时,Git 内的状态此时为:
git am
或git 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/orgit 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 停止。