多人共用仓库时,我们定了一套分支命名和权限规则,误删保护分支的事再没发生过
多人共用一个仓库,最容易出事的不是代码冲突,而是有人把 release 或 main 当成自己的功能分支,顺手一个 force push 或 delete,全组当场血压拉满。要根治这个问题,核心就两条:用命名前缀把分支按用途物理隔离,用保护规则把关键分支的写权限收口到人。下面是我们踩坑后沉淀下来的一套完整规则,可直接抄作业。
分支命名:前缀即身份,一眼识别用途
先定命名规范,再谈权限。我们的原则是:分支名前缀必须能直接映射到责任人和用途,不允许出现 test、fix、tmp 这类无主分支。
我们用的前缀体系如下:
| 前缀 | 用途 | 示例 | 谁能建 | 谁能删/强推 |
|---|---|---|---|---|
main |
生产基线 | main |
无(锁定) | 无人(仅 CI 可写) |
release/ |
发布候选 | release/2024.12.0 |
Release 负责人 | Release 负责人 |
hotfix/ |
线上紧急修复 | hotfix/pay-callback-timeout |
轮值 Oncall | Oncall + Tech Lead |
feature/ |
功能开发 | feature/order-refund-flow |
开发者本人 | 开发者本人 |
bugfix/ |
缺陷修复 | bugfix/order-status-sync |
开发者本人 | 开发者本人 |
exp/ |
实验性分支 | exp/kafka-batch-consume |
任何人 | 任何人(但 14 天自动清理) |
这里有两个关键设计:
第一,main 不允许任何人直接 push。 所有进入 main 的代码必须走 PR + CI 全绿 + 至少一位 Reviewer 的 Approve。我们在 GitLab 上配了「Allowed to push: No one」「Allowed to merge: Maintainers」,并且打开了「Prevent force push」和「Prevent branch deletion」。这条规则执行两年,main 没出过一次事故。
第二,exp/ 前缀给了开发者一个安全阀。 老手总有些想快速验证的激进想法,如果所有分支都卡权限,他们就会找各种歪路子(比如直接推到 feature/ 下面再删)。exp/ 分支完全放开,但有两个约束:不允许合入任何受保护分支(只能 cherry-pick 到正式分支);14 天无提交自动清理。这样既给了自由度,又不会污染仓库长期历史。
命名上我们还加了一条硬性规则:分支名中必须包含 Jira 工单号,格式为 <prefix>/<JIRA-ID>-<short-desc>,如 feature/ORDER-3214-refund-flow。这样任何分支都能在 3 秒内定位到需求背景和负责人,审计和追溯成本几乎为零。
权限落点:GitLab 保护规则 + Code Owner 双保险
命名规范只是约定,真正防误删的是在 GitLab(或 GitHub 对应功能)里把分支保护规则配死。我们的配置分三层:
第一层:全局保护分支规则,覆盖所有 release/* 和 hotfix/*。
在 GitLab 的 Settings → Repository → Protected Branches 中,我们用通配符 release/* 和 hotfix/* 建立规则,而不是一条一条加。配置如下:
Allowed to merge: Maintainers
Allowed to push and merge: No one
Allowed to force push: 关闭
Allowed to delete: 关闭
Code owner approval: 开启(针对 release/*)
注意 release/* 的 push 权限是「No one」,意味着连 Maintainer 都不能直接推,必须走 MR。这看起来繁琐,但发布分支的每一次改动都应该有记录和审阅,MR 本身就是审计日志。我们 2023 年 Q3 就因为一个 Maintainer 直接 amend 了 release/2023.9.0 上的一个 commit,导致线上版本和 tag 对不上,排查了两天。从那以后 release/* 全部锁死走 MR。
第二层:feature/* 和 bugfix/* 只保护「删除」操作,不限制 push。
具体做法是在 GitLab 的 Protected Branches 里为 feature/* 和 bugfix/* 设置:
Allowed to push: Developers + Maintainers
Allowed to force push: 开启(仅分支创建者)
Allowed to delete: Maintainers only
这里有个 GitLab 的细节:「Allowed to force push」不能直接限定到「分支创建者」这个粒度,GitLab 的 force push 权限是跟着角色走的。我们的变通方案是:不开启 force push,而是要求开发者用 git push --force-with-lease 并且分支保护规则里关闭 force push 后,GitLab 会直接拒绝。如果确实需要重写历史(比如误提交了大文件),走一个简化的审批流程:找 Maintainer 临时解除该分支保护,操作完立刻恢复。这个流程一个月都走不了一次,成本可接受。
第三层:CODEOWNERS 文件把关键目录的审阅权钉死。
光靠分支规则管不住「谁有资格批准合入」。我们在仓库根目录放了 CODEOWNERS:
# 支付、结算相关模块,必须有支付组的人 Approve
src/payment/ @team-payment-leads
src/settlement/ @team-payment-leads
# 数据库迁移脚本,必须有 DBA 或后端 Tech Lead 确认
migrations/ @backend-leads @dba
# 公共工具库,改动影响面大
src/common/ @backend-leads
# CI/CD 配置
.gitlab-ci.yml @devops-leads
配合 GitLab 的「Code owner approval」保护规则,release/* 分支的 MR 如果触碰了 src/payment/,支付组的 Lead 不点 Approve,这个 MR 就无法合入。这个机制挡掉了至少三次「其他组顺手改了支付模块的字段类型」之类的事故。
误删后的兜底:30 分钟内的恢复路径
规则再严,也挡不住有人拿了个不该有的 token 或者 GitLab 权限配错。所以我们还准备了一条误删保护分支后的快速恢复流程,目标是 30 分钟内恢复。
具体做法:
-
开启 GitLab 的审计日志导出。 在 Admin Area → Monitoring → Audit Events 里,把删除分支、修改保护规则、修改成员权限这三类事件导出到外部日志系统(我们用 ELK)。一旦分支消失,先查审计日志定位操作人和时间。
-
利用 GitLab 的「Repository mirroring」做单向备份。 我们在另一台内网 GitLab 实例上配置了仓库镜像(Pull 模式,每 15 分钟同步一次)。主仓库分支被误删后,镜像仓库里依然保有最后一次同步的 ref,可以直接从镜像仓库恢复。这个方案比依赖开发者本地副本可靠得多——不是每个开发者都会 fetch 所有分支。
-
恢复命令固定为:
# 在镜像仓库所在机器上执行
git clone --mirror <镜像仓库地址> /tmp/repo-backup.git
cd /tmp/repo-backup.git
git push <主仓库地址> <被删分支的完整ref>:refs/heads/<被删分支名>
我们把这条命令写进了 Runbook,Oncall 照着执行即可,不用临场想方案。
实际演练过一次:2024 年 6 月,一个实习生拿到了 Maintainer 权限(权限审批流程出了问题,那是另一个故事),手滑删掉了 release/2024.6.0。审计日志定位到人花了 3 分钟,从镜像恢复花了 8 分钟,总共 11 分钟恢复。如果没有镜像,那次至少要等负责该分支的同事从本地 push 回来,运气不好可能就没救了。
规则要写进文档,更要塞进 CI 检查
命名规范如果只靠自觉,三个月后就会有人建 fix_xxx 这种分支。我们把命名检查做成了 CI 的第一道门禁:
# .gitlab-ci.yml 中的分支名校验 job
validate-branch-name:
stage: .pre
script:
- |
BRANCH_NAME="$CI_COMMIT_REF_NAME"
if [[ "$BRANCH_NAME" =~ ^(main|release\/[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}|hotfix\/[A-Z]+-[0-9]+-[a-z0-9-]+|feature\/[A-Z]+-[0-9]+-[a-z0-9-]+|bugfix\/[A-Z]+-[0-9]+-[a-z0-9-]+|exp\/[a-z0-9-]+)$ ]]; then
echo "分支名合法: $BRANCH_NAME"
else
echo "分支名不符合规范,请参考 docs/branch-naming.md"
exit 1
fi
rules:
- if: '$CI_PIPELINE_SOURCE == "push"'
这个 job 跑在 .pre 阶段,任何 push 都会先过这一关。分支名不合规直接 pipeline 失败,开发者会收到明确的报错信息。配合文档 docs/branch-naming.md(里面写清了每个前缀的用途、权限、生命周期),新成员入职第一天就能对齐。
另外,保护分支规则本身也要做版本管理。 GitLab 的保护规则在 UI 上配,但很容易被手滑改掉。我们每季度把保护规则导出(通过 GitLab API 拉取)并 diff,一旦发现有人私自放宽规则,直接追责。这个动作可以做成一个定时脚本,跑在 CI 的 scheduled pipeline 里。
常见问题
Q:开发者说 feature 分支 force push 被拒了,但确实需要重写提交历史怎么办?
A:走临时解保护流程。开发者先在 MR 或群聊里说明原因(比如误提交了大文件、commit message 写错需要 squash),由 Maintainer 在 GitLab 里临时关闭该分支的「Prevent force push」,开发者执行 git push --force-with-lease 后,Maintainer 立刻恢复保护。整个过程控制在 5 分钟内,且 Maintainer 在操作时能看到分支当前状态,不会误伤。
Q:exp/ 分支 14 天自动清理,怎么实现的?
A:GitLab 没有内置的分支过期功能,我们用一个跑在 scheduled pipeline 里的脚本清理。脚本通过 GitLab API 列出所有 exp/ 前缀分支,检查最后一次 commit 的时间,超过 14 天且分支未合并到任何受保护分支的,自动删除并通知分支创建者。脚本放在仓库的 scripts/cleanup-exp-branches.py,每周一凌晨跑一次。
Q:多人协作时,两个人同时往同一个 feature 分支 push,会不会出问题?
A:我们的规则是 feature/* 分支归创建者所有,其他人原则上不直接 push,而是通过 MR 向该分支合入。如果确实需要多人协作同一个功能(比如前后端联调),会单独开一个 feature/<JIRA-ID>-fullstack 分支,并在 MR 描述里明确负责人。实际上大部分场景下,一个功能分支对应一个开发者,冲突面很小。
Q:GitHub 上怎么实现类似的分支保护?
A:GitHub 的 Protected Branches 支持通配符(如 release/*),可以设置「Require pull request reviews before merging」「Dismiss stale pull request approvals when new commits are pushed」「Do not allow bypassing the above settings」。但 GitHub 没有「Allowed to push: No one」这种细粒度到角色的 push 限制——要么允许 push,要么完全禁止。变通做法是:只允许通过 GitHub Actions 或 Bot 账号 push 到受保护分支,开发者一律走 PR。另外 GitHub 的 CODEOWNERS 功能比 GitLab 更成熟,可以直接要求指定 owner 的 review。