简体   繁体   English

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

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

In a branch, say, there are 3 commits: A <- B <- C .例如,在一个分支中,有 3 个提交: A <- B <- C If I cherry-pick B directly ( Test A ), Git says:如果我直接挑选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

I can understand that because B is already in this branch, it's no-op to cherry-pick it again.我可以理解,因为B已经在这个分支中,所以无法再次挑选它。

Then I reverted B and C in a batch commit by:然后我通过以下方式在批量提交中恢复了BC

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

This would be a new big commit D which reverts B and C , the branch should be like A <- B <- C <- D .这将是一个新的大提交D ,它恢复BC ,分支应该像A <- B <- C <- D

Then I need to redo B and C due to some reason.然后由于某种原因我需要重做BC I tried:我试过了:

git cherry-pick B^..C

I see two new commits B' and C' are appended to the branch: A <- B <- C <- D <- B' <- C' .我看到两个新的提交B'C'附加到分支: A <- B <- C <- D <- B' <- C'

My first question is, How can Git intelligently knows it should create B' and C' ?我的第一个问题是,Git 如何智能地知道它应该创建B'C' I thought Git would find B and C are already in branch history, so it may just skip them like when I cherry-pick 'B' directly in Test A .我认为 Git 会发现BC已经在分支历史中,所以它可能会像我直接在Test A中选择“B”一样跳过它们。

Then, after that, since the branch is already A <- B <- C <- D <- B' <- C' , I run this command again:然后,在那之后,由于分支已经是A <- B <- C <- D <- B' <- C' ,我再次运行这个命令:

git cherry-pick B^..C

I expected Git can recognize this is a no-op operation.我预计 Git 可以识别这是一个无操作操作。 But this time Git complains confliction.但是这次 Git 抱怨冲突。 My second question is, why does Git fail to recognize and skip this operation this time?我的第二个问题是,为什么Git这次无法识别并跳过这个操作?

cherry-pick is a merge, of the diffs from your cherry-pick's parent to the cherry-pick, with the diffs from your cherry-pick's parent to your checked-out tip. cherry-pick 是从您的cherry-pick 的父母到cherry-pick 的差异的合并,以及从您的cherry-pick 的父母到您的签出提示的差异。 That's it.而已。 Git doesn't have to know any more than that. Git 不需要知道更多。 It doesn't care "where" any of the commits are, it cares about merging those two sets of diffs.它不关心任何提交的“位置”,它关心合并这两组差异。

revert is a merge of the diffs from your revert to its parent with the diffs from your revert to your checked-out tip. revert 是从您的 revert 到其父级的差异与从您的 revert 到您的签出提示的差异的合并。 That's it.而已。 Git doesn't have to know any more. Git 不用再知道了。

Here: try this:在这里:试试这个:

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

Run git diff:/1:/2 and git diff:/1:/3 .运行git diff:/1:/2git diff:/1:/3 Those are the diffs git runs when you say git cherry-pick:/2 here.当您在此处说git cherry-pick:/2时,这些是 git 运行的差异。 The first diff changes line 2, and the second commit changes lines 2 and 4;第一个 diff 更改了第 2 行,第二个 commit 更改了第 2 行和第 4 行; the line 4 change does not abut any changes in the first diff and the line 2 change is identical in both.第 4 行更改不与第一个差异中的任何更改相邻,并且第 2 行更改在两者中都是相同的。 There's nothing left to do, all the :/1 - :/2 changes are also in :/1 - :/3 .没有什么可做的,所有:/1 - :/2更改也在:/1 - :/3中。

Now before you start on what follows, let me say this: this is harder to explain in prose than it is to just see.现在,在你开始接下来的内容之前,让我这样说:用散文来解释比仅仅看到更难。 Do the example sequence above and look at the output.执行上面的示例序列并查看 output。 It is much , much easier to see what's going on by looking at it than by reading any description of it.通过查看它阅读它的任何描述容易看到正在发生的事情。 Everybody goes through a stretch where this is too new and maybe a little orientation will help, and that's what the paragraphs below are for, but again: the prose, alone, is harder to understand than the diffs.每个人都经历了一段太新的时期,也许一点方向会有所帮助,这就是下面的段落的目的,但同样:单独的散文比差异更难理解。 Run the diffs, try to understand what you're looking at, if you need a little help over what I promise is a very small hump follow along in the text below.运行差异,试着理解你在看什么,如果你需要一点帮助我 promise 是一个非常小的驼峰跟随在下面的文本中。 When it snaps into focus see if you don't at least mentally slap your forehead and think "wow why was that so hard to see?", just like, well, just about everybody.当它聚焦时,看看你是否至少在精神上拍打你的额头并想“哇,为什么这么难看到?”,就像,嗯,几乎每个人。

Git's merge rules are pretty straightforward: identical changes to overlapping or abutting lines are accepted as-is. Git 的合并规则非常简单:对重叠或相邻线的相同更改按原样接受。 Changes to lines with no changes in one diff for changed lines, or lines abutting changed lines, in the other, are accepted as is.对已更改行的一个差异中没有更改的行的更改,或在另一行中与更改的行相邻的行,按原样接受。 Different changes to any overlapping or abutting lines, well, there's an awful lot of history to look at and nobody's ever found a rule that will predict what the results of that should be every time, so git declares the changes conflict, dumps both sets of results into the file and lets you decide what the result should be.对任何重叠或邻接线的不同更改,嗯,有很多历史可以查看,没有人找到一个规则来预测每次结果应该是什么,所以 git 声明更改冲突,转储两组结果到文件中,并让您决定结果应该是什么。

So what happens if you now change line 3?那么如果你现在更改第 3 行会发生什么?

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

run git diff:/1:/2 and git diff:/1:/x , and you'll see that where, relative to the cherry-pick's parent, :/2 changed line 2 and your tip changed lines 2,3 and 4. 2 and 3 abut, that's historically too close for automated genies to handle properly, so yay, you get to do it: git cherry-pick:/2 now will declare a conflict, showing you the change to line 2 and the two different versions of lines 3 and 4 (:/2 changed neither, your tip changed both, in context here it's clear the line 3 and 4 changes are fine as-is but again: nobody's ever figured out an automatic rule for reliably identifying such contexts).运行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 行的更改按原样很好,但又一次:没有人想出可靠识别此类上下文的自动规则)。

You can ring changes on this setup to test out how reverts work.您可以在此设置上响铃更改以测试还原的工作方式。 Also stash pops, and merges, and git checkout -m which runs a quick ad-hoc merge with your index.还存储弹出和合并,以及git checkout -m与您的索引运行快速临时合并。

Your git cherry-pick B^..C is a cherry-pick of two commits, B and C .您的git cherry-pick B^..C是两个提交BC的精选。 It does them one after another, exactly as described above.就像上面描述的那样,它一个接一个地执行它们。 Since you've reverted B and C , and then cherry-picked them again, this has the exact same effect as applying B and C and then cherry-picking B (with the intent of then cherry-picking C ).由于您已恢复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 here, in other circumstances nobody can write a rule for identifying, an identical-looking choice will be wrong. 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在这里,在其他情况下,没有人可以编写识别规则,看起来相同的选择将是错误的。 So git says the two sets of changes conflict and you get to sort it out.所以 git 说这两组更改冲突,你可以解决它。

This expands @jthill's answer .这扩展了@jthill 的答案

Consider a regular merge in a history like this:考虑这样的历史记录中的常规合并:

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

Git performs the merge by looking only at the contents of these commits: Git 通过仅查看这些提交的内容来执行合并:

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

