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 <hash-of-F>
):
...--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
命令。它需要一些参数,文档称之为--onto
和upstream
参数。这些指定了一个 target 提交,这是副本应该去的地方,以及哪些提交应该被复制:
$ git rebase master
你可以只给出一个参数,就像这里一样——git rebase master
——在这种情况下,目标提交和提交集都可以使用那个名称找到。这里,目标提交是提交 H
,要复制的提交集是提交 I
和 J
。
rebase 命令现在复制每个提交,就像使用 git cherry-pick
一样。副本获得新的哈希 ID。这里有很多棘手的极端情况,您可以使用git rebase
的选项,但在这种情况下,它很简单,我们最终会得到一个I
的副本,它有一个新的和不同的哈希ID,它我们将调用I'
,以及J
的副本,我们将调用J'
。 I
和 I'
之间有两个很大的区别,我们在图中可以看到,I'
的父级不是G
,而是H
。提交副本J'
也是如此:
I'-J' <-- HEAD (detached)
/
...--F--G--H <-- master
\
I--J <-- feature
(您在此图中看不到的区别在于,使用提交I'
保存的快照可能与使用I
保存的快照不同,因为cherry-pick 有效地接受了更改从G
到I
并将该更改应用于H
中的快照,而不是G
。)
复制这两个提交后,通过 移动分支名称来完成 rebase:
I'-J' <-- feature (HEAD)
/
...--F--G--H <-- master
\
I--J [abandoned]
提交I
和J
会发生什么?答案有点复杂,但我们现在需要的是:什么都没有。 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
。让我们更仔细地看一下:
N
的链接)。从D
到C
,然后到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 通常会丢弃所有合并提交,因此它会同时丢弃 M
和 N
,但你只剩下 8 个提交被复制而不是 4 个。
您可以减少 rebase 复制的提交次数
你可以做的一件事就是运行:
git rebase --onto upstream/master upstream/dev
这将 什么不要复制 参数与 将副本放在哪里 参数分开。我们仍然告诉 rebase:将副本放在提交 Z
之后,但是这一次,我们告诉 rebase:*不要复制来自 X
的提交。所以 Git 列出了提交 A-B-M-C-D-N
作为要复制的提交,然后抛出 M
和 N
因为它们是合并的,剩下的就是复制 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 -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]
(在 HEAD
和 yourbranch
的 reflog 中的最终结果方面存在一些细微的其他差异,但我们在此并未真正介绍 reflog)。
(我不会讨论 git merge --squash
是如何工作的,因为这已经很长了。)
【讨论】:
以上是关于git rebase 合并远程分支后的分支的主要内容,如果未能解决你的问题,请参考以下文章
Git----拉取远程分支,git pull,git rebase,git pull --rebase的区别