Git如何回滚一次错误的合并

原文发表在知乎专栏 前端杂货铺, 欢迎关注个人专栏,转载请注明出处前端

今天不说前端,来聊聊git吧。 发现如今的小孩,玩框架一套一套的,等到玩点实质的工程化的东西就不行了。 git 这么好的工具,培训班怎么能够忽视他的重要性呢?git

再来聊聊git的工做流程

不少人对Git到底是一个怎样的系统,仍是只知其一;不知其二。 在这里强烈建议你们先理解git的核心思想和工做原理,有过subversion或者perforce使用经验的人更是须要摒弃以前所见所学,从新接受这样一个新思想。 咱们再也不这里赘述其几本原理,咱们来介绍一下其简单工做流程。 Git以一个自有的思惟框架管理着三个不一样的盒子Commit HistoryINDEXWorking Directorygithub

  • Commit History 历史记录,存储着全部提交的版本快照,并由当前分支引用的指针HEAD指向该分支最新一条提交。
  • INDEX 索引,也叫暂存区域。它是一个文件,保存着即将提交的文件列表快照。
  • Working Directory 工做目录,是从git仓库压缩数据当前版本中解包出来的文件列表。因此你在本地磁盘看到的你项目源码的文件列表,其实就是git开放给你的一个沙盒。在你将文件的修改天道到暂存区域并将快照记录到历史以前,你能够随意更改。

理解了这三者的含义后,咱们试着来理解一下git的工做流程。 一切的开始,混沌之间,咱们要干一件大事,在terminal里面敲打了几下键盘缓存

git init 
复制代码

混沌初开,幻化三界:HEADINDEXWorking Directory。这就是世界最开始的样子git仓库仿佛就是掌管三界之神。而Working Directory就是他分配给你生产和工做的地方,你能够在这里肆意的创造。而为了安全和管理的有序咱们须要把咱们的添加与修改的文件交给git仓库。Git首先会将修改的文件标记起来放入暂存区、而后git找到暂存区域的文件内容将其永久性的存储为快照到git仓库,此时HEAD的指针指向这个最新的快照。安全

如图,总结下三个步骤bash

  1. 在工做目录中修改文件。
  2. 暂存文件,将文件的快照放入暂存区域。git add
  3. 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录 git commit

git 的基本工做流程就是在不断的重复这三个步骤,最终git仓库目录造成了一个快照堆栈,每产生一次新的版本,HEAD就会指向这个版本。框架

这里咱们建立了下面这些文件:工具

├── README.md
├── v1.js
├── v2.js
└── v3.js

复制代码

造成了下图的提交历史学习

3aa5dfb v3  (<- HEAD)
        |
5aab391 v2
        |
ff7b88e v1
        |
95d7816 init commit

复制代码

下面咱们来看看怎么利用checkout、reset、revert 来操做这个仓库目录测试

checkout 、reset 仍是 revert ?

checkout

版本控制系统背后的思想就是「安全」地储存项目的拷贝,这样你永远不用担忧何时不可复原地破坏了你的代码库。当你创建了项目历史以后,git checkout 是一种便捷的方式,来将保存的快照「解包」到你的工做目录上去。 git checkout 能够检出提交、也能够检出单个文件甚至还能够检出分支(此处省略)。

git checkout 5aab391
复制代码

检出v2,当前工做目录和5aab391彻底一致,你能够查看这个版本的文件编辑、运行、测试都不会被保存到git仓库里面。你能够git checkout master 或者 git checkout -回到原来的工做状态上来。

git checkout 5aab391 v1.js
复制代码

以检出v2版本对于v1.js的改动,只针对v1.js这个文件检出到5aab391版本。因此 它会影响你当前的工做状态,它会把当前状态的v1.js文件内容覆盖为5aab391版本。因此除非你清楚你在作什么,最好不要轻易的作这个操做。但这个操做对于舍弃我当前的全部改动颇有用:好比当前我在v1.js上面作了一些改动,但我又不想要这些改动了,而我又不想一个个去还原,那么我能够git checkout HEAD v1.js 或者 git checkout -- v1.js

reset 重置

git checkout 同样, git reset 有不少用法。

git reset <file>
复制代码

从暂存区移除特定文件,但不改变工做目录。它会取消这个文件的缓存,而不覆盖任何更改。

git reset
复制代码

重置暂存区,匹配最近的一次提交,但工做目录不变。它会取消全部文件的暂存,而不会覆盖任何修改,给你了一个重设暂存快照的机会。

git reset --hard
复制代码

加上--hard标记后会告诉git要重置缓存区和工做目录的更改,就是说:先将你的暂存区清除掉,而后将你全部未暂存的更改都清除掉,因此在使用前肯定你想扔掉全部的本地工做。

git reset <commit>
复制代码

将当前分支的指针HEAD移到 ,将缓存区重设到这个提交,但不改变工做目录。全部 以后的更改会保留在工做目录中,这容许你用更干净、原子性的快照从新提交项目历史。

git reset --hard <commit>
复制代码

将当前分支的指针HEAD移到 ,将缓存区和工做目录都重设到这个提交。它不只清除了未提交的更改,同时还清除了 以后的全部提交。

能够看出,git reset 经过取消缓存或者取消一系列提交的操做会摒弃一些你当前工做目录上的更改,这样的操做带有必定的危险性。下面咱们开始介绍一种相对稳妥的方式 revert

revert 撤销

