团队工作流实战:从一个人能打到一队人能战
# 第6章 · 团队工作流
# 一、团队失控
前五章,小李都是一个人或两三个人协作。虽然偶尔有点混乱,但靠沟通就能解决——喊一嗓子「别 push,我在改这个文件」,大家就停下来。
三个月后,公司拿到了融资,团队从 3 人扩到 15 人。噩梦开始了:
周一早上 9 点,小李打开 Slack,看到 47 条未读消息:
小王:「谁 push 了 broken code 到 master?线上挂了!」 小张:「我昨天推的,但我本地是好的啊。」 小王:「你是不是没 pull 最新代码就 push 了?」 小张:「我 pull 了啊……可能是李哥的吧?」 小李:「我改的是 coupons.js,跟支付无关。」
没人知道线上挂的原因,也没人知道谁应该修。 更糟的是:
- 🚫 有人直接往 master 推未经审查的代码,线上两次回滚
- 🚫 有人在同一个 feature 分支上同时做三个功能,想拆都拆不开
- 🚫 没有发布流程——每次上线靠「感觉差不多了就推」
- 🚫 Commit message 惨不忍睹:
fix、update、改了一下、asdf
问题本质:不是团队缺 Git 技能,而是缺一套每个人都遵守的游戏规则。
这就是团队工作流要解决的问题:不是让你更会用 Git,而是让 15 个人像一个人一样协同。
# 二、Git Flow
# 2.1 五种分支,各司其职
Git Flow 是 Vincent Driessen 在 2010 年提出的,虽然现在被认为「重量级」,但它把「分支职责」这个概念讲得最透彻。
┌─ feature/支付 ─┐
╱ ╲
──○──○────○──────○────────────────────○────────────○── main
╲ ╱ ╲ ╱ ╲ ╱
○──○──○───○──○──○────○────○───○──○──○──○── develop
╲ ╱ ╲ ╱
release/1.0 hotfix/紧急
| 分支 | 生命周期 | 来源 | 合并到 | 命名 |
|---|---|---|---|---|
| main | 永久 | - | - | main 或 master |
| develop | 永久 | main | - | develop |
| feature | 临时 | develop | develop | feature/xxx |
| release | 临时 | develop | main + develop | release/v1.2.0 |
| hotfix | 临时 | main | main + develop | hotfix/xxx |
核心规则:
- main 只存生产代码——任何时候从 main 拉出来的代码都能直接部署。
- develop 是集成分支——所有 feature 先合到 develop,测试通过后才合到 main。
- feature 从 develop 出,回 develop——一个 feature 一个分支,做完就删。
- release 是发布前的最后一道关——修 bug、改版本号、打 tag,不改新功能。
- hotfix 从 main 出,同时回合 main 和 develop——确保修复在主线和开发线都不丢失。
# 2.2 完整的 Git Flow 模拟
mkdir gitflow-lab && cd gitflow-lab && git init
# ==================== Step 1:建立主分支结构 ====================
echo "const app = 'Shop v0.0.1';" > app.js
git add . && git commit -m "init: 项目初始化"
# 从 main 分出 develop
git branch develop
# main 打初始标签
git tag v0.0.1
# ==================== Step 2:两人并行开发两个 feature ====================
# 小李:开发商品搜索
git switch develop
git switch -c feature/商品搜索
echo '
function searchProducts(keyword) {
return db.query("SELECT * FROM products WHERE name LIKE ?", [`%${keyword}%`]);
}
' > search.js
git add . && git commit -m "feat: 商品搜索功能"
# 小张:开发购物车(从 develop 开出另一个分支)
git switch develop
git switch -c feature/购物车
echo '
const cart = [];
function addToCart(id) { cart.push(id); }
function getCartTotal() { return cart.length; }
' > cart.js
git add . && git commit -m "feat: 购物车添加功能"
echo '
function removeFromCart(id) {
const idx = cart.indexOf(id);
if (idx > -1) cart.splice(idx, 1);
}
' >> cart.js
git add . && git commit -m "feat: 购物车移除功能"
# ==================== Step 3:feature 合并到 develop ====================
# 小李先合并搜索功能
git switch develop
git merge --no-ff feature/商品搜索 -m "merge: 合并商品搜索功能"
git branch -d feature/商品搜索
# 小张接着合并购物车功能
git merge --no-ff feature/购物车 -m "merge: 合并购物车功能"
git branch -d feature/购物车
git log --oneline --graph
# 输出:
# * merge: 合并购物车功能
# |\
# | * feat: 购物车移除功能
# | * feat: 购物车添加功能
# |/
# * merge: 合并商品搜索功能
# |\
# | * feat: 商品搜索功能
# |/
# * init: 项目初始化
# ==================== Step 4:准备发布——创建 release 分支 ====================
git switch -c release/v1.0.0 develop
# 在 release 上只做发布准备:修小 bug、改版本号
echo "const APP_VERSION = '1.0.0';" >> app.js
git add . && git commit -m "chore: 更新版本号到 1.0.0"
# 发现一个 display bug——修复
echo "// 修复:移动端搜索框宽度问题" >> search.js
git add . && git commit -m "fix: 搜索框移动端适配"
# 测试通过 → 发布!
# 合并到 main
git switch main
git merge --no-ff release/v1.0.0 -m "release: v1.0.0 正式发布"
# 打发布标签
git tag -a v1.0.0 -m "v1.0.0 正式版:商品搜索 + 购物车"
# 别忘了:release 分支上的修复也要同步回 develop!
git switch develop
git merge --no-ff release/v1.0.0 -m "merge: 同步 release v1.0.0 的修复"
# 清理 release 分支
git branch -d release/v1.0.0
# ==================== Step 5:线上出 bug → hotfix ====================
# v1.0.0 发布后,线上发现购物车的移除功能在空购物车时报错
# hotfix 从 main 开出
git switch -c hotfix/空购物车崩溃 main
# 修复
cat > cart.js << 'EOF'
const cart = [];
function addToCart(id) { cart.push(id); }
function getCartTotal() { return cart.length; }
function removeFromCart(id) {
const idx = cart.indexOf(id);
if (idx > -1) cart.splice(idx, 1);
}
// 修复:空购物车返回空数组而非报错
function getCartItems() {
return [...cart];
}
EOF
git add . && git commit -m "hotfix: 修复空购物车调用 remove 崩溃"
# 修改版本号
echo "const APP_VERSION = '1.0.1';" > app.js
git add . && git commit -m "chore: 版本号 1.0.1"
# 合并到 main
git switch main
git merge --no-ff hotfix/空购物车崩溃 -m "merge: hotfix 空购物车崩溃"
git tag -a v1.0.1 -m "v1.0.1 热修复:空购物车崩溃"
# 🚨 关键!合并到 develop!否则 develop 上还会有这个 bug!
git switch develop
git merge --no-ff hotfix/空购物车崩溃 -m "merge: 同步 hotfix 空购物车修复"
# 清理
git branch -d hotfix/空购物车崩溃
# 2.3 Git Flow 的优势与劣势
优势:
- ✅ 结构清晰,五种分支各司其职,新人一看就懂
- ✅ 严格的发布流程,不会发生「忘记修了什么就上线了」
- ✅ main 永远稳定,任何时候都能部署
劣势:
- ❌ 重型——小团队三四个人的项目,走完整流程太慢
- ❌ 分支多——同时维护 feature / release / hotfix 多条线,容易忘同步
- ❌ 不适合持续部署——如果你的团队一天发 10 次版,等 release 分支太慢了
# 三、GitHub流
# 3.1 只有一条长期分支 + 临时 feature 分支
main ──○────────○─────────○──────────○────
╲ ╲ ╲ ╲
○──○──○ ○──○ ○──○──○ ○──○ ← feature 分支
规则极其简单:
main永远是可部署状态。- 任何新东西,从
main开出 feature 分支。 - feature 分支开发完,提 Pull Request。
- PR 审查通过 → 合并到 main → 立即部署。
- 部署后发现 bug → 从 main 开新的分支修复(同步骤 2~4)。
没有 develop,没有 release,只有 main + 一堆短暂的 feature 分支。
# 3.2 GitHub Flow 实战
# 和 Git Flow 的区别:没有 develop,直接从 main 出分支
git switch main
git switch -c feature/商品推荐
# 开发...
echo "function recommend() { /* 推荐算法 */ }" > recommend.js
git add . && git commit -m "feat: 商品推荐功能"
# push + 提 PR
git push -u origin feature/商品推荐
# 去 GitHub 创建 PR:base main ← compare feature/商品推荐
# 审查 → 通过 → Merge → 立即部署到生产环境
# 清理
git switch main
git pull origin main
git branch -d feature/商品推荐
# 3.3 选 Git Flow 还是 GitHub Flow?
| 维度 | Git Flow | GitHub Flow |
|---|---|---|
| 长期分支 | main + develop | 只有 main |
| 发布节奏 | 定期发布(周/月) | 持续部署(每天多次) |
| 适用场景 | 有固定发版周期的项目(App、硬件) | Web 服务、SaaS |
| 团队规模 | 中大型(10+ 人) | 不限 |
| 学习曲线 | 陡 | 平 |
| 回滚难度 | 低(有 release 分支做缓冲) | 中等(main 即生产) |
💡 建议:如果你的项目有「版本号」这个概念且用户不能随时更新(如手机 App),用 Git Flow。如果你的项目是 Web 服务、用户访问时自动拿到最新版,用 GitHub Flow。
# 四、保护分支
无论用哪种 Flow,有一个铁律必须强制执行:禁止任何人直接 push 到 main。
# 4.1 在 GitHub 上配置
在仓库 → Settings → Branches → Add branch protection rule:
Branch name pattern: main(或 master)
☑ Require a pull request before merging
☑ Require approvals (至少 1 人通过)
☑ Require status checks to pass before merging
☑ Require branches to be up to date before merging
☑ Include administrators (连管理员也不能绕过)
# 4.2 配置后的效果
# 当有人试图直接 push main:
git push origin main
# 输出:
# remote: error: GH006: Protected branch update failed for refs/heads/main.
# remote: error: At least 1 approving review is required.
# ! [remote rejected] main -> main (protected branch hook declined)
直接 push 被拒绝! 只能通过 Pull Request 合入。
# 4.3 保护分支的典型配置组合
# 最小保护(小团队推荐):
- 需要 PR 才能合并
- 至少 1 人审查通过
# 标准保护(中大型团队):
- 需要 PR 才能合并
- 至少 2 人审查通过
- CI 测试必须通过
- 分支必须是最新的(不能和 main 有冲突)
- 禁止 force push
# 严格保护(金融/医疗):
- 以上全部
- 要求的审查者必须来自 CODEOWNERS 文件
- 提交必须签名验证
- 合并前必须解决所有评论
# 五、提交规范
# 5.1 为什么消息格式很重要?
看两组 commit log:
# 组 A(混乱)
fix
update
改了一下
asdf
修 bug
feat: 具体的功能
真的修好了
# 组 B(规范)
feat: 添加商品搜索功能
fix: 修复购物车空数组崩溃
docs: 更新 API 文档
refactor: 重构支付模块回调逻辑
chore: 升级依赖到 v2.0
test: 添加搜索功能单元测试
组 B 的好处:
- 📜
git log --oneline就是你的 CHANGELOG - 🤖 可以自动生成版本号(
feat→ 次版本号 +1,fix→ 修订号 +1) - 👀 Code Review 时,审查者一秒知道你的意图
- 🔍
git log --grep="fix:"快速过滤所有修复
# 5.2 Conventional Commits 规范
<type>(<scope>): <subject>
# type(必填):
feat → 新功能
fix → 修复 bug
docs → 文档
style → 格式(不影响逻辑)
refactor → 重构(不增功能也不修 bug)
perf → 性能优化
test → 测试
chore → 构建/依赖/工具
ci → CI 配置
build → 构建系统
# scope(可选):
feat(cart): 添加购物车优惠计算
fix(auth): 修复 token 过期未刷新
docs(api): 更新商品接口文档
# subject(必填):
- 用祈使句("添加" 而不是 "添加了")
- 首字母小写
- 结尾不加句号
- 不超过 72 字符
# body(可选):
为什么这么改、怎么改的、有什么副作用
# footer(可选):
BREAKING CHANGE: 不兼容变更说明
Closes #123
# 5.3 Commit 规范速查
| 你的改动 | 用这个 type | 例子 |
|---|---|---|
| 加了登录功能 | feat | feat(auth): 添加用户登录接口 |
| 修了空指针崩溃 | fix | fix(cart): 修复空购物车 remove 崩溃 |
| 改了 README | docs | docs: 更新安装说明 |
| 重命名变量 | refactor | refactor(user): 重命名 getUser 为 fetchUser |
| 删了无用 import | style | style: 移除未使用的 import |
| 加了单元测试 | test | test(cart): 添加购物车计算测试 |
| 升级 React 版本 | chore | chore(deps): 升级 react 到 18.2 |
# 5.4 用交互式 rebase 修正之前的 message
# 最近 3 个 commit 的 message 写得不规范
git rebase -i HEAD~3
# 把 pick 改成 reword:
# reword aaa111 fix
# reword bbb222 update
# reword ccc333 改了一下
# Git 依次弹出编辑器让你改 message:
# fix → fix(cart): 修复空购物车崩溃
# update → feat(cart): 添加优惠计算
# 改了一下 → refactor(auth): 简化 token 验证逻辑
# 六、忽略文件
# 6.1 基本用法
# .gitignore 文件
node_modules/ # 忽略整个目录
dist/ # 构建产物
.env # 环境变量(含密钥!)
*.log # 所有日志文件
.DS_Store # macOS 系统文件
*.swp # vim 临时文件
.vscode/ # 编辑器配置(看团队约定)
# 例外:虽然忽略 .env,但要保留模板
!.env.example # 感叹号 = 不忽略
# 6.2 全局 .gitignore
# 对于所有项目的通用忽略(如 .DS_Store):
git config --global core.excludesfile ~/.gitignore_global
# 编辑 ~/.gitignore_global
echo ".DS_Store" >> ~/.gitignore_global
echo "*.swp" >> ~/.gitignore_global
# 6.3 如果已经提交了不该提交的文件
# 第一步:从 Git 跟踪中移除(但保留本地文件)
git rm --cached node_modules/ -r
# 第二步:把 node_modules/ 加入 .gitignore
echo "node_modules/" >> .gitignore
# 第三步:提交
git add .gitignore
git commit -m "chore: 从仓库移除 node_modules,加入 gitignore"
# 第四步:push
git push
# 七、CI/CD 集成
# 7.1 概念
git push ──→ CI 自动运行 ──→ 测试通过? ──→ 自动部署
│ │
└── 测试失败? ← 通知开发者修复
CI(持续集成):每次 push 自动跑测试、lint、构建。 CD(持续部署):测试通过后自动部署到生产环境。
# 7.2 GitHub Actions 示例
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm test
- run: npm run lint
有了这个文件,每次 push 或提 PR,GitHub 自动跑测试。测试不通过 → PR 不能合并(配合保护分支的 Require status checks)。
# 八、综合实战
# 场景设定
团队要发布电商平台 v1.1.0,这个版本包含:
- 小李:商品收藏功能
- 小王:订单历史查询
- 期间:一个线上紧急 bug(图片加载失败)
采用 GitHub Flow(因为这是个 Web 项目,持续部署)。
# 实战开始
mkdir team-workflow && cd team-workflow && git init
# 初始化项目
echo 'const app = { name: "Shop", version: "1.0.0" };' > app.js
echo 'node_modules/' > .gitignore
git add . && git commit -m "init: 电商平台 v1.0.0"
git tag -a v1.0.0 -m "v1.0.0 初始版本"
# ==================== Sprint 1:并行开发两个 feature ====================
# --- 小李:商品收藏 ---
git switch -c feature/商品收藏
echo '
const favorites = [];
function addFavorite(productId) {
if (!favorites.includes(productId)) {
favorites.push(productId);
}
}
function removeFavorite(productId) {
const idx = favorites.indexOf(productId);
if (idx > -1) favorites.splice(idx, 1);
}
' > favorites.js
git add . && git commit -m "feat(favorites): 添加商品收藏功能"
echo '
function isFavorite(productId) {
return favorites.includes(productId);
}
' >> favorites.js
git add . && git commit -m "feat(favorites): 添加收藏状态查询"
# 整理 commit
git rebase -i HEAD~2
# squash 成一个:feat(favorites): 实现完整商品收藏功能
git push -u origin feature/商品收藏
# 去 GitHub 创建 PR
# --- 小王:订单历史 ---
git switch main
git switch -c feature/订单历史
echo '
const orders = [];
function getOrderHistory(userId) {
return orders.filter(o => o.userId === userId);
}
function getOrderDetail(orderId) {
return orders.find(o => o.id === orderId);
}
' > orders.js
git add . && git commit -m "feat(orders): 添加订单历史查询"
echo '
function getOrdersByDate(userId, startDate, endDate) {
return orders.filter(o =>
o.userId === userId &&
o.date >= startDate &&
o.date <= endDate
);
}
' >> orders.js
git add . && git commit -m "feat(orders): 添加按日期范围查询"
git rebase -i HEAD~2
# squash → feat(orders): 实现完整订单历史查询功能
git push -u origin feature/订单历史
# 去 GitHub 创建 PR
# ==================== 审查 & 合并 ====================
# Code Review 流程:
# 1. 小李审查小王的 PR → 提了一个问题:"缓存策略是什么?"
# 2. 小王回复:"第一版不做缓存,下个 Sprint 加"→ 审查通过 → Merge
# 3. 小王审查小李的 PR → 建议:"addFavorite 应该返回布尔值表示是否成功"
# 4. 小李修改:
git switch feature/商品收藏
sed -i '' 's/favorites.push(productId);/favorites.push(productId); return true;/' favorites.js
git add . && git commit -m "fix(favorites): 收藏操作返回布尔结果"
git push origin feature/商品收藏
# PR 自动更新 → 审查通过 → Merge
# 拉最新 master
git switch main
git pull origin main
# ==================== 🚨 紧急:线上图片加载失败 ====================
# 用户反馈:商品详情页图片全部不显示
# 排查:CDN 链接过期
git switch -c hotfix/图片CDN main
echo 'const IMAGE_CDN = "https://new-cdn.example.com";' > config.js
git add . && git commit -m "hotfix(cdn): 修复图片 CDN 链接过期"
# 提 PR,紧急审查,2 分钟通过 → Merge 到 main
# 自动部署 → 线上恢复 🎉
# ==================== 发布 v1.1.0 ====================
git switch main
git pull origin main
# 更新版本号
echo 'const app = { name: "Shop", version: "1.1.0" };' > app.js
git add . && git commit -m "chore(release): 准备发布 v1.1.0"
git tag -a v1.1.0 -m "v1.1.0: 商品收藏 + 订单历史 + 图片 CDN 修复"
git push origin main --tags
# 🎉 v1.1.0 发布完成!
# ==================== Sprint 回顾 ====================
git log --oneline --graph main
# 输出:
# * chore(release): 准备发布 v1.1.0
# * Merge PR #3 (hotfix/图片CDN)
# * Merge PR #2 (feature/订单历史)
# * Merge PR #1 (feature/商品收藏)
# * init: 电商平台 v1.0.0
# 🎯 Sprint 时间线
周一 周三 周五
────┼───────────────────┼─────────────────────┼────→
│ │ │
Sprint 开始 🚨 线上 CDN bug v1.1.0 发布
小李开收藏分支 紧急 hotfix 修复 两个 feature 合并
小王开订单分支 15分钟搞定 打 tag 上线
│
└── hotfix 修复直接部署,不阻塞 feature 开发
# 九、本章回顾
| 我学会了什么 | 一句话总结 |
|---|---|
| Git Flow | 五种分支:main / develop / feature / release / hotfix,适合定期发版 |
| GitHub Flow | 只有 main + feature 分支,PR 合并即部署 |
| 保护分支 | 禁止直接 push main,必须 PR + 审查 + CI 通过 |
| Conventional Commits | feat: / fix: / docs: 等规范前缀,让 log 变成 changelog |
.gitignore | 排除 node_modules、.env、日志等不该进仓库的文件 |
| CI/CD | 每次 push 自动跑测试,通过后自动部署 |
| Code Review | 到具体代码行提建议,修改后 PR 自动更新 |
🎯 团队工作流的核心不是技术,是纪律:
- 永远不直接 push main——无论多小的改动,一律走 PR。
- Commit 用规范前缀——
feat:/fix:/docs:,让历史自己说话。- Hotfix 的修复必须同步到所有活跃分支——修了 main 别忘了 develop。
- 合并完就删分支——别让
git branch输出滚三页。
🏃 下一章预告:现在你已经能在团队中作战了。但 Git 不是完美的——有时候你会遇到各种报错、误操作、灾难现场。下一章是故障排除 & 常见问题:detached HEAD 是什么鬼、merge conflict 的五种场景、误删分支怎么恢复、大文件误提交怎么清理历史。从「会用」到「能排错」。
# 📎 本章涉及的命令速查
# === Git Flow 完整流程 ===
git branch develop # 从 main 创建 develop
git switch -c feature/xxx develop # 从 develop 开 feature
git merge --no-ff feature/xxx # 合并 feature(保留历史)
git switch -c release/v1.0.0 develop # 从 develop 开 release
git merge --no-ff release/v1.0.0 # release 合并到 main + develop
git tag -a v1.0.0 -m "message" # 打发布标签
git switch -c hotfix/xxx main # 从 main 开 hotfix
# hotfix 必须同时合并到 main 和 develop!
# === GitHub Flow ===
git switch -c feature/xxx main # 从 main 开 feature
git push -u origin feature/xxx # 推送
# 去 GitHub 创建 PR → 审查 → Merge → 部署
git branch -d feature/xxx # 清理本地
# === 保护分支(在 GitHub 网页端配置) ===
# Settings → Branches → Add rule
# Branch name pattern: main
# ☑ Require a pull request before merging
# ☑ Require approvals
# ☑ Require status checks to pass
# === Commit 规范 ===
# feat(scope): 描述 → 新功能
# fix(scope): 描述 → 修复 bug
# docs(scope): 描述 → 文档
# refactor(scope): 描述 → 重构
# chore(scope): 描述 → 构建/依赖
# test(scope): 描述 → 测试
# 修改历史 message:git rebase -i HEAD~N → reword
# === .gitignore ===
# node_modules/ → 依赖目录
# .env → 环境变量(密钥!)
# dist/ build/ → 构建产物
# *.log → 日志文件
# .DS_Store → macOS 系统文件
# !.env.example → 例外(不忽略模板)
# === 已推送的大文件清理 ===
git rm --cached <file> # 从跟踪中移除
git commit -m "chore: 移除大文件"
git push