如何正确使用 Git rebase 指令
理解 Rebase 变基
git rebase 命令的文档描述是 Reapply commits on top of another base tip
,从字面上理解是「在另一个基端之上重新应用提交」,这个定义听起来有点抽象,换个角度可以理解为「将分支的基础从一个提交改成另一个提交,使其看起来就像是从另一个提交中创建了分支一样」,如下图:
假设我们从 Master 的提交 A 创建了 Feature 分支进行新的功能开发,这时 A 就是 Feature 的基端。接着 Matser 新增了两个提交 B 和 C, Feature 新增了两个提交 D 和 E。现在我们出于某种原因,比如新功能的开发依赖 B、C 提交,需要将 Master 的两个新提交整合到 Feature 分支,为了保持提交历史的整洁,我们可以切换到 Feature 分支执行 rebase 操作:
1 | git pull origin master --rebase |
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 | D---E---F feature |
- merge 非线性
- rebase 线性
rebase 潜在问题
- 如果涉及到已经推送过的提交,需要强制推送才能将本地 rebase 后的提交推送到远程。因此绝对不要在一个公共分支(也就是说还有其他人基于这个分支进行开发)执行 rebase,否则其他人之后执行 git pull 会合并出一条令人困惑的本地提交历史,进一步推送回远程分支后又会将远程的提交历史打乱(详见 Rebase and the golden rule explained)
- 假如你频繁的使用 rebase 来集成主分支的更新,一个潜在的后果是你会遇到越来越多需要合并的冲突。尽管你可以在 rebase 过程中处理这些冲突,但这并非长久之计,更推荐的做法是频繁的合入主分支然后创建新的功能分支,而不是使用一个长时间存在的功能分支。
- 对新手不友好,新手很有可能在交互模式中误操作「丢失」某些提交(实际可以通过 reflog 找回)。
交互模式
如何合并多次提交纪录
- 我们来合并最近的 4 次提交纪录,执行:
1 | git rebase -i HEAD~4 |
- 这时候,会自动进入 vi 编辑模式:
1 | pick aa3d9c6c add: commit A |
- 如果修改,这时候会进入 vi 编辑描述信息模式:
1 | # 这是一个 4 个提交的组合。 |
常用命令
1 | git pull origin <分支> --rebase // 不产生新的合并commit ID,合并远程代码 |