[英]Git rebase when previous commit changed
我经常发现自己正在处理不同git分支上的两个不同的工作票,但是一个依赖于另一个,如下所示:
* later-branch
|
* earlier-branch
|
* some prior commit
|
(每个都是一个提交,因为我们使用的是gerrit,但是这个问题也可能适用于每个提交的多个提交。)早期的分支可能正在进行审核,因此我可能必须返回并在某个时候修改它使用git commit --amend
。 必须发生的事情,这将分叉历史:
* earlier-branch
|
| * later-branch
| |
| * previous version of earlier-branch
| /
* some prior commit
|
在这一点上,我要变基的later-branch
上的新版本之上earlier-branch
。 但是,如果我只是做一个git checkout later-branch
随后是git rebase earlier-branch
,它总是得到冲突,因为(我觉得)它必须先申请的previous version of earlier-branch
提交到最新版本的earlier-branch
。
我最终做的是git checkout earlier-branch -b new-later-branch-name
然后是git cherry-pick later-branch
和git br -D later-branch
。 这是一种痛苦。 谁能建议一个更好的方法来处理这个?
我看到两种简单的方法来做到这一点。
第一个,最好的选择是在交互模式下使用git rebase
。 要做到这一点,你会这样做
git checkout later-branch
git rebase -i earlier-branch
在弹出的屏幕中,您将选择drop
previous version of earlier-branch
:
drop efb1c19 previous version of earlier-branch
pick a25ba16 later-branch
# Rebase 65f3afc..a25ba16 onto 65f3afc (2 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
...
这将在earlier-branch
later-branch
的顶部重新earlier-branch
,提供以下树:
* later-branch
|
* earlier-branch
|
| * previous version of earlier-branch
| /
* some prior commit
|
另一种选择是简单地做一个git cherry-pick
。 如果你这样做:
git checkout earlier-branch
git cherry-pick later-branch
你会得到以下树:
* earlier-branch -> cherry-picked commit 1
|
* earlier-branch -> amended commit 0, now commit 2
|
| * later-branch -> commit 1
| |
| * previous version of earlier-branch -> commit 0
| /
* some prior commit
|
因此,实际上,这将产生您想要的结果,但它将提前earlier-branch
。 如果分支名称对您很重要,您可以相应地重命名和重置它们。
除了交互式rebase(如houtanb的回答 ),还有两种方法可以更自然地执行此操作:
git rebase --onto
,或 要使用后者,可以在later-branch
上运行git rebase --fork-point earlier-branch
later-branch
。
(你可以改为将early earlier-branch
设置为later-branch
earlier-branch
的上游 later-branch
可能只是暂时的,在git rebase
期间 - 然后在later-branch
时运行git rebase
。原因是--fork-point
是使用自动上游模式时的默认值 ,但在使用git rebase
的显式<upstream>
参数时必须显式请求 。)
不幸的是,最后一个看起来特别神奇,特别是那些刚接触Git的人。 幸运的是,你的图表中有理解它的种子 - 以及git rebase --onto
。
让我们把你上面绘制的东西转向侧面,然后再转动一点。 这给了我一些绘制分支名称的空间。 我会用圆o
节点或大写字母和数字替换每次提交的*
s。 我将向后一个分支添加第三个提交C
,以便进行说明。
:
.
\
o
\
A1 <-- earlier-branch
\
B--C <-- later-branch
现在,无论出于何种原因,您都被迫将提交A1
复制到新提交A2
,并将分支标签earlier-branch
移动到指向新副本:
让我们把你上面绘制的东西转向侧面,然后再转动一点。 这给了我一些绘制分支名称的空间。 我将使用圆形o
节点或大写字母替换每次提交的*
s。
:
.
\
o--A2 <-- earlier-branch
\
A1
\
B--C <-- later-branch
如果只有Git会记住提交A1
存在,因为earlier-branch
用于包含提交A1
,我们可以告诉Git:“当复制later-branch
,删除现在仍在其上的任何提交,但过去仅仅是因为earlier-branch
“。
但Git 确实记得这个,至少30天,默认情况下。 Git具有reflogs -logs,用于存储在每个引用中 (包括常规分支和Git的所谓远程跟踪分支 )。 如果我们将reflog信息添加到绘图中,它看起来像这样:
:
.
\
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- later-branch
实际上,如果由于某种原因你必须将A2
复制到A3
,那么该图只会增加另一个reflog条目,重新编号现有的:
:
. A3 <-- earlier-branch
\ /
o--A2 <-- [earlier-branch@{1}]
\
A1 <-- [earlier-branch@{2}]
\
B--C <-- later-branch
fork-point代码的作用是扫描reflog以获取其他引用,例如early earlier-branch
,并找到这些提交(在这种情况下, A1
-it实际上找到A1
和A2
,在后一种情况下,但是然后winnows它下到两个分支上的A1
;另见Git rebase - 在fork-point模式下提交select 。 然后它为你运行git rebase --onto
,就像你手动运行一样:
git rebase --onto earlier-branch hash-of-A1
这让我们了解了--onto
参数的工作原理。
--onto
通常,您可以使用一个参数运行git rebase
,如git rebase branch-name
,甚至根本没有参数。 根本没有参数, git rebase
使用当前分支的上游设置。 使用branch-name
参数, git rebase
调用该参数<upstream>
。 (作为一个奇怪的副作用,这也是 - 因为Git版本2.0无论如何 - 自动启用或禁用--fork-point
选项,要求你使用显式--no-fork-point
或--fork-point
如果你想另一种模式。)
在任何情况下,如果你没有指定一个用于两个目的,Git会自动使用<upstream>
- 选择。 一种是限制将被复制的提交集:Git将考虑通过运行复制列出的提交集:
git rev-list <upstream>..HEAD
要以更友好的方式查看它们,请使用git log
或我首选的方法git log --oneline --decorate --graph
,而不是git rev-list
here:
git log --oneline --decorate --graph earlier-branch..HEAD
理想情况下,我们会在这里看到提交B
和C
,首先列出C
(Git必须使用--reverse
以确保它首先复制B
)。 但是,如果您将A1
复制到A2
和/或复制到A3
,并移动了分支earlier-branch
,我们将看到所有A1
, B
和C
(Git不包括A2
或A3
以earlier-branch
点为准 - 但它们无论如何都不在列表中。然后使用排除的A2
或A3
排除A1
之前的提交,这就是为什么我们看不到这些。)
此<upstream>
分支名称(或提交哈希)的另一个目的是选择副本的位置 。 当我们复制一个或多个提交时,每个复制的提交必须在一些现有提交之后进行。 <upstream>
参数提供将作为我们复制的第一个提交的父级的提交的ID。
因此,运行git rebase earlier-branch
会使Git列表按顺序提交A1
, B
和C
然后使用“分离的HEAD”模式 - 将A1
复制到earlier-branch
:
:
. A1' <-- HEAD
\ /
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- later-branch
然后将B
复制到A1'
:
:
. A1'--B' <-- HEAD
\ /
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- later-branch
调整基线然后复制C
到C'
和移动分支标签, later-branch
,到哪里HEAD
卷起,再附上你的头在这个过程中:
:
. A1'--B'--C' <-- later-branch (HEAD)
\ /
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- [later-branch@{1}]
--onto
参数让你告诉Git 副本去哪里 。
--onto
与git rebase
当您添加--onto
,您告诉Git rebase将副本放在何处。 这释放了<upstream>
参数,现在它只指定不要复制的内容! 所以现在你可以自由地告诉Git:“通过写入来复制提交A1
之后的所有内容”:
git rebase --onto earlier-branch <hash-of-A1>
Git做了它常用的事情,列出要复制的提交( B
和C
),从later-branch
分离你的HEAD,一次复制一个提交,副本跟在earlier-branch
的尖端之后,最后移动名字later-branch
重新连接你的HEAD。
这正是我们想要的,都是半自动完成的:我们告诉Git 不要复制A1
本身,所以它只复制B
和C
当我们指定上游时,就像在git rebase earlier-branch
,Git 禁用 fork-point模式。 如果我们明确启用它,Git将通过earlier-branch
reflog。 只要提交A1
的reflog条目尚未到期,Git就会发现A1
曾经在earlier-branch
并且将使用--onto
为我们从to-copy列表中丢弃它。
请注意,这里有一点危险。 如果我们真的想要 A1
会怎么样呢,例如,如果我们支持earlier-branch
到A1
只是因为我们意识到A1
不属于另一个分支? Git仍然认为我们将它复制到其他一些提交中,并且现在不想复制它,并且会抛弃列表。 幸运的是,你总是可以撤消一个rebase:rebase根本不丢弃任何东西,它只是复制 。 然后它会更新一个分支,它将以前的值保存在分支的reflog中。 但是通过reflogs钓鱼,试图找到一组特定的提交,在一个完全相同的提交迷宫中,并不是很有趣 - 所以在运行rebase之前考虑一下是否明智--fork-point
无论有没有--fork-point
。
在一些(罕见)情况下,Git的你不必做任何事情(无分叉点模式,无需手动--onto
分离,没有--interactive
)。 具体来说,如果补丁本身根本没有改变,但只有提交消息中的措辞发生了变化,Git将检测已经复制的提交并跳过它。 这是因为git rebase
实际上使用git rev-list
的对称差异模式和--cherry-pick --right-only --no-merges
选项。 也就是说,而不是:
git rev-list <upstream>..HEAD
Git实际上运行:
git rev-list --cherry-pick --right-only --no-merges <upstream>...HEAD
(注意三个点)。 不过,我没时间在这里详细介绍。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.