使用不同的分支撤消对旧提交的修改
Undo an amendment of an older commit using a different branch
假设我已经 git rebase
我的存储库的一个分支 b2
,修改了一个旧的提交 (c1
)。此提交未经修改地存在于另一个分支 b1
上(并且在 c1
之后两个分支上都有一些更常见的提交,然后 b2
与 b1
不同)。
现在,我想使用 来撤消我在 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 show
或 git 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'
的区别与从c2
到c1
的区别相同。也就是说,如果我们比较 c2
和它的 parent c1
,我们会得到一些变化。如果我们将 c2'
与 c1'
进行比较,我们将得到相同的 更改 ,即使 c1
与 c1'
的内容不同。
现在 c2
被替换为 c2'
,这也迫使 Git 将 c3
复制到 c3'
。这迫使 Git 也复制 c6
和 c7
。最终复制的提交是 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复制至少
c8
和c9
(假设它们存在),以及
- 告诉Git不要通过
c3'
复制c1'
这意味着使用 git rebase --onto
,这样您就可以将 git rebase
通常合并的两条指令分开。
即git rebase
所做的是:
枚举一些要复制的提交列表。在上面的示例中,这些是 c1
到 c7
。如果您使用 git rebase -i
,该列表将进入可修改指令 sheet,使用单词 pick
以及每个提交的哈希值(缩短)和提交日志消息的主题行。
通常,merge 提交——那些有两个或更多 parents 的提交——会立即从列表中弹出。 (有些模式不拒绝,比较复杂,这里忽略)
列出了哪些提交?那来自你的 upstream
论点:他承诺被复制是那些可以通过向后走从HEAD
到达的那些,承诺承诺:c7
回到c6
,它跳转返回到 c3
,返回到 c2
,依此类推。但是,从这个列表——它可以追溯到很长的路要走——我们 删除 任何可以从 upstream
参数到达的提交。因此,如果 upstream
是 c0
的哈希 ID,我们将从 c0
away列表,以及 c0
之前的任何提交。这意味着列表以 c1
开始并以 c7
结束,跳过 unreachable-this-way c4
和 c5
.
为 --onto
选择一个目标。如果你使用--onto
,你直接选择这个。如果不是,您可以使用 upstream
参数选择它。例如,对于 git rebase master
,上游是 master
,--onto
目标是名称 master
指向的提交。 Git 在这里(或内部等效项)执行 git checkout --detach
以便使用您正在复制的提交离开 b运行ch。
开始复制提交,就像 git cherry-pick
一样,一次一个。一些 rebase 操作字面上使用 git cherry-pick
而有些则不使用
复制完成后,移动原来的b运行ch name,使其指向HEAD
,即最后复制的提交,或者——如果我们毕竟没有复制任何提交——--onto
目标。然后回到那个b运行ch,好像通过git checkout <em>name</em>
.
使用 --onto
可让您更改 upstream
参数,而无需同时设置变基目标。
所以,如果你想复制只是c8
和c9
,你可以通过检查知道--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'
、c8
和 c9
作为要复制的提交。副本将在 c3
(--onto
) 之后。请注意,提交 c3
和 c3'
在 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
这样。
假设我已经 git rebase
我的存储库的一个分支 b2
,修改了一个旧的提交 (c1
)。此提交未经修改地存在于另一个分支 b1
上(并且在 c1
之后两个分支上都有一些更常见的提交,然后 b2
与 b1
不同)。
现在,我想使用 来撤消我在 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 show
或 git 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 branchb1
(and there are some more common commits on both branches afterc1
, thenb2
diverges fromb1
).
我会尽可能准确地画出原始设置的图片:
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'
的区别与从c2
到c1
的区别相同。也就是说,如果我们比较 c2
和它的 parent c1
,我们会得到一些变化。如果我们将 c2'
与 c1'
进行比较,我们将得到相同的 更改 ,即使 c1
与 c1'
的内容不同。
现在 c2
被替换为 c2'
,这也迫使 Git 将 c3
复制到 c3'
。这迫使 Git 也复制 c6
和 c7
。最终复制的提交是 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复制至少
c8
和c9
(假设它们存在),以及 - 告诉Git不要通过
c3'
复制
c1'
这意味着使用 git rebase --onto
,这样您就可以将 git rebase
通常合并的两条指令分开。
即git rebase
所做的是:
枚举一些要复制的提交列表。在上面的示例中,这些是
c1
到c7
。如果您使用git rebase -i
,该列表将进入可修改指令 sheet,使用单词pick
以及每个提交的哈希值(缩短)和提交日志消息的主题行。通常,merge 提交——那些有两个或更多 parents 的提交——会立即从列表中弹出。 (有些模式不拒绝,比较复杂,这里忽略)
列出了哪些提交?那来自你的
upstream
论点:他承诺被复制是那些可以通过向后走从HEAD
到达的那些,承诺承诺:c7
回到c6
,它跳转返回到c3
,返回到c2
,依此类推。但是,从这个列表——它可以追溯到很长的路要走——我们 删除 任何可以从upstream
参数到达的提交。因此,如果upstream
是c0
的哈希 ID,我们将从c0
away列表,以及c0
之前的任何提交。这意味着列表以c1
开始并以c7
结束,跳过 unreachable-this-wayc4
和c5
.为
--onto
选择一个目标。如果你使用--onto
,你直接选择这个。如果不是,您可以使用upstream
参数选择它。例如,对于git rebase master
,上游是master
,--onto
目标是名称master
指向的提交。 Git 在这里(或内部等效项)执行git checkout --detach
以便使用您正在复制的提交离开 b运行ch。开始复制提交,就像
git cherry-pick
一样,一次一个。一些 rebase 操作字面上使用git cherry-pick
而有些则不使用复制完成后,移动原来的b运行ch name,使其指向
HEAD
,即最后复制的提交,或者——如果我们毕竟没有复制任何提交——--onto
目标。然后回到那个b运行ch,好像通过git checkout <em>name</em>
.
使用 --onto
可让您更改 upstream
参数,而无需同时设置变基目标。
所以,如果你想复制只是c8
和c9
,你可以通过检查知道--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'
、c8
和 c9
作为要复制的提交。副本将在 c3
(--onto
) 之后。请注意,提交 c3
和 c3'
在 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
这样。