将现有提交的工作移至新的本地分支

时间:2021-03-05 19:03:19

标签: git

我有一个分支 (branch-B),我正在处理它最初从另一个分支 (branch-A) 分支出来的分支。

不幸的是,A 分支中暴露了一些秘密。我需要有效地从分支 B 中“解除”所有与 master 不同的我的工作文件,以便我可以将它们隐藏起来并将它们带到一个新的干净分支。

1 个答案:

答案 0 :(得分:0)

我很确定这已经被问到并得到了回答,但我无法很快找到一个好的答案,所以我就写一个。请注意,这与 Move the most recent commit(s) to a new branch with Git 不同。你只需要git rebase --onto。然而,关于这一点还有很多需要了解的地方。

我认为,绘制“之前”和“之后”结果的图片(即使是简化的图片)会有所帮助。这是你现在所拥有的,以我喜欢的方式绘制:

...--E--F   <-- main
         \
          G--H   <-- branch-A
              \
               I--J   <-- branch-B (HEAD)

也就是说,我们用一个大写字母表示每个提交(Git 都是关于提交,由丑陋的哈希 ID 标识)。 (Git 使用大长 hexadecimal 数字——那些哈希 ID——因为宇宙中的每个提交都必须有自己唯一的数字。使用大写字母,就像我一样,我们会用完大约 26 次提交。)括号中的附加 HEAD 只是告诉我们我们检查了哪个分支(通过 git checkoutgit switch)。

较新的提交出现在右侧。每个提交都连接(向后,尽管我绘制了没有正确箭头的箭头)到前一个提交。像 mainbranch-B 这样的分支名称只包含被视为该分支一部分的最后提交的实际哈希 ID。< /p>

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

您发现 branch-A branch-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 转到提交 { 时所做的更改作为其快照{1}}。也就是说,您进行相同的更改,但针对不同的基础
  • 同样,提交 I 向后连接到 J' ...
  • ...并且具有与从 I'I 相同的变化,但具有不同的基础

由于像这样“放弃”的 Git 提交——没有分支名称可以找到它们——变得几乎不可见,对改造后的存储库的随意检查显示相同的编号的提交似乎做同样的事情,但现在你的提交,在你的分支上,包含在 {{1 }}。更仔细、更仔细地观察会发现,这些仅在 J 上的提交与原始提交具有不同的哈希 ID 号,表明它们是副本。

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

方法一:branch-A

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

branch-B

我们需要一个 new 分支,可能是 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 的哈希 ID。如果要复制更多提交,我们会找到所有哈希 ID。我们将它们保存在某个地方——可能是在一个文件中,或者将它们记在草稿纸上,或者其他任何东西。我们确保此列表的顺序正确:首先提交 I,然后提交 J

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

I

这告诉 Git 去查看提交 J,找到它的父提交——git cherry-pick <hash-of-I> ——并比较 IH 的内容。然后它应该应用从 HI 所需的任何更改,将它们添加到从 H 回退到 I 所需的任何更改中(即,取出来自H的坏东西)。

您可以将其视为向 F 添加 H-vs-I 更改,但这仅在您添加的内容与我们需要删除的内容之间没有冲突时才有效。 A在内部,cherry-pick 实际上是一个 merge 操作。很多时候,没有任何合并冲突,这个“可以想到”的部分工作正常。但是如果你在这个操作中遇到合并冲突,请记住 Git 正在结合添加你的更改——从 Fbranch-A —“撤消更改”以从 H 返回到 I

此外,因为 H,通过 F,选择提交 HEAD,Git 将 new-and-improved-B-to-F 的更改视为“我们的”更改,H-to-F 会随着“他们”的变化而变化。因此,当您解决任何冲突时,请记住,此时 Git 所说的“他们的”实际上是的工作。

如果一切顺利,Git 会自己进行一次新的提交,我们将调用 H 来表示它是 I 的副本。新提交 I' 的提交消息,Git 只是从 I 复制。 (您可以在运行 I' 命令时选择编辑或不编辑它,方法是添加 I。)

git cherry-pick

如果事情进展顺利——如果你有合并冲突——Git会停下来让你清理混乱。有关如何执行此操作的详细信息,请参阅有关使用 --edit 清除合并冲突的任何说明:方法相同。请记住, I' <-- new-and-improved-B (HEAD) / ...--E--F <-- main \ G--H <-- branch-A \ I--J <-- branch-B 表示 your 提交 git merge,而 --theirs 表示......好吧,谁的提交 I 是:Git 正在尝试添加“撤消分支-A 的东西”以“执行提交-I 的东西”。最终,一旦你解决了这个烂摊子,你将运行 --ours,Git 会根据你的解决方案继续提交 F

