Skip to content

Git 工作流实践

Git 工作流是团队协作开发中的重要规范,合理的分支策略和提交规范能够显著提升团队整体的开发效率与项目代码的可维护性。

分支策略

基础分支模型

main           # 主分支(稳定版本,生产环境代码)
├── develop    # 开发分支(可选,集成测试环境)
└── feature/*  # 功能分支(开发新功能)

分支说明

分支类型命名规范用途生命周期
main固定生产环境代码,随时可部署永久
develop固定开发环境,集成所有功能永久
feature/*feature/功能名开发新功能临时
bugfix/*bugfix/问题描述修复已知 bug临时
hotfix/*hotfix/紧急问题生产环境紧急修复临时
release/*release/版本号发布准备(测试、文档)临时

日常开发流程

1. 开始新功能

bash
# 方式 A:基于 main 创建功能分支(开始前当前分支:任意)
git checkout main
# 当前分支:main
git pull origin main
# 当前分支:main
git checkout -b feature/user-login
# 当前分支:feature/user-login

# 或者基于 develop 分支
# 方式 B:基于 develop 创建功能分支(开始前当前分支:任意)
git checkout develop
# 当前分支:develop
git pull origin develop
# 当前分支:develop
git checkout -b feature/user-login
# 当前分支:feature/user-login

2. 开发过程

bash
# 当前分支:feature/*(功能分支)
# 查看修改状态
git status

# 添加修改到暂存区
git add .                    # 添加所有修改
git add src/Login.tsx        # 添加特定文件

# 提交修改
git commit -m "feat: 实现用户登录功能"

# 查看提交历史
git log --oneline -10

3. 推送到远程

bash
# 当前分支:feature/*(功能分支)
# 首次推送(创建远程分支)
git push -u origin feature/user-login

# 后续推送
git push

4. 合并到目标分支(main 或 develop)

如果团队使用 develop 作为集成分支,日常合并目标应为 develop,发布时再将 develop 合并到 main;如果不使用 develop,则直接合并到 main

bash
# 切换回目标分支(开始前当前分支:任意;下方以 main 为例,若使用 develop,将 main 替换为 develop)
git checkout main

# 当前分支:main
git pull origin main

# 当前分支:main
git merge feature/user-login

# 如果有冲突,解决后:
git add .
git commit -m "merge: 合并用户登录功能"

# 推送到远程
git push origin main

5. 清理功能分支

bash
# 删除分支时,开始前当前分支:任意(不要停留在要删除的分支上)
# 删除本地分支
git branch -d feature/user-login

# 删除远程分支
git push origin --delete feature/user-login

高级工作流

Rebase 工作流(推荐)

这套流程的目标是:当主干(通常是 main,也可能是 develop)不断有新提交时,你的功能分支能随时“跟上最新基座”,同时让提交历史尽量线性,方便代码审查与回滚定位。

下面用一个完整的真实场景来讲清楚每一步“做什么”和“为什么”:

场景:你从 main(A - B - C)的提交 C 拉出功能分支 feature/login,在上面提交了 c1、c2。这期间其他人把 main 更新到了 D。你想把 feature/login 同步到最新的 main 再提 PR。

当前状态(开始 rebase 前)可以这样理解:

  • 你现在应当在功能分支:feature/loginHEAD 指向最新提交 c2
  • 远程的 origin/main 已经推进到 D,但你本地还不一定知道,需要先 fetch 才能拿到 D
text
开始前(你本地可能还没 fetch 到远程的 D):

main:        A──B──C          (本地 main)
origin/main: A──B──C          (本地记录的“远程 main”,可能还是旧的)

                   └──c1──c2   (feature/login,你当前在这里,HEAD -> c2)

执行 git fetch origin 之后:

main:        A──B──C          (本地 main 不会变化)
origin/main: A──B──C──D       (更新为远程最新)

                   └──c1──c2   (feature/login,你当前分支也不变)

1)确认工作区干净

bash
# 当前分支:feature/login(功能分支)
git status
  • 为什么:rebase 会逐个“重放”提交;如果你还有未提交的改动,冲突会更难分辨,也更容易误操作。
  • 经验做法:先把改动提交成 WIP(Work In Progress,表示“未完成但先保存现场”的临时提交,例如 git commit -m "WIP: login form"),或先 stash,再开始 rebase。

2)只拉取远程最新状态(当前分支不变;更新本地的远程跟踪分支)

bash
# 当前分支:feature/login(功能分支)
git fetch origin main
  • 这一步做了什么:只更新 origin/main(远程跟踪分支),让它反映远程 main 的最新状态。
  • 这一步没做什么:不会更新本地 main,也不会把任何东西合并到你当前分支。
  • 为什么:过程更可控;你可以先拿到远程最新状态,再决定用 rebase、merge 或其他方式同步。

3)在功能分支上执行 rebase(当前分支:feature/login,把 c1/c2 搬到 D 后面)

bash
# 当前分支:任意
git checkout feature/login
# 当前分支:feature/login(功能分支)
git rebase origin/main

你可以把这一步理解为:

text
rebase 之前:
main:    …───C───D
feature:      └──c1──c2   (HEAD)

rebase 之后:
main:    …───C───D
feature:          └──c1'──c2'  (HEAD)
  • 做了什么:把功能分支相对 origin/main “多出来的提交”(c1、c2)按顺序拿出来,依次套用到 D 之后。
  • 为什么有撇号:c1/c2 会变成 c1'/c2',因为它们的父提交从 C 变成了 D,提交 SHA 必然变化(这就是“改写历史”)。

4)如果遇到冲突:解决后继续(当前分支:feature/login;或必要时中止)

bash
# 当前分支:feature/login(rebase 进行中)
git add .
git rebase --continue
  • 为什么会冲突:main 的 D 可能改了与你的 c1/c2 相同的代码位置,Git 不知道该保留哪边,需要你手动裁决。
  • 如果处理不下去:可以 git rebase --abort 回到 rebase 前状态,再换 merge 或请同事一起看冲突。

5)如果该功能分支已经推送过:用更安全的方式更新远程分支(当前分支:feature/login)

bash
# 当前分支:feature/login(功能分支)
git push --force-with-lease
  • 为什么要“强推”:rebase 后提交 SHA 变了,远程分支历史必须被更新。
  • 为什么用 --force-with-lease:它会在远端分支被他人更新时拒绝覆盖,避免误删同事提交(比 --force 更安全)。

6)合并到主干(目标分支:main)

不要对多人共享分支(公共的 main/develop)随意 rebase:共享分支被改写历史会让其他人的分支历史对不上。

具体操作(命令行:合并到本地 main 后推送远程 main;如果远程 main 受保护则需通过 PR 合并):

1)推送功能分支到远程(确保远端可见、便于 CI/评审)

bash
# 当前分支:feature/login(功能分支)
git push -u origin feature/login

2)更新本地 main 到远程最新(避免在旧基座上合并)

bash
# 当前分支:任意
git checkout main
# 当前分支:main
git pull --ff-only origin main

3)在本地 main 合并功能分支并推送

bash
# 当前分支:main
git merge feature/login
# 当前分支:main
git push origin main

4)清理分支(可选)

bash
# 当前分支:任意(不要停留在要删除的分支上)
git branch -d feature/login
git push origin --delete feature/login

如何“产生 merge commit”(两种方式):

1)在 GitHub PR 页面选择 “Merge pull request”(合并提交)

  • Base 选择 main,Compare 选择 feature/login
  • 通过 Checks / Review 后,选择 Merge pull request 并合并

2)在命令行强制产生 merge commit

bash
# 当前分支:main
git merge --no-ff feature/login

补充:如果 git merge feature/login 可以快进(fast-forward),则可能不会产生 merge commit;需要 merge commit 时用 --no-ff

补充:常见的三种策略

  • Merge Commit:合并时创建一个“合并提交”,把功能分支整体合入(信息完整、保留分支关系;缺点是主干历史会更“分叉”)。
  • Squash Merge:把功能分支上的多个提交压缩成 1 个提交再合入(主干最干净、回滚一个提交就能撤回整个功能;缺点是主干上看不到功能分支的细粒度提交)。
  • Rebase & Merge:把功能分支的提交按顺序“接”到目标分支后面,不产生合并提交(主干线性且保留每个提交;缺点是合并后提交 SHA 会变化,且不显式体现“这是一次合并”)。

策略选择:

  • 业务迭代型团队常用 Squash Merge:主干历史更短、更像“发布日志”
  • 偏底层/框架类项目常用 Rebase & Merge:保留每个提交,且保持线性历史
  • 需要保留“分支合并关系”(例如长周期分支或复杂发布分支)更偏向 Merge Commit

GitHub/GitLab 都支持在仓库设置中限制可用的合并方式,团队通常会只保留 1~2 种以减少分歧。

Cherry-pick 特定提交

bash
# 当前分支:你希望接收提交的分支(例如 feature/* 或 hotfix/*)
# 挑选特定提交到当前分支
git cherry-pick <commit-hash>

# 挑选多个提交
git cherry-pick <commit1> <commit2>

# 挑选一个范围(不包含 start)
git cherry-pick <start-commit>..<end-commit>

📋 提交规范(Conventional Commits)

提交消息格式

<type>(<scope>): <subject>

<body>

<footer>

Type 类型

Type说明示例
feat新功能feat: 添加用户注册功能
fixBug 修复fix: 修复登录失败的问题
docs文档修改docs: 更新 API 文档
style代码格式(不影响逻辑)style: 格式化代码
refactor重构(不修复 bug 也不添加功能)refactor: 重构登录模块
perf性能优化perf: 优化列表渲染性能
test添加测试test: 添加登录单元测试
chore构建/工具变动chore: 升级 React 到 19
ciCI 配置修改ci: 添加 GitHub Actions
revert回滚提交revert: 回滚提交 abc123

示例

bash
# 当前分支:任意(通常在 feature/* 上提交)
# 简单提交
git commit -m "feat: 添加商品搜索功能"

# 详细提交
git commit -m "feat(search): 添加商品搜索功能

- 支持按名称、分类搜索
- 添加搜索历史记录
- 实现搜索结果高亮

Closes #123"

常用 Git 命令

查看状态

bash
# 当前分支:任意
git status                    # 查看工作区状态
git log --oneline -10         # 查看最近 10 条提交
git log --graph --all         # 图形化查看所有分支
git diff                      # 查看工作区修改
git diff --staged             # 查看暂存区修改

撤销修改

bash
# 当前分支:任意
# 撤销工作区修改(未 add)
git checkout -- <file>
git restore <file>            # 新命令

# 撤销暂存区修改(已 add,未 commit)
git reset HEAD <file>
git restore --staged <file>   # 新命令

# 撤销最后一次提交(保留修改)
git reset --soft HEAD~1

# 撤销最后一次提交(丢弃修改)
git reset --hard HEAD~1

# 修改最后一次提交消息
git commit --amend -m "新的提交消息"

分支操作

bash
# 当前分支:任意
# 查看分支
git branch                    # 本地分支
git branch -r                 # 远程分支
git branch -a                 # 所有分支

# 创建并切换分支
git checkout -b feature/new
git switch -c feature/new     # 新命令

# 切换分支
git checkout main
git switch main               # 新命令

# 删除分支
git branch -d feature/old     # 安全删除(已合并)
git branch -D feature/old     # 强制删除

远程操作

bash
# 当前分支:任意
# 查看远程仓库
git remote -v

# 拉取远程更新(fetch 不改变当前分支;pull 会把拉取到的内容合并到“当前分支”)
git fetch origin              # 只拉取不合并
git pull origin main          # 通常在 main 上执行,用于更新本地 main

# 推送到远程
git push origin main
git push -u origin feature/new  # 首次推送

# 查看远程分支
git ls-remote origin

冲突解决

合并冲突

当两个分支修改了同一文件的同一位置时会产生冲突:

bash
# 当前分支:目标分支(例如 main)
# 1. 尝试合并
git merge feature/user-login

# 2. 出现冲突提示
Auto-merging src/App.tsx
CONFLICT (content): Merge conflict in src/App.tsx

# 3. 打开冲突文件,手动解决冲突
# 文件中会看到:
# <<<<<<< HEAD
# 当前分支的内容
# =======
# 要合并分支的内容
# >>>>>>> feature/user-login

# 4. 解决冲突后,标记为已解决
git add src/App.tsx

# 5. 完成合并
git commit -m "merge: 解决冲突并合并用户登录功能"

Rebase 冲突

bash
# 当前分支:功能分支(例如 feature/*)
# 1. 开始 rebase
git rebase main

# 2. 出现冲突,解决后:
git add <file>
git rebase --continue

# 3. 如果想放弃 rebase
git rebase --abort

💡 最佳实践

1. 提交粒度

  • 一个提交只做一件事:方便回滚和审查
  • 频繁提交:每完成一个小功能就提交
  • 避免巨大的提交:难以审查和回滚

2. 分支管理

  • 及时删除已合并的分支:保持仓库整洁
  • 功能分支从最新的主分支创建
  • 定期同步主分支到功能分支:避免大冲突

3. 代码审查

  • 使用 Pull Request(PR) 进行代码审查
  • 小而频繁的 PR 比大而少的 PR 更容易审查
  • PR 描述清晰:说明修改内容和原因

4. 保护主分支

在 GitHub/GitLab 中设置分支保护规则:

  • 禁止直接推送到 main
  • 要求 PR 审查通过后才能合并
  • 要求 CI 通过后才能合并

🎯 团队协作建议

对于小团队(2-5 人)

采用简化的工作流:

main
└── feature/*
  • 所有功能分支从 main 创建
  • 完成后直接合并回 main
  • 使用 PR 进行代码审查

对于中大型团队(5+ 人)

采用 Git Flow:

main
├── develop
├── feature/*
├── release/*
└── hotfix/*
  • 日常开发在 develop 分支
  • 准备发布时创建 release 分支
  • 紧急修复使用 hotfix 分支

完整流程:

1)日常开发(feature 开发与集成)

  • developfeature/* 开发
  • feature/* 通过 PR 合并回 develop

2)发版流程(release 冻结与发布回流)

  • developrelease/x.y.z(冻结范围,进入测试/修复阶段)
  • release/x.y.z 上只做修复与发布准备(不再引入新特性)
  • release/x.y.z 合并到 main(发布),并在 main 打 tag:vX.Y.Z
  • release/x.y.z 还需要合并回 develop(把修复回流,避免下次发布丢失修复)
  • 删除 release/x.y.z

3)紧急修复(hotfix)

  • mainhotfix/x.y.(z+1) 修复线上问题
  • hotfix/* 合并回 main(发布),并在 main 打 tag
  • hotfix/* 还需要合并回 develop(把修复回流)
  • 删除 hotfix/*

补充:合并与打 tag 的两种做法

  • 方式 A:通过 PR 合并(推荐,适合远程 main 受保护的仓库)

    • 在 GitHub 创建 PR:Base 选 main,Compare 选 release/x.y.z
    • 等待 CI Checks 通过、Reviewer Approve 后合并(一般用 Merge Commit)
    • 合并后在 main 打 tag 并推送 tag:vX.Y.Z
  • 方式 B:命令行在本地 main 合并(仅适用于允许直接 push 远程 main 的仓库)

    bash
    # 当前分支:任意
    git checkout main
    # 当前分支:main
    git pull --ff-only origin main
    
    # 当前分支:main
    git merge --no-ff release/x.y.z
    git push origin main
    
    # 当前分支:main(打 tag 并推送)
    git tag vX.Y.Z
    git push origin vX.Y.Z

Released under the MIT License.