如何还原多个git commit?


981

我有一个git存储库,看起来像这样:

A -> B -> C -> D -> HEAD

我希望分支的头部指向A,即我希望B,C,D和HEAD消失,并且我希望head与A成为同义词。

听起来我可以尝试进行基准调整(由于我已在两者之间进行了更改,因此不适用)或恢复。但是,如何还原多个提交?我一次还原一次吗?订单重要吗?


3
如果您只想重置遥控器,则可以用任何东西来破坏它!但是让我们使用前面的第四个提交:(git push -f HEAD~4:master假设远程分支是master)。是的,您可以像这样推送任何提交。
u0b34a0f6ae

21
如果有人撤回,您必须使用进行还原更改的提交git revert
2009年

1
使用git显示HEAD〜4,以确保您推到正确的远程
马特·弗里曼


5
“订单重要吗?” 是的,如果提交会影响相同文件中的相同行。然后,您应该开始还原最近的提交,并以自己的方式工作。
avandeursen

Answers:


1332

扩展我在评论中写的内容

一般规则是,您不应重写(更改)已发布的历史记录,因为有人可能会基于此记录进行工作。如果重写(更改)历史记录,则合并它们的更改和对其进行更新时会遇到问题。

因此,解决方案是创建一个新的提交,该提交还原您要删除的更改。您可以使用git revert命令执行此操作。

您有以下情况:

A <-B <-C <-D <-主<-头

(这里的箭头指的是指针的方向:在提交的情况下是“父”引用,在分支头的情况下是顶级提交(分支ref),在HEAD的情况下是分支的名称)。

您需要创建以下内容:

A <-B <-C <-D <-[(BCD)^-1] <-主控<-头

