Git版本控制与团队协作
Git 是 CI/CD 的输入层。流水线能不能稳定,首先取决于代码历史是否可追溯、分支是否可合并、发布点是否可复现、回滚路径是否明确。
这篇不把 Git 当命令大全,而是把它当团队协作系统:本地如何组织改动,分支如何进入主干,tag 如何驱动发布,事故时如何恢复。
1. Git 的核心模型
Git 管理的是一张 commit graph,不是文件夹快照列表。每个 commit 指向父 commit,branch / tag / HEAD 都只是指向 commit 的引用。
working tree --git add--> index --git commit--> repository
当前文件 暂存区 commit graph
| 概念 | 本质 | 工程意义 |
|---|---|---|
| working tree | 当前磁盘文件 | 还没进入版本历史,最容易误删 |
| index | 下一次提交的暂存快照 | 用来拆 commit、控制提交边界 |
| commit | 一次不可变快照 + 元信息 | 可审计、可回滚、可定位问题 |
| branch | 可移动指针 | 团队协作的临时工作线 |
| tag | 通常不移动的发布指针 | CI/CD 的版本锚点 |
| HEAD | 当前检出的 commit / branch | 决定下一次 commit 接到哪里 |
| remote-tracking branch | 本地记录的远端状态 | 例如 origin/main,不是远端实时状态 |
为什么这很重要:大多数 Git 事故都来自混淆了“移动 branch 指针”“改暂存区”“改工作区”这三件事。先判断命令影响哪一层,再执行。
2. 日常工作流
2.1 开始前同步主干
git switch main
git fetch origin
git pull --ff-only
git switch -c feat/login-rate-limit
--ff-only 的价值是发现本地分支已经分叉时立即停止,避免无意识产生 merge commit。
2.2 小步提交
git status --short
git diff
git add src/auth/rate-limit.ts
git diff --cached
git commit -m "feat(auth): add login rate limit"
提交边界建议按“可独立 review / 可独立回滚”拆,而不是按“今天写了哪些文件”拆。
2.3 推送并创建 PR
git push -u origin feat/login-rate-limit
PR 不是补流程,而是主干保护的一部分。最低要求:
- CI 状态全绿
- 至少 1 人 review
- 不允许直接 push
main - 合并后删除 feature branch
- 对生产相关变更要求变更说明和回滚方案
3. 看懂历史
3.1 常用历史视图
git log --oneline --graph --decorate --all
git show HEAD
git diff main...HEAD
git diff --name-only main...HEAD
这里要区分 git log 的“提交集合”和 git diff 的“内容比较”。main..HEAD 和 main...HEAD 在不同子命令里的语义不完全一样:
| 写法 | 含义 | 常见用途 |
|---|---|---|
git log main..HEAD | HEAD 可达、main 不可达的提交 | 看当前分支多了哪些 commit |
git diff main..HEAD | 比较 main 和 HEAD 两个端点的内容差异 | 看两个分支当前快照差异 |
git diff main...HEAD | 从 main 和 HEAD 的共同祖先比较到 HEAD | PR diff 更接近这个语义 |
不要把 git log main...HEAD 直接等同于 PR diff;在提交集合里,三点表示两边可达但不同时可达的对称差集。
3.2 blame 不是甩锅工具
git blame src/api/client.ts
git show <commit>
git blame 只能告诉你某一行最近是谁改的,不能直接说明事故原因。真实排查应该结合 commit message、PR、测试、发布时间和监控。
4. 分支策略
4.1 GitHub Flow
main ──●──●──●──●
\ \
feature ●──● PR -> merge
适合大多数前端团队:
main永远可发布- feature branch 通过 PR 合并
- main 合并后自动部署 staging
- release tag 或审批后部署 production
4.2 Trunk-Based Development
只有短生命周期分支,甚至直接向主干集成,但依赖强 CI 和 feature flag。
适用条件:
- 单测 / E2E / 类型检查速度足够快
- 变更可通过 feature flag 隐藏
- 团队能接受高频小 PR
不适合:测试薄弱、发布周期长、多人长期占用同一模块的大改。
4.3 Git Flow
main / develop / release/* / hotfix/* 的重型流程适合版本周期明确、发布窗口严格的产品。前端业务迭代通常不建议默认使用,因为长期分支会放大冲突和集成风险。
5. merge、rebase、cherry-pick
5.1 merge
git switch main
git merge --no-ff feat/login-rate-limit
merge 保留分支结构,不改已有 commit。适合公共分支、集成分支、需要保留完整上下文的合并。
5.2 rebase
git switch feat/login-rate-limit
git fetch origin
git rebase origin/main
rebase 会把当前分支的 commit 重新接到新的 base 上,历史更线性,但会重写当前分支 commit id。
团队规则:
- 本地未共享分支:可以 rebase。
- 自己的 PR 分支:可以 rebase 后
--force-with-lease,前提是确认没有别人基于它提交。 main、release/*等公共分支:不要 rebase。
5.3 cherry-pick
git switch release/1.8
git cherry-pick <fix-commit>
cherry-pick 适合把一个已验证的小修复移植到 release / hotfix 分支。不适合批量搬复杂 feature;那通常说明分支策略已经失控。
6. reset、revert、restore、checkout
这四类命令最容易造成事故。
| 命令 | 影响 | 是否改历史 | 团队建议 |
|---|---|---|---|
git restore <file> | 恢复工作区文件 | 否 | 丢弃本地文件改动时首选 |
git restore --staged <file> | 从暂存区撤出文件 | 否 | 拆 commit 时常用 |
git reset --soft <commit> | 移动 HEAD,保留 index 和工作区 | 是 | 本地整理 commit 可用 |
git reset --mixed <commit> | 移动 HEAD,重置 index,保留工作区 | 是 | 默认模式,本地重新组织提交 |
git reset --hard <commit> | 移动 HEAD,覆盖 index 和工作区 | 是且丢本地改动 | 只在明确备份后使用 |
git revert <commit> | 新建一个反向 commit | 否 | 公共分支回滚首选 |
git checkout <ref> | 老命令,多义 | 视场景而定 | 新脚本中优先用 switch / restore |
公共分支上的错误提交,用 revert:
git switch main
git pull --ff-only
git revert <bad-commit>
git push origin main
本地提交还没推送,想重新整理:
git reset --soft HEAD~2
git commit -m "feat(auth): add rate limit with tests"
reset --hard 前至少先看:
git status --short
git branch backup/before-hard-reset
7. 冲突处理
冲突不是 Git 失败,而是 Git 无法判断业务语义。
git status
# 打开冲突文件,处理 <<<<<<< ======= >>>>>>> 区域
git add <resolved-file>
git rebase --continue # rebase 场景
# 或
git merge --continue # merge 场景
处理原则:
- 先理解双方意图,不只保留“自己的代码”。
- 每个冲突文件处理后跑相关测试。
- 大范围格式化和业务改动不要混一个 PR,否则冲突会被放大。
- 长期分支每天同步主干,别等上线前一次性合并。
8. tag 与发布
发布系统应该绑定不可变引用。生产发布推荐使用 tag 或唯一镜像 digest,而不是“当前 main”。
npm version patch
git push origin main
git push origin v1.8.3
常见约定:
| 产物 | 推荐标识 |
|---|---|
| npm package | semver,例如 1.8.3 |
| Docker image | v1.8.3 + sha-<short-sha> |
| 静态资源包 | frontend-v1.8.3-<short-sha>.tar.gz |
| Sentry release | frontend@1.8.3+<short-sha> |
为什么要同时带版本和 sha:版本给人看,sha 给机器追溯。线上问题能精确定位到源码。
9. 回滚与恢复
9.1 回滚公共分支
git revert <bad-commit>
如果要回滚一组提交:
git revert --no-commit <oldest>^..<newest>
git commit -m "revert(release): rollback broken checkout flow"
回滚 merge commit 需要指定主线父节点:
git revert -m 1 <merge-commit>
这类操作建议在 PR 中完成,让 CI 和 reviewer 帮你兜底。
9.2 找回误删提交
git reflog
git show HEAD@{1}
git branch rescue/lost-work HEAD@{1}
reflog 记录本地 HEAD 和分支引用移动轨迹。它能救很多 reset --hard、误删 branch、rebase 失误,但不是远端备份,也会过期。
9.3 暂存临时改动
git stash push -m "wip: debug login redirect"
git stash list
git stash pop
stash 适合临时切分支,不适合长期保存工作。超过一天的改动更应该建 WIP commit 或临时分支。
10. worktree 并行开发
git worktree 可以让同一个仓库有多个工作目录,共享同一份 Git 对象库,但每个目录检出不同分支。
git worktree add ../app-hotfix -b hotfix/login-loop origin/main
git worktree list
适合:
- 当前分支有大改,但线上 hotfix 要立刻处理。
- 同时跑两个 Agent / 两套实验,避免互相覆盖工作区。
- 一个目录跑长时间构建,另一个目录继续开发。
清理:
git worktree remove ../app-hotfix
git branch -d hotfix/login-loop
不要直接 rm -rf worktree 目录,否则 Git 还会保留已失效记录,需要额外 git worktree prune。
11. bisect 定位回归
当你只知道“上周是好的,今天坏了”,用 git bisect 二分定位第一个坏 commit。
git bisect start
git bisect bad HEAD
git bisect good v1.8.0
# Git 检出中间 commit,手动跑验证
npm test -- --run login
git bisect good # 或 git bisect bad
git bisect reset
可以自动化:
git bisect start HEAD v1.8.0
git bisect run npm test -- --run login
git bisect reset
前提是测试能稳定复现问题。flaky test 会让 bisect 给出错误结论。
12. 团队保护规则
12.1 branch protection
main 至少应配置:
- Require pull request before merging
- Require status checks to pass
- Require branches to be up to date before merging
- Require linear history 或统一 squash merge 策略
- Restrict force pushes
- Restrict deletions
12.2 commit message
使用 Conventional Commits:
feat(auth): add login rate limit
fix(payment): handle Alipay callback timeout
docs(devops): add Git workflow guide
scope 要表达业务或模块,不要写成 fix(common): update。好的 commit message 是 release note、回滚判断和 blame 排查的基础。
12.3 review 清单
PR 合并前至少确认:
- diff 范围和 PR 描述一致
- CI 全绿,失败项有明确解释
- 新增配置没有泄露密钥
- 需要回滚的地方有明确方案
- 数据结构、路由、缓存、环境变量变更已经同步部署文档
13. 前端项目高频场景
13.1 拆掉误加的大文件
文件还没提交:
git restore --staged dist/bundle.js
git restore dist/bundle.js
已经提交但没 push:
git reset --soft HEAD~1
git restore --staged dist/bundle.js
git commit -m "feat(build): add bundle analyzer config"
已经 push 到公共分支:不要改历史,走 revert 或新 PR 删除。
13.2 PR 落后主干
git fetch origin
git rebase origin/main
git push --force-with-lease
如果团队禁止 rebase PR 分支,用 merge:
git fetch origin
git merge origin/main
git push
关键不是选哪个,而是团队统一策略。
13.3 线上版本出问题
优先顺序:
- 用监控确认问题版本和影响范围。
- 找到发布 tag / sha。
- 如果部署系统支持,直接回滚到上一产物。
- 代码层面用
git revert生成回滚 PR。 - 事后用
bisect/ PR diff 找根因,补测试。
14. 常见反模式
- 公共分支
reset --hard后 force push:会破坏所有协作者历史。 - 大 PR 堆几千行:review 失效,冲突集中爆发。
- 格式化和业务改动混在一起:diff 噪音掩盖真实逻辑。
- 只打
latest镜像:线上无法对应源码。 - tag 发布后又移动 tag:发布锚点失真。
- 长期依赖 stash:本地状态不可审计,机器坏了就丢。
- 冲突时无脑选 ours/theirs:容易吞掉别人逻辑。
- commit message 写
update:以后排查和生成 changelog 都痛苦。
15. 延伸阅读
核对日期:2026-07-07