如何撤销多个Git提交?

1612

我有一个Git仓库,看起来像这样:

A <- B <- C <- D <- HEAD

我希望将分支的头指向A,也就是说,我想让B、C、D和HEAD消失,并且我想让head成为A的同义词。

听起来我可以尝试revert(不适用,因为我已经在中间推送了更改),或者回滚。但是如何回滚多个提交?我一次一个还是一起回滚?顺序重要吗?


5
如果您只是想重置远程仓库,可以使用任何方法进行覆盖!但我们可以使用前四个提交之前的代码:git push -f HEAD~4:master(假设远程分支是master)。是的,您可以像这样推送任何提交。 - u0b34a0f6ae
38
如果有人提交了错误的代码,你可以使用 git revert 命令撤销这些更改。请注意,这不会删除错误代码,而是创建一个新的提交来撤销它们。 - Jakub Narębski
3
使用 git show HEAD~4 命令确保你正在将正确的提交推送到远程仓库。 - Mâtt Frëëman
2
可能是如何在Git中撤消最后一次提交?的重复问题。 - Jim Fell
8
"顺序重要吗?" 如果提交的内容涉及相同文件中的相同行,则是的。那么您应该从最近的提交开始还原,然后逐步回退。 - avandeursen
请注意,git revert将在history中保留介入的工作。使用git checkoutgit reset --hard来清除历史记录:请参见答案https://dev59.com/8HM_5IYBdhLWcg3wPAjU#38317763。 - WestCoastProjects
18个回答

2061

扩展我在评论中写的内容

一般规则是,不应更改已经发布的历史记录,因为可能有人根据它们的工作。如果您重写(更改)历史记录,将会出现合并其更改和更新问题。

因此,解决方案是创建一个新提交,该提交撤消要摆脱的更改。您可以使用git revert命令来执行此操作。

您有以下情况:

A <-- B  <-- C <-- D                                  <-- master <-- HEAD

(箭头指向指针的方向:提交的“父”引用,在分支头(分支ref)的顶部提交,在HEAD引用的分支名称中)。

您需要创建以下内容:

A <-- B  <-- C <-- D <-- [(BCD)-1]                   <-- master <-- HEAD

其中[(BCD)^-1]表示撤消提交B、C、D中的更改的提交。数学告诉我们(BCD)-1 = D-1 C-1 B-1,因此您可以使用以下命令获取所需情况:

$ git revert --no-commit D
$ git revert --no-commit C
$ git revert --no-commit B
$ git commit -m "the commit message for all of them"

对所有内容都有效,除了合并提交。


另一种解决方案是使用git checkout命令检出提交A的内容并提交这个状态。 这种方法也适用于合并提交。但是新增的文件将不会被删除。如果您有任何本地更改,请先使用git stash命令进行保存:

$ git checkout -f A -- . # checkout that revision over the top of local files
$ git commit -a

那么你会得到以下情况:

A <-- B  <-- C <-- D <-- A'                       <-- master <-- HEAD

提交 A' 的内容与提交 A 相同,但是它是一个不同的提交(包括提交信息、父提交和提交日期)。


Jeff Ferland提出的替代解决方案,由Charles Bailey进行修改,基于同样的思路,但使用了git reset。这里稍作修改,这种方法可以适用于所有情况:

$ git reset --hard A
$ git reset --soft D # (or ORIG_HEAD or @{1} [previous location of HEAD]), all of which are D
$ git commit

54
如果您在B、C或D中添加了文件,则 git checkout -f A -- . 不会删除这些文件,您需要手动删除。我现在采用了这种策略,谢谢Jakub。 - oma
21
这些解决方案并不等同。第一个解决方案不会删除新创建的文件。 - m33lky
13
@Jerry:git checkout foo 可以指代切换分支 foo 或者从索引中检出文件 foo。为消除歧义,需要使用 --,例如 git checkout -- foo 总是与文件相关。 - Jakub Narębski
155
除了伟大的答案外,对我来说这个速记法很有用:git revert --no-commit D C B - welldan97
12
感谢 @welldan97 的评论。在撰写这篇答案时,git revert命令还不支持同时撤销多个提交记录;这是一个比较新的更新。 - Jakub Narębski
显示剩余30条评论

