前言

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

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

一、Git cherry-pick

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

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

可以看到,在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值:

移植过程为:

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

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

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

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

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

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

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

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

  • 由于提交3rd是基于提交2nd创建的,因此3rd中保留了2rd中对文件的操作记录;

  • 如果直接将3rd拼接到initial commit后面,就会失去提交2nd的记录;

  • 由此提交3rd就不能通过提交2nd找到公共提交节点init,这就会导致合并失败;

所以,无论内容是否冲突,合并过程都会出现冲突:

解决方法:手动合并三步曲:

  • 首先,选择要保留的内容,解决冲突:

  • 然后,通过git add将修改信息纳入暂存区:

  • 最后,通过git commit提交修改信息:

完成后查看master分支的提交历史:

可以看到解决冲突,手动合并后,成功完成了整个cherry-pick过程。并且新增的提交是手动合并时进行的提交,而不是直接复制的提交3rd

2.第二步

此时两分支的状态为:

接下来就要删除dev分支上错误的两次提交2nd3rd,相当于版本回退;可以使用三种方法:revertresetcheckout,这里演示checkoutreset两种方法。

使用checkout

首先切换到dev分支,然后通过以下指令切换到提交initial commit

//dd703是提交initial_commit的SHA1值
git checkout dd703

此时该节点处于游离状态:

然后再删除dev分支:

由于之前修改的dev分支没有与master进行合并,所以删除时需要使用参数-D强制删除。

删除后,剩下master分支与游离提交。此时再通过以下指令将游离的节点设置为dev分支即可:

git checkout -b dev

由此通过"偷天换日"的方式使dev分支回到了错误提交前的状态;

使用reset

由于使用checkout只是移动了HEAD指针,没移动dev分支指针,所以会出现游离提交节点;而reset会同步移动HEADdev分支指针,不会造成这样的问题。所以这里使用reset进行版本回退会简单很多:

git reset --hard dd703

二、git rebase简介

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

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

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

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

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

三、merge rebase

1.采用merge合并分支

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

合并后产生一次新的提交c7,该提交有两个父节点c4c6。具体的合并方式为:如果没有冲突git就会自动采用Fast-forward方式进行合并,有冲突就解决冲突再进行手动合并。

2.采用rebase合并分支

由于是mywork分支需要变基合并到origin分支上,所以首先切换到mywork分支(注意这里与采用merge方法时所在的分支相反):

git checkout mywork

再进行合并:

git rebase origin

合并后的结果为:

注意:被合并的分支origin保持不动,而合并它的分支mywork将自己的提交作为补丁(patch)一个个应用(applying)到分支origin指向的提交后面;

在这个过程中git会自动创建c5'c6'。原来的c5c6就没用了,会被git gc回收。合并后分支mywork的提交记录变成了一条直线:

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

四、rebase原理分析

如图所示,masterdev分支都以提交节点A为基准点:

如果dev分支想要变换A这个基准点,那么:

第一步:切换到dev分支上;

第二步:执行git rebase master,过程如下;

上述命令中rebase参数后面指定的就是变更后的基准点:

  • 如果是分支,如master,基准点为该分支的最新提交节点,也就是C
  • 如果是一个commit_id,基准点为该commit_id对应的提交节点;

1.基准点为分支

沿用以上模型:

  • 首先,将dev分支上除了基准点A外的所有节点复制一份,即D'E',作为补丁备用,并将分支dev指向新基准点C

  • 然后,按原来dev上的节点顺序(D->E)将补丁应用(Patch Applying)到新基准点C后面,并同时改变分支dev指向:

追加补丁D'

每次向新基准点应用补丁时,都会出现三个选项

git rebase --continue

该选项表示:解决了合并冲突后,继续应用剩余补丁E'

git rebase --skip

该选项表示:跳过当前补丁,继续应用下一个补丁:

如果一直执行该选项,直到应用完分支dev上的补丁,结束rebase后,两分支的状态为:

git rebase --abort

该选项表示:终止rebase操作,回到执行rebase指令前的状态:

2.基准点为提交

过程详解

