Pulpcode

捕获,搅碎,拼接,吞咽

0%

如何用git回到过去

这篇博客我打算总结一下,如何用git来完成某些撤销操作。当然这里指的撤销操作,一部分是说,git命令本身就支持的撤销操作,还有一部分是说我们为了对自己错误的补救而做的撤销操作。

git的结构

我把git所控制的项目结构,分成了四个区。当然这种分法并不完全严谨,但是我认为它对我理解git这个模型很有用。

git-origin

第一个是工作目录,其实工作目录就是你的项目本身,你平时写代码,都是在和工作目录打交道。而git其实就是在跟踪你的工作目录。所以如果你在开发一个项目,但是根本不打算用git管理,连git init的命令都没有输,那你就只有工作目录本身了。

第二个是暂存区,一开始学git的时候,都有这样一个疑问,为什么直接git commit不行。还非要git add一次。其实git的add一个文件,就是把一个文件放到暂存区。而你commit,其实就是commit暂存区的内容。这样的好处是你可以选择你准备要提交的修进行提交。而不是把所有修改的代码都进行提交。

git的这个add是很有歧义的,因为它和早期的svn意思并不一样,在svn里,add就是将某个文件加入版本控制了。还有这个stage,在git中有些被称为cache的命令:如 git diff --cached,其实就是和stage比较,所以git在新的版本中虽然保留了这两个命令作为后相兼容,同时又提供了新的命令共选择:

git stage 作为 git add 的同义词
git diff --staged 作为 git diff --cached 的同义词

第三个区域是代码库,这就是git存储代码版本的地方,你可以理解每一个git项目,都在内部维护了一个数据库,数据库里存放着历史代码,不管它底层是怎样的,它在表现形式上,就像是一颗树,记录着每一个分支的每一个版本。

第四个区域是回收站,我不知道应该怎样定义这样一个区域,但之所以提到这样一个区域,是因为git支持把你自己主动删除的分支或者版本给找回来,当然年代不能太过久远,否则就会被git给真的清理掉。

《PRO GIT》这本书大概提到了一部分git这几个命令的原理:Git用blob对象来存储文件内容,用tree对象存储目录里的文件名,用commit对象存储每一次提交。而git add负责将文件内容存入blob对象,并更新index,git commit负责根据index生成tree对象,然后生成commit对象指向这个tree对象。

使用场景

从使用的场景出发,有哪些场景你要回到过去。

不要工作区或者不要暂存区

你可能在修改了一部分代码之后,发现这个实现方式有问题,所以你要把工作目录还原成没开发的样子,又或者你觉得这部分修改后又add的内容不应该被提交,你后悔了,其实对于这两种内容,没什么好记住的,因为对于工作区的撤销和暂存区的撤销,你在操作git的时候,它就在友好的告诉你了。

1
2
3
4
5
6
7
8
On branch master

Initial commit

Changes to be committed:
(use "git rm --cached <file>..." to unstage)

new file: aaa.txt
1
2
3
4
5
6
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: aaa.txt

merge代码之后你后悔了

你把你代码的功能开发完了,然后准备推送到服务器的时候,发现服务器有人提交了代码,所以这个时候,你要先把服务器上的代码拉下来,然后你先使用git fetch,获取到本地。之后你git merge了这个分支,然而它出现了大量的冲突。你尝试解决冲突,乱改到一半的时候,你发现冲突并不好解决,你要先询问下你的同事相关信息,所以你希望你能回到没merge之前的样子。这个时候你就需要命令:git checkout

git checkout,被称为检出命令。我们一般用checkout的时候,就是在各个版本切换,或者用当前分支,切出一个新分支。其实我理解checkout,就是git数据库的查询语句,负责把git库中的某个提交,读到本地工作目录中。

所以我们在使用checkout切换分支的时候,其实就是读取代码库中的分支本到工作目录。而我们打算还原被修改的代码时,git checkout -- file,就是读取git库中的文件到工作区中,将工作区中的文件覆盖。

撤回到原来某个版本

git 有两种回到原来某个版本的方法

增量撤销

git revert 是一个不是很常用的命令,它使用增量的方式撤销修改,所谓增量,就是我之前提到的“只做加法,不做减法”。比如现在的git分支树的commit是A->B->C,我们revert C,其实是在C的基础上,衍生出了一个commit D,这时后commit栈就变成了: A->B->C->D,而这个commitD其实是对C的逆操作,也就是把C所有的修改,进行还原。这样做的好处是虽然撤回了commit C,但是提交C还在commit树里。

一般在本地,我们很少用revert的,用revert的原因一般是,对于你已经push的内容要进行回退,如果你用reset进行回退后,你会发现自己无法push的。(除非你用一个很被人反感的命令:git push -f)
所以你只能用revert来增量的把那些功能都取反,然后在push。

修改历史

还是上面那个例子,如果你想干脆修改历史,连C都不要了,那就可以使用 git reset。比如你git reset C,那你的commit栈就变成了: A->B。这样你就做到了真正的修改历史。当然,这种修改并不是找不回来。后面的博客,我会介绍如何找回这种被你撤销的历史。

git reset,提供了参数:

1
2
git reset <last good SHA>: 保留工作目录
git reset —hard <last good SHA> 不保留工作目录

所谓保留工作目录,是指把那次commit(last good SHA),扔到工作目录,哪些修改过的文件都变成了M标记(Modify)
而不保留工作目录,就仅仅回退到那个commit的样子,不给你重新提交的机会。
所以如果是撤销某次成功的merge,那用git reset –hard就最好了。

回收站

上面提到即使被你reset的提交,其实也可以找回来,只要你能找到它的commit的id(版本号)就行,这个时候就需要git reflog。

先用git reflog,找到那次被你删除的commit id,然后在git reset –hard它就行了。但是要注意的是,如果是被删了很久的commit,那么用git reflog也未必能找回来,可能被git库自己清了。

HEAD是什么

你可以理解HEAD就是一个指针,指向当前分支最新的一次提交。所以每一个分支都有一个自己的HEAD,所以每一次commit,都曾经被HEAD指针指向过。而我们对代码的commit或者撤销,都是在移动HEAD指针。

所以你就能理解,为什么很多撤销命令,都和HEAD有关。因为一般撤销都是撤销最近一次的操作,而HEAD指针就指向上一次提交。

git stash pop的失败

这里再提一点git stash的内容,git stash大家都知道,用来在开发途中,需要切换到别的分支写代码的时候非常有用。但是如果你本来要切回到C分支之后再git stash pop的,但是却一不小心在B分支上git stash pop了,那要怎么办?
其实没大碍,因为虽然你pop了stash,但是git记录了这是你哪个分支上的stash pop,所以如果你没有在正确的分支pop,虽然会pop出结果,但git不会删除栈顶的stash,这个时候,你只需要checkout让B变成原来的样子,再切到C分支,重新git stash pop就行。