在 git 中 "changes introduced by a commit" 是什么意思

What it means "changes introduced by a commit" in git

我到处都看到这个:“...cherry-pick 应用提交引入的更改...”

我这样做了:在 master 中创建了这个文件:

** File 1 **

Content

** Footer **

然后分支到 branch2 并提交更改:

** File 1 **

Content
Edit 1

** Footer **

然后是另一个:

** File 1 **

Content
Edit 2
Edit 1

** Footer **

现在我回到 master 并尝试从 branch2 中挑选最新的提交。我预计只有 'Edit2' 会被导入,因为与前一个相比,这不是该提交引入的更改吗?

我得到的是以下合并冲突:

** File 1 **

Content
<<<<<<< HEAD
=======
Edit 2
Edit 1
>>>>>>> b634e53...
** Footer **

现在我的明显问题是,我对 cherry-pick 的工作原理有什么误解,具体是为什么这里存在合并冲突,这将是 git 合并的快进?

重要提示:这并不是关于合并冲突的问题,我感兴趣的是 cherry-pick 在这里实际做了什么。我不是从 curiosity/whatever 询问,而是因为我 运行 在工作中使用 git 遇到麻烦。

正如一些人在评论中指出的(并链接到其他问题),git cherry-pick 实际上进行了三向合并。 How do cherry-pick and revert work? 描述了这一点,但更多的是在内容方面而不是机制方面。

我在 中描述了特定 合并冲突的来源,以及 cherry-pick 和还原的一般轮廓,但我认为最好退后一步,问问您做过的 机制 问题。不过,我会 re-frame 一点,因为这三个问题:

  • 提交真的是快照吗?
  • 如果提交是快照,git showgit log -p 如何将其显示为 更改?
  • 如果提交是快照,git cherry-pickgit revert 如何工作?

回答最后一个问题需要先回答一个问题:

  • Git如何执行git merge

那么,让我们按照正确的顺序回答这四个问题。这将相当长,如果您愿意,可以直接跳到最后一节 — 但请注意,它建立在第三节的基础上,第三节建立在第二节的基础上,第三节又建立在第一节的基础上。

提交真的是快照吗?

是的——尽管从技术上讲,提交 指的是 快照,而不是 快照。这非常简单明了。要使用 Git,我们通常从 运行ning git clone 开始,这会为我们提供一个新的存储库。有时,我们首先创建一个空目录并使用 git init 创建一个 empty 存储库。不过,无论哪种方式,我们现在都拥有三个实体:

  1. 存储库本身,它是 对象的大数据库 ,加上 名称到哈希 ID 映射的较小数据库 (例如,b运行ch 名称),加上许多其他 mini-databases 作为单个文件实现(例如,每个 reflog 一个)。

  2. 某物 Git 调用 index,或 暂存区 ,有时 缓存。调用什么取决于调用者。索引本质上是你 Git 构建你将要进行的 下一个 提交的地方,尽管它在合并期间扮演了一个扩展的角色。

  3. work-tree,您可以在此处实际查看文件并处理/使用它们。

对象数据库保存四种类型的对象,Git调用commitstrees斑点 注释标签 。树和 blob 主要是实现细节,我们可以在这里忽略带注释的标签:这个大数据库的主要功能,为了我们的目的,是保存我们所有的提交。这些提交然后引用保存文件的树和 blob。最后,实际上是 trees-plus-blobs 的组合才是快照。尽管如此,每个提交都只有一棵树,而这棵树就是让我们完成快照的其余部分,因此除了许多糟糕的实现细节外,提交本身也可能是快照。

我们如何使用索引制作快照

我们不会太深入杂草,但我们会说索引通过保存每个文件的压缩 Git-ified、mostly-frozen 副本来工作。从技术上讲,它包含对 actually-frozen 副本的 引用,存储为 blob。也就是说,如果你开始做 git clone <em>url</em>,Git 有 运行 git checkout <em>b运行ch</em> 作为克隆的最后一步。这个 checkout filled-in 来自 b运行ch 提交的索引,因此该索引具有其中每个文件的副本提交。

的确,大多数1 git checkout操作填写both索引 来自提交的 work-tree。这使您可以查看和使用 work-tree 中的所有文件,但 work-tree 副本并不是真正 提交中的副本。提交中的内容是(是?)所有这些文件的冻结、压缩、Git-ified、can-never-be-changed blob 快照。这将永远保留这些文件的那些版本——或者只要提交本身存在——并且非常适合归档,但对于做任何实际工作都无用。这就是为什么 Git de-Git-ifies 文件进入 work-tree.

