模块四 · 协作与表达
Git 与协作:分支模型、撤销与代码评审
Git 的难点不是命令,而是"每个操作影响哪一层"。这一节讲清四个区、四种撤销、两种分支模型,以及一次合格的 PR 应该包含什么。
读完这一节,你能做到
- 用"四个区"解释任何一条 git 命令在改什么。
- 在 merge、rebase、reset、revert 之间做出安全选择。
- 独立解决一次冲突,并判断该保留谁的改动。
- 写出可评审的提交与 PR 描述。
一、四个区:所有命令都在这张图上
工作区(你编辑的文件)
↓ git add
暂存区(index,下一次提交的内容)
↓ git commit
本地仓库(.git 里的提交历史)
↓ git push
远程仓库(origin)
反方向的三个命令:
| 命令 | 影响 | 安全性 |
|---|---|---|
git restore <file> | 用暂存区/HEAD 覆盖工作区 | 会丢失未提交修改 |
git restore --staged <file> | 把文件移出暂存区 | 安全 |
git reset --soft HEAD~1 | 撤销提交,改动留在暂存区 | 安全 |
git reset --hard HEAD~1 | 撤销提交并丢弃改动 | 危险,不可恢复 |
记住一句话:reset 改的是历史,revert 改的是内容。已推送到共享分支的提交,只能用 revert,不能用 reset。
二、merge、rebase、reset、revert
| 命令 | 做什么 | 历史形态 | 适用 |
|---|---|---|---|
merge | 新建合并提交,保留分支拓扑 | 有分叉 | 合入主分支、保留真实历史 |
rebase | 把当前分支的提交"重放"到目标分支之后 | 线性 | 个人分支同步主干、提交前整理 |
reset | 移动当前分支指针 | 重写 | 本地撤销,未推送 |
revert | 新建一个"反向提交" | 追加 | 已推送的提交回滚 |
rebase 的代价是改写提交哈希。如果别人已经基于你的分支工作,rebase 后他们必须重新拉取,否则会产生重复提交。所以:
- 个人分支可以 rebase。
- 共享分支不要 rebase。
- 已经 push 的分支如果必须 rebase,要用
--force-with-lease而不是--force,前者会在远端有他人提交时拒绝覆盖。
三、冲突:不是"选一边"那么简单
冲突的本质是同一文件的同一区域被两边修改。解决步骤:
git status看清哪些文件冲突。- 打开文件,找到
<<<<<<<、=======、>>>>>>>三段标记。 - 理解两边的意图,决定保留、合并还是重写。
- 删除标记,运行测试。
git add标记已解决,继续merge --continue或rebase --continue。
三条经验:
- 不要直接选 "ours" 或 "theirs",先看两边为什么改。选错边通常能在测试阶段发现,但语义冲突测不出来。
- rebase 冲突会逐个提交解决,merge 冲突只解决一次。提交多时,rebase 更累。
- 冲突解决后一定要跑测试。Git 只保证文本合并没有标记,不保证逻辑正确。
预防冲突的办法比解决更重要:小步提交、频繁同步主干、避免多人同时改同一个大文件。
四、两种分支模型
| 维度 | Git Flow | Trunk-Based |
|---|---|---|
| 长期分支 | main + develop + release + hotfix | 只有 main |
| 分支寿命 | 长 | 短(1-2 天) |
| 发布节奏 | 定期发布 | 持续部署 |
| 适合 | 有明确版本周期的产品 | SaaS、持续交付团队 |
| 代价 | 分支多、合并痛 | 依赖自动化测试与特性开关 |
判断标准:发布节奏与自动化程度。没有可靠的 CI 与测试,Trunk-Based 会把主干变成事故现场;有完善流水线还坚持 Git Flow,则会被合并成本拖慢。
中间路线很常见:主干 + 短生命周期特性分支 + 发布标签。名字不重要,关键是分支寿命短、合并频繁。
五、提交与 PR 的规范
Conventional Commits:
feat(order): 支持按状态筛选
fix(auth): 修复 token 过期后未跳转登录
refactor(api): 抽出统一请求层
docs(readme): 补充本地启动说明
格式是 类型(范围): 描述。收益不是"好看",而是:
- 能自动生成 changelog。
- 能按类型过滤 review 重点。
- 回滚时能快速定位相关提交。
一次合格的 PR 应包含:
| 部分 | 内容 |
|---|---|
| 标题 | 一句话说明改动,与 commit 规范一致 |
| 背景 | 为什么要改,关联的 issue |
| 改动 | 改了哪几块,关键决策与取舍 |
| 验证 | 怎么测的,截图/录屏/指标 |
| 风险 | 影响范围、回滚方式 |
Review 时优先看:边界条件、错误处理、并发与竞态、权限校验、测试覆盖。不要在第一轮纠结命名与格式,那些交给 lint。
六、三个真实场景
场景一:提交信息写错(未推送)
git commit --amend -m "feat(order): 支持按状态筛选"
只改最近一次,且未推送时安全。
场景二:误把密钥提交了
git rm --cached .env
echo ".env" >> .gitignore
git commit -m "chore: 移除误提交的环境变量"
注意:这只能让后续提交不含该文件,历史里仍然有。真正的补救是立即轮换密钥,必要时用 git filter-repo 重写历史并通知所有协作者。
场景三:线上出问题,要回滚一个提交
git revert <commit>
生成一个反向提交,历史保留,适合共享分支。回滚后要重新验证,因为后续提交可能依赖被回滚的代码。
七、常见坑
- 在共享分支上用
reset --hard强推,覆盖别人的提交。 - 用
--force而不是--force-with-lease,把远端新提交冲掉。 - 冲突时无脑选一边,测试没覆盖语义问题。
- 一个 PR 改 30 个文件,评审只能看个大概。
- 提交信息写"fix bug",半年后没人知道修了什么。
.gitignore写得晚,node_modules与构建产物进了历史。- 把密钥提交后又只删文件,没有轮换密钥。
八、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 在一个练习仓库里,分别用
merge与rebase同步主干,对比git log --graph。 - 故意制造一次冲突,用三种方式解决(保留本地、保留远端、手工合并),并记录差异。
- 把最近三个提交用
rebase -i整理成一个,写清提交信息。 - 给一个 PR 写完整的描述,包含背景、改动、验证与回滚方案。
② 让 AI 写同一题
把第 2 题(撤销与整理历史)交给 AI,补一句 说清每一步能不能撤回,已推送的分支怎么办。
③ 对比要盯的点
- 它给了
reset --hard却没提示风险吗?那是不可恢复的本地改动。 rebase已推送分支它警告了吗?改写公共历史会害到所有人。reflog它提了吗?这是最后的救命索。
④ 一句话取舍
照它给的命令执行,哪一步之后你就找不回原来的提交了?
九、自测
- 四个区分别是什么,
add与commit各改变哪一层? merge与rebase的历史差异是什么?- 已推送的提交为什么不能用
reset回滚? - 冲突解决后为什么必须跑测试?
- Git Flow 与 Trunk-Based 的判断标准是什么?
参考答案要点:工作区/暂存区/本地/远程;分叉 vs 线性;会改写历史影响他人;Git 只保证文本合并;发布节奏与自动化程度。
附:命令速查表
日常真正用到的命令并不多,按场景记比按字母记有用:
| 场景 | 命令 | 说明 |
|---|---|---|
| 看当前状态 | git status -sb | 精简模式,带分支信息 |
| 看改了什么 | git diff / git diff --staged | 工作区 / 暂存区 |
| 看提交历史 | git log --oneline --graph -20 | 图形化最近 20 条 |
| 看某行是谁改的 | git blame -L 10,20 file | 排查“为什么这么写” |
| 找引入 bug 的提交 | git bisect start/good/bad | 二分法,回归神器 |
| 临时保存改动 | git stash push -m "wip" | 切分支前用 |
| 只挑一个提交 | git cherry-pick <hash> | 热修复常用 |
| 撤销工作区修改 | git restore <file> | 不可恢复 |
| 撤回已提交未推送 | git reset --soft HEAD~1 | 改动留在暂存区 |
| 回滚已推送 | git revert <hash> | 安全 |
| 看远端分支 | git branch -r / git remote -v | 确认推送目标 |
| 清理已删分支 | git fetch -p | 删除远端已不存在的追踪分支 |
一个容易被低估的命令是 git bisect。当不知道哪个提交引入的 bug 时,手动翻历史可能要看几十条;二分法在 100 个提交里只需 7 次验证就能定位。配合 git bisect run npm test 还能自动跑。
最后提醒:git push --force 不要出现在团队命令历史里。确需改写已推送的个人分支时,用 --force-with-lease,它会先检查远端是否有人推了新提交,减少覆盖别人的风险。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
提示:读完可以顺手做几道对应题目,检验是否真的理解