在 Git 中交互式变基时什么是 "label"

What is a "label" when rebasing interactively in Git

当交互式变基时,Git 将打开一个编辑器,其中包含可以使用的命令。其中三个命令与名为 label.

的东西有关
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.

这是什么 label 以及如何使用它?

完全正确:标签是 git rebase --rebase-merges 的工作方式。如果您不使用 --rebase-merges,则无需进一步了解。

重新定基作为一个概念

Rebase 通常通过 复制 提交来工作,就好像通过 git cherry-pick 一样。这是因为不可能 更改 任何现有的提交。当我们使用 git rebase 时,最终我们想要的是对某些现有提交进行一些更改(微妙或明显取决于我们)。

这在技术上根本不可能,但如果我们看看我们(人类)如何使用Git和找到提交,毕竟这很容易。我们根本不更改提交!相反,我们 将它们复制 到 new-and-improved 提交,然后使用新的并忘记(或放弃)旧的。

寻找提交:画图

我们使用 Git 和查找提交的方式是依赖于这样一个事实,即每个提交记录其直接前身的哈希 ID 或 parent 提交。这意味着提交形式 backwards-looking chains:

... <-F <-G <-H   <--branch

分支名称 branch 包含链中 last 提交的实际原始哈希 ID。在这种情况下,无论实际提交的哈希 ID 是什么,我们都用字母 H 将其绘制为 stand-in.

提交 H 包含作为其元数据的一部分的早期提交的原始哈希 ID,我们称之为 G。我们说H指向Gbranch指向H .

提交 G 当然指向它的 parent F,它指向更远的地方。因此,当我们使用 Git 时,我们从 分支名称 开始,对于我们 Git, last 在链中提交。从那里我们有 Git 向后工作,通过链一次提交一个。

A merge commit 只是一个至少有两个 parent 的提交,而不是通常的提交。所以合并提交 M 看起来像这样:

...--J
      \
       M   <-- somebranch
      /
...--L

其中JL是合并的两个parent。通常(虽然不是绝对必要),历史首先分叉,然后合并:

          I--J
         /    \
...--G--H      M--N--...
         \    /
          K--L

我们可以将 I-JK-L 分支称为 分支 ,或者我们可以处理包括 M and/or N 作为单个分支——毕竟,必须有某个分支名称指向右侧的某个提交。我们如何首先找到提交 M

(如果我们愿意,我们可以随时 添加 指向任何提交的分支名称。添加分支名称意味着提交现在都在一个额外的分支上,在它们之前所在的任何分支之上。删除名称将从包含这些提交的分支集中删除该分支。)

通过合并提交向后走是棘手的:Git 必须开始查看两个分支,这里是 I-JK-L 分支。 Git 在内部使用 git loggit rev-list 使用 priority queue 执行此操作,但我们不会在这里讨论任何细节。

无论如何,这里的关键是因为提交存储parent哈希ID,并且箭头都指向后方,所以提交形成Directed Acyclic Graph或DAG .我们和 Git-find 使用分支名称提交,根据定义指向 DAG 某些部分中的 last 提交.从那里我们 Git 向后走。

简而言之重新定基

假设我们要采用一些现有的简单提交链,例如 A-B-C 这里:

...--o--o--*--o--o   <-- master
            \
             A--B--C   <-- branch (HEAD)

将它们复制到新的提交中,如下所示:

                   A'-B'-C'  <-- HEAD
                  /
...--o--o--*--o--o   <-- master
            \
             A--B--C   <-- branch

这使用 Git 的 分离 HEAD 模式,其中 HEAD 直接指向提交。所以名称 branch 仍然找到原始提交,而 HEAD,现在分离,找到新的副本。无需过多担心新副本中 究竟有何不同,如果我们现在强制 Git 将 移动 name branch 这样它指向的不是 C,而是 C'?也就是说,在绘图方面,我们将这样做:

                   A'-B'-C'  <-- branch (HEAD)
                  /
...--o--o--*--o--@   <-- master
            \
             A--B--C

移动 branch 之后,我们还 re-attach 我们的 HEAD 这样我们就可以回到正常的日常 Git 模式,而不是在变基过程中.现在,当我们寻找提交时,我们会找到新的副本,而不是原件。新副本是 new: 它们具有不同的哈希 ID。如果我们实际上 记住了 哈希 ID,我们会看到...但是我们 find 通过从分支名称开始并向后工作来提交,并且当我们这样做时,我们已经完全放弃了原件,只看到了新的副本。

这就是 rebase 的工作原理,无论如何,在没有合并的情况下。 Git:

  • 列出一些要复制的提交;
  • HEAD 分离到副本应该去的地方;
  • 复制提交,就好像通过git cherry-pick(通常实际上是 git cherry-pick),一次一个;然后
  • 移动分支名称并 re-attaches HEAD.

(这里有很多极端情况,例如:如果您从分离的 HEAD 开始会发生什么,以及合并冲突会发生什么。我们将忽略所有这些。)

关于cherry-pick

的一点

以上,我说:

Without worrying too much about what exactly is different in the new copies ...

究竟有什么不同?好吧,提交本身包含所有文件的 快照 ,以及元数据:提交者的姓名和电子邮件地址、日志消息等,以及 all-important 对于 Git 的 DAG,该提交的 parent 哈希 ID。由于新副本位于不同的点之后——旧基数为 *,新基数为 @——显然 parent 哈希 ID 必须更改。

鉴于通过将新提交的 parent 设置为当前提交来添加新提交,更新的 parents 在复制过程中自动发生,因为我们复制提交,一个提交在一个时间。也就是说,首先我们检查提交 @,然后我们将 A 复制到 A'A' 的 parent 自动为 @。然后我们将B自动复制到B'B'的parent是A'。所以这里没有真正的魔法:这只是基本的日常 Git.

不过,快照可能也不同,这就是 git cherry-pick 真正发挥作用的地方。Cherry-pick 必须将每个提交视为一组 更改.要将提交视为更改,我们必须 比较 提交的快照与提交的 parent 的快照。

即给定:

...--G--H--...

我们可以先解压G到一个临时区,然后解压H到一个临时区,看看H中改变了什么 ,然后比较两个临时区域。对于相同的文件,我们什么都不说;对于不同的文件,我们生成一个差异列表。这告诉我们在 H.

发生了什么变化

所以git cherry-pick要复制提交,只需将提交变成更改即可。这需要查看提交的 parent。对于提交 A-B-C,这没问题:A 的 parent 是 *B 的 parent 是 AC 的 parent 是 B。 Git 可以找到第一组更改 — *A — 并将更改应用到 @ 中的快照,并以这种方式制作 A'。然后它找到 A-vs-B 变化并将这些变化应用于 A' 以生成 B,依此类推。

这适用于普通的 single-parent 提交。它对合并提交根本不起作用

复制合并是不可能的,所以 rebase 不会尝试

假设我们有一组带有合并气泡的提交,并且这组提交本身可以变基:

           I--J
          /    \
         H      M   <-- feature (HEAD)
        / \    /
       /   K--L
      /
...--G-------N--O--P   <-- mainline

我们现在可能希望 git rebase feature 提交在提交 P 之上。如果我们这样做,default 结果是:

...--G-------N--O--P   <-- mainline
                    \
                     H'-I'-J'-K'-L'  <-- feature (HEAD)

或:

...--G-------N--O--P   <-- mainline
                    \
                     H'-K'-L'-I'-J'  <-- feature (HEAD)

(为了保存 space,我没有费心绘制废弃的提交。)

在变基过程的 list-commits-to-copy 部分,由 git rev-listI-JK-L 选择订单。合并提交 M 被简单地删除:导致合并提交 M 的两个分支被展平为一个简单的线性链。这避免了复制提交 M 的需要,代价是有时不能很好地复制提交(有很多合并冲突),如果我们想保留,当然会破坏我们漂亮的小合并气泡它。

Cherry-pick不能复制合并...

虽然您可以 运行 git cherry-pick 合并提交,但生成的提交是普通的 non-merge 提交。此外,您必须告诉 Git 使用哪个 parent。 Cherry-picking 从根本上必须区分提交的 parent 与提交,但是合并有 两个 parent,而 Git 只是不知道使用哪两个。您必须告诉它是哪一个...然后它会复制 diff 发现的更改,这不是 git merge 的全部内容。

...所以要变基并保持合并,git rebasere-performs合并

这对 git rebase 意味着,为了 "preserve" 合并,Git 必须 运行 git merge 本身。

也就是说,假设我们得到:

           I--J
          /    \
         H      M   <-- feature (HEAD)
        / \    /
       /   K--L
      /
...--G-------N--O--P   <-- mainline

我们想要实现:

                         I'-J'
                        /    \
                       H'     M'  <-- feature (HEAD)
                      / \    /
                     /   K'-L'
                    /
...--G-------N--O--P   <-- mainline

Git的rebase可以做到这一点,但是要做到这一点,必须:

  • H复制到H'并在此处放置一个标记;
  • 选择IK之一复制到I'K',然后复制JL;假设我们选择 I-J 做;
  • 放置一个指向 J' 的标记;
  • git checkout 之前使用标记制作的 H' 副本;
  • 复制KL 现在,到 K'L',并在此处放置一个标记

因此作为我们的中间体 result-so-far,我们有:

                         I'-J'   <-- marker2
                        /
                       H'  <-- marker1
                      / \
                     /   K'-L'   <-- marker3
                    /
...--G-------N--O--P   <-- mainline

Git 现在可以 git checkout 使用标记 2 提交 J',运行 git merge 使用标记 3 提交 L',从而生成提交 M',一个使用 H' 作为其合并基础并使用 J'L' 作为其两个 branch-tip 提交的新合并。

一旦合并完成,rebase-as-a-whole 就完成了,Git 可以像往常一样删除标记并拉出分支名称 feature

如果我们聪明一点,有时我们可以让HEAD充当三个标记之一,但每次只丢弃标记更直接。我不确定 git rebase --rebase-merges 实际使用了哪种技术。

labelresetmerge 命令创建和使用各种标记。 merge 命令要求 HEAD 指向将成为结果合并的第一个 parent 的提交(因为 git merge 以这种方式工作)。有趣的是,语法表明章鱼合并在这里是被禁止的:它们应该只是工作,因此应该被允许。

merge 命令中的 -C 可以使用原始合并提交的原始哈希 ID,因为它始终不变。如果使用 [=24,您将看到的标签=] 和一组包含合并的提交,由 Git 从提交消息中生成,直到最近这里还有一个错误。)

旁注:邪恶合并和 --ours 合并无法生存

当 Git re-performs 合并时,它只使用常规合并引擎。 Git 不知道合并期间使用的任何标志,或作为 "evil merge" 引入的任何更改。所以 -X ours--ours 或额外的更改在这种变基过程中会丢失。当然,如果合并有合并冲突,您有机会 re-insert evil-merge 更改,或者根据需要完全重做合并。

另见 Evil merges in git?