上次提交更改时Git rebase

时间:2017-07-06 09:10:23

标签: git

我经常发现自己正在处理不同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-namegit cherry-pick later-branch。这是一种痛苦。任何人都可以提出更好的方法来解决这个问题吗?

2 个答案:

答案 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
  • 使用" fork-point"代码(自Git 2.0版以来在Git中)。

要使用后者,您可以在git rebase --fork-point earlier-branch上运行later-branch

(您可以将earlier-branch设置为later-branch上游 - 大概只是暂时的,在变基期间 - 然后运行git rebaselater-branch时。原因是--fork-point在使用自动上游模式时是默认,但在使用显式时必须显式请求 <upstream>的{​​{1}}参数。)

不幸的是,最后一个看起来特别神奇,特别是那些刚接触Git的人。幸运的是,您的图表中包含了理解它的种子 - git rebase

定义fork-point

让我们把你在上面绘制的东西转向侧面,然后再转动一点。这给了我一些绘制分支名称的空间。我将使用圆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 - 它在后一种情况下,实际上会同时找到A1A1,但随后会将其下移到两个分支上的A2;另请参阅Git rebase - commit select in fork-point mode)。然后它为您运行A1,就好像您已手动运行:

git rebase --onto

让我们了解git rebase --onto earlier-branch hash-of-A1 参数的工作原理。

常规rebase,没有--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排除了BC - 无论哪个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

(注意三个点)。我没有时间在这里详细介绍。