当以交互方式 rebase 时,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.
Run Code Online (Sandbox Code Playgroud)
这是什么label以及如何使用它?
tor*_*rek 14
Chepner 的评论是完全正确的:标签是如何git rebase --rebase-merges工作的。如果您不使用--rebase-merges,则无需进一步了解。
Rebase 通常通过复制提交来工作,就像通过git cherry-pick. 这是因为不可能更改任何现有提交。当我们使用 时git rebase,我们最终想要的是对一些现有提交的一些更改——微妙或公然由我们决定。
这在技术上根本不可能,但如果我们看看我们(人类)如何使用Git 并找到提交,这毕竟很容易。我们根本不更改提交!相反,我们将它们复制到新的和改进的提交中,然后使用新的并忘记(或放弃)旧的。
我们使用 Git 和查找提交的方式是依赖于这样一个事实,即每个提交都记录其直接前一个或父提交的哈希 ID 。这意味着提交形成了向后看的链:
... <-F <-G <-H <--branch
Run Code Online (Sandbox Code Playgroud)
该分支名称 branch持有的实际,原始哈希ID最后在链提交。在这种情况下,无论实际提交的哈希 ID 是什么,我们都用字母H作为替代来绘制它。
H作为其元数据的一部分,提交包含先前提交的原始哈希 ID,我们称之为G. 我们说那个H 指向 G,那个branch 指向 H。
CommitG当然指向它的 parent F,它又指向更远的地方。因此,当我们使用 Git 时,我们从一个分支名称开始,该名称对我们和Git 而言都记住了链中的最后一次提交。从那里我们让 Git 向后工作,一次提交一次,通过链。
一个合并提交仅仅是一个至少有两个父母犯而不是通常的一个。所以合并提交M看起来像这样:
...--J
\
M <-- somebranch
/
...--L
Run Code Online (Sandbox Code Playgroud)
其中J和L是合并的两个父项。通常(虽然不是绝对必要),历史首先分叉,然后合并:
I--J
/ \
...--G--H M--N--...
\ /
K--L
Run Code Online (Sandbox Code Playgroud)
我们可以调用I-J和K-L马刺分支,或者我们可以把一切直至并包括M和/或N作为单个分支,要有,毕竟,有些分支名指向一些朝着正确的提交。我们M首先是如何找到提交的?
(如果我们愿意,我们可以随时添加指向任何提交的分支名称。添加一个分支名称意味着提交现在都在一个额外的分支上,在它们之前所在的分支之上。删除一个名称会删除那个从包含这些提交的分支集中分支。)
通过合并提交向后走是很棘手的:Git 必须开始查看两个分支,这里是 theI-J和K-Lfork。Git 在内部使用git log并git rev-list使用优先级队列来完成这项工作,尽管我们不会在这里详细介绍。
无论如何,这里的关键是,因为提交存储父哈希 ID,并且箭头都指向后,提交形成了有向无环图或 DAG。我们和 Git使用分支名称查找提交,根据定义,该名称指向DAG 某些部分中的最后一次提交。从那里我们让 Git 倒退。
假设我们要采用一些现有的简单提交链,例如A-B-C:
...--o--o--*--o--o <-- master
\
A--B--C <-- branch (HEAD)
Run Code Online (Sandbox Code Playgroud)
并复制到新的提交是这样的:
A'-B'-C' <-- HEAD
/
...--o--o--*--o--o <-- master
\
A--B--C <-- branch
Run Code Online (Sandbox Code Playgroud)
这使用 Git 的分离 HEAD模式,其中HEAD直接指向提交。所以 namebranch仍然可以找到原始提交,而HEAD现在 detached 会找到新的副本。没有过分担心究竟是在新副本不同,如果我们现在被迫GIT中移动的名称 branch,以便它指向,而不是C,而是要C'呢?也就是说,就绘图而言,我们将这样做:
A'-B'-C' <-- branch (HEAD)
/
...--o--o--*--o--@ <-- master
\
A--B--C
Run Code Online (Sandbox Code Playgroud)
移动后branch,我们还重新附加了我们的,HEAD以便我们可以回到正常的日常 Git 模式,而不是在 rebase 中。现在,当我们寻找提交时,我们会找到新的副本,而不是原件。新副本是新的:它们具有不同的哈希 ID。如果我们真的记住了哈希 ID,我们会看到......但是我们通过从分支名称开始向后工作来找到提交,当我们这样做时,我们已经完全放弃了原件,只看到新的副本。
所以这就是 rebase 的工作方式,无论如何,在没有合并的情况下。吉特:
HEAD到副本应该去的地方;git cherry-pick(常常实际上与 git cherry-pick),每次一个; 进而HEAD.(这里有很多极端情况,例如:如果从分离的 HEAD 开始会发生什么,以及合并冲突会发生什么。我们将忽略所有这些。)
上面,我说:
不用太担心新副本到底有什么不同......
究竟有什么不同?好吧,提交本身包含所有文件的快照,以及元数据:提交者的姓名和电子邮件地址、日志消息等,对于 Git 的 DAG 来说,父哈希 ID非常重要) 的那个提交。由于新副本出现在不同的点之后——旧基是*和新基是——@显然父哈希 ID 必须改变。
鉴于通过将新提交的父项设置为当前提交来添加新提交的工作,更新的父项在复制过程中自动发生,因为我们一次复制提交,一次提交。也就是说,首先我们检查 commit @,然后我们复制A到A'. 的父级A'是@,自动。然后我们自动复制B到isB'的父级。所以这里没有真正的魔法:这只是基本的日常 Git。B'A'
不过,快照也可能不同,这才是git cherry-pick真正起作用的地方。Cherry-pick 必须将每个提交视为一组更改。要将提交视为更改,我们必须将提交的快照与提交的父级快照进行比较。
也就是说,给定:
...--G--H--...
Run Code Online (Sandbox Code Playgroud)
我们可以通过先提取到临时区域,然后提取到临时区域,然后比较两个临时区域来查看发生了什么变化。对于相同的文件,我们什么都不说;对于不同的文件,我们会生成一个差异列表。这告诉我们什么改变在。HGHH
因此,git cherry-pick要复制提交,只需将提交转换为更改即可。这需要查看提交的父级。对于 commits A-B-C,这没问题: 的父级A是*; 的父母B是A; 的父级C是B。Git 可以找到第一组更改 — *vs A— 并将更改应用到 中的快照@,并以A'这种方式进行。然后它找到A-vs-B更改并将它们应用于A'make B,依此类推。
这适用于普通的单亲提交。它根本不适用于合并提交。
假设我们有一组带有合并气泡的提交,并且这组提交本身可以重新定位:
I--J
/ \
H M <-- feature (HEAD)
/ \ /
/ K--L
/
...--G-------N--O--P <-- mainline
Run Code Online (Sandbox Code Playgroud)
我们现在可能喜欢git rebase在feature提交之上提交P。如果我们这样做,默认结果是:
...--G-------N--O--P <-- mainline
\
H'-I'-J'-K'-L' <-- feature (HEAD)
Run Code Online (Sandbox Code Playgroud)
或者:
...--G-------N--O--P <-- mainline
\
H'-K'-L'-I'-J' <-- feature (HEAD)
Run Code Online (Sandbox Code Playgroud)
(为了节省空间,我没有费心绘制废弃的提交。)
它是由git rev-list挑选的顺序I-J和K-L重订基期进程的列表提交到复制部分时。Commit M,即合并,被简单地删除:导致合并提交的两个分支M被扁平化为一个简单的线性链。这避免了复制 commit 的需要M,代价是有时无法很好地复制提交(有很多合并冲突),当然如果我们想保留它,也会破坏我们漂亮的小合并气泡。
虽然您可以git cherry-pick在合并提交上运行,但生成的提交是普通的非合并提交。此外,您必须告诉 Git 使用哪个父级。Cherry-picking 从根本上必须区分提交的父级与提交,但合并有两个父级,Git 根本不知道使用这两者中的哪一个。你必须告诉它是哪一个......然后它复制差异发现的更改,这不是git merge全部。
git rebase 重新执行合并这一切的意思git rebase是,为了“保留”合并,Git 必须自己运行git merge。
也就是说,假设我们得到:
I--J
/ \
H M <-- feature (HEAD)
/ \ /
/ K--L
/
...--G-------N--O--P <-- mainline
Run Code Online (Sandbox Code Playgroud)
我们希望实现:
I'-J'
/ \
H' M' <-- feature (HEAD)
/ \ /
/ K'-L'
/
...--G-------N--O--P <-- mainline
Run Code Online (Sandbox Code Playgroud)
Git 的 rebase 可以做到这一点,但要做到这一点,它必须:
H到H'这里并放置一个标记;I或之一K复制到I'或K',然后复制其中一个J或L下一个;假设我们选择I-J做;J';git checkoutH'它之前使用标记制作的副本;K和L现在,到K'和L',并在此处放置一个标记所以到目前为止,作为我们的中间结果,我们有:
I'-J' <-- marker2
/
H' <-- marker1
/ \
/ K'-L' <-- marker3
/
...--G-------N--O--P <-- mainline
Run Code Online (Sandbox Code Playgroud)
GIT中现在可以git checkout提交J'使用标记2,运行git merge在提交L'使用标记3,并由此产生提交M',一个新的合并所使用H'作为其合并基础和J'和L'作为其两个分支末端的提交。
合并完成后,整个 rebase 就完成了,Git 可以feature像往常一样删除标记并拉取分支名称。
如果我们有点聪明,我们HEAD有时可以让作为三个标记之一,但每次只删除标记更直接。我不确定git rebase --rebase-merges实际使用哪种技术。
的label,reset和merge命令创建和使用各种标记。该merge命令需要HEAD指向将成为结果合并的第一个父级的提交(因为git merge这样工作)。有趣的是,语法表明这里禁止章鱼合并:它们应该 Just Work,因此应该被允许。
(该-C在merge命令可以使用原来合并的原始哈希ID提交,因为这是永远不变的。你会看到的标签,如果你使用--rebase-merges了一组提交的包含合并,由Git的产生从提交信息,直到最近这里还有一个错误。)
--ours合并无法生存当 Git 重新执行合并时,它只使用常规的合并引擎。Git 不知道合并期间使用的任何标志,或作为“邪恶合并”引入的任何更改。因此-X ours,--ours在这种变基期间,或,或额外的更改会丢失。当然,如果合并有合并冲突,您有机会重新插入邪恶合并更改,或者根据您的喜好完全重做合并。
另请参阅git 中的邪恶合并?