我经常发现自己正在处理不同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
提交应用于最新版本的{{1} }}
我最终做的是earlier-branch
,然后是git checkout earlier-branch -b new-later-branch-name
和git cherry-pick later-branch
。这是一种痛苦。任何人都可以提出更好的方法来解决这个问题吗?
答案 0 :(得分:2)
我认为有两种简单的方法可以做到这一点。
第一个,最好的选择是在交互模式下使用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
...
这会在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
提前一个。如果分支名称对您很重要,您可以相应地重命名和重置它们。
答案 1 :(得分:1)
除了交互式rebase(如在houtanb's answer中)之外,还有两种方法可以做到这一点,或更多,更自动:
git rebase --onto
或要使用后者,您可以在git rebase --fork-point earlier-branch
上运行later-branch
。
(您可以将earlier-branch
设置为later-branch
的上游 - 大概只是暂时的,在变基期间 - 然后运行git rebase
在later-branch
时。原因是--fork-point
在使用自动上游模式时是默认,但在使用显式时必须显式请求 <upstream>
的{{1}}参数。)
不幸的是,最后一个看起来特别神奇,特别是那些刚接触Git的人。幸运的是,您的图表中包含了理解它的种子 - git rebase
。
让我们把你在上面绘制的东西转向侧面,然后再转动一点。这给了我一些绘制分支名称的空间。我将使用圆git rebase --onto
个节点或大写字母和数字替换每个提交的*
。我将第三个提交o
添加到后一个分支中,仅用于说明。
C
现在,无论出于何种原因,您都被迫将提交:
.
\
o
\
A1 <-- earlier-branch
\
B--C <-- later-branch
复制到新提交A1
,并移动分支标签A2
以指向新副本:
让我们把你在上面绘制的东西转向侧面,然后再转动一点。这给了我一些绘制分支名称的空间。我将使用圆earlier-branch
个节点或大写字母替换每个提交的*
。
o
如果只有Git会记住提交:
.
\
o--A2 <-- earlier-branch
\
A1
\
B--C <-- later-branch
,因为A1
用于包含提交earlier-branch
,我们可以告诉Git:&#34;复制时A1
,删除现在仍在其中的所有提交,但仅仅因为later-branch
&#34;而已经在其上。
但Git 确实记住这个,至少30天,默认情况下。 Git有 reflogs -logs以前存储在每个引用中的内容(包括常规分支和Git所谓的远程跟踪分支)。如果我们将reflog信息添加到图形中,它看起来像这样:
earlier-branch
事实上,如果由于某种原因你必须将:
.
\
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- later-branch
复制到A2
,那么该图只会增加另一个reflog条目,重新编号现有的:
A3
fork-point代码的作用是扫描 reflog 以获取其他引用,例如:
. A3 <-- earlier-branch
\ /
o--A2 <-- [earlier-branch@{1}]
\
A1 <-- [earlier-branch@{2}]
\
B--C <-- later-branch
,并查找这些提交(在本例中为earlier-branch
- 它在后一种情况下,实际上会同时找到A1
和A1
,但随后会将其下移到两个分支上的A2
;另请参阅Git rebase - commit select in fork-point mode)。然后它为您运行A1
,就好像您已手动运行:
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
。 (作为一个奇怪的副作用,这也是 - 因为Git版本2.0无论如何 - 自动启用或禁用<upstream>
选项,要求你使用明确的--fork-point
或--no-fork-point
如果你想要另一个模式。)
在任何情况下,如果您没有为两个指定一个,则Git会自动选择--fork-point
。一种是限制将被复制的提交集:Git将考虑通过运行来复制列出的提交集:
<upstream>
要以更友好的方式查看它们,请使用git rev-list <upstream>..HEAD
或我首选的方法git log
,而不是git log --oneline --decorate --graph
:
git rev-list
理想情况下,我们会在此处看到提交git log --oneline --decorate --graph earlier-branch..HEAD
和B
,首先列出C
(Git必须使用C
才能确保复制--reverse
第一)。如果您将B
复制到A1
和/或复制到A2
,并移动了分支A3
,我们会看到所有earlier-branch
,{{ 1}}和A1
。 (Git排除了B
或C
- 无论哪个A2
指向 - 但他们仍然不在列表中。然后使用排除的A3
或{{1在 earlier-branch
之前排除提交,这就是为什么我们不能看到这些提示。)
此A2
分支名称(或提交哈希)的另一个目的是选择副本的位置。当我们复制一个或多个提交时,每个复制的提交必须在一些现有提交之后进行。 A3
参数提供将作为我们复制的第一个提交的父级的提交ID。
因此,运行A1
会使Git列表按此顺序提交<upstream>
,<upstream>
和git rebase earlier-branch
。然后使用&#34;分离的HEAD&#34;模式复制A1
以追溯B
:
C
然后复制A1
以追踪earlier-branch
:
:
. A1' <-- HEAD
\ /
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- later-branch
Rebase然后将B
复制到A1'
并将分支标签:
. A1'--B' <-- HEAD
\ /
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- later-branch
移动到C
结束的地方,在此过程中重新附加您的HEAD:
C'
later-branch
参数可让您告诉Git 副本的位置。
HEAD
与:
. A1'--B'--C' <-- later-branch (HEAD)
\ /
o--A2 <-- earlier-branch
\
A1 <-- [earlier-branch@{1}]
\
B--C <-- [later-branch@{1}]
当您添加--onto
时,您告诉Git rebase将副本放在何处。这将释放--onto
参数,以便它现在仅指定要复制的 not !所以现在你可以自由地告诉Git:&#34;在提交git rebase
&#34;之后复制的所有内容。写作:
--onto
Git做了它常用的事情,列出了要复制的提交(<upstream>
和A1
),从git rebase --onto earlier-branch <hash-of-A1>
分离你的HEAD,一次复制一个提交副本在B
的提示之后,最后移动名称C
以重新连接您的头部。
这正是我们想要的,都是半自动完成的:我们告诉Git 不复制later-branch
本身,以便它只复制earlier-branch
和{{1 }}
当我们指定上游时,如later-branch
中那样,Git 禁用叉点模式。如果我们明确启用它,Git将浏览A1
reflog。只要提交B
的reflog条目尚未过期,Git就会发现C
曾经git rebase earlier-branch
,并会使用earlier-branch
让我们将其丢弃复制清单。
请注意,这里存在一些危险。 如果我们真的想要 A1
怎么办,例如什么如果我们仅仅因为我们意识到A1
不属于另一个分支而支持earlier-branch
超过--onto
? Git仍然认为我们将其复制到其他一些提交中,并且现在不想复制它,并且会抛弃列表。幸运的是,你总是可以撤消一个rebase:rebase根本没有丢弃任何东西,它只是拷贝。然后它会更新一个分支,它将以前的值保存在分支的reflog中。但是通过reflogs钓鱼,尝试找到一组特定的提交,在一个完全相同的提交迷宫中,并不是很有趣 - 所以在运行rebase之前想一点是明智的,有或没有{{ 1}}。
在少数(罕见)情况下,Git你不必做任何事情(没有叉点模式,没有手动A1
分离,没有earlier-branch
)。具体来说,如果补丁本身根本没有改变,但只有提交消息中的措辞发生了变化,Git将检测已经复制的提交并跳过它。这是因为A1
实际上使用A1
的{em>对称差异模式和--fork-point
选项。也就是说,而不是:
--onto
Git实际上运行:
--interactive
(注意三个点)。我没有时间在这里详细介绍。