
AI 编程工具让代码生成、重构和修复变得越来越快,但它也放大了另一个问题:改动来得越快,越需要清晰的版本历史来解释这些改动从哪里来、为什么发生、何时进入稳定版本。
作为版本管理工具,Git 在 AI 编程时代扮演着越来越重要的角色,它可以为工程协作提供的时间线、审计记录和回滚机制。越是在 AI 能快速生成大量代码的场景下,越需要理解标准工作流,避免把临时实验、半成品功能和稳定发布混在一起。
Git 命令本身往往不是难点,难点在于搞清何时用它们。
git merge [--no-ff|--squash]、git rebase 都能把改动带到另一个分支,但它们对提交历史的处理完全不同。什么时候应该用 merge,什么时候应该用 rebase?这就是本文要回答的问题。下面以一个虚构项目 world-cup-dashboard 为例,说明这两个命令的用法,最后推荐一种围绕 git 工具的项目开发流程。
1. 分支约定
通常,一个项目会有三类分支:
main 稳定发布分支
dev 日常集成分支
feature/* 单个功能分支
下面的命令,会初始化项目,并建立 dev 分支:
git init
git add .
git commit -m "Initial project"
git switch -c dev
项目开发流程:main 保持稳定,日常改动先进 dev,较大的任务从 dev 拉 feature/*。
2. 在 feature 分支开发功能
假设要做赛程页面,从 dev 分支上新建一个 feature/schedule-page 分支:
git switch dev
git switch -c feature/schedule-page
开发过程中可能产生几个有意义的提交:
git commit -m "Add schedule layout"
git commit -m "Load match schedule"
git commit -m "Add stage filters"
feature/schedule-page 分支是短生命周期分支,只服务单个功能。
3. feature 分支同步 dev 最新改动:优先 rebase
开发过程中,dev 可能已经被别人推进。提交功能前,要先在 feature 分支同步 dev 最新改动:
git switch feature/schedule-page
git fetch origin
git rebase dev
这里对 feature 分支使用 git rebase 命令,rebase 的作用是把当前 feature 分支上的提交接到 dev 最新提交的后面。
比如:
main/dev: A - B
feature: A - C - D
在 feature 上执行:git rebase dev
结果是:
main/dev: A - B
feature: C' - D'
rebase 会改写目标分支提交哈希,公共分支一旦被改写,其他人的本地历史就会和 origin 端不一致。
因此,适合使用 rebase 的场景有:
- 当前分支是个人短期 feature 分支。
- 分支尚未被别人基于它继续开发。
- 希望合并前先解决冲突,并保持功能提交线性。
不建议使用 rebase 的场景:
main、dev这类长期公共分支。- 已经被多人基于其继续开发的分支。
3. feature 改动合入 dev:merge –no-ff 或 –squash
feature 分支开发完成并同步 dev 最新改动后,可以合入 dev 分支。
如果 feature 分支的提交历史有保留价值,使用 git merge --no-ff:
git switch dev
git merge --no-ff feature/schedule-page
命令行选项 --no-ff 是 --no-fast-forward 的缩写,它的作用是:即使 Git 可以直接快进,也强制生成一个 merge commit。这样做的价值是保留功能边界,之后看提交历史时能知道哪些提交属于同一个功能。
如果 feature 分支里大多是临时提交,提交历史没有保留的必要,例如:
try layout
fix
debug
fix again
final
那么可以使用 git merge --squash:
git switch dev
git merge --squash feature/schedule-page
git commit -m "Add schedule page"
--squash 把来源分支上的所有提交内容作为一次变动,不会保留来源分支的提交历史。它适合清理短期分支的混乱、不规范的提交历史。
推荐 git merge 原则:
- feature 分支提交清晰:
git merge --no-ff feature/xxx - feature 分支提交混乱:
git merge --squash feature/xxx
4. dev 分支发布到 main:使用 merge –no-ff
当 dev 准备发布时,合入到 main 分支,合并后可以根据需要给 main 分支打 Tag:
git switch main
git pull --ff-only
git merge --no-ff dev
git tag v0.1.0
git push origin main
git push origin v0.1.0
向 main 分支合并时,推荐 merge --no-ff,不推荐 --squash。原因是 dev 是长期分支,它的提交历史有价值,通过 merge --no-ff 将 dev 发布到 main 时可以保留提交历史,明确合并关系,让 Git 知道 dev 已经合并过。
如果把 dev squash 到 main,Git 只知道 main 上多了一个“内容相同”的提交,却不知道 dev 已经被合并过。后续继续从同一个 dev 合并时,容易出现重复改动、冲突或历史混乱的情况。
5. 普通 merge 与 –no-ff 的区别
假设历史是:
main: A
dev: A - B - C
普通合并:
git merge dev
此时可以快进,结果是:
main/dev: A - B - C
使用 --no-ff:
git merge --no-ff dev
结果是:
* M
|\
| * C
| * B
|/
* A
M 是 merge commit,有两个父节点:一个是 main 原来的位置 A,一个是 dev 的最新提交 C。
在 git log --oneline 中,历史可能看起来像线性的 A - B - C - M,但实际提交图不是普通直线。M 记录的是一次合并事件。
6. 什么时候用什么
简化成一张表:
| 场景 | 推荐命令 | 原因 |
|---|---|---|
| feature 同步最新 dev | git rebase dev | 让个人短期分支接到最新主线后面 |
| feature 提交清晰,合入 dev | git merge --no-ff feature/x | 保留功能边界和提交历史 |
| feature 提交混乱,合入 dev | git merge --squash feature/x | 只保留最终改动 |
| dev 发布到 main | git merge --no-ff dev | 保留长期分支的真实合并关系 |
| main/dev 公共分支 | 避免 rebase | 不改写公共历史 |
| 长期 dev 合到 main | 避免 squash | 避免丢失合并关系和开发历史 |
7. 常见反模式
7.1 在公共分支上 rebase
git switch dev
git rebase main
如果 dev 已经被推送并被其他人使用,这会改写公共历史。除非团队明确约定,否则不要这样做。
7.2 将 dev 分支 squash 到 main
git switch main
git merge --squash dev
git commit -m "Release v0.1.0"
这可以让 main 历史很干净,但不会建立 dev 和 main 的真实合并关系。对长期分支来说,这通常不是好选择。
7.3 squash 后再 reset dev
git switch dev
git reset --hard main
这样会让 dev 上的细粒度开发历史从分支线上消失。内容还在 main 的 squash 提交里,但 dev 分支的提交历史没有了。
7.4 所有改动直接提交到 main
这会让稳定分支混入半成品,也让发布节点变得模糊。小项目也应尽量保留 dev 或 feature 分支。
8. 推荐工作流
推荐一种适用于个人项目的开发共奏流程:main 保持稳定,日常改动先进 dev,较大的任务从 dev 创建 feature/* 或者 fix/*:
feature/* --rebase dev
feature/* --merge --no-ff 或 squash --> dev
dev --merge --no-ff --------------> main
main --tag ------------------------> vX.Y.Z
一句话总结:
rebase 用于让个人短期开发分支与公共分支保持同步,merge 用于长期分支之间的合并集成,merge --squash 用于清理来源分支的杂乱提交,merge --no-ff 用于保留来源分支的提交历史。