在 Git 中以交互方式变基时什么是“标签”

Luc*_*Luc 8 git git-rebase

当以交互方式 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)

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

          I--J
         /    \
...--G--H      M--N--...
         \    /
          K--L
Run Code Online (Sandbox Code Playgroud)

我们可以调用I-JK-L马刺分支,或者我们可以把一切直至并包括M和/或N作为单个分支,要有,毕竟,有些分支名指向一些朝着正确的提交。我们M首先是如何找到提交的?

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

通过合并提交向后走是很棘手的:Git 必须开始查看两个分支,这里是 theI-JK-Lfork。Git 在内部使用git loggit 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 @,然后我们复制AA'. 的父级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*; 的父母BA; 的父级CB。Git 可以找到第一组更改 — *vs A— 并将更改应用到 中的快照@,并以A'这种方式进行。然后它找到A-vs-B更改并将它们应用于A'make B,依此类推。

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

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

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

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

我们现在可能喜欢git rebasefeature提交之上提交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-JK-L重订基期进程的列表提交到复制部分时。Commit M,即合并,被简单地删除:导致合并提交的两个分支M被扁平化为一个简单的线性链。这避免了复制 commit 的需要M,代价是有时无法很好地复制提交(有很多合并冲突),当然如果我们想保留它,也会破坏我们漂亮的小合并气泡。

Cherry-pick无法复制合并...

虽然您可以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 可以做到这一点,但要做到这一点,它必须:

  • 复制HH'这里并放置一个标记;
  • 选择I或之一K复制到I'K',然后复制其中一个JL下一个;假设我们选择I-J做;
  • 放置一个指向 的标记J'
  • git checkoutH'它之前使用标记制作的副本;
  • 复制KL现在,到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实际使用哪种技术。

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

(该-Cmerge命令可以使用原来合并的原始哈希ID提交,因为这是永远不变的。你会看到的标签,如果你使用--rebase-merges了一组提交的包含合并,由Git的产生从提交信息,直到最近这里还有一个错误。)

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

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

另请参阅git 中的邪恶合并?

  • 哦,伙计……这个答案让我希望我从来没有问过这个问题。但是,我猜当涉及 Git 时就会发生这种情况。我认为您可以放心地期待宽限期后的复选标记。 (2认同)
  • @Luc:好吧,请记住顶部的第二句话:*如果您不使用`--rebase-merges`,您不需要进一步了解任何内容。* (2认同)
  • 在这种情况下,您可能想看看[this](http://web.torek.net/torek/tmp/book.pdf),但只有它的第一部分(背景和理论)接近完成... (2认同)