大多数人都知道强烈不建议使用 Git 的 push --force
命令,并且认为它具有破坏性。
然而,对我来说,将我所有的信任都放在 Git 上来管理我的项目,同时又完全避免使用其流行的命令之一,这似乎非常奇怪。
这促使我去研究为什么人们认为这个命令如此有害。
它最初为什么会存在?以及底层发生了什么?
在本文中,我分享我的发现,以便您也能了解这个命令对您的项目的用法和影响,学习新的更安全的替代方案,并掌握恢复损坏分支的技能。您可能会惊讶地发现,force
实际上与您同在。
git push 命令
要理解 Git 的工作原理,我们需要退后一步,检查 Git 如何存储其数据。对于 Git 来说,一切都与提交有关。提交是一个对象,包括几个键,例如唯一的 ID,指向暂存内容快照的指针,以及指向直接在该提交之前的提交的指针。
就此而言,分支只不过是指向单个提交的指针。
git push
基本上执行以下操作
1. 复制本地分支中存在的所有提交。
2. 通过将远程分支转发以引用新提交来整合历史记录,也称为快进引用。
快进引用
快进只是转发分支的当前提交引用。当您推送更改时,Git 会自动搜索从当前引用到目标提交引用的线性路径。
如果远程存在祖先提交,而本地不存在(有人更新了远程,但本地尚未更新),Git 将找不到提交之间的线性路径,并且 git push
将失败。

(Noaa Barki,CC BY-SA 4.0)
何时使用 --force
您可以使用 git rebase
、git squash
和 git commit --amend
来更改提交历史记录并重写以前推送的提交。但是请注意,我的朋友们,这些强大的命令不仅仅是更改提交,它们还会替换所有提交,完全创建新的提交。
简单的 git push
会失败,您必须绕过“快进”规则。
输入 --force
。
此选项会覆盖“快进”限制,并将我们的本地分支与远程分支匹配。--force
标志允许您命令 Git 无论如何都要这样做。
当您更改历史记录,或者当您想要推送与远程分支不一致的更改时,可以使用 push --force
。

(Noaa Barki,CC BY-SA 4.0)
简单场景
假设莉莉和鲍勃是开发人员,他们在同一个功能分支上工作。莉莉完成了她的任务并推送了她的更改。过了一会儿,鲍勃也完成了他的工作,但在推送他的更改之前,他注意到了一些添加的更改。他执行了 rebase
以保持树的清洁,然后使用 push --force
将他的更改推送到远程。不幸的是,由于未更新到远程分支,鲍勃不小心擦除了莉莉所有更改的记录。

