繁体   English   中英

为什么 Git 知道它可以挑选恢复的提交?

[英]Why does Git know it can cherry-pick a reverted commit?

例如,在一个分支中,有 3 个提交: A <- B <- C 如果我直接挑选B测试 A ),Git 说:

The previous cherry-pick is now empty, possibly due to conflict resolution.
If you wish to commit it anyway, use:

    git commit --allow-empty

我可以理解,因为B已经在这个分支中,所以无法再次挑选它。

然后我通过以下方式在批量提交中恢复了BC

git revert -n B^..C
git commit -a -m "xxx"

这将是一个新的大提交D ,它恢复BC ,分支应该像A <- B <- C <- D

然后由于某种原因我需要重做BC 我试过了:

git cherry-pick B^..C

我看到两个新的提交B'C'附加到分支: A <- B <- C <- D <- B' <- C'

我的第一个问题是,Git 如何智能地知道它应该创建B'C' 我认为 Git 会发现BC已经在分支历史中,所以它可能会像我直接在Test A中选择“B”一样跳过它们。

然后,在那之后,由于分支已经是A <- B <- C <- D <- B' <- C' ,我再次运行这个命令:

git cherry-pick B^..C

我预计 Git 可以识别这是一个无操作操作。 但是这次 Git 抱怨冲突。 我的第二个问题是,为什么Git这次无法识别并跳过这个操作?

cherry-pick 是从您的cherry-pick 的父母到cherry-pick 的差异的合并,以及从您的cherry-pick 的父母到您的签出提示的差异。 而已。 Git 不需要知道更多。 它不关心任何提交的“位置”,它关心合并这两组差异。

revert 是从您的 revert 到其父级的差异与从您的 revert 到您的签出提示的差异的合并。 而已。 Git 不用再知道了。

在这里:试试这个:

git init test; cd $_
printf %s\\n 1 2 3 4 5 >file; git add .; git commit -m1
sed -si 2s,$,x, file; git commit -am2
sed -si 4s,$,x, file; git commit -am3

运行git diff:/1:/2git diff:/1:/3 当您在此处说git cherry-pick:/2时,这些是 git 运行的差异。 第一个 diff 更改了第 2 行,第二个 commit 更改了第 2 行和第 4 行; 第 4 行更改不与第一个差异中的任何更改相邻,并且第 2 行更改在两者中都是相同的。 没有什么可做的,所有:/1 - :/2更改也在:/1 - :/3中。

现在,在你开始接下来的内容之前,让我这样说:用散文来解释比仅仅看到更难。 执行上面的示例序列并查看 output。 通过查看它阅读它的任何描述容易看到正在发生的事情。 每个人都经历了一段太新的时期,也许一点方向会有所帮助,这就是下面的段落的目的,但同样:单独的散文比差异更难理解。 运行差异,试着理解你在看什么,如果你需要一点帮助我 promise 是一个非常小的驼峰跟随在下面的文本中。 当它聚焦时,看看你是否至少在精神上拍打你的额头并想“哇,为什么这么难看到?”,就像,嗯,几乎每个人。

Git 的合并规则非常简单:对重叠或相邻线的相同更改按原样接受。 对已更改行的一个差异中没有更改的行的更改,或在另一行中与更改的行相邻的行,按原样接受。 对任何重叠或邻接线的不同更改,嗯,有很多历史可以查看,没有人找到一个规则来预测每次结果应该是什么,所以 git 声明更改冲突,转储两组结果到文件中,并让您决定结果应该是什么。

那么如果你现在更改第 3 行会发生什么?

sed -si 3s,$,x, file; git commit -amx

运行git diff:/1:/2git diff:/1:/x ,您会看到,相对于cherry-pick的父级, :/2更改了第 2 行,并且您的提示更改了第 2,3 行和4. 2 和 3 邻接,这在历史上太接近自动精灵无法正确处理,所以是的,你可以这样做: git cherry-pick:/2现在将声明冲突,向您显示对第 2 行的更改和这两个第 3 行和第 4 行的不同版本 (:/2 都没有改变,你的提示都改变了,在这里的上下文中,很明显第 3 行和第 4 行的更改按原样很好,但又一次:没有人想出可靠识别此类上下文的自动规则)。

您可以在此设置上响铃更改以测试还原的工作方式。 还存储弹出和合并,以及git checkout -m与您的索引运行快速临时合并。

您的git cherry-pick B^..C是两个提交BC的精选。 就像上面描述的那样,它一个接一个地执行它们。 由于您已恢复BC ,然后再次选择它们,这与应用BC然后选择樱桃B的效果完全相同(目的是然后选择樱桃采摘C )。 I conclude that B and C touch overlapping or abutting lines, so git diff B^ B will show changes that overlap or abut changes in git diff B^ C' , and that's what Git's not going to just pick for you, because whatever looks right在这里,在其他情况下,没有人可以编写识别规则,看起来相同的选择将是错误的。 所以 git 说这两组更改冲突,你可以解决它。

