理解 Rebase 变基

git rebase 命令的文档描述是 Reapply commits on top of another base tip,从字面上理解是「在另一个基端之上重新应用提交」,这个定义听起来有点抽象,换个角度可以理解为「将分支的基础从一个提交改成另一个提交,使其看起来就像是从另一个提交中创建了分支一样」,如下图:
rebase

假设我们从 Master 的提交 A 创建了 Feature 分支进行新的功能开发,这时 A 就是 Feature 的基端。接着 Matser 新增了两个提交 B 和 C, Feature 新增了两个提交 D 和 E。现在我们出于某种原因,比如新功能的开发依赖 B、C 提交,需要将 Master 的两个新提交整合到 Feature 分支,为了保持提交历史的整洁,我们可以切换到 Feature 分支执行 rebase 操作:

1
2
3
git pull origin master --rebase

git rebase master

rebase 的执行过程是首先找到这两个分支(即当前分支 Feature、 rebase 操作的目标基底分支 Master) 的最近共同祖先提交 A,然后对比当前分支相对于该祖先提交的历次提交(D 和 E),提取相应的修改并存为临时文件,然后将当前分支指向目标基底 Master 所指向的提交 C, 最后以此作为新的基端将之前另存为临时文件的修改依序应用。

我们也可以按上文理解成将 Feature 分支的基础从提交 A 改成了提交 C,看起来就像是从提交 C 创建了该分支,并提交了 D 和 E。但实际上这只是「看起来」,在内部 Git 复制了提交 D 和 E 的内容,创建新的提交 D’ 和 E’ 并将其应用到特定基础上(A→B→C)。尽管新的 Feature 分支和之前看起来是一样的,但它是由全新的提交组成的。

rebase 操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。

rebase 和 merge 的区别

  • merge 保留所有 commit,并会创建一个 merge commit
  • rebase 基于目标基底,类似 cherry-pick 生成新的 commit,不会创建 merge commit

以上场景同样可以使用 merge 来达成目的,假设我们有如下分支:

1
2
3
      D---E---F     feature
/
A---B---C master
  • merge 非线性
    merge
  • rebase 线性
    rebase

rebase 潜在问题

  • 如果涉及到已经推送过的提交,需要强制推送才能将本地 rebase 后的提交推送到远程。因此绝对不要在一个公共分支(也就是说还有其他人基于这个分支进行开发)执行 rebase,否则其他人之后执行 git pull 会合并出一条令人困惑的本地提交历史,进一步推送回远程分支后又会将远程的提交历史打乱(详见 Rebase and the golden rule explained
  • 假如你频繁的使用 rebase 来集成主分支的更新,一个潜在的后果是你会遇到越来越多需要合并的冲突。尽管你可以在 rebase 过程中处理这些冲突,但这并非长久之计,更推荐的做法是频繁的合入主分支然后创建新的功能分支,而不是使用一个长时间存在的功能分支。
  • 对新手不友好,新手很有可能在交互模式中误操作「丢失」某些提交(实际可以通过 reflog 找回)。

交互模式

如何合并多次提交纪录

  • 我们来合并最近的 4 次提交纪录,执行:
1
git rebase -i HEAD~4
  • 这时候,会自动进入 vi 编辑模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pick aa3d9c6c add: commit A
pick f072ef48 update:commit B
pick 4e84901a feat: commit C
pick aa3d9c6c feat: commit D

# 变基 aa3d9c6c..aa3d9c6c 到 aa3d9c6c(4 个提交)
#
# 命令:
# p, pick <提交> = 使用提交
# r, reword <提交> = 使用提交,但编辑提交说明
# e, edit <提交> = 使用提交,但停止以便在 shell 中修补提交
# s, squash <提交> = 使用提交,但挤压到前一个提交
# f, fixup [-C | -c] <提交> = 类似于 "squash",但只保留前一个提交
# 的提交说明,除非使用了 -C 参数,此情况下则只
# 保留本提交说明。使用 -c 和 -C 类似,但会打开
# 编辑器修改提交说明
# x, exec <命令> = 使用 shell 运行命令(此行剩余部分)
# b, break = 在此处停止(使用 'git rebase --continue' 继续变基)
# d, drop <提交> = 删除提交
# l, label <label> = 为当前 HEAD 打上标记
# t, reset <label> = 重置 HEAD 到该标记
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . 创建一个合并提交,并使用原始的合并提交说明(如果没有指定
# . 原始提交,使用注释部分的 oneline 作为提交说明)。使用
# . -c <提交> 可以编辑提交说明。
<oneline>]

  • 如果修改,这时候会进入 vi 编辑描述信息模式:
1
2
3
4
5
6
7
8
9
# 这是一个 4 个提交的组合。
# 这是第一个提交说明:

commit A

# 这是提交说明 #2:

commit B
......

常用命令

1
2
3
4
5
6
7
git pull origin <分支> --rebase // 不产生新的合并commit ID,合并远程代码
git rebase <分支> // 不产生新的合并commit ID,仅支持合并本地代码
git rebase -i HEAE~2 // 合并前2次commit
git rebase -i <start commit> <end commit> // 合并指定多个commit,如果中间的commit,会发现HEAD指针是指向对应的commit,这时候可以基于这个Commit新建分支`rebase-branch`,切换回老分支,执行 git rebase rebase-branch
git rebase --edit-todo // 异常操作
git rebase --continue // 继续rebase操作
git rebase --abort // 取消rebase操作

参考地址: