Git 是一部设计精良的时光机器,使用 Git ,我们可以在不同时期的版本之间来回穿梭,上节课我们无意间讲到了两个关于 回退版本的命令:reset 和 checkout
•add
–用于把工作目录的文件放入暂存区域
•commit
–用于把暂存区域的文件提交到Git仓库
•reset
–用于把Git仓库的文件还原到暂存区域
•checkout
–用于把暂存区域的文件还原到工作目录
add 和 commit 命令相信大家已经很熟悉了,但是 reset 和 checkout 命令你也不要说自己已经懂了,因为 这两个命令是 Git 里面最复杂的命令,它们的功能可不止我们刚才讲的这么简单。它们有很多复杂的功能也很实用,这节课我们就主要来讲解 reset 命令,checkout 命令我们会在分支管理的章节进行讲解。
我们在上一节课工作目录的基础上,执行 git log 命令:
E:\MyProject>git log
commit 08f7a42b90ff076d7f26fb6dbdbc9b86808256c0 (HEAD -> master)
Author: XiangyangDai <[email protected]>
Date: Mon Jan 14 21:28:24 2019 +0800change the LICENSE file
commit 7540e6c3dca0b17139054df7548fe0570fd741f2
Author: XiangyangDai <[email protected]>
Date: Mon Jan 14 21:27:03 2019 +0800add a LICENSE file
commit 1fe46d092b4a7f1c99ddac65d7c2958000325f1e
Author: XiangyangDai <[email protected]>
Date: Mon Jan 14 21:24:14 2019 +0800add a README file
E:\MyProject>
目前的 Git 仓库有3个版本,第一个是 add a README file;第二个是 add a LICENSE file;第三个是 change the LICENSE file。
为了方便大家理解,我们将其可视化,Git 仓库目前的情况应该就是这样子:
HEAD 指向的是最新的版本,第一次提交我们只是增加了一个 README.md,第二次提交我们增加了 LICENSE,第三次提交我们修改了 LICENSE。v1 是第一版,v2 是第二版,我们也用不同的颜色加以区分。
三棵树应该就如下图所示:
因为我们最新的在工作目录修改后的 add 到了缓存区,然后commit 到了 Git 仓库,所以,现在 三棵树应该是一样的。
我们执行 git reset HEAD~
我们知道 HEAD 现在是指向 08f7a42.... 这个版本,而加一个 波浪线(~)表示它的上一个,也就是 7540e6c... 这个版本。
我们回车:
E:\MyProject>git reset HEAD~
Unstaged changes after reset:
M LICENSEE:\MyProject>
那谁知道现在我们的 7540e6c... 这个快照现在回滚到哪一棵树里了呢?
大家考虑一下。答案是第二棵树,就是暂存区。也就是说,我们执行了 git reset HEAD~ 这个语句之后,前一个版本是回滚到了第二棵树里面。我们证明一下:
我们现在执行 git status:
E:\MyProject>git status
On branch master
Changes not staged for commit:
(use "git add..." to update what will be committed)
(use "git checkout --..." to discard changes in working directory) modified: LICENSE
no changes added to commit (use "git add" and/or "git commit -a")
E:\MyProject>
有些同学就会有不同意见了,看提示的话,这样子不是回滚到第一棵树吗,Changes not staged for commit: 就是说第一棵树的内容发生了改变,你还没暂存,那应该就是回滚到第一棵树才是没有暂存啊?
其实不是这样的,你的思维要反过来,因为 git 会时刻跟踪你的文件的版本变化,所以我们刚才把 7540e6c... 这个快照回滚到第二棵树这里,
目前的情况就是下图所示:回滚到了 7540e6c....版本,然后是在第二颗树暂存区这里。
这个时候,git status 就会检测到 文件不同了。然后就会提醒还没有 staged,还没有被暂存。
我们现在执行 git log:
E:\MyProject>git log
commit 7540e6c3dca0b17139054df7548fe0570fd741f2 (HEAD -> master)
Author: XiangyangDai <[email protected]>
Date: Mon Jan 14 21:27:03 2019 +0800add a LICENSE file
commit 1fe46d092b4a7f1c99ddac65d7c2958000325f1e
Author: XiangyangDai <[email protected]>
Date: Mon Jan 14 21:24:14 2019 +0800add a README file
E:\MyProject>
就会发现只有两个快照了,且明确显示现在 HEAD 是指向 7540e6c3dca0b17139054df7548fe0570fd741f2。
08f7a42.....这个快照并没有丢失,只是 HEAD 没有指向它,所以通过 git log 看不到它了,只能看到 HEAD 及其往前的,因为没有箭头指向后边。
ok。这里还有一点需要补充的:
HEAD~ 表示 HEAD 的上一个快照(7540e6...),HEAD~~(1fe46d...)则表示 HEAD 的上上一个快照,如果希望表示上上上上上上上上上上一个快照(数了一下,这里有 10 个“上” ),那么可以直接用 HEAD~10 来表示。
我们接下来讲讲 reset 这个命令的选项:
reset命令的选项
•git rest --mixed HEAD~(这是默认情况,直接写 git rest HEAD~ 就会默认加上 --mixer)
–移动HEAD的指向,将其指向上一个快照(这里就修改了第三棵树)
–将HEAD移动后指向的快照回滚到暂存区域(这里就修改了第二棵树)
为了更灵活的操纵这三棵树,git 还为 rest 命令安排了两个选项,--soft 和 --hard。软硬兼施,不到你不服。
首先是 --soft,使 reset 变得软:
•git rest --soft HEAD~
–移动HEAD的指向,将其指向上一个快照
这个命令就只影响了一棵树,就只是改变了第三棵树,改变了HEAD指针的指向,但不会更改暂存区里面的内容,暂存区里面依旧存放的是上一次提交的最新的内容。
该选项的作用相当于撤销上一次的提交(commit),比如说不小心提交了,后悔了,就可以直接 git rest --soft HEAD~,相当于撤销一次错误的 commit 命令。
接下来就是 --hard,就是使得 reset 变得硬。
•git rest --hard HEAD~
–移动HEAD的指向,将其指向上一个快照
–将HEAD移动后指向的快照回滚到暂存区域
–将暂存区域的文件还原到工作目录
这个命令不仅改变了 HEAD 的指向,影响了第三棵树,也同时影响了第二棵树,就是将指向的快照回滚到暂存区域,同时还会影响第一棵树,将暂存区域的文件还原到工作目录。所以呢,使用 hard 命令你就要注意了,这是有危险的,因为它会把你工作目录里的最新的文件给覆盖掉。
我们来演示一下 hard 命令:
我们现在的工作目录下有两个文件:LICENSE 和 README.md。
我们执行 git reset --hard HEAD~
(刚才 HEAD 已经指向的是 7540e6c....,,所以现在再上一个就是 第一个版本 1fe46d....)
E:\MyProject>git reset --hard HEAD~
HEAD is now at 1fe46d0 add a README file
E:\MyProject>
LICENSE 也被删掉了。
所以我们说 git reset --hard HEAD~ 这个命令是有危险的,它会把工作目录里面修改得很辛苦的文件给删除了。
现在再执行 git log:就只有一个了。
E:\MyProject>git log
commit 1fe46d092b4a7f1c99ddac65d7c2958000325f1e (HEAD -> master)
Author: XiangyangDai <[email protected]>
Date: Mon Jan 14 21:24:14 2019 +0800
add a README file
E:\MyProject>
这时的情况就是下图所示:这就是hard 的威力,同时影响3棵树。
我们来总结一下,reset 命令回滚快照有三部曲:
1.移动 HEAD 的指向(--soft)
2.将快照回滚到暂存区域([--mixed],默认)
3.将暂存区域还原到工作目录(--hard)
除此之外,还可以回滚指定快照。
比如说,你的快照比较多,你又懒得去数前面到底有多少个 “上”,你就可以通过指定具体的快照 ID 来回滚快照,我们知道, 快照 ID 是一个很长的唯一值,你只要指定前面几个数字让 git 可以识别就可以了
git reset 版本快照的ID号
回滚个别文件
reset 不仅可以回滚指定的快照,还可以回滚快照中的个别文件,命令如下:
git reset 版本快照 文件名/路径
这里需要注意的一点是:如果只是回滚个别个别文件的话,会忽略 第一步 HEAD 指针移动的这一步,因为你只是回滚快照中的个别文件,而不是回滚整个快照,所以 HEAD 指针不会改变。
另外:reset 不仅可以往回滚,还可以往前滚!
(不仅可以回到过去,还可以去到未来)命令如下:
git reset 版本快照的ID号
唯一的条件就是要知道 快照的 ID 号,但其实知道 ID 号也不是一件很难的事情,只需要 git log 就可以了。
E:\MyProject>git log
commit 1fe46d092b4a7f1c99ddac65d7c2958000325f1e (HEAD -> master)
Author: XiangyangDai <[email protected]>
Date: Mon Jan 14 21:24:14 2019 +0800
add a README file
我们现在在这里,并且我们的 工作目录中的 LICENSE 也没有了,我们试图回到 最新的 版本,将我们 第二个版本的 LICENSE 弄回来。
E:\MyProject>git reset 08f7a42
Unstaged changes after reset:
D LICENSE
E:\MyProject>git log
commit 08f7a42b90ff076d7f26fb6dbdbc9b86808256c0 (HEAD -> master)
Author: XiangyangDai <[email protected]>
Date: Mon Jan 14 21:28:24 2019 +0800
change the LICENSE file
commit 7540e6c3dca0b17139054df7548fe0570fd741f2
Author: XiangyangDai <[email protected]>
Date: Mon Jan 14 21:27:03 2019 +0800
add a LICENSE file
commit 1fe46d092b4a7f1c99ddac65d7c2958000325f1e
Author: XiangyangDai <[email protected]>
Date: Mon Jan 14 21:24:14 2019 +0800
add a README file
通过 git reset 08f7a42,就将 HEAD 指向了最新的版本快照。
但是我们在 工作目录中一看,我们的 LICENSE 还是没有出现啊。
问题在于你使用的是默认的 mixer 方式,mixer 的方式只是恢复到暂存区域,并没有把暂存区的内容恢复回工作目录。
我们可以通过 git status 查看:
E:\MyProject>git status
On branch master
Changes not staged for commit:
(use "git add/rm..." to update what will be committed)
(use "git checkout --..." to discard changes in working directory) deleted: LICENSE
no changes added to commit (use "git add" and/or "git commit -a")
E:\MyProject>
你可以通过 checkout 把它拉回工作目录。
事实上,我们直接加上 hard 也是一样的:
E:\MyProject>git reset --hard 08f7a42
HEAD is now at 08f7a42 change the LICENSE file
E:\MyProject>
你就会看到,我们的第二个版本的 LICENSE 又出现在了工作目录了。
那有的朋友又有问题了,我一时手快,把 cmd 命令窗口给关上了,那我们就不能往前拉,找到所有版本快照的 ID号了。
那怎么办才好呢?
事实上也是有办法的:git reflog:
E:\MyProject>git reflog
08f7a42 (HEAD -> master) HEAD@{0}: reset: moving to 08f7a42
08f7a42 (HEAD -> master) HEAD@{1}: reset: moving to 08f7a42
1fe46d0 HEAD@{2}: reset: moving to HEAD~
7540e6c HEAD@{3}: reset: moving to HEAD~
08f7a42 (HEAD -> master) HEAD@{4}: commit: change the LICENSE file
7540e6c HEAD@{5}: commit: add a LICENSE file
1fe46d0 HEAD@{6}: commit (initial): add a README file