现在我们已经提交 git cherry-pick --continue,我们对所有剩余的提交重复此操作。如果只剩下一个提交——I'——我们只有一个樱桃选择要运行;如果有很多,我们就跑,不管有多少。与 I' 一样,这 可能 有合并冲突:也许 J-to-I 更改与“退出所有来自 I" 的分支-A 内容发生了变化。

如果您确实有冲突,请记住 J 在这一点上表示提交 I(您正在复制的那个)而 --theirs 表示J--ours 是您刚刚使用上一个 樱桃挑选的产品。您可能需要再次解决同样的冲突。这并不常见,但这是反复采摘樱桃的痛苦方面之一。

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

commit I'

您现在必须做一些最后的事情。特别是,您需要说服 Git 将名称 I' 从提交 I'-J' <-- new-and-improved-B (HEAD) / ...--E--F <-- main \ G--H <-- branch-A \ I--J <-- branch-B 中删除,并使其指向提交 branch-B,然后您可能想要查看 {{1} } 将 J 附加到那里并摆脱临时分支。

你可以用一个命令完成前两部分:

J'

或者您可以使用 branch-B 移动 HEAD,然后使用 git checkout -B branch-B # or git switch -C branch-B git branch -f branch-B 切换到移动的分支。然后您只需要使用 branch-B 删除临时分支。

当然,这是相当多的工作。如果 Git 为我们做这件事就好了……事实上,Git 为我们做。

使用 git checkout

git switch 命令就是专门为我们做这种反复挑选的。它:

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

与cherry-pick 方法一样,每个 cherry-pick 都可能导致合并冲突。如果是这样,Git 会在中间停下来让我们清理烂摊子。然后我们运行 git branch -d new-and-improved-B 让 Git 返回到剩余的挑选和最后的 rebase move-the-branch 步骤。

问题在于 git rebase --onto 是专门为稍微不同的场景设计的。如果我们有这个

git rebase

我们想要这个:

git rebase --continue

我们只需执行 rebase(如果需要 - 绘图表明它不是),然后执行 ...--o--o--o <-- main \ A--B--C--D <-- feature (HEAD) 。 rebase 命令通过确定哪些提交在 our 分支上而不在 target 分支 A'-B'-C'-D' <-- feature (HEAD) / ...--o--o--o <-- main \ A--B--C--D [abandoned] 上来确定要复制的提交。

但我们不想那样。我们有:

git checkout feature

和 rebase 因此会假设我们要复制提交 git rebase main,因为提交 main...--E--F <-- main \ G--H <-- branch-A \ I--J <-- branch-B (HEAD) 在我们的分支上。他们只是另一个分支上。所以我们必须告诉 rebase:嘿,不要复制 G-H-I-J

那么,我们必须做的是:

G

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

因此,rebase 操作将在这里列出提交 branch-AH 的提交哈希 ID,作为要复制的。然后它将使用 detached-HEAD 技巧来创建一个未命名的分支,该分支以名称 main 选择的提交结束(使用适合您情况的任何名称)。然后,它会对之前列出的每个提交进行一一复制,最后,它会将我们所在所在的分支名称——I——指向指向最后复制的提交。

杂项但重要的额外细节

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

  • 默认情况下,旧版本的 Git 将使用 branch-Bgit cherry-pick 而不是使用 git rebase 进行复制。大多数情况下,这是相同的,但也有细微的差别:

    • 在某些情况下速度要快得多;和
    • 它不能很好地处理文件重命名(这使它更快)。

    您可以通过向 rebase 添加各种选项或升级您的 Git 版本来强制这些较旧的 Git 使用cherry-pick。

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

  • 默认情况下,自 Git 2.0 版起,rebase 使用 Git 调用的 fork-point 代码来确定要删除的其他提交。同样,我们不会在这里详细介绍:它们应该不会影响您的案例。

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

  • 最后,rebase 有一个“交互”模式,您可以在其中获得很多权力。在这种情况下您不需要它,但是如果需要,您可以使用它而不是 git format-patch 方法。 git am 技巧对您的情况更容易,但请记住存在交互式变基。

    如果您的 Git 足够现代,它还有一种方法可以重做合并提交,如果您想“复制”它们,使用此交互式 rebase 功能。如果您有一组复杂的分支和内部合并要变基,您将需要这个。