Git 真正难的地方并不是记住几个命令,而是理解「分支为什么存在」「Merge 和 Rebase 到底有什么区别」以及「出现冲突以后应该怎么处理」。
日常开发中,一个项目通常同时存在多个功能开发、Bug 修复和版本发布任务。如果所有人都直接修改同一个分支,很快就会出现代码互相覆盖、提交历史混乱以及难以追踪的问题。
Git 的分支机制解决了这个问题。开发者可以从主分支创建独立分支,在自己的分支上完成开发,然后通过 Merge 或 Rebase 将代码重新整合到目标分支。
本文不从 Git 的基础命令开始,而是直接从实际开发流程理解 Git 分支、Merge、Rebase 和冲突处理。
Git 分支到底是什么
Git 中的分支本质上是指向某个 Commit 的引用。
例如项目最开始只有一条 main:
A --- B --- C
↑
main
开发新功能时创建 feature/login:
A --- B --- C
↑
main
↑
feature/login
继续开发后:
A --- B --- C --- D --- E
↑ ↑
main feature/login
此时 main 并没有发生变化,feature/login 在自己的提交历史上继续向前发展。
Git 官方文档也强调,Git 分支非常轻量,创建分支本质上只是创建一个指向 Commit 的引用,因此 Git 鼓励开发者频繁使用分支。
这也是 Git 分支和一些传统版本控制系统最大的区别之一。
创建和切换分支
现代 Git 推荐使用 git switch:
git switch -c feature/login
它相当于创建并切换到:
feature/login
查看当前分支:
git branch
切换到已有分支:
git switch main
查看所有分支:
git branch -a
查看分支对应的最新 Commit:
git branch -v
如果项目使用较新的 Git 版本,建议优先使用 switch 表达分支切换,而不是继续把 checkout 用作所有场景的通用命令。
一个真实的多人开发场景
假设项目目前只有:
main
现在同时出现两个需求:
feature/login
feature/payment
于是:
┌── feature/login
│
A --- B --- C ---┤
│
└── feature/payment
↑
main
两个开发者分别在自己的分支工作。
登录功能产生:
A --- B --- C --- D --- E
↑ ↑
main feature/login
支付功能产生:
A --- B --- C --- F --- G
↑ ↑
main feature/payment
此时 main 并不知道 D、E、F、G。
当功能完成后,就需要把这些变化重新整合到 main。
这就是 Merge 和 Rebase 要解决的问题。
Merge:把两个开发历史合并起来
Merge 的目标非常直接:
把一个分支的开发历史合并到当前分支。
例如:
A --- B --- C --- D --- E
\ ↑
F --- G --┘
现在位于 main:
git switch main
执行:
git merge feature/login
如果两个分支已经产生了不同的提交,Git 可能创建一个新的 Merge Commit:
A --- B --- C --- D --- E
\ \
F --- G --- M
其中:
M
就是 Merge Commit。
它有两个父 Commit:
E
G
Git 官方将 merge 定义为「Join two or more development histories」,也就是将多个开发历史连接起来。
Fast-forward Merge
并不是每一次 Merge 都会产生 Merge Commit。
例如:
A --- B --- C --- D
↑
main
↑
feature
如果 main 没有产生新的提交,那么:
git switch main
git merge feature
Git 可以直接把 main 指向 D:
A --- B --- C --- D
↑
main/feature
这就是 Fast-forward Merge。
因为两个分支之间没有真正产生分叉,所以不需要创建额外的 Merge Commit。Git 官方文档也明确说明了 Fast-forward 的这种行为。
如果希望无论如何都创建 Merge Commit,可以使用:
git merge --no-ff feature/login
形成:
A --- B --- C -------- M
\ /
D --- E
在团队开发中,--no-ff 有一个实际价值:可以让 Git 历史明确保留「这个功能分支曾经被合并进来」这一信息。
Rebase:重新整理提交历史
Merge 是「把两个历史合并」。
Rebase 的思路则不同:
把当前分支的提交重新应用到另一个基础之上。
假设现在:
A --- B --- C --- D --- E
\
F --- G
其中:
main: A-B-C-D-E
feature: A-B-C-F-G
在 feature 上执行:
git rebase main
Git 会把 F、G 的修改重新应用到 E 后面。
结果类似:
A --- B --- C --- D --- E --- F' --- G'
↑
feature
注意:
F != F'
G != G'
虽然代码修改可能相同,但它们是新的 Commit。
Git 官方文档对 Rebase 的描述也是「重新应用提交」:Git 会找出当前分支相对于上游分支的提交,然后依次重新应用这些提交。
为什么需要 Rebase
最大的价值是让提交历史更加线性。
使用 Merge:
A --- B --- C --- D --- M
\ /
E --- F
使用 Rebase:
A --- B --- C --- D --- E' --- F'
对于需要频繁同步主分支的功能开发来说,Rebase 可以减少大量没有实际业务意义的 Merge Commit。
例如:
git switch feature/login
git fetch origin
git rebase origin/main
然后继续开发。
Merge 和 Rebase 到底怎么选
这是 Git 开发中最容易产生争议的问题。
可以简单理解为:
| 场景 | 更适合 |
|---|---|
| 合并已经完成的功能分支 | Merge |
| 保留真实分支拓扑 | Merge |
| 希望历史保持线性 | Rebase |
本地开发分支同步最新 main | Rebase |
| 已经共享给其他人的公共分支 | 谨慎 Rebase |
| 希望避免修改已有公共历史 | Merge |
最重要的一条原则是:
不要随意 Rebase 已经被其他人使用的公共分支。
因为 Rebase 会创建新的 Commit。
例如:
A --- B --- C --- D
\
E --- F
别人已经基于 F 开发:
F --- G --- H
如果你对公共分支进行 Rebase,F 可能被重新写成 F':
A --- B --- C --- D --- E' --- F'
此时:
G
H
依赖的历史已经发生变化。
这也是 Git 官方文档提醒 Rebase 需要谨慎使用的原因。
实际开发中推荐的分支流程
对于普通 Web 项目,可以采用比较简单的模型:
main
│
├── feature/login
│
├── feature/payment
│
└── fix/user-login
开发功能时:
git switch main
git pull --ff-only
git switch -c feature/login
开发完成后:
git add .
git commit -m "feat: add user login"
在提交 Pull Request 之前,先同步主分支:
git fetch origin
git rebase origin/main
如果没有冲突:
main
\
A --- B --- C
最终通过 Pull Request 合并。
如果团队希望保留功能分支的完整边界,也可以采用:
git merge --no-ff
关键不是强制所有项目使用一种方式,而是团队必须统一规则。
Git 冲突是怎么产生的
冲突通常发生在两个分支修改了 Git 无法自动判断如何合并的相同内容。
例如:
main:
$name = 'Leanku';
feature/user:
$name = 'Tom';
两个分支分别修改了同一行。
此时:
git merge feature/user
Git 可能无法判断应该保留哪个版本:
<<<<<<< HEAD
$name = 'Leanku';
=======
$name = 'Tom';
>>>>>>> feature/user
这就是 Merge Conflict。
Git 的三方合并机制会尝试根据共同祖先、当前分支和被合并分支进行合并;无法自动判断的部分才需要开发者处理。
Merge 冲突的正确处理流程
发生冲突后,首先查看状态:
git status
可能看到:
both modified: src/User.php
打开文件:
<<<<<<< HEAD
当前分支内容
=======
其他分支内容
>>>>>>> feature/user
根据业务需求修改成最终版本:
$name = 'Leanku';
然后告诉 Git:
git add src/User.php
检查:
git status
最后完成 Merge:
git commit
或者:
git merge --continue
如果发现这次 Merge 不应该继续:
git merge --abort
Git 官方文档明确提供了 merge --continue 和 merge --abort 用于处理暂停中的 Merge。
Rebase 冲突怎么处理
Rebase 的处理方式和 Merge 类似,但过程更容易让初学者迷惑。
执行:
git rebase origin/main
如果出现冲突:
CONFLICT (content): Merge conflict in ...
首先:
git status
解决冲突文件。
然后:
git add .
继续:
git rebase --continue
如果后面还有冲突,就重复:
解决冲突
↓
git add
↓
git rebase --continue
↓
继续处理下一个冲突
如果发现整个 Rebase 不应该继续:
git rebase --abort
Git 官方文档同样提供了 rebase --continue、rebase --abort 等操作。
如何减少 Git 冲突
真正优秀的 Git 使用方式不是「冲突以后解决得很快」,而是尽量减少不必要的冲突。
第一,功能分支不要长期存在
不推荐:
feature/login
↓
开发 2 个月
↓
最后一次性合并
更推荐:
创建分支
↓
完成一个小功能
↓
提交
↓
Pull Request
↓
Merge
分支生命周期越长,与 main 产生差异的可能性越大。
第二,及时同步主分支
开发期间可以定期:
git fetch origin
git rebase origin/main
这样可以尽早发现冲突,而不是等到功能完成之后才一次性解决。
第三,Commit 不要过于庞大
不推荐:
feat: 完成整个用户系统
一个 Commit 修改几百个文件,最终很难 Review,也容易产生冲突。
更推荐:
feat: add user repository
feat: add user service
feat: add user controller
test: add user service tests
每个 Commit 尽量表达一个相对独立的变化。
git pull 到底使用 Merge 还是 Rebase
很多 Git 冲突实际上不是来自手动执行 merge,而是来自:
git pull
git pull 本质上先执行 Fetch,再将远程变化整合到当前分支。
根据配置,可以采用不同方式:
git pull --rebase
或者:
git pull --no-rebase
也可以强制只接受 Fast-forward:
git pull --ff-only
Git 官方文档明确支持这几种整合方式。
个人开发环境中,如果希望提交历史保持干净,可以考虑:
git config --global pull.rebase true
如果团队希望明确禁止 Pull 自动创建 Merge Commit,也可以使用:
git config --global pull.ff only
但具体选择应该以团队协作规范为准,而不是每个人自行配置一套规则。
一套推荐的实际工作流
对于普通 PHP、Java、Go、Node.js 等 Web 项目,可以采用下面的流程:
main
│
│
创建 feature
│
▼
feature/login
│
开发 + Commit
│
▼
同步 origin/main
│
Rebase
│
▼
Pull Request
│
Code Review
│
▼
Merge
│
▼
main
对应操作:
# 1. 更新主分支
git switch main
git pull --ff-only
# 2. 创建功能分支
git switch -c feature/login
# 3. 开发并提交
git add .
git commit -m "feat: add user login"
# 4. 同步主分支
git fetch origin
git rebase origin/main
# 5. 推送功能分支
git push -u origin feature/login
# 6. 创建 Pull Request
如果 Rebase 修改了已经推送过的个人功能分支,通常需要:
git push --force-with-lease
而不是:
git push --force
--force-with-lease 能够在一定程度上避免覆盖远程分支上其他人的新提交。
最容易犯的几个错误
直接在 main 上开发
git switch main
# 直接开始写代码
个人实验项目问题不大,但团队项目应该避免。
把 Merge 和 Rebase 当成完全相同的操作
两者都可以整合代码,但对 Commit 历史的影响完全不同。
Merge
A---B---C-------M
\ /
D---E---/
Rebase
A---B---C---D'---E'
Merge 保留分叉历史,Rebase 会重新构造提交历史。
Rebase 公共分支
这是最危险的习惯之一。
尤其不要随意对:
main
develop
release/*
等已经被团队共享的分支执行 Rebase。
使用 --force 覆盖远程分支
如果确实需要强制更新已经 Rebase 的个人分支:
git push --force-with-lease
通常比:
git push --force
更加安全。
Git 分支管理的核心原则
真正需要记住的不是几十个命令,而是下面几条原则:
第一,功能开发使用独立分支。
main
├── feature/A
├── feature/B
└── fix/C
第二,Merge 用于整合开发历史。
git merge feature/login
第三,Rebase 主要用于整理自己的开发历史和同步上游分支。
git rebase origin/main
第四,不要随意修改公共历史。
尤其是已经被其他开发者使用的分支。
第五,冲突不可怕,真正危险的是不了解当前 Git 操作处于什么状态。
遇到问题首先:
git status
然后根据当前状态决定:
git merge --abort
git merge --continue
git rebase --abort
git rebase --continue
而不是盲目执行 reset --hard。
总结
Git 分支的核心价值不是「多几个分支」,而是让不同开发任务能够相互隔离。
Merge 和 Rebase 则代表两种不同的历史整合方式:
Merge
保留真实分支历史
↓
适合整合团队开发结果
Rebase
重新整理提交历史
↓
适合个人分支同步和历史整理
在实际团队开发中,可以采用一个简单原则:
公共历史优先保持稳定,个人历史可以整理;功能分支及时同步,完成后尽快合并。
掌握 Branch、Merge、Rebase 之后,Git 真正值得学习的下一部分就是「出了问题怎么恢复」。下一篇将集中解决 reset、restore、revert、stash、reflog 这些最容易混淆、但在实际开发中非常重要的操作。