📦 第02章 · 分支/合并/变基

branch checkout merge rebase

分支、合并与变基

上一章我们学会了把改动一次次提交。但所有提交都堆在 main 上,出了问题很难收拾。分支 (branch) 就是 Git 给你的一条"平行轨道":可以大胆地另起一条线开发新功能,互不干扰,最后再把成果并回来。这一章讲清 branch、merge、rebase 三件事。

分支的本质:一个会移动的指针

Git 的提交对象(chapter 1 的 git log 里那串短哈希)像单向链表,每个提交记录它的父提交。分支只是一根指向某个提交的"可移动指针"(label)。当你 commit,当前分支的指针就自动向前滑一格,指向新提交。

        main
          │
  A ← B ← C ← D    (main 指向 D,HEAD 也指向 main)

        main           feature
          │              │
  A ← B ← C ← D ← E ← F   (在 feature 上又提交了 E、F)

因为创建分支只是加一个指针,而不是复制文件,所以 Git 建分支极快、极省——这也是它广受欢迎的原因。

术语 含义
HEAD 指向"你当前所在分支"的指针,Git 用它在哪根轨道上移动
main 默认主干分支(传统叫法 master,第 1 章已用 init.defaultBranch 设为 main)
检出 (checkout/switch) 把工作区切换成某分支指向的版本,同时移动 HEAD

创建与切换分支

现代 Git 推荐用 switch 替代老式的 checkout,语义更清晰:

git branch                 # 列出所有本地分支,★ 标记当前分支
git branch feature/login   # 从当前分支创建新分支 feature/login(不切换)
git switch feature/login   # 切换过去
git switch -c feature/login # 创建并立刻切换(一步完成)
git switch -               # 切回上一个分支(类似 cd -)
git switch main && git branch -d feature/login   # 删已合并分支
git branch -D feature/login                      # 强制删除未合并分支(危险)

安全 git branch -D(大写)会直接丢弃分支上尚未合并的提交。只有当确定这些提交不要了,才用大写 -D;默认请先用小写 -d,Git 会拒绝删除未合并的分支作为保护。

实战习惯:开发新功能前,先从最新的 main 切一条分支,把它完全隔离。例如给登录页做改造:

git switch main          # 确保基于最新 main
git switch -c feature/login
# ...写代码、git add、git commit(可以在 feature/login 上提交多次)...
git switch main          # 回到主干
git log --oneline        # 主干仍是干净的历史,没有功能分支的提交

git merge:把分支合回来

做完功能,要把 feature/login 并入 main,用 merge。合并分两种:

类型 触发条件 结果 何时偏好
Fast-forward (快进) main 在分支分叉后没有任何新提交 main 指针直接滑到 feature/login 的尖端,线性,无额外提交 简单、想保持线性时
三方合并 (merge commit) main 在分叉后也有新提交 生成一个新的合并提交(merge commit),有两个父提交 想保留"在哪合过"的痕迹时
git switch main
git merge feature/login          # 自动尝试合并,若无冲突会产出新提交
git merge --no-ff feature/login  # 强制生成合并提交,即使可以快进
git merge --squash feature/login # 把整条分支压缩成一次提交(只合并改动,不留分支痕迹)

看 git log --oneline --graph 就能辨认:合并会产生分叉又汇合的形状(像个"门字形")。

合并冲突

当两个分支改了同一个文件的同一行,Git 无法自动判断谁对,就停下来让你裁决,这就是冲突 (conflict)。合并后 Git 会在文件里插入冲突标记:

<<<<<<< HEAD
console.log("来自 main 的版本");
=======
console.log("来自 feature/login 的版本");
>>>>>>> feature/login

处理步骤:

  1. 打开冲突文件,在 <<<<<<< 与 >>>>>>> 之间手工保留想要的那一行,删掉标记行;
  2. 保存文件并 git add 该文件;
  3. 再次 git commit(不带 -m,Git 会自动填充"合并"信息,建议核对)。
git merge feature/login   # 报冲突:CONFLICT (content)
git status                # 显示 both modified,待解决
# 编辑文件合并标记...之后
git add app.js
git commit                # 完成合并