705

我发现有用的简洁方法

git revert --no-commit HEAD~3..
git commit -m "your message regarding reverting the multiple commits"

这个命令可以撤销最近的3次提交并只创建一个提交。

同时不会重写历史,因此不需要强制推送。

.. 用于创建一个范围。意思是 HEAD~3..HEAD~3..HEAD 是相同的。


如果没有提交,这怎么能起作用呢?除了上述命令,还需要做什么?在此命令之前/之后需要哪些git命令? - John Little
8
@JohnLittle 它会将更改分阶段。在那里使用 git commit 命令实际上会执行提交操作。 - x1a4
53
如果有些提交是合并提交,那么这将行不通。 - MegaManX
16
这两个点在句末有什么作用? - cardamom
19
@cardamom 这些指定了一个范围。HEAD~3..HEAD~3..HEAD 是相同的。 - Toine H
显示剩余3条评论

284

要实现这个目标,您只需要使用 revert 命令,并指定您想要撤消的提交范围。

根据您的示例,您可以执行以下操作(假设您在 'master' 分支上):

git revert master~3..master

或者git revert B...D 或者 git revert D C B

这将在您的本地创建一个新的提交,该提交会撤销B、C和D所引入的更改(即它会撤消这些提交所做的更改):

A <- B <- C <- D <- BCD' <- HEAD

151
git revert --no-commit HEAD~2.. 是一个更加惯用的方法。如果你在主分支上,不需要再指定主分支。 --no-commit 选项允许Git尝试一次性还原所有提交,而不是在历史记录中留下多个“还原提交…”消息(假设这正是您想要的)。 - kubi
8
@Victor 我已经修正了你的提交范围。这个范围的起始位置是排除在外的,也就是说不包括在内。所以如果你想要撤回最近的3次提交,你需要从第3次提交的父提交开始指定范围,即 master~3 - user456814
3
@kubi,有没有办法在提交消息中包含SHAs,使用单个提交(您的方法,但无需手动输入已还原的提交)? - Chris S
@ChrisS 我的第一反应是不要使用 --no-commit(这样你就可以为每个还原操作获得单独的提交记录),然后在交互式变基中将它们全部压缩在一起。合并后的提交消息将包含所有的 SHA,您可以使用您喜欢的提交消息编辑器来安排它们的顺序。 - Radon Rosborough
果然,如果你不加上 --no-commit 参数,它会一个一个地执行,你需要为每个提交不断地输入 git revert --continue...我本来希望 Git 有一个友好的提交选项,比如“全部撤销并在单个提交中列出所有哈希值”,但看起来并没有 :| - rogerdpack

111
git reset --hard a
git reset --mixed d
git commit

这将一次性为它们所有的版本进行还原。请提供一个良好的提交信息。


11
如果他希望HEAD看起来像A,那么他可能希望索引匹配,因此git reset --soft D可能更合适。 - CB Bailey
2
软重置不会移动索引,因此当他提交时,看起来提交是直接来自a而不是D。这将使分支分开。混合模式保留更改,但移动索引指针,因此D将成为父提交。 - Jeff Ferland
5
是的,我认为git reset --keep正是我之前提到的内容。它在2010年4月发布的1.7.1版本中推出,所以在那个时候还没有这个答案。 - Jeff Ferland
1
“git checkout A” 然后 “git commit” 上面的方法对我不起作用,但这个答案有效。 - SimplGy
3
为什么需要使用 git reset --mixed D?具体来说,为什么要使用 reset 命令?是因为如果不将 HEAD 重置到 D,那么 HEAD 将指向 A,导致 B、C 和 D 变成“悬空”状态并被垃圾回收,这不是用户想要的结果吗?但是为什么要加上 --mixed 参数呢?你已经回答过了,“--soft 重置不会移动索引...”,所以通过移动索引,这意味着索引将包含 D 的更改,而工作目录将包含 A 的更改——这样 git statusgit diff(比较索引 [D] 和工作目录 [A])就会显示出实质内容;也就是说用户正在从 D 回到 A。 - Nate Anderson
显示剩余4条评论