Git 可以 到此为止,只需提交和 work-trees。 Mercurial——它在很多方面都像 Git—— 到此为止:你的 work-tree 是你提议的下一次提交。您只需更改 work-tree 中的内容,然后更改 运行 hg commit 中的内容,它就会从您的 work-tree 中生成新的提交。这具有明显的优势,即没有讨厌的索引制造麻烦。但它也有一些缺点,包括天生比 Git 的方法慢。无论如何,Git 所做的是从 previous commit 的信息开始 saved i 索引,准备再次提交。

然后,每次 运行 git add,Git 压缩和 Git-ifies 您添加的文件,现在更新索引。如果您只更改几个文件,然后 git add 只是那几个文件,Git 只需要更新几个索引条目。所以这意味着 在所有时间 索引中都有 下一个快照,在特殊的 Git-only 压缩和 ready-to-freeze表格。

这反过来意味着 git commit 只需要冻结索引内容。从技术上讲,它将索引变成一棵新树,为新的提交做好准备。在少数情况下,例如在一些还原之后,或者对于 git commit --allow-empty,新树实际上将是 与之前的提交相同的 树,但你不需要知道或关心这个。

此时,Git 收集您的日志消息和进入每个提交的其他元数据。它将当前时间添加为 time-stamp——这有助于确保每次提交都是完全唯一的,并且通常有用。它使用 current 提交作为新提交的 parent 哈希 ID,使用 tree 生成的哈希 ID保存索引,并写出新的提交对象,它获得一个新的且唯一的提交哈希 ID。因此,新提交包含您之前签出的任何提交的实际哈希 ID。

最后,Git 将新提交的哈希 ID 写入当前的 b运行ch 名称,因此 b运行ch 名称现在指的是 new 提交,而不是像以前那样提交给新提交的父级。也就是说,无论提交 b运行ch 的尖端,现在提交是 behind b 尖端的一步运行通道。新提示是您刚刚做出的提交。


1你可以使用git checkout <em>commit</em> -- <em>path</em> 从一个特定的提交中提取一个特定的文件。这个仍然首先将文件复制到索引中,所以这并不是一个真正的例外。但是,您也可以使用 git checkout 将文件仅从索引复制到 work-tree,并且您可以使用 git checkout -p 有选择地、交互地 补丁 文件,例如。对于索引 and/or work-tree.

的处理方式,这些变体中的每一个都有自己的一组特殊规则

由于 Git 从 索引构建新的提交 ,因此经常 re-check 文档可能是明智的——尽管很痛苦。幸运的是,git status 告诉你现在索引中有什么——通过比较当前提交与索引,然后比较索引与 work-tree,并且对于每个这样的比较,告诉你什么是 不同。所以很多时候,你不必在脑海中随身携带每个 Git 命令对索引 and/or work-tree 的影响的所有细节:你可以运行命令,稍后使用git status


git showgit log -p 如何将提交显示为更改?

每个提交都包含其父提交的原始哈希 ID,这反过来意味着我们始终可以从某些提交字符串的 last 提交开始,然后工作 backwards 找到所有以前的提交:

... <-F <-G <-H   <--master

我们只需要有办法找到最后次提交。那种方式是:b运行ch name,比如这里的master,标识了last提交。如果最后一次提交的哈希 ID 是 H,Git 在对象数据库中找到提交 HH 存储 G 的哈希 ID,Git 从中查找 G,存储 F 的哈希 ID,Git 从中查找F,依此类推。

这也是将提交显示为补丁的指导原则。我们 Git 查看提交本身,找到它的父级,并提取该提交的快照。然后我们 Git 也提取提交的快照。现在我们有两个快照,现在我们可以比较它们——可以说是用较早的快照减去较晚的快照。无论不同,那一定是那个快照中改变的地方。

请注意,这仅适用于 non-merge 提交。当我们 Git 构建一个 merge 提交时,我们 Git 存储的不是一个而是 两个 父哈希 ID。例如,在 运行ning git merge featuremaster 之后,我们可能有:

       G--H--I
      /       \
...--F         M   <-- master (HEAD)
      \       /
       J--K--L   <-- feature

