有没有办法撤销 git push -f 命令?

21

如果我使用 push -f 命令覆盖了远程仓库上的最后一次提交,即 A 提交,并且其他人在我推送之前已经拉取了 A 提交,那么有没有办法撤销这个操作,以免给别人带来麻烦?

是否可以使用 push -f 命令向远程仓库 "伪装" 未被修改过的原始提交,以达到掩盖操作的目的?

或者

Git 如何判断本地仓库与远程跟踪仓库是否存在分歧?


一个.gitattributes文件将允许您指定哪些文件/目录在合并时由本地保留。 - Pedro del Sol
1
Cbuckley已经完美地解释了如何回退。留下的问题是你是否应该这样做。其他人是否已经拉取了你新推送的状态?如果是这样,恢复到旧状态可能只会导致你现在所处的相同情况。 - Chronial
1个回答

58

有几种方法可以找到推送之前的原始HEAD:

终端回滚

如果你幸运地仍有打开的终端,那么当推送被执行时,会有一些输出看起来像这样:

...
To user@host:repo.git
 + abcdef0...1234567 HEAD -> branchname (forced update)

这里,abcdef0 是之前的 HEAD(即你的 A),而 1234567 则是你强制指定的提交 ID。

git reflog

git reflog 的输出告诉你你的操作历史。你需要回到你检出分支并进行更改之前的那一行,并从第一列中取得提交 ID。

最有用的命令是 git reflog show remotes/origin/branchname。这应该会显示你的强制更新(1234567)和上一个提交 ID(abcdef0)作为前两行。

之前的引用

这里可能有几个提交引用非常有用。它们基本上是指向 reflog 上不同点的引用:

  • @{1}(或者branchname@{1},如果你不在该分支上)是该引用的先前值。仅在你没有对本地分支进行任何其他提交时有效。(但是,@{2}@{3} 等可以让你进一步后退)
  • 类似地,remotes/origin/branchname@{1} 将是远程引用的先前值。仅在其他人没有推送到远程时有效。(同样适用于上面的 @{n}

检查你是否获取了正确的提交ID

如果你想确认你从上述方法中获取的 ID 是正确的,请简单地将其检出:

git checkout abcdef0

浏览一下,如果git log看起来很熟悉(我还建议使用tig来浏览您的存储库,您甚至可以运行tig abcdef0来查看给定提交的日志,这不会影响您的reflog),那么您可以确信正在重置到正确的位置。

重置为上一个状态

获取上一个提交的ID后,您可以重置并强制重新推送:

git checkout branchname # if you're not on it already
git reset --hard abcdef0
git push -f

或者只是:

git push -f origin abcdef0:branchname

这将恢复分支的状态到强制推送之前的状态。(快速提醒:第一个片段将更新本地分支和远程分支;第二个只更新远程分支。)

影响是什么?

每当你强制推送时,可能会对其他检出分支的用户造成问题。

如果你不小心进行了强制推送,然后再次强制推送回到之前的状态,并且没有人有机会获取或拉取分支,那么你可能没事。

如果其他人自从你强制推送以来已经拉取了该分支,并且你再次强制推送到之前的提交,则他们在随后的更新中将遇到问题(他们可能已经在第一次运行中遇到了问题,这将使情况更糟)。

如果他们没有对其本地分支做任何提交,可以直接删除并重新检出(在git fetch确保他们拥有最新参考资料后),或执行以下操作:

git fetch
git checkout branchname # if you're not on it already
git reset origin/branchname

如果他们有进行本地提交,那么他们需要将这些更改变基于正确的历史记录(并可能解决任何冲突):
git fetch
git checkout branchname # if you're not on it already
git rebase --onto origin/branchname 1234567

以上意味着“在origin/branchname(正确的头部)的基础上回放1234567(错误的头部)之后的所有提交。”


分歧的概念

要回答git如何确定远程和本地分支是否发生了分歧,考虑这两个提交图:

                      B
              o---o---o
             /
o---o---o---o
            A
                      D
              o---o---o
             /
o---o---o---o---o---o E
            C

在上图中,直观地说BA之前,或者更准确地说,B包含A
在下图中,既不是D包含E,也不是E包含D。它们都从一个共同的祖先C分叉开来。
如果您想将B合并到A中,则可以使用快进式合并,它简单地更新A的引用以与B相同。
                      B
              o---o---o
             /        A
o---o---o---o

如果您想将 D 合并到 E 中(或反之亦然),则不允许使用快进式合并:您必须创建一个 合并提交
                      D
              o---o---o
             /         \
o---o---o---o---o---o---o E
            C

或者,您可以将更改rebase,这将获取CE之间的所有提交,并在D上重播它们:

                      D
              o---o---o---o---o E'
             /
o---o---o---o---o---o
            C       E

我展示了原始的E和变基后的E',以此来证明变基通常会导致与原始状态的分歧。

注意,我这里一直在一般性地讨论分支,而不是特定于某个分支的远程/本地版本。然而,概念是相同的;唯一的区别是推送(即本地到远程的合并)必须是一个快进式合并

如果本地和远程分化了,你必须先拉取,可以通过将本地更改变基到新的远程上,或者创建一个合并提交来实现。在这两种情况下,本地现在领先于远程,这意味着再次进行推送是可能的。


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