zoukankan      html  css  js  c++  java
  • Git应用详解第九讲:Git cherry-pick与Git rebase

    前言

    前情提要:Git应用详解第八讲:Git标签、别名与Git gc

    这一节主要介绍git cherry-pickgit rebase的原理及使用。

    一、Git cherry-pick

    Git cherry-pick的作用为移植提交。比如在dev分支错误地进行了两次提交2nd3rd,如果想要将这两次提交移植到master分支上。采用先删除再添加的方法将会很繁琐,而使用cherry-pick就能轻松实现这一需求。

    首先在版本库中创建了两个分支masterdev,并模拟上述场景:

    image-20200418213440673

    可以看到,在dev分支上进行了两次提交,在master分支上只进行了一次提交。现在想要将这两次提交移植master分支上。整体分为两步:

    • 第一步:dev分支上多余的两次提交移植到master分支上;
    • 第二步:删除dev分支上多余的两次提交;

    1.第一步

    git cherry-pick commit_id

    首先切换到master分支,然后使用如下命令将dev分支上的两次提交移植到master分支上:

    //移植2nd提交
    git cherry-pick 009dd
    //移植3rd提交
    git cherry-pick aec8c
    

    009ddaec8c分别表示需要移植的提交2nd3rdSHA1值:

    image-20200418215229274

    移植过程为:

    image-20200418220353735

    • 如上图所示,执行了两次cherry-pick指令,创建了两个内容与2nd、3rd一致的提交对象50477f05a0。所以,cherry-pick指令移植提交的实质是:先将需要移植的提交复制一份,再拼接到master分支上,简称先复制,再拼接

    • 上面按照顺序先移植了提交2nd再移植提交3rd,不会发生冲突;

    • 不按顺序移植,如先移植提交3rd会发生合并冲突,需要手动解决:

    image-20200418220823727

    通过vi test.txt查看发生合并冲突的test.txt文件:

    image-20200408123432173

    可以发现master分支上initial commit提交中的文件test.txt直观上并不与提交3rd中的test.txt冲突,如下图所示:

    image-20200408123754034

    但是为什么会发生合并冲突呢?原因在于三方合并原则

    image-20200408143344853

    如上图所示,当想要将dev中的提交Emaster分支的提交B合并时,首先要找到BE的公共父节点A,在A的基础上根据BE进行三方合并;

    了解了三方合并原则后就能解释上面发生合并冲突的原因了:

    • 由于提交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分支上错误的两次提交2nd3rd,相当于版本回退;可以使用三种方法:revertresetcheckout,这里演示checkoutreset两种方法。

    使用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会同步移动HEADdev分支指针,不会造成这样的问题。所以这里使用reset进行版本回退会简单很多:

    git reset --hard dd703
    

    image-20200418224610750

    二、git rebase简介

    首先,rebase有两个意思:变基衍合,即变换分支的参考基点。默认情况下,分支会以分支上的第一次提交作为基点,如下图所示master分支默认以提交1st作为基点:

    image-20200409151236167

    如果以提交4th作为master分支的基点,master分支就会变为:

    image-20200409151428243

    这个变化基点的过程就称之为变基(rebase);

    rebasemerge十分相似,不过二者的工作方式有着显著的差异。比如:将AB两分支进行合并:

    • A分支上执行git merge B ,表示的是将B分支合并到A分支上;
    • 而在A分支上执行git rebase B,则表示将A分支通过变基合并到B分支上;

    三、merge rebase

    1.采用merge合并分支

    image-20200408232708342

    现在有两个分支originmywork,如果想要将origin分支合并到mywork分支上。根据三方合并原则,需要在c4c6和它们的公共父提交节点c2的基础上进行合并:

    image-20200408232523880

    合并后产生一次新的提交c7,该提交有两个父节点c4c6。具体的合并方式为:如果没有冲突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'。原来的c5c6就没用了,会被git gc回收。合并后分支mywork的提交记录变成了一条直线:

    image-20200408231936193

    也就是说:rebase会将被合并分支(mywork)上的提交应用到合并分支(origin)上,并且修改被合并分支(mywork)的提交记录。

    四、rebase原理分析

    如图所示,masterdev分支都以提交节点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
    

    会直接将原来的节点CD应用到新基准点B后,相当于没有发生变化,这个变基的过程为:

    • 首先,将基准点和test分支指向改变为节点B,并将test分支上基准点往后的提交节点作为补丁:

    image-20200409195531185

    • 然后,按顺序将补丁CD应用到新基准点B后面:

    image-20200409202803624

    • 最后,test分支的状态为:

    image-20200409202843582

    所以,直接执行git rebase 678e0不会有任何变化:

    image-20200409203900098

    但是,我们可以通过在rebase中添加参数-i,进入rebase交互模式,这样就能在rebase操作过程中对特定的补丁进行一系列操作;

    实战演示

    首先在test分支上进行了四次提交:

    image-20200409191637780

    执行以下指令将test分支的基准点变为提交节点B678e0),并进行变基:

    git rebase -i 678e0
    

    执行该指令后,会进入vim编辑器:

    image-20200409192056322

    可以根据需要将pick参数,改变为下面代表不同作用的参数;这样就可以对节点CD进行不同的操作了。比如:

    • pick:默认参数,表示不对提交节点进行任何操作,直接应用原提交节点。不创建新提交;
    • reword:应用复制过后的原提交节点,但是可以编辑该节点的提交信息。通过这个参数,可以修改特定提交的提交信息。会创建新的提交;
    • edit:应用复制过后的原提交节点,会在设置了该参数的补丁上停止rebase操作。待修改完该补丁后,调用git rebase --continue继续进行rebase。会创建新的提交;
    • squash:将新基点后面的全部提交节点进行合并,也就是将这里的CD两个节点进行合并。会创建新的提交;
    • 还有其他参数这里就不一一介绍了。

    这次直接使用默认的pick参数,通过:wq保存并退出vim编辑器,完成rebase操作:

    image-20200409194956051

    执行rebase操作前:

    image-20200409191637780

    可以看到当新基准点为特定提交时:

    • rebase的过程中使用默认参数pick,并不会像当新基准点为分支时那样创建新的提交;
    • 而一旦使用其他参数(如reword)对补丁进行了修改,就会创建新的提交;

    五、rebase注意事项

    • 不要对master分支执行rebase,否则会引起很多的问题(master一定是远程共享的分支);

    • 一般来说,执行rebase的分支都是自己的本地分支,千万不要在与其他人共享的远程分支上使用rebase

      这不难理解,远程分支上的代码可能已经被其他人克隆到本地了,如果通过rebase修改了远程分支的提交历史,这样其他人每次拉取代码到本地时,就都需要进行复杂的合并。

    • 所以,本地的非master分支合并时推荐使用git rebase,其他分支的合并推荐使用git merge

    注意:git mergegit rebase的显著区别是,前者不会修改git的提交记录,而后者会!

    六、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

    七、rebase实战

    为了演示,额外创建两个分支devtest,分别在两个分支上进行两次提交:

    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分支上新增两次提交dev3dev4

      image-20200419153831132

      切换回test分支同样新增两次提交tes3tes4

      image-20200419154032932

      此时两分支的状态为:

      image-20200419184609655

      随后在test分支上执行git rebase dev,在处理test分支上的第一个补丁tes3时出现冲突:

      image-20200419155615321

      打开冲突文件test.txt,手动解决冲突:

      image-20200419155711866

      删除4、7、9行:

      image-20200419155812608

      解决冲突后,执行git add将对文件``test.txt`的修改操作纳入暂存区,标识已解决冲突:

      注意:这里并不需要进行一次提交,继续执行rebase操作即可;

      image-20200419160220448

      随后再执行git rebase --continue,继续处理test分支的下一个补丁(变基):

      image-20200419160305488

      rebase结束后,查看test分支的提交记录:

      image-20200419160412284

      可以发现修改了test分支的提交历史,达到了预期的合并效果。

      并且,此时test分支上的tes3tes4两次提交的SHA1值与执行rebase前这两次提交的SHA1值是不一样的:

      image-20200419160741986

      这也就验证了,gitrebase过程中会自动创建提交节点的结论。此时dev分支与test分支的状态如下所示:

      image-20200419161342071

      如果在dev分支上执行git merge test ,采用的应当是Fast-forward方式:

      image-20200419161456806

      使用gitk可以更加直观地表示这一状态:

      image-20200419161529226

    细心的你可能已经发现了,rebasecherry-pick十分类似。只不过cherry-pick不会修改分支提交记录,而rebase会。

    八、mergerebase的选择

    使用rebase时要遵循rebase的黄金法则:永远不要在公共分支上使用rebase。公共分支可以理解为master分支。由于rebase会重写分支提交记录,因此会给项目的回溯带来危险。以下为它与merge的区别:

    • merge是一个合并操作,使用git merge提交历史会出现分叉,显得不是那么简洁。但是,它的好处在于不会修改任何一次提交,会完整地将所有的提交都保存下来,方便回溯。并且只能合并有公共提交节点的分支;

    • rebase是没有合并操作的,它只是将当前分支所做的修改复制到了目标分支的最后一次提交上。所以可以不受三方合并原则约束,合并没有公共提交节点的分支;

      使用rebase会修改提交历史,得到的分支提交历史更加整洁。就好像写书,只会出版最终版本,之前的书稿并不会出版。但是,一定要注意不能在共享的分支上使用rebase

    二者都是很强大的分支整合命令,使用哪个由具体情境决定。

    九、rebaseresetrevert

    这三个指令的名字很像,容易混淆,下表对比了它们的用途以及区别:

    指令 改变提 交历史 用途
    Reset 把目前分支的状态设定成某个指定的Commit状态,通常适用于尚未推送的Commit
    Rebase 不管是新增、修改、删除Commit都相当方便。可用来整理、编辑还未推送的Commit,通常也只适用于尚未推送的Commit
    Revert 新增一个Commit来反转(取消)另一个Commit内容,原本的Commit依旧会保留在提交历史中。虽然会因此而增加Commit数,但通常比较适用于已经推送的Commit,或者不允许使用ResetRebase指令修改提交历史的场合

    十、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
      

    通过这一节的学习,相信你已经熟练掌握了cherry-pickrebase的原理及使用方法了。下一节将会介绍Git子库:submodulesubtree。期待与你再次相见!

  • 相关阅读:
    基础
    基础
    基础
    基础
    基础
    基础
    基础
    基础
    Gym102361A Angle Beats(直角三角形 计算几何)题解
    Petrozavodsk Summer Training Camp 2016H(多标记线段树)题解
  • 原文地址:https://www.cnblogs.com/AhuntSun-blog/p/12732946.html
Copyright © 2011-2022 走看看