git 合并远程分支后重新设置分支

git rebase a branch when it has merged a remote branch

我对 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 中所做的所有更改,就好像我是重新实现它们。

我试过: - -

我知道我可以尝试 merge --squash(例如,),但这会创建一个单独的分支。

为了清楚起见,此处的示例提交已简化,实际分支包含约 250 个提交,使用 rebase 时,它​​显示约 300,000 个提交,这是有道理的,因为它是在超过 2 年的活跃存储库。

关于如何最好地将此分支重新设置为单个提交有什么建议吗?

TL;DR

你几乎可以肯定想要git merge --squash,但是用一个分离的HEAD然后branch-movement操作完成,例如:

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

(但请参阅下面的长答案)。

I know I can try merge --squash (e.g., ), but that creates a separate branch.

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

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

好吧,一方面,每个提交都存储了一组其他早期提交的哈希 ID。这些是这个特定提交的 parent 提交。大多数提交只存储一个 parent 哈希 ID。

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

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

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

这是 分支名称 的用武之地。分支名称只包含 last 提交的哈希 ID,我们想说的是"on the branch"。因此,如果 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 上的最后一次提交。

让我们删除 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 的 parent 是 H,因此 I 指向 H:

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

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

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

这给我们带来了变基

git rebase所做的是复制一些提交集,然后移动分支名称。例如,考虑这张图:

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

你首先做一个 git checkout 你想要的名字 move-after-copying:

$ 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'的parent不是G而是H。 commit-copy J':

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

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

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

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

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


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


为什么 rebase 复制太多

一个合并提交有两个(或更多)parent。当我们 Git 遵循 backwards-pointing link 从提交到他们的 parent 时,它通常遵循 all [=416] =]秒。因此,从合并提交中,Git 下降 both 路径。

您的真实提交图一点也不简单,但是您的 question-example 提交图可能还不错。它可能看起来像这样:

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

这里,"on"(可从)yourbranch 无法从 upstream/dev 访问的六个提交,即提交 X,是 A-B-M-C-D-N.让我们更仔细地看一下:

  • 如果我们从提交 X 开始并向后工作,我们将找到的提交都是 WX 之间的所有未字母提交,加上 W,加上W.

  • 前所有不带字母的
  • 如果我们从 yourbranch 开始——提交 N——并向后工作,我们访问提交 X(通过 link 来自 N) 提交 D(通过 N 中的 link)。从 D 我们到达 C,然后到 M,然后到 both B 和一些未命名的提交。我们也从 X 得到那个未命名的提交。

  • 如果我们从 origin/master 开始,或者提交 Z,我们将访问 Z,然后是 Y,然后是 W,然后是 W.

  • 之前的所有未命名提交

所以如果我们 运行 git rebase 像这样:

git checkout yourbranch
git rebase upstream/master

您的 Git 将列出所有可从 N 访问的提交,这些提交 not 可从 Z 访问。您使用 upstream/master 作为目标 (--onto) 和上游 ("don't copy commits reachable from Z")。这意味着 Git 不会 复制提交 W 和更早的提交——它们可以从 Z 访问——但是 也复制 o-o-o-X 提交 A-B-C-D。 Rebase 通常会丢弃所有合并提交所以它会抛出 MN,但是你只剩下八个提交被复制而不是四个。

您可以使 rebase 复制更少的提交

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

git rebase --onto upstream/master upstream/dev

这将不复制 参数与放置副本 参数的位置分开。我们仍然告诉 rebase:将副本放在提交 Z 之后,但是这次,我们告诉 rebase:*不要复制可从 X 访问的提交。所以 Git 列出提交 A-B-M-C-D-N 作为要复制的提交,然后扔掉 MN 因为它们是合并的,剩下的就是复制 [=148] =].

如果这个 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]

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

您想要一次提交

I want to [end up with] a single commit.

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

                     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

第一个 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 使用您想要的内容构建一个新的 non-merge 提交:

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

(或名称 temp 附有 HEAD 的同一绘图,现在已移至指向 ABCD)。

最后一步是从提交 N 中提取(yoink?)名称 yourbranch 并使其指向新提交 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]

(在 HEADyourbranch 的 reflog 中最终出现的内容存在一些其他细微差别,但我们并未真正涵盖此处的 reflogs)。

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