写在前面

之前在公司和外包公司协作开发的时候,遇到了一个问题,是外包那边的代码合并和我们这边的代码合并出错了

具体效果就是“合并之后,他们写的部分代码就没了”

他们说修复,结果就是重新提交之后强制推送了一次。

都不知道是谁教的规范……

所以我想着是在服务端禁用一下这个不安全也不合规范的行为

踩坑日记

本来以为很简单的,结果

又是一个坑……

百度AI解决方案

百度搜到一个解决方案,还是百度ai给出来的,建立hooks/pre-receive钩子

百度AI给出解决方案

百度AI给出解决方案

脚本内容:(有坑)

#!/bin/bash while read oldrev newrev refname do # 检查是否使用了--force选项 if [[ $refname == refs/heads/* ]] && git rev-parse --verify refs/remotes/origin/$(basename $refname) > /dev/null 2>&1; then echo "不允许使用--force选项进行推送。" exit 1 fi done exit 0

本来以为挺好用,但实际发现仍然还是可以强制推送,这个脚本屁用没有!

豆包AI解决方案 及 代码不生效的原因

然后去豆包找了解决方案

豆包解决百度问题

豆包解决百度问题

你的钩子脚本尝试通过检查 refname 变量来检测强制推送,但这种方式存在问题:

  1. 无法直接检测-f参数:Git 钩子无法直接获取用户执行的完整命令行参数,所以无法直接判断是否使用了-f--force
  2. 错误的检测逻辑:当前脚本只是检查推送的是否是分支且远程分支是否存在,但这并不能区分普通推送和强制推送。

脚本内容:

#!/bin/bash while read oldrev newrev refname do # 只处理分支 if [[ $refname == refs/heads/* ]]; then # 获取分支名称 branch=$(basename $refname) # 检查是否是删除分支操作(如果newrev是0值哈希) if [[ $newrev == 00000000000000000000000000000000000000 ]]; then echo "删除分支操作被禁止。" exit 1 fi # 检查是否是强制推送(历史记录是否丢失) if ! git merge-base --is-ancestor $newrev $oldrev; then echo "禁止强制推送!请使用普通推送或rebase后再推送。" exit 1 fi fi done exit 0

这个就能够很好的解决了。

下面我给出详细设置方法。

设置方法

1. 客户端模拟实现强制推送(仅为了测试,可跳过)

这里我在本地的一个项目里,重复修改文件,正常推送后

然后用commit --amend提交,覆盖上一次的提交内容

模拟本地重复修改,追加提交

模拟本地重复修改,追加提交

用了git commit --amend命令之后,会显示一个编辑器,可以修改你的提交信息(当然我这里就不修改了,直接保存退出)

保存提交内容

保存提交内容

我尝试一下进行普通推送,很明显,是推不上去的,因为本地的这个提交已经在远端了

修改后的提交相当于覆盖了远端历史,直接推送肯定是无法推送的

无法推送分支

无法推送分支

2. 服务端增加钩子,阻止强制推送

2.1 找到服务端仓库的钩子目录

首先,去到服务端的git仓库位置(你要修改哪个仓库就去到哪个目录)

比如像我之前写过的这样,自建的仓库教程里,/home/git/resp/类似目录

服务器的git代码仓库

服务器的git代码仓库

目录结构是一致的才可以进行下一步噢。

然后打开你的hooks目录,这里存放着很多钩子的示例文件

.sample结尾的都是示例文件

不带后缀的才能够正常生效,比如我这里红线画的这个

git仓库钩子

git仓库钩子

然后需要创建这样一个钩子文件,我是用了pre-receive钩子,豆包建议,用update钩子会更好,内容如下:

#!/bin/bash while read oldrev newrev refname do # 只处理分支 if [[ $refname == refs/heads/* ]]; then # 获取分支名称 branch=$(basename $refname) # 检查是否是删除分支操作(如果newrev是0值哈希) if [[ $newrev == 00000000000000000000000000000000000000 ]]; then echo "删除分支操作被禁止。" exit 1 fi # 检查是否是强制推送(历史记录是否丢失) if ! git merge-base --is-ancestor $newrev $oldrev; then echo "禁止强制推送!请使用普通推送或rebase后再推送。" exit 1 fi fi done exit 0

新增并保存此文件

ps:我这里保存的时候出了点小事故,我忘了用sudo权限去修改了

导致保存的时候才发现无法保存,还好有办法解决

2.2 修改权限(不修改无法阻止强推)

保存之后,需要修改权限信息,不然的话,依然是可以强行推送的,如下:

未开启配置的话,依然可以强推

未开启配置的话,依然可以强推

这里说的是未开启配置,但实际上是找不到,无法执行这个pre-receive钩子

下面我们来修改权限

注意权限问题

注意权限问题

可以看到,用sudo保存后的文件,是root所有者的,需要修改所属者

用命令修改:sudo chown git.git pre-receive即可

修改归属者

修改归属者

然后修改这里的权限信息

sudo chmod 755 pre-receive

这样是修改为所属者拥有全部权限,其他人有执行权限

修改权限信息

修改权限信息

2.3 测试能否强推

这里就配置完成了,我们来测试一下。

强制推送拦截成功

强制推送拦截成功

发现确实拦截住了,代码规范又进一步!

附录

原理:该钩子会在每次推送时检查是否有历史记录丢失(强制推送的特征)

其他补充说明

  • update 钩子:这是服务器端钩子,在接收推送时执行,比 pre-receive 更适合逐分支检查
  • 分支删除:脚本中同时禁止了分支删除操作,如果需要允许删除某些分支,可以添加例外逻辑
  • git 配置方式:另一种更简单的方式是在服务器仓库中设置receive.denyNonFastForwards = true,但这种方式无法提供自定义错误信息

欢迎关注拓行公众号,分享各种技术博客文章

拓行——奋勇进取,开拓未来,砥砺前行

最后修改:2025 年 07 月 13 日
如果您对各种技术博客文章感兴趣,欢迎关注拓行公众号,分享各种专业技术知识~