Git教程 · 变基与拣取
通常, 一段提交历史中往往都存在着许多杂乱的分支。Git 可以尽可能地帮助我们理顺这些历史记录。这里会用到的最重要的工具当然就是 rebase
命令了,它可以可以将某一次提交在提交图上产生的影响从一个节点转移到另一节点。
我们可以用该命令做以下几件事情。
- 如果你不小心在错误的分支上执行了一次提交。例如你可能将一次 bug 修复提交到了 当前开发线(即 master 分支)上。
- 当多个开发者在致力于开发同一软件时,他们会频繁地整合自己的修改。如果不进行 变基,他们可能会创建出一部带有多个小分支和分岔的历史(我们称之为钻石链)。 通过
rebase
命令,我们可以将其改造成一部较为平滑的线性历史。
1️⃣ 工作原理:复制提交
变基操作的工作原理很简单: Git 会让我们想要移动的提交序列在目标分支上按照相同的顺序重新再现一遍。这就相当于我们为各个原提交做了个副本,它们拥有相同的修改集、同一作者、日期以及注释信息。
请注意:乍看之下,好像 Git 只是在执行变基重操作时移动了相关提交。但事实上,这 些“被转移”的提交往往都是一些拥有不同提交散列值的新提交。了解这一情况非常重要,
尤其是在提交已经从原分支已经扩散到其子分支时。
由于新提交会被记录在提交图中的不同位置,所以当然有可能会引发冲突,因为其原本的修改未必适合当前的情况。对于这类修改,我们必须要通过手动来解决合并冲突。
2️⃣ 避免“钻石链”
如果为同一软件工作的若干个开发者频繁地归并各种修改,该项目所建立起来的提交历史看起来就会像一条钻石链。这时候我们可以利用变基操作将其整理成一部内容等效,但线性发展的历史。
下面,我们通过下图中的具体例子来看看变基操作的具体过程。如你所见,从 master 分支上岔出了一个名为 feature-a 的分支,其中包含了C 和 D 两个提交。同时, master 分支也得到了进一步的开发,于是多出了一个提交B。
现在,你可以通过 git merge master
命令来合并这些修改,然后再用 rebase
命令理顺其
历史纪录。该命令需要一个参数,以说明我们要将活动分支上的最新修改纳入哪一个分支。
> #Branch "feature-a" is active
> git rebase master
在收到这个命令之后,Git就会去做以下事情,以便将活动分支(feature-a)融合到 master
分支上。
- 确认涉及到哪些提交:Git会确认是要将活动分支 feature-a 上的哪一些目前不在目标分支 ( master) 上,在这里就是提交C 和 D。
- 确认目标位置:Git 会确认目标提交的位置,该提交就是 master 上 feature-a 将要执行变基操作地方,在这里就是提交B。
- 复制提交:以目标提交为基础重演上述提交中的所有修改,并相应创建提交 C’ 和 D’。
- 将活动分支重置:活动分支将被移动到上述被复制提交的顶部,在这里就是提交D’。
然而在很多情况下,我们可能不会直接去调用 rebase
命令。相反,我们通常会用 pull 命令加上 --rebase
选项来对远程版本库中的修改进行变基处理。
注意:旧提交 C 和 D 偶尔还会留在版本库中,虽然它们已经不再直接可见,因为 feature-a 分支现在已经指向了 D’ 。但是,我们依然还是可以通过散列值对C 和 D 进行访问。 只有在用 gc 命令执行垃圾回收之后,它们才会真正从版本库中消失。
3️⃣ 什么情况下会遇到冲突
和 merge
命令一样,rebase
命令也会在相关修改不匹配的时候以冲突的形式被终止。 但它们之间有个重要的区别:即在合并过程中,我们得到的是两个分支合体之后的单一提交结果。而在变基过程中,我们是在依次执行重复的若干次提交。如果一切顺利,其最后一次所提交的内容应该会与其执行 merge
命令时的结果相同,因为 Git 在这两个命令中采用 了相同的冲突解决算法。但如果 rebase
命令在执行过程中遇到冲突情况,该命令进程就会被打断,相关文件中也会出现冲突标志。我们需要先手动或通过合并工具对文件进行清理, 并重新将它们添加到暂存区中。然后再执行 rebase
命令加 --continue
选项,从该点继续之前的进程。
> git add foo.txt
> git add bar.txt
> git rebase --continue
当然,我们也可以用 --abort
选项取消这次的 rebase
命令,或者用 --skip
选项跳过引起冲突的提交。这样该次提交就被直接忽略,其中的修改将不会出现在新分支上。
需要特别注意,与合并操作不同的是,在被中断变基作业的那些提交副本中可能已经有一部分被执行变基操作了。
4️⃣ 移植分支
有时候,在已经创建了一个分支,并完成其首次提交的情况下,我们也可以通过 --onto
选项将该分支移植到提交图中的另一个位置上。
在下面的例子中, feature-a 分支被移植到了release1 分支上。
> #Branch "feature-a" is active
> git rebase master --onto release1
在这里, rebase
命令的第一个参数所指定的是原分支(即这里的 master 分支)。然后,
Git 就会去确认活动分支(即 feature-a) 上所有不属于原分支的所有提交(在这里就是提交E 和 F) 。 然后通过--onto
选项将这些提交拷贝到指定位置上(即这里的 release1 分支)。
注意: rebase
命令中的原位置并不一定非得是一个分支。它也可以是任何提交。
5️⃣ 执行变基后原提交的情况
这些提交会在变基过程中被复制。但其原件(即本例中的提交C 和 D) 依然还可以通过散列值来进行访问,如图所示。通常情况下,当没有分支可以进一步从这些提交中继续发展时,下一轮垃圾收集过程(通过 gc
命令)就会直接将它们从版本库中删除。
6️⃣ 提交的原件与副本存在于同一版本库中所带来的问题
重复容易造成版本库中的混乱。它们可以很容易引起误解,让人以为某段既定的代码修 改包含在哪一分支上,不包含在哪一分支上。通常来说,git log HEAD..a-branch
显示的是在a-branch 上而不在当前分支的那些提交。如果存在重复的话,当前分支也可能已经包含了该代码的修改。这会增加审查以及质量保证方面的复杂性。
除此之外,这种重复还有可能会给我们稍后对带有重复提交的分支与带有原始提交的分 支之间的合并带来麻烦。在最好的情况下,Git会自己识别出同样的修改出现了不止一次,并对其只采用一次。而在最坏的情况下,如果该重复提交被当作冲突来处理, Git 是无法检测到的,然后它会试图多次采用这一修改。结果就会产生一些令用户意外的冲突。
一旦我们将某次提交传递给了一个远程版本库,就不应该再用 rebase
命令来移动该提交了。否则,由于其他开发者可能会在其原作上继续他们的工作,这在将来再次合并修改的时候一定会带来问题。
7️⃣ 捡取
接下来,我们再来介绍另一种复制提交的方式: cherry-pick
命令。我们可以用它来指定自己需要的提交,Git 会为此创建一次新的提交,该提交中会拥有相同的修改集与当前分支中的元数据。
> git cherry-pick 23ec70f6b0
那么对于捡取操作,我们应该了解哪些事情呢?
cherry-pick
不会参考历史纪录。因而merge
和rebase
还可以被正确地识别成文件的 重命名与移动操作,cherry-pick
则不能。- 捡取操作有时候会被用来将一些小bug 的修复传递到各种不同的发行版中。
- 该操作的另一种应用是从即将删除的分支中转移出有用的提交。
- 警告:捡取操作也有可能会引发我们之前所说的重复提交问题。
🌾 总结
- 变基操作:Git 能将提交复制到提交图中的其他地方。尽管其中的修改与元数据(作者、日期)将保持不变,但该复制结果会有一个新的提交散列值。你可以通过
rebase
命令以多种方式对提交图进行重构。 - 只适用于推送之前:通常情况下,我们应该只对那些还未被传递给其他版本库的提交试用rebase 命令。否则,这样做可能给日后带来非常麻烦的合并冲突。
- 理顺历史:如果我们在并行式开发的过程中使用
merge
命令解决了其中的冲突,就会得到一部经历了多次分岔与合并的历史。如果用rebase
来代替merge
, 我们就会得到一部呈线性发展的历史。 - 变基过程中的冲突:Git 会逐段逐段重演被复制的提交。如果因为某些修改与工作区内容不相符而引发了冲突,变基的进程就会被中断。与执行 merge 命令的过程一样, 开发者可以先手动解决掉冲突,再继续变基的过程。
- rebase --onto :通过该选项,我们可以将某一分支移动到提交图中另一个完全不同的位置。