如图所示,若将提交节点B作为基准点,在当前test分支上执行:

git rebase 3ccc8

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

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

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

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

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

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

实战演示

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

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

git rebase -i 678e0

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

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

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

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

执行rebase操作前:

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

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

五、rebase注意事项

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

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

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

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

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

六、rebase应用场合

1.合并分支

由于git merge采用的是三方合并的原则,没有公共提交节点就无法进行合并,此时可以采用rebase进行合并。如下图所示:

本地master与远程master分支没有公共提交节点,无法采用git merge合并。可采用rebase进行合并:

//origin/master代表着远程master分支
git rebase origin/master

合并后本地master分支的状态为:

2.修改特定提交

以下情况就适合使用rebase来解决,当回退版本并进行修改时:

比如在master分支上进行了3次提交:

回退到第二次提交2nd,并对提交信息进行修改:

当我们回到原来的第三次提交3rd时,会发现之前的修改并没有被保存:

此时可以使用rebase,将提交1st作为新的提交节点(正如第四大点讲解的)。首先执行:

git rebase -i 5ab3f

通过添加参数-i进入交互模式,将提交2nd默认的pick参数修改为reword参数:

保存并退出后,进入修改提交信息界面:

保存并退出,由此完成修改:

七、rebase实战

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

它们有一个共同的父节点提交节点init,此时本地仓库的状态如下:

  • 由于要对test分支进行变基,从而合并到dev分支上,所以需要先切换到test分支上,这与merge操作是相反的;

  • 随后在test分支上执行如下命令对该分支进行变基:

git rebase dev

该指令翻译过来就是:我test 分支,现在要重新定义我的基准点,即使用 dev 分支指向的提交作为我新的基准点。过程如下:

  • 首先,将test分支上的提交(补丁)tes1应用到新基准点dev2尾部,出现了合并冲突:

    查看状态,发现test分支变基过程中的新基准点正是dev分支指向的提交361be,即提交节点dev2

如图所示,此时有三个选项:

  • 选项一:git rebase --abort:表示终止rebase操作,恢复到操作前;

  • 选项二:git rebase --skip:表示丢弃当前test分支的补丁,如果一直执行该选项,变基完成后,两分支的状态如下所示:

    即此时test分支与dev分支上具有相同的文件:

    并且test分支上的提交记录被改变为了dev分支上的提交记录:

    这就是一直执行选项git rebase --skip,丢弃全部test分支补丁的结果:

  • 选项三:git rebase --continue:解决冲突,手动合并后,继续变基;

    dev分支上新增两次提交dev3dev4

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

    此时两分支的状态为:

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

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

    删除4、7、9行:

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

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

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

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

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

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

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

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

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

细心的你可能已经发现了,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。期待与你再次相见!

Git应用详解第九讲:Git cherry-pick与Git rebase的更多相关文章

  1. Git应用详解第二讲:Git删除、修改、撤销操作

    前言 前情提要:Git应用详解第一讲:Git分区,配置与日志 在第一讲中我们对Git进行了简单的入门介绍,相信聪明的你已经了解Git的基本使用了. 这一讲我们来进一步深入学习Git应用,着重介绍Git ...

  2. Git应用详解第十讲:Git子库:submodule与subtree.md

    前言 前情提要:Git应用详解第九讲:Git cherry-pick与Git rebase 一个中大型项目往往会依赖几个模块,git提供了子库的概念.可以将这些子模块存放在不同的仓库中,通过submo ...

  3. Git应用详解第三讲:本地分支的重要操作

    前言 前情提要:Git应用详解第二讲:Git删除.修改.撤销操作 分支是git最核心的操作之一,了解分支的基本操作能够大大提高项目开发的效率.这一讲就来介绍一些分支的常见操作及其基本原理. 一.分支概 ...

  4. Git应用详解第六讲:Git协作与Git pull常见问题

    前言 前情提要:Git应用详解第五讲:远程仓库Github与Git图形化界面 git除了可以很好地管理个人项目外,最大的一个用处就是实现团队协作开发.况且,linus大神开发git的初衷就是为了维护L ...

  5. Git应用详解第七讲:Git refspec与远程分支的重要操作

    前言 前情提要:Git应用详解第六讲:Git协作与Git pull常见问题 这一节来介绍本地仓库与远程仓库的分支映射关系:git refspec.彻底弄清楚本地仓库到底是如何与远程仓库进行联系的. 一 ...

  6. Git应用详解第八讲:Git标签、别名与Git gc

    前言 前情提要:Git应用详解第七讲:Git refspec与远程分支的重要操作 这一节主要介绍Git标签.别名与Git的垃圾回收机制. 一.Git标签(tag) 1.标签的实质 标签与分支十分相似, ...

  7. Git应用详解第四讲:版本回退的三种方式与stash

    前言 前情提要:Git应用详解第三讲:本地分支的重要操作 git作为一款版本控制工具,其最核心的功能就是版本回退,没有之一.熟悉git版本回退的操作能够让你真真正正地放开手脚去开发,不用小心翼翼,怕一 ...

  8. Git命令详解

    一个中文git手册:http://progit.org/book/zh/ 原文:http://blog.csdn.net/sunboy_2050/article/details/7529841 前面两 ...

  9. git命令详解( 七 )

    此为git命令详解的第七篇 这章我们可以来逐渐揭开 git push.fetch 和 pull 的神秘面纱了.我们会逐个介绍这几个命令,它们在理念上是非常相似的.   git push的参数 git ...

