为了得到你想要的结果——嗯,至少可能是你想要的结果——你必须完全停止使用现有的提交 D 和 E,并且原因是没有人——除了你或 Git 本身——可以完全改变 any 关于 any 现有提交的任何事情,以及 提交之间的连接实际上是存储在内部父/子对的子的哈希 ID。
也就是说,给定第一张图,提交A 是根提交:它没有父提交。没有箭头表示A 之前的提交是_____,因为A 之前没有提交。但是提交B确实有一个箭头,从它指向提交A:我面前的提交是提交A。提交C 包含一个指向B 的箭头; D 包含一个指向 C 的箭头;而E 包含一个指向D 的箭头:
A <-B <-C <-D <-E
与commits 不同,分支名称可以更改:它们充当指向您选择的任何一个提交的箭头。所以master 当前指向现有提交D,another 指向现有提交E。 Git可以从another开始查找E,使用E查找D,使用D查找C,以此类推;或者Git可以从master开始找到D,找到C和B和A。
你想要的结果有一个提交 B 指向 A,C 指向 B,所以通过 C 的现有提交都可以。但是你想要一个新的和改进的D 变体,它不是指向C,而是直接指向A。
这个新的和改进的D' 大概有一个现有提交没有的快照。要为 D' 创建快照,您希望 Git 获取 C 和 D 中的快照之间的差异,并将该差异应用于 A 中的快照。
Git 可以自动执行此操作。执行此操作的基本 Git 命令是 git cherry-pick。我们稍后会看到如何使用git rebase 来运行(正确的一组)git cherry-pick 命令,但让我们从cherry-pick 本身开始。
同样,您想要一个新的和改进的副本 E,我们可以称之为E',其中的改进是:
- 指向
C,而不是D;和
- 拥有通过将快照
D 和E 之间的差异应用于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
这会将提交 D,master 指向的那个——我们可以通过它的哈希 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]
虽然提交 D 和 E 没有名称——这使得它们很难找到——但它们会在你的 Git 存储库中逗留很长一段时间,通常至少 30天。 (这可以通过各种 reflog 过期设置来控制。)如果你已经将它们的哈希 ID 保存在某个地方(并且你已经 - 或者更确切地说,Git 已经将哈希 ID 保存在一些 reflog 中),你仍然可以获得他们在这段时间内回来。
您现在可以git checkout 任何一个原始分支名称并删除两个temp 名称。
使用git rebase 执行此操作
git rebase 所做的实质上是1 运行 一系列 git cherry-pick 命令,然后通过运行 git branch -f 的等效命令来完成所有操作强制分支名称指向 last 复制的提交,并 git checkout 该分支。 git rebase 将复制的提交集来自 rebase 调用的 upstream 参数。 rebase 将它们复制到的位置,就像 git cherry-pick 一样,来自 rebase 调用的 onto 参数。
也就是说,你运行:
git rebase --onto <target> <upstream>
其中 target 是你想要在 第一个复制的提交之前的提交,upstream 告诉 Git 什么提交 不复制。这个“不要复制的东西”起初看起来很奇怪,但你会习惯它。2它还允许你在大多数情况下省略--onto(尽管不是在你的特定情况下)。
Git 的作用是枚举<em>upstream</em>..HEAD 中的提交,不包括某些通常不受欢迎的提交。3 这提供了应复制/挑选的提交哈希 ID 列表。这个列表被保存到一个临时文件中。4 然后,Git 运行 git checkout 的 HEAD 分离变体来检查 --onto 的 target 提交,或 upstream 如果您没有指定 --onto。然后,Git 对保存的哈希 ID 进行挑选。最后,如果一切顺利,Git 会强制将分支及其 HEAD 重新附加到从 rebase 操作中复制的最后一次提交。
对于您的特殊情况,eftshift0 has already shown the appropriate git rebase commands 比我早了约 20 分钟得到了这个答案。 :-) 这只是对实际情况的详细解释。
1我在这里说 as if 是因为某些变基方法使用其他方法,而某些变基实际上运行 git cherry-pick,或者——在最现代的Git——直接内置在 Git 内部调用的 sequencer 中,它实现了樱桃采摘。
2这实际上是很自然的,因为 Git 的 A..B 限制语法。这告诉 Git:找到 可从 B 访问的提交,不包括那些可从 A 访问的提交。 更多关于可达性的信息,见Think Like (a) Git。
3不受欢迎的是现有的合并提交,以及任何已经精心挑选的提交。 Git 使用git patch-id 程序找到后者。正确描述有点棘手,这里不再赘述。
4它位于.git 下,但在 Git 的开发过程中该位置已移动。如果您好奇,有时可以在 .git/rebase-todo 或类似名称中找到它们。