前言
这一节主要介绍git cherry-pick与git rebase的原理及使用。git
1、
Git cherry-pick
Git cherry-pick的做用为移植提交。好比在dev分支错误地进行了两次提交2nd和3rd,若是想要将这两次提交移植到master分支上。采用先删除再添加的方法将会很繁琐,而使用cherry-pick就能轻松实现这一需求。github
首先在版本库中建立了两个分支master和dev,并模拟上述场景:vim
image-20200418213440673
能够看到,在dev分支上进行了两次提交,在master分支上只进行了一次提交。如今想要将这两次提交「移植」到master分支上。总体分为两步:app
「第一步」:将dev分支上多余的两次提交移植到master分支上;
「第二步」:删除dev分支上多余的两次提交;
1.第一步
git cherry-pick commit_id
首先切换到master分支,而后使用以下命令将dev分支上的两次提交移植到master分支上:编辑器
//移植2nd提交 git cherry-pick 009dd //移植3rd提交 git cherry-pick aec8c
009dd和aec8c分别表示须要移植的提交2nd和3rd的SHA1值:ide
image-20200418215229274
移植过程为:翻译
image-20200418220353735
如上图所示,执行了两次cherry-pick指令,建立了两个内容与2nd、3rd一致的提交对象50477和f05a0。因此,cherry-pick指令移植提交的实质是:先将须要移植的提交复制一份,再拼接到master分支上,简称「先复制,再拼接」;3d
上面按照顺序先移植了提交2nd再移植提交3rd,不会发生冲突;指针
不按顺序移植,如先移植提交3rd会发生「合并冲突」,须要手动解决:code
image-20200418220823727
经过vi test.txt查看发生合并冲突的test.txt文件:
image-20200408123432173
能够发现master分支上initial commit提交中的文件test.txt「直观上」并不与提交3rd中的test.txt冲突,以下图所示:
image-20200408123754034
可是为何会发生合并冲突呢?缘由在于「三方合并原则」:
image-20200408143344853
如上图所示,当想要将dev中的提交E与master分支的提交B合并时,首先要找到B和E的公共父节点A,在A的基础上根据B和E进行三方合并;
了解了三方合并原则后就能解释上面发生合并冲突的缘由了:
因为提交3rd是基于提交2nd建立的,所以3rd中保留了2rd中对文件的操做记录;
若是直接将3rd拼接到initial commit后面,就会失去提交2nd的记录;
由此提交3rd就不能经过提交2nd找到公共提交节点init,这就会致使合并失败;
因此,不管内容是否冲突,合并过程都会出现冲突:
image-20200418222100291
「解决方法」:手动合并三步曲:
首先,选择要保留的内容,解决冲突:
image-20200408133308462
而后,经过git add将修改信息归入暂存区:
image-20200408133412891
最后,经过git commit提交修改信息:
image-20200418222349351
完成后查看master分支的提交历史:
image-20200418222512780
能够看到解决冲突,手动合并后,成功完成了整个cherry-pick过程。而且新增的提交是手动合并时进行的提交,而不是直接复制的提交3rd:
image-20200418222844236
2.第二步
此时两分支的状态为:
image-20200418223143850
接下来就要删除dev分支上错误的两次提交2nd和3rd,至关于版本回退;可使用三种方法:revert、reset和checkout,这里演示checkout和reset两种方法。
使用checkout
首先切换到dev分支,而后经过如下指令切换到提交initial commit:
//dd703是提交initial_commit的SHA1值 git checkout dd703
此时该节点处于游离状态:
image-20200418223451519
而后再删除dev分支:
image-20200418223548734
因为以前修改的dev分支没有与master进行合并,因此删除时须要使用参数-D强制删除。
删除后,剩下master分支与游离提交。此时再经过如下指令将游离的节点设置为dev分支便可:
git checkout -b dev
image-20200418223939367
由此经过"「偷天换日」"的方式使dev分支回到了错误提交前的状态;
使用reset
因为使用checkout只是移动了HEAD指针,没移动dev分支指针,因此会出现游离提交节点;而reset会同步移动HEAD和dev分支指针,不会形成这样的问题。因此这里使用reset进行版本回退会简单不少:
git reset --hard dd703
image-20200418224610750
2、
git rebase
简介
首先,rebase有两个意思:「变基」、「衍合」,即变换分支的参考基点。默认状况下,分支会以分支上的第一次提交做为基点,以下图所示master分支默认以提交1st做为基点:
image-20200409151236167
若是以提交4th做为master分支的基点,master分支就会变为:
image-20200409151428243
这个变化基点的过程就称之为变基(rebase);
rebase与merge十分类似,不过两者的工做方式有着显著的差别。好比:将A和B两分支进行合并:
在A分支上执行git merge B,表示的是将B分支「合并到」A分支上;
而在A分支上执行git rebase B,则表示将A分支经过变基「合并到」B分支上;
3、
merge
与
rebase
1.采用merge合并分支
image-20200408232708342
如今有两个分支origin和mywork,若是想要将origin分支「合并到」mywork分支上。根据三方合并原则,须要在c四、c6和它们的公共父提交节点c2的基础上进行合并:
image-20200408232523880
合并后产生一次新的提交c7,该提交有两个父节点c4和c6。具体的合并方式为:若是没有冲突git就会自动采用Fast-forward方式进行合并,有冲突就解决冲突再进行手动合并。
2.采用rebase合并分支
因为是mywork分支须要变基合并到origin分支上,因此首先切换到mywork分支(注意这里与采用merge方法时所在的分支相反):
git checkout mywork
再进行合并:
git rebase origin
合并后的结果为:
image-20200408232225944
「注意」:被合并的分支origin「保持不动」,而合并它的分支mywork将本身的提交做为补丁(patch)一个个应用(applying)到分支origin指向的提交后面;
在这个过程当中git会自动建立c5'和c6'。原来的c5和c6就没用了,会被git gc回收。合并后分支mywork的提交记录变成了一条直线:
image-20200408231936193
❝
也就是说:rebase会将被合并分支(mywork)上的提交应用到合并分支(origin)上,而且修改被合并分支(mywork)的提交记录。
❞
4、
rebase
原理分析
如图所示,master和dev分支都以提交节点A为基准点:
image-20200418232253571
若是dev分支想要变换A这个基准点,那么:
「第一步」:切换到dev分支上;
「第二步」:执行git rebase master,过程以下;
❝
上述命令中rebase参数后面指定的就是变动后的基准点:
若是是分支,如master,基准点为该分支的最新提交节点,也就是C;
若是是一个commit_id,基准点为该commit_id对应的提交节点;
❞
1.基准点为分支
沿用以上模型:
image-20200418232806243
首先,将dev分支上除了基准点A外的全部节点复制一份,即D'和E',做为补丁备用,并将分支dev指向新基准点C:
image-20200418232419176
而后,按原来dev上的节点顺序(D->E)将补丁应用(Patch Applying)到新基准点C后面,并同时改变分支dev指向:
「追加补丁D'」:
image-20200418232650653
每次向新基准点应用补丁时,都会出现「三个选项」:
image-20200418232951097
git rebase --continue
该选项表示:解决了合并冲突后,继续应用剩余补丁E':
image-20200418233223765
git rebase --skip
该选项表示:跳过当前补丁,继续应用下一个补丁:
image-20200418233400640
若是一直执行该选项,直到应用完分支dev上的补丁,结束rebase后,两分支的状态为:
image-20200418233514562
git rebase --abort
该选项表示:终止rebase操做,回到执行rebase指令前的状态:
image-20200418233837513
2.基准点为提交
过程详解
image-20200409184756113
如图所示,若将提交节点B做为基准点,在当前test分支上执行:
git rebase 3ccc8
会直接将原来的节点C和D应用到新基准点B后,至关于没有发生变化,这个变基的过程为:
首先,将基准点和test分支指向改变为节点B,并将test分支上基准点日后的提交节点做为补丁:
image-20200409195531185
而后,按顺序将补丁C和D应用到新基准点B后面:
image-20200409202803624
最后,test分支的状态为:
image-20200409202843582
因此,直接执行git rebase 678e0不会有任何变化:
image-20200409203900098
可是,咱们能够经过在rebase中添加参数-i,进入rebase交互模式,这样就能在rebase操做过程当中对特定的补丁进行一系列操做;
实战演示
首先在test分支上进行了四次提交:
image-20200409191637780
执行如下指令将test分支的基准点变为提交节点B(678e0),并进行变基:
git rebase -i 678e0
执行该指令后,会进入vim编辑器:
image-20200409192056322
能够根据须要将pick参数,改变为下面表明不一样做用的参数;这样就能够对节点C和D进行不一样的操做了。好比:
pick:默认参数,表示不对提交节点进行任何操做,直接应用原提交节点。不建立新提交;
reword:应用复制事后的原提交节点,可是能够编辑该节点的提交信息。经过这个参数,能够修改特定提交的提交信息。会建立新的提交;
edit:应用复制事后的原提交节点,会在设置了该参数的补丁上中止rebase操做。待修改完该补丁后,调用git rebase --continue继续进行rebase。会建立新的提交;
squash:将新基点后面的所有提交节点进行合并,也就是将这里的C和D两个节点进行合并。会建立新的提交;
还有其余参数这里就不一一介绍了。
此次直接使用默认的pick参数,经过:wq保存并退出vim编辑器,完成rebase操做:
image-20200409194956051
执行rebase操做前:
image-20200409191637780
能够看到当新基准点为特定提交时:
在rebase的过程当中使用默认参数pick,并不会像当新基准点为分支时那样建立新的提交;
而一旦使用其余参数(如reword)对补丁进行了修改,就会建立新的提交;
5、
rebase
注意事项
不要对master分支执行rebase,不然会引发不少的问题(master必定是远程共享的分支);
通常来讲,执行rebase的分支都是本身的本地分支,千万不要在与其余人共享的远程分支上使用rebase;
这不难理解,远程分支上的代码可能已经被其余人克隆到本地了,若是经过rebase修改了远程分支的提交历史,这样其余人每次拉取代码到本地时,就都须要进行复杂的合并。
因此,本地的非master分支合并时推荐使用git rebase,其余分支的合并推荐使用git merge;
「注意:git merge和git rebase的显著区别是,前者不会修改git的提交记录,然后者会!」
6、
rebase
应用场合
1.合并分支
因为git merge采用的是「三方合并」的原则,没有公共提交节点就没法进行合并,此时能够采用rebase进行合并。以下图所示:
image-20200411205020369
本地master与远程master分支没有公共提交节点,没法采用git merge合并。可采用rebase进行合并:
//origin/master表明着远程master分支 git rebase origin/master
合并后本地master分支的状态为:
image-20200411205034662
2.修改特定提交
如下状况就适合使用rebase来解决,当回退版本并进行修改时:
好比在master分支上进行了3次提交:
image-20200419174116301
回退到第二次提交2nd,并对提交信息进行修改:
image-20200419174313522
当咱们回到原来的第三次提交3rd时,会发现以前的修改并无被保存:
image-20200419174404816
此时可使用rebase,将提交1st做为新的提交节点(正如第四大点讲解的)。首先执行:
git rebase -i 5ab3f
经过添加参数-i进入交互模式,将提交2nd默认的pick参数修改成reword参数:
image-20200419174618553
保存并退出后,进入修改提交信息界面:
image-20200419174829838
保存并退出,由此完成修改:
image-20200419174935598
7、
rebase
实战
为了演示,额外建立两个分支dev和test,分别在两个分支上进行两次提交:
image-20200419150528809
它们有一个共同的父节点提交节点init,此时本地仓库的状态以下:
image-20200419154602095
因为要对test分支进行变基,从而「合并到」dev分支上,因此须要先切换到test分支上,这与merge操做是相反的;
随后在test分支上执行以下命令对该分支进行变基:
git rebase dev
该指令翻译过来就是:我test 分支,如今要从新定义个人基准点,即便用 dev 分支指向的提交做为我新的基准点。过程以下:
首先,将test分支上的提交(补丁)tes1应用到「新基准点」dev2尾部,出现了合并冲突:
image-20200419151735485
查看状态,发现test分支变基过程当中的新基准点正是dev分支指向的提交361be,即提交节点dev2:
image-20200419152146120
如图所示,此时有三个选项:
「选项一」:git rebase --abort:表示终止rebase操做,恢复到操做前;
「选项二」:git rebase --skip:表示丢弃当前test分支的补丁,若是一直执行该选项,变基完成后,两分支的状态以下所示:
image-20200419154352758
即此时test分支与dev分支上具备相同的文件:
image-20200419155017163
而且test分支上的提交记录被改变为了dev分支上的提交记录:
image-20200419153242071
这就是一直执行选项git rebase --skip,丢弃所有test分支补丁的结果:
「选项三」:git rebase --continue:解决冲突,手动合并后,继续变基;
在dev分支上新增两次提交dev3和dev4:
image-20200419153831132
切换回test分支一样新增两次提交tes3和tes4:
image-20200419154032932
此时两分支的状态为:
image-20200419184609655
随后在test分支上执行git rebase dev,在处理test分支上的第一个补丁tes3时出现冲突:
image-20200419155615321
打开冲突文件test.txt,手动解决冲突:
image-20200419155711866
删除四、七、9行:
image-20200419155812608
解决冲突后,执行git add将对文件`test.txt
的修改操做归入暂存区,标识已解决冲突:
❝
「注意」:这里并不须要进行一次提交,继续执行rebase操做便可;
❞
image-20200419160220448
随后再执行git rebase --continue,继续处理test分支的下一个补丁(变基):
image-20200419160305488
rebase结束后,查看test分支的提交记录:
image-20200419160412284
能够发现修改了test分支的提交历史,达到了预期的合并效果。
而且,此时test分支上的tes3与tes4两次提交的SHA1值与执行rebase前这两次提交的SHA1值是不同的:
image-20200419160741986
这也就验证了,git在rebase过程当中会自动建立提交节点的结论。此时dev分支与test分支的状态以下所示:
image-20200419161342071
若是在dev分支上执行git merge test,采用的应当是Fast-forward方式:
image-20200419161456806
使用gitk能够更加直观地表示这一状态:
image-20200419161529226
❝
细心的你可能已经发现了,rebase与cherry-pick十分相似。只不过cherry-pick不会修改分支提交记录,而rebase会。
❞
8、
merge
与
rebase
的选择
使用rebase时要遵循rebase的黄金法则:永远不要在公共分支上使用rebase。公共分支能够理解为master分支。因为rebase会重写分支提交记录,所以会给项目的回溯带来危险。如下为它与merge的区别:
merge是一个合并操做,使用git merge提交历史会出现分叉,显得不是那么简洁。可是,它的好处在于不会修改任何一次提交,会完整地将全部的提交都保存下来,方便回溯。「而且只能合并有公共提交节点的分支;」
rebase是没有合并操做的,它只是将当前分支所作的修改复制到了目标分支的最后一次提交上。因此能够不受「三方合并原则」约束,合并无公共提交节点的分支;
使用rebase会修改提交历史,获得的分支提交历史更加整洁。就好像写书,只会出版最终版本,以前的书稿并不会出版。可是,必定要注意「不能在共享的分支上使用rebase」。
两者都是很强大的分支整合命令,使用哪一个由具体情境决定。
9、
rebase
、
reset
、
revert
这三个指令的名字很像,容易混淆,下表对比了它们的用途以及区别:
10、
git
最佳实践
学到这里就能够彻底理解使用git将本地仓库文件推送到远程仓库的通常步骤了:
「第一步」:建立本地仓库:
git init
「第二步」:添加用户信息:
git config --global user.name '张三'
git config --global user.email 'zhangsan@git.com'
「第三步」:添加远程仓库地址:
git remote add origin https://www.github.com/example
「第四步」:修改文件;
「第五步」:将工做区中的文件归入暂存区:
git add .
「第六步」:将暂存区中的文件提交到版本库:
git commit -m '注释'
「第七步」:与远程仓库进行同步:
git pull --rebase origin master
「第八步」:创建本地分支与远程分支的联系,并进行推送:
git push -u origin master