在 Git 中撤销一系列已推送的合并和提交(而不重写历史记录)

31

背景

我的一个队友错误地将一些提交推送到了我们的主开发分支。我们是一个小型的面对面团队。我们的远程仓库托管在内部服务器上。

以下是我们提交记录的顶部(所有这些提交已经被推送):

$ git log develop -6 --pretty=oneline --abbrev-commit
faada93 Merge branch 'develop' of <our_repo_path>.git
244d174 Support classes again
a97a877 Pruned all unused references (again).
8c29252 Merge branch 'develop' of <our_repo_path>.git
a78b993 Support models & methods - product types & categories
da8b496 Resolved JIRA issue PPF-182

da8b496是我们想要保留在develop分支中的最后一个提交,所以我们需要撤销最后5次提交。我们从8c29252创建了一个新分支以在“功能分支”中继续工作。

我尝试了很多事情,参考了这个答案Linus的这篇文章,最终在我的终端历史记录中做了下面你可以看到的操作。但我不确定我最终做的是否是“正确的方法”。我找到的信息非常复杂,我无法为这个特定的问题找到“最佳解决方案”。

问题

我选择的方法(请见下文)是撤销这5个提交而不会损害我们的历史记录的好方法吗?是否有更简单或“更正确”的方法来完成同样的事情?

除其他事项外,我还考虑从da8b496创建一个新分支(git checkout -b new-develop da8b496),并放弃当前的develop分支,但那样做感觉不太对。


我最终做的事情(细节)

首先,我为提交a78b9938c29252创建了一个新分支,因为这些提交包含我们想要保留并最终合并回我们的主开发分支的工作。

$ git checkout -b new-feature-brach 8c29252

然后,我开始在我们的开发分支中还原有问题的提交。

我首先尝试了这个方法,但它没有起作用(可能是因为某些提交是合并提交):

$ git revert a78b993..HEAD
error: a cherry-pick or revert is already in progress
hint: try "git cherry-pick (--continue | --quit | --abort)"
fatal: revert failed

所以...我手动逐个还原每个提交:

$ git revert -m 1 faada93
[develop 40965a5] Revert "Merge branch 'develop' of <our_repo_path>.git"
8 files changed, 167 insertions(+), 3 deletions(-)

$ git revert 244d174
[develop 3cebd68] Revert "Support classes again"
45 files changed, 557 insertions(+), 1572 deletions(-)
(list of affected files)

$ git revert a97a877
error: could not revert a97a877... Pruned all unused references (again).
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'

$ git mergetool
Merging:
exampleFile1.cs
exampleFile2.cs

Deleted merge conflict for 'exampleFile1.cs':
{local}: deleted
{remote}: modified file
Use (m)odified or (d)eleted file, or (a)bort? m

Deleted merge conflict for 'exampleFile2.cs':
{local}: deleted
{remote}: modified file
Use (m)odified or (d)eleted file, or (a)bort? m

$ git commit -m "Adding files to be reverted along with the next commit."
[develop 15bc02b] Adding files to be able to revert the next commit in line.
2 files changed, 239 insertions(+)
(list of affected files here)

$ git revert -m 1 8c29252
# On branch develop
# Your branch is ahead of 'origin/develop' by 3 commits.
#   (use "git push" to publish your local commits)
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       exampleFile1.cs.orig
#       exampleFile2.cs.orig
nothing added to commit but untracked files present (use "git add" to track)

$ git revert a78b993
[develop 841e77c] Revert "Support models & methods - product types & categories"
2 files changed, 239 deletions(-)
(list of affected files here)

所有还原完成后的提交日志:

$ git log develop -10 --pretty=oneline --abbrev-commit
841e77c Revert "Support models & methods - product types & categories"
15bc02b Adding files to be able to revert the next commit in line.
3cebd68 Revert "Support classes again"
40965a5 Revert "Merge branch 'develop' of <our_repo_path>.git"
faada93 Merge branch 'develop' of <our_repo_path>.git
244d174 Support classes again
a97a877 Pruned all unused references (again).
8c29252 Merge branch 'develop' of <our_repo_path>.git
a78b993 Support models & methods - product types & categories
da8b496 Resolved JIRA issue PPF-182

反转后的图表:

