zoukankan      html  css  js  c++  java
  • 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的图形化操作界面。期待与你再次相见!

  • 相关阅读:
    Effective Java 第三版——72. 赞成使用标准异常
    Effective Java 第三版——71. 避免不必要地使用检查异常
    Effective Java 第三版——70. 对可恢复条件使用检查异常,对编程错误使用运行时异常
    Effective Java 第三版——69. 仅在发生异常的条件下使用异常
    Effective Java 第三版——68. 遵守普遍接受的命名约定
    Effective Java 第三版——67. 明智谨慎地进行优化
    Effective Java 第三版——66. 明智谨慎地使用本地方法
    Effective Java 第三版——65. 接口优于反射
    Effective Java 第三版——64. 通过对象的接口引用对象
    Effective Java 第三版——63. 注意字符串连接的性能
  • 原文地址:https://www.cnblogs.com/AhuntSun-blog/p/12700155.html
Copyright © 2011-2022 走看看