在分叉中变基时保留删除



我分叉了一个代码库并删除了很多代码。我时不时地根据原始项目重新调整代码以接收更新。在其中一些更新中,我在 fork 中删除的文件被重新引入,git rebase 要求我解决这些冲突,导致我手动 git rm 这些文件。

有没有办法告诉 git rebase "如果我已经在我的 fork 中删除了这些文件,就不要重新引入它们"?

简短的回答是一个简单的(尽管不是很令人满意)的否定。 事实上,git rerere也无济于事,原因有二:

  1. 它仅适用于文件内冲突,不适用于"高级别"或"树级别"冲突。
  2. 重新引入文件根本不会导致冲突。

也就是说,这种"重新引入文件"的说法并不完全正确。 你得到的是上面第 1 点中提到的高级(或树级)冲突。 为了理解这一点,我们需要看看变基是如何作为一系列git cherry-pick操作工作的,从而了解一个git cherry-pick操作是如何工作的。

(对于您可以执行的操作,请跳到此答案的末尾。

变碱的胶囊摘要

我将跳过大部分变基细节,只注意git rebase

  1. 枚举一组要复制的提交;
  2. 分离的 HEAD是否git checkout(或 Git 2.23 或更高版本中git switch --detach)复制的提交应降落的位置;
  3. 复制每个提交,一次一个,就像调用git cherry-pick一样,或者有时实际上是通过调用 ;
  4. 复制所有提交后,像按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:这是我们在工作树中签出的文件集的来源。当前分支branch1branch1选择提交J的名称(无论其实际的大丑陋哈希 ID 是什么)。

提交J,像每个提交一样,有一个快照和一组父项。 像大多数提交一样,它只有一个父级,在本例中提交I. 提交I有一个父级,提交H;提交H有一个父级,提交G;等等。 同时,图中的另一个分支名称(branch2)选择提交L。 提交L具有快照和单个父K。 提交K有一个父级,H. 从现在开始向后 - 请记住,Git向后工作 - 一切都与分支branch1相同。

所有这些都意味着提交H(与每个提交一样,它有一个快照)是两个分支上最好的共享提交H之前的提交,如G,也是共享的,但它们不如JL两个分支提示提交更远。 Git 将通过从JL开始并像往常一样向后工作来找到提交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>

比较HL中的内容。 这种比较告诉 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 Jgit diff K L,但这实际上并不能帮助我们实现目标。 我们想使用git diff H Jgit diff H L,使用H两次,因此我们需要三次提交。

2这里的分叉这个词意味着GitHub分实际上也有类似的事情。 这些不是一回事,但由于 Git 是向后工作的,如果我们有一个包含合并的历史记录,Git 会将合并视为岔路口。 (而且,正如你可能听说过的那样,"当你来到一个岔路口时,就把它拿走。 使用 GitHub 分叉,最初的划分——H分叉到IK——发生在人们进行新提交时。 合并(如果有)稍后发生。

然而,更奇怪的是,由于 Git 是反向工作的,我们认为是一个分支,Git认为是一个组合。 它们形成合并基点。我们看作合并Git看作分叉!


处理冲突

当我们和他们都更改同一文件的相同行但以不同的方式更改时,就会发生通常的合并冲突:

I--J   <-- branch1 (HEAD)
/
...--G--H

K--L   <-- branch2

假设在提交H的文件F中,错误的单词中有拼写错误。 我们修复了拼写错误,它们替换了单词(反之亦然)。 当 Git 将我们对文件F的更改与他们对文件F的更改合并时,Git 将声明合并冲突并让我们修复混乱。

我们可以手工完成。 我们打开生成的工作树文件(其中包含两组更改,周围环绕着冲突标记),并看到它们通过修复错误的单词来修复拼写错误,因此我们保留了它们的更改,并通过删除我们的行和冲突标记来丢弃我们的更改,只留下它们的行。 或者我们可以使用合并工具,它通常会向我们显示所有三个文件——来自合并基础提交HF,来自我们提交的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,它位于JL中的一个但不是两个,但它的内容在这两个提交中的任何一个中都发生了变化。 这意味着要么我们完全删除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)

我们发现PC之间的区别正是我们需要添加的内容,在我们的提交J之后,进行新的提交C'K或任何我们选择称之为它的东西。 这将是C的副本,可以说是。 换句话说,我们希望让 Git 运行git diff P C来找出更改的内容,然后在我们的提交J中找到相同的代码并进行相同的更改。

如果一切顺利,我们将得到我们的提交C'。 Git 甚至会从提交C复制提交消息,以便我们新提交git show具有相同的日志消息。 快照J和快照C'之间的差异将与PC之间的差异相同,但行号可能除外。 因此,我们将这个新的提交称为C',以显示它与C的接近程度。

但是:Git 应该如何知道哪些匹配? 也许,在P-vs-C中,更改确实接近文件的顶部,但是在我们的J文件中,文件顶部有一堆新东西。 或者,也许在P-vs-C中,更改在文件的中间向下,但在J中,我们没有所有这些额外的东西,它靠近文件的顶部。

那么,我们需要 Git 做的是运行git diff P J. 这将告诉 Git 从PJ有什么不同,以便它可以排列PC更改。

但是如果我们要有 Git 差异PvsJ,以及PvsC...这听起来很像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 答案底部的建议可能是最通用的方法,因为您可以灵活地在自定义脚本中根据需要控制冲突解决。我以前做过类似的事情,在已知文件列表上取得了良好的结果,这些文件有时预计会出现冲突。

话虽如此,因为你是变基而不是合并,你愿意重写你现有的提交,让我认为你可以尝试一种相当简单的方法。您提到您不想每次都再次删除文件,但是如果您自动删除怎么办?换句话说,编写一个脚本来简单地删除所有不需要的文件,在脚本结束时它可以提交更改。您的新工作流程将是:

  1. 从所需存储库的最新副本中签出提交。
  2. 运行脚本以删除文件,并使用该更改进行新提交。
  3. 使用git rebase --onto将分支的旧副本(从删除文件的提交之后开始)到步骤 #2 中的新提交中重播提交范围。

所有这些步骤都应该直接自动化,以便您可以随时更新分支时运行单个脚本。这样,根本不需要任何冲突解决方案,至少不需要任何已删除的文件。

相关内容

  • 没有找到相关文章

最新更新