$ git log --graph --oneline -8 develop
* 841e77c Revert "Support models & methods - product types & categories"
* 15bc02b Adding files to be able to revert the next commit in line.
* 3cebd68 Revert "Support classes again"
* 40965a5 Revert "Merge branch 'develop' of <our_repo_path>.git"
*   faada93 Merge branch 'develop' of <our_repo_path>.git
|\
| * a97a877 Pruned all unused references (again).
| *   8c29252 Merge branch 'develop' of <our_repo_path>.git
| |\
| | * da8b496 Resolved JIRA issue PPF-182

我认为看起来是正确的。最后,我删除了一些不想保留的备份文件:

$ git clean -fd
(list of affected files here)

当前状态为干净:

$ git status
# On branch develop
# Your branch is ahead of 'origin/develop' by 4 commits.
#   (use "git push" to publish your local commits)
#
nothing to commit, working directory clean

然后我将所有内容推送回远程:

git push origin develop

请展示 git log --graph --oneline develop 的输出。另外,假设 faada93develop 分支合并的地方,你只需要还原那个提交,所有不属于当前分支的子提交也将被还原,无需还原其他任何提交。你所做的看起来比必要的复杂得多。 - user456814
谢谢你,@Cupcake - 我已经在问题中添加了图表。我在我的问题中犯了一些混淆的错误;我现在会纠正它们。 - leifericf
这个仓库是公共的还是一个由小团队共享的私有仓库?如果是后者,你不想将分支硬重置到合并之前的状态吗?这样做是否有原因呢? - user456814
这是一个私有仓库,托管在内部服务器上,由一个小团队的开发人员使用。如果使用“硬重置”,会不会对我们的分支历史产生不利影响?我们都很新手,只是觉得还原是一种更安全和“更正确”的方法。我们找到的信息最多只能让人感到困惑,因此提出了这个问题 :) - leifericf
如果你想加入我,我们可以实时讨论你的问题,我在Git聊天室里。哦,等等,抱歉,那个房间已经冻结了,让我试试别的东西。 - user456814
显示剩余2条评论
4个回答

15

即使你的历史已经改变,你可以创建分支进行回溯和实验。 Git 意味着永远不用说“你本应该这样做”。 如果你达到了一个更好的现实状态,那就采用它。否则,就抛弃它。

下面的示例将创建新的分支,保持仓库中其他所有内容不变。

方案1:git revert

首先,在您开始冒险的点上创建一个临时分支。

$ git checkout -b tmp-revert faada93

通过指定提交范围,git revert 将撤消多个提交。

$ git revert da8b496..faada93

方案2:git commit-tree

考虑下面来自 《Git 内部原理 —— Git 对象》 的图表,并且是第二版的 Pro Git 一书的第 10.2 节。最上面的提交(“第三次提交”)具有以 1a410e 开头的 SHA1 哈希。在这个历史记录的上下文中,1a410e^{tree} 将解析为 3c4e9c,即紧挨着第三个提交右侧的树对象。

Git Object Graph, Figure 151 from *Pro Git* 来自 Pro Git,第二版的第 151 图。

学习这个模型以了解git如何跟踪内容。创建一个新的第四个提交,其树与第二个提交的(即0155eb)相同,将添加一个新的提交对象,该对象将共享或“指向”现有的树和blob,而不是添加新的重复对象。

继续阅读以了解如何使用git commit-tree执行此低级缝合操作。

首先创建另一个临时分支进行操作。

$ git checkout -b tmp-ctree faada93

此时,您想要创建一个新的提交,其中其树(即提交的代码)与您想要保留的最后一个提交da8b496的树相同。在git中,可以直接寻址该树:da8b496^{tree}

git commit-tree是git中的“plumbing”即低级命令,与之相对应的是git中的“porcelain”。在这种情况下,它提供了精确控制所需结果的方式,可能会感觉笨拙或陌生。

创建一个新的未附加的提交,其树与da8b496的相同,其父(-p)是当前分支的末端faada93。请注意,git commit-tree会在标准输入上读取新提交的提交消息,下面的命令使用echo命令提供该消息。

$ echo Revert back to da8b496 | \
    git commit-tree da8b496^{tree} -p $(git rev-parse tmp-ctree)
new-commit-sha1

上面的斜体部分不是命令的一部分。它表示git commit-tree输出新创建提交的SHA1哈希值。知道新提交的SHA1后,可以将分支移动到该点,例如:

