使用不同的分支撤消对旧提交的修改

Undo an amendment of an older commit using a different branch

假设我已经 git rebase 我的存储库的一个分支 b2,修改了一个旧的提交 (c1)。此提交未经修改地存在于另一个分支 b1 上(并且在 c1 之后两个分支上都有一些更常见的提交,然后 b2b1 不同)。

现在,我想使用 来撤消我在 b2 上对 c1 的修改。我应该怎么做才能使两个分支的历史再次变得最大相同?

TL;DR

使用git rebase --onto。使用 --onto 参数指定目标,并使用通常的 upstream 参数指定要复制的提交 not。从这里很难确切地说出这些参数应该是什么;请参阅下面的长篇讨论。

这个要求:

... that the two branches' history becomes maximally identical again

意味着您知道 git rebase 通过 复制 提交变得很重要。为了快速回顾想法,请注意:

  • 每个提交都有一个唯一的哈希 ID;
  • 每次提交都会保存所有文件的快照;和
  • 每个提交还包含一些元数据。

提交中的元数据包括作者和提交者以及日志消息,而且——整个过程的关键——parent 的哈希 ID提交。

要将提交变成 change-set,即找出某人在任何给定提交中更改的内容,我们 Git 将提交与其 parent 进行比较。提交存储其 parent 的哈希 ID,因此 git showgit log -p 可以自行找到它。

与此同时,一个 b运行ch name 就像 b2 只是保存 last 的哈希 ID在 b运行ch 中提交。所以我们可以绘制它们——提交和 b运行ch 名称——像这样:

... <-c1 <-c2 <-c3    <--b2

其中每个 ci 都是由其哈希表示的实际提交,并且从某物中出来的箭头表示 指向: b运行ch name b2 指向提交 c3, c3 指向 c2, c2指向 c1,依此类推。

任何提交都不会改变,所以我们可以绘制内部箭头,从提交到提交,作为连接线而不是箭头,只要我们记住它们是 child 和向后指向 parent。这让我们可以在粗略的文本图形中绘制不止一个 b​​运行ch。我将使用 X1 及更高版本,因为这只是一个示例,与您的起点并不完全相关:

...--X1   <-- branch1
       \
        X2--X3   <-- branch2

如果 branch1 获得更多提交,最新的最终会返回到 X1:

...--X1--X4--X5   <-- b1
       \
        X2--X3   <-- b2

现在回到你原来的设置:

Suppose I've git rebase'd a branch b2 of my repository, amending an older commit (c1). This commit exists, unamended, on another branch b1 (and there are some more common commits on both branches after c1, then b2 diverges from b1).

我会尽可能准确地画出原始设置的图片:

                    c4--c5   <-- b1
                   /
...--c0--c1--c2--c3
                   \
                    c6--c7   <-- b2

(我用猜测来填补遗漏的细节,虽然最终猜测应该没有多大关系。)

当您 运行 git rebase -i <start-point>b2 和 amended/edited 上提交 c1 时,Git 必须 复制 c1 一些新的和不同的提交。新提交像往常一样有一个作者和日志消息,最初设置为从 c1 复制,但它有一个新的和不同的哈希 ID,可能还有一个不同的快照 and/or 不同的日志消息(甚至不同的作者),具体取决于您所做的更改。让我们调用新副本c1'来区分它们:

                    c4--c5   <-- b1
                   /
...--c0--c1--c2--c3
       \
        c1'  [copying in progress]

因为 c1 被复制到 c1',Git 现在 被迫c2 复制到新的 c2':

                    c4--c5   <-- b1
                   /
...--c0--c1--c2--c3
       \
        c1'-c2'  [copying in progress]

c1'c2'的区别与从c2c1的区别相同。也就是说,如果我们比较 c2 和它的 parent c1,我们会得到一些变化。如果我们将 c2'c1' 进行比较,我们将得到相同的 更改 ,即使 c1c1' 的内容不同。

现在 c2 被替换为 c2',这也迫使 Git 将 c3 复制到 c3'。这迫使 Git 也复制 c6c7。最终复制的提交是 c7' 并且 git rebase 通过将 name b2 拉过来完成,因此最终结果是:

                    c4--c5   <-- b1
                   /
...--c0--c1--c2--c3--c6--c7   [old b2, now abandoned]
       \
        c1'-c2'-c3'-c6'-c7'  <-- b2 (HEAD)

Now, I want to use to essentially undo my amendment to c1 on b2. How should I do this, so that the two branches' history becomes maximally identical again?

您可能还添加了更多的提交:

                    c4--c5   <-- b1
                   /
