跳到正文

我所理解的 Git 最佳实践(3):利用 amend 和 rebase 保持提交简洁

我所理解的 Git 最佳实践

  1. 从工作区到一条可审阅的提交
  2. 什么时候用 rebase,什么时候用 merge
  3. 利用 amend 和 rebase 保持提交简洁(当前)
  4. 在 GitHub 和 GitLab 配置 PR 的合入方式
  5. 共享测试分支与按需求部署
  6. 弄清工作树、嵌套仓库和远端的归属
  7. 没有远端时怎样用 Git 维护项目
  8. 为什么切到 orphan 分支后源码不见了
  9. 先量清仓库体积,再决定怎么迁移
  10. 用 Node.js 检查 Git 路径,提前发现 Windows 检出问题

假设正在开发一个需求。第一条提交实现功能并触发 CI,第二条补上漏掉的测试用例,第三条根据测试环境的验收反馈修正实现。开发时,每条提交都对应一个可以验证的版本。三次改动一起交付时,主干可以只留一条说明功能和必要背景的提交。

把本轮修正补进最新一条提交

如果最新提交就是正在完善的修复,先检查索引,再把需要补进去的改动暂存。以下命令假定当前在允许整理的个人分支,HEAD 是要更新的提交,索引里没有另一项任务的改动。

git status --short --branch
git log -1 --oneline
git add -p
git diff --cached
git commit --amend --no-edit

--no-edit 沿用原提交说明。改动已经影响原来的解释时,去掉这个参数,一起更新说明。amend 会用暂存后的内容生成替换 HEAD 的提交,即使只改说明也会产生新的提交 ID。只修说明且暂存区另有工作时,可以使用不带路径的 git commit --amend --only,保留原提交的文件树。

把几次 CI 和提测修正折回原提交

如果三条提交都已经存在,amend 只能处理最后一条。假设它们连续排列,且功能分支没有合并节点,先确认 BASE_SHA 是第一条功能提交的父提交,再在工作树和索引都干净时运行 git rebase -i BASE_SHA。Git 打开的清单按先旧后新的顺序排列,可以把后两条改成 fixup。

pick 4c8f030 feat: 实现本次需求
fixup 7f24b91 test: 补充遗漏的用例
fixup 68f3de2 fix: 修正提测发现的问题

这里的提交 ID 是示例。fixup 把补丁并进前一组提交,丢弃自己的提交说明。修正提交如果记录了最终实现仍依赖的兼容条件,可以用 squash 把说明一并带进编辑器,整理成最终正文。开发时已经知道要补哪条,也可以把改动暂存后用 git commit --fixup IMPLEMENTATION_SHA 标记,再执行 git rebase -i --autosquash BASE_SHA 让 Git 排好清单。

开始前可以用 git branch backup/feature-before-rewrite 保留原分支位置。只整理提交、沿用同一个基线时,git diff --exit-code backup/feature-before-rewrite HEAD 可以确认最终文件树是否相同。基线也换过时,要对照新旧提交序列和最终的 PR 差异,尤其检查冲突解决过程中有没有改动行为。对要交付的版本重新跑 CI。若中间一条提交还有独立交付价值,例如可以先上线的兼容改造,就让它留在历史里。

改写已经推送的提交后,用显式 lease 更新远端

普通 push 更新分支时,远端 tip 必须是新 tip 的祖先。amend 或 rebase 替换了远端已经拥有的提交之后,这个条件往往不成立,普通 push 就会拒绝更新。强制推送允许把远端分支改指到另一段历史。裸 --force 会跳过非快进检查,可能把别人刚推上去的提交从这条分支上移走。个人分支确实需要更新时,我会选 Git 的 --force-with-lease。

整理前先 fetch,检查 origin/feature,记录自己审过并同意替换的远端 tip。上面的备份分支指向本地整理前的位置,远端 tip 有可能与它不同。只有团队允许重写这条分支,且没有其他人在这些提交上接着开发时,才考虑替换远端。

推送前先确认 HEAD 就是整理后的版本,目标是远端的 feature。把 REVIEWED_REMOTE_SHA 换成重写前核对过的完整 ID。

git push --force-with-lease=refs/heads/feature:REVIEWED_REMOTE_SHA origin HEAD:refs/heads/feature

显式 lease 要求远端当前值仍是指定提交,分支保护也照常生效。省掉预期 ID 的形式会使用本地远端跟踪引用,IDE 后台 fetch 有可能把它更新到自己还没审过的位置。lease 只检查远端是否还在预期位置,谁依赖这条分支、改写后哪些提交会离开它的历史,仍要自己确认。遇到 stale info,先取回并审查远端新增的改动。推送成功后,用 git ls-remote origin refs/heads/feature 对照本地 HEAD。

团队日常开发可以让 main 保持可部署,个人主题分支负责实现和提测,共享 test 负责集成,发布分支按项目需要保留。main、共享 test 和发布分支适合用保护规则限制强推。已泄露凭据必须从历史中清理等特殊情况,应按共享历史重写流程协调停写、保留恢复点并核对新旧引用。

共享分支可以在合入时再整理

如果别人已基于功能分支开发,或者这些提交已经通过普通 merge 进了共享 test,我会保留原提交,把后续修正继续追加在分支上。主干只需要一次完整交付时,用平台的 squash 合入生成一条新的交付记录。测试分支保留原来的提测过程;两边以后怎么同步,取决于共享测试分支的合入流程。

总结

amend 适合补最新提交,交互式 rebase 适合整理一段仍可改写的历史,平台 squash 则可以把整理推迟到合入时。提交要留几条,取决于最终有几个值得单独审阅、验证和追溯的变化。