将现有的已提交工作移动到新的本地分支

  • 本文关键字:移动 分支 工作 提交 git
  • 更新时间 :
  • 英文 :


我有一个分支(分支-B),我正在研究它最初是从另一个分支(分支-A)分支而来的。

不幸的是,一些秘密在分支A中暴露出来。我需要有效地从分支 B 中"提升"所有与 master 不同的文件,以便我可以将它们存储并将它们带到一个新的干净分支。

我很确定这已经被问过并回答了,但我无法快速找到一个好的答案,所以我只写一个。 请注意,这与使用 Git 将最近的提交移动到新分支不同。 你只需要git rebase --onto. 然而,关于这一点有很多需要了解的。

我认为,绘制一幅"之前"和"之后"结果的图片是有帮助的——即使是简化的。 这是你现在拥有的,按照我喜欢的方式绘制:

...--E--F   <-- main

G--H   <-- branch-A

I--J   <-- branch-B (HEAD)

也就是说,我们已经用一个大写字母表示了每个提交(Git 都是关于提交,由丑陋的大哈希 ID 标识)。 (Git 使用大的长十六进制数字——那些哈希 ID——因为宇宙中的每个提交都必须获得自己唯一的编号。 像我一样使用大写字母,我们会在 26 次左右提交后用完。 括号中的附加HEAD只是告诉我们签出了哪个分支(通过git checkoutgit switch)。

较新的提交显示在右侧。 每个提交都连接到(向后,尽管我已经绘制了没有正确箭头的箭头)到以前的提交。分支名称(如mainbranch-B)仅包含将被视为该分支一部分的最后一个提交的实际哈希 ID。

每个较早的提交(通过从最后一个开始并向后工作获得)都"在"该分支上,包括其他分支上的提交。 所以在这里,每个提交都在branch-B,但只有H的提交在branch-A,只有那些到F的提交在main上。 提交E在所有三个分支上;提交G在两个;并且提交I仅在branch-B.

您发现branch-Abranch-B上的某些提交都存在问题。您断言(或假设,或已检查)仅在branch-B提交的提交没有问题。 所以现在您希望以某种方式更改上面的图片,使其看起来更像这样:

I--J   <-- branch-B (HEAD)
/
...--E--F   <-- main

G--H   <-- branch-A

你实际上无法得到这个,但你可以得到一些几乎一样好甚至更好的东西,它看起来像这样:

I'-J'  <-- branch-B (HEAD)
/
...--E--F   <-- main

G--H   <-- branch-A

I--J   [abandoned]

提交I'J'是原始提交IJ的副本,有许多区别:

  • 提交I'向后连接以提交F,而不是提交H
  • 提交I'具有来自提交F的文件以及您在从提交H到提交I时所做的更改作为其快照。 也就是说,您进行了相同的更改,但基础不同。
  • 同样,提交J'向后连接到I'...
  • 。并且具有与从IJ相同的变化,但基础不同。

由于像这样"放弃"的 Git 提交(没有可以找到它们的分支名称)变得几乎不可见,因此对改进后的存储库的随意检查会显示相同数量的提交,这些提交似乎执行相同的操作,但现在您的分支上的提交不包含branch-A提交中犯的错误。 仔细观察会发现,这些仅在branch-B上的提交具有与原始提交不同的哈希 ID 号,这表明它们是副本。

有两种方法可以实现结果。 一个很慢,需要你做很多工作,一个让 Git 自己完成所有这些工作,这样它既快速又简单。 不过,最好先了解速方法,因为有时,任何给定提交的复制步骤都会出现故障。

方法1:git cherry-pick

让我们再次绘制起始设置:

...--E--F   <-- main

G--H   <-- branch-A

I--J   <-- branch-B (HEAD)

我们需要一个新的分支new-and-improved-B也许,指向提交F。 所以我们这样做:

git checkout -b new-and-improved-B main    # or git switch -c etc

这给了我们这个:

...--E--F   <-- main, new-and-improved-B (HEAD)

G--H   <-- branch-A

I--J   <-- branch-B

我们还使用git log来查找提交IJ的哈希 ID。 如果有更多的提交要复制,我们会找到所有的哈希 ID。 我们把它们保存在某个地方——也许是在一个文件中,或者把它们记在草稿纸上,或者其他什么。 我们确保此列表的顺序正确:首先提交I,然后提交J

现在我们准备开始复制了。 我们运行:

git cherry-pick <hash-of-I>

这告诉 Git 去查看提交I,找到它的父提交——H——并比较HI的内容。 然后,它应该应用从HI所需的任何更改,将它们添加到从H回退到F所需的任何更改中(即,从branch-A中删除坏东西)。

您可以将其视为将 H-vs-I 更改添加到 F,但这仅在您添加的内容与我们需要删除的内容之间没有冲突时才有效。在内部,樱桃采摘实际上是一种合并操作。 通常,没有任何合并冲突,并且这个"可以想到"的部分工作正常。 但是,如果您从此操作中遇到合并冲突,请记住,Git 将添加更改(从HI)与"撤消更改"(从H返回到F)相结合。

此外,因为HEAD通过new-and-improved-B选择提交F,Git 认为HF的变化是"我们的"变化,而HI的变化是"他们的"变化。 因此,当你去解决任何冲突时,请记住,在这一点上,Git 所谓的"他们的"实际上是你的工作。

如果一切顺利,Git 将自行进行新的提交,我们将调用I'以表明它是I的副本。 新提交I'的提交消息,Git只是从I复制。 (在运行git cherry-pick命令时,可以通过添加--edit来选择编辑或不编辑它。

I'  <-- new-and-improved-B (HEAD)
/
...--E--F   <-- main

G--H   <-- branch-A

I--J   <-- branch-B

如果事情进展顺利——如果你有合并冲突——Git 会停止并让你清理混乱。 有关如何执行此操作的详细信息,请参阅有关清理与git merge的合并冲突的任何说明:方法是相同的。 请记住,--theirs意味着您的提交I,而--ours意味着......好吧,无论谁提交F:Git 试图添加"撤消分支 A 的东西"来"做提交我的东西"。 最终,一旦你修复了混乱,你将运行git cherry-pick --continue,Git 将继续从你的修复分辨率进行提交I'

现在我们已经提交了I',我们对所有剩余的提交重复此操作。 如果只剩下一个提交 —J— 我们只有一个樱桃选择要运行;如果有很多,我们不管有多少,我们都会运行。 与I一样,这可能会产生合并冲突:也许IJ的更改不能很好地与"从I中回退所有分支A的东西"更改混合。

如果您确实有冲突,请记住,此时--theirs表示提交J(您正在复制的那个),--ours表示commit I'I'是你刚刚用之前的樱桃挑选做的那个。 您可能会再次解决相同的冲突。 这不是那么常见,但这是反复采摘樱桃的痛苦方面之一。

完成所有操作后,您将拥有每个提交的副本:

I'-J'  <-- new-and-improved-B (HEAD)
/
...--E--F   <-- main

G--H   <-- branch-A

I--J   <-- branch-B

你现在必须做最后几件事。 特别是,您需要说服 Git 将名称branch-B从提交J中删除,并使其指向提交J',然后您可能希望签出branch-B以将HEAD附加到那里并摆脱临时分支。

您可以使用一个命令执行前两部分:

git checkout -B branch-B     # or git switch -C branch-B

或者您可以使用git branch -f branch-B来移动branch-B,然后git checkoutgit switch切换到移动的分支。 然后,您只需要使用git branch -d new-and-improved-B删除临时分支即可。

当然,这是相当多的工作量。 如果 Git 能为我们做这件事就好了......事实上,Git为我们做到这一点。

使用git rebase --onto

git rebase命令正是为了给我们做这种重复的樱桃采摘而设计的。 它:

  • 列出要复制的提交;
  • 使用 Git 的分离 HEAD模式,而不是临时分支,进行复制,一次一个挑剔,就像我们上面显示的那样;
  • 之后,移动分支名称。

与樱桃拣选方法一样,每个挑拣都可能导致合并冲突。 如果是这样,Git 会在中间停止并让我们清理混乱。 然后我们运行git rebase --continue,让 Git 返回到剩余的樱桃选择和最终变基移动分支步骤。

这里的皱纹是rebase是专门为略有不同的场景而设计的。 如果我们有这个

...--o--o--o   <-- main

A--B--C--D   <-- feature (HEAD)

我们想要这个:

A'-B'-C'-D'  <-- feature (HEAD)
/
...--o--o--o   <-- main

A--B--C--D   [abandoned]

我们只需要做git checkout feature(如果需要 - 图纸显示它不是),然后git rebase main. rebase 命令通过确定哪些提交在我们的分支上而不在目标分支main上来确定要复制的提交。

但我们不希望这样。 我们有:

...--E--F   <-- main

G--H   <-- branch-A

I--J   <-- branch-B (HEAD)

因此,变基会假设我们要复制G-H-I-J提交,因为提交GH在我们的分支上。 他们只是也在另一个分支上。 所以我们必须告诉变基:嘿,不要复制G-H.

那么,我们必须做的是:

git checkout branch-B
git rebase --onto main branch-A

--onto告诉 Git:这就是副本的去向。 这将释放最后一个参数作为名称branch-Abranch-A选择提交H的名称。 所以现在我们告诉 Git:复制所有在分支 B 上但不在分支 A 上的提交。 将这些副本放在按名称main选择的提交之后。

因此,变基操作将列出提交IJ的提交哈希 ID 作为要复制的 ID。 然后它将使用分离的 HEAD 技巧创建一个未命名的分支,该分支以名称main选择的提交结束(在此处使用适合您情况的任何名称)。 然后,它将对之前列出的每个提交进行逐个复制,最后,它将拉动我们所在的分支名称(branch-B)以指向最后一个复制的提交。

杂项但重要的额外细节

复制提交的想法,一次一个,好像通过使用git cherry-pick确实是git rebase的核心。 我们只需要让 Git 列出正确的提交,执行分离的 HEAD 技巧,进行复制,并在最后更新分支名称——这就是它所做的。 但它背后有着悠久的历史,以及一堆需要了解的特殊情况:

  • 默认情况下,旧版本的 Git 将使用git format-patchgit am而不是使用git cherry-pick进行复制。 大多数情况下,这工作原理相同,但存在细微差异:

    • 在某些情况下,它要快得多;和
    • 它不能很好地处理文件重命名(这就是使其更快的原因)。

    您可以通过向变基添加各种选项或升级 Git 版本来强制这些较旧的 Git 使用樱桃挑选。

  • 默认情况下,变基删除完全合并提交。 造成这种情况的原因有很多,我们不会在这里讨论。 只要您自己的分支中没有合并,这不会影响您。

  • 默认情况下,从 Git 版本 2.0 开始,rebase 使用 Git 调用的分叉点代码来找出要删除的其他提交。 同样,我们不会在这里详细介绍:它们不会影响您的情况。

  • Git 还将从复制中省略它认为已经在目标中的任何提交。 这也不会影响您的情况(但了解这一点很有用)。

  • 最后,变基有一个"互动"模式,您可以在其中获得大量力量。 在这种情况下,您不需要它,但如果需要,您可以使用而不是--onto方法。--onto技巧对于您的情况来说更容易,但请记住,交互式变基是存在的。

    如果你的 Git 足够现代,如果你想"复制"它们,它也有一种方法可以重做合并提交,使用这个交互式变基功能。 如果你有一组复杂的分支,其中包含要变基的内部合并,你将需要这个。

最新更新