分支与合并
本章要点
- 分支的本质:一个可移动的"指针",所以创建分支极其廉价
- 常用分支命令:list / create / switch / delete
- 合并的两种方式:fast-forward 与 三方合并(merge commit)
- 冲突的产生、查看与解决
- 合并策略与常见陷阱
分支的本质
分支(branch)本质上只是一个指向某次提交的可移动指针。
text
main
│
▼
●───●───● ← 每次提交,main 指针前移创建一个新分支:
bash
$ git branch feature/login # 新建分支(指针指向当前所在位置)text
main
│
▼
●───●───●
▲
│
feature/login创建分支 = 复制一个指针,不复制任何文件,所以极其廉价、瞬间完成。 "分支廉价"是 Git 鼓励"一功能一分支"的根本原因。
分支常用命令
bash
$ git branch # 列出本地分支(* 表示当前分支)
$ git branch -a # 列出所有分支(含远程 origin/xxx)
$ git branch -v # 显示每个分支的最新提交
$ git branch <名字> # 创建分支
$ git switch <名字> # 切换分支(推荐,语义清晰)
$ git checkout <名字> # 切换分支(老命令,等价)
$ git switch -c <名字> # 创建并切换(等价 git checkout -b)
$ git branch -d <名字> # 删除分支(-D 强制删除未合并分支)
$ git branch -m <新名字> # 重命名当前分支
$ git branch -vv # 查看分支与上游跟踪关系💡
git switch(Git 2.23+)是更明确的切换命令,避免checkout的多重含义(切换分支 / 恢复文件 / 检出提交)。
完整分支流程
bash
$ git switch main # 回到主分支
$ git switch -c feature/login # 从 main 创建并切到新分支
# ...开发、提交...
$ git switch main # 切回 main
$ git merge feature/login # 把 feature 合并进来
$ git branch -d feature/login # 合并完成后删除特性分支合并(Merge)
场景一:Fast-forward 合并(快进)
当被合并的分支没有分叉(main 自始至终没动过)时,Git 只需把 main 指针直接前移:
text
合并前:
●───●───● main
└───●───● feature/login
合并后:
●───●───●───●───● main(与 feature/login 重合)bash
$ git merge feature/login
# Updating 3b1e7d0..8f4a2c9
# Fast-forward- 不产生新的合并提交,历史保持线性,非常干净。
- 如果希望即使能快进也强制生成合并提交(保留"功能合并"痕迹),用
git merge --no-ff。
场景二:三方合并(产生 merge commit)
当两个分支各自都有新提交(分叉了)时:
text
●───●───● main
│
└───●───● feature/loginbash
$ git switch main
$ git merge feature/login
# Merge made by the 'ort' strategy.Git 会比较三个位置:两个分支的共同祖先(merge base)、main、feature,然后合并出结果,生成一个合并提交(merge commit):
text
●───●───●───● ← main(新的 merge commit,有两个父提交)
│ ╱
└───●───● feature/loginbash
$ git log --oneline --graph # 可以看到分叉合并的分支图
* 9c3a1f2 Merge branch 'feature/login'
|\
| * 8f4a2c9 feat(login): ...
* | 5d0e1a3 fix: ...
|/
* 3b1e7d0 ...💡
--no-ff在团队协作中常被强制使用(如 GitHub 的 "Merge pull request" 按钮),目的是在历史上保留"这是一个功能合入点"的节点。
合并冲突(Merge Conflict)
冲突是怎么产生的
当两个分支修改了同一个文件的同一处内容时,Git 无法自动判断该保留谁的,于是停下并报告冲突:
bash
$ git merge feature/login
# Auto-merging config.js
# CONFLICT (content): Merge conflict in config.js
# Automatic merge failed; fix conflicts and then commit the result.查看冲突
bash
$ git status
# both modified: config.js ← 冲突文件
$ git diff # 查看冲突详情冲突标记长什么样
text
<<<<<<< HEAD
const timeout = 3000; # ← 当前分支(HEAD)的内容
=======
const timeout = 5000; # ← 被合并分支(feature/login)的内容
>>>>>>> feature/login<<<<<<< HEAD:冲突块开始,以下是当前分支的版本=======:分界线>>>>>>> feature/login:以下是对方分支的版本
解决冲突的步骤
- 打开冲突文件,阅读两版内容。
- 决定保留哪边、或两边都要、或写全新的内容,删除所有冲突标记。
git add标记为已解决。- 提交(merge 场景下直接
git commit,Git 会带好默认的合并提交信息)。
bash
$ code config.js # 编辑,删除 <<<<<<< ======= >>>>>>> 标记
$ git add config.js # 标记为已解决
$ git commit # 完成合并⚠️ 冲突标记(
<<<<<<<等)是需要你手动删除的文本,不是自动消失的。 ⚠️ 解决冲突时不要执行git commit -a,确保所有冲突文件都git add过。 💡 如果合并到一半想放弃,git merge --abort可回到合并前的状态。
解决冲突的辅助工具
bash
$ git mergetool # 启动图形化合并工具(需先配置,如 Beyond Compare / meld)
$ git config --global merge.tool vscode # 示例配置合并策略一览
| 策略 | 触发条件 | 特点 |
|---|---|---|
| fast-forward | 无分叉 | 不产生 merge commit,历史线性 |
| recursive / ort | 有分叉 | 默认三方合并,产生 merge commit |
| ours / theirs | 特殊需求 | 直接采用某一方内容 |
| squash(不是策略而是选项) | git merge --squash | 把对方所有提交压成一个提交,不保留来源历史 |
bash
$ git merge --squash feature # 压缩合并(常用于整理 PR 历史)分支管理的最佳实践
- 一功能一分支:分支应该短命,功能完成即合并、即删除。
- 主分支保持可发布:
main/master上的代码任何时候都应该是能跑的。 - 频繁合并:分支存在越久,冲突概率越高、解决越痛。鼓励小步提交、小步合并。
- 合并前先更新:
git switch main && git pull再合并,减少冲突。 - 命名规范:
feature/xxx、fix/xxx、hotfix/xxx、release/xxx前缀让分支一目了然。