and nothing else.没有别的了。 Note that at the conceptual level it is completely irrelevant which side is denoted "ours" and which is "theirs";请注意,在概念层面上,哪一方是“我们的”,哪一方是“他们的”是完全无关的; they are totally interchangeable.它们是完全可以互换的。 (The only time it makes a difference is when there are conflicts and Git has to decide how it marks the sides as "theirs" and "ours" for the user.) (I'll omit the labels "base", "theirs" and "ours" in the following charts.) (唯一不同的是当存在冲突时,Git 必须决定如何将侧面标记为用户的“他们的”和“我们的”。)(我将省略标签“基地”、“他们的”和以下图表中的“我们的”。)

In your history在你的历史

A--B--C

the merge operation behind the first git cherry-pick B looked at the following commits:第一个git cherry-pick B后面的合并操作查看了以下提交:

A--B
 \
  C

Here, A is chosen because it is the parent of B , aka, B^ .在这里,选择A是因为它是B的父级,也就是B^ Obviously, the changes from A to C also contain the changes from A to B and the merge machinery produces a no-change-merge-result, and that produces the cherry-pick is now empty message.显然,从AC的更改也包含从AB的更改,并且合并机制会产生 no-change-merge-result,这会产生cherry-pick is now empty消息。

Then you made this history by reverting both B and C :然后你通过恢复BC创造了这个历史:

A--B--C--R

Then the next git cherry-pick B looked at these commits:然后下一个git cherry-pick B查看了这些提交:

A--B
 \
  R

This time, the changes from A to R do no longer contain the changes from A to B because they have been reverted.这一次,从AR的更改不再包含从AB的更改,因为它们已被还原。 Therefore, the merge no longer produces an empty result.因此,合并不再产生空结果。

A small detour: When you do git revert B in your history, the merge machinery looks at these commits:一个小弯路:当您在历史记录中执行git revert B时,合并机制会查看这些提交:

B--A
 \
  C

Note that only B and the parent of B , aka, A are swapped around compared to git cherry-pick B .请注意,与git cherry-pick B相比,只有BB的父级A被交换。

(I was describing a single-commit reversal as I am unsure how a multi-commit reversal works.) (我描述的是单次提交撤销,因为我不确定多提交撤销是如何工作的。)

Let's step back about ten feet here and get a bigger mental picture of what Git is.让我们退后大约 10 英尺,对 Git 是什么有一个更大的了解。

A Git commit is a snapshot of all the files . Git 提交是所有文件的快照。 It represents your whole project, basically.它基本上代表了你的整个项目。 It is not about diffs.这与差异无关。 This is a brilliant architecture because it is extremely fast and effectively infallible.这是一个出色的架构,因为它非常快速且有效地可靠。 Any commit can absolutely restore that state of your project, kaboom, just by checking it out;任何提交都可以完全恢复您的项目 kaboom 的 state,只需检查一下即可; there is no need to "think".没有必要“思考”。

However, Git can make diffs between two commits, and that is how it implements what we may call "merge logic".然而,Git 可以在两次提交之间产生差异,这就是它实现我们所谓的“合并逻辑”的方式。 Every merge consists of applying two diffs simultaneously.每个合并都包括同时应用两个差异。 [Well, it might be more than two, but pretend it isn't.] A merge, a cherry pick, a rebase, a revert are all merges in that sense — they all use "merge logic" to form a commit expressing the result of applying two diffs. [嗯,它可能不止两个,但假装不是。] 从这个意义上说,合并、樱桃挑选、变基、恢复都是合并——它们都使用“合并逻辑”来形成表达应用两个差异的结果。 The trick is to know who the comparands are in the construction of the two diffs.诀窍是要知道在两个差异的构造中谁是比较对象。

  • When you ask for a true git merge , say of two branches, Git figures out where those branches last diverged.当您要求真正的git merge时,比如说两个分支,Git 会计算出这些分支最后分歧的位置。 This is called the merge base .这称为合并基础 The comparands are: the merge base and the tip of branch1, and the merge base and tip of branch2.比较对象是:branch1 的合并基数和尖端,以及 branch2 的合并基数和尖端。 Both of those two diffs are applied to the merge base and the result is used to form a commit with two parents (the branch tips).这两个差异都应用于合并基础,结果用于与两个父级(分支提示)形成提交。 The first branch name then slides up one, to point to that new commit.然后第一个分支名称向上滑动一个,以指向该新提交。

  • When you ask for a cherry-pick , the merge base is the parent of the commit being picked.当您要求一个cherry-pick时,合并基础是被挑选的提交的父级。 The comparands are: the merge base and the head, and the merge base and the picked commit.比较对象是:合并基础和头部,合并基础和选择的提交。 Both of those two diffs are applied to the merge base and the result is used to form a commit with one parent (the head).这两个差异都应用于合并基础,结果用于与一个父级(头部)形成提交。 The head branch name then slides up one, to point to that new commit.然后头分支名称向上滑动一个,以指向该新提交。 [And a rebase is just a series of cherry picks!] [而变基只是一系列精选!]

  • A revert also uses merge logic. revert使用合并逻辑。 As jthill has explained, it's just a matter of forming one of the diffs backwards .正如 jthill 所解释的,这只是向后形成差异之一的问题。 The merge base is the commit that you are trying to reverse.合并基础是您尝试撤消的提交。 The comparands are: the merge base and its parent ( in that direction ), and the merge base and the head.比较对象是:合并基础及其父级(在该方向上),以及合并基础和头部。 These diffs are applied to the merge base and used to form a commit whose parent is the head.这些差异应用于合并基础并用于形成其父级为头部的提交。 The head branch name then slides up one, to point to that new commit.然后头分支名称向上滑动一个,以指向该新提交。 If this suggests to you that a revert is basically a backwards cherry-pick, you are absolutely right.如果这向您表明恢复基本上是向后的樱桃选择,那么您是绝对正确的。


The cool thing is that once you know this, you can predict what will happen when you give one of these commands, because you can extract those same diffs yourself by saying git diff .很酷的是,一旦你知道了这一点,你就可以预测当你给出这些命令之一时会发生什么,因为你可以通过说git diff自己提取这些相同的差异。 Git's merge logic essentially lies open to your gaze. Git 的合并逻辑本质上是对你开放的。 It remains then only to understand the circumstances under which Git stops in the middle of the operation because it cannot proceed without further explicit instructions.剩下的只是了解 Git 在操作中间停止的情况,因为如果没有进一步的明确指示就无法继续。 That's called (unfortunately) a conflict, and there are two main ways it can arise:这被称为(不幸的是)冲突,它可以通过两种主要方式出现:

  • The same line in the same file was changed in two different ways in the two diffs.同一文件中的同一行在两个差异中以两种不同的方式进行了更改。 Git's idea of what constitutes the same line is rather broader than you might expect; Git 关于什么构成同一行的想法比你想象的要广泛得多。 this surprises beginners.这让初学者感到惊讶。

  • The same file, qua file, was treated in two incompatible ways: for example, one diff deletes it but the other diff keeps and edits it.同一个文件 qua 文件以两种不兼容的方式处理:例如,一个 diff 删除它,而另一个 diff 保留并编辑它。


I should add one more fact that explains a lot of behavior, including part of what you're asking about.我应该再添加一个事实来解释很多行为,包括您所询问的部分内容。 This may seem obvious, but it is worth stating explicitly: in a diff, "nothing" is not a thing.这似乎很明显,但值得明确说明:在差异中, “无”不是事物。 What I mean is this.我的意思是这个。 Supposing one diff changes a line and the other diff does nothing to that line.假设一个 diff 更改了一行,而另一个 diff 对该行没有任何作用。 Then the way to enact both diffs is: change the line.然后制定两个差异的方法是:更改线路。 Doing nothing is not a thing: it does not "tussle" against the change.什么都不做不是一件事:它不会“争吵”反对变化。

That is worth mentioning especially because beginners often don't grasp it.这是值得一提的,尤其是因为初学者通常不掌握它。 The other day there was a question where a user was complaining that in a merge where the second branch deleted a file, the file did indeed end up deleted even though the first branch kept it.前几天有一个问题,用户抱怨在第二个分支删除文件的合并中,该文件确实最终被删除,即使第一个分支保留了它。 That user was thinking of "don't delete the file" as a thing, and indeed as a primary thing.该用户认为“不要删除文件”是一件事情,实际上是一件主要的事情。 But it isn't.但事实并非如此。 The two diffs are weighed equally by default, so one branch did nothing and one branch deleted the file, and doing nothing is not a thing, so the result is to delete the file.默认情况下,两个 diff 的权重是相等的,所以一个分支什么都不做,一个分支删除了文件,什么都不做也不是一件事,所以结果是删除了文件。

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

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