(Noaa Barki,CC BY-SA 4.0)
鲍勃在使用 --force
选项时犯了一个常见的错误。鲍勃忘记更新 (git pull
) 他的本地跟踪分支。对于用户尚未更新的分支,使用 --force
会导致 Git 推送鲍勃的更改,而无需考虑远程跟踪分支的状态,因此提交会丢失。当然,您并没有丢失所有内容,团队可以采取步骤进行恢复,但如果不加以处理,这个错误可能会造成很多麻烦。
替代方案:push --force-with-lease
--force
选项有一个不太出名的亲戚叫做 --force-with-lease
,它使您能够 push --force
您的更改,并保证您不会覆盖其他人的更改。默认情况下,--force-with-lease
拒绝更新分支,除非远程跟踪分支和远程分支指向相同的提交引用。非常棒,对吧?更棒的是。
您可以指定 --force-with-lease
要与哪个提交、分支或引用进行比较。--force-with-lease
选项使您可以灵活地覆盖远程分支上的新提交,同时保护您的旧提交历史记录。它具有相同的强制性,但带有救生衣。
指南:如何处理破坏性的 `--force`
毫无疑问,您是一位负责任的开发人员,但我敢打赌,您或您的队友至少发生过一次意外地将 git push --force
运行到不应该有人动的重要分支中。眨眼间,所有人的最新工作都丢失了。
无需惊慌!如果您非常幸运,在您破坏它之前,在同一代码上工作的其他人刚刚拉取了该分支的最新版本。如果是这样,您所要做的就是请他们 --force push
他们最近的更改!
但即使您没有那么幸运,您仍然很幸运地找到了这篇文章。
1. 您是犯错之前最后推送的人吗?
首先,不要关闭您的终端。
其次,去您的队友那里忏悔您的罪过。
最后,确保在接下来的几分钟内没有人动代码仓库,因为您有一些工作要做。
回到您的工作站。在终端中 git push --force
命令的输出中,查找类似于下面这一行的行
+ d02c26f…f00f00ba [branchName] -> [branchName] (forced update)
第一组符号(看起来像提交 SHA 前缀)是修复此问题的关键。
假设您在造成损害之前对分支的最后一次良好提交是 d02c26f
。您唯一的选择是以火攻火,并将此提交 push --force
回到错误的分支之上
$ git push — force origin deadbeef:[branchName]
恭喜!您拯救了这一天!
2. 我不小心对我的代码仓库使用了 `--force push`,我想回到之前的版本。我该怎么办?
想象一下在一个功能分支上工作。您拉取了一些更改,创建了一些提交,完成了您负责的功能部分,并将您的更改推送到了主代码仓库。然后您使用 git rebase -i
将提交压缩为一个,并再次使用 push --force
推送。但是发生了一些不好的事情,您想将您的分支恢复到 rebase -i
之前的状态。关于 Git 的伟大之处在于,它非常擅长永不丢失数据,因此 rebase
之前的代码仓库版本仍然可用。
在这种情况下,您可以使用 git reflog
命令,它会输出代码仓库的详细历史记录。
对于您在本地代码仓库中所做的每个“更新”,Git 都会创建一个引用日志条目。git reflog
命令输出这些 reflog
,它们存储在您的本地 Git 代码仓库中。git reflog
的输出包含所有已更改本地代码仓库中分支和其他引用提示的操作,包括切换分支和 rebases
。分支的提示(称为 HEAD)是对当前活动分支的符号引用。它只是一个符号引用,因为分支是指向提交的指针。
这是一个简单的 reflog
,显示了我上面描述的场景:
1b46bfc65e (HEAD -> test-branch) HEAD @ {0} : rebase -i (finish): returning to refs/heads/test-branch
b46bfc65e (HEAD -> test-branch) HEAD @ {1}: rebase -i (squash): a
dd7906a87 HEAD @ {2} : rebase -i (squash): # This is a combination of 2 commits.
a3030290a HEADC {3}: rebase -i (start): checkout refs/heads/master
Oc2d866ab HEAD@{4}: commit: c
6cab968c7 HEAD@ {5} : commit: b
a3030290a HEAD @ {6}: commit: a
c9c495792 (origin/master, origin/HEAD, master) HEAD@ {7}: checkout: moving from master to test-branch
c9c495792 (origin/master, origin/HEAD, master) HEAD@ {8} : pull: Fast-forward
符号 HEAD@{number}
是 HEAD 在 number
次更改之前的的位置。因此 HEAD@{0}
是 HEAD 现在的位置,HEAD@{4}
是 HEAD 四步之前的位置。您可以从上面的 reflog
中看到,HEAD@{4}
是您需要去恢复分支到 rebase 之前的状态的位置,而 0c2d866ab
是该提交的提交 ID。
因此,要将测试分支恢复到您想要的状态,您需要重置分支
$ git reset — hard HEAD@{4}
然后您可以再次强制推送以将代码仓库恢复到之前的状态。
通用恢复
任何时候您想在 push --force
之后将您的分支恢复到之前的版本,请遵循此通用恢复解决方案模板
1. 使用终端获取之前的提交。
2. 创建一个分支或重置到之前的提交。
3. 使用 push --force
。
如果您创建了一个新分支,请不要忘记重置该分支,以便通过运行以下命令将其与远程同步
$ git reset --hard origin/[new-branch-name]
3. 使用 git fsck 恢复 push --force 删除的分支
假设您拥有一个代码仓库。
您有一位开发人员为您编写了项目。
该开发人员决定删除所有分支并 push --force
提交了一条消息“项目在这里”。
该开发人员离开了这个国家,无法联系或找到他们。您没有代码,并且从未克隆过代码仓库。
首先,您需要找到之前的提交。
可悲的是,在这种情况下,使用 git log
无济于事,因为分支指向的唯一提交是“项目在这里”,没有任何相关的提交。在这种情况下,您必须找到已删除的提交,这些提交没有任何子提交、分支、标签或其他引用链接到它们。幸运的是,Git 数据库存储了这些孤立的提交,您可以使用强大的 git fsck
命令找到它们。
git fsck --lost-found
他们称这些提交为“悬空提交”。根据文档,简单的 git gc
会删除两周前的悬空提交。在这种情况下,您拥有的只是悬空提交,您剩下要做的就是找到损坏之前的上一个提交,并按照上面的通用恢复步骤进行操作。
受保护的分支和代码审查
正如老话所说
“聪明人与聪明人的区别在于,聪明人知道如何摆脱聪明人一开始就不会陷入的麻烦。”
如果您希望完全避免 push --force
,GitHub 和 GitLab 都提供了一个非常酷的功能,称为受保护的分支,它允许您将任何分支标记为受保护,这样就没人可以对其 push --force
。
您还可以设置管理员首选项来限制权限。
或者,您可以建立 Git 钩子 以要求在任何人可以将代码推送到重要分支之前进行代码审查或批准。
总结
希望您现在了解何时需要添加 --force
选项以及使用它时涉及的风险。请记住,--force
在那里为您服务。它只是一种绕过,就像每种绕过一样,您应该谨慎使用它。
愿 --force
与您同在。
本文改编自作者的 Medium 文章 并经许可重新发布。
评论已关闭。