Git 工作流
问题
在团队协作开发中,如何选择合适的 Git 工作流?Git Flow、GitHub Flow、Trunk Based Development 各有什么优劣?如何规范提交信息和版本管理?
答案
Git 工作流是团队协作开发的基石。选择合适的工作流能显著提升开发效率、降低冲突概率、保证代码质量。下面从三大主流工作流、提交规范、版本管理和常用命令等维度全面解析。
一、Git Flow 详解
Git Flow 是 Vincent Driessen 在 2010 年提出的经典分支模型,适合发布周期较长、需要严格版本管理的项目。
分支结构
五种分支类型
| 分支 | 命名规范 | 生命周期 | 来源 | 合入 | 说明 |
|---|---|---|---|---|---|
main | main / master | 永久 | - | - | 生产代码,每个 commit 对应一个版本 |
develop | develop | 永久 | main | - | 开发主线,集成最新特性 |
feature/* | feature/login | 临时 | develop | develop | 新功能开发 |
release/* | release/1.0.0 | 临时 | develop | main + develop | 发布准备,只修 bug |
hotfix/* | hotfix/1.0.1 | 临时 | main | main + develop | 线上紧急修复 |
完整操作流程
# 1. 创建 feature 分支
git checkout develop
git checkout -b feature/user-auth
# 开发完成后合并回 develop
git checkout develop
git merge --no-ff feature/user-auth
git branch -d feature/user-auth
# 2. 创建 release 分支
git checkout develop
git checkout -b release/1.0.0
# 在 release 分支上修复 bug、更新版本号
# 完成后合并到 main 和 develop
git checkout main
git merge --no-ff release/1.0.0
git tag -a v1.0.0 -m "Release version 1.0.0"
git checkout develop
git merge --no-ff release/1.0.0
git branch -d release/1.0.0
# 3. 紧急修复 hotfix
git checkout main
git checkout -b hotfix/1.0.1
# 修复完成后合并到 main 和 develop
git checkout main
git merge --no-ff hotfix/1.0.1
git tag -a v1.0.1 -m "Hotfix version 1.0.1"
git checkout develop
git merge --no-ff hotfix/1.0.1
git branch -d hotfix/1.0.1
--no-ff(no fast-forward)参数非常关键,它会创建一个合并提交,保留分支历史信息。面试中经常会问到 fast-forward 和 no-fast-forward 的区别。
Git Flow 的分支模型较为复杂,对于迭代频繁的互联网项目可能过于繁重。Vincent Driessen 本人在 2020 年也补充说明,Git Flow 并非万能方案,持续交付的项目应考虑更轻量的工作流。
二、GitHub Flow
GitHub Flow 是 GitHub 推崇的极简工作流,核心理念是 main 分支始终可部署,适合持续部署的 Web 应用。
流程图
核心规则
main分支始终是可部署的- 从
main创建描述性分支(如fix/header-layout、feat/dark-mode) - 频繁 push 到远程同名分支
- 随时发起 Pull Request 进行讨论
- Code Review 通过后合并
- 合并后立即部署
操作示例
# 1. 从 main 创建功能分支
git checkout -b feat/dark-mode main
# 2. 开发并频繁推送
git add .
git commit -m "feat: add dark mode toggle"
git push origin feat/dark-mode
# 3. 在 GitHub 上发起 PR(也可用 gh CLI)
gh pr create --title "feat: add dark mode toggle" \
--body "## Summary\n- Add dark mode toggle\n- Persist preference in localStorage"
# 4. Review 通过后在 GitHub 上 Squash and Merge
# 5. 删除远程分支
git push origin --delete feat/dark-mode
GitHub Flow 只有一个长期分支 main,所有功能分支从 main 拉出、合并回 main。它依赖 CI/CD 管道保证代码质量,合并即部署。
三、Trunk Based Development
Trunk Based Development(TBD)是 Google、Meta 等大厂广泛采用的工作流,核心是所有开发者直接向 trunk(主干)提交代码。
流程图
核心原则
- 短生命周期分支:feature 分支不超过 1-2 天
- 频繁集成:每天至少合并一次到 trunk
- Feature Flag:通过特性开关控制未完成功能的可见性
- Release 分支:仅在需要时从 trunk 拉出,只做 bugfix
Feature Flag 示例
// Feature Flag 管理
interface FeatureFlags {
darkMode: boolean;
newCheckout: boolean;
aiRecommendation: boolean;
}
const defaultFlags: FeatureFlags = {
darkMode: true,
newCheckout: false,
aiRecommendation: false,
};
// 从远程配置中心获取 Feature Flags
async function getFeatureFlags(): Promise<FeatureFlags> {
try {
const response = await fetch('/api/feature-flags');
const remoteFlags: Partial<FeatureFlags> = await response.json();
return { ...defaultFlags, ...remoteFlags };
} catch {
return defaultFlags;
}
}
// 在组件中使用
function CheckoutPage(): JSX.Element {
const flags = useFeatureFlags();
if (flags.newCheckout) {
return <NewCheckoutFlow />;
}
return <LegacyCheckoutFlow />;
}
Trunk Based Development 要求团队具备较高的工程能力:完善的 CI/CD、充分的自动化测试、成熟的 Feature Flag 基础设施。它能极大地减少分支合并冲突,加速交付速度。
四、三种工作流对比
| 维度 | Git Flow | GitHub Flow | Trunk Based |
|---|---|---|---|
| 分支数量 | 5 种分支 | 2 种(main + feature) | 1 主干 + 短期分支 |
| 复杂度 | 高 | 低 | 中 |
| 发布方式 | 版本发布 | 持续部署 | 持续部署 |
| 分支生命周期 | 长(feature 可能数周) | 中(通常数天) | 短(1-2 天) |
| 适用场景 | 传统软件、移动端 App | Web 应用、SaaS | 大型团队、微服务 |
| 合并冲突 | 较多 | 适中 | 较少 |
| 团队规模 | 中小团队 | 中小团队 | 任意规模 |
| CI/CD 依赖 | 低 | 中 | 高 |
| Feature Flag | 不需要 | 可选 | 必要 |
| 代表公司 | 传统企业 | GitHub、创业公司 | Google、Meta |
- 小团队 + Web 应用 --> GitHub Flow(简单高效)
- 需要严格版本控制(如 App、SDK) --> Git Flow
- 大团队 + 持续交付 --> Trunk Based Development
- 开源项目 --> GitHub Flow(配合 Fork + PR)
五、Conventional Commits 提交规范
Conventional Commits 是一套结构化的提交信息规范,与 Semantic Versioning 配合使用,可以自动生成 Changelog、自动决定版本号。
提交格式
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
type 类型一览
| type | 说明 | 版本影响 | 示例 |
|---|---|---|---|
feat | 新功能 | minor | feat: add dark mode |
fix | Bug 修复 | patch | fix: resolve login error |
docs | 文档更新 | - | docs: update API guide |
style | 格式调整(不影响逻辑) | - | style: format with prettier |
refactor | 重构(非新功能、非修复) | - | refactor: extract utils |
perf | 性能优化 | patch | perf: lazy load images |
test | 测试相关 | - | test: add unit tests |
chore | 构建/工具/依赖 | - | chore: update deps |
ci | CI 配置 | - | ci: add GitHub Actions |
revert | 回滚 | - | revert: revert feat xyz |
提交示例
# 普通 feat
git commit -m "feat: add user registration form"
# 带 scope
git commit -m "feat(auth): add OAuth2 login"
# 带 body 和 footer(BREAKING CHANGE 会触发 major 版本升级)
git commit -m "feat(api)!: change response format
BREAKING CHANGE: API response now wraps data in { data, meta } structure.
The old flat response format is no longer supported.
Closes #123"
用 commitlint + husky 强制校验
- npm
- Yarn
- pnpm
- Bun
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
yarn add --dev @commitlint/cli @commitlint/config-conventional husky
pnpm add --save-dev @commitlint/cli @commitlint/config-conventional husky
bun add --dev @commitlint/cli @commitlint/config-conventional husky
- npm
- yarn
- pnpm
npx husky init
echo 'npx commitlint --edit "$1"' > .husky/commit-msg
yarn husky init
echo 'yarn commitlint --edit "$1"' > .husky/commit-msg
pnpm husky init
echo 'pnpm commitlint --edit "$1"' > .husky/commit-msg
import type { UserConfig } from '@commitlint/types';
const config: UserConfig = {
extends: ['@commitlint/config-conventional'],
rules: {
// type 必须是指定值之一
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'chore', 'ci', 'revert',
]],
// subject 不超过 72 个字符
'subject-max-length': [2, 'always', 72],
},
};
export default config;
BREAKING CHANGE 写在 footer 中,或者在 type 后加 !(如 feat!:),都会触发主版本号升级。这是面试中的高频考点。
六、Semantic Versioning 版本号
Semantic Versioning(语义化版本)格式为 MAJOR.MINOR.PATCH:
v2.3.1
│ │ │
│ │ └── PATCH: 向后兼容的 bug 修复
│ └──── MINOR: 向后兼容的新功能
└────── MAJOR: 不兼容的 API 变更
版本号规则
| 变更类型 | 版本变化 | Commit type | 示例 |
|---|---|---|---|
| Bug 修复 | 1.0.0 -> 1.0.1 | fix | 修复表单验证 |
| 新功能 | 1.0.1 -> 1.1.0 | feat | 添加暗色模式 |
| 破坏性变更 | 1.1.0 -> 2.0.0 | feat! / BREAKING CHANGE | 修改 API 响应结构 |
预发布版本
# alpha: 内部测试版
v2.0.0-alpha.1
# beta: 公测版
v2.0.0-beta.1
# rc: 候选发布版
v2.0.0-rc.1
npm 版本范围
// package.json 中的版本范围语法
const dependencies = {
// ^ (caret): 兼容 MINOR 和 PATCH 更新
"react": "^18.2.0", // >=18.2.0 <19.0.0
// ~ (tilde): 仅兼容 PATCH 更新
"lodash": "~4.17.21", // >=4.17.21 <4.18.0
// 精确版本
"typescript": "5.3.3", // 只能是 5.3.3
// 范围
"node": ">=18.0.0", // 大于等于 18
};
七、Changesets 版本管理
Changesets 是一套用于 Monorepo 的版本管理与 Changelog 生成工具,被 Radix UI、Pnpm 等知名项目采用。
基本流程
操作示例
- npm
- Yarn
- pnpm
- Bun
npm install --save-dev @changesets/cli
yarn add --dev @changesets/cli
pnpm add --save-dev @changesets/cli
bun add --dev @changesets/cli
- npm
- yarn
- pnpm
# 1. 初始化
npx changeset init
# 2. 开发完成后,创建 changeset
npx changeset
# 交互式选择:哪些包受影响、版本升级类型、变更描述
# 3. 消费 changesets,更新版本号和 Changelog
npx changeset version
# 4. 发布
npx changeset publish
# 1. 初始化
yarn changeset init
# 2. 开发完成后,创建 changeset
yarn changeset
# 3. 消费 changesets,更新版本号和 Changelog
yarn changeset version
# 4. 发布
yarn changeset publish
# 1. 初始化
pnpm changeset init
# 2. 开发完成后,创建 changeset
pnpm changeset
# 3. 消费 changesets,更新版本号和 Changelog
pnpm changeset version
# 4. 发布
pnpm changeset publish
创建 changeset 后,会在 .changeset/ 目录下生成一个 markdown 文件:
---
"@mylib/core": minor
"@mylib/react": patch
---
Add dark mode support to core theme engine.
React bindings updated to pass through new theme prop.
Changesets 特别适合 Monorepo 场景。每个 PR 附带一个 changeset 文件描述变更,合并后由 CI 统一处理版本更新和发布,避免了手动管理版本号的混乱。
八、Git 常用命令详解
1. rebase vs merge
这是面试中最高频的 Git 问题之一。
| 对比维度 | git merge | git rebase |
|---|---|---|
| 原理 | 创建一个合并提交 | 把 commits 「移植」到目标分支末端 |
| 提交历史 | 保留完整分支历史(非线性) | 线性历史,更整洁 |
| 冲突处理 | 一次性解决所有冲突 | 逐个 commit 解决冲突 |
| 安全性 | 不改写历史,安全 | 改写 commit hash,公共分支慎用 |
| 适用场景 | 合并 feature 到 main | 在 feature 上同步 main 的更新 |
# merge 方式:在 main 上合并 feature
git checkout main
git merge feature/login
# 结果:生成一个 merge commit,保留分支拓扑
# rebase 方式:在 feature 上变基到 main
git checkout feature/login
git rebase main
# 结果:feature 的 commits 被重新应用到 main 最新提交之后
# 注意:rebase 会改写 commit hash,不要对已推送的公共分支 rebase
# 交互式 rebase:整理提交历史
git rebase -i HEAD~3
# 可以 squash(合并)、reword(修改信息)、drop(删除)commits
黄金法则:不要对已推送到远程的公共分支执行 rebase。因为 rebase 会改写 commit hash,其他协作者 pull 时会产生大量冲突。只在本地的 feature 分支上 rebase。
2. cherry-pick
从其他分支「摘取」特定的 commit 应用到当前分支:
# 摘取单个 commit
git cherry-pick abc1234
# 摘取多个 commit
git cherry-pick abc1234 def5678
# 摘取一个范围(不包含起始 commit)
git cherry-pick abc1234..def5678
# 包含起始 commit 用三个点:abc1234...def5678
# 遇到冲突时
git cherry-pick --continue # 解决冲突后继续
git cherry-pick --abort # 放弃本次 cherry-pick
典型场景:
- hotfix 修复后,需要把同一个修复应用到 develop 分支
- 某个 feature 分支中有一个 commit 被其他分支急需
3. stash
临时保存工作区的修改,切换到其他分支处理事情后再恢复:
# 保存当前修改(包括已暂存和未暂存的)
git stash
# 保存时附带描述信息(推荐)
git stash push -m "WIP: user registration form"
# 查看 stash 列表
git stash list
# 恢复最近的 stash(保留 stash 记录)
git stash apply
# 恢复最近的 stash(删除 stash 记录)
git stash pop
# 恢复指定的 stash
git stash apply stash@{2}
# 保存时包含未追踪文件
git stash push -u -m "include untracked files"
# 从 stash 创建分支(避免恢复时冲突)
git stash branch new-branch stash@{0}
4. reset vs revert
| 对比维度 | git reset | git revert |
|---|---|---|
| 原理 | 移动 HEAD 指针,「回退」历史 | 创建一个新 commit 来「撤销」某次提交 |
| 是否改写历史 | 是 | 否 |
| 适用范围 | 本地未推送的提交 | 已推送的公共提交 |
| 安全性 | 可能丢失提交 | 安全,不影响其他人 |
# reset --soft: 回退到指定 commit,保留修改在暂存区
git reset --soft HEAD~1
# reset --mixed(默认): 回退到指定 commit,保留修改在工作区
git reset HEAD~1
# reset --hard: 回退到指定 commit,丢弃所有修改(危险!)
git reset --hard HEAD~1
# revert: 创建一个新 commit 撤销指定提交(安全)
git revert abc1234
# revert 一个 merge commit(需指定保留哪个 parent)
git revert -m 1 <merge-commit-hash>
面试核心回答:reset 是「回退历史」,适合本地操作;revert 是「反向提交」,适合撤销已推送的公共提交。团队协作中优先使用 revert,因为它不改写历史。
5. 其他高频命令
# 查看某个文件的修改历史
git log --oneline --follow -- src/App.tsx
# 查看某次提交的具体改动
git show abc1234
# 查看谁修改了某行代码(排查 bug 利器)
git blame src/utils/auth.ts
# 查看两个分支的差异
git diff main..feature/login
# 修改最近一次提交信息
git commit --amend -m "fix: correct typo in login form"
# 只修改最近一次提交的文件(不改 message)
git add forgotten-file.ts
git commit --amend --no-edit
# 找回被 reset --hard 丢失的提交
git reflog
git reset --hard HEAD@{2}
九、PR / MR 最佳实践
Pull Request(GitHub)/ Merge Request(GitLab)是 Code Review 的核心载体。
PR 规范
PR 模板示例
## 变更类型
- [ ] feat: 新功能
- [ ] fix: Bug 修复
- [ ] refactor: 重构
- [ ] docs: 文档更新
- [ ] test: 测试相关
- [ ] chore: 其他
## 变更描述
<!-- 简要描述本次改动的内容和原因 -->
## 影响范围
<!-- 本次变更影响了哪些模块/页面 -->
## 测试方式
- [ ] 单元测试通过
- [ ] E2E 测试通过
- [ ] 手动测试通过
## 截图(如有 UI 变更)
## 相关 Issue
Closes #
CI 检查清单
// 以下为 GitHub Actions 配置的 TypeScript 风格伪代码展示
// 实际 CI 配置使用 YAML
interface CIChecks {
// 必须通过的检查项
lint: 'eslint + prettier 代码规范检查';
typeCheck: 'TypeScript 类型检查';
unitTest: '单元测试 + 覆盖率';
build: '构建是否成功';
commitlint: '提交信息规范检查';
// 可选检查项
e2e?: 'Playwright / Cypress E2E 测试';
bundleSize?: 'Bundle 体积变化检查';
lighthouse?: 'Lighthouse 性能跑分';
preview?: '预览部署(Vercel / Netlify)';
}
实际的 GitHub Actions 配置:
# name: CI
# on: [push, pull_request]
# jobs:
# check:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: pnpm/action-setup@v2
# - uses: actions/setup-node@v4
# with:
# node-version: 20
# cache: pnpm
# - run: pnpm install --frozen-lockfile
# - run: pnpm lint
# - run: pnpm type-check
# - run: pnpm test --coverage
# - run: pnpm build
Code Review 要点
| 审查维度 | 关注点 |
|---|---|
| 正确性 | 逻辑是否正确,边界条件是否处理 |
| 可读性 | 命名是否清晰,注释是否充分 |
| 性能 | 是否有不必要的循环、渲染 |
| 安全性 | 是否有 XSS、注入风险 |
| 测试 | 是否有对应的测试用例 |
| 一致性 | 是否符合项目编码规范 |
| 可维护性 | 是否容易理解和修改 |
- PR 大小控制在 200-400 行以内,过大的 PR 审查质量会急剧下降
- 使用 Draft PR 提前展示设计思路,获取早期反馈
- Review 时关注「为什么这样做」而不仅仅是「做了什么」
- 善用 GitHub 的 Suggestion 功能,直接在 Review 中提供修改建议
常见面试问题
Q1: git rebase 和 git merge 有什么区别?什么时候用 rebase,什么时候用 merge?
答案:
两者都是整合分支的方式,但原理和效果完全不同:
git merge 会创建一个合并提交(merge commit),保留完整的分支拓扑历史:
# merge 前
# main: A---B---C
# feature: \---D---E
# merge 后
# main: A---B---C-------F (merge commit)
# feature: \---D---E-/
git rebase 会把当前分支的 commits 「移植」到目标分支的末端,产生线性历史:
# rebase 前
# main: A---B---C
# feature: \---D---E
# rebase 后(在 feature 上执行 git rebase main)
# main: A---B---C
# feature: \---D'---E' (新的 commit hash)
使用场景对比:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| feature 合并到 main | merge --no-ff | 保留 feature 分支历史 |
| 在 feature 上同步 main 最新代码 | rebase | 保持 feature 分支线性 |
| 已推送到远程的公共分支 | merge | 不改写历史,安全 |
| 本地未推送的私有分支 | rebase | 线性历史,整洁 |
| 整理本地多个零碎 commit | rebase -i(交互式) | squash 合并提交 |
# 推荐工作流:在 feature 分支上用 rebase 同步 main
git checkout feature/login
git rebase main # 把 feature 的 commits 移到 main 最新提交之后
# 合并时用 merge --no-ff 保留历史
git checkout main
git merge --no-ff feature/login
核心原则:永远不要对已推送到远程的公共分支执行 rebase,因为 rebase 会改写 commit hash,导致其他协作者出现冲突。
Q2: Conventional Commits 规范有什么好处?BREAKING CHANGE 是什么?
答案:
Conventional Commits 是一套结构化的提交信息格式规范,核心好处有三个:
- 自动生成 Changelog:工具(如 conventional-changelog)可以根据 commit type 自动分类生成版本变更记录
- 自动决定版本号:配合 Semantic Versioning,
fix触发 patch、feat触发 minor、BREAKING CHANGE触发 major - 提高可读性:团队成员一眼就能看出每个提交的类型和影响范围
BREAKING CHANGE(破坏性变更)指的是不向后兼容的 API 变更。标记方式有两种:
# 方式一:在 type 后加 !
git commit -m "feat(api)!: change response format to envelope pattern"
# 方式二:在 footer 中写 BREAKING CHANGE
git commit -m "feat(api): change response format
BREAKING CHANGE: Response now uses { data, meta, error } envelope.
Old format { result, status } is removed."
两种方式都会触发主版本号升级(如 1.x.x -> 2.0.0)。
自动化工具链示例:
// semantic-release 配置
const config = {
branches: ['main'],
plugins: [
'@semantic-release/commit-analyzer', // 分析 commit 类型
'@semantic-release/release-notes-generator', // 生成 release notes
'@semantic-release/changelog', // 更新 CHANGELOG.md
'@semantic-release/npm', // 发布到 npm
'@semantic-release/github', // 创建 GitHub Release
'@semantic-release/git', // 提交版本号变更
],
};
export default config;
配合 commitlint + husky,可以在 git commit 时自动校验提交信息是否符合规范,不符合则拒绝提交。
Q3: 你在团队中是如何管理 Git 工作流和 Code Review 的?
答案:
这是一道开放性题目,建议从以下几个层面组织回答:
1. 分支策略:
根据项目特点选择合适的工作流。Web 项目通常选择 GitHub Flow,从 main 拉出 feature 分支,通过 PR 合并:
# 分支命名规范
feat/user-auth # 新功能
fix/login-redirect # Bug 修复
refactor/api-client # 重构
chore/update-deps # 依赖更新
docs/api-guide # 文档
2. 提交规范:
使用 Conventional Commits + commitlint + husky 强制校验:
// 完整工具链
const toolchain = {
commitlint: '校验 commit message 格式',
husky: 'Git hooks 管理,commit-msg 钩子触发 commitlint',
'lint-staged': 'pre-commit 钩子只检查暂存文件',
czg: '交互式 commit 辅助工具(替代 commitizen)',
};
3. PR / Code Review 流程:
- PR 使用统一模板,包含变更描述、影响范围、测试方式
- PR 大小控制在 200-400 行,过大则拆分
- 至少 1 人 Approve 才能合并
- CI 必须全部通过(lint、type-check、test、build)
- 合并策略使用 Squash and Merge,保持 main 分支每个 commit 对应一个完整功能
4. 自动化:
const automation = {
'pre-commit': 'lint-staged(ESLint + Prettier)',
'commit-msg': 'commitlint 校验提交信息',
'CI/CD': 'GitHub Actions(lint -> test -> build -> deploy)',
'preview': 'Vercel Preview Deployments(每个 PR 自动部署预览)',
'release': 'changesets 或 semantic-release 自动版本管理',
'dependabot': '自动依赖更新 PR',
};
5. 冲突处理:
- 鼓励频繁 rebase main 到 feature 分支,减少最终合并冲突
- 大的重构提前通知团队,避免多人修改同一文件
- 使用
git rerere(reuse recorded resolution)自动解决重复出现的冲突
# 开启 rerere,Git 会记住你的冲突解决方式
git config --global rerere.enabled true