其中“ [[(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"

另一种解决方案是检出 提交A的内容,并提交以下状态:

$ git checkout -f A -- .
$ git commit -a

那么您将遇到以下情况:

A <-B <-C <-D <-A'<-主<-头

提交A'与提交A具有相同的内容,但是是不同的提交(提交消息,父母,提交日期)。

Jeff Ferland解决方案(由Charles Bailey修改)基于相同的想法,但是使用git reset

$ git reset --hard A
$ git reset --soft @{1}  # (or ORIG_HEAD), which is D
$ git commit

40
如果您在B,C或D中添加了文件。git checkout -f A -- .将不会删除这些文件,则必须手动执行。我现在应用这个策略,感谢的Jakub
OMA

18
这些解决方案不是等效的。第一个不会删除新创建的文件。
m33lky 2012年

10
@Jerry:git checkout foo可能意味着签出分支 foo(切换到分支)或签出文件 foo(来自索引)。--用于消除歧义,例如git checkout -- foo始终关于文件。
JakubNarębski2013年

87
除了很好的答案。这个速记对我git revert --no-commit D C B
有用

9
@ welldan97:感谢您的评论。在编写此答案git revert时,不接受多次提交;这是相当新的功能。
2013年

246

我觉得有用的干净方式

git revert --no-commit HEAD~3..

此命令仅还原一次提交即可还原最近的3次提交。

也不会重写历史记录。


16
这是最简单,最好的答案
Julien Deniau

3
@JohnLittle它负责更改。git commit从那里实际上将进行提交。
x1a4

14
如果某些提交是合并提交,则将无法使用。
MegaManX

5
最后的两个点是做什么的?
cardamom

5
@cardamom那些指定范围。HEAD~3..HEAD~3..HEAD
Toine H '18

238

为此,您只需要使用revert命令,指定要还原的提交范围。

考虑到您的示例,您必须执行以下操作(假设您位于分支“ master”上):

git revert master~3..master

这将使用B,C和D的反向提交在您的本地创建一个新的提交(这意味着它将撤消这些提交引起的更改):

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

129
git revert --no-commit HEAD~2..是一种比较惯用的方式。如果您在master分支上,则无需再次指定master。该--no-commit选项让git尝试一次还原所有提交,而不是用多条revert commit ...消息乱写历史记录(假设这就是您想要的)。
kubi

6
@Victor我确定了您的提交范围。该范围的开头是互斥的,表示不包括在内。因此,如果要还原最后3次提交,则需要从第3次提交的父级(即)开始范围master~3

2
@kubi是否无法使用单个提交(您的方法,而不必手动输入已还原的提交)将SHA包含在提交消息中?
克里斯·S

@ChrisS我的第一个想法是不使用--no-commit(因此您为每个还原都提交一个单独的提交),然后将它们全部压缩到一个交互式的rebase中。合并的提交消息将包含所有SHA,您可以使用自己喜欢的提交消息编辑器来排列它们。
Radon Rosborough

71

与Jakub的答案类似,这使您可以轻松选择要还原的连续提交。

# revert all commits from B to HEAD, inclusively
$ git revert --no-commit B..HEAD  
$ git commit -m 'message'

9
您的解决方案对我来说效果很好,但稍作修改。如果我们有这种情况Z-> A-> B-> C-> D-> HEAD并且如果我想返回到A状态,那么很奇怪,我将不得不执行git revert --no-commit Z.。 HEAD
Bogdan

3
同意@Bogdan,还原范围如下:SHA_TO_REVERT_TO..HEAD
Vadym Tyemirov

11
范围错误。应该为B^..HEAD,否则B被排除。
tessus

3
同意@tessus,所以正确的做法是:git revert --no-commit B^..HEADgit revert --no-commit A..HEAD
Yoho

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

这将立即使所有这些恢复。给一个好的提交信息。


4
如果他想HEAD看起来像,A那么他可能希望索引匹配,所以git reset --soft D可能更合适。
CB Bailey 2009年

2
--soft重置不会移动索引,因此当他提交时,看起来提交直接来自a而不是来自D。这将使分支分裂。--mixed保留更改,但移动索引指针,因此D将成为父提交。
杰夫·弗兰

5
是的,我认为git reset --keep就是我上面的内容。它是在2010年4月发布的1.7.1版本中发布的,因此当时还没有答案。
杰夫·弗兰

git checkout A那么git commit以上对我没有用,但是这个答案确实有用。
SimplGy 2013年

为什么git reset --mixed D需要?具体为什么reset?是否因为没有重置为D,HEAD会指向A,导致B,C和D被“悬挂”并被垃圾回收-这不是他想要的吗?但是那为什么--mixed呢?您已经回答“ --soft重置不会移动索引...”,因此,通过移动索引,这意味着索引将包含D的更改,而工作目录将包含A的更改-这样a git statusgit diff(将索引[D]与工作目录[A])将显示该内容;该用户从D转到A?
红豌豆

39

首先,请确保您的工作副本未被修改。然后:

git diff HEAD commit_sha_you_want_to_revert_to | git apply

然后提交。不要忘记记录恢复的原因。


1
为我工作!功能分支出现了一个问题,使在developer分支中所做的更改过时了(修复了一些错误),因此Feature分支必须覆盖在develop中所做的所有更改(包括删除一些文件)。
沉默者

1
无法使用二进制文件:error: cannot apply binary patch to 'some/image.png' without full index line error: some/image.png: patch does not apply
GabLeRoux

2
这是比接受的答案灵活得多的解决方案。谢谢!

2
git diff --binary HEAD commit_sha_you_want_to_revert_to | git apply
回复

2
即使您想要还原包含合并提交的一系列提交,这也将起作用。使用时,git revert A..Z您会得到error: commit X is a merge but no -m option was given.
Juliusz Gonera '18

35

我很沮丧,不能仅仅回答这个问题。其他所有问题都与如何正确还原和保存历史有关。这个问题说:“我希望分支的头部指向A,即我希望B,C,D和HEAD 消失,并且我希望head与A成为同义词。”

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

我在阅读Jakub的文章时学到了很多东西,但是公司中的某个人(可以在没有Pull-Request的情况下访问我们的“测试”分支)可以像5个错误的提交一样试图修复并修复和纠正他5个提交之前的错误。不仅如此,还接受了一个或两个“拉取请求”,这很糟糕。算了吧,我找到了最后一个好的提交(abc1234),然后运行了基本脚本:

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

我告诉这个仓库中的其他5个人,他们最好记下最近几个小时的更改以及最新测试中的擦除/重新分支。故事的结局。


2
我没有发布提交,所以这是我需要的答案。谢谢,@ Suamere。
汤姆·巴伦

更好的方法是git push --force-with-lease,只有在进行汽化的提交之后或范围内没有其他人提交到分支时,才会重写历史记录。如果其他人使用了该分支,则永远不要重写其历史记录,而应该简单地将提交还原。
frandroid

1
@frandroid“历史记录永远都不能重写”,只有西斯绝对。这个线程的问题以及我的回答的重点就是,对于特定的情况,应该清除所有历史记录。
Suamere

@Suamere当然,这就是问题。但是,正如您的答案提到您必须告诉其他人的内容一样,存在潜在的麻烦。从个人经验来看,如果其他人在您尝试擦除的内容后做出了承诺,则push -f可能会使您的代码库混乱。--force-with-lease达到相同的结果,不同之处在于,如果您要弄乱仓库,它可以节省您的资金。为什么要抓住机会?如果--force-with-lease失败,您可以查看哪个提交方式受阻,正确评估,调整并重试。
frandroid

1
@Suamere谢谢!我同意这个问题明确指出它想重写历史。我和您的处境相同,我猜是在OP中,有人偶然(在我度假时)进行了数十次丑陋的还原,奇怪的提交和还原,并且状态需要还原。无论如何,如果有良好的健康警告,这应该是公认的答案。
李理查森

9

这是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 '16

3

在共享存储库上还原一组提交的简单方法(人们使用并且您想保留历史记录)是git revert与git结合使用rev-list。后者将为您提供一份提交列表,前者将自己进行还原。

有两种方法可以做到这一点。如果要在单个提交中还原多个提交,请使用:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert -n $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

刚用这个。谢谢!
Ran Biron

2

这些都不对我有用,所以我要还原三个提交(最后三个提交),所以我做了:

git revert HEAD
git revert HEAD~2
git revert HEAD~4
git rebase -i HEAD~3 # pick, squash, squash

像魅力一样工作:)


2
仅当尚未推送您的更改时,这才是可行的选择。
kboom

0

我认为一种非常简单,干净的方法可能是:

回到A

git checkout -f A

将主人的头转到当前状态

git symbolic-ref HEAD refs/heads/master

git commit

1
您能解释一下拒绝投票的原因吗?
nulll

这很好。对这个有用的答案投反对票是什么意思,或者谁能解释什么是最佳实践?
Levent Divilioglu

一样git checkout master; git reset --hard A吗?否则,您能否进一步解释一下该功能?
MM

只是工作,而是象征性的,裁判似乎HEAD不会是一个“安全”的命令
塞尔吉奥

犯错我不想修复高手,我要解决的一个分支
塞尔吉奥

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.