整合变更的两种方式
当你使用 Git branch 工作时,总会遇到需要将一个 branch 的变更引入另一个 branch 的时候。Git 为此提供了两种主要策略:merge 和 rebase。两者都能达到相同的最终结果——你的 branch 包含所有最新变更——但它们以根本不同的方式实现这一点,选择正确的方式对项目的历史记录和团队的 workflow 至关重要。
Merge 的工作原理
merge 接受两个 branch 并通过创建一个新的 merge commit 来合并它们。这个 merge commit 有两个父节点:你当前 branch 的顶端和你正在整合的 branch 的顶端。
git checkout feature/login
git merge main
此后,你的功能 branch 包含了来自 main 的所有变更,由 merge commit 连接在一起。两个 branch 的历史记录完全按照实际发生的情况保留。如果你查看 commit 图,会看到两条开发线在 merge 点汇合。
merge 的主要特征:
- 非破坏性。不会修改任何现有 commit。历史记录就是实际发生的事情。
- 创建 merge commit。这个额外的 commit 将两个历史记录绑定在一起。
- 保留上下文。你始终可以看到 branch 何时创建以及何时被合并回来。
Fast-forward merge
如果你的 branch 没有与目标分叉(即自你创建 branch 以来目标上没有新的 commit),Git 默认执行 fast-forward merge。它只是将 branch 指针向前移动。不需要创建 merge commit。即使在这种情况下,你也可以强制创建 merge commit:
git merge --no-ff feature/login
当你想保留工作在单独 branch 上进行的事实时,这很有用,即使 merge 很简单。
Rebase 的工作原理
rebase 采用不同的方式。它不创建 merge commit,而是将你的 commit 重放到另一个 branch 之上。结果是一个线性历史,看起来就像你从目标 branch 的最新点开始工作。
git checkout feature/login
git rebase main
幕后发生的事情:
- Git 找到两个 branch 的共同祖先。
- 临时移除你的 branch 的 commit。
- 将你的 branch 指针移到 main 的顶端。
- 逐个重放你的 commit。
结果历史是完美线性的。没有 merge commit,图中没有分叉和汇合的线条。看起来就像你在 main 的最新变更之后编写了所有代码,即使实际情况并非如此。
rebase 的主要特征:
- 创建线性历史。commit 图干净且易于阅读。
- 重写 commit 哈希。重放的 commit 获得新的 SHA 哈希,即使内容相同。
- 无 merge commit。branch 干净地整合,无需额外 commit。
何时使用 Merge
在多种情况下,merge 是更安全、更直接的选择:
- 共享 branch。当多人在同一 branch 上工作时,merge 保留每个人的历史记录,不会重写他人依赖的 commit。
- 保留历史很重要。在需要精确追踪变更何时以及如何被整合的项目中,merge 提供完整、未修改的记录。
- 整合长期 branch。将发布 branch merge 回 main,或合并两个明显分叉的 branch 时,merge commit 清楚地标记整合点。
- 你想要简单。merge 在概念上更容易。出错的可能性更小,对 Git 新手开发者尤其如此。
何时使用 Rebase
当你想要干净的线性项目历史时,rebase 表现出色:
- 更新功能 branch。在将功能 merge 到 main 之前,在最新的 main 上 rebase 可以得到一组干净的 commit,无需 merge commit 即可直接应用。
- 保持干净的历史。线性历史更容易用
git log导航,更容易在追踪 bug 时使用 bisect,更容易在 pull request 中审查。 - 本地清理。如果你在工作时做了小的增量 commit,可以在共享 branch 之前使用 rebase 进行整理。
Rebase 的黄金法则
永远不要对已经 push 到共享 branch 的 commit 进行 rebase。
这是最重要的规则。因为 rebase 会重写 commit 哈希,任何基于原始 commit 进行工作的人都会遇到问题。他们的历史不再与 rebase 后的历史匹配,导致 commit 重复、冲突和混乱。
规则很简单:如果其他人可能已经 pull 了你的 commit,就不要 rebase。只在你自己的本地 branch 上使用 rebase,或者在你是唯一贡献者的 branch 上使用。
如果你不小心对共享 branch 做了 rebase,团队中的其他开发者将需要处理分叉的历史。这通常意味着 force-push(覆盖远程仓库)并让所有人 reset 他们的本地副本。这是破坏性的,也是可以避免的。
交互式 Rebase:精确地重写历史
交互式 rebase(git rebase -i)不仅仅是移动 commit。它允许你修改 commit 历史本身:
git rebase -i HEAD~5
这会打开最近 5 个 commit 的列表,你可以:
- pick -- 保持 commit 不变
- reword -- 修改 commit 消息
- edit -- 暂停以修改 commit
- squash -- 与前一个 commit 合并
- fixup -- 类似 squash,但丢弃 commit 消息
- drop -- 完全删除 commit
- reorder -- 通过重新排列行来改变顺序
交互式 rebase 对于在 merge 前整理混乱的 branch 非常强大。你可以将"修复错别字"的 commit squash 到其父 commit 中,重写不清晰的消息,并重新排列 commit 使其讲述一个逻辑故事。
实用的工作流程
许多团队使用结合两种策略优势的方法:
- 从 main 创建功能 branch。
- 工作期间,定期将功能 branch rebase 到 main 上以保持最新。
- 在创建 pull request 之前,使用交互式 rebase 清理 commit。
- 将功能 branch merge 到 main(通常使用 merge commit 来标记整合点)。
这为你提供了功能 branch 上干净的线性历史和 main 上清晰的 merge 点。这是一个适用于大多数团队的务实折中方案。
GitSquid 中的 Rebase 和 Merge
GitSquid 直接从界面支持 merge 和 rebase。你可以从 commit 图中任何 branch 标签的上下文菜单中 merge branch。对于 rebase,GitSquid 包含一个可视化的交互式 rebase 编辑器,让你通过拖拽和点击来重新排序、squash、fixup、reword 和 drop commit,而不是在终端中编辑文本文件。即使你觉得命令行版本令人望而生畏,这也使交互式 rebase 变得易于使用。
总结
| Merge | Rebase | |
|---|---|---|
| 历史 | 非线性,保留 branch | 线性,干净 |
| Commit 哈希 | 不变 | 被重写 |
| Merge commit | 是(fast-forward 除外) | 否 |
| 对共享 branch 安全 | 是 | 否 |
| 最适合 | 整合共享工作 | 清理功能 branch |
merge 和 rebase 都不是普遍更好的选择。它们是用于不同情况的工具。理解每个工具对你的历史做了什么,遵循 rebase 的黄金法则,并选择适合当前情况的。未来的你在阅读 git log 时会感谢自己的。