我对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年以上。
关于如何最好地将该分支重新构建为单个提交的任何建议?
答案 0 :(得分:1)
您几乎可以肯定地要做要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
上的最后一次提交。
我们删除base
(git 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
命令。它带有一些参数,文档将其称为--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
而是{{1} }。提交副本H
也是这样:
J'
(您在该图中看不到的区别是,通过提交 I'-J' <-- HEAD (detached)
/
...--F--G--H <-- master
\
I--J <-- feature
保存的快照可能与通过I'
保存的快照有所不同,因为cherry-pick有效地将更改从I
更改为G
,并将更改应用于I
中的快照,而不是H
。)
已经复制了这两个提交,并通过移动分支名称完成了变基:
G
提交 1 目前,真的很容易找到:Git将其保存为 一个合并提交有两个(或更多)父母。当我们让Git遵循从提交到其父母的向后指向链接时,通常遵循 all 链接。因此,从合并提交开始,Git沿两个路径前进。 您真正的提交图根本不简单,但是您的问题示例提交图可能还不错。可能看起来像这样: 在这里, 如果我们从提交 如果我们从 如果我们从 因此,如果我们像这样运行 您的Git将从 您可以执行以下操作: 这会将不复制内容参数与复制副本的位置分开。我们仍然告诉rebase:在提交 如果此重新部署一切顺利,您将得到以下帮助: 您现在可以从中创建拉取请求。 我想[结束]一次提交。 也就是说,如果我们绘制所需的结果,它可能看起来像这样: 其中 要到达那里,可以使用以下命令序列: 第一个 这给您: 或: (或同一名称为 最后一步是将名称 或: (在 (我不会讨论 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>
J
。但是其他命令将替换ORIG_HEAD
的哈希ID。不过,还有第二种方法可以使用ORIG_HEAD
来找到它,这就是默认情况下将提交至少保留一个月左右的原因。
为什么要重新设置副本太多
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
开始并向后工作,我们将发现的提交都是X
和W
之间的所有未标记的提交,再加上X
,以及W
之前的所有未标记字符。W
开始-提交yourbranch
-向后工作,我们将访问commit N
(通过X
的链接) 提交N
(通过D
的链接)。从N
到D
,然后到C
,然后到两者 M
和一些未命名的提交。我们也从B
到达了未命名的提交。X
开始或提交origin/master
,我们将访问Z
,然后依次访问Z
,然后访问Y
和W
之前的所有未命名的提交。W
:git rebase
git checkout yourbranch
git rebase upstream/master
列出所有无法通过N
到达的提交。您将Z
用作目标(upstream/master
)和上游(“不要复制从--onto
可到达的提交”)。这意味着Git 不会复制Z
或更早的提交(它们可以从W
到达,但是会复制Z
也提交o-o-o-X
。 Rebase通常会丢弃所有合并提交,因此它将同时丢掉A-B-C-D
和M
,但是剩下的是复制了八份提交,而不是四份。您可以使基准副本的提交次数减少
N
git rebase --onto upstream/master upstream/dev
之后放入副本,但是这次,我们告诉rebase:*不要复制Z
可以到达的提交。因此,Git将提交X
列为要复制的提交,然后将A-B-M-C-D-N
和M
扔掉,因为它们是合并的,剩下的工作就是复制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]
的工作方式,因为它已经相当长了。)