作为DavidN said in a comment,你的计划看起来很合理。
(其余部分太长且杂乱无章;它是在其他任务之间/期间编写的。)
题外话和细节
由于H是git merge --squash创建的,所以画错了。它应该是:
A -- B -- C -- D -- H master
\
E -- F -- G topicA
\
I -- J -- K topicB
关键区别在于提交H 与E--F--G 序列不相关,至少在任何Git 检测到的意义上都没有。提交H 的content 会受到E--F--G 序列中发生的任何事情的影响(当然,也受到C--D 序列中发生的任何事情的影响)但据Git 现在所知,有人来写了H,甚至没有看E--F--G。
我现在要在这里有一个相当大的题外话了。
如果我现在将master 合并到topicB 中
如果你做了真正的合并
好的,让我们将其绘制为提交图,以确保这意味着您的意图。我将使用我通常的形式(稍微紧凑一点,分支名称的箭头向右移动一点):
A--B--C---D----H <-- master (still points to H)
\ \
E--F--G \ <-- topicA (still points to G)
\ \
I--J--K--L <-- topicB (points to new L)
请注意,我画了一个真正的合并,而不是一个假的、根本不是合并的“壁球合并”。正如我们将看到的,这确实很重要。
当 Git 去 make 这个新的提交 L 时,它必须合并提交 H 和 K。为此,它必须找到它们的合并库(在某些情况下可能有多个合并库,但这里只有一个)。
任何两个提交的合并基础是 / 是最低共同祖先:也就是说,最接近两个起始提交(H 和 K)的提交可以从 both 那些开始提交。
让我们先从H 和K 开始。 H 可以从H(当然)访问,但不能从K 访问。 K 可以从K 访问(当然),但不能从H 访问。现在我们可以检查D 与H 和K:D 可以从H 访问,但不能从K 访问。现在我们可以检查J,但无法从H 访问。现在我们考虑C 和I 和F 和E,但直到我们回到提交B 时,我们才发现可以从H 和@987654362 访问提交@。提交A 也可以,但它离H 和K 更远,所以提交B 是合并基础。
然后合并以两个差异开始:
git diff B H
和
git diff B K
第一个差异显示了我们从 B 到 H 的变化。当然,H 包含我们在 C 和 D 中所做的更改,以及我们在 E、F 和 G 中所做的更改。第二个差异显示了我们从B 到K 的变化。当然,K 包含我们在 E 中更改的内容,以及我们在 I--J--K 中更改的内容。
这有我们在 Etwice 中所做的任何更改,但 Git 通常——并非总是如此,但 通常——能很好地注意到这一点并接受更改只有一次。所以commit L 可能拥有之前每次提交的所有内容,只完成一次。
然后做一个diff master...topicB
请注意,这是使用 three-dot ... 语法,而不是 two-dot .. 语法。我不确定您在这里的意图是什么,但三点语法本质上意味着“找到(或)合并基础”。所以让我们再做一遍这个练习:master 仍然指向提交 H 和 topicB 现在指向新的合并提交 L,我们找到了 H 和 L 的合并基,现在我们有一个真正的合并(这些愚蠢的“壁球合并”假对我们来说没有合并东西,不可能!)。
让我们先从H 和L 开始。 L 可以从L(当然)访问,但不能从H 访问。 H 可以从H(当然)和L 访问。这意味着H和L的合并基是H:master和topicB的合并基是master。
...diff master...topicB
由于master 位于三点的左侧,它被替换为合并基础,即提交H。三点的右侧被解析为它的提交,即提交L。然后差异会显示H 和L 之间的差异。
在这种情况下,效果与git diff master..topicB 相同,这与git diff master topicB 的含义相同:按顺序比较提交H 和L。
这应该是一个相当合理的差异,尽管我们最初为了制作H 做了可怕的假 squash-merge。 真正的合并修复了这个问题,至少对于H vs L。
如果你做了一个假的、非合并的“壁球合并”
让我们再次绘制这个东西,但这次使用伪造的 not-a-merge git merge --squash 技术。我们的新提交 L 的 内容 将与我们进行真正的合并一样,但 graph 会有所不同:
A--B--C---D----H <-- master (still points to H)
\
E--F--G <-- topicA (still points to G)
\
I--J--K--L <-- topicB (points to new L)
现在我们回到:
diff master...topicB
再一次,我们需要找到H 和L 之间的合并基,但是现在L 没有指向两者 K 和 H,但仅限于K。 H 和 L 都不是合并基础。 D 和 J 都不起作用:我们不能从 L 向后走到 D,我们也不能从 H 向后走到 J。事实上,merge base commit 又是 commit B,所以意思是一样的:
git diff B L
而且这个差异会完全不同。
我不知道您对差异的期望,所以我无法解决这部分:
差异被搞砸了,并且包含许多不应该存在的更改或撤消。
返回合并、变基等
现在让我们回到问题:
在这种情况下,我通常将master 合并为topicA,然后将topicA 合并为topicB,然后再执行上一段中所说的操作。但是,有时这是不可能的(例如,分支被删除了)并且我以很多冲突结束。
请注意,删除分支 name 对其提交 没有立即影响。它所做的是停止保护这些提交。也就是说,因为每个分支名称都使得提交可达,这些提交对于 Grim Collector ...er... Grim Reaper 垃圾是安全的集电极。我们做了几次可达性的事情来找到合并基地;不过,Git 更频繁地在 GC 期间查找要保留的提交和要丢弃的提交。在push 和fetch 期间承诺转移;等等。如果提交受到其他方式的保护——通过真正的合并的可达性,或者通过来自另一个分支或标签名称的可达性,或者其他方式——它们会一直存在。如果你可以通过哈希ID找到它们,你可以把它们带回来。
更重要的是,对于您的rebase 案例,如果您可以通过git log 找到它们,您可以将它们切断。我们稍后会看到。
壁球“合并”的一般注意事项
因为 squash “合并”根本不是真正的合并,它们不会保护其他提交链,而且——这通常是未来合并冲突的关键——它们不提供future 与更新的合并基础合并。这意味着那些未来的合并必须检查巨大的差异,而不是小的差异,然后 Git 的自动“冗余更改”检测失败。
这在实践中意味着什么取决于您如何使用这些 squash not-a-merge “合并”。当您使用它们进行开发线并将其减少到单个提交时,完全停止使用其他开发线可能是一个好主意。您可以保存它(使用分支或标签名称,甚至是分支和标签名称空间之外的一些其他引用,这样您通常不会看到它,这可以防止提交链被 GC-ed)或者只是让它得到收获,但无论哪种方式,您可能都不应该继续处理它,这包括您从某些提交中分出的任何其他分支。
使用git rebase
rebase --onto master topicA topicB 是正确的解决方案吗?
使用git rebase,您可以将这些其他链(在本例中为您的topicB)复制到新链,然后将标签 (topicB) 指向复制链的末端。您要复制的提交是那些没有被压缩的提交:这里是I--J--K 链。使用topicA 作为<upstream> 的参数git rebase将选择正确的提交集。注意topicA 达到提交G、F、E、B 和A,而topicB 达到K,J,F,F,@, 987654467@等;所以使用topicA 作为<upstream> 会砍掉后面的F 的所有内容,但随后需要您提供的明确的--onto。
如果标签 topicA 被删除,你仍然可以做这个变基,只是变得更棘手了。您需要做的是通过它们的哈希 ID 指定提交 G 或 F,以便切断提交 F 和更早的时间。 G 的哈希 ID 从难以找到(GC 尚未删除它,但任何实时引用都无法访问)到不存在(GC 已将其删除)。但是,F 的 ID 就在 topicB 链中:K 的父级是 J,J 的父级是 I,I 的父级是 @ 987654484@。问题是 没有简单的方法可以确定提交 F 是否在早期 git merge --squash 处理的链中的提交集中。
(这与我之前加粗的评论有关,但并不完全相同。)