提交 M 两个 父级:它的第一个父级是 I 提示提交刚才在 master 上。它的第二个父级是 L,它仍然是 feature 上的 tip 提交。很难——嗯,不可能,真的——将提交 M 呈现为 IL 的简单更改,默认情况下,git log 只是 懒得在这里显示任何更改!

(您可以告诉 git loggit show 实际上, 拆分 合并:o 显示从 IM 的差异,然后使用 git log -m -p 或 [=62= 显示从 LM 的第二个单独差异].默认情况下,git show 命令生成 Git 所谓的 组合差异 ,这有点奇怪和特殊:它实际上是由 运行关于 -m 的两个差异,然后 忽略他们所说的大部分内容 并仅向您展示来自 both 提交。这与合并的工作方式密切相关:想法是显示可能存在合并冲突的部分。)

这将我们带到我们的嵌入式问题,我们需要在到达 cherry-pick 并恢复之前解决这个问题。我们需要谈谈 git merge 的机制,即我们如何首先为提交 M 获得 快照

Git如何执行git merge

让我们首先注意合并的要点——好吧,无论如何,在大多数合并中——是合并工作。当我们做 git checkout master 然后 git merge feature 时,我们的意思是:我在 master 上做了一些工作。其他人在 feature 上做了一些工作。我想将他们所做的工作与我所做的工作结合起来。有一个进行这种结合的过程,然后是一个更简单的过程来保存结果。

因此,真正的合并有两个部分,导致像上面的 M 这样的提交。第一部分是我喜欢称之为 verb 部分,to merge。这部分实际上结合了我们不同的变化。第二部分是合并,或合并提交: 这里我们使用"merge"这个词作为名词或形容词。

这里还值得一提的是 git merge 并不总是进行合并。命令本身很复杂,并且有很多有趣的标志参数来以各种方式控制它。在这里,我们只考虑它确实进行实际合并的情况,因为我们查看合并是为了理解 cherry-pick 和还原。

合并为名词或形容词

真正合并的第二部分是比较容易的部分。一旦我们完成 合并 过程,即 merge-as-a-verb,我们就会 Git 使用索引中的任何内容以通常的方式进行新提交。这意味着索引需要以其中的合并内容结束。 Git 将像往常一样构建树并像往常一样收集日志消息——我们可以使用 not-so-good 默认值, merge b运行ch <em>B</em>,或者如果我们觉得特别勤奋的话,构造一个好的。 Git 将像往常一样添加我们的姓名、电子邮件地址和时间戳。然后 Git 将写出一个提交——但不是存储,在这个新提交中,只是 one 父级,Git 将存储一个额外的,second parent,也就是我们在运行 git merge.

时选择的commit的hash ID

例如,对于我们的 git merge feature 而在 master,第一个父项将提交 I——我们已由 运行ning git checkout master。第二个父项将提交 Lfeature 指向的那个。这就是 a 合并的全部内容:合并提交只是具有至少两个父项的提交,标准合并的标准两个父项是第一个与 for 相同any 提交,第二个是我们通过 运行ning git merge <em>something</em> 选择的那个.

合并为动词

merge-as-a-verb 是比较难的部分。我们在上面注意到 Git 将从索引中的任何内容进行 new 提交。所以,我们需要将 放入 索引中,或者将 Git 放入其中, combining work.[=199 的结果=]

我们在上面声明我们对 master 进行了一些更改,而他们——无论他们是谁——对 feature 进行了一些更改。但是我们已经看到 Git 不会 store 变化。 Git 存储 快照。我们如何从 snapshotchange?

我们已经知道那个问题的答案了!我们在看git show的时候就看到了。 Git 比较 两个快照。所以对于git merge,我们只需要选择正确的快照。但哪些是正确的快照?

这个问题的答案在于提交图。在我们 运行 git merge 之前,图表看起来像这样:

       G--H--I   <-- master (HEAD)
      /
...--F
      \
       J--K--L   <-- feature

我们正处于提交 Imaster 的顶端。他们的提交是提交Lfeature的提示。从 I,我们可以倒退到 H,然后是 G,然后是 F,然后大概是 E,依此类推。同时,从 L,我们可以倒退到 K,然后是 J,然后是 F,大概是 E,依此类推。

当我们实际上这个work-backwards技巧时,我们收敛在提交时 F。显然,那么,无论 w 发生什么变化制作,我们从 F 中的快照开始...无论 他们 做了什么更改,他们 F!所以我们要做的就是合并我们的两组更改:

  • 比较 FI:这就是我们改变的地方
  • 比较 FL:这就是他们改变的地方

