【问题标题】:Revert a commit on remote branch恢复远程分支上的提交
【发布时间】:2025-12-11 08:30:02
【问题描述】:

我在本地分支上工作过,添加了许多提交。
然后我把它推到了remote staging 分支。

现在我必须撤消已推送到 remote staging 的最后一次提交,即我的本地分支与 remote staging 的合并

我从其他答案中了解到的是,我必须使用 revert 而不是 reset 才能以一种干净的方式进行操作,不是吗?

所以我要做的是:

  1. 创建一个名为 cleaning 的新本地分支,以 master 作为父分支(在暂存之后)
  2. 将遥控器staging拉到清洁中
  3. 使用git revert {last good commit hash in staging}
  4. 现在 cleaning 应该处于良好的提交状态,并且在我的错误推送之前处于 remote staging 的相同状态,不是吗?
  5. 现在我应该将cleaning 推入remote staging 以恢复远程分支。使用哪些标志?

我说的对吗?因为git status 在第 4 点告诉我我与staging 保持同步

【问题讨论】:

  • 几点:您只需要远程分支的本地副本;它与master 的关系并不真正相关。 git checkout -b cleaning remote/staging 应该足以替换 1) 和 2)。其次,git revert 将您想要 撤消 的提交作为参数,而不是您想要保留的最后一个提交。我想你想要像go revert {last good commit}..HEAD 这样的东西来撤消所有最后一次好的提交之后。

标签: git


【解决方案1】:

不要把它弄复杂。

首先你需要做一个git log 来找出你想要恢复的提交ID。例如它是提交abc123。如果您知道它是最后一个,则可以使用特殊标识符“HEAD”。

然后您首先在本地“暂存”分支中将其还原:

git checkout staging
git revert abc123

注意:对于日志中的最后一次提交,您应该写 git revert HEAD

然后你更新你的远程“登台”:

git push

解释:在 git 中,如果你有一个远程,你正在处理 2 个不同的存储库(本地和远程)。在您执行git checkout staging 之后,您实际上创建了一个代表远程分支的不同本地名称。 git revert 实际上不会删除您的提交,但它会在顶部创建一个新提交,这会撤消所有更改(如果您添加了一个文件 - 新提交将删除它,如果您删除了一行 - 新提交将添加它返回等),即它是对日志的补充,这就是推送将是干净的原因。

如果你真的想让它消失,这样没有人可以责怪你,你可以自己承担风险:

git checkout staging
git reset --hard HEAD^
git push -f

(重置行重新定位本地“暂存”分支,使其指向在您的最高提交之前的提交)

一般来说,强制推送是一种不好的做法,但保留无用的提交和恢复也不是很好,因此另一种解决方案是实际创建一个新分支“staging2”并在该分支中进行测试:

git checkout staging
git checkout -b staging2
git reset --hard HEAD^
git push

【讨论】:

  • OP 正在尝试恢复推送的合并,因此简单的恢复可能不够。你需要-m
  • 在不更改历史记录的情况下还原更改通常是最好的方法!出于某种原因,你真的想改变历史,它不仅仅是revert :)
【解决方案2】:

TL;DR

