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



当交互式变基时,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以及如何使用它?

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

重新定位为一个概念

通常通过复制提交来变基,就像通过git cherry-pick一样。 这是因为不可能更改任何现有提交。 当我们使用git rebase时,我们最终想要的是一些现有的提交所做的一些更改——微妙的或公然的取决于我们。

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

查找提交:绘制图形

我们使用 Git 并查找提交的方式是依赖于每个提交记录其直接前身或提交的哈希 ID 的事实。 这意味着提交形成向后看的链:

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

分支名称branch保存链中最后一个提交的实际原始哈希 ID。 在这种情况下,无论实际提交的哈希 ID 是什么,我们都用字母H作为替身来绘制它。

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

提交G当然指向它的父F,后者指向更远的地方。 因此,当我们使用 Git 时,我们从一个分支名称开始,对于我们Git 来说,它都会记住链中的最后一次提交。 从那里我们让 Git 向后工作,一次一个提交,通过链。

合并提交只是具有至少两个父级的提交,而不是通常的提交。 因此,合并提交M如下所示:

...--J

M   <-- somebranch
/
...--L

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

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

我们可以将I-JK-L的支线称为分支,或者我们可以将所有内容(包括M和/或N)视为单个分支 - 毕竟,必须有一些分支名称指向右侧的某个提交。 我们最初是如何找到提交M的?

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

向后浏览合并提交是很棘手的:Git 必须开始查看两个分支,这里是I-JK-L分支。 Git 使用优先级队列在内部使用git loggit rev-list执行此操作,尽管我们不会在这里详细介绍。

无论如何,这里的关键是,因为提交存储哈希 ID,并且箭头都指向后方,所以提交形成有向无环图或 DAG。 我们和 Git 使用分支名称查找提交,根据定义,分支名称指向 DAG 某些部分中的最后一次提交。 从那里我们有 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 将名称branch移动,以便它指向,而不是指向C,而是指向C'呢? 也就是说,就绘图而言,我们将这样做:

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

A--B--C

移动branch后,我们还重新附加了HEAD,以便我们可以回到正常的日常 Git 模式,而不是在变基过程中。 现在,当我们寻找提交时,我们会找到新的副本,而不是原始副本。 新副本是新的:它们具有不同的哈希 ID。 如果我们真的记得哈希 ID,我们会看到......但是我们通过从分支名称开始并向后工作来找到提交,当我们这样做时,我们完全放弃了原始副本,只看到新副本。

所以这就是变基的工作方式,无论如何,在没有合并的情况下。 Git:

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

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

关于樱桃采摘的一点

上面,我说:

不用太担心新副本中到底有什么不同......

究竟有什么不同? 好吧,提交本身包含所有文件的快照,以及元数据:提交者的姓名和电子邮件地址,日志消息等,对于Git的DAG来说非常重要,即该提交的哈希ID。 由于新副本来自不同的点(旧基*,新基@),显然父哈希 ID 必须更改。

鉴于添加新提交的工作原理是将新提交的父级设置为当前提交,则更新的父级在复制过程中会自动发生,因为我们复制提交时,一次一个提交。 也就是说,首先我们签出提交@,然后将A复制到A'A'的父项是自动@的。 然后我们将B复制到B',并且B'的父级会自动A'。 所以这里没有真正的魔力:这只是基本的日常 Git。

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

也就是说,给定:

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

我们可以看到H的变化,首先将G提取到临时区域,然后将H提取到临时区域,然后比较两个临时区域。 对于相同的文件,我们什么都不说;对于不同的文件,我们生成差异列表。 这告诉我们H发生了什么变化。

因此,对于复制提交git cherry-pick,它只需要将提交转换为更改。 这需要查看提交的父级。 对于提交A-B-C,没问题:A的父级是*;B的父级是A;而C的父母是B. Git 可以找到第一组更改(*vsA),并将更改应用于@中的快照,并以这种方式进行A'。 然后,它找到A-vs-B更改,并将其应用于A'以进行B,依此类推。

这适用于普通的单亲提交。 它根本不适用于合并提交。

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

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

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

我们现在可能想在提交Pgit rebasefeature提交。 如果我们这样做,默认结果为:

...--G-------N--O--P   <-- mainline

H'-I'-J'-K'-L'  <-- feature (HEAD)

或:

...--G-------N--O--P   <-- mainline

H'-K'-L'-I'-J'  <-- feature (HEAD)

(为了节省空间,我没有费心在废弃的提交中绘制。

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

樱桃采摘无法复制合并...

虽然您可以在合并提交上运行git cherry-pick,但生成的提交是普通的非合并提交。 此外,您必须告诉 Git 要使用哪个父级。 樱桃采摘从根本上必须区分提交的父级和提交的父级,但是合并有两个父级,Git 根本不知道使用两者中的哪一个。 你必须告诉它哪一个...然后它复制差异找到的更改,这不是git merge的全部内容。

。因此,要变基并保持合并,git rebase重新执行合并

这一切对git rebase意味着,为了"保留"合并,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 的变基可以做到这一点,但要做到这一点,它必须:

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

因此,作为到目前为止的中间结果,我们有:

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

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

合并完成后,变基作为一个整体就完成了,Git 可以像往常一样删除标记并将分支名称拉feature

如果我们有点聪明,我们有时可以让HEAD充当三个标记之一,但每次都放下标记会更简单。 我不确定git rebase --rebase-merges实际使用哪种技术。

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

(merge命令中的-C可以使用原始合并提交的原始哈希 ID,因为这始终保持不变。 如果您将--rebase-merges与一组包含合并的提交一起使用,您将看到的标签是由 Git 从提交消息生成的,直到最近这里还有一个错误。

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

当 Git 重新执行合并时,它只使用常规的合并引擎。 Git 不知道合并期间使用的任何标志,也不知道作为"邪恶合并"引入的任何更改。 因此,-X ours,或--ours,或额外的更改在这种变基过程中会丢失。 当然,如果合并有合并冲突,您有机会重新插入邪恶合并更改,或者完全按照您喜欢的方式重做合并。

参见 邪恶合并在 git 中?

最新更新