当交互式变基时,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
指向G
,branch
指向H
。
提交G
当然指向它的父F
,后者指向更远的地方。 因此,当我们使用 Git 时,我们从一个分支名称开始,对于我们和Git 来说,它都会记住链中的最后一次提交。 从那里我们让 Git 向后工作,一次一个提交,通过链。
合并提交只是具有至少两个父级的提交,而不是通常的提交。 因此,合并提交M
如下所示:
...--J
M <-- somebranch
/
...--L
其中J
和L
是合并的两个父项。 通常(尽管不是绝对必要的),历史首先分叉,然后合并:
I--J
/
...--G--H M--N--...
/
K--L
我们可以将I-J
和K-L
的支线称为分支,或者我们可以将所有内容(包括M
和/或N
)视为单个分支 - 毕竟,必须有一些分支名称指向右侧的某个提交。 我们最初是如何找到提交M
的?
(如果我们愿意,我们可以随时添加指向任何提交的分支名称。 添加分支名称意味着提交现在都位于另一个分支上,而不是它们之前所在的分支。 删除名称会从包含这些提交的分支集中删除该分支。
向后浏览合并提交是很棘手的:Git 必须开始查看两个分支,这里是I-J
和K-L
分支。 Git 使用优先级队列在内部使用git log
和git 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
我们现在可能想在提交P
上git rebase
feature
提交。 如果我们这样做,默认结果为:
...--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-J
和K-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'
的I
或K
之一,然后复制J
或L
下一步; 假设我们选择I-J
做; - 放下指向
J'
的标记; git checkout
之前使用标记制作的H'
副本;- 立即复制
K
并L
,以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
实际使用哪种技术。
label
、reset
和merge
命令创建和使用各种标记。merge
命令要求HEAD
指向将成为结果合并的第一个父级的提交(因为git merge
以这种方式工作)。 有趣的是,语法表明章鱼合并在这里是被禁止的:它们应该正常工作,因此应该被允许。
(merge
命令中的-C
可以使用原始合并提交的原始哈希 ID,因为这始终保持不变。 如果您将--rebase-merges
与一组包含合并的提交一起使用,您将看到的标签是由 Git 从提交消息生成的,直到最近这里还有一个错误。
旁注:邪恶合并和--ours
合并无法生存
当 Git 重新执行合并时,它只使用常规的合并引擎。 Git 不知道合并期间使用的任何标志,也不知道作为"邪恶合并"引入的任何更改。 因此,-X ours
,或--ours
,或额外的更改在这种变基过程中会丢失。 当然,如果合并有合并冲突,您有机会重新插入邪恶合并更改,或者完全按照您喜欢的方式重做合并。
参见 邪恶合并在 git 中?