The git rebase command has a reputation for being magical Git voodoo that beginners should stay away from, but it can actually make life much easier for a development team when used with care. In this article, we’ll compare git rebase with the related git merge command and identify all of the potential opportunities to incorporate rebasing into the typical Git workflow.

概念性概述

要了解 git rebase,首先要明白的是它解决的问题与 git merge 一样。这两个命令都是将一个分支的变更集成到另一个分支—只是两者的方式截然不同。

设想一下,您开始在一个专用分支中处理新功能,然后其他团队成员使用新的提交更新 master 分支。这会生成新拷贝的历史记录,对于使用 Git 作为协作工具的人来说,这一切应该都不陌生。

A forked commit history

现在,假设 master 中的新提交与您处理的功能相关。要将新的提交并入您的 feature 分支中,有两个选项:合并或变基。

The Merge Option

The easiest option is to merge the master branch into the feature branch using something like the following:

git checkout feature
git merge master

Or, you can condense this to a one-liner:

git merge feature master

This creates a new “merge commit” in the feature branch that ties together the histories of both branches, giving you a branch structure that looks like this:

Merging master into the feature branch

合并是不错的选择,因为它是一种非破坏性的操作。现有分支不会得到任何更改。这避免了变基操作的所有隐患(后面将会讨论)。

另一方面,这也意味着每次需要并入上游变更时,feature 分支将会产生一个无关的合并提交。如果 master 非常活跃,这可能会污染您的功能分支的历史记录。虽然可以使用高级 git log 选项来缓解此问题,但可能会让其他开发人员难以理解项目的历史记录。

The Rebase Option

As an alternative to merging, you can rebase the feature branch onto master branch using the following commands:

git checkout feature
git rebase master

这会移动整个 feature 分支,以在 master 分支的节点开始,从而有效地将所有新提交并入 master 中。但是,变基并不使用合并提交,而是为原始分支中的每个提交创建全新的提交来重写项目历史记录。

Rebasing the feature branch onto master

变基的主要优势在于您可以获得更干净的项目历史记录。首先,它不像 git merge 一样需要不必要的合并提交。其次,如上图所示,变基还会产生完美的线性项目历史记录—您可以在没有任何新拷贝的情况下,始终按照 feature 的提示找到项目的源头。这可以让您更轻松地使用 git loggit bisectgitk 等命令导航项目。

但是,对于这种清晰的提交历史记录,存在两个需要权衡的地方:安全性和可追溯性。如果您不遵循变基的黄金法则,重写项目历史记录可能会对您的协作工作流造成潜在危害。另外,变基会丢失合并提交所带来的上下文—您无法看到上游变更何时被并入功能。

Interactive Rebasing

交互式变基可让您在提交移动到新分支时对提交进行更改。这甚至比自动变基更强大,因为这可以让您完全控制分支的提交历史记录。通常情况下,可用它来清理混乱的历史记录,然后再将功能分支合并到 master 中。

To begin an interactive rebasing session, pass the i option to the git rebase command:

git checkout feature
git rebase -i master

This will open a text editor listing all of the commits that are about to be moved:

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

This listing defines exactly what the branch will look like after the rebase is performed. By changing the pick command and/or re-ordering the entries, you can make the branch’s history look like whatever you want. For example, if the 2nd commit fixes a small problem in the 1st commit, you can condense them into a single commit with the fixup command:

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

When you save and close the file, Git will perform the rebase according to your instructions, resulting in project history that looks like the following:

Squashing a commit with an interactive rebase

像这样清除不重要的提交,可以让功能的历史记录更容易理解。这是 git merge 完全无法做到的事情。

变基的指导原则

在了解什么是变基之后,最重要的是要学习何时使用它。git rebase 的黄金法则是永远不要在公有分支上使用它。

例如,想一下如果您将 master 变基到 feature 分支,会发生什么:

Rebasing the master branch

The rebase moves all of the commits in master onto the tip of feature. The problem is that this only happened in your repository. All of the other developers are still working with the original master. Since rebasing results in brand new commits, Git will think that your master branch’s history has diverged from everybody else’s.

同步两个 master 分支的唯一方法是将它们重新合并在一起,从而产生一个额外的合并提交两组包含相同更改的提交(原始提交和变基分支的提交)。不用说,这是一个非常令人困惑的情况。

So, before you run git rebase, always ask yourself, “Is anyone else looking at this branch?” If the answer is yes, take your hands off the keyboard and start thinking about a non-destructive way to make your changes (e.g., the git revert command). Otherwise, you’re safe to re-write history as much as you like.

Force-Pushing

如果您尝试将变基的 master 分支推回到远程代码库,Git 将阻止您这样做,因为它与远程 master 分支冲突。但是,您可以通过使用 --force 标记来强制推送,如下所示:

# Be very careful with this command!
git push --force

这将覆盖远程 master 分支,以匹配来自您的代码库的变基分支,并让您团队的其余成员感到非常困惑。所以,只有当您确切知道您在做什么时,才应该非常小心地使用此命令。

