跳到主要内容

Git 工作流

问题

在团队协作开发中,如何选择合适的 Git 工作流?Git Flow、GitHub Flow、Trunk Based Development 各有什么优劣?如何规范提交信息和版本管理?

答案

Git 工作流是团队协作开发的基石。选择合适的工作流能显著提升开发效率、降低冲突概率、保证代码质量。下面从三大主流工作流、提交规范、版本管理和常用命令等维度全面解析。


一、Git Flow 详解

Git Flow 是 Vincent Driessen 在 2010 年提出的经典分支模型,适合发布周期较长、需要严格版本管理的项目。

分支结构

五种分支类型

分支命名规范生命周期来源合入说明
mainmain / master永久--生产代码,每个 commit 对应一个版本
developdevelop永久main-开发主线,集成最新特性
feature/*feature/login临时developdevelop新功能开发
release/*release/1.0.0临时developmain + develop发布准备,只修 bug
hotfix/*hotfix/1.0.1临时mainmain + develop线上紧急修复

完整操作流程

git-flow-workflow.sh
# 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 应用。

流程图

核心规则

  1. main 分支始终是可部署的
  2. main 创建描述性分支(如 fix/header-layoutfeat/dark-mode
  3. 频繁 push 到远程同名分支
  4. 随时发起 Pull Request 进行讨论
  5. Code Review 通过后合并
  6. 合并后立即部署

操作示例

github-flow.sh
# 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 示例

utils/featureFlags.ts
// 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 FlowGitHub FlowTrunk Based
分支数量5 种分支2 种(main + feature)1 主干 + 短期分支
复杂度
发布方式版本发布持续部署持续部署
分支生命周期长(feature 可能数周)中(通常数天)短(1-2 天)
适用场景传统软件、移动端 AppWeb 应用、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新功能minorfeat: add dark mode
fixBug 修复patchfix: resolve login error
docs文档更新-docs: update API guide
style格式调整(不影响逻辑)-style: format with prettier
refactor重构(非新功能、非修复)-refactor: extract utils
perf性能优化patchperf: lazy load images
test测试相关-test: add unit tests
chore构建/工具/依赖-chore: update deps
ciCI 配置-ci: add GitHub Actions
revert回滚-revert: revert feat xyz

提交示例

conventional-commits-examples.sh
# 普通 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 install --save-dev @commitlint/cli @commitlint/config-conventional husky
npx husky init
echo 'npx commitlint --edit "$1"' > .husky/commit-msg
commitlint.config.ts
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.1fix修复表单验证
新功能1.0.1 -> 1.1.0feat添加暗色模式
破坏性变更1.1.0 -> 2.0.0feat! / BREAKING CHANGE修改 API 响应结构

预发布版本

# alpha: 内部测试版
v2.0.0-alpha.1

# beta: 公测版
v2.0.0-beta.1

# rc: 候选发布版
v2.0.0-rc.1

npm 版本范围

version-range.ts
// 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 install --save-dev @changesets/cli
# 1. 初始化
npx changeset init

# 2. 开发完成后,创建 changeset
npx changeset
# 交互式选择:哪些包受影响、版本升级类型、变更描述

# 3. 消费 changesets,更新版本号和 Changelog
npx changeset version

# 4. 发布
npx changeset publish

创建 changeset 后,会在 .changeset/ 目录下生成一个 markdown 文件:

.changeset/happy-dog-123.md
---
"@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 mergegit rebase
原理创建一个合并提交把 commits 「移植」到目标分支末端
提交历史保留完整分支历史(非线性)线性历史,更整洁
冲突处理一次性解决所有冲突逐个 commit 解决冲突
安全性不改写历史,安全改写 commit hash,公共分支慎用
适用场景合并 feature 到 main在 feature 上同步 main 的更新
rebase-vs-merge.sh
# 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 应用到当前分支:

cherry-pick.sh
# 摘取单个 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.sh
# 保存当前修改(包括已暂存和未暂存的)
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 resetgit revert
原理移动 HEAD 指针,「回退」历史创建一个新 commit 来「撤销」某次提交
是否改写历史
适用范围本地未推送的提交已推送的公共提交
安全性可能丢失提交安全,不影响其他人
reset-vs-revert.sh
# 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. 其他高频命令

useful-git-commands.sh
# 查看某个文件的修改历史
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 模板示例

.github/pull_request_template.md
## 变更类型

- [ ] feat: 新功能
- [ ] fix: Bug 修复
- [ ] refactor: 重构
- [ ] docs: 文档更新
- [ ] test: 测试相关
- [ ] chore: 其他

## 变更描述

<!-- 简要描述本次改动的内容和原因 -->

## 影响范围

<!-- 本次变更影响了哪些模块/页面 -->

## 测试方式

- [ ] 单元测试通过
- [ ] E2E 测试通过
- [ ] 手动测试通过

## 截图(如有 UI 变更)

## 相关 Issue

Closes #

CI 检查清单

.github/workflows/ci.yml
// 以下为 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 配置:

.github/workflows/ci.yml (YAML)
# 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、注入风险
测试是否有对应的测试用例
一致性是否符合项目编码规范
可维护性是否容易理解和修改
Code Review 建议
  • 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 合并到 mainmerge --no-ff保留 feature 分支历史
在 feature 上同步 main 最新代码rebase保持 feature 分支线性
已推送到远程的公共分支merge不改写历史,安全
本地未推送的私有分支rebase线性历史,整洁
整理本地多个零碎 commitrebase -i(交互式)squash 合并提交
recommended-workflow.sh
# 推荐工作流:在 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 是一套结构化的提交信息格式规范,核心好处有三个:

  1. 自动生成 Changelog:工具(如 conventional-changelog)可以根据 commit type 自动分类生成版本变更记录
  2. 自动决定版本号:配合 Semantic Versioning,fix 触发 patch、feat 触发 minor、BREAKING CHANGE 触发 major
  3. 提高可读性:团队成员一眼就能看出每个提交的类型和影响范围

BREAKING CHANGE(破坏性变更)指的是不向后兼容的 API 变更。标记方式有两种:

breaking-change-examples.sh
# 方式一:在 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)。

自动化工具链示例

release-config.ts
// 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)自动解决重复出现的冲突
enable-rerere.sh
# 开启 rerere,Git 会记住你的冲突解决方式
git config --global rerere.enabled true

相关链接