106

Jakub的回答类似,这让您可以轻松选择连续的提交来还原。

# Revert all commits from and including B to HEAD, inclusively
git revert --no-commit B^..HEAD
git commit -m 'message'

12
你的解决方案对我很有帮助,但稍作修改。如果我们有这种情况 Z -> A -> B -> C -> D -> HEAD,如果我想返回到状态A,那么奇怪的是,我必须执行 git revert --no-commit Z..HEAD 。 - Bogdan
3
同意 @Bogdan 的说法,撤销范围如下:SHA_TO_REVERT_TO..HEAD。 - Vadym Tyemirov
16
范围有误,正确应为 B^..HEAD,否则 B 将被排除在外。 - tessus
6
同意@tessus的观点,正确的做法应该是:git revert --no-commit B^..HEADgit revert --no-commit A..HEAD - Yoho

89

我很沮丧,因为这个问题不能简单地回答。每一个其他的问题都是关于如何正确恢复并保留历史记录的。这个问题说:“我希望分支的头指向A,也就是说,我想让B、C、D和HEAD消失,并且我希望头部与A同义。”

git checkout <branch_name>
git reset --hard <commit Hash for A>
git push -f

我在阅读Jakub的帖子时学到了很多,但是公司里有个人(可以直接推送到我们的“测试”分支而不需要发起拉取请求)提交了五个糟糕的提交,试图修复他五个提交之前的错误。不仅如此,还有一两个拉取请求被接受,而这些现在都是错的。所以算了吧,我找到了最后一个好的提交(abc1234),然后只运行了基本脚本:

git checkout testing
git reset --hard abc1234
git push -f

我告诉了这个代码库中另外五个人,他们最好记录下过去几个小时的更改,并从最新的测试中清除/重新分支。故事结束。


6
我还没有发布这些提交,所以这就是我需要的答案。谢谢,@Suamere。 - Tom Barron
5
更好的做法是使用 git push --force-with-lease 命令,它会在且仅在没有其他人在提交范围内或之后对该分支进行过提交时,才重写历史记录。如果其他人已经使用了这个分支,则其历史记录不应被重写,而应该只是可见地还原提交。 - frandroid
4
“历史永远不应该被重写”,只有西斯才会绝对地处理事情。这个线程的问题,以及我的答案的重点,正是对于特定情况,所有历史都应该被抹去。 - Suamere
2
@Suamere 谢谢!我同意这个问题明确表示它想要重写历史。我和你处于同样的情况,猜测那位提问者也是如此,有人在我度假期间不断进行丑陋的还原、奇怪的提交和还原还原操作,现在需要将状态还原。无论如何,在一个良好的健康警告下,这应该是被接受的答案。 - Lee Richardson
1
这就是答案,特别是问题没有说明相关提交是否已经推送到任何远程仓库。 - davidbak
显示剩余4条评论

75

首先确保您的工作副本未被修改。

然后:

git diff --binary HEAD commit_sha_you_want_to_revert_to | git apply

然后就提交吧。不要忘记记录回滚的原因。


1
无法处理二进制文件: 错误:没有完整的索引行,无法将二进制补丁应用于“some/image.png” 错误:some/image.png:补丁不适用 - GabLeRoux
2
这是比被接受的答案更灵活的解决方案。谢谢! - Brian Kung
4
使用 --binary 选项:git diff --binary HEAD commit_sha_you_want_to_revert_to | git apply 该命令用于二进制文件的更改恢复。 - weinerk
2
即使您想还原包含合并提交的一系列提交,此方法也适用。当使用 git revert A..Z 时,您会得到 error: commit X is a merge but no -m option was given. 的错误提示。 - Juliusz Gonera
2
哇,太酷了,正是我在寻找的。所以这是反向差异,然后将这些更改应用于现有代码。非常聪明,谢谢。;) - Somebody
显示剩余4条评论