...--c0--c1--c2--c3--c6--c7   [old b2, now abandoned]
       \
        c1'-c2'-c3'-c6'-c7'-c8--c9   <-- b2 (HEAD)

您可能希望得到的结果是:

                    c4--c5   <-- b1
                   /
...--c0--c1--c2--c3--c6--c7--c8'-c9'  <-- b2 (HEAD)
       \
        c1'-c2'-c3'-c6'-c7'-c8--c9   [abandoned]

如果你最终得到:

,这可能没问题(通常会容易得多)
                    c4--c5   <-- b1
                   /
...--c0--c1--c2--c3--c6"-c7"-c8'-c9'  <-- b2 (HEAD)

with none 被放弃的提交。1

要获得其中任何一个,您需要:

  • 告诉Git复制至少c8c9(假设它们存在),以及
  • 告诉Git不要通过c3'
  • 复制c1'

这意味着使用 git rebase --onto,这样您就可以将 git rebase 通常合并的两条指令分开。

git rebase所做的是:

  1. 枚举一些要复制的提交列表。在上面的示例中,这些是 c1c7。如果您使用 git rebase -i,该列表将进入可修改指令 sheet,使用单词 pick 以及每个提交的哈希值(缩短)和提交日志消息的主题行。

    通常,merge 提交——那些有两个或更多 parents 的提交——会立即从列表中弹出。 (有些模式不拒绝,比较复杂,这里忽略)

    列出了哪些提交?那来自你的 upstream 论点:他承诺被复制是那些可以通过向后走从HEAD到达的那些,承诺承诺:c7回到c6,它跳转返回到 c3,返回到 c2,依此类推。但是,从这个列表——它可以追溯到很长的路要走——我们 删除 任何可以从 upstream 参数到达的提交。因此,如果 upstreamc0 的哈希 ID,我们将从 c0 away列表,以及 c0 之前的任何提交。这意味着列表以 c1 开始并以 c7 结束,跳过 unreachable-this-way c4c5.

  2. --onto 选择一个目标。如果你使用--onto,你直接选择这个。如果不是,您可以使用 upstream 参数选择它。例如,对于 git rebase master,上游是 master--onto 目标是名称 master 指向的提交。 Git 在这里(或内部等效项)执行 git checkout --detach 以便使用您正在复制的提交离开 b运行ch。

  3. 开始复制提交,就像 git cherry-pick 一样,一次一个。一些 rebase 操作字面上使用 git cherry-pick 而有些则不使用

  4. 复制完成后,移动原来的b运行ch name,使其指向HEAD,即最后复制的提交,或者——如果我们毕竟没有复制任何提交——--onto 目标。然后回到那个b运行ch,好像通过git checkout <em>name</em>.

使用 --onto 可让您更改 upstream 参数,而无需同时设置变基目标。

所以,如果你想复制只是c8c9,你可以通过检查知道--onto目标是c7,而您不想 复制的第一个提交是 c7。毕竟这不需要 git rebase --onto 。如果您有可用的哈希 ID c7,例如,您可以 运行:

git rebase <hash-of-c7>

同时在 b运行ch b2。但是要在第一次变基之前找到原来的 c7,您将不得不翻阅 reflog。这可能很困难,因为 reflogs 往往包含很多动作,一旦你复制了一次提交,你可能已经复制了很多次。2

所以我们可以让 Git 再次复制 c6'c7'。我们将 upstream 设置为 c3' 作为第一个提交 而不是 复制,并设置 c3 作为 --onto 目标:

git rebase --onto <hash-of-c3> <hash-of-c3'>

例如。 Git 将通过从 HEAD (c9) 返回来枚举提交,直到它到达 c3',你说不要复制(也不是更早的任何内容)。这将列出 c6'c7'c8c9 作为要复制的提交。副本将在 c3 (--onto) 之后。请注意,提交 c3c3' 在 Git 绘制的历史和粗略的 ASCII 图形中都很容易看到,您可以通过以下方式查看:

git log --graph --oneline b1 b2

所以这为您提供了 --onto 和上游参数的哈希 ID。


1c9 及其废弃的历史都还在那里,在您的 Git 存储库中,如果您以后想要它们的话。它们可以通过 Git 的 reflogs 找到。 reflog 条目仅持续一段时间。默认情况下,1 到 3 个月后,reflog 条目将过期并被删除。一旦发生这种情况,被放弃的提交本身也可以被真正删除,之后你 不能 找回它们,至少不能通过你自己的 Git.

(reflog 和 reflog 过期的详细信息有点复杂,但与这里无关。)

2查看 b运行ch b2、运行 git reflog b2 的引用日志。如果你幸运的话,没有很多副本,也没有很多 运行dom 运动,你 可以 找到 c7 这样。