本质上,我们将只有 Git 运行 两个 git diff。人们会弄清楚 我们 改变了什么,也会弄清楚 他们 改变了什么。 CommitF是我们共同的起点,或者在version-control-speak,merge base.

现在,要真正完成合并,Git 扩展索引。 每个文件的一个个副本,Git现在将索引每个文件的三个个副本。一个副本将来自合并基地 F。第二个副本将来自我们的提交 I。最后,第三个副本来自他们的提交 L.

同时,Git 也会查看两个差异的结果,file-by-file。只要commits F, I, and L 都有相同的文件,2 只有这五种可能:

  1. 没有人动过这个文件。使用任何版本:它们都是一样的。
  2. 我们更改了文件,但他们没有。只需使用我们的版本。
  3. 他们更改了文件,而我们没有。就用他们的版本吧。
  4. 我们和他们都更改了文件,但我们做了 相同的 更改。使用我们的或他们的——两者都是一样的,所以哪个并不重要。
  5. 我们和他们都更改了相同的文件,但我们进行了不同的更改。

案例 5 是唯一困难的案例。对于所有其他情况,Git 知道——或者至少假设它知道——正确的结果是什么,因此对于所有其他情况,Git 将相关文件的索引槽缩回为一个保存正确结果的插槽(编号为零)。

但是,对于情况 5,Git 将三个输入文件的所有三个副本填充到索引中的三个编号槽中。如果文件名为 file.txt:1:file.txt 包含来自 F 的合并基础副本,:2:file.txt 包含我们来自提交 I 的副本,以及 :3:file.txt持有 L 的副本。然后 Git 运行 一个 low-level merge driver——我们可以在 .gitattributes 中设置一个,或者使用默认的

默认的 low-level 合并采用两个差异,从基础到我们的,从基础到他们的,并尝试通过采用 both 组更改来组合它们。每当我们触摸文件中的 不同的 行时,Git 就会接受我们或他们的更改。当我们触摸 same 行时,Git 声明合并冲突。3 Git 将结果文件写入 work-tree 为 file.txt,如果有冲突则带有冲突标记。如果将 merge.conflictStyle 设置为 diff3,冲突标记包括插槽 1 中的 base 文件,以及插槽 2 和 3 中文件中的行。我比默认方式更喜欢这种冲突方式,它省略了 slot-1 上下文并仅显示 slot-2 与 slot-3 冲突。

当然,如果有冲突,Git声明合并冲突。在这种情况下,它(最终,在处理所有其他文件之后)在合并的中间停止,在 work-tree 中留下 conflict-marker 混乱,在 file.txt 中留下所有三个副本索引,在插槽 1、2 和 3 中。但是如果 Git 能够自行解析两个不同的 change-sets,它会继续并 擦除 插槽1-3,将successfully-merged文件写入work-tree,4将work-tree文件复制到索引的正常槽零处,继续像往常一样处理其余文件。

如果合并 停止,您的工作就是解决这个问题。许多人这样做是通过编辑冲突的 work-tree 文件,弄清楚正确的结果是什么,写出 work-tree 文件,然后 运行ning git add 将该文件复制到index.5 copy-into-index步骤移除stage 1-3条目并写入正常的stage-zero条目,这样冲突就解决了,我们准备好了承诺。然后你告诉合并继续,或者直接 运行 git commit 因为 git merge --continue 只是 运行s git commit 无论如何。

这个 合并 过程虽然有点复杂,但最终非常简单:

  • 选择合并基地。
  • 将合并基础与当前提交进行比较,我们已经签出我们将通过合并修改的提交,以查看我们 更改了什么。
  • 将合并基础与 其他 提交(我们选择合并的那个)进行比较,以查看它们 发生了什么变化。
  • 合并更改,将合并的更改应用到合并基础中的快照。这就是索引中的结果。没关系我们从合并基础版本开始,因为 combined 更改 include 我们的更改:我们不会丢失它们 除非 我们说 只获取他们的文件版本

合并合并为动词 过程之后是 合并为名词 步骤,进行合并提交,合并完成。


2如果三个输入提交 具有所有相同的文件,事情就会变得棘手。我们可以有add/add冲突,modify/rename冲突,modify/delete冲突,等等,我称之为高级冲突。这些也会在中间停止合并,并根据需要填充索引的 1-3 槽。 -X 标志,-X ours-X theirs 不会 影响高层冲突。