33

使用 Git 的 restore 命令

你也可以使用 restore 命令:

A <- B <- C <- D <- HEAD

假设你想让HEAD看起来与A完全相同。确保你已经拉取了最新的master,然后创建一个新的分支。

git switch -c feature/flux-capacitor  # synonymous with checkout -b
git restore --source A .
git add .
git commit
git push

restore命令将一切(.)更改为在--source提交时的状态。 然后,您将其提交到本地分支并将其推送到源。 然后,您可以针对它打开PR。

这样做的好处是不会更改其他人可能已经基于工作的历史记录。 它还为未来的用户留下了有用的历史记录。

文档:git restore


1
恢复方法对我来说非常干净简单。 - Liyong Zhou
2
这对我非常有帮助,因为我不想使用删除提交的Reset命令。运行这些命令后,当然只需切换到主分支并合并即可。谢谢。 - Mauro Torres
但是还原更改会更改PR分支,这是不期望的。PR具有注释或备注等源,因此该方法应在同一分支中起作用。 - vgdub
@vgdub,感谢您的评论。Git本身没有PR的概念。PR是GitHub的构造。通过一些创意,您可以像在此列表中提到的任何其他方法一样,在所需的分支上使用“restore”。 - meh

20

这是对Jakub的答案中提供的解决方案之一的扩展。

我遇到了一个情况,需要回滚的提交比较复杂,其中几个提交是合并提交,并且我需要避免重写历史记录。我无法使用一系列git revert命令,因为最终会出现反转更改之间出现冲突的情况。我最终采用了以下步骤。

首先,在将HEAD保留在分支尖端的同时,检出目标提交的内容:

git checkout -f <target-commit> -- .

(这个 -- 确保 <target-commit> 被解释为一次提交而不是一个文件;. 指的是当前目录。)

然后,确定在回滚的提交中添加了哪些文件,因此需要将其删除:

git diff --name-status --cached <target-commit>

新增的文件应该在行首显示"A"字母,且没有其他差异。现在,如果有任何需要删除的文件,请将这些文件标记为待删除状态:

git rm <filespec>[ <filespec> ...]

最后,提交还原:

git commit -m 'revert to <target-commit>'
如果需要的话,请确保我们回到所需的状态:
git diff <target-commit> <current-commit>

不应该有任何差异。


你确定只用分支的尖端就能 git HEAD 吗? - Suamere
2
这对我来说是一个更好的解决方案,因为我的提交中有合并提交。 - sovemp

9

在共享仓库中(人们使用并且您想保留历史记录)撤销一组提交的简单方法是使用git revert与git rev-list结合使用。后者将为您提供一个提交列表,前者将执行还原操作。

有两种方法可以实现这一点。如果您想要在单个提交中撤销多个提交,请使用:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert --no-commit $i; done

这将撤销您需要的一组提交,但保留您工作树上的所有更改。您应该像往常一样提交它们。另一个选项是每个撤销更改都有一个单独的提交:
for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert --no-edit -s $i; done

例如,如果您有一个类似于提交树的结构:
 o---o---o---o---o---o--->    
fff eee ddd ccc bbb aaa

要将更改从eee还原为bbb,请运行
for i in `git rev-list eee^..bbb`; do git revert --no-edit -s $i; done

只是补充一下: <first-commit-sha> ^ .. <last-commit-sha>包含范围。此外,非常好的答案。在 Git Bash 中它对我起作用了。 - P D
这通常不起作用:错误:提交XXX是合并,但未给出-m选项。 致命错误:还原失败。我开始认为最好的一般方法是git checkout <first commit>然后git archive,然后手动创建一个解压归档版本的提交... - GACy20

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