这扩展了@jthill 的答案

考虑这样的历史记录中的常规合并:

a--b--c--d--e--f--g--h
       \
        r--s--t

Git 通过仅查看这些提交的内容来执行合并:

c--h   <-- theirs
 \
  t    <-- ours
^
|
base

没有别的了。 请注意,在概念层面上,哪一方是“我们的”,哪一方是“他们的”是完全无关的; 它们是完全可以互换的。 (唯一不同的是当存在冲突时,Git 必须决定如何将侧面标记为用户的“他们的”和“我们的”。)(我将省略标签“基地”、“他们的”和以下图表中的“我们的”。)

在你的历史

A--B--C

第一个git cherry-pick B后面的合并操作查看了以下提交:

A--B
 \
  C

在这里,选择A是因为它是B的父级,也就是B^ 显然,从AC的更改也包含从AB的更改,并且合并机制会产生 no-change-merge-result,这会产生cherry-pick is now empty消息。

然后你通过恢复BC创造了这个历史:

A--B--C--R

然后下一个git cherry-pick B查看了这些提交:

A--B
 \
  R

这一次,从AR的更改不再包含从AB的更改,因为它们已被还原。 因此,合并不再产生空结果。

一个小弯路:当您在历史记录中执行git revert B时,合并机制会查看这些提交:

B--A
 \
  C

请注意,与git cherry-pick B相比,只有BB的父级A被交换。

(我描述的是单次提交撤销,因为我不确定多提交撤销是如何工作的。)

让我们退后大约 10 英尺,对 Git 是什么有一个更大的了解。

Git 提交是所有文件的快照。 它基本上代表了你的整个项目。 这与差异无关。 这是一个出色的架构,因为它非常快速且有效地可靠。 任何提交都可以完全恢复您的项目 kaboom 的 state,只需检查一下即可; 没有必要“思考”。

然而,Git 可以在两次提交之间产生差异,这就是它实现我们所谓的“合并逻辑”的方式。 每个合并都包括同时应用两个差异。 [嗯,它可能不止两个,但假装不是。] 从这个意义上说,合并、樱桃挑选、变基、恢复都是合并——它们都使用“合并逻辑”来形成表达应用两个差异的结果。 诀窍是要知道在两个差异的构造中谁是比较对象。

  • 当您要求真正的git merge时,比如说两个分支,Git 会计算出这些分支最后分歧的位置。 这称为合并基础 比较对象是:branch1 的合并基数和尖端,以及 branch2 的合并基数和尖端。 这两个差异都应用于合并基础,结果用于与两个父级(分支提示)形成提交。 然后第一个分支名称向上滑动一个,以指向该新提交。

  • 当您要求一个cherry-pick时,合并基础是被挑选的提交的父级。 比较对象是:合并基础和头部,合并基础和选择的提交。 这两个差异都应用于合并基础,结果用于与一个父级(头部)形成提交。 然后头分支名称向上滑动一个,以指向该新提交。 [而变基只是一系列精选!]

  • revert使用合并逻辑。 正如 jthill 所解释的,这只是向后形成差异之一的问题。 合并基础是您尝试撤消的提交。 比较对象是:合并基础及其父级(在该方向上),以及合并基础和头部。 这些差异应用于合并基础并用于形成其父级为头部的提交。 然后头分支名称向上滑动一个,以指向该新提交。 如果这向您表明恢复基本上是向后的樱桃选择,那么您是绝对正确的。


很酷的是,一旦你知道了这一点,你就可以预测当你给出这些命令之一时会发生什么,因为你可以通过说git diff自己提取这些相同的差异。 Git 的合并逻辑本质上是对你开放的。 剩下的只是了解 Git 在操作中间停止的情况,因为如果没有进一步的明确指示就无法继续。 这被称为(不幸的是)冲突,它可以通过两种主要方式出现:

  • 同一文件中的同一行在两个差异中以两种不同的方式进行了更改。 Git 关于什么构成同一行的想法比你想象的要广泛得多。 这让初学者感到惊讶。

  • 同一个文件 qua 文件以两种不兼容的方式处理:例如,一个 diff 删除它,而另一个 diff 保留并编辑它。


我应该再添加一个事实来解释很多行为,包括您所询问的部分内容。 这似乎很明显,但值得明确说明:在差异中, “无”不是事物。 我的意思是这个。 假设一个 diff 更改了一行,而另一个 diff 对该行没有任何作用。 然后制定两个差异的方法是:更改线路。 什么都不做不是一件事:它不会“争吵”反对变化。

这是值得一提的,尤其是因为初学者通常不掌握它。 前几天有一个问题,用户抱怨在第二个分支删除文件的合并中,该文件确实最终被删除,即使第一个分支保留了它。 该用户认为“不要删除文件”是一件事情,实际上是一件主要的事情。 但事实并非如此。 默认情况下,两个 diff 的权重是相等的,所以一个分支什么都不做,一个分支删除了文件,什么都不做也不是一件事,所以结果是删除了文件。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM