在 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
指向G
,branch
指向H
.
提交 G
当然指向它的 parent F
,它指向更远的地方。因此,当我们使用 Git 时,我们从 分支名称 开始,对于我们 和 Git, last 在链中提交。从那里我们有 Git 向后工作,通过链一次提交一个。
A merge commit 只是一个至少有两个 parent 的提交,而不是通常的提交。所以合并提交 M
看起来像这样:
...--J
\
M <-- somebranch
/
...--L
其中J
和L
是合并的两个parent。通常(虽然不是绝对必要),历史首先分叉,然后合并:
I--J
/ \
...--G--H M--N--...
\ /
K--L
我们可以将 I-J
和 K-L
分支称为 分支 ,或者我们可以处理包括 M
and/or N
作为单个分支——毕竟,必须有某个分支名称指向右侧的某个提交。我们如何首先找到提交 M
?
(如果我们愿意,我们可以随时 添加 指向任何提交的分支名称。添加分支名称意味着提交现在都在一个额外的分支上,在它们之前所在的任何分支之上。删除名称将从包含这些提交的分支集中删除该分支。)
通过合并提交向后走是棘手的:Git 必须开始查看两个分支,这里是 I-J
和 K-L
分支。 Git 在内部使用 git log
和 git 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 是 A
; C
的 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-list
为 I-J
和 K-L
选择订单。合并提交 M
被简单地删除:导致合并提交 M
的两个分支被展平为一个简单的线性链。这避免了复制提交 M
的需要,代价是有时不能很好地复制提交(有很多合并冲突),如果我们想保留,当然会破坏我们漂亮的小合并气泡它。
Cherry-pick不能复制合并...
虽然您可以 运行 git cherry-pick
合并提交,但生成的提交是普通的 non-merge 提交。此外,您必须告诉 Git 使用哪个 parent。 Cherry-picking 从根本上必须区分提交的 parent 与提交,但是合并有 两个 parent,而 Git 只是不知道使用哪两个。您必须告诉它是哪一个...然后它会复制 diff 发现的更改,这不是 git merge
的全部内容。
...所以要变基并保持合并,git rebase
re-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'
并在此处放置一个标记;
- 选择
I
或K
之一复制到I'
或K'
,然后复制J
或L
;假设我们选择 I-J
做;
- 放置一个指向
J'
的标记;
git checkout
之前使用标记制作的 H'
副本;
- 复制
K
并L
现在,到 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
实际使用了哪种技术。
label
、reset
和 merge
命令创建和使用各种标记。 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?
当交互式变基时,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
指向G
,branch
指向H
.
提交 G
当然指向它的 parent F
,它指向更远的地方。因此,当我们使用 Git 时,我们从 分支名称 开始,对于我们 和 Git, last 在链中提交。从那里我们有 Git 向后工作,通过链一次提交一个。
A merge commit 只是一个至少有两个 parent 的提交,而不是通常的提交。所以合并提交 M
看起来像这样:
...--J
\
M <-- somebranch
/
...--L
其中J
和L
是合并的两个parent。通常(虽然不是绝对必要),历史首先分叉,然后合并:
I--J
/ \
...--G--H M--N--...
\ /
K--L
我们可以将 I-J
和 K-L
分支称为 分支 ,或者我们可以处理包括 M
and/or N
作为单个分支——毕竟,必须有某个分支名称指向右侧的某个提交。我们如何首先找到提交 M
?
(如果我们愿意,我们可以随时 添加 指向任何提交的分支名称。添加分支名称意味着提交现在都在一个额外的分支上,在它们之前所在的任何分支之上。删除名称将从包含这些提交的分支集中删除该分支。)
通过合并提交向后走是棘手的:Git 必须开始查看两个分支,这里是 I-J
和 K-L
分支。 Git 在内部使用 git log
和 git 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 是 A
; C
的 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-list
为 I-J
和 K-L
选择订单。合并提交 M
被简单地删除:导致合并提交 M
的两个分支被展平为一个简单的线性链。这避免了复制提交 M
的需要,代价是有时不能很好地复制提交(有很多合并冲突),如果我们想保留,当然会破坏我们漂亮的小合并气泡它。
Cherry-pick不能复制合并...
虽然您可以 运行 git cherry-pick
合并提交,但生成的提交是普通的 non-merge 提交。此外,您必须告诉 Git 使用哪个 parent。 Cherry-picking 从根本上必须区分提交的 parent 与提交,但是合并有 两个 parent,而 Git 只是不知道使用哪两个。您必须告诉它是哪一个...然后它会复制 diff 发现的更改,这不是 git merge
的全部内容。
...所以要变基并保持合并,git rebase
re-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'
并在此处放置一个标记; - 选择
I
或K
之一复制到I'
或K'
,然后复制J
或L
;假设我们选择I-J
做; - 放置一个指向
J'
的标记; git checkout
之前使用标记制作的H'
副本;- 复制
K
并L
现在,到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
实际使用了哪种技术。
label
、reset
和 merge
命令创建和使用各种标记。 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?