假设我已git rebase
进入了存储库的分支b2
,并修改了较早的提交(c1
)。此提交未经修改地存在于另一个分支b1
上(并且c1
之后的两个分支上都有一些更常见的提交,然后b2
与b1
有所不同)。
现在,我想使用基本上撤消对c1
对b2
的修订。我应该如何做,以便两个分支的历史再次变得最大相同?
答案 0 :(得分:2)
使用git rebase --onto
。使用--onto
参数指定目标,并使用通常的 upstream
参数指定要复制的提交 not 。从这里很难确切地说出这些论点应该是什么。请参阅下面的详细讨论。
此要求:
...两个分支的历史再次变得完全相同
意味着您必须知道git rebase
是通过复制提交起作用的,这一点很重要。为了快速回顾一下想法,请注意:
提交中的元数据不仅包括作者,提交者和日志消息,而且是整个过程的关键-提交的父级的哈希ID。
要将某项提交转变为变更集,即找出某人在任何给定提交中所做的更改,我们让Git将提交与其父项进行比较。提交存储了其父级的哈希ID,因此git show
或git log -p
可以自己找到它。
同时,像b2
这样的分支名称仅保存分支中 last 提交的哈希ID。这样我们就可以绘制它们(提交和分支名称),如下所示:
... <-c1 <-c2 <-c3 <--b2
其中每个 c i 是由其散列表示的实际提交,并且从某处出来的箭头表示指向:分支名称b2
指向提交c3
,c3
指向c2
,c2
指向c1
,依此类推。
关于任何提交的任何事情都不会改变,因此我们可以绘制从提交到提交的内部箭头,而不是箭头,而是连接线,只要我们记得它们是孩子的一部分并向后指向父对象即可。这使我们可以在原始文本图形中绘制多个分支。我将使用X1
及更高版本,因为这只是一个示例,与您的起点不完全相关:
...--X1 <-- branch1
\
X2--X3 <-- branch2
如果branch1
获得了更多提交,则最新的最终会导致返回到X1
:
...--X1--X4--X5 <-- b1
\
X2--X3 <-- b2
现在回到您的原始设置:
假设我已经对存储库的分支
b2
进行了git变基,修改了较旧的提交(c1
)。此提交未经修改地存在于另一个分支b1
上(并且c1
之后的两个分支上都有一些更常见的提交,然后b2
与b1
有所不同)。
我会尽可能准确地绘制原始设置的图片:
c4--c5 <-- b1
/
...--c0--c1--c2--c3
\
c6--c7 <-- b2
(我用猜测填充了缺失的细节,尽管最后猜测应该没有多大关系。)
当您在git rebase -i <start-point>
上运行b2
并修改/编辑提交c1
时,Git必须复制 c1
到一些新的和不同的地方承诺。新提交具有与往常一样的作者和日志消息,它们最初设置为从c1
复制,但是它具有新的和不同的哈希ID以及不同的快照和/或不同的日志消息(甚至不同的作者),具体取决于您所做的更改。让我们将新副本称为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
与它的父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)
现在,我想使用基本上撤消对b2上c1的修改。我应该如何做,以便两个分支的历史再次变得最大相同?
此后,您可能还添加了更多提交:
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)
没有引入任何放弃的提交。 1
要获取其中任何一个,您想要:
c8
和c9
(假设它们存在),并且c1'
复制c3'
这意味着使用git rebase --onto
,以便您可以分隔git rebase
通常合并的两条指令。
也就是说,git rebase
的作用是:
枚举一些要复制的提交列表。在上面的示例中,它们是c1
至c7
。如果您使用git rebase -i
,则该列表将进入可修改的说明表,其中使用单词pick
以及每个提交的哈希(缩短)和提交日志消息中的主题行。
通常,合并提交(具有两个或多个父级的提交)会立即从列表中弹出。 (有些模式不能拒绝它们;它们更复杂,我们在这里将忽略它们。)
列出哪些提交?这是基于您的 upstream
参数:要进行复制的提交是从HEAD
可以通过向后走来到达的那些,提交以提交:{{ 1}}回到c7
,然后再跳回到c6
,再回到c3
,依此类推。但是,从该列表(可能会追溯到很长的时间)中,我们删除从 c2
参数可以到达的所有提交。因此,如果 upstream
是upstream
的哈希ID,我们将从列表中删除c0
,以及所有提交在c0
之前。这意味着列表以c0
开头,以c1
结尾,并跳过了无法到达的c7
和c4
。
为c5
选择目标。如果使用--onto
,则直接选择它。如果不是,则使用 --onto
参数选择它。例如,对于upstream
,上游是git rebase master
,而master
目标是名称--onto
指向的提交。 Git在此处(或内部等效项)执行master
,以使您要复制的提交脱离分支。
开始复制,就像一次git checkout --detach
一样。一些变基操作实际上使用git cherry-pick
,而有些则没有。
复制完成后,移动原始的分支名称,使其指向git cherry-pick
,这是最后复制的提交,或者-如果我们未复制毕竟是任何提交-HEAD
目标。然后回到该分支,就像通过--onto
。
使用git checkout name
可以更改 --onto
参数,而无需设置基准的目标。
因此,如果您要复制 just upstream
和c8
,则可以通过检查得知c9
目标是--onto
,并且您不想要复制的第一个提交是c7
。毕竟,这不需要c7
。如果您拥有git rebase --onto
的哈希ID,则可以运行:
c7
在分支git rebase <hash-of-c7>
上。但是要找到原始的b2
,在第一次重新设置基准之前,您必须仔细阅读reflog。这可能很困难,因为reflog往往包含很多动作,并且一旦复制了一次提交,就可能复制了很多次。 2
因此,我们可以让Git再次复制c7
和c6'
。我们将 c7'
设置为upstream
作为要复制的第一个提交 not ,并将c3'
设置为{{1 }}目标:
c3
例如。 Git将通过从--onto
(git rebase --onto <hash-of-c3> <hash-of-c3'>
)向后走直到到达HEAD
(您说不可以复制(之前也没有复制))来枚举提交。这将列出c9
,c3'
,c6'
和c7'
作为要复制的提交。副本将放在c8
(c9
)之后。请注意,提交c3
和--onto
在Git绘制的历史记录和原始ASCII图中都很容易看到,您可以使用以下视图查看:
c3
因此,这为您提供了c3'
和上游参数的哈希ID。
1 git log --graph --oneline b1 b2
及其废弃的历史记录仍在Git存储库中。可以通过Git的 reflogs 找到它们。 reflog条目仅持续一段时间。默认情况下,在1到3个月后,reflog条目将过期并被删除。一旦发生这种情况,被遗弃的提交本身也可以被删除,之后,您 至少不能通过自己的Git将它们找回。
(reflog和reflog到期的细节有些复杂,但在这里并不重要。)
2 要查看分支--onto
的引用日志,请运行c9
。如果幸运的话,副本不多,随机运动也不多,您可以以这种方式找到b2
。