Rebase 分支保留基于它的另一个分支上的提交

Rebase branch preserving commits on another branch based on it

抱歉,如果标题有误导性,但我不太确定如何描述我遇到的情况。

我已经像这样提交和分支了

A --- B --- C --- D (master)
                   \
                    E (another)

我想从 master 分支中删除提交 BC(保留 D),但将它们保留在 another 分支中在主人身上。 所以在转换之后我的树应该是这样的:

A --- D (master)
 \
  B --- C --- E (another)

我想,我可能应该只变基 master,但是我不确定 BC 是否仍会包含在 another 中提到从中删除/省略 D

我应该如何进行才能达到上述效果?

假设您要移动变更集,应该没有那么难:

git rebase --onto A C master

这会将分支 master 移动到 A 之上,放弃直到 C 的修订(因此我只会移动 D 以及分支指针)。那么:

git rebase --onto C D another

这会将 E 变基到 C 之上,放弃直到 D 的修订(换句话说,仅移动 C 之上的 E...同时移动分支指针)。

应该可以。

实现它的另一种方法是简单地使用交互式变基:

https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History

从 master 创建另一个分支并手动重写它们:

$ git checkout -b another
$ git rebase -i 

git rebase 没有参数会给你一个列表,其中包含该分支上的所有提交。然后只需在提交列表中用 'd' 标记要删除的那些。例如对于大师,你想要 "remove" B、C 和 E。

d b72a395    E
pick 5bca8d9 D
d 15aab26    C
d 25aab26    B
pick 35aab26 A

对于 "another" 分支,标记 "D" 将被删除。

要得到你想要的结果——好吧,可能是你想要的结果,至少——你必须停止使用现有的提交DE 完全,原因是没有人——不是你或 Git 本身——可以改变 任何 关于 任何 现有提交, 提交之间的连接实际上是哈希 ID 存储在内部 parent/child 对的子项中。

也就是说,给定第一张图,提交 A 是根提交:它没有父项。没有箭头说 A 之前的提交是 _____ 因为 A 之前没有提交。但是提交B确实有一个箭头,从它指向提交A我之前的提交是提交A。提交 C 包含指向 B 的箭头; D 包含指向 C 的箭头; E 包含指向 D:

的箭头
A <-B <-C <-D <-E

不同于提交,分支名称可以更改:它们充当指向您选择的任何一个提交的箭头。因此 master 当前指向现有提交 D,并且 another 指向现有提交 E。 Git可以从another开始找E,用ED,用DC,还有很快;或者Git可以从master开始找D,这样就可以找到CBA.

您想要的结果有一个提交 B 指向 AC 指向 B,因此现有提交通过 C 都很好。但是您想要 D 的新改进变体,它不是指向 C,而是直接指向 A.

这个新的和改进的 D' 大概有一个现有提交没有的快照。要为 D' 制作快照,您希望 Git 获取 CD 中快照之间的差异,并将该差异应用于 [=] 中的快照23=].

Git 可以自动执行此操作。执行此操作的基本 Git 命令是 git cherry-pick。我们稍后会看到如何使用 git rebase运行(正确的一组)git cherry-pick 命令,但让我们从 cherry- 开始选择自己。

同样,你想要一个 E 的新的和改进的 copy,我们可以称之为 E',其中的改进是:

  • 指向C,而不是D;和
  • 有一个快照是通过将快照 DE 之间的差异应用于 C 中的快照而制作的。

同样,这是 git cherry-pick 的工作。那么让我们看看如何做到这一点。

使用git cherry-pick

要创建父级为 A 的新的和改进的 D',我们必须首先 git checkout 提交 A 本身,最好还在那里附加一个临时分支名称以免混淆。 (在内部,使用 git rebase,Git 使用 no 临时分支名称完成所有这些。)所以我们将 运行:

git checkout -b temp <hash-of-A>

这给了我们:

A   <-- temp (HEAD)
 \
  B--C--D   <-- master
         \
          E   <-- another

现在我们像这样使用git cherry-pick

git cherry-pick <hash-of-D>
# or: git cherry-pick master

这会复制提交 Dmaster 指向的那个——我们可以通过它的哈希 ID 或名称 master 给它——到新的提交 D',temp 现在指向它。 (任何时候我们进行新的提交,Git 将新提交的哈希 ID 存储在 current 分支中:HEAD 附加到。所以 temp 现在指向复制 D'.)

