当git合并了一个远程分支时,它会为该分支重新设置基础

时间:2019-12-11 15:30:35

标签: git github version-control commit rebase

我对github上一个分支的PR显示了如下提交:

- "commit msg 1"
- "commit msg 2"
- "Merge remote-tracking branch 'upstream/dev' into this branch."
- "commit msg 3"
- "commit msg 4"
- "Merge remote-tracking branch 'upstream/dev' into this branch."

我想重新建立该分支的基础,并将所有四个带有消息"commit msg *"的提交压缩为一个提交。

首先我尝试:

git rebase -i <commit id of the first commit> 

它向我展示了包含许多其他提交的历史记录,这些历史记录是通过合并upstream/dev的结果而引入的;显示如下输出:

- pick "commit msg1"
- pick "someone else's commit 1"
- pick "someone else's commit 2"
- pick "someone else's commit 3"
- pick "commit msg2"
- pick "someone else's commit 4"
- pick "someone else's commit 5"
... 

我尝试将所有提交的pick设置为f,解决合并冲突后,它显示了分支中upstream/dev中所做的所有更改,就好像我重新实施它们。

我尝试过: -https://stackoverflow.com/a/5201642 -https://stackoverflow.com/a/5190323

我知道我可以尝试merge --squash(例如https://stackoverflow.com/a/5309051/947889),但这会创建一个单独的分支。

为简化说明,此处的示例提交得到了简化,实际分支包含〜250个提交,使用rebase时,它显示了〜300,000个提交,这是有道理的,因为它是在活动存储库上实现的一项重要功能历时2年以上。

关于如何最好地将该分支重新构建为单个提交的任何建议?

1 个答案:

答案 0 :(得分:1)

TL; DR

您几乎可以肯定地要做git merge --squash,但是要完成一个分离的HEAD,然后执行分支移动操作,例如:

$ git checkout upstream/master
$ git merge --squash yourbranch
$ git checkout -B yourbranch     # or git branch -f yourbranch HEAD

(但请参见下面的详细答案)。

  

我知道我可以尝试合并--squash(例如https://stackoverflow.com/a/5309051/947889),但这会创建一个单独的分支。

使用git merge --squash 不会创建一个单独的分支(它只会创建一个提交)。但是,是否这样做并不重要,因为Git的分支基本上是没有意义的:您可以随时随地更改或重新排列分支名称。在Git中重要的不是分支-更确切地说是分支名称-而是 commits Git存储库是提交的集合,外加一些辅助信息。辅助信息可以更改。提交不能。 分支名称是此可变辅助信息的一部分。

每个提交都有自己独特的大丑陋哈希ID。这些哈希ID是提交的真实名称。每个提交都是完全,完全只读的。您不能更改任何现有的提交。提交的哈希ID表示那个提交,没有其他提交。但是关于这些哈希ID的事情是,它们似乎是完全随机的。您将如何找到正确的哈希ID?

一方面,每个提交都存储其他一些较早提交的哈希ID。这些是这一特定提交的 parent 提交。大多数提交仅存储一个父哈希ID。

当一个提交存储另一个较早提交的哈希ID时,我们说后面的提交指向。 (请注意,没有提交可以存储以后提交的哈希ID,因为在创建较早的提交时,该后来的提交的哈希ID尚不存在,一旦创建,则不再提交可以更改。)因此,当一长串的提交被一个接一个地创建时,每个指向向后指向上一个提交。如果将其绘制出来(使用大写字母代表真实的提交哈希ID),我们将获得如下图所示:

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

这里H最新提交,带有一些哈希ID H 。现在一直冻结的实际提交本身包含较早提交G的原始哈希ID。提交G包含较早提交F的原始哈希ID,依次包含另一个较早提交哈希ID,依此类推。

这是分支名称的来源。分支名称仅包含我们要说的“ <分支>”上的最后提交的哈希ID。因此,如果H是某个分支上的 last 提交,我们只需将其哈希ID放入分支名称中即可。现在,该名称指向提交H,就像H指向G一样:

...--F--G--H   <-- branch1

我们现在可以创建另一个分支名称,例如base,并使其指向现有的提交F(使用git branch base <hash-of-F>):

...--F   <-- base
      \
       G--H   <-- branch1

我们不得不绘制提交图-F-G-H线-挤压名称有些不同,但是 commits 完全没有改变。我们所做的只是命名一个指向F的新名称,因此F是分支base上的最后一次提交。名称branch1仍标识H,因此H是分支branch1上的最后一次提交。

我们删除basegit branch -d base),并重新命名为feature的新名称H。我们还要确保也git checkout feature,以便将HEAD附加到名称feature上:

...--F--G--H   <-- branch1, feature (HEAD)

(例如,我们可以使用git checkout -b feature branch1进行此操作。)现在,我们将以通常的方式进行新的提交。这个新的提交获得了一个新的唯一的哈希ID,但我们仅将其称为I。 Git现在要做的是移动名称feature,使其指向新的提交I。新提交I的父级是H,因此I指向H

...--F--G--H   <-- branch1
            \
             I   <-- feature (HEAD)

这就是分支:它们只是指向特定提交的名称或标签,并带有一个特殊的技巧,当您git checkout其中一个分支时,不仅要准备好使用该提交,您还可以安排 next git commit操作来 update 名称。

您在Git中所做的几乎所有事情都是关于创建或获取提交,然后使各种名称指向这些新创建或获取的提交中的特定提交。分支名称仅允许您查找一些特定的提交。根据定义,名称是该分支上的 last 提交。无论如何移动分支名称都没有关系。只要存在该名称,就指向某个提交。该提交是分支上的最后一个提交。移动名称,您已经更改了哪个提交是分支上的最后一个提交。您尚未更改任何 commits -它们都还存在-您仅更改了哪个提交是那个分支的最后一个

这使我们得以重新定位

git rebase的作用是复制一组提交,然后移动分支名称。例如,考虑以下图形:

...--F--G--H   <-- master
         \
          I--J   <-- feature

首先,对要复制后移动的名称进行git checkout

$ git checkout feature

然后,您运行git rebase命令。它带有一些参数,文档将其称为--ontoupstream参数。这些指定了 target 提交,这是副本应该去的地方,以及应该被复制的提交:

$ git rebase master

您只能提供一个参数,例如此处git rebase master,在这种情况下,使用该名称既可以找到目标提交,也可以找到一组提交。这里,目标提交是提交H,要复制的提交集是提交IJ

现在,rebase命令将复制每个提交,就像使用git cherry-pick一样。副本获得新的哈希ID。这里有很多小巧的案例,您可以使用git rebase的选项,但是在这种情况下,这很简单,我们最终得到的I副本具有不同的哈希ID(我们称为I')和J的副本(我们称为J')。 II'之间有两个大区别,我们在图中可以看到的是I'的父不是G而是{{1} }。提交副本H也是这样:

J'

(您在该图中看不到的区别是,通过提交 I'-J' <-- HEAD (detached) / ...--F--G--H <-- master \ I--J <-- feature 保存的快照可能与通过I'保存的快照有所不同,因为cherry-pick有效地将更改从I更改为G,并将更改应用于I中的快照,而不是H。)

已经复制了这两个提交,并通过移动分支名称完成了变基:

G

提交 I'-J' <-- feature (HEAD) / ...--F--G--H <-- master \ I--J [abandoned] I会发生什么?答案有点复杂,但是我们现在需要的是:什么都没有。 Git将它们保留一段时间,以防万一您认为重新设置基准不是一个好主意。但是它们变得很难找到。新提交J很容易找到:名称J'找到了。提交feature很容易找到:我们只需要转到I',然后沿着其向后的箭头指向J'。但是提交I'的哈希ID是什么?它是J ,但现在已经不存在了。如果可以找到feature,则可以用它来找到J,但是除非您将I的哈希ID保存在某个地方,否则可能会有些棘手。 1 < / sup>最终-通常是从现在起30天后的某个时间-如果您未使用其他名称(例如,其他分支或标签名称)来确保它们仍然存在,Git将完全收回它们,不再需要。 / p>


1 目前,真的很容易找到:Git将其保存为J。但是其他命令将替换ORIG_HEAD的哈希ID。不过,还有第二种方法可以使用ORIG_HEAD来找到它,这就是默认情况下将提交至少保留一个月左右的原因。


为什么要重新设置副本太多

一个合并提交有两个(或更多)父母。当我们让Git遵循从提交到其父母的向后指向链接时,通常遵循 all 链接。因此,从合并提交开始,Git沿两个路径前进。

您真正的提交图根本不简单,但是您的问题示例提交图可能还不错。可能看起来像这样:

git reflog

在这里, Y--Z <-- upstream/master / ...--o--o--o--W--o--o--o--X <-- upstream/dev \ \ \ A--B-----M--C--D--N <-- yourbranch (HEAD) 上不可到达的yourbranch上的六个提交(即upstream/dev不能到达)是X。让我们仔细看看:

  • 如果我们从提交A-B-M-C-D-N开始并向后工作,我们将发现的提交都是XW之间的所有未标记的提交,再加上X ,以及W之前的所有未标记字符。

  • 如果我们从W开始-提交yourbranch-向后工作,我们将访问commit N(通过X的链接) 提交N(通过D的链接)。从ND,然后到C,然后到两者 M和一些未命名的提交。我们也从B到达了未命名的提交。

  • 如果我们从X开始或提交origin/master,我们将访问Z,然后依次访问Z,然后访问YW之前的所有未命名的提交。

因此,如果我们像这样运行W

git rebase

您的Git将从git checkout yourbranch git rebase upstream/master 列出所有无法通过N到达的提交。您将Z用作目标(upstream/master)和上游(“不要复制从--onto可到达的提交”)。这意味着Git 不会复制Z或更早的提交(它们可以从W到达,但是复制Z也提交o-o-o-X。 Rebase通常会丢弃所有合并提交,因此它将同时丢掉A-B-C-DM,但是剩下的是复制了八份提交,而不是四份。

您可以使基准副本的提交次数减少

您可以执行以下操作:

N

这会将不复制内容参数与复制副本的位置分开。我们仍然告诉rebase:在提交git rebase --onto upstream/master upstream/dev 之后放入副本,但是这次,我们告诉rebase:*不要复制Z可以到达的提交。因此,Git将提交X列为要复制的提交,然后将A-B-M-C-D-NM扔掉,因为它们是合并的,剩下的工作就是复制N。 / p>

如果此重新部署一切顺利,您将得到以下帮助:

A-B-C-D

您现在可以从中创建拉取请求。

您要提交一次

  

我想[结束]一次提交。

也就是说,如果我们绘制所需的结果,它可能看起来像这样:

                     A'-B'-C'-D'  <-- yourbranch (HEAD)
                    /
                Y--Z   <-- upstream/master
               /
...--o--o--o--W--o--o--o--X   <-- upstream/dev
         \        \        \
          A--B-----M--C--D--N   [abandoned]

其中 ABCD <-- yourbranch (HEAD) / Y--Z <-- upstream/master / ...--o--o--o--W--o--o--o--X <-- upstream/dev \ \ \ A--B-----M--C--D--N [abandoned] 是单次提交,如果您首先进行基础调整,然后再进行第二次基础调整,则将它们压榨成一次提交即可。

要到达那里,可以使用以下命令序列:

ABCD

第一个$ git checkout upstream/master $ git merge --squash yourbranch $ git checkout -B yourbranch # or git branch -f yourbranch HEAD 为您提供一个分离的HEAD ,指向由git checkout标识的提交,即提交upstream/master。如果愿意,可以使用临时分支名称:

Z

这给您:

$ git checkout -b temp upstream/master

或:

                Y--Z   <-- upstream/master, HEAD
               /
...--o--o--o--W--o--o--o--X   <-- upstream/dev
         \        \        \
          A--B-----M--C--D--N   <-- yourbranch

Y--Z <-- upstream/master, temp (HEAD) / ...--o--o--o--W--o--o--o--X <-- upstream/dev \ \ \ A--B-----M--C--D--N <-- yourbranch 使用您想要的内容构建一个新的非合并提交:

git merge --squash

(或同一名称为 ABCD <-- HEAD / Y--Z <-- upstream/master / ...--o--o--o--W--o--o--o--X <-- upstream/dev \ \ \ A--B-----M--C--D--N <-- yourbranch 的{​​{1}}附件的图形,现在已指向HEAD。)

最后一步是将名称temp从提交ABCD中取消(yoink?),并使其指向新的提交yourbranch,在此位置N或{ {1}}进入。这两者之间的主要区别是之后是否将ABCD附加到git branch -f上:

git checkout -B

或:

HEAD

(在yourbranch ABCD <-- HEAD, yourbranch / Y--Z <-- upstream/master / ...--o--o--o--W--o--o--o--X <-- upstream/dev \ \ \ A--B-----M--C--D--N [abandoned] 的刷新记录中,还有一些小的差异,但是我们这里并未真正涉及刷新记录。)

(我不会讨论 ABCD <-- yourbranch (HEAD) / Y--Z <-- upstream/master / ...--o--o--o--W--o--o--o--X <-- upstream/dev \ \ \ A--B-----M--C--D--N [abandoned] 的工作方式,因为它已经相当长了。)