3您可以使用-X ours-X theirs让Git选择"our change"或"their change"因冲突而停止。请注意,您将其指定为 git merge 的参数,因此它适用于 所有 有冲突的文件。有可能在冲突发生后,使用 git merge-file 以更智能和有选择性的方式一次处理一个文件,但 Git 并没有让这变得像它应该的那样容易。

4至少,Git认为文件合并成功。 Git 无非是基于 合并的两侧触及同一文件的不同行,这必须是 OK,但实际上不一定是 OK。不过,它在实践中效果很好。

5有些人更喜欢合并工具,它通常会显示所有三个输入文件并允许您构建正确的合并结果不知何故,how 取决于工具。合并工具可以简单地从索引中提取这三个输入,因为它们就在三个槽中。

git cherry-pickgit revert 是如何工作的?

这些也是 three-way 合并操作。他们使用提交图,其方式类似于 git show 使用它的方式。它们不像 git merge 那样花哨,即使它们使用 merge 作为合并代码的动词 部分。

相反,我们从您可能拥有的任何提交图开始,例如:

...---o--P--C---o--...
      .      .
       .    .
        .  .
 ...--o---o---H   <-- branch (HEAD)

HHP之间以及HC之间的实际关系如果有的话是不重要。这里唯一重要的是 current (HEAD) 提交是 H,并且有一些提交 C (the child) with a (one , 单) 父提交 P。也就是说,PC 直接是我们要选择或还原的提交的 parent-and-commit。

由于我们正在提交 H,这就是我们的索引和 work-tree 中的内容。我们的 HEAD 附加到名为 branch 的 b运行ch,并且 branch 指向提交 H.6 现在,Git 对 git 做了什么 cherry-pick <em>hash-of-C</em>很简单:

  • 选择 commit P 作为合并基础。
  • 执行标准 three-way 合并,合并为动词 部分,使用当前提交 H 作为我们的并提交 C作为他们的。

这个 merge-as-a-verb 过程发生在索引中,就像 git merge 一样。当一切都成功完成时——或者你已经清理了混乱,如果没有成功,而你运行git cherry-pick --continue—Git 继续进行 普通的 non-merge 提交。

如果你回顾一下 merge-as-a-verb 过程,你会发现这意味着:

  • diff commit P vs C:这就是他们改变的地方
  • diff commit P vs H:这就是我们改变的地方
  • 结合这些差异,将它们应用于 P
  • 中的内容

所以git cherry-pick一个three-way合并。只是他们改变的git show显示的是一样的!与此同时,我们改变的是我们将P变成H所需的一切——我们需要它,因为我们希望保持H作为我们的起点,并且只添加他们的改变那。

但这也是 cherry-pick 有时会看到一些 st运行ge(我们认为)冲突的方式和原因。它必须 combine 整套 P-vs-H 变化与 P-vs-C 变化。如果 PH 相距很远,这些变化可能是巨大的。

git revert命令和git cherry-pick一样简单,实际上是由Git中相同的源文件实现的。它所做的只是使用提交 C 作为合并基础,并将提交 P 作为 他们的 提交(同时像往常一样使用 H 作为我们的)。也就是说,Git 将 diff C,提交到 rever, vs H,看看我们做了什么。然后它将 diff C,提交恢复,与 P 来查看他们做了什么——当然,这与他们实际所做的相反。然后合并引擎,实现 merge as a verb 的部分,将组合这两组变化,将组合的变化应用到 C 并将结果放入索引和我们的 work-tree。合并后的结果保留了我们的更改(C vs H)并且撤消了他们的更改(C vs P 是reverse-diff).

如果一切顺利,我们将得到一个非常普通的新提交:

...---o--P--C---o--...
      .      .
       .    .
        .  .
 ...--o---o---H--I   <-- branch (HEAD)

HI 的区别,这是我们将在 git show 中看到的,是 copy P-to-C 变化 (cherry-pick) 或 P-to-C 变化的 逆转 (还原)。


6cherry-pick 和 revert 都拒绝 运行 除非索引和 work-tree 匹配当前提交,尽管它们确实有模式让他们与众不同。 "allowed to be different" 只是调整预期的问题。事实上,如果选择或恢复 失败 ,可能无法完全恢复。如果 work-tree 和索引与提交匹配,很容易从失败的操作中恢复,所以这就是存在此要求的原因。