正在加载,请稍候…

Git Rebase vs Merge:何时使用以及它们如何工作

理解 git rebase 和 git merge 的区别,它们如何改变历史,以及为功能分支、热修复和团队项目选择哪种工作流

Git Rebase vs Merge:何时使用以及它们如何工作

相同目标,不同历史

git mergegit rebase 都将一个分支的更改集成到另一个分支。区别在于它们如何处理提交历史——这种区别对可读性、调试和协作有实际影响。

Git Rebase vs Merge:何时使用以及它们如何工作 插图

Git Merge:保留历史

git merge 创建一个新的提交来连接两个分支的历史。它是非破坏性的:没有现有提交被更改。

合并前:
main:    A --- B --- C
feature:          \ D --- E

将 main 合并到 feature 后:
main:    A --- B --- C
feature:          \ D --- E --- M (合并提交)
                   \___________/

M 是合并提交,有两个父提交:E(feature 的顶端)和 C(main 的顶端)。所有历史都按原样保留。

git checkout feature
git merge main
# 或:git merge main --no-ff  # 即使可以快进也强制创建合并提交

快进合并: 如果 main 没有分叉(自 feature 分支以来没有新提交),git 可以直接向前移动指针而不创建合并提交:

之前:
main:    A --- B
feature:          \ C --- D

执行 git checkout main && git merge feature(快进)后:
main:    A --- B --- C --- D
feature:          \________/  (现在与 main 相同)

使用 --no-ff 总是创建合并提交,保留功能分支存在的事实。

Git Rebase:线性历史

git rebase 将一个分支的提交重放到另一个分支之上。它重写历史——提交获得新的 SHA。

之前:
main:    A --- B --- C
feature:      \ D --- E

在 feature 分支上执行 git rebase main 后:
main:    A --- B --- C
feature:              \ D' --- E'  (D 和 E 被重新应用到 C 之上)

D'E' 具有与 DE 相同的更改,但父提交不同(因此 SHA 也不同)。分支历史现在是线性的——就好像你一直是从 C 分支出来的。

git checkout feature
git rebase main

处理冲突

当相同行被不同方式更改时,两种方法都需要解决冲突。

合并期间:

git merge main
# CONFLICT:解决文件
git add resolved-files
git merge --continue
# 或:git merge --abort  (取消合并)

Git Rebase vs Merge:何时使用以及它们如何工作 插图

变基期间:

git rebase main
# 在提交 D 上冲突:解决文件
git add resolved-files
git rebase --continue
# 在提交 E 上冲突:再次解决
git add resolved-files
git rebase --continue
# 或:git rebase --abort  (取消,回到变基前状态)

变基冲突可能出现多次——每个被重放的提交一次。使用合并,你只需在合并提交中解决所有冲突一次。

交互式变基:清理历史

git rebase -i(交互式)允许你在分享之前重写、压缩、重新排序和编辑提交:

git rebase -i HEAD~4  # 交互式变基最后 4 个提交

这会打开一个编辑器:

pick a1b2c3 Add user authentication
pick d4e5f6 Fix typo in auth module
pick g7h8i9 Add token refresh logic
pick j0k1l2 WIP: fix edge case

# 命令:
# p, pick <commit> = 使用提交
# r, reword <commit> = 编辑提交信息
# e, edit <commit> = 停止以修改
# s, squash <commit> = 合并到前一个提交
# f, fixup <commit> = 合并,丢弃日志信息
# d, drop <commit> = 删除提交

你可以更改为:

pick a1b2c3 Add user authentication
fixup d4e5f6 Fix typo in auth module  ← 压缩到前一个
pick g7h8i9 Add token refresh logic
drop j0k1l2 WIP: fix edge case       ← 删除此提交

结果:干净的两个提交历史,拼写修复被吸收到原始提交中,WIP 提交消失。

变基的黄金法则

永远不要变基已经推送到共享分支的提交。

当你变基时,你在重写历史——创建具有新 SHA 的新提交。如果队友基于你的原始提交工作,他们的 git 历史会与你的不同。合并会变得一团糟。

# ❌ 如果其他人已经拉取了 main,不要这样做
git checkout main
git rebase feature  # 重写 main 历史

# ✅ 变基在这里是安全的
git checkout feature
git rebase main  # 重写 feature(尚未共享或强制推送到你自己的分支)

何时可以强制推送? 当你在自己的功能分支上工作,且没有其他人跟踪时:

git rebase -i HEAD~3  # 清理你的功能分支
git push --force-with-lease origin feature  # 比 --force 更安全:如果远程已更新则失败

--force-with-lease--force 更安全:如果自上次拉取后远程已更新,它会失败,防止意外覆盖。

比较表

方面 Merge Rebase
历史形状 非线性(分支) 线性
提交 SHA 不变 重写
合并提交 是(通常)
冲突解决 一次,在合并提交中 每个提交一次
在共享分支上安全使用 ✅ 是 ❌ 否
适合功能分支 ✅ 是(no-ff 合并) ✅ 是(推送前)
使用 git bisect 调试 可行 更好(线性)
CHANGELOG 可读性 将功能显示为一个单元 更难看到功能

Git Rebase vs Merge:何时使用以及它们如何工作 插图

团队工作流

功能分支工作流(合并)

# 开始功能
git checkout -b feature/user-auth main

# 工作,提交,工作,提交
git commit -m "Add login endpoint"
git commit -m "Add JWT validation"

# 完成后合并回
git checkout main
git merge --no-ff feature/user-auth -m "Merge feature/user-auth"
git branch -d feature/user-auth

这保留了功能作为一个单元在历史中。适合查看哪些提交属于哪个功能。

合并前变基(干净历史)

# 开始功能
git checkout -b feature/user-auth main

# 工作,工作,工作(混乱的提交可以)
git commit -m "WIP auth"
git commit -m "fix"
git commit -m "more fixes"

# 合并前,用变基清理
git rebase -i main  # 压缩 WIP 提交,修复信息
git rebase main     # 更新到最新的 main

# 现在合并——一个干净的提交或几个逻辑提交
git checkout main
git merge feature/user-auth

主干开发(压缩合并)

许多团队使用 --squash 合并功能分支,将所有功能提交合并为一个:

git checkout main
git merge --squash feature/user-auth
git commit -m "Add user authentication (#142)"

main 总是每个功能有一个提交。历史干净,但失去了细粒度的功能提交。

何时使用每种方法

使用 merge 时:

  • 将长期存在的功能分支合并回 main
  • 在其他人跟踪的共享分支上工作
  • 你想保留事件发生的确切顺序
  • 代码审查在 PR 级别进行(合并整个功能)

使用 rebase 时:

  • 在 PR 之前用最新的 main 更新功能分支
  • 在代码审查前清理混乱的 WIP 提交
  • 你想要一个线性、可读的 git 日志
  • 独自在尚未共享的功能分支上工作

→ 使用 Git Memo 工具 参考常见 git 命令。