随机推荐

  1. [C++]HelloWorld背后的故事!

    人物介绍 姓名 HelloWorld 性别 .cpp 住址 D:\ 身份证号(SHA1) 25106D2879A9EA300BB264F8155A71D7C44DA2E8 故事简介 编写源文件 预编译 ...

  2. HTML节点操作

    HTML节点操作 HTML节点的基本操作,添加节点,替换节点,删除节点,绑定事件,访问子节点,访问父节点,访问兄弟节点. 文档对象模型Document Object Model,简称DOM,是W3C组 ...

  3. Apache Druid 底层存储设计(列存储与全文检索)

    导读:首先你将通过这篇文章了解到 Apache Druid 底层的数据存储方式.其次将知道为什么 Apache Druid 兼具数据仓库,全文检索和时间序列的特点.最后将学习到一种优雅的底层数据文件结 ...

  4. 报错:Error instantiating class com.liwen.mybatis.bean.Employee with invalid types () or values ().

    实体类默认构造方法是无参构造方法,一旦重写构造方法,默认方法就会变成重写之后的构造方法,所以该错误报的错就是实体类缺少无参构造方法

  5. adb的基本安装和介绍(一)

    一,什么是adb? adb全称为Android Debug Bridge,就是起到调试桥的作用.顾名思义,adb就是android sdk 的一个工具 借助adb工具,我们可以管理设备或手机模拟器的状 ...

  6. VBScript 打开含有"空格"的路径 (Open Path with Space)

    记录,VBScript 如何打开,含有"空格"的路径.这个问题和常见,却总是忘! 直接上代码了,多说无益. Option Explicit Dim obj Dim path Set ...

  7. Springcloud config + zuul 搭建动态网关

    1,实现的效果,就是zuul 网关的配置路由实现负载均衡,zuul 的配置文件放在springcloud config 上 2,需要的服务如下: 3,其实就是配置下springcloud-zuul 的 ...

  8. Spring事物传播行为

    Spring事物传播行为 Spring中事务的定义: Propagation(key属性确定代理应该给哪个方法增加事务行为.这样的属性最重要的部份是传播行为.)有以下选项可供使用: PROPAGATI ...

  9. ES6中async与await的使用方法

    promise的使用方法 promise简介 是异步编程的一种解决方案.从语法上说,Promise 是一个对象,从它可以获取异步操作的消息.解决回调函数嵌套过多的情况 const promise =n ...

  10. cento升级openssl依旧显示老版本

    不久前拿到了一季度的服务器漏洞扫描报告,还是一些老生常谈的软件.按照报告上的漏洞一个个处理,开始升级openssl的时候一切都很顺利,上传源码包,解压,编译,安装,全部都没有报错.opessl --v ...