$ git merge new-commit-sha1
在上述命令中,将new-commit-sha1替换为git commit-tree的输出。(你也可以使用git reset --hard new-commit-sha1,但强制重置是一个很锋利的工具,最好避免随意使用。)
您可以将所有内容合并成单个复合命令。
$ git merge --ff-only $(echo Revert back to da8b496 | \
    git commit-tree da8b496^{tree} -p $(git rev-parse tmp-ctree))
--ff-only开关用于git merge,旨在防止意外情况。您的意图是将新提交作为当前分支头的快进或后代,实际上是其直接子代!

清理

要删除上面的临时分支,请切换到另一个分支并执行以下操作,McManus先生。您的其他分支将与您离开时一样。
$ git checkout develop
$ git branch -D tmp-revert tmp-ctree
这两个应该是相同的,您可以使用以下命令进行验证:
$ git diff tmp-revert tmp-ctree
如果要保留其中一个,请将其合并到您的develop分支中。
$ git checkout develop
$ git merge --ff-only tmp-ctree
$ git push origin develop

这似乎比当前版本的Git已经内置的命令所需的要复杂得多。为什么不只使用硬重置或还原呢? - user456814
谢谢你的回答,Greg。在我敢运行它之前,我需要花更多时间来理解它的确切作用。此外,我们有一些基于8c29252的不同分支的工作;如果这有所不同,我已经在我的问题中更新了这些信息。 - leifericf
再次感谢你详细的回答,Greg。我会花些时间研究这些选项,并更深入地了解它们的工作原理,特别是commit-tree。目前我没有足够的时间仔细研究这个问题,因为有一个即将到来的客户演示需要尽快解决,所以我选择了另一个答案。(在完全理解命令的作用之前,我不喜欢运行任何命令。) - leifericf
1
commit-tree 赢了!谢谢。 - Luke Yeager
1
当运行上述git commit-tree命令时,其上面的echo命令非常重要。我尝试跳过它,结果命令似乎一直停滞不动。这是因为它正在等待提交消息,然后按Ctrl+D结束。通过使用echo的结果管道可以跳过此步骤。 - commscheck
为什么这个如此复杂?还原更改应该是版本控制系统的主要功能。如果完成这项任务需要太多手动工作,那么我会说GIT在这项工作上失败了。应该有一个单一的命令:git revert-to <commit>,它可以将存储库恢复到此提交的状态,而不重写历史记录,并让我们暂存这些新更改。 - HamsterWithPitchfork

13

您有一个小的协作团队,因此沟通不是问题。使提交历史记录看起来应该是这样的:

git branch -f develop dab4896
git branch newfeature 8c29252
git push -f origin develop newfeature

并让每个人重新获取。你完成了。

这种错误是重写的原因之一。


谢谢,@jthill。最后一个命令具体是做什么的,需要输入3个分支名称吗?我之前已经从8c29252创建了一个分支,并且其他人已经在该分支上提交并推送了新内容,所以我不想再次重置“newfeature”分支。 - leifericf
谢谢!简洁明了。我执行了 git branch -f develop da8b496git push -f origin develop,然后要求每个人执行 git fetch。效果非常好。 - leifericf
这仅适用于团队在develop分支上没有设置CI/CD的情况。换句话说,如果您的develop分支在该存储库之外使用,请勿使用“强制推送”,否则会出现非常混乱的情况。 - Roeniss

7

我可以建议将这个问题视为这个回答的重复:如何将当前的git分支变成主分支

Jefromi提供了一个很好的解决方案:

[git branch better_branch <last good commit>]
git checkout better_branch
git merge --strategy=ours master    # keep the content of this branch, but record a merge
git checkout master
git merge better_branch             # fast-forward master up to the merge

这绝对是最佳方法。 - Ev3rlasting

0

你正在尝试做的事情非常危险。

确实,你可以撤销并删除已经推送到仓库的提交,但是如果有人已经拉取了你的更改并且他拥有你要删除的提交ID,那么仓库可能会变得“不稳定”,git将无法处理拉取和推送,因为你删除了已从历史记录中删除的提交。

只有在没有人拉取此提交时才执行此操作(撤销和删除提交)。


这就是为什么我最终选择了正常地还原提交。你是在说我选择的方法(手动还原每个提交)很危险吗?我没有删除任何提交;我只是将它们还原了。(不确定我是否理解正确)。 - leifericf

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