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


    前言

    前情提要:Git应用详解第三讲:本地分支的重要操作

    git作为一款版本控制工具,其最核心的功能就是版本回退,没有之一。熟悉git版本回退的操作能够让你真真正正地放开手脚去开发,不用小心翼翼,怕一不小心删除了不该删除的文件。本节除了介绍版本回退的内容之外,还会介绍stash的使用。

    一、版本回退

    git中永远有后悔药可吃,总是可以回到版本库的某一个时刻,这就叫做版本回退

    image-20200406144058526

    如上图所示:当前master分支指针指向D,通过版本回退可以使master指向CBA。进行版本回退的命令大体上有三种:resetrevertcheckout。下面就来一一讲解:

    Ⅰ.git reset

    1.参数

    reset命令可以添加很多参数,常用的有--mixed--soft--hard三种。下图为一次完整提交的四个阶段:

    image-20200412192613526

    三个参数大体上的区别为:

    • --mixed:为默认值,等同于git reset。作用为:将文件回退到工作区,此时会保留工作区中的文件,但会丢弃暂存区中的文件;
    • --soft:作用为:将文件回退到暂存区,此时会保留工作区暂存区中的文件;
    • --hard:作用为:将文件回退到修改前,此时会丢弃工作区暂存区中的文件;

    下面就来详细地讲解它们的使用方法:

    首先在master分支进行四次提交,每次提交在test.txt中添加一行文本信息:

    image-20200406164503683

    --mixed

    该参数为默认值,作用为:将文件回退到工作区中:如下图所示,将test.txt文件回退一次提交:

    image-20200412194811197

    可以看到第四次提交对test.txt的修改操作被回退到了工作区当中,并且保留了工作区中第四次提交对test.txt所做的修改,所以工作区中的test.txt文件内容与回退前一致。

    --soft

    该参数的作用为:将文件回退到暂存区中:如下图所示,将test.txt文件回退一次提交:

    image-20200412195321082

    可以看到第四次提交对test.txt的修改操作被回退到了暂存区当中,并且保留了工作区和暂存区中第四次提交对test.txt所做的修改,所以,工作区中的文件内容与回退前一致。

    --hard

    该参数的作用为:将文件回退到修改前:如下图所示,将test.txt文件回退一次提交:

    image-20200412205112201

    可以看到test.txt直接回到了进行第四次提交前,此时删除了工作区和暂存区中第四次提交对test.txt所做的修改。所以,工作区变得干净了,test.txt文件内容回退到刚完成第三次提交时。

    2.写法

    为了方便演示reset的各种使用方法,下面的指令都采用--hard参数。

    git reset --hard HEAD^

    该命令的作用为回退一次提交:

    image-20200406164628192

    回退后的状态为:

    image-20200406164713774

    可以看到,该方法会同时改变了HEADmaster指针的指向;

    git reset --hard HEAD^^

    该命令的作用为回退两次提交:

    image-20200406170323254

    回退后的状态为:

    image-20200406170352024

    同样,使用--hard参数回退,工作区是干净的;可以看到,该方法也会同时改变HEADmaster指针的指向;

    git reset --hard HEAD~n

    该命令的作用为回退n次提交:

    image-20200406203027868

    可以看到使用了--hard参数,回退结果符合预期,并且该方法也会同步修改HEAD和分支master指针的指向。

    注意:该方式只能向前回退,不能向后回退

    上述命令中的HEAD可以更换为分支名,比如master

    git reset --hard master~n
    

    该命令表示将master分支回退n次提交。由于HEAD始终指向当前分支,所以使用分支名和使用HEAD效果是一样的。

    git reset --hard commit_id

    该指令的作用为回退到指定的commit id的提交版本;由于commit id是不会重复的,一般只需要写前几(6)位就可以识别出来。通过commit id的回退方式既可以向前回退,也可以向后回退。如下所示,从1st commit往后回退到4th commit,其中4th commitcommit id = bdb373...

    为了熟悉该指令,我们分两种方式进行回退:使用--hard参数与使用默认参数。

    • 使用--hard参数

      image-20200406193422130

      从图中可以看出:通过第四次提交的commit_id: bdb373顺利地从第一次提交向后回退到了第四次提交,并且工作区干净。该方法也同时修改了HEAD和分支master的指向,具体过程为:

      image-20200414171228274

    • 使用默认参数

      image-20200406193005200

      可以看到切换回了4th commit,但是工作区的test.txt文件并没有变化;这是因为,在4th -> 1st的过程中,需要在工作区中删除test.txt文件中的2nd line、3rd line、4th line。通过默认参数--mixed,将4th commit对文件的修改回退到了工作区当中,如下图所示:

      image-20200406202451310

      这个过程丢弃了暂存区中对文件的删除操作,但是保留了工作区中对文件的删除操作。所以,工作区中的test.txt文件仍然处于删除了三行内容的状态。

      此时只需要将修改操作从阶段1移动到修改前的阶段0,即可将文件恢复到修改前的状态,并清空工作区。可以采用git restore test.txt实现:

      image-20200406202716247

    Ⅱ.git revert

    revert是回滚,重做的意思。不同于reset直接通过改变分支指向来进行版本回退,并且不产生新的提交;revert是通过额外创建一次提交,来取消分支上指定的某次提交的方式,来实现版本回退的。如下图所示,假如想要重做提交B,重做前与重做后的状态为:

    image-20200413234440432

    所谓重做提交B,指的是在新建的提交B'中取消提交B中所做的一切操作。也就是说revert的思想为:通过创建一个新提交来取消不要的提交。所以,提交数会增加。

    1.参数

    git同样为revert提供了许多参数,常用的有以下三种。为了演示它们的作用,首先需要设置对应的测试环境:在dev分支上进行四次提交,每次提交都为test.txt添加一行内容:

    image-20200414000404304

    -e

    -e参数是--edit的缩写,为revert指令的默认参数,即git revert -e等同于git revert。该参数的作用为在重做过程中,新建一次提交的同时编辑提交信息。比如通过以下命令重做上述的dev2提交:

    git revert f4a95
    

    执行该指令后会创建一次新的提交来取消提交dev2所做的一切操作,并且会进入vim编辑器,编辑新提交的提交注释:

    image-20200414115052089

    如下图所示,提交dev2为文件test.txt添加的dev2文本被取消了,并且dev分支上多了一次提交:

    image-20200414114945783

    --no-edit

    该参数的作用为不编辑由于revert重做,所新增提交的注释信息。如下图所示,通过:

    git revert --no-edit f4a95b
    

    重做提交dev2的过程中,并不会进入vim编辑器编辑新增提交的注释信息,而是采用默认的注释信息:Revert "dev2"

    image-20200414114748865

    -n

    -n参数是--no-commit的简写形式,作用为对revert重做某次提交时所产生的修改,不进行提交,也就是不会新增一次提交;

    如下图所示,这是revert指令通过新建提交B'来取消提交B的过程,分为0~4个阶段。不添加-n参数时,revert指令会产生一次额外提交B',此时处于下图中的第3阶段。而使用-n参数时,虽然revert指令也会通过新建提交B'来重做提交B。但是,此时还处于生成提交B'的过程,还没有完全生成提交B',也就是处于下图中的第2阶段。

    image-20200414002942670

    这种做法的好处是,允许我们干涉revert重做过程,手动进行提交。如下图所示,通过:

    git revert -n f4a95
    

    重做提交dev2的过程中,手动暂停了重做过程。虽然提交dev2test.txt所做的修改已被撤销,但是这一重做操作还未进行提交:

    image-20200414120436217

    这样我们既可以修改重做过程中不满意的地方,也可以随意添加注释。修改完后,通过手动提交的方式,完成重做(REVERTING)操作:

    image-20200414121147251

    2.写法

    revert指令也有多种写法,下面介绍主要的几种。为了方便演示,下列指令都采用默认参数-e手动编辑每次新增提交的注释信息。

    git revert commit_id

    这是最常用的写法,通过commit_id精准地选择想要重做的提交。分两种情况:

    • 情况一:重做最新一次提交,不会发生冲突。

      例如:通过以下指令,重做dev分支上最新的一次提交dev2

      git revert f4a95b
      

      首先进入vim编辑器编辑新增提交的注释信息:

      image-20200414135326937

      随后完成重做操作,如下图所示;可见提交dev2test.txt添加的dev2内容被删除了,并且多出一次提交,说明重做成功:

      image-20200414140040443

    • 情况二:重做非最新一次提交,会发生冲突。

      例如:通过以下指令,重做dev分支上的第三次提交dev1

      git revert dbde45
      

      会出现合并冲突:

      image-20200414140502098

      使用git mergetool指令,通过vim编辑器的工具vimdiff显示冲突文件test.txt

      image-20200414140645448

      回车进入vim编辑器界面,解决冲突:

      image-20200414141354304

      解决冲突之后,手动进行一次提交,完成revert过程:

      image-20200414142323103

    • 为什么会出现冲突?

      通过上面的例子不难看出,revert操作生成的新提交其实是通过两次提交合并而成的。如下图所示:

      image-20200414143430837

      • 首先,将被重做的提交dev1的前一次提交2nd复制一份,即图中的2nd'
      • 然后,将它与当前分支的最新提交dev2进行合并,由此生成revert操作新增的提交;

      知道了revert操作新增的提交的由来后,就不难解释为什么会出现合并冲突了,如下图所示:

      image-20200414144109389

      合并的两次提交中,文件test.txt的内容不一样。git不知道以哪个版本为准,自然会导致自动合并失败,需要手动合并。

    git revert HEAD

    该指令的作用为重做所在分支的最新一次提交,并且不会发生冲突:

    image-20200414150640086

    git revert HEAD^

    该指令的作用为重做所在分支的倒数第二次提交,会发生冲突,需要手动合并,完成重做操作:

    image-20200414151002143

    git revert HEAD^^

    该指令的作用为重做所在分支的倒数第三次提交,会发生冲突,需要手动合并,完成重做操作:

    image-20200414180953703

    git revert HEAD~n

    该指令的作用为重做所在分支的倒数第n+1次提交,会发生冲突,需要手动合并,完成重做操作。过程与上述一致,这里就不再赘述了。

    总结:常用git revert commit_id这种方式。

    3.撤销revert操作

    思路很简单,再次通过revert操作取消上一次的revert操作(即所谓"负负得正")。

    操作前,dev分支上的提交记录和test.txt文件内容如下:

    image-20200414153206034

    通过:git revert --no-edit f4a95重做提交dev2--no-edit表示不修改新增提交的注释):

    image-20200414153456451

    重做后,多了一次提交,并且test.txt文件中删除了dev2这一行内容。此时,可以通过:

    git revert --no-edit 582d127
    

    重做上一次重做操作,以此达到取消上一次重做操作的目的:

    image-20200414153724455

    如上图所示,虽然多出了一次提交,但是test.txt文件中被删除的dev2内容被恢复了,这样就撤销了revert操作。

    Ⅲ.git checkout

    1.git checkout commit_id

    使用checkout可以进行版本回退,如直接使用:

    git checkout cb214 
    

    回退到提交3rd,此时会出现如下提示:

    image-20200311111540863

    注意到,切换后HEAD指向的不再是master分支,而是cb214...即第三次提交,查看历史提交记录:

    image-20200311111719389

    可看到只有3次提交,什么意思呢?如下图所示:

    image-20200412001646768

    image-20200311112656834

    通过git checkoutHEAD指针指向了第3次提交,可以将它想象为一个新的分支。但是却没有实际创建分支,即此时head指向的由提交1~3组成的commit对象链条处于游离状态;

    接着,在HEAD还指向游离的提交节点3的基础上对文件做出新的修改:

    image-20200311113237150

    • 此时如果我们切换回master分支,会出现下列错误

    image-20200311113209483

    提示显示:如果没有保存就从游离的提交上切换到master分支,这一修改就会被checkout命令覆盖。我们可以在切换前进行一次提交操作:

    image-20200311113625297

    此时的状态为:

    image-20200412002213790

    • 在游离的Commit对象链中进行了一次提交之后,再次通过:git checkout master切换到master分支:

    image-20200311114055018

    提示大意为:如果没有任何分支指向刚才在游离的Commit对象链中进行的提交,那么该提交就会被忽略。此时的状态如下图所示:

    image-20200412002655921

    如果想要创建一个分支保存(指向)这条游离的Commit对象链,现在就是很好的时机。根据上述提示的命令:

    git branch mycommit  c4d5cc3
    

    创建指向commit_idc4d5cc3的提交(即上述的提交节点5)的分支mycommit

    image-20200311115117279

    由此游离的commit对象链得以被新分支所指向,并得到了保存,此时的状态如下图所示:

    image-20200412004042471

    总结:

    • 通过checkout进行版本回退会造成游离的提交对象链,需要额外创建一个分支进行保存;

    • 因此,使用checkout进行版本回退的思路为,先切换到想要回退的提交版本,再删除进行版本回退的分支dev。最后,创建一个新的dev分支指向游离的提交对象链,完成分支dev的版本回退,简称"偷天换日";

    • 只要有分支指向,提交就不会被丢弃。

    Ⅳ.revertreset的选择

    由于checkout会造成游离的提交对象链,所以,一般不使用checkout而是使用resetrevert进行版本回退:

    • revert通过创建一个新提交的方式来撤销某次操作,该操作之前和之后的提交记录都会被保留,并且会将该撤销操作作为最新的提交;

    • reset是通过改变HEAD和分支指针指向的方式,进行版本回退,该操作之后的提交记录不会被保留,并且不会创建新的提交;

    在个人开发上,建议使用reset;但是在团队开发中建议使用revert,特别是公共的分支(比如master),这样能够完整保留提交历史,方便回溯。

    Ⅴ.回退方法汇总

    版本回退主要有三大方式resetrevertcheckout,各方式的比较如下:

    方法 效果 向前回退 向后回退 同步修改HEAD与分支指向
    git reset --hard HEAD^ 往前回退1次提交
    git reset --hard HEAD^^ 往前回退2次提交
    git reset --hard HEAD~n 往前回退n次提交
    git reset --hard <commit_id> 回退到指定commit id的提交
    git revert HEAD 重做最新一次提交
    git revert HEAD^ 重做倒数第二次提交
    git revert HEAD^^ 重做倒数第三次提交
    git revert HEAD~n 重做倒数第n+1次提交
    git revert commit_id 重做指定commit_id的提交
    git checkout commit_id 回退到指定commit id的提交

    从上表可知,只有下列三种方式可以自由地向前向后回退:

    git reset --hard commit_id
    git revert commit_id
    git checkout commit_id
    

    但是,使用checkout进行回退会出现游离的提交,需要创建一个新分支进行保存,所以不常用。

    二、git stash

    1.git stash的作用

    git stash指令的作用为:对没有提交到版本库的,位于工作区或暂存区中游离的修改进行保存,在需要时可进行恢复。具体应用场景如下:

    master分支进行两次提交:1st2nd,随后创建并切换到dev分支。在dev分支上进行一次提交(dev1),此时两分支的状态为:

    image-20200412235844426

    随后在dev分支上给文件test.txt添加一行dev2,但是不提交到暂存区,直接切换到master分支,会出现如下错误:

    image-20200413001632846

    图中显示的错误大意为:在dev分支上的修改会被checkout操作覆盖。下面我们来看看,将dev分支上的这一修改操作添加到暂存区后,再切换分支,是否还会出现同样的问题:

    image-20200413001752227

    可见还是会出现该错误,这初步验证了位于工作区和暂存区中的修改都会被checkout操作覆盖的结论。原因如下图所示:

    image-20200413001917190

    虽然在dev分支上修改了文件,但是没有将这一修改操作进行提交。这样就不会产生提交节点,就如上图所示,修改dev2是游离的,在切换分支的时候会被丢弃。

    这种情况在日常开发中很常见,当在develop分支上开发新功能的时候,master分支出现紧急情况需要切换回去进行修复。但是,当前分支的新功能还没开发完全,贸然切换分支,原来开发的内容就会因被覆盖而丢失,怎么办呢?

    有人可能会说进行一次commit不就可以了吗?确实可以。但是,这样不符合提交的代码就是正确代码的原则。更好的解决方法为使用git stash,如下图所示:

    image-20200413002115302

    可见git stash可以将当前dev分支上,位于在工作区或暂存区中的修改,在未提交的情况下进行了保存;并且将分支回退到修改前的状态,保存过后,就可以很顺畅地切换回master分支了。

    图中的WIPworking in progress)表示的是正在进行的工作;

    当我们在master分支上完成了工作,再次切换回dev分支时,查看test.txt文件:

    image-20200413002256321

    发现切换分支前所做的修改dev2消失了,这是为什么呢?

    • 其实,上面通过git stashdev分支上工作区或暂存区中的修改,提交到了stash区域进行保存,并将dev分支回退到修改前的状态。如下图所示:

      image-20200413003349365

    • 切换到master分支时test分支上的修改依旧会被覆盖。所以,再次回到dev分支时需要从stash区域中恢复切换分支前保存的修改;

    怎样恢复通过git stash保存到stash中的修改呢?可以通过:

    git stash list
    

    查看该分支上被stash保存的修改:

    image-20200413224408623

    继续给test.txt文件添加内容:dev3,并通过以下指令保存修改的同时添加注释:

    git stash save '注释'
    

    image-20200413225024618

    • 首先,通过上述命令可以修改stash中存储修改的备注信息;
    • 其次,虽然在test分支上进行了两次修改,但是使用git stash保存修改后,文件test.txt并没有实际被修改;

    2.恢复stash存储的修改

    方法有很多,主要有以下三种:

    git stash pop

    image-20200413225140030

    如图所示,通过上述命令将stash中存储的最新一次修改恢复了。相信你已经发现了,stash非常类似:先保存的修改,排在最后,序号最大;后保存的修改,排在最前,序号最小;

    恢复了最新一次修改后,再次查看stash

    image-20200413225221071

    可以看到存储的修改只剩下一条了,由此可推断出git stash pop作用为:

    • 第一:恢复stash中存储的最新一次修改;
    • 第二:将该修改从stash中删除;
    git stash apply

    image-20200413225457480

    如上图所示,使用该指令时发生了合并冲突。这是因为,stash中保存的每一次修改代表的都是一个版本。

    image-20200413231349820

    • 如上图所示,在test分支上,进行第一次修改后,通过git stash将该修改作为修改0保存到stash中,此时分支中的文件并没有发生改变;

    • 进行第二次修改后,通过git stash将修改作为修改1保存到stash中,分支中的文件依旧没有发生改变;此时的stash中相当于保存着同一分支上两个修改后的版本;

    • 此时通过git stash pop取出修改0,与test分支进行合并;再通过git stash pop取出修改1,再次与test分支进行合并,两个版本合并自然会产生冲突。

    手动解决冲突后,要进行一次提交才算完成了手动合并;随后查看stash

    image-20200413230750201

    修改0仍然存在,说明git stash apply的作用为取出stash中最新(前面)的修改并与分支进行合并。但是,stash中存储的该修改并不会被删除;

    git stash apply stash@{n}

    这是最常用的方法,作用为从stash中恢复特定的修改,并且不删除stash中的该修改。

    test.txt的两次修改通过git stash存储到stash中,如下图所示:

    image-20200413232024080

    通过git stash apply stash@{1}恢复stash中存储的修改1

    image-20200413232309330

    如上图所示,成功地恢复了stash中的修改1,并且stash中的修改1并没有被删除;

    总结:

    • git stash pop:恢复并删除stash中存储的最新修改;
    • git stash apply:恢复但不删除stash中存储的最新修改;
    • git stash apply stash@{0}:恢复但不删除stash中存储的特定提交;

    以上就是这一节的全部内容了,相信看到这里的你已经能够熟练地使用Git进行版本回退了。下一节将会介绍大名鼎鼎的GithubGit的图形化操作界面。期待与你再次相见!

  • 相关阅读:
    Android开发学习之路-插件安装、检查应用是否安装解决方案
    Android开发学习之路-自定义控件(天气趋势折线图)
    Android开发学习之路-记一次CSDN公开课
    Android开发学习之路-RecyclerView滑动删除和拖动排序
    Android开发学习之路-带文字的图片分享
    Android开发学习之路-Android N新特性-多窗口模式
    Android开发学习之路-Volley源码解析
    Android开发学习之路-Android Studio开发小技巧
    Android开发学习之路-提升用户体验小技巧
    Android开发学习之路-Android6.0运行时权限
  • 原文地址:https://www.cnblogs.com/AhuntSun-blog/p/12700155.html
Copyright © 2020-2023  润新知