Git merge 与 rebase 的区别:示例及如何选择

  1. 首页
  2. 博客
  3. 软件开发
软件开发

Git merge 与 rebase 的区别:示例及如何选择

Git merge 与 rebase 的选择决定了项目历史的样子。通过同一个示例和真实的 log 输出对比两者,学会避免灾难的那条规则,并直接套用一套安全的团队工作流。

阅读约 7 分钟
Git merge 与 rebase 的区别:示例及如何选择
要点
  • Merge 用一个新的合并提交连接两个分支,并保持历史的原貌。
  • Rebase 把你的提交在另一个分支之上重放,得到一条直线,但提交哈希会变。
  • 永远不要对别人已经拉取的提交执行 rebase。
  • 在发起拉取请求之前对自己的功能分支做 rebase,把它并入主分支时使用 merge。

你的功能分支落后于 main,而你需要新的改动。Git merge 与 rebase 就是获取这些改动的两种方式之间的选择,两者都是正确的,区别在于之后的历史长什么样。

我们建了一个很小的仓库,运行了两个命令,并把真实输出贴在下面,让你比较结果而不是凭空想象。刚接触 Git?请先看我们的 Git 入门教程(英文)。

git merge 和 git rebase 分别做什么

git merge 取两个分支的最新提交并把它们合并。如果两个分支都有新提交,Git 会创建一个有两个父提交的合并提交,已存在的内容不会被改动。

git rebase 取出你分支独有的提交,先放在一边,把你的分支移到另一个分支的最新提交上,然后逐个重放你的提交。被重放的提交是带有新哈希的新提交,尽管改动内容相同。

git merge 实战

起点如下:main 上有提交 A,feature 上有两个提交(F1、F2),之后 main 又多了一个提交(M1)。现在把 main 合并进 feature。

git switch feature
git merge main
git log --oneline --graph

*   f507aab Merge main into feature
|\
| * 8842fb5 M1
* | 082cc6a F2
* | 5a71e3c F1
|/
* 8766865 A

图中展示了真实的过程:两条工作线被合并在一起。原来的提交 F1 和 F2 保留各自的哈希。代价是多了一个合并提交,而在活跃的项目里,这张图会变成一张网。

git rebase 实战

起点相同,但这次我们把 feature 变基到 main 上。

git switch feature
git rebase main
git log --oneline --graph

* 1ff474b F2
* bf9149e F1
* 8842fb5 M1
* 8766865 A

历史成了一条直线,就像你是在 M1 之后才开始工作的。注意哈希:F1 原来是 5a71e3c,现在是 bf9149e;F2 从 082cc6a 变成了 1ff474b。Git 重写了它们,这就是 rebase 需要小心的原因。

merge 与 rebase 并排对比

方面MergeRebase
历史形状带合并提交的分叉图一条直线
已有提交保持不变以新哈希重写
冲突在合并提交中一次性解决每个被重放的提交都可能需要处理
在共享分支上是否安全安全不安全
最适合把完成的工作并入 main评审前更新自己的分支

提示:拿不定主意时,问问还有谁拥有你的分支。没有人的话,rebase 是安全的;如果同事已经拉取了它,就用 merge。

rebase 的黄金法则

不要对存在于你自己仓库之外的提交做 rebase。rebase 之后,你的分支与服务器上的副本不再一致,所以推送需要强制。如果同事在旧提交的基础上做了工作,他们的历史就会与你的产生分歧,处理起来很痛苦。

对自己的私有功能分支做 rebase 没问题;对 main 或其他人在用的分支做则不行。

安全的团队工作流

这套流程能保持历史整洁,又不会让任何人意外。大多数团队在 CI 流水线前也这么做,例如我们的 GitHub Actions CI/CD 指南(英文)里的那个。

git switch feature
git fetch origin
git rebase origin/main        # or: git pull --rebase
# fix conflicts, then: git add <file> && git rebase --continue
git push --force-with-lease   # only on your own branch

如果自上次 fetch 以来有人向服务器分支推送过,--force-with-lease 会拒绝覆盖它,而普通的 --force 会悄悄把它毁掉。拉取请求通过后,把它合并进 main。

如何撤销错误的 rebase

rebase 看起来有破坏性,但旧提交会在 Git 的 reflog 中保留一段时间,所以你可以退回去。

git reflog                    # find the commit before the rebase
git reset --hard HEAD@{5}     # example position, check reflog first

在任何有风险的 rebase 之前,用 git branch backup 创建一个备份分支,恢复就只需一条命令。旁边还要在容器里运行数据库或工具?请看我们的 Docker 入门教程(英文)。

总结

Merge 保留真相,rebase 保持整洁。用 rebase 刷新自己的分支,用 merge 交付它,并且永远不要改写别人依赖的历史。

常见问题

git merge 和 git rebase 有什么区别?

Merge 用一个合并提交合并两个分支,并保留所有已有提交。Rebase 把你的提交移到另一个分支之上并重写它们,从而得到线性的历史。

我该用 merge 还是 rebase?

对自己的功能分支用 rebase 保持最新,把它交付到 main 这样的共享分支时用 merge。只要其他人共享该分支,就用 merge。

git rebase 危险吗?

在私有分支上是安全的,在共享分支上有风险,因为它会重写提交哈希。出问题时,可以用备份分支和 reflog 来恢复。

force-with-lease 是做什么的?

只有当远程分支仍在你上次看到的位置时才强制推送,因此不会意外覆盖同事的新提交。

rebase 会删除我的提交吗?

不会。它会创建内容相同的新提交,旧提交会留在 reflog 中,直到 Git 清理它们。

继续学习(英文文章)

与 Stack Quora 合作

需要测试、WordPress 或 SEO 方面的帮助?

我们帮助团队交付更好的软件并让网站成长。告诉我们你在做什么,我们会给出清晰的下一步建议。

继续阅读

更多指南 测试、人工智能和现代开发

喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。

人工智能

人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它

人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。

阅读文章 →
DevOps

环境变量解释:.env 文件、秘密和配置正确完成

使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。

阅读文章 →
软件开发

API 速率限制解释:算法、标头和处理 429 错误

API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。

阅读文章 →
测试自动化

Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。

阅读文章 →
软件测试

代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义

用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker Volumes 与 Bind Mounts:以正确的方式保留容器数据

Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。

阅读文章 →
测试自动化

TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式

TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。

阅读文章 →
软件开发

Git merge 与 rebase 的区别:示例及如何选择

用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。

阅读文章 →
软件开发

Python 列表推导式:语法、示例与何时避免使用

Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →