我所理解的 Git 最佳实践(2):什么时候用 rebase,什么时候用 merge
主干更新以后,可以用 merge 或 rebase 让功能分支跟上。merge 把两边已有的历史接起来,rebase 把功能提交重新放到更新的主干之后。两种方式都能拿到主干的改动,留下的提交关系不同。
从同一个分叉看两种结果
提交记录一棵文件树和它的父提交。分支是指向某条提交的引用。从同一个提交切出 feature 后,main 和 feature 各自新增提交,就有了两条开发线。
两边已经分叉时,在 feature 上执行 git merge main,Git 会创建有两个父提交的合并节点。原提交 ID 保留下来,回头能看出哪两段历史在何处合并。功能分支每次同步都产生合并节点时,图上也会积累很多分叉。
在同一个分叉上执行 git rebase main,Git 会把功能分支的改动逐条重放到 main 之后,通常会生成新的提交 ID。这个例子里,重放后的功能提交连成一条线,旧分叉位置和同步时间不会作为合并节点保留。重放时可能逐条遇到冲突,也可能跳过主干已有的等价补丁。
两边尚未分叉时,还可以快进。假如 main 已经是 feature 的祖先,在 main 上运行 git merge --ff-only feature 只会把 main 的指针往前移,原提交的 ID 也保留下来。两边一旦分叉,--ff-only 会拒绝合并。团队如果希望快进时也保留一次合入记录,可以用 --no-ff 创建合并提交。需要先检查合并结果时,把 --no-commit 和 --no-ff 一起用,因为快进没有创建提交的步骤,单用 --no-commit 拦不住。检查前最好让工作树和索引保持干净。
个人分支同步主干时,我倾向用 rebase
我读个人功能分支时,更关心这项功能分几步完成,通常用不上每次同步主干的时间。只要这条分支仍由我自己维护,我会用 rebase 少留几次同步产生的合并节点。同仓协作时,我通常直接从刚取回的 origin/main 切主题分支,以免带上本地 main 的试验提交。下面假定 feat/user-profile 尚未创建,origin 是团队仓库。
git fetch origin &&
git switch --no-track -c feat/user-profile origin/main
--no-track 只借用 origin/main 的起点。第一次推送时,用 git push -u origin feat/user-profile 把上游跟踪分支设为远端的同名分支。后来 main 更新,如果这个功能分支仍由自己维护,可以这样同步。
git fetch origin &&
git switch feat/user-profile &&
git rebase origin/main
git rebase origin/main 重放的是当前检出分支的提交,origin/main 提供新的基线。遇到冲突,先用 git status 确认文件,解决并暂存后运行 git rebase --continue。发现基线选错,可以用 git rebase --abort 回到操作前。重放完成后,跑一遍受影响的测试,尤其要覆盖手工解决的冲突。若功能分支已经推送,更新远端前还得核对能不能替换远端已有提交。
公共分支保留其他人依赖的历史
我的黄金原则是不要在公共分支上 rebase 来改写已共享的历史。 同样的限制也适用于 amend 后强推。main、共享的 test、团队发布分支都在这个范围内。个人主题分支即使已经推送,如果团队明确约定可以整理,且没人基于它的旧提交继续开发,仍可按个人分支处理。一旦同事从这条线接着写,就应保留那些提交。否则,重放后内容看起来差不多,同事本地的分支却还接在原来的提交上,双方得重新协调基线。
多人共用的功能分支需要跟上主干时,可以在功能分支上合入 origin/main,再按团队流程推送。维护长期发布线时,merge 也能留下哪批变更在哪一次合入的信息。发生回归,可以用 git log --first-parent 看合入顺序,再用 git show -s --format='%P' MERGE_SHA 查看合并提交的父节点。只有第一父提交确实是合入前的目标分支,才用 git revert -m 1 MERGE_SHA 以它为基准撤销;后续改动依赖被撤销的代码时,还要一并检查依赖。
总结
给个人功能分支换基线,用 rebase 可以省掉反复同步主干产生的合并节点。给其他人正在使用的分支带进新变化,用 merge 保留已有的开发基线。等功能评审结束,目标分支最后要保留几条记录,则由项目的 PR 或 MR 合入方式决定。