小时候歌儿有得唱“想回到过去 / 试着抱你在怀里 / 羞怯的脸带有一点稚气 / 想看你看的世界 / 想在你梦的画面 / 只要靠在一起 / 就能感觉甜蜜”
小时候让你不抓紧,整天撸啊撸,玩 DOTA,长大你就知道后悔!
也罢也罢,伤心事儿咱不提了……
人生没有后悔药,Git 则是一部设计精良的时光机器。
(这不废话嘛,版本控制说白了就是可以回到以前的版本快照)
上一讲我无意中接触到了两个有关回退的命令:reset 和 checkout
来,帮你捋一捋思路:
现在几个命令应该相当清晰了:
- git add 命令用于把工作目录的文件放入暂存区域
- git commit 命令用于把暂存区域的文件提交到 Git 仓库
- git reset 命令用于把 Git 仓库的文件还原到暂存区域
- git checkout 命令用于把暂存区域的文件还原到工作目录
前边两个命令我有信心你已经相当熟悉了,但后边两个千万别说你已懂,因为它们是 Git 里边最复杂的命令(它们的功能可不止上边文字描述的这么简单 )
先给大家重点讲解 reset 命令,checkout 命令在分支管理中再细讲。
先执行 git log 命令查看历史提交:
这里记录了我们之前的 3 次提交(排序是按时间从近到远的),Author 后边是提交者,Date 后边是提交日期,下边是当次提交的说明。那……草黄色的那个 commit +“乱码”是什么鬼?
咳咳~~
这里并没有什么“乱码”,这个是 Git 为每次提交计算出来的 ID,它其实一个完整的 SHA-1 校验和(尽管你的文件内容可能跟我的完全一致,但这个值却不一样,这是因为账号、时间不同而导致)。你不需要知道 SHA-1 的原理,只需要知道它在任何时候都是唯一的,通过这个 ID,你就可以找到对应的那个版本。
有朋友可能会问:为何 ID 不是传统的 1、2、3、4、5…… 难道作者是为了装 X?
非也非也,因为 Git 是分布式的版本控制系统,如果多人同时工作,那么使用 1、2、3、4、5 就很容易产生冲突,继而打群架……
根据 log 记录,现在我们将 Git 仓库如果可视化,应该是这样子:
三棵树现在应该是下面酱紫:
回滚快照
注:快照即提交的版本,每个版本我们称之为一个快照。
现在我们利用 reset 命令回滚快照,并看看 Git 仓库和三棵树分别发生了什么。
执行 git reset HEAD~ 命令:
注:HEAD 表示最新提交的快照(08f7a42),而 HEAD~ 表示 HEAD 的上一个快照(754016c)
然后执行 git status 命令查看现在的状态:
有哪位童鞋可以回答我:现在我们的快照(754016c)回滚到了哪一棵树里?
答案是:第二棵树(暂存区域)!
有些朋友可能会持不同意见:不应该是回滚到第一棵树(工作目录)吗?你看,Git 不是写得很清楚吗 -> Changes not staged for commit,它还好心提醒我们使用 add 命令将修改添加到暂存区域丫!
其实真相是这样的:我们执行 git reset HEAD~ 命令之后,快照(754016c)回滚到暂存区域,此时工作目录里存放的却是最新的文件(08f7a42)。由于 Git 会跟踪文件的变化,所以执行 git status 命令时,git 发现工作目录中的文件比暂存区域的要新(对比日期),所以才有这样的提示……
好了,现在执行完 git reset HEAD~ 命令之后,Git 仓库应该是这样子:
三棵树现在应该是下面酱紫:
这里有一点要补充的:HEAD~ 表示 HEAD 的上一个快照(7540e6c),HEAD~~(1fe46d)则表示 HEAD 的上上一个快照,如果希望表示上上上上上上上上上上一个快照(数了一下,这里有 10 个“上” ),那么可以直接用 HEAD~10 来表示。
git reset HEAD~ 命令其实是 git reset --mixed HEAD~ 的缩写,因为 --mixed 选项是默认的,所以我们可以偷懒。
我们发现,git reset HEAD~ 命令其实影响了两棵树:首先是移动 HEAD 的指向,将其指向上一个快照(HEAD~);然后再将该位置的快照回滚到暂存区域。
为了灵活地操纵这三棵树,Git 还为 reset 命令安排了 --soft 和 --hard 选项,可谓软硬兼施,不到你不服~
--soft 选项
加上 --soft 选项的结果是使得 reset 变“软”了,也就没有原来那么持久……
So, git reset --soft HEAD~ 命令就相当于只移动 HEAD 的指向,但并不会将快照回滚到暂存区域。
这个选项有什么作用呢?
事实它就是相当于撤消了上一次的提交(commit)。
一不小心提交了,后悔了,那么你就执行 git reset --soft HEAD~ 命令即可(此时执行 git log 命令,也不会再看到已经撤消了的那个提交)。
--hard 选项
加上 --hard 选项的结果是使得 reset 变“硬”……
你猜的不错,加上 --hard 选项,reset 不仅移动 HEAD 的指向,将快照回滚动到暂存区域,它还将暂存区域的文件还原到工作目录。
来,上点图吧!
刚才执行完 git reset HEAD~ 命令后,Git 仓库里的数据是这样:
三棵树是这样:
那么在这种状态下,我再执行 git reset --hard HEAD~ 命令:
Git 仓库中就剩下最后一个快照了:
还原案发现场,Git 仓库现在应该是这样:
而三棵树现在应该都被回归到第一个版本(1fe46d):
不信?自己瞧瞧你的文件夹:
LICENSE 文件没了
最后总结一下:reset 回滚快照三部曲
1. 移动 HEAD 的指向(--soft)
2. 将快照回滚到暂存区域([--mixed],默认)
3. 将暂存区域还原到工作目录(--hard)
回滚指定快照
如果快照比较多,你又懒得去数有多少个“上”,那么你可以通过指定具体的快照 ID 来回滚该快照。
比如 git reset 00c2929
上,你不必把辣么长的 ID 号都给输入进去,一般只要输入前几位(5 位或以上吧)就可以了。
回滚个别文件
reset 不仅可以回滚指定快照,还可以回滚个别文件。
命令格式为 git reset 快照 文件名/路径
这样,它就会将忽略移动 HEAD 的指向这一步(因为你只是回滚快照的部分内容,并不是整个快照,所以 HEAD 的指向不应该发生改变),直接将指定快照的指定文件回滚到暂存区域。
不仅可以往回滚,还可以往前滚!
这里需要强调的是:reset 不仅是一个“复古”的命令,它不仅可以回到过去,还可以去到“未来”。
唯一的一个前提条件是:你需要知道指定快照的 ID 号。
现在执行 git log 命令只剩下一个最原始的提交了:
但是将命令行窗口向上拉,我们可以喵到之前提交的几个版本 ID 号(聪明如我 ):
所以我们可以执行 git reset --hard 31f46be 命令:
再次执行 git log 命令:
我们又回到了最新的版本!
是的,我们就这样在历史的长河里滚来滚去……
但是……故事还没完,如果某天你 reset --hard 将工作目录回滚到了某个版本,但特么的隔天你就后悔了(有封写给小花的情书也放里边)!此时命令行窗口早已关闭,你又没用小本本把每次 commit 的 ID 号给记下来,这可肿么办才好?
reflog 命令可以拯救你!
执行 git reflog 命令,告诉我,你看到了什么:
没错,Git 偷偷记录下了你每一次的操作(无耻小人),第一列就是每次执行完命令,HEAD 指向的版本 ID 号啦~