陈海蛟

V1

2022/09/16阅读:33主题:前端之巅同款

Merging vs. Rebasing

之前对 git mergegit rebase 理解不深。找到一篇觉得还不错的介绍文章,做了翻译。原文链接:https://www.atlassian.com/git/tutorials/merging-vs-rebasing#conceptual-overview

Merging vs. Rebasing

git rebase 命令因“魔幻的 Git 巫术”而名声在外,因此初学者应该尽量远离它;但是对于一个研发团队来讲,“小心”的使用,的确可以让工作轻松不少。在这篇文章中,我们将对比 get rebasegit merge 命令,并找出把它纳入到典型 Git 工作流中的所有潜在可能性。

概念概述

要理解 git rebase 的第一件事就是要知道,它和 git merge 解决的是用一个问题。都是被设计用来把一个分支的“改动”合并到另一个分支上,只是实现方法不同。

考虑如下场景:当你在一个专属的 feature 分支上开发了一个新的需求,同时其他团队在主分支 main 上提交了新的 commit ,这导致出现了分叉历史(forked history)。这对于任何一个将 Git 作为协同开发工具的开发者来说应该非常熟悉。

现在,我们假设在 main 分支上新的 commit 和你当前工作的 feature 分支是相关的,要把新的 commit 合并到你的 feature 分支,有两个选择:Merge 或者 Rebase 。

Merge

最容易的选项就是把 main 分支 Merge 到需求分支,通常使用如下命令:

git checkout feature # 确保当前处于自己的 feature 分支
git merge main # 把 main 分支上的新改动合并过来

或者,你可以使用一行命令:

git merge feature main

这会在 feature 分支上产生一个新的 “合并记录”(merge commit) 把两个分支的改动历史关联到一起,得到一个如下所示的分支结构:

Merge 是优雅的,因为它是“非破坏性的(non-destructive)”操作。已经存在的分支没有发生任何变化。这样避免了很多 Rebase 的潜在陷阱(会在后面讨论)。

但同时,这也意味着每次你在合并上游分支的改动时,你的 feature 分支都会有产生一个无关的合并记录。如果 main 分支是非常活跃的,这会有些污染你当前的 feature 分支。尽管可以通过 git log 的高阶用法来消除这个问题,但这会为其他开发者在理解当前项目的历史时带来一定的困难。

Rebase 选项

另一个可选方法是,你可以在 main 分支的基础上 Rebase 你的 feature 分支,命令如下:

git checkout feature
git rebase main

这会将整个的 feature 分支移动到 main 分支的顶端,把 main 分支的所有 commit 都有效的合并。但是,区别于使用 git mergegit rebase 会为原 feature 分支上的改动 commit 创建新的 commit。

“Rebase” 的主要好处就是你可以得到一个比较干净的历史记录。首先,它消除了不重要的 merge commit 信息。其次,如上图所见,Rebase 会得到一个时间线性的历史记录,进而可以在无需任何分叉的情况下,跟随 feature 分支的记录一直看到项目的最开始。如此,通过 git loggit bisectgitk 等命令来查看项目会变的非常简单。

但是,对于这种崭新的 commit 历史,有两件事需要权衡:安全性和可追溯性。如果你不遵循“Rebase 黄金法则”,修改项目的改动历史记录可能会为你的协作流程带来潜在的灾难。并且,那些帮助你了解上游的改动在什么时候被合并进来的相关信息也丢失了。

交互式 Rebase

当 commit 被移动到新的分支时,交互式 Rebase 为你提供了修改这些 commit 的机会。相对比自动 Rebase ,交互式 Rebase 的强大之处在于,它允许使用者完全控制分支的 commit 历史。典型场景,在 feature 分支被合并到 main 分支前,用来清除 feature 上杂乱的 commit 记录。

通过传递 i 参数给 git rebase 命令,来开启交互式 Rebase 的会话:

git checkout feature
git rebase -i main

上面命令会打开一个文本编辑器,其中列举了即将被移动的所有 commit 信息。

pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

如上列表准确的定义了在 Rebase 执行之后,分支会成为什么样子。通过改变 pick 命令并且/或者将整体重新排序,你可以让分支的历史记录成为任何你想要的。比如:如果第二个 commit 修复了 第一个 commit 里面的一个很小的问题,你可以通过 fixup 命令压缩两个成为一个 commit 。

pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

当你保存并且关闭文本编辑器的时候,Git 会依照你所编辑的指令进行 Rebase ,最终项目的历史会如下所示:

如此来消除无意义的 commit 可以让你的 feature 的 commit 记录比较简洁和容易理解。在通常情况下,这不是 git merge 轻而易举就能实现的。

Rebase 的黄金法则