易错 合并冲突不是错误,不要慌着执行 git merge --abort 逃课。逐文件解决才是正途;如果确实想完全放弃这次合并、回到合并前状态,再用 git merge --abort。

git rebase:把分支"挪"到最新主干上

rebase(变基)是另一种整合思路:它把你 feature 分支上的提交,一条条"重放"到目标分支的最新提交之后,让历史保持一条直线,没有合并提交。

合并前:
        main
          │
  A ← B ← C ← E      (feature: C ← D 分叉自 B)
            │
        D   (feature)

rebase main 后:
  A ← B ← C ← D ← E      单条直线,D、E 都在 main 上
git switch feature/login
git rebase main     # 把当前分支的提交重放到 main 之后
git rebase --continue   # 解决冲突后继续
git rebase --abort      # 放弃本次 rebase,回到之前状态

merge 与 rebase 怎么选

维度 merge rebase
历史形状 有分叉与合并提交,保留"在何处合并" 线性直线,更干净易读
是否改写提交 不改写已有提交 改写了被重放提交的哈希
对已推送分支的影响 安全 会改动历史,已共享的分支别人拉取会冲突
适用场景 把功能分支合进主干、长期分支 拉取到新主代码后,把个人分支更新到最新,再看 diff

一条通用经验: - 本地的、尚未推送的个人分支 → 用 rebase 保持线性、好读; - 已经推送到共享远程、或多人协作的分支 → 用 merge,不要 rebase。

安全 对已经推送到共享仓库的分支执行 rebase 并 force push,会改写别人的历史,是很危险的操作。除非你是这条分支的唯一作者且确认无人依赖,否则一律 merge,并绝不 git push --force。这可能是 Git 团队协作里最容易出事故的动作。

案例场景:一次完整的功能分支开发

用本服务器上一个前端项目举例,从 main 拉分支、开发、重基、合并:

git switch main
git switch -c feature/dashboard

# 开发并提交若干次
echo "export default (/* */) => 1;" >> dashboard.js
git add dashboard.js
git commit -m "feat: 仪表盘骨架"

# 期间主线上也有别人提交,先拉最新再变基保持线性
git switch main
git pull --rebase        # 见第 3 章:拉取最新并变基,而不是合并
git switch feature/dashboard
git rebase main

# 若无冲突,回到主干合并
git switch main
git merge feature/dashboard   # 此时一般是快进,历史为一条直线
git log --oneline --graph

若合并产生冲突,按上面冲突流程手动解决;git rebase --continue 后历史依旧呈单线。

延伸:更常用的分支工具

  • git stash:临时把工作区未提交的改动"存起来",切去别的分支不带着它,git stash pop 随时找回。适合改了一半想临时切走。
  • git cherry-pick <哈希>:把别处某一次提交的改动复制到当前分支,不合并整条分支。
  • git switch --detach / 直接切哈希:让 HEAD 指向某个不在分支上的提交,即"游离 HEAD"。可以查看旧版本,但这种状态下 commit 会留在游历处,很容易弄丢,看完记得切回分支。
  • git worktree:同一仓库同时检出多个分支到不同目录,适合一台服务器上并行改多个分支(第 3 章部署时很实用)。

易错点

  • 在错误分支上提交:忘了 switch,把改动提交到了 main 或别人分支。养成开工前 git status 看当前分支、git switch -c 一步到位。
  • --no-ff vs --squash 混淆:--no-ff 保留合并痕迹与所有提交;--squash 只把改动压成一次提交、不留分支合并历史,二者方向相反。
  • 解决冲突时没删干净标记:忘了删除 <<<<<<<、=======、>>>>>>> 三行,提交进去会在源码里留着怪异文本。解决后用 grep "<<<<<<<" 文件 复查。
  • 合并与变基混用当万能药:变基改写提交,不适合共享分支;合并产生额外提交,不适合想保持干净主线。按"是否已推送"来选。

相关命令

git branch、git switch、git checkout、git merge、git rebase、git stash、git cherry-pick、git reset、git show、git log --graph。