文章目录
- 一、整合不同分支间的变更
- 1. 功能原理
- 2. 使用方法
- 3. 使用场景:
- 4. rebase VS merge
- 4.1 异同
- 4.2 优缺点
- 4.3 小结
- 5. 工作流实战
- 二、重建提交历史
- 1. 使用场景
- 2.使用方法
- 2.1 参数说明
- 3.工作流实战
- 三、git pull --rebase
- 四、总结
git rebase>git rebase,顾名思义即变基,不过这是一条多功能命令,既可以重建提交历史,还可以整合不同分支间的变更。本文将对相关功能进行介绍。
一、整合不同分支间的变更
该功能也即rebase的本意:变基。
1. 功能原理
当执行rebase操作时,git会从两个分支的共同祖先开始提取待变基分支上的修改,然后将待变基分支指向基分支的最新提交,最后将刚才提取的修改应用到基分支的最新提交的后面。
2. 使用方法
git">git rebase>git rebase [基节点]
git rebase>git rebase [基节点] [待变基节点]
这里可以看到rebase后面的参数可以是两个,也可以是一个,当rebase为一个参数的时候其实是省略了第二个参数,第二个参数为HEAD指针当前指向的那个节点。
3. 使用场景:
在一个项目中,你们的代码经过多次开发终于上线了。为了便于说明,我们先假设所有的代码提交都是在主分支 main 上进行的(主分支为什么不叫master?GitHub改版之后的默认分支名改成main了,这里与时俱进一下),并且已经有了C0,C1,C2三个节点的提交(这里的节点名C0用以代指相关commit操作完成后得到的那个HASH值,下同)。
此时你的leader想到了一个好的功能点,并专门指派你去将这个功能开发出来。与此同时,main 分支上仍有其它小伙伴进行更新提交。
于是,过了一段时间,你们的git分支大概被整成了这样:你在你的分支上一次提交了三次:C3,C4,C5。你们的主分支在此期间更新了C6,C7,C8三个节点
现在要进行功能合并:将你开发新功能的feature分支合并到主分支上。此时你可能会脱口而出:“merge一下不就好了吗?“
的确。如果你之前几乎没用过这个命令的话,这是最自然而然的一种做法。但是今天我们要聊的是rebase,接下来先看一下使用rebase是怎么解决的:
(1)首先切换到feature分支:
git">git checkout feature
(2)然后执行变基操作:
git">git rebase>git rebase main
#此处我们使用了带有一个参数的命令
#如果使用带有两个参数的命令则是:git rebase>git rebase main feature
操作后的结果是这样的:
原来的分支已经不存在,而是以main分支的最新节点为基,通过“嫁接”的方式迁移到main分支
根据前文所描述的操作原理:
- 首先,它会找出当前所在分支和变基分支的共同Parent,即C2;
- 然后,将当前分支中C2之后节点的快照暂存,即:C3,C4,C5;
- 最后,将刚刚暂存的快照应用到基分支main的最新提交的后面,便得到了上图。
需要注意的是这里的第三步一般会产生冲突,这时候就不是简单的迁移了,而是先将待迁移节点(这里为C3,C4,C5)依次逐个与最新节点进行冲突合并.
比如这里第一次冲突是C3与最新节点C8冲突,解决冲突之后得到节点C3`,而得到的这个节点此时也成为了最新节点。之后继续拿C4,C5节点重复此动作依次得到最新的节点。即便是这里不会发生冲突,迁移后的节点commit的哈希值也已经发生变化,所以相应的节点才叫C3`,而不是C3。
也正是出于这个原因,一般建议是在待合并分支上(此例中为feature)进行rebase操作,而不是在主分支上,最后切换到主分支merge过去即可
。原因是有时候可能会通过HASH值去回溯节点,如果在主分支进行了此操作,则HSAH值会发生变化,不利于回溯。
其实在使用者的视角里这里的整个过程说白了就像是把你feature分支上的节点依次重新更新到主分支上一样
如果采用merge呢?
git">git checkout main
git merge feature
操作结果如下:
可以看到:在主分支main后面生成了一个新的节点C9(生成新节点也同时意味着这里会有一条日志:Merge XXX from XXX)。
底层的操作原理为:先将主分支的最新节点C8的快照,feature分支最新节点C5的快照与两个分支共同Parent节点C2的快照执行三路合并生成新的快照,并基于此快照生成一个连接主分支与feature分支的节点C9,最后调整main的指向。
4. rebase VS merge
4.1 异同
首先二者都具备整合分支间变更的能力,但二者的实现手段却大不相同。git merge总是在推进提交历史,并不会影响提交的原始状态,而git rebase>git rebase整合变更的方式则是对提交历史进行重写,但从结果上看,最后git rebase>git rebase形成的节点C5`与git merge形成的节点C9完全相同。
4.2 优缺点
优点:
rebase不产生新节点,当然也不会产生新的commit日志,但是merge过程中会产生一条几乎“无用”的Merge日志。使用rebase操作的最大好处在于你可以让项目提交历史变得非常干净整洁。首先,它消除了git merge操作所需创建的没有必要的合并提交。其次,rebase会造就一个线性的项目提交历史——也就是说你可以从feature分支的顶部开始向下查找到分支的起始点,而不会碰到任何历史分叉。
rebase产生冲突并合并冲突发生在你操作git rebase>git rebase时,而合并冲突这个操作是你自己进行的;但是你提交合并申请的时候一般情况下会有评审,由评审者解决冲突,开发者人多的情况下工作量可想而知。
缺点:
rebase操作产生冲突需要依次逐个进行解决,重写提交历史可能引起混乱,对新手使用也不是很友好。
4.3 小结
在日常的小规模项目开发中,这种差异几乎可以忽略。但是在复杂的多人协作开发场景下,随着项目迭代的不断推进和工程复杂度的不断提高,rebase往往能助力生成相对清爽的提交历史,进而方便追溯工程的演进历史和缺陷排查。
不过为了获得这种便于理解的提交历史,却需要付出两种代价:安全性和可追溯性。如果不能遵循rebase的黄金法则,重写项目提交历史会为协作工作流程带来潜在的灾难性后果。再次,rebase操作丢失了合并提交能够提供的上下文信息——所以你就无法知道功能分支是什么时候应用了上游分支的变更。
如果使用rebase进行合并,就会呈现出比较清爽的提交日志,也不会有那么多线,而是一条直线
5. 工作流实战
请留意此过程中的git分支变化图
现在要开发一款游戏,在主分支上有三次提交:
现在让你切一个feature分支去开发交易功能,你开发这个功能经历了三次提交,与此同时主分支上也增加了两次提交:
现在我们在你的feature分支上执行rebase操作:
这里会出现冲突,请注意这里的冲突会出现三次,原因我们前面已经阐述过了:你在feature分支上提交了三次,而变基操作其实可以理解为在现有的基分支的最新节点后面进行更新,所以会将你提交的这三个节点依次与master主分支上的最新节点的内容进行比较,然后解决冲突即可。
我们依次解决这三个冲突:
这里通过图形界面可以看出:提交记录很清爽,feature分支通过嫁接的方式已经和master分支合二为一了。但是现在master的指针还没有进行移动,因此接下来我们需要将master的指针迁移到最新的节点:
到这里就完成了一次完整的git rebase>git rebase操作。
作为对比,如果你使用merge操作将会得到:
会多出一次合并冲突提交,分支看起来没有rebase清爽。
二、重建提交历史
1. 使用场景
gitbase 的另一个功能主要用来重建提交历史,什么意思呢?你在本地commit过程中有时候可能刚提交完,然后发现有个很小的地方需要修正一下,而当你修正完之后又得提交,但是这两次提交完成的任务可能一模一样,因为第二次修改的地方实在是微不足道,或者开发一个新功能的时候分多次提交,每次的变更都很小,这个时候就可以使用git rebase>git rebase的另一个功能:重建提交历史。
2.使用方法
git rebase>git rebase -i [地址引用]
交互式 rebase 指的是使用带参数 --interactive 的 rebase 命令, 简写为 -i。如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。在实际使用时,所谓的 UI 窗口一般会在文本编辑器 —— 如 Vim —— 中打开一个文件。
当 rebase UI界面打开时, 你能做3件事:
- 调整提交记录的顺序
- 删除你不想要的提交
- 合并提交。
简而言之,它允许你把多个提交记录合并成一个。
例如:
C2-C5的提交都是为了完成一个功能,现在我想合并这些提交:
git rebase -i HEAD~4
这样HEAD指针就会直线后退
4个节点到C1,然后列出这个节点之后你想操作的节点,C2,C3,C4,C5,之后你就可以编辑操作这些节点:变更提交顺序,删除部分提交历史等,最后得到结果:
但是你可能也感受到了,这个命令的参数似乎有些抽象。
2.1 参数说明
上文案例中后面的参数有些看不懂?下面简单介绍一下:
git rebase -i HEAD~4
HEAD
: 是个指针,当前这个指针指向main,main指向的节点是C5`;
~
:可以理解为在本分支上直线后退
一个节点。HEAD~就是在HEAD指向的节点处后退一个节点,HEAD~~就是在HEAD指向的节点处后退两个节点,也可以直接写成HEAD~2,二者是一样的。因此,在上例中,HEAD~4 的另一种写法是 HEAD~~~~。
^
: 操作符 ^ 与 ~ 符一样,后面也可以跟一个数字。但是该操作符后面的数字与 ~ 后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个 parent 提交。还记得前面提到过的一个合并提交有两个 parent 提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。
Git 默认选择合并提交的“第一个” parent 提交,在操作符 ^ 后跟一个数字可以改变这一默认行为。
举个例子:
这里有一个合并提交记录,C3节点有两个parent。如果不加数字修改符直接切换到 main^,会回到第一个 parent 提交记录。(在我们的图示中,第一个 parent 提交记录是指合并提交记录正上方的那个提交记录。)因此,执行:
git checkout main^
HEAD指针会指向C1节点:
这正符合我们的预期。但是当我们换成:
git checkout main^2
HEAD指针则会指向C2节点,也就是指向了第二个parent的节点:
使用 ^ 和 ~ 可以自由地在提交树中移动,非常给力。
我现在想让HEAD指向C3节点,我该如何操作?
git checkout HEAD~git checkout HEAD^2git checkout HEAD~2
甚至更简洁一点:
git checkout HEAD~^2~2
3.工作流实战
接着上面那个小案例,接下来我们开发一个订单系统并提交三次:
然后合并改成一次提交:
git rebase HEAD~3
接下来你就拥有了修改HEAD指针之后节点的能力:
这里我们按照指示将三次提交合并成一次:
结果为:
相对应的代码内容已经合并:
以上便完成了一次git rebase>git rebase 合并修改多次commit操作。
git_pull_rebase_311">三、git pull --rebase
这个功能的使用场景为:多个开发者操作一个分支。
比如开发者A在master分支节点C0将远程代码clone到本地并在本地进行开发,同时B开发者也进行了这种操作,但是B提交的快,B提交完之后master分支产生了新节点C1,A开发者提交的时候由于B在这时候已经做了提交,因此会产生冲突。
如果这时候使用git pull去直接拉取并合并冲突会产生一次无意义的合并提交日志节点,而这时候使用git pull --rebase操作,则会将你本地master分支C0节点之后的内容“嫁接”到远程C1节点之后,期间可能会有冲突合并,但是不回产生新的无意义的合并节点及日志。
四、总结
回过头来想一下这个命令的这几种用途你就会发现,这些功能说来说去其实归根结底就一个功能:变基。
- 合并多个分支:将你的分支”嫁接“到其它分支上,是变基;
- 合并修改某一节点之后的commit节点:可以理解成将本分支上的某些节点进行变基;
- git pull --rebase 更是一种变基:将本地开发的某一节点之后的内容“嫁接”到最新的节点上。
这个名字起的还是相当贴切的。
以上便是本文的内容,如果觉得写的还不错还请一键三连,如果觉得有哪些地方写的有误欢迎评论区或者私信指出,感谢!