适合使用强制推出的时机之一是,您将私有功能分支推送到远程代码库(例如,用于备份目的)之后,执行了本地清理。这就像是在说“哎呀,我真的不想推送这个功能分支的原始版本。我要推送当前版本。”再次重申,没有人正在处理来自功能分支的原始版本的提交,这一点很重要。

工作流演练

Rebasing can be incorporated into your existing Git workflow as much or as little as your team is comfortable with. In this section, we’ll take a look at the benefits that rebasing can offer at the various stages of a feature’s development.

The first step in any workflow that leverages git rebase is to create a dedicated branch for each feature. This gives you the necessary branch structure to safely utilize rebasing:

Developing a feature in a dedicated branch

Local Cleanup

One of the best ways to incorporate rebasing into your workflow is to clean up local, in-progress features. By periodically performing an interactive rebase, you can make sure each commit in your feature is focused and meaningful. This lets you write your code without worrying about breaking it up into isolated commits—you can fix it up after the fact.

当调用 git rebase 时,您有两个针对新基准的选项:功能的父分支(例如,master)或功能中更早的提交。我们在交互式变基这一部分看到了第一个选项的示例。当您只需要修复最近几个提交时,后一个选项是很好的选择。例如,以下命令仅对最后 3 个提交进行交互式变基。

git checkout feature
git rebase -i HEAD~3

通过指定 HEAD~3 作为新基准,您实际上并没有移动分支—您只是以交互的形式重写其后的 3 个提交。请注意,这将不会将上游变更并入到 feature 分支。

Rebasing onto Head~3

如果您想要使用此方法重写整个功能,则可以使用 git merge-base 命令查找 feature 分支的原始基准。以下内容返回原始基准的提交 ID,然后您可以传递给 git rebase

git merge-base feature master

使用这种交互式变基的是将 git rebase 引入到工作流中的好方法,因为它仅影响本地分支。其他开发人员可以看到的只是您的成品,也就是一个干净、易于追踪的功能分支历史记录。

但是,这仅适用于私有功能分支。如果您通过一个功能分支与其他开发人员进行协作,则该分支是公有分支,您不能重写其历史记录。

git merge 没有替代方法来使用交互式变基清理本地提交。

Incorporating Upstream Changes Into a Feature

In the Conceptual Overview section, we saw how a feature branch can incorporate upstream changes from master using either git merge or git rebase. Merging is a safe option that preserves the entire history of your repository, while rebasing creates a linear history by moving your feature branch onto the tip of master.

This use of git rebase is similar to a local cleanup (and can be performed simultaneously), but in the process it incorporates those upstream commits from master.

请注意,变基到远程分支而不是 master 分支上是完全合理的。当与其他开发人员在同一功能上进行协作时,可能会发生这种情况,您需要将他们的变更并入到您的代码库中。

For example, if you and another developer named John added commits to the feature branch, your repository might look like the following after fetching the remote feature branch from John’s repository:

Collaborating on the same feature branch

您可以使用与从 master 中集成上游变更完全相同的方法解决此新拷贝:将本地 featurejohn/feature 合并,或将本地 feature 变基到 john/feature 节点。

Merging vs. rebasing onto a remote branch

Note that this rebase doesn’t violate the Golden Rule of Rebasing because only your local feature commits are being moved—everything before that is untouched. This is like saying, “add my changes to what John has already done.” In most circumstances, this is more intuitive than synchronizing with the remote branch via a merge commit.

By default, the git pull command performs a merge, but you can force it to integrate the remote branch with a rebase by passing it the --rebase option.

Reviewing a Feature With a Pull Request

如果您在代码评审过程中使用拉取请求,那么在创建拉取请求后需要避免使用 git rebase。当您提出拉取请求后,其他开发人员将会查看您的提交,这意味着它是一个公有分支。重写其历史记录将使 Git 和您的队友无法跟踪任何添加到该功能的后续提交。

来自其他开发人员的任何变更都需要使用 git merge 而不是使用 git rebase 并入。

因此,在提交您的拉取请求之前,通过交互式变基清理代码通常是一个好办法。

Integrating an Approved Feature

在您的团队批准了一项功能后,您可以选择在将功能变基到 master 分支的节点,然后使用 git merge 将功能集成到主基准代码中。

这与将上游变更并入到功能分支中类似,但是由于您不被允许在 master 分支中重写提交,您最终必须使用 git merge 来集成该功能。但是,通过在合并之前执行变基,可确保合并将以快进模式进行,从而形成完美的线性历史记录。这也使您有机会在拉取请求期间添加任何后续提交。

Integrating a feature into master with and without a rebase

如果您完全不习惯使用 git rebase,可以随时在临时分支中执行变基。这样,如果您不小心弄乱了功能的历史记录,则可以检出原始分支,然后重试。例如:

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

Summary

以上就是您在开始变基分支之前真正需要了解的内容。如果您喜欢干净的线性历史记录且没有不必要合并提交,那么您应该在集成其他分支的变更时使用 git rebase 而不是 git merge

On the other hand, if you want to preserve the complete history of your project and avoid the risk of re-writing public commits, you can stick with git merge. Either option is perfectly valid, but at least now you have the option of leveraging the benefits of git rebase.