我跑了git rebase -i <target hash>
.进行更改后,我运行了git rebase --continue
.然后,Git 正确地重定了 15 次提交中的 15 次。成功执行此操作后,我收到此错误:
error: could not apply <bad hash>... <commit message>
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
Could not apply <bad hash>... <commit message>
我检查了与bad hash
相关的提交。我发现此提交发生在target hash
前大约 3 个月。那么为什么 git 变基甚至触及这个提交呢?我认为只变基从当前提交到target hash
的目标提交。
有谁知道问题可能是什么?我对变基有根本性的误解还是犯规?
TL;博士
如果没有实际的存储库,很难判断,但您可能会在合并中变基。
长
变基不查看提交日期。 理解 Git 变基的关键,无论是否交互式,都是:
- 了解分支如何生长;
- 了解
git cherry-pick
;以及 - 查看(并在提交图中选择提交),尤其是可访问性的概念。
这些有点交织在一起。 有关可访问性概念的更长但很好的介绍,请参阅像 (a) Git 一样思考。
了解 Git 提交
让我们先看一下简单提交的剖析。 下面是 Git 存储库中的一个:
$ git rev-parse HEAD
5be1f00a9a701532232f57958efab4be8c959a29
$ git cat-file -p 5be1f00a9a701532232f57958efab4be8c959a29 | sed 's/@/ /'
tree 8ccb7d4fa49449a843b00aca64baf99feb10e2ab
parent e7e80778e705ea3f9332c634781d6d0f8c6eab64
author Junio C Hamano <gitster pobox.com> 1516742470 -0800
committer Junio C Hamano <gitster pobox.com> 1516742470 -0800
First batch after 2.16
Signed-off-by: Junio C Hamano <gitster pobox.com>
提交或任何其他 Git 内部对象由其哈希 ID唯一标识,例如5be1f00a9a701532232f57958efab4be8c959a29
。 这些对人类不是很有用,所以我们使用master
和HEAD
和v2.16.0
等名称来识别它们,但 Git 最终使用这些原始哈希 ID。
提交存储:
- 快照哈希 (
tree ...
); - 父提交哈希(
parent ...
),有时是多个父级; - 作者和提交者(姓名、电子邮件和时间戳),您通过配置自动提供;以及
- 日志消息(这是您为提交手动提供的唯一部分)。
每个提交(实际上,每个 Git 对象)都是只读的。 您无法更改有关任何现有提交的任何内容。
由于每次提交都会记录其父级,因此提交会形成一个链。 如果我们从最近的一个开始——这就是 Git 所做的——我们可以查看它并找到它的父级。 这给了我们第二个提交哈希,所以我们可以查看该提交,并找到它的父级,这给了我们第三个哈希,依此类推。 我们说这些存储的哈希 ID 中的每一个都指向另一个提交。
绘制提交以及分支如何增长
换句话说,通过从单个指针开始,指向提交链的末尾,我们可以沿着提交链向后工作:
... <-o <-o <-o <-o <--last-one
这个"最后一个"指针就是 Git 所说的分支(或者更准确地说,分支名称)。 分支名称只是存储一个特定 Git 提交的哈希 ID:即分支顶端的那个。 (因此,我们称之为提示提交。
为了发展一个分支,就像git commit
一样,Git 将首先通过为它写出一个tree
对象(以查找树的哈希)来创建新的提交,然后从已知数据创建其余的提交:树,存储在last-one
中的当前提交的哈希,您作为作者和提交者(以"now"作为时间戳), 和您的提交消息。 此提交将进入存储库,存储库将生成一个新的唯一哈希 ID。
新的提交N
指向分支的上一个提示:
...--o--o--o--o <-- last-one
N
现在 Git 知道新提交的新哈希 ID 是什么,Git 所要做的就是将该哈希 ID写入last-one
,以便last-one
指向新提交:
...--o--o--o--o
N <-- last-one
(然后我们可以画这个没有最后的弯曲)。
挑选:复制提交
虽然提交是快照,但我们通常喜欢将其视为更改。 要将提交视为更改,我们只需首先获取提交父级的快照,然后获取提交本身的快照,并比较它们:
git diff <parent> <child>
此命令的输出是一组指令:如果对父项进行这些更改,则会获得子项。(理想情况下,这与进行更改的人实际所做的相同,尽管人们会看到 Git 经常达不到这个理想。
假设我们有一个分支,另一个分支从中间生长出来,或者两个分支共享一些共同的基础:
C--D <-- br1 (HEAD)
/
...--A--B
E--F--G <-- br2
(在这里,我使用了一个字母的名称来提交,而不是大而丑陋的哈希ID,因此我们在提交26次后就会用完字母。 进一步假设您已经运行了git checkout br1
- 这会将您的HEAD
附加到br1
,这就是我们以这种方式绘制它的原因 - 此时您意识到,如果您可以br1
抓住您之前所做的相同更改以使提交F
,事情会好得多。
git cherry-pick
命令将执行此操作。 它将检查F
与E
相比,看看发生了什么变化。 然后它将(尝试)对提交的内容进行相同的更改D
,我们现在所处的位置。 最后,如果一切顺利,它将从结果中进行新的提交。 这个新提交很像F
除了:
- 它的父级是
D
,而不是E
,并且 - 虽然它做了与
F
相同的更改,但它将它们用于D
中的任何内容,而不是E
中的任何内容。
换句话说,git show <new-copy>
将显示与git show <commit-F>
相同的更改,即使应用这些更改的基础可能不同。
因为这是F
的副本,我们称之为F'
而不是H
:
C--D--F' <-- br1 (HEAD)
/
...--A--B
E--F--G <-- br2
这就是樱桃选择是什么/做什么:它具有复制提交的效果。
变基
有几种不同的变基用例,但它们都建立在这样一个基本思想之上:我们可以复制提交。 当我们进行复制时,就在实际提交(只读提交)之前,我们可以更改有关新副本的某些内容。
第一个常见用例是移植分支。 假设我们没有像以前那样绘制上面的分支 1 和 2,而是像这样绘制它们:
C--D <-- develop (HEAD)
/
...--A--B--E--F--G <-- master
(这实际上是完全相同的图表,我们只是给它不同的分支名称并展平了底行。 现在假设我们希望develop
基于提交G
而不是提交B
。 假设我们要创建一个从G
开始的新的临时分支,并使新分支成为HEAD
:
C--D <-- develop
/
...--A--B--E--F--G <-- tmp (HEAD), master
现在我们在这里挑选C
,制作一个副本C'
,该副本在G
之后进行更新tmp
以指向新副本:
C--D <-- develop
/
...--A--B--E--F--G <-- master
C' <-- tmp (HEAD)
我们对提交D
重复此操作:
C--D <-- develop
/
...--A--B--E--F--G <-- master
C'-D' <-- tmp (HEAD)
最后,我们告诉 Git 从提交D
剥离标签develop
并将其粘贴到提交D'
上,当 Git 使用它时,也扔掉临时名称并再次HEAD
develop
:
C--D [abandoned]
/
...--A--B--E--F--G <-- master
C'-D' <-- develop (HEAD)
原始提交D
不再有名称,所以我们不会看到它,最终(默认情况下在 30 天或更长时间后)Git 会垃圾回收它,它真的会消失。
最后,看起来我们以某种方式移动了提交C
和D
. 我们没有,真的:我们将C
复制到一个新的,略有不同的C'
,并将D
复制到D'
。 但只要没有人记得原始C
和D
,我们不妨移动和更改提交。develop
这个名字现在定位的是提交D'
,而不是D
;只要我们使用名称来查找提交,我们就只能看到闪亮的新替代品。
定期变基
常规变基的简单形式是:
git checkout <somebranch> # first, ensure your HEAD is attached
git rebase <target> # then do the rebase-by-copy thing
此处target
通常是另一个分支名称。 例如,为了实现我们上面绘制的变基,我们将签出develop
并运行git rebase master
。master
部分告诉 Git 从哪里开始进行复制 - 临时分支在构建时通过提交提交。 但是这里缺少一些重要的东西:git rebase
如何知道要复制哪些提交?
答案在于 Git 使用的一个更通用的技巧。 你会经常看到和使用它,例如,git log
:你告诉它从哪里开始,你也告诉它在哪里停止。 如果通过提交哈希 ID 执行此操作,则可以编写如下内容:
git log master ^1234567
它告诉它从master
的尖端开始,但当它到达提交1234567
时停止,不管那个是什么。 您可以将其编写为:
git log 1234567..master
因为这些意思是一样的:从master
的尖端开始;从1234567
停止。
这里棘手的部分是 Git 不必直接遇到提交1234567
本身。 "stop"指令在到达可从停止点到达的任何提交时停止 Git。 这让我们可以写这样的东西:
git log master..develop
即使master
包含develop
没有的提交。
在我们的例子中,git rebase
使用这种双点表示法从复制过程中排除B
及更早的提交。 (引擎盖下要复杂得多,但这一切都源于这个想法。 也就是说,我们让 Git 使用单个名称选择要复制的内容和放置副本的位置,master
:副本在当前master
提示之后,并复制可从HEAD
访问但无法从master
的提示访问的提交。
如有必要,您可以拆分这两个部分,有时这是必要的:您可以使用git rebase --onto <target> <stop-at>
表示副本应该在target
标识的提交之后,但在提交stop-at
停止时应该从HEAD
中获取提交(或从该点访问)。 这使您可以获取如下所示的图表:
C--D <-- important-fix
/
A--B--E <-- feature1
/
...--o--F--G--H--I <-- mainline
并告诉 Git 只复制C
和D
去追mainline
:
$ git checkout important-fix
$ git rebase --onto mainline feature1
Git 将列出HEAD
akaimportant-fix
上无法从feature1
访问的提交(因此只是C--D
)。 这些将是复制的提交;他们将在提交I
(mainline
)之后进行。 结果将是:
C--D [abandoned]
/
A--B--E <-- feature1
/
...--o--F--G--H--I <-- mainline
C'-D' <-- important-fix
这里还有一点值得注意:变基过程留下了原始的提交链。您始终可以撤消变基,直到原始链最终被垃圾收集。 如果您想尝试变基,这将特别有用。
交互式变基
使用git rebase -i
我们可以告诉 Git 复制提交(与其他变基一样),但暂停并让我们进行更改,或者在制作我们应该使用的最终更新、更闪亮的副本之前合并几个现有的提交而不是原始副本。
交互式变基需要与非交互式变基相同的输入:
- 它应该复制哪些提交?
- 提交应该去哪里?
主要区别在于,在列出要复制的提交后,它会将该列表写入包含指令的文件中。 要制作的每个副本都将列为pick
操作:对该提交进行挑选。 您可以更改列表! 当你完成对列表的更改后,无论以何种方式,你写出操作列表,然后 Git 执行它们。
要复制的提交集是写出后列表中的任何内容。 如果需要,您甚至可以添加到列表中。 "Pick"意味着做一个樱桃选择,而"squash"或"fixup"表示在不完全提交的情况下执行前面的樱桃选择步骤(见git cherry-pick -n
),然后再挑选一些,然后才提交。 "编辑"的意思是挑选樱桃,但停止修改。 "Drop"与注释掉或删除"pick"行的含义相同:不要对提交做任何事情。 (其中大多数依赖于我在这里故意掩盖的其他一些特殊技巧;这只是一般的想法。
请注意,在所有情况下,您都在构建线性提交链
Git 的rebase
命令总是一次构建一个新的提交链,就像运行git cherry-pick
一样。 在某些情况下,例如交互式变基,Git 确实运行git cherry-pick
。 这其中有一件非常重要的事情,那就是:很难挑选合并。 结果,git rebase
甚至没有尝试:
D--E
/
A--B G--H <-- feature
/ /
/ C--F
/
...--o--o--o--o <-- mainline
如果运行git checkout feature; git rebase mainline
,Git 必须选择要复制的提交,然后复制它们。 Git 将选择的提交是A
、B
、C
和F
、D
和G
以及 ...H
. 它将跳过合并提交G
。 如果一切顺利,副本将如下所示,尽管很难说C'-F'-D'-E'
链实际显示的顺序:
D--E
/
A--B G--H [abandoned]
/ /
/ C--F
/
...--o--o--o--o <-- mainline
A'-B'-D'-E'-C'-F'-H' <-- feature
有一种特殊的变基,git rebase --preserve-merges
,它试图在变基时保留合并。 这在技术上是不可能的;因此,它会重新执行合并。 结果非常棘手,并且不能很好地与交互式变基一起使用。 (换句话说,除非你知道自己在做什么,否则不要使用它。
我喜欢这样说的方式是变基扁平化合并。 在某些情况下,这就是您想要的。 在大多数情况下,事实并非如此。 在交互式变基中,这通常意味着您在列表中为--onto
目标和停止点选择了"太远"的提交,因此它出现在合并之前:
...--o--A--B--C---F--G <-- branch (HEAD)
/
D--E
如果你在这里运行git rebase -i <hash-of-C>
,你会让 Git copy 提交D-E-G
并将它们全部放在C
之后。 您可能只想复制F
并G
. 但是你不能复制F
:这是一个合并提交。
直接使用变基时,有两种方法可以解决此问题:
- 只需复制
G
;F
留在原处。 - 在交互模式下使用合并保留代码(但这很棘手)。
还有第三种方法,即通过创建自己的临时分支来逐个变基。 也就是说,回到你上面学到的方式,在你学会使用git rebase
电动工具之前。 创建一个临时分支并挑选单个提交;当您到达要合并的点时,运行git merge
;然后挑选更多单独的提交。