【问题标题】:Why does Git know it can cherry-pick a reverted commit?为什么 Git 知道它可以挑选一个恢复的提交?
【发布时间】:2021-04-28 05:46:05
【问题描述】:

例如,在一个分支中,有 3 个提交:A <- B <- C。如果我直接选择 BTest 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 无法识别并跳过这个操作?

【问题讨论】:

    标签: git git-revert cherry-pick git-cherry-pick


    【解决方案1】:

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

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

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

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

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

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


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

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

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


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

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

    【讨论】:

      【解决方案2】:

      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 中。

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

      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 行的更改可以按原样进行,但同样:没有人想出一个自动规则来可靠地识别此类上下文)。

      您可以在此设置上响铃更改以测试还原的工作原理。还 stash pops、merge 和 git checkout -m 运行与您的索引的快速临时合并。

      您的git cherry-pick B^..C 是两个提交的精选,BC。就像上面描述的那样,它一个接一个地执行它们。由于您已恢复 BC,然后再次选择它们,这与应用 BC 然后再选择 B 具有完全相同的效果(意图那时樱桃采摘C)。我的结论是BC 触摸重叠或邻接的线,所以git diff B^ B 将显示重叠或邻接git diff B^ C' 中的变化,这就是Git 不会为你挑选的,因为任何看起来都在这里,在其他情况下,没有人可以编写识别规则,看起来相同的选择将是错误的。所以 git 说这两组更改有冲突,你可以解决它。

      【讨论】:

      • 樱桃不是变基吗?
      • @MadPhysicist 否。变基是一系列精选。
      • @j6t。但这也不是合并,确切地说
      • @MadPhysicist 查看我的答案。当然,cherry-pick 产生的提交不是合并提交。但是cherry-pick操作如何得到它的结果一个合并操作,
      • 感谢您的详细解释。我想我误解了很久。我将 git commit 视为“文件差异”,因为我使用 svn 有一段时间了。所以在我看来,一个提交不能被重播两次。但是由于实际上 git commit 使用“文件快照”和类似 LCS 的算法来区分文件快照,所以可以忽略重复的 change。我说 change 因为git commit没有“文件更改”的概念,而只是“文件快照”,“文件更改”是在执行某些操作时实时计算的(如合并,cherry-pick , 等等)。我说的对吗?
      【解决方案3】:

      这扩展了@jthill's answer

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

      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 交换了位置。

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

      【讨论】:

      • 多次提交反转,使用git revert -n,只是重复执行每个反向樱桃选择而不提交。 Git 更新索引和工作树,以便它们在每一步之后同步进行下一步。 (请注意,用于合并的“我们的”提交是索引中的任何内容:如果索引和工作树不同步,您可能会弄得一团糟。)
      • 感谢@torek 的澄清。我不会把它写到答案中,因为无论如何这只是一个弯路。
      猜你喜欢
      • 1970-01-01
      • 2018-03-20
      • 2011-07-04
      • 2014-07-30
      • 2021-03-04
      • 2014-11-21
      相关资源
      最近更新 更多