git rebase 合并远程分支后的分支

Posted

技术标签:

【中文标题】git rebase 合并远程分支后的分支【英文标题】:git rebase a branch when it has merged a remote branch 【发布时间】:2020-04-04 22:23:16 【问题描述】:

我在 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://***.com/a/5201642 - https://***.com/a/5190323

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

为了清楚起见,此处的示例提交进行了简化,实际分支包含约 250 次提交,当使用 rebase 时,它​​显示约 300,000 次提交,这是有道理的,因为它是在活动存储库上实现的一项重要功能,而不是2年以上的课程。

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

【问题讨论】:

【参考方案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://***.com/a/5309051/947889),但这会创建一个单独的分支。

使用git merge --squash 不会创建一个单独的分支(它只是创建一个提交)。但如果这样做也没关系,因为 Git 的分支基本上没有意义:您可以随时以您喜欢的方式更改或重新排列您的分支名称。在 Git 中重要的不是分支——或者更准确地说,分支名称——而是提交Git 存储库是提交的集合,以及一些辅助信息。可以更改辅助信息。提交不能。 分支名称是这个可变辅助信息的一部分。

每个提交都有自己独特的大丑哈希 ID。这些哈希 ID 是提交的真实名称。每个提交都是完全的,完全只读的。您不能更改任何现有的提交。提交的哈希 ID 表示 that 提交,没有其他提交。但是这些哈希 ID 的问题是它们看起来是完全随机的。您将如何找到正确的哈希 ID?

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

当一个提交存储其他一些较早提交的哈希 ID 时,我们说后来的提交指向较早的提交。 (请注意,没有提交可以存储 later 提交的哈希 ID,因为在创建较早的提交时,该稍后提交的哈希 ID 尚不存在,并且一旦创建,就没有提交可以永远更改。)因此,当您有一个又一个创建的一长串提交时,每一个都将 backwards 指向上一个提交。如果我们把它画出来——使用大写字母代表真正的提交哈希 ID——我们会得到一张看起来像这样的图片:

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

这里H最新 提交,带有一些哈希ID H。实际提交本身,现在一直被冻结,包含先前提交 G 的原始哈希 ID。提交G 包含早期提交F 的原始哈希ID,而后者又包含另一个早期提交哈希ID,依此类推。

这是 branch names 的用武之地。分支名称仅包含 last 提交的哈希 ID,我们想说的是“在分支上”。因此,如果H 是某个分支上的last 提交,我们只需将其哈希 ID 放入分支名称中。这个名字现在指向提交H,就像H指向G

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

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

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

我们必须绘制提交图——F-G-H 线——有点不同以挤入名称,但 提交 在此操作中完全没有改变。我们所做的只是创建一个指向F 的新名称,以便F 是分支base 上的最后一次提交。名称branch1 仍然标识H,因此H 是分支branch1 上的最后一次提交。

让我们删除base (git branch -d base),并创建一个指向H 的新名称feature。我们也会确保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 操作以更新名称。

您在 Git 中所做的几乎所有事情都是关于创建或获取提交,然后使各种名称指向这些新创建或获得的提交中的特定名称。分支名称只是让您找到一些特定的提交。根据定义,名称是该分支上的 last 提交。如何移动分支名称并不重要。只要该名称存在,它就指向某个提交。该提交是分支上的最后一次提交。移动名称,您已经更改了哪个提交是分支上的最后一个提交。您没有更改任何 commits — 它们都还在那里 — 您只是更改了 which 提交是 该分支的最后一个提交 .

这让我们变基

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,而是H。提交副本J' 也是如此:

             I'-J'  <-- HEAD (detached)
            /
...--F--G--H   <-- master
         \
          I--J   <-- feature

(您在此图中看不到的区别在于,使用提交I' 保存的快照可能与使用I 保存的快照不同,因为cherry-pick 有效地接受了更改从GI 并将该更改应用于H 中的快照,而不是G。)

复制这两个提交后,通过 移动分支名称来完成 rebase:

             I'-J'  <-- feature (HEAD)
            /
...--F--G--H   <-- master
         \
          I--J   [abandoned]

提交IJ 会发生什么?答案有点复杂,但我们现在需要的是:什么都没有。 Git 将它们保留一段时间,以防你认为 rebase 是一个坏主意。但他们已经变得很难找到。新提交 J' 很容易找到:名称 feature 找到它。提交I' 很容易找到:我们只需转到J',然后按照它的向后箭头指向I'。但是提交J 的哈希ID 是什么?它曾经在名称feature 中,但现在不再是了。如果你能找到J,你可以用它来找到I,但除非你把J的哈希ID保存在某个地方,否则这可能有点棘手。1最终——通常从现在起 30 天后的某个时间——如果你没有使用其他名称(例如其他分支或标签名称),Git 会完全回收它们,以确保它们存在。


1现在,它真的很容易找到:Git 将它保存在名称 ORIG_HEAD 下。但其他命令将替换ORIG_HEAD 的哈希ID。不过,还有第二种方法可以找到它,使用 git reflog,这就是默认情况下让提交至少再持续一个月左右的原因。


为什么变基复制太多

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

您的真实提交图一点也不简单,但您的问题示例提交图可能还不错。它可能看起来像这样:

                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。让我们更仔细地看一下:

1234563 987654422@. 1234563 (通过N 的链接)。从DC,然后到M,然后到both B 和一些未命名的提交。我们也从X 获得了那个未命名的提交。 1234563 /p>

如果我们像这样运行git rebase

git checkout yourbranch
git rebase upstream/master

您的 Git 将列出所有可从 N 访问的提交,而 可从 Z 访问。您将upstream/master 用作目标(--onto)和上游(“不要复制可从Z 访问的提交”)。这意味着 Git 不会复制 W 和更早的提交——它们可以从 Z 访问——但 复制 o-o-o-X 提交以及 A-B-C-D . Rebase 通常会丢弃所有合并提交,因此它会同时丢弃 MN,但你只剩下 8 个提交被复制而不是 4 个。

您可以减少 rebase 复制的提交次数

你可以做的一件事就是运行:

git rebase --onto upstream/master upstream/dev

这将 什么不要复制 参数与 将副本放在哪里 参数分开。我们仍然告诉 rebase:将副本放在提交 Z 之后,但是这一次,我们告诉 rebase:*不要复制来自 X 的提交。所以 Git 列出了提交 A-B-M-C-D-N 作为要复制的提交,然后抛出 MN 因为它们是合并的,剩下的就是复制 A-B-C-D 的工作。

如果这个 rebase 一切顺利,你会得到这个:

                     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 是一个单一的提交,如果你第一次 rebase,然后进行第二次 rebase 以将它们全部压缩为一个提交,就会产生效果。

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

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

第一个git checkout 为您提供一个分离的HEAD,指向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

(或将HEAD 附加到名称temp 的同一绘图,现在已移动到指向ABCD)。

最后一步是将名称 yourbranch 从提交 N 拉出(yoink?)并使其指向新提交 ABCD,这是 git branch -fgit 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]

(在 HEADyourbranch 的 reflog 中的最终结果方面存在一些细微的其他差异,但我们在此并未真正介绍 reflog)。

(我不会讨论 git merge --squash 是如何工作的,因为这已经很长了。)

【讨论】:

以上是关于git rebase 合并远程分支后的分支的主要内容,如果未能解决你的问题,请参考以下文章

git rebase 不使用分支

Git----拉取远程分支,git pull,git rebase,git pull --rebase的区别

Git----拉取远程分支,git pull,git rebase,git pull --rebase的区别

git merge 和 git rebase 小结(转)

git rebase

git rebase简介 基本篇