请记住,git revert 的真正含义是取消更改恢复到(即恢复)某个特定版本。可以实现revert to / restore,但是执行此操作的命令是git checkoutgit read-tree(由于不同的原因,这两个命令都比较棘手)。请参阅 How to revert Git repository to a previous commit?Rollback to an old Git commit in a public repo(特别是 jthill's answer)。

这里唯一真正棘手的部分是“恢复合并”。这相当于在合并提交上使用git revert -m 1,这很容易运行;但这意味着之后,您无法重新合并,因为 Git 非常确定(并且正确)您已经合并了所有这些工作并且正确的结果已经到位(就是这样,您只是后来解开了)。要将其全部还原,您可以还原还原。

Reverting 会创建一个新的提交,就像每次提交一样,只是将一个新的提交添加到当前分支。您像往常一样在自己的存储库中执行此操作。剩下的工作就是将新提交推送到某个 other Git 存储库,将其添加到另一个 Git 的集合中,作为 它们的 分支之一的新提示。

很长(涉及很多细节)

首先,让我们稍微备份一下,因为短语 remote branch 没有任何意义,或者更确切地说,对于太多不同的人来说,意味着太多不同的东西——无论哪种情况,最终都会导致失败交流。

Git 确实有分支,但即使是“分支”这个词也是模棱两可的(参见 What exactly do we mean by "branch"?)。我发现最好具体一点:我们有 分支名称,例如 masterstaging,并且根据您的建议,cleaning。 Git 也有 remote-tracking 名称remote-tracking 分支名称,例如 origin/staging,但令人困惑的是,Git 将这些名称存储在 本地,即在您自己的存储库中(仅限)。你的 Git 使用 your origin/staging 来记住你的 Git 在他们的 Git 上看到了什么,当你的 Git 上次与他们的 Git 交谈并询问他们一些关于 他们的 staging.

因此,问题的根源在于您的 Git 存储库是您的,但是还有第二个 Git 存储库不是您的,而您最终希望在那个 other Git 存储库上做某事。这里的限制是你的 Git 只允许你在 你的 存储库中做这些事情,之后你最终将运行git pushgit push 步骤会将一些提交或提交从您的 Git 存储库转移到他们的 Git 存储库。这时候,你可以让他们的Git设置他们的名字——他们的staging——他们要么会说是的,我已经设置了(你的 Git 现在将更新你的 origin/staging 以记住这一点),或者不,我拒绝设置它,原因如下:_____(在此处插入原因) .

因此,您要在 您的 存储库中执行的操作是设置适当的步骤,以便 他们的 Git,在 他们的 存储库中,将接受您git push 的提交并将更新他们的staging。在我们完成这些步骤时请记住这一点。有多种方法可以做到这一点,但这里是我会使用的方法。

  1. 运行git fetch。此命令始终可以安全运行。如果你有多个遥控器,你可以给它一个特定遥控器的名称,但大多数人只有一个,命名为origin,如果你只有一个,则无需命名。

    (远程的名称——origin——主要是拼写你的计算机应该用来访问其他 Git 存储库的 URL 的一种简写方式。)

    这个git fetch 可能什么都不做,或者可能会更新您的一些远程跟踪 (origin/*) 名称。 git fetch 所做的是调用另一个 Git,从中获取所有分支的列表以及与它们相关的提交哈希,然后带来他们拥有的任何你没有的提交。您的origin/staging 现在记住了他们的staging。您现在可以为此添加新的提交。

  2. 既然您的 origin/staging 与其他 Git 的 staging 同步,请根据需要创建或更新本地分支名称。此处使用的最佳名称是staging,但如果您愿意,也可以使用cleaning。因为这一步是创建或更新,所以有子步骤:

    • 如果是“创建”,则可以使用git checkout -b staging origin/staging新建一个staging,其上游为origin/staging

      您可以将其缩短为 git checkout staging,因为您没有自己的 staging,它将搜索您的所有 origin/* 名称(以及任何其他远程跟踪名称,如果您有超过一个遥控器)来查找匹配的。然后它就像您使用了更长的命令一样。 (只有一个遥控器,唯一可以匹配的名称是origin/staging。如果你同时拥有originxyzzy 作为遥控器,你可以同时拥有origin/stagingxyzzy/staging;那么你需要更长的时间命令。)

    • 或者,如果是“更新”,您将已经有一个 staging 已经将 origin/staging 设置为其上游,因为您之前已经这样做了。在这种情况下,只需运行:

      git checkout staging
      git merge --ff-only origin/staging
      

      让您的登台与origin/staging 重新同步。如果快进合并失败,你有一些提交,他们没有,你将需要更复杂的东西,但我们在这里假设这会成功。

      您也可以稍微缩写这些命令,但我将在此处将它们拼写出来。请注意,第一个命令与上述第一种情况的简短版本相同,您可以通过git checkout staging 的输出判断发生了哪个命令。 (我会留下其他问题或练习的详细信息。)

    我们可以在您自己的存储库中绘制您现在拥有的图片,它看起来像这样:

    ...--o--o--o---M   <-- staging (HEAD), origin/staging
             \    /
              o--o   <-- feature/whatever
    

    每一轮o 代表一个提交。 M 代表合并提交,它的结果你不喜欢,但它也存在于另一个 Git 中 origin他们的 名称 staging,这就是为什么你自己的 Git 有名称origin/staging 指向提交M

  3. 您现在想要创建一个撤消错误提交的提交。这可能会使用git revert,但请记住,revert 意味着撤消退出,而不是切换到旧版本。你告诉 Git 撤消哪个提交,Git 通过弄清楚你做了什么来撤消它,然后做相反的事情。

    例如,如果您说要恢复的提交显示“删除文件 README”,则更改将包括“将文件 README 恢复为删除时的形式”。如果您说要还原的提交显示“将此行添加到 Documentation/doc.txt”,则更改将包括“从 Documentation/doc.txt 中删除该行”。如果您说要恢复的提交在某个第三个文件中显示“将您好为再见”,那么恢复将做的更改是将第三个文件中的“再见”更改为“您好”,在同一行(找到一些魔法如果它移动了线)。

    这意味着git revert 可以撤消任何提交,即使它不是最新的提交。但是,要这样做,它必须将该提交与其直接父级进行比较。如果您尝试恢复的提交是 merge 提交,它有多个父级,您需要指定 Git 应该使用哪个父级。

    要使用的正确父级并不总是很明显。但是,对于大多数合并,它只是“父编号 1”。这是因为 Git 特别强调了合并的 first 父级:当您运行 git merge 时,它是 HEAD 的提交。所以这就是合并带来的一切,但还没有出现。

    git revert 成功时,它会进行一个新的提交,以撤消合并的效果:

                     W   <-- staging (HEAD)
                    /
    ...--o--o--o---M   <-- origin/staging
             \    /
              o--o   <-- feature/whatever
    

    这里,W 代表这个新的提交:它是 M 颠倒过来的。你现在要做的就是运行 git push origin staging 将你自己的新提交 W 发送到另一个 Git:

  4. git push origin staging:这会调用另一个 Git 并提供它提交 W——这是我们拥有但他们没有的每一个提交;他们有M 和之前的所有内容(左侧),但没有W

    只要没有特别的限制,他们就会接受这个新的提交,并改变他们的 staging 指向新的提交W。您的 Git 会记住更改:

                     W   <-- staging (HEAD), origin/staging
                    /
    ...--o--o--o---M
             \    /
              o--o   <-- feature/whatever
    

    (没有必要在单独的行上继续绘制W,但我在这里使用复制粘贴来保持形状不变。)

如您所见,您现在已经完成了。您和他们都同意您和他们的staging 都应该指向具有撤消提交M 的效果的提交W。如果您愿意,现在可以安全地删除您自己的 staging 名称:

git checkout <something-else>
git branch -d staging

产生:

...--o--o--o---M--W   <-- origin/staging
         \    /
          o--o   <-- feature/whatever

【讨论】: