Git 变基尝试在目标提交之前发生的变基提交

  • 本文关键字:提交 目标 Git git
  • 更新时间 :
  • 英文 :


我跑了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。 这些对人类不是很有用,所以我们使用masterHEADv2.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命令将执行此操作。 它将检查FE相比,看看发生了什么变化。 然后它将(尝试)对提交的内容进行相同的更改D,我们现在所处的位置。 最后,如果一切顺利,它将从结果中进行新的提交。 这个新提交很像F除了:

  1. 它的父级是D,而不是E,并且
  2. 虽然它做了与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 使用它时,也扔掉临时名称并再次HEADdevelop

C--D   [abandoned]
/
...--A--B--E--F--G   <-- master

C'-D'  <-- develop (HEAD)

原始提交D不再有名称,所以我们不会看到它,最终(默认情况下在 30 天或更长时间后)Git 会垃圾回收它,它真的会消失。

最后,看起来我们以某种方式移动了提交CD. 我们没有,真的:我们将C复制到一个新的,略有不同的C',并将D复制到D'。 但只要没有人记得原始CD,我们不妨移动和更改提交。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 mastermaster部分告诉 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 只复制CD去追mainline

$ git checkout important-fix
$ git rebase --onto mainline feature1

Git 将列出HEADakaimportant-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 将选择的提交是ABCFDG以及 ...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之后。 您可能只想复制FG. 但是你不能复制F:这是一个合并提交。

直接使用变基时,有两种方法可以解决此问题:

  • 只需复制G;F留在原处。
  • 在交互模式下使用合并保留代码(但这很棘手)。

还有第三种方法,即通过创建自己的临时分支来逐个变基。 也就是说,回到你上面学到的方式,在你学会使用git rebase电动工具之前。 创建一个临时分支并挑选单个提交;当您到达要合并的点时,运行git merge;然后挑选更多单独的提交。

最新更新