如何撤销一个使用 git commit --amend 命令的提交

57

我不小心输入了 git commit --amend 命令,这是一个错误,因为我意识到提交实际上是全新的,应该使用新消息进行提交。 我想创建一个新的提交。我该如何撤销之前的命令?


12
git reset --soft @{1} - user4003407
1
git reset --soft @{1} 对我不起作用。我只是恢复了提交,然后压缩它们,解决了这个问题。 - Zhang
这个问题的重复导致了更好的信息,只是说一下。 - Jwan622
1个回答

161

PetSerAl的评论是关键。以下是两个命令序列,可以实现您想要的操作:

git reset --soft @{1}
git commit -C @{1}

描述

当您进行新提交时,Git通常会使用以下事件序列:

  1. 读取当前提交的ID(SHA-1哈希,例如a123456...)(通过HEAD,它给出了当前分支)。 让我们称此ID为C(用于当前)。 请注意,此当前提交具有父提交; 让我们称其ID为P(用于父级)。
  2. 将索引(也称为暂存区)转换为树。 这会产生另一个ID; 让我们称此ID为T(用于树)。
  3. 写入具有parent = C和tree = T的新提交。 此新提交获得另一个ID。 让我们称此为N(用于新建)。
  4. 使用新提交ID N更新分支。

使用--amend时,Git会稍微更改该过程。 它仍然像以前一样编写新提交,但在步骤3中,它不是使用parent = C编写新提交,而是使用parent = P来编写它。

图片

从图示上,我们可以这样绘制发生的情况。 我们从以P--C结尾的提交图开始,由branch指向:

...--P--C   <-- branch

当我们进行新提交N时,我们会得到:

...--P--C--N   <-- branch

当我们使用--amend时,我们得到以下结果:
       C
      /
...--P--N   <-- branch

请注意,提交 C 仍然在存储库中;它只是被推到一边,以便新提交 N 可以指向旧的父提交 P
目标
在执行 git commit --amend 后,您意识到想要的是让提交链看起来像这样:
...--P--C--N   <-- branch

我们不能完全做到这一点 - 我们无法更改 N;Git永远不会更改存储在仓库中的任何提交(或任何其他对象) - 但请注意,...--P--C链仍然完整存在。您可以通过reflogs找到提交C,这就是@{1}语法所做的事情。(具体来说,这是currentbranch@{1}的缩写,2意味着“currentbranch指向一步之前”,即“指向提交C”)。因此,我们现在运行git reset --soft @{1},它会执行以下操作:
       C   <-- branch
      /
...--P--N

现在,branch 指向 C,而 C 又指回 PN 发生了什么? 和 C 之前发生的一样: 它经过 reflog 的保存一段时间。
虽然我们不是真的需要它 (尽管它可能有用),但是 git reset 命令的 --soft 标志保持了索引/暂存区不变 (以及工作树)。这意味着我们现在可以再次创建一个新提交,只需运行另一个 git commit 命令即可。它将经历相同的四个步骤(从 HEAD 中读取 ID、创建树、创建新提交并更新分支):
       C--N2   <-- branch
      /
...--P--N

其中N2将成为我们的新提交。

我们甚至可以让git commit重新使用提交N的提交消息。 git commit命令有一个--reuse-message参数,也可以拼写为-C;我们所要做的就是给它一些信息,让它找到原始的新提交N,从中复制消息,以此创建N2。 那么我们该如何做呢? 答案是:它在reflog中,就像我们需要执行git reset时的C一样。

事实上,它是同一个@{1}

请记住,@{1}表示“刚才它在哪里”,而git reset只是更新了它,将其从C移动到N。 我们还没有创建新的提交N2。(一旦我们这样做,N将变为@{2},但我们尚未这样做。)

因此,综合起来,我们得到:

git reset --soft @{1}
git commit -C @{1}

1当你修改合并时,当你处于分离的HEAD状态时,以及当你使用替代索引时,此描述可能会失效。即使在这种情况下,修改描述也是非常明显的。

2如果HEAD是分离的,那么就没有当前分支,意思变成了HEAD@{1}。请注意,单独的@表示HEAD,因此@{n}指的是当前分支,而不是HEAD本身,这有点不一致。

为了看到它们之间的区别,请考虑执行git checkout develop,然后执行git checkout master(假设两个分支都存在)。第一个checkoutHEAD更改为指向develop,第二个将HEAD更改为指向master。这意味着master@{1}是最后更新master之前master所指向的任何提交;但HEAD@{1}develop现在指向的提交-可能是其他提交。

(总结:在这两个git checkout命令之后,@{1}现在表示master@{1}HEAD@{1}表示与develop现在相同的提交,@表示HEAD。如果你感到困惑,那么我也是,显然我不是唯一一个,可以看看评论。)


1
据我所知,@{1}currentbranch@{1}的缩写,而不是HEAD@{1} - user4003407
1
@PetSerAl:啊哈,你说得对!虽然如果HEAD是分离的,它会作为“HEAD@{1}”工作。这似乎有点不一致,因为“@”表示“HEAD”,但我想如果根本没有当前分支,“HEAD@{0}”和“currentbranch@{0}”必须是同义词。谢谢,我会修正答案的。 - torek
@andrybak: 通过多次移动“HEAD”和/或一些分支,分离“HEAD”,再移动它并重新附加它来测试它。语义比语法更复杂。 - torek
1
@andrybak 即使 HEAD 是符号引用,@{1}@@{1} 仍有不同的含义。 - user4003407
2
@8bitjunkie:不要盲目地使用@{1},因为如果你在git commit --amend之后做了任何更改,它将不再是1。如有需要,请运行git reflog以查找正确的哈希ID或数字以供@{...}语法使用。 - torek
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接