A--D'  <-- temp (HEAD)
 \
  B--C--D   <-- master
         \
          E   <-- another

现在我们需要另一个新的临时分支,指向提交 C,所以我们 运行 git checkout -b temp2 <em>hash- of-C</em>。 (除了原始哈希,我们可以使用 Git 必须找到提交 C 的任何其他方式,例如 master~1,但是原始哈希可以剪切和粘贴,因为只要你剪对了。)这给了我们:

A--D'  <-- temp
 \
  B--C   <-- temp2 (HEAD)
      \
       D   <-- master
        \
         E   <-- another

(注意 HEAD 现在是如何附加到 temp2,因为 git checkout -b。)现在我们挑选提交 E 来制作 E':

git cherry-pick another

会成功,因为 another 指向提交 E。如果一切顺利,Git 自己进行新的提交,我们有:

A--D'  <-- temp
 \
  B--C--E'  <-- temp2 (HEAD)
      \
       D   <-- master
        \
         E   <-- another

我们现在需要做的是强制名称 master 引用提交 D',名称 another 引用提交 E'。现在要做到这一点,我们可以使用 git branch -f:

git branch -f master temp
git branch -f another temp2

这给了我们:

A--D'  <-- master, temp
 \
  B--C--E'  <-- another, temp2 (HEAD)
      \
       D   [abandoned]
        \
         E   [abandoned]

尽管提交 DE 没有 名称——这使得它们很难找到——它们会在你的 Git 存储库中相当长一段时间,通常至少 30 天。 (这可以通过各种 reflog 过期设置来控制。)如果你已经将他们的哈希 ID 保存在某个地方(并且你已经 - 或者更确切地说,Git 已经将哈希 ID 保存在一些reflogs),您仍然可以在这段时间内取回它们。

您现在可以 git checkout 任一原始分支名称并删除两个 temp 名称。

使用 git rebase

执行此操作

git rebase所做的本质上是1运行[=63=的系列 ] 命令,并通过 运行ning 等效于 git branch -f 来完成所有操作,以强制分支名称指向 last 复制的提交,并且 [=77= 】 那个分支。 git rebase 将复制的提交集来自 rebase 所称的 upstream 参数。 rebase 将它们复制到的位置,就像 git cherry-pick 一样,来自 rebase 调用它的 onto 参数。

也就是你运行:

git rebase --onto <target> <upstream>

其中 target 是您想要第一个复制提交之前的提交,并且 upstream 告诉 Git 什么提交 而不是 复制。这个 "what not to copy" 一开始看起来很奇怪,但你会习惯它。2 它也允许你在大多数时候省略 --onto (虽然不是在你的具体情况)。

Git 所做的是枚举 <em>upstream</em>..HEAD 中的提交,排除某些通常不受欢迎的提交。3 这会产生一个应该复制/挑选的提交哈希 ID 列表。此列表被保存到一个临时文件中。4 然后,Git 运行 是 git checkout 的 HEAD 分离变体以检查 target 提交 --onto,或者 upstream 如果您没有指定 --onto。然后,Git 对保存的哈希 ID 进行挑选。最后,如果一切顺利,Git 强制将分支及其 HEAD 重新附加到 rebase 操作中最后复制的提交。

对于您的特殊情况,,比我快 20 分钟得到这个答案。 :-) 这只是对实际情况的长篇解释。


1我在这里说 好像 一样,因为有些 rebase 方法使用其他方法,有些 rebases 字面意思 运行 git cherry-pick,或者——在最现代的 Git 中——直接构建在 Git 内部调用的 sequencer 中,它实现了 cherry -采摘。

2由于Git的A..B限制语法,这实际上是自然的。这告诉Git:找到可从到达的提交B,排除那些可从到达的提交A. 有关可达性的(更多)信息,请参阅 Think Like (a) Git.

3不受欢迎的是现有的合并提交,以及任何已经被挑选出来的提交。 Git 使用 git patch-id 程序找到后者。描述得当有点棘手,这里就不赘述了。

4它在 .git 之下,但在 Git 的开发过程中位置已经移动。根据其他情况,有时您可以在 .git/rebase-todo 或类似的名称中找到它们,如果您好奇的话。