git revert被用来撤销一个已经提交的快照。但实现上和reset是彻底不一样的。经过搞清楚如何撤销这个提交引入的更改,而后在最后加上一个撤销了更改的 新 提交,而不是从项目历史中移除这个提交。

git revert <commit>
复制代码

生成一个撤消了 引入的修改的新提交,而后应用到当前分支。

例如:

81f734d commit after bug
        |
3a395af bug
        |
3aa5dfb v3  (<- HEAD)
        |
5aab391 v2
        |
ff7b88e v1
        |
95d7816 init commit

复制代码

咱们在3a395af 引入了一个bug,咱们明确是因为3a395af形成的bug的时候,以其咱们经过新的提交来fix这个bug,不如git revert, 让他来帮你剔除这个bug。

git revert 3a395af
复制代码

获得结果

cfb71fc Revert "bug"
        |
81f734d commit after bug
        |
3a395af bug
        |
3aa5dfb v3  (<- HEAD)
        |
5aab391 v2
        |
ff7b88e v1
        |
95d7816 init commit

复制代码

这个时候bug的改动被撤销了,产生了一个新的commit,可是commit after bug没有被清初。

因此相较于resetrevert不会改变项目历史,对那些已经发布到共享仓库的提交来讲这是一个安全的操做。其次git revert能够将提交历史中的任何一个提交撤销、而reset会把历史上某个提交及以后全部的提交都移除掉,这太野蛮了。

另外revert的设计,还有一个考量,那就是撤销一个公共仓库的提交。至于为何不能用reset,大家能够本身思考一下。 下面咱们就用一个麻烦事(回滚一个错误的合并),来说解这个操做。

合并操做

相对于常规的commit,当使用git merge <branch>合并两个分支的时候,你会获得一个新的merge commit. 当咱们git show <commit>的时候会出现相似信息:

commit 6dd0e2b9398ca8cd12bfd1faa1531d86dc41021a
Merge: d24d3b4 11a7112
Author: 前端杂货铺 
...............
复制代码

Merge: d24d3b4 11a7112 这行代表了两个分支在合并时,所处的parent的版本线索。

好比在上述项目中咱们开出了一个dev分支并作了一些操做,如今分支的样子变成了这样:

init -> v1 -> v2 -> v3  (master)
           \      
            d1 -> d2  (dev)
复制代码

当咱们在dev开发的差很少了

#git:(dev)
git checkout master 
#git:(master)
git merge dev
复制代码

这个时候造成了一个Merge Commit faulty merge

init -> v1 -> v2 -> v3 -- faulty merge  (master)
           \            /
            d1  -->  d2  (dev)
复制代码

此时faulty merge有两个parent 分别是v3 和 d2。

回滚错误的合并

这个merge以后还继续在dev开发,另外一波人也在从别的分支往master合并代码。变成这样:

init -> v1 -> v2 -> v3 -- faulty merge -> v4 -> vc3 (master)
        \  \            /                     /
         \  d1  -->  d2  --> d3 --> d4  (dev)/
          \                                 / 
           c1  -->  c2 -------------------c3 (other)
复制代码

这个时候你发现, 妈也上次那个merge 好像给共享分支master引入了一个bug。这个bug致使团队其余同窗跑不通测试,或者这是一个线上的bug,若是不及时修复老板要骂街了。

这个时候第一想到的确定是回滚代码,但怎么回滚呢。用reset?不现实,由于太流氓不说,还会把别人的代码也干掉,因此只能用revert。而revert它最初被设计出来就是干这个活的。

怎么操做呢?首先想到的是上面所说的 git revert <commit> ,可是貌似不太行。

git revert faulty merge
error: Commit faulty merge is a merge but no -m option was given.
fatal: revert failed
复制代码

这是由于试图撤销两个分支的合并的时候Git不知道要保留哪个分支上的修改。因此咱们须要告诉git咱们保留那个分支m 或者mainline.

git revert -m 1 faulty merge
复制代码

-m后面带的参数值 能够是1或者2,对应着parent的顺序.上面列子:1表明v3,2表明d2 因此该操做会保留master分支的修改,而撤销dev分支合并过来的修改。

提交历史变为

init -> v1 -> v2 -> v3 -- faulty merge -> v4 -> vc3 -> rev3 (master)
          \            /                     
           d1  -->  d2  --> d3 --> d4  (dev)
复制代码

此处rev3是一个常规commit,其内容包含了以前在faulty merge撤销掉的dev合并过来的commit的【反操做】的合集。

到这个时候还没完,咱们要记住,由于咱们抛弃过以前dev合并过来的commit,下次dev再往master合并,以前抛弃过的实际上是不包含在里面的。那怎么办呢?

恢复以前的回滚

很简单咱们把以前master那个带有【反操做】的commit给撤销掉不就行了?

git checkout master
git revert rev3
git merge dev
复制代码

此时提交历史变成了

init -> v1 -> v2 -> v3 -- faulty merge -> v4 -> vc3 -> rev3 -> rev3` -> final merge (master)
          \            /                                               /
           d1  -->  d2  --> d3 --> d4  --------------------------------(dev)
复制代码

总结

以上就是我想要讲的关于git回滚代码的一些操做,有不对的地方还望指正。另Git 是一门艺术,是一种很是精妙的设计,当你使用上手后,你会发现愈来愈多好玩的东西,并为设计git的人默默点个赞。也但愿在前端领域不管是初学仍是深凿者,在追逐流行框架的时候,都不要忘了学习这些基础的工具。

参考

相关文章
相关标签/搜索