我见过用 Git 最混乱的团队是这样的:主分支直接 push、commit message 全是"fix"、一个 PR 改了 40 个文件、合并冲突靠"谁先来谁赢"……结果每次发版都是噩梦,回滚更是灾难。
Git 本身只是工具,工作流才是让团队有效协作的契约。这篇文章系统整理了我在不同规模团队实践过的分支策略和协作规范,以及具体的踩坑经验。
三种主流工作流对比#
Git Flow#
Git Flow 是最重、分支最多的模型,核心包含五种分支:
main:永远是生产代码develop:集成分支,下一个版本的开发基准feature/*:功能开发,从 develop 切,合回 developrelease/*:发版准备,从 develop 切,测试完合回 main 和 develophotfix/*:紧急修复,从 main 切,合回 main 和 develop
main: ──────────●──────────────────────────●────
↑ ↑
release: └──●──────────●────────────┘
↑ ↓
develop: ──●──────────●──────────────────────●────
↑ ↑ ↓ ↑
feature: └─────────●──────┘ └──feature-B────┘
适合场景:有明确版本号的软件(手机 App、SaaS 按季度发版)、需要同时维护多个版本、QA 测试周期长的团队。
缺点:维护成本高,长时间运行的 feature 分支容易积累大量冲突;对于 CI/CD 成熟的团队显得过度设计。
GitHub Flow#
简化版:只有 main 分支是长期分支,其他分支都是短命的 feature 分支。
# 完整流程
git checkout -b feature/user-auth main
# ... 开发、提交 ...
git push origin feature/user-auth
# 发起 PR,Code Review
# Review 通过后 Merge 进 main
# CI/CD 自动部署
适合场景:持续部署的 Web 服务、小型团队(5-15人)、发版频率高(每天多次)。
缺点:对 CI/CD 和测试覆盖率要求高;main 分支质量完全依赖 PR Review 质量。
Trunk-Based Development(TBD)#
所有人直接在 main(trunk)上工作,或者使用极短命的分支(存活不超过一两天)。大特性用 Feature Flags 控制上线时机,而不是靠分支隔离。
# 开发流程(短命分支版)
git checkout -b feat/small-change
# 最多1天内完成
git push origin feat/small-change
# 快速 Review,当天合入
适合场景:工程文化成熟的大型团队(Google、Meta 内部)、Feature Flag 基础设施完善、有强大的自动化测试兜底。
缺点:对工程纪律要求极高;Feature Flag 管理有额外成本;不适合需要稳定 release 窗口的产品。
怎么选?#
| 维度 | Git Flow | GitHub Flow | Trunk-Based |
|---|---|---|---|
| 团队规模 | 中大型 | 小中型 | 大型 |
| 发版频率 | 低(每月/每季) | 中(每天/每周) | 高(每天多次) |
| CI/CD 成熟度 | 低要求 | 中等 | 高要求 |
| 复杂度 | 高 | 低 | 中 |
我的建议:大多数中小团队用 GitHub Flow 就够了。如果你的团队同时维护多个版本(比如 SaaS 有企业客户锁定在旧版本),才需要引入 Git Flow 的 release 分支概念。
分支命名规范#
type/short-description
type/issue-id-short-description
常用类型:
feature/— 新功能fix/— Bug 修复refactor/— 重构(不改行为)hotfix/— 紧急线上修复release/— 版本发布准备chore/— 构建/工具链变更
示例:
feature/user-oauth-login
fix/gh-123-null-pointer-on-logout
hotfix/memory-leak-connection-pool
release/v2.3.0
规则:全小写、连字符分隔、不超过 50 字符、不包含个人名字。
Commit Message 规范:Conventional Commits#
没有规范的 commit 历史是这样的:
fix
aaa
test
update
改了个东西
wip
有了 Conventional Commits 规范后:
feat(auth): add OAuth2 login with Google
fix(api): return 404 when resource not found
refactor(db): extract connection pool to separate module
docs(readme): add deployment instructions
chore(deps): upgrade express from 4.17 to 4.18
格式:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
type 必须是以下之一:
feat— 新功能(对应 MINOR 版本号)fix— Bug 修复(对应 PATCH 版本号)refactor— 重构docs— 文档test— 测试chore— 工具链/配置变更perf— 性能优化ci— CI/CD 变更build— 构建系统变更revert— 回滚
破坏性变更用 ! 标注或在 footer 写 BREAKING CHANGE::
feat(api)!: change response format for /users endpoint
BREAKING CHANGE: response is now paginated, clients need to handle
the new `data` and `pagination` fields
落地执行:commitlint#
光靠人工审查 commit message 不现实,用 commitlint 配合 husky 强制校验:
# 安装
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
# 配置
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
# 添加 git hook
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'
非 JS 项目可以用 pre-commit + conventional-pre-commit:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v2.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
rebase vs merge:选择哲学#
这个话题能引发宗教战争,我的观点是:没有绝对对错,关键是团队要统一。
merge 的逻辑#
merge 保留了完整的历史,包括分支何时创建、何时合并:
git checkout main
git merge feature/user-auth
历史是这样的:
* 合并提交 (main)
|\
| * feat: add login page (feature/user-auth)
| * feat: add auth service
* | fix: something on main
|/
* initial commit
适合场景:需要保留完整历史轨迹;多人长期协作的分支;已 push 到远程的分支。
rebase 的逻辑#
rebase 把当前分支的提交"嫁接"到目标分支的最新点,历史看起来像是线性的:
git checkout feature/user-auth
git rebase main
# 如果有冲突,解决后 git rebase --continue
git checkout main
git merge feature/user-auth # 此时是 fast-forward,无合并提交
历史是这样的:
* feat: add login page (main, feature/user-auth)
* feat: add auth service
* fix: something on main
* initial commit
适合场景:本地整理提交历史;个人分支同步主分支最新代码(代替 merge);保持 main 分支历史整洁。
黄金法则#
永远不要 rebase 已经推送到远端、其他人在基于此工作的分支。 rebase 会重写 commit hash,强制 push 后其他人的本地分支历史会与远端不一致,解决起来非常麻烦。
# 安全的 rebase 场景:同步主分支
git fetch origin
git rebase origin/main # 把自己的提交接在最新 main 之后
# 不安全:已推送的分支上 rebase 后 force push(除非是个人分支且确认没人基于此)
git push --force-with-lease origin feature/xxx # 比 --force 更安全,会检查远端状态
交互式 rebase:整理本地提交#
提交 PR 前,把"wip"“fix typo"这类噪音提交清理掉:
# 整理最近 4 个提交
git rebase -i HEAD~4
# 编辑器中会出现:
# pick abc1234 feat: add user model
# pick def5678 wip
# pick ghi9012 fix typo
# pick jkl3456 feat: add user controller
# 修改为:
# pick abc1234 feat: add user model
# squash def5678 wip # squash 合入上一个提交
# fixup ghi9012 fix typo # fixup 合入上一个提交,丢弃 commit message
# pick jkl3456 feat: add user controller
cherry-pick:精准移植提交#
cherry-pick 用于把特定提交从一个分支移植到另一个分支,最典型的场景是 hotfix:
# 场景:在 main 上修了一个 bug,需要同步到还在维护的 v1.x 分支
git log main --oneline
# abc1234 fix(api): fix null pointer in getUserById
git checkout release/v1.x
git cherry-pick abc1234
不要滥用 cherry-pick: 如果你频繁 cherry-pick,说明分支策略有问题。cherry-pick 不传递历史,如果同一个 commit 在两个分支上都存在,之后合并时会制造混乱。
大型重构的分支策略#
重构通常是"最难管理的 PR”——改动范围大、持续时间长、合并冲突噩梦。几个实用策略:
策略一:Branch by Abstraction#
不创建长生命周期的重构分支,而是在主干上通过抽象层逐步替换:
Step 1: 引入抽象接口(向后兼容)
Step 2: 新实现实现该接口(两套并存)
Step 3: 切换调用方指向新实现
Step 4: 删除旧实现
每一步都是可独立合入的小 PR,风险可控。
策略二:Strangler Fig Pattern#
系统级重写时,新旧系统并行运行,通过路由/特性开关逐步把流量切到新系统。
策略三:拆分大 PR#
如果非得用分支,把大重构拆成多个小 PR:
# 父分支:整个重构的集成分支
git checkout -b refactor/payment-system main
# 子分支1:只迁移数据模型
git checkout -b refactor/payment-system/models refactor/payment-system
# ... 完成后 PR 进父分支 ...
# 子分支2:迁移业务逻辑
git checkout -b refactor/payment-system/service refactor/payment-system
最终父分支再合入 main,每个子 PR 的 diff 更小、更容易 Review。
保护分支与 PR Review 配置#
以 GitHub 为例,main 分支保护配置:
# 通过 GitHub API 或 Terraform 配置
branch_protection_rules:
- pattern: "main"
required_status_checks:
strict: true # 必须基于最新 main
contexts:
- "ci/tests"
- "ci/lint"
required_pull_request_reviews:
required_approving_review_count: 1
dismiss_stale_reviews: true # push 新代码后旧 approval 失效
require_code_owner_reviews: true # 修改 CODEOWNERS 覆盖的文件必须 owner review
enforce_admins: true # 管理员也不能绕过
allow_force_pushes: false
allow_deletions: false
CODEOWNERS 配置示例:
# .github/CODEOWNERS
# 全局默认 owner
* @team/backend
# 特定目录
/frontend/ @team/frontend
/infra/ @team/sre
/.github/ @team/sre
# 特定文件
/go.mod @team/backend @team/sre
/Dockerfile @team/sre
.gitignore 和 .gitattributes 工程化配置#
# .gitignore 分层管理
# 系统文件(放 ~/.gitignore_global)
.DS_Store
Thumbs.db
*.swp
# IDE 文件(也可以放 global)
.idea/
.vscode/
*.iml
# 项目级
.env
.env.local
*.secret
# 构建产物
dist/
build/
*.pyc
__pycache__/
node_modules/
# 测试覆盖率
coverage/
.coverage
.gitattributes 解决跨平台换行问题:
# .gitattributes
# 默认:文本文件统一用 LF,checkout 时根据平台转换
* text=auto
# 明确指定文本文件使用 LF
*.sh text eol=lf
*.py text eol=lf
*.go text eol=lf
*.yml text eol=lf
# Windows 批处理文件保持 CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
# 二进制文件不做转换
*.png binary
*.jpg binary
*.pdf binary
*.zip binary
常见问题处理#
回滚错误提交#
# 场景一:刚提交,还没 push,完全撤销(修改回到暂存区)
git reset --soft HEAD~1
# 场景二:刚提交,还没 push,完全丢弃这次修改
git reset --hard HEAD~1
# 场景三:已 push,用 revert 创建一个"撤销提交"(不重写历史,更安全)
git revert abc1234
git push
# 场景四:已 push 到受保护分支,回滚到某个 tag(紧急情况)
git revert abc1234..HEAD # 批量 revert 一个范围
解决合并冲突#
# 冲突标记
<<<<<<< HEAD
当前分支的内容
=======
要合并进来的内容
>>>>>>> feature/xxx
# 使用 vimdiff 可视化解决
git mergetool --tool=vimdiff
# 选择某一方的版本
git checkout --ours path/to/file # 保留当前分支的版本
git checkout --theirs path/to/file # 采用对方分支的版本
# 预防:合并前先用 diff 看看差异
git diff main...feature/xxx
找回丢失的提交#
# git reflog 记录了所有 HEAD 移动历史,即使 reset --hard 也能找回
git reflog
# 输出类似:
# abc1234 HEAD@{0}: reset: moving to HEAD~1
# def5678 HEAD@{1}: commit: feat: add user model ← 这个被 reset 的提交
# 恢复
git checkout def5678 # 临时查看
git checkout -b recover/xxx # 创建新分支保存
总结#
工作流真正要解决的就三件事:不同类型的工作互不干扰、变更能及时合回主线、历史能回溯到"谁在什么时候因为什么改了什么"。前两件靠分支策略和 PR 流程,第三件靠 commit message。
价值要等时间证明。六个月后你还能通过 git log 快速看懂某段代码的演变,就是规范的回本时刻。