一旦你理解了 Rebase 是什么,那么接下来最重要的事情就是要了解:在什么情况下不要使用它。答案就是:永远不要在公共(public)分支中使用它。

比如:想象一下如果把 main 分支 Rebase 到你的 feature 分支上,会发生什么情况 ?

Rebase 会把 main 上的新的 commit 移动到你 feature 分支的顶端。问题是:这仅仅发生在你自己的本地仓库的 main 分支上,所有其他开发者仍然需要基于原来的 main 工作。由于 Rebase 会生成全新的 commit ,Git 会认为你的 main 分支已经和其他人的 main 分支产生了分叉。

同步两个 main 分支的唯一方法就是把你的 main Merge 到上游 main 上去。这会导致两个结果:一份因为 Merge 导致的 merge commit ,和两份包含了相同改动的 commit (一份是原来 main 分支上的,另一份是基于你的 feature 分支 Rebase main 分支后为 main 的改动自动生成的完全新的 commit)。无需多言,这是非常让人困惑的结局。

因此,在你执行 git rebase 之前,永远都要问问自己“还有没有其他人在使用这个分支?”,如果答案是肯定的,把手从键盘上拿开并且开始思考一个非破坏性的方式来合并改动(比如:git revert 命令);相反,如果答案是否定的,你可以随心所欲的安全的去修改 commit 记录。

Force-Pushing

如果你想把 Rebase 后的 main 分支推送到远程仓库,Git 会因为本地 main 和远程 main 存在冲突,进而阻止你这样做。但是,你可以强制推送通过使用 --force 标记。像这样:

# 要非常谨慎的使用这个命令。
git push --force

为了让远程的 main 分支和你 Rebase 过的本地 main 分支相匹配,它会覆盖远程的 main 分支,这会让你团队的其他人对此非常困惑。因此,即便你非常清楚你在做什么,在使用这个命令时,依然有必要保持相当的谨慎。

只有一种情况你可以使用强制推送,就是当你把本地 feature 分支推送到远程仓库后,你又做了一些该分支的清理(比如:出于回退的目的)。就好像说:“哦天啊,我不想推送这个分支原来的版本了,用当前版本代替它。” 同样重要的依然是:没有人基于你之前推送上去的原始版本里的 commit 在工作。

Workflow Walkthrough

可以根据团队的需要,或多或少的将 Rebase 放入到你们已经存在的 Git 工作流程中。本节,我们来探讨,在需求开发的各个阶段,Rebase 可以提供哪些好处。

要使用 git rebase ,第一步是要为需求创建一个专用的分支,这种分支结构对于后续安全的使用 Rebase 至关重要。

Local Cleanup

一种在工作流中使用 Rebase 的最佳方式是清理本地在进行中的 feature 分支的 commit 。通过定期的使用交互式 Rebase ,你可以让你当前 feature 分支上的每一个 commit 都保持专注和有意义。这可以让你尽情的写代码,无需担心同一个功能打了多少个 commit ,因为之后你都可以修改它们。

当使用 git rebase 的时候,有两种选择可以作为 Rebase 的基准。一个是你的 feature 的父分支(比如:main),或者是你的 feature 分支的之前的 commit 。第一种情况,我们已经在之前的交互式 Rebase章节看过一个例子了。后面这种情况,当你想仅仅修复最近的几个 commit 的时候,是很好用的。比如,下面的命令会针对最近的 3 个 commit 启动交互式 Rebase 。

git checkout feature
git rebase -i HEAD~3

通过指明 HEAD~3 作为新的 base,你并不会真正的去移动分支,而是仅仅修改最近的 3 个 commit 。注意,这并不会把上游的改动合并到当前 feature 分支。

如果你想用这种方式重新修改整个 feature 分支,git merge-base 命令可以帮助发现 feature 分支原来的 base。下面命令会返回原始 base 的 commit ID ,然后就可以将其传递给 git rebase 了:

git merge-base feature main

通过使用交互式 Rebase ,是一种将 git rebase 引入你工作流程中很好的方式,因为它只影响你的本地分支。在你完成的项目需求中,其他开发者只能看到一个干净、容易追踪的分支历史。

再次说明,仅仅用在你的私有分支上。如果你和其他开发者通过同一个分支进行协作开发,那么这个分支就是公共的,那么你就不能去改动这个分支的提交记录。

使用交互式 Rebase 来清理本地 commit 的用法,是 git merge 所不能代替的。

将上游改动合并到 feature 分支

概念概述 章节,我们知道了如何使用 git merge 或者 git rebase 将上游来在 main 分支的改动合并到本地的 feature 分支。Merge 是较为安全的选项,因为它保护了项目的完整历史改动信息,而 Rebase 通过移动 feature 分支的 commit 到 main 的最前面创建了新的符合时间线的历史改动信息。

git rebase 的这种用法和本地清理有些相似,区别在于处理过程中,它合并的那些上游 commit 是来自 main 分支。

记住,基于一个远程的非 main 分支去 Rebase 是完全合法的。这可能会发生在你和其他人在同一个分支上协作开发,你需要把其他人的改动合并到你的本地仓库。

举个例子:如果你和另一个叫 John 的开发者一起添加了 commit 到 feature 分支上,在 fetch 了远程的 feature 分支后你的仓库可能会看起来是下面这样子的:

你完全可以像合并上游 main 上的改动一样来解决这个分歧:要么把你的本地 feature 分支和 john/feature 进行 Merge, 要么把你的 feature 分支 Rebase 到 john/feature 分支的最末端。

注意,这里的 Rebase 并不违反 Rebase 的黄金法则 因为仅仅 feature 分支上的本地 commit 被移动了,所有之前的是没有被涉及到的。这就像是说:“把我的改动放到 John 的改动之后。” 在大多数情况下,这比通过一个 merge commit 来同步远程分支更加的符合直觉。

默认情况下,git pull 命令会表现为 Merge ,但你可以强制它通过 Rebase 来合并远程的改动通过传一个 --rebase 选项。

Reviewing a Feature With a Pull Request

如果你使用 Pull Request 作为你 Code Review 的一部分,在创建 Pull Request 之后,你要避免使用 git rebase。一旦你创建了 Pull Request ,其他开发者可能正在看你的 commit ,这就意味着他已经是一个公共分支了。重写他的 commit 记录会让 Git 或者你的同事不能跟踪那些追加到当前分支上的 commit 。

所有来自其他开发者的改动都需要通过 git merge 合并,而非 git rebase

基于此原因,在你提交 Pull Request 之前,通过交互式 Rebase 去清理你的代码是一个好主意。

Integrating an Approved Feature

当你的 feature 分支已经被团队批准通过了,你可以选择在使用 git merge 将 feature 分支合并到 main 分支之前,先将 feature 分支 Rebase 到 main 的最末端。

这和合并上游的改动到一个 feature 分支是相似的,但由于你不能够修改 main 上的 commit ,你最终必须使用 git merge 去合并 feature 分支。但是,在 Merge 之前执行一下 Rebase ,可以确保 Merge 过程是 fast-forwarded 的,得到一个时间线性的改动记录。这也会为你提供一个合并那些在 Pull Request 期间追加的 commit 的机会。

如果你不完全熟悉 git rebase ,你可以选择一直在一个临时分支去执行 Rebase ,这样,如果你突然搞砸了你 feature 分支的 commit 记录,你依然可以回到原来的 feature 分支去重新尝试。比如:

git checkout feature
git checkout -b temporary-branch
git rebase -i main
# [Clean up the history]
git checkout main
git merge temporary-branch

总结

这就是在开始使用 Rebase 来合并分支改动之前,你需要知道的全部了。如果你偏好简洁、时间线性、不保留 merge commit ,那么当你要把别的分支上的改动合并到自己分支时,你可以使用 git rebase 来代替 git merge

另一方面,如果你想保持项目具有完整的历史改动信息,以及避免重写公共 commit 的风险,你可以坚持使用 git merge。两者都是完美有效的,但至少现在当你了解了这么多之后,多了一个使用 git rebase 来合并改动的选项。


译者拙见:

  • 所谓 rebase 其实就是 re-base。那么就要了解什么是 base ?这样看来,当从一个分支开出一个分支的时候,那个选择各奔前程的 commit 点就是两个分支最初的 base 。所以文中提到了一个命令 merge-base 来查看两个分支的 base 点是什么。那么我猜测进行 rebase 之后,通过这个命令获得的 base 点应该也就发生了变化了。
  • 继续理解下去,Rebase 就是要把我们分开的点重新调整一下。调整到哪里呢 ?就要提供一个新的 base 点了。提供的这个点要么是一个分支名,会把这个分支的最后一个 commit 作为新的 base 点;要么就直接是一个 commit 作为最新的 base 点。
  • 所谓交互式 Rebase 就是在给定的新的 base 点之后的所有 commit ,允许你修改,其实只能修改和减少 commit ,不能把一个 commit 的改动拆成 2 个 commit 。
  • 文中列举的几个 rebase 在工作流程中的使用场景。简单来讲就是:在本地清理 commit、同步远程主分支的改动。好处也说了,可以让你的本地开发分支的 commit 历史简洁易懂不混乱。

分类:

前端

标签:

Git

作者介绍

陈海蛟
V1