我分叉了一个代码库并删除了很多代码。我时不时地根据原始项目重新调整代码以接收更新。在其中一些更新中,我在 fork 中删除的文件被重新引入,git rebase 要求我解决这些冲突,导致我手动 git rm 这些文件。
有没有办法告诉 git rebase "如果我已经在我的 fork 中删除了这些文件,就不要重新引入它们"?
简短的回答是一个简单的(尽管不是很令人满意)的否定。 事实上,git rerere
也无济于事,原因有二:
- 它仅适用于文件内冲突,不适用于"高级别"或"树级别"冲突。
- 重新引入文件根本不会导致冲突。
也就是说,这种"重新引入文件"的说法并不完全正确。 你得到的是上面第 1 点中提到的高级(或树级)冲突。 为了理解这一点,我们需要看看变基是如何作为一系列git cherry-pick
操作工作的,从而了解一个git cherry-pick
操作是如何工作的。
(对于您可以执行的操作,请跳到此答案的末尾。
变碱的胶囊摘要
我将跳过大部分变基细节,只注意git rebase
:
- 枚举一组要复制的提交;
-
分离的 HEAD是否
git checkout
(或 Git 2.23 或更高版本中git switch --detach
)复制的提交应降落的位置; - 复制每个提交,一次一个,就像调用
git cherry-pick
一样,或者有时实际上是通过调用 ;
和 - 复制所有提交后,像按
git branch -f
一样移动分支名称,然后将HEAD
重新附加到该分支名称。
结果是,如果我们从例如:
I--J--K <-- ourbranch (HEAD)
/
...--G--H--L <-- updated-upstream
并运行git rebase updated-upstream
,我们得到:
I--J--K [abandoned]
/
...--G--H--L <-- updated-upstream
I'-J'-K' <-- ourbranch (HEAD)
其中I'
、J'
和K'
是三个 Cherry pick-Pick 操作I-J-K
提交的副本。 原始提交仍然存在(并且可以恢复一段时间,以防变基失败)。 只是它们现在更难找到,因为名称ourbranch
现在定位提交K'
- 新的和改进的(?)副本 - 而不是原始的提交K
。
变基的关键是确保步骤 1 中枚举的提交集正确,步骤 2 中的位置正确,并且步骤 3 中的每个副本都正确。 步骤 4 中的分支名称摆弄是流程中最明显但最不重要的事情。 这是因为 Git 是关于提交的;分支名称只是用来查找提交。 (查找步骤当然很重要! 如果我们找不到提交,那有什么用呢? 但是还有其他方法可以找到提交,一旦我们找到它们,提交才是最重要的。
樱桃采摘是一种合并
因为挑剔操作是一种合并(尽管有一个扭曲),所以我们应该首先查看正常的合并。 同样,这只是一个隐藏大量重要细节的胶囊摘要,但我们从一系列提交开始,如下所示:
I--J <-- branch1 (HEAD)
/
...--G--H
K--L <-- branch2
这张图意味着我们"在分支 1 上",正如git status
所说。 当前提交是提交J
:这是我们在工作树中签出的文件集的来源。当前分支为branch1
。branch1
选择提交J
的名称(无论其实际的大丑陋哈希 ID 是什么)。
提交J
,像每个提交一样,有一个快照和一组父项。 像大多数提交一样,它只有一个父级,在本例中提交I
. 提交I
有一个父级,提交H
;提交H
有一个父级,提交G
;等等。 同时,图中的另一个分支名称(branch2
)选择提交L
。 提交L
具有快照和单个父K
。 提交K
有一个父级,H
. 从现在开始向后 - 请记住,Git向后工作 - 一切都与分支branch1
相同。
所有这些都意味着提交H
(与每个提交一样,它有一个快照)是两个分支上最好的共享提交。H
之前的提交,如G
,也是共享的,但它们不如J
和L
两个分支提示提交更远。 Git 将通过从J
和L
开始并像往常一样向后工作来找到提交H
。 由于它位于两个分支上,并且是最好的此类提交,因此 Git 将使用提交H
作为合并操作的合并基础。
在 Git 中,合并包括或多或少地执行两个git diff
。 要执行两个git diff
,我们需要三个提交。1这让 Git 运行:
git diff --find-renames <hash-of-H> <hash-of-J>
要比较提交H
快照中的内容与提交J
快照中的内容。 这种比较告诉 Git我们在branch1
中改变了什么。
然后 Git 重复此操作,但提交L
:
git diff --find-renames <hash-of-H> <hash-of-L>
比较H
和L
中的内容。 这种比较告诉 Git他们在branch2
中改变了什么。
如果我们现在合并两组更改,并将组合的更改应用于来自H
的快照 — 不是来自K
的更改,不是来自L
的快照,而是来自H
的快照 — 这将把我们的更改及其更改加在一起。 生成的组合更改,应用于快照H
,为我们提供了一个快照,该快照保留了我们的更改,但也添加了它们的更改。 或者,如果您愿意,它会保留他们的更改,但也添加我们的更改。 无论哪种方式,只要没有冲突,效果都是一样的。
因此,如果这里一切顺利,Git会保留我们的更改并添加它们的更改,或者添加我们的更改及其更改并将其应用于基础,或者您希望查看它的任何方式。 结果是一个新的快照,准备进入新的提交。 Git 自行进行这个新提交,并将其称为合并提交。 Git 记得这是一个合并提交,通过与两个父级进行提交,而不是通常的提交:
I--J
/
...--G--H M <-- branch1 (HEAD)
/
K--L <-- branch2
这是一个正常的、无冲突的合并。 与任何提交一样,Git 将新提交的哈希 ID 写入当前分支名称,以便branch1
现在选择新的合并提交M
。 与所有提交一样,M
有一个快照:快照是应用两组更改的结果,在使用git diff
两次以查找两组更改后。 与大多数提交不同,M
有两个父级,而不是通常的父级。 这意味着当 Git 查看历史记录时,通过向后工作,它必须在这里的两个"分叉"上向后工作。阿拉伯数字
1可以这样想:git diff
总是需要两次提交。 如果我们使用相同的两个提交(例如,如果我们运行git diff J L
),我们只会得到相同的差异。 因此,要获得两个不同的差异输出,我们至少需要三个提交。 我们可以使用四个,例如,git diff I J
和git diff K L
,但这实际上并不能帮助我们实现目标。 我们想使用git diff H J
和git diff H L
,使用H
两次,因此我们需要三次提交。
2这里的分叉这个词意味着GitHub分叉实际上也有类似的事情。 这些不是一回事,但由于 Git 是向后工作的,如果我们有一个包含合并的历史记录,Git 会将合并视为岔路口。 (而且,正如你可能听说过的那样,"当你来到一个岔路口时,就把它拿走。 使用 GitHub 分叉,最初的划分——H
分叉到I
和K
——发生在人们进行新提交时。 合并(如果有)稍后发生。
然而,更奇怪的是,由于 Git 是反向工作的,我们认为是一个分支,Git认为是一个组合。 它们形成合并基点。我们看作合并,Git看作分叉!
处理冲突
当我们和他们都更改同一文件的相同行但以不同的方式更改时,就会发生通常的合并冲突:
I--J <-- branch1 (HEAD)
/
...--G--H
K--L <-- branch2
假设在提交H
的文件F中,错误的单词中有拼写错误。 我们修复了拼写错误,它们替换了单词(反之亦然)。 当 Git 将我们对文件F的更改与他们对文件F的更改合并时,Git 将声明合并冲突并让我们修复混乱。
我们可以手工完成。 我们打开生成的工作树文件(其中包含两组更改,周围环绕着冲突标记),并看到它们通过修复错误的单词来修复拼写错误,因此我们保留了它们的更改,并通过删除我们的行和冲突标记来丢弃我们的更改,只留下它们的行。 或者我们可以使用合并工具,它通常会向我们显示所有三个文件——来自合并基础提交H
的F,来自我们提交的F,以及来自其提交的F。 合并工具执行此操作的确切方法在很大程度上取决于工具;我们不会担心这一点,只会假设我们得到了正确的结果。
或者,我们可以使用-X ours
或-X theirs
来选择我们的更改或更改,而忽略另一"方"的更改。 这里的缺点是我们必须知道谁的更改是正确的:我们在运行时选择-X
选项git merge
,然后再看到冲突本身。 如果,有时,我们的改变更好,-X theirs
是行不通的。 如果他们的改变有时更好,-X ours
也不会奏效。 有时你可能会确定他们的改变,或者你的改变,总是会更好;这就是-X
选项的帮助所在。3如果你不能绝对确定,我建议你避免-X
,只是自己解决冲突。
3请记住,-X 在变基过程中倾向于"向后":-X theirs
表示我们的原始提交,-X ours
表示......有些复杂。 请参阅 git 中"我们的"和"他们的"的确切含义是什么?
高级别冲突
在讨论上面的冲突时,我们正在查看一个特定文件的特定行。 但这些并不是我们能遇到的唯一冲突。 假设在H
中没有名为 F的文件。 相反,我们从头开始编写自己的文件F。 它有针对一种情况的东西。同时,他们从头开始编写自己的文件F,它完全适用于其他情况。 只选择我们的 F 是不正确的,选择他们的F显然也是不正确的。例如,也许我们应该将F
重命名为其他名称,以便我们可以存储两个文件。 或者,将他们的文件与我们的文件合并是有意义的。
不过,就Git而言,关键是提交H
中根本没有文件F。 Git 称之为添加/添加冲突,这意味着 Git 无法自行解决它,即使使用-X
也无法解决。 我喜欢称这些高级冲突,因为它们是在进行低级文件合并之前发生的 Git 部分中生成的。 (低级文件合并——至少是它的开始——在ll-merge.c
中,ll
代表低级。 其他人喜欢称之为树冲突,因为参与查找它的 Git 部分正在查看 Git 提交中的文件树结构。
还有其他冲突会达到相同的高(或树)级别代码。 这包括如果您删除了一个文件,他们对其进行了修改,反之亦然:也就是说,H
中有一些文件F,它位于J
和L
中的一个但不是两个,但它的内容在这两个提交中的任何一个中都发生了变化。 这意味着要么我们完全删除F,要么他们完全删除F。 谁没有删除F修复/更改其中的某些内容。 这是一个修改/删除冲突,与添加/添加冲突一样,Git将始终在此处停止冲突。
樱桃采摘合并
当我们(人类)运行git cherry-pick
时,我们通常想要复制一个提交。 也就是说,假设我们在某个分支上有这一系列提交:
...--o--o--P--C--o--...--tip <-- branch1
有一些子提交C
与父P
。 如果我们运行 Git:
git diff <hash-of-P> <hash-of-C>
(或者只是git showhash-of-C
,其中包括此差异)我们将看到P
中的快照和C
中的快照之间的变化。
同时,我们正在进行其他一些提交,也许完全在其他分支上:
...--H--I--J <-- branch2 (HEAD)
我们发现P
和C
之间的区别正是我们需要添加的内容,在我们的提交J
之后,进行新的提交C'
或K
或任何我们选择称之为它的东西。 这将是C
的副本,可以说是。 换句话说,我们希望让 Git 运行git diff P C
来找出更改的内容,然后在我们的提交J
中找到相同的代码并进行相同的更改。
如果一切顺利,我们将得到我们的提交C'
。 Git 甚至会从提交C
复制提交消息,以便我们新提交git show
具有相同的日志消息。 快照J
和快照C'
之间的差异将与P
和C
之间的差异相同,但行号可能除外。 因此,我们将这个新的提交称为C'
,以显示它与C
的接近程度。
但是:Git 应该如何知道哪些行匹配? 也许,在P
-vs-C
中,更改确实接近文件的顶部,但是在我们的J
文件中,文件顶部有一堆新东西。 或者,也许在P
-vs-C
中,更改在文件的中间向下,但在J
中,我们没有所有这些额外的东西,它靠近文件的顶部。
那么,我们需要 Git 做的是运行git diff P J
. 这将告诉 Git 从P
到J
有什么不同,以便它可以排列P
与C
更改。
但是如果我们要有 Git 差异P
vsJ
,以及P
vsC
...这听起来很像git merge
,不是吗? 假设我们让 Git 执行这两个差异,然后将提交P
视为合并基础提交。 在P
的快照中,Git 将在J
中添加所有"我们的"更改,以便我们保留更改。 在P
的快照中,Git 将添加C
中所有"他们的"更改,以便我们获得它们的更改。 这将为我们提供正确的组合,以便我们保留我们所拥有的,但添加他们的P
- vs-C
更改。
这就是git cherry-pick
所做的。 它将父提交P
视为合并基,J
(我们的HEAD
)视为我们的提交,C
(他们的子提交)视为他们的提交。 所有-X
选项都像以前一样工作,-X ours
表示 P 与J,-X theirs
表示P 与 C。
现在,在您的提交中,您已经完全删除了一些文件。 他们没有。 所以P
-vs-J
会说删除这片文件。 在他们的提交C
中,他们可能已经更改了他们的一个文件。 这是一个修改/删除冲突,合并的"一方"删除了文件,而他们的一方修改了文件。 由于这是高级别/树级冲突,因此无论任何-X
选项如何,Git都将停止。 您必须通过确认要删除文件来解决此冲突。
您可以做些什么来让您的生活更轻松
您可以编写自己的脚本,以便在git cherry-pick
(或git rebase
的樱桃选择)产生包含修改/删除冲突的合并冲突的情况下运行。 您可以通过检查 Git 的索引来查找这些冲突。 查看git ls-files --stage
输出(请注意,它很长),并查找在第 1 阶段(合并基础,即它们的 P 提交)和第 3 阶段(它们的 C)中存在文件但在阶段 2(HEAD,即您的提交 J 或等效项)中不存在的情况。 通过删除阶段 1 和阶段 3 中的两个条目来解决冲突。 例如,您可以使用您知道有意删除的文件列表以编程方式执行此操作。 之后,git status
会告诉您是否还有其他冲突。
令人讨厌的是,没有办法让 Git 在冲突时自动运行此脚本。 但是,如果你让脚本检测是否仍然存在任何冲突,以及git status
是否告诉你你正处于变基过程中,你可以让它适当地运行git rebase --continue
,这至少将所有内容简化为单个 shell 命令。
如果你假设答案是否定的,那么你只剩下你应该怎么做?
我认为 torek 答案底部的建议可能是最通用的方法,因为您可以灵活地在自定义脚本中根据需要控制冲突解决。我以前做过类似的事情,在已知文件列表上取得了良好的结果,这些文件有时预计会出现冲突。
话虽如此,因为你是变基而不是合并,你愿意重写你现有的提交,让我认为你可以尝试一种相当简单的方法。您提到您不想每次都再次删除文件,但是如果您自动删除怎么办?换句话说,编写一个脚本来简单地删除所有不需要的文件,在脚本结束时它可以提交更改。您的新工作流程将是:
- 从所需存储库的最新副本中签出提交。
- 运行脚本以删除文件,并使用该更改进行新提交。
- 使用
git rebase --onto
将分支的旧副本(从删除文件的提交之后开始)到步骤 #2 中的新提交中重播提交范围。
所有这些步骤都应该直接自动化,以便您可以随时更新分支时运行单个脚本。这样,根本不需要任何冲突解决方案,至少不需要任何已删除的文件。