Git 重置
1. 分支游标 master 探秘
master 分支在版本库的引用目录( .git/refs )中体现为一个引用文件 .git/refs/heads/master。
先来看看当有新的提交发生的时候,文件 .git/refs/heads/master 的内容如何改变。首先在工作区查 u 你更加爱你一个新文件,姑且叫做new-commit.txt, 然后提交到版本库中。
来看看 master 分支指向的提交ID 是否改变了。
- 可以看出在版本库引用空间(.git/refs/目录)下的 master 文件内容的确改变了,指向了新的提交。
- 再用 git log 查看一下提交日志,可以看到刚刚完成的提交。
[git@iZbp12wtztgoi1eseucsoyZ heads]$ git log --graph --oneline * ba03344 commit new-commit1.txt * f178513 commit new-commit.txt * 3f7c481 commit world * fb97aa3 commit welcome.txt
引用 refs/heads/master 就好像是一个游标,再有新的提交发生的时候指向了新的提交。可是如果只可上、不可下,就不能称为“游标”。Git 提供了 git reset 命令,可以将“游标”指向任意一个存在的提交 ID。下面的示例就尝试认为地更改游标。(注意下面的命令中使用了--hard 参数,会破坏工作区为提交的改动,慎用。)
git reset --hard HEAD^
HEAD^ 代表了HEAD 的父提交,这条命令就相当于将master 重置到了上一个老的提交上。我们来看一下 master 文件的内容时候更改了。
结果:
1. .master 分支的引用文件的指向更改为前一次提交的ID了。
2. 新增加的new-commit1.txt 文件也丢失了。
重置命令不仅可以重置到前一次提交,而且还可以直接使用提交ID重置到任何一次提交。
- 通过 git log 查询到最早的提交ID
[git@iZbp12wtztgoi1eseucsoyZ demo]$ git log --graph --oneline * f178513 commit new-commit.txt * 3f7c481 commit world * fb97aa3 commit welcome.txt
- 然后重置到最早的一次提交。
git reset --hard fb97aa3
- 重置后发现welcome.txt 也会推倒原始版本库,曾经的修改都丢失了。
使用重置命令很危险,会彻底地丢弃历史。那么,还能够通过浏览提交历史的办法找到丢弃的提交ID,再使用重置命令恢复历史吗?不可能!因为重置让提交历史也改变了,提交日志如下:
[git@iZbp12wtztgoi1eseucsoyZ demo]$ git log commit fb97aa31bd154158df7eeafbece10d747c59190f Author: xumingrui <xumingrui@126.com> Date: Thu May 3 13:44:36 2018 +0800 commit welcome.txt
2. 用 reglog 挽救错误的重置
如果没有记下重置前 master 分支指向的提交ID,想要重置回原来的提交似乎是一件麻烦的事情(去对象库中一个一个地找)。幸好Git 提供了一个挽救机制,通过 .git/logs 目录下日志文件记录了分支的变更。默认非裸版本库(带有工作区)都提供分支日志功能,这是因为带有工作区的版本库都有如下设置:
git config core.logallrefupdates true
查看一下master 分支的日志文件 .git/logs/refs/heads/master 中的内容。下面的命令显示了该文件的最后几行。为了排版的需要,还将输出的40位的SHA1提交ID缩短。
tail -5 .git/logs/refs/heads/master
[git@iZbp12wtztgoi1eseucsoyZ demo]$ tail -5 .git/logs/refs/heads/master fb97aa31bd154158df7eeafbece10d747c59190f 3f7c4817e2fe36344579e57c6861ba6eab3aadcb xumingrui <xumingrui@126.com> 1525327691 +0800 commit: commit world 3f7c4817e2fe36344579e57c6861ba6eab3aadcb f1785138872bb4a6b5f6475ae22401237ce2fbae xumingrui <xumingrui@126.com> 1525329315 +0800 commit: commit new-commit.txt f1785138872bb4a6b5f6475ae22401237ce2fbae ba03344328127c77c6a4f0a17d5db09b86bf9862 xumingrui <xumingrui@126.com> 1525329438 +0800 commit: commit new-commit1.txt ba03344328127c77c6a4f0a17d5db09b86bf9862 f1785138872bb4a6b5f6475ae22401237ce2fbae xumingrui <xumingrui@126.com> 1525330062 +0800 reset: moving to HEAD^ f1785138872bb4a6b5f6475ae22401237ce2fbae fb97aa31bd154158df7eeafbece10d747c59190f xumingrui <xumingrui@126.com> 1525330587 +0800 reset: moving to fb97aa3
可以看出这个文件记录了master分支指向的变迁,最新的改变追加到文件的末尾,因此最后出现。最后一行可以看出因为执行了 git reset --hard 命令,指向的提交ID 由 f178513 改变为 fb97aa3
Git 提供了一个 git reflog 命令,对这个文件进行操作。使用show子命令可以显示此文件的内容。
[git@iZbp12wtztgoi1eseucsoyZ demo]$ git reflog show master | head -5 fb97aa3 master@{0}: reset: moving to fb97aa3 f178513 master@{1}: reset: moving to HEAD^ ba03344 master@{2}: commit: commit new-commit1.txt f178513 master@{3}: commit: commit new-commit.txt 3f7c481 master@{4}: commit: commit world
查看git relog 的输出和直接查看日志文件最大的不同在于显示顺序的不同,即:最新改变放在了最前面显示,而且只显示每次改变的最终的SHA1 哈希值。还有个重要的区别在于 git reflog 命令的输出中还提供了一个方便易记的表达式:<refname>@{<n>}。这个表达式的含义是引用<regname> 之前第<n>次改变时的SHA1哈希值。
那么将引用master切换到两次变更之前的值,可以使用下面的命令。
1. 重置master为两次改变之前的值。
git reset --hard master@{2}
2. 重置后工作区中的文件 new-commit.txt 又回来了。
3. 提交历史也回来了。
此时如果再用 git reflog 查看,会看到恢复 master 的操作也记录在日志中了
[git@iZbp12wtztgoi1eseucsoyZ demo]$ git reflog show master | head -5 ba03344 master@{0}: reset: moving to master@{2} fb97aa3 master@{1}: reset: moving to fb97aa3 f178513 master@{2}: reset: moving to HEAD^ ba03344 master@{3}: commit: commit new-commit1.txt f178513 master@{4}: commit: commit new-commit.txt
3. 深入了解 git reset 命令
重置命令(git reset)是Git 最常用的命令之一,也是最危险最容易误用的命令。来看看 git reset 命令的用法。
上面列出了两个用法,其中<commit> 都是可选项,可以使用引用或提交ID,如果省略<commit>则相当于使用了HEAD的指向作为提交ID。
上面列出的两种用法的区别在于,第一种用法在命令中包含路径<paths>。 为了避免路径和引用(或者提交ID)同名而发生冲突,可以在<paths>前用两个连续的短线(减号)作为分隔。
第一种用法(包含了路径<paths>的用法)不会重置引用,更不会改变工作区,而是用指定提交状态(<commit>)下的文件(<paths>)替换掉暂存区中的文件。eg: git reset HEAD <paths> 相当于取消之前执行的 git add <paths> 命令时改变的暂存区。
第二种用法 (不使用路径 <paths> 的用法)则会重置引用。根据不同的选项,可以对暂存区或工作区进行重置。参照下面的版本库模型图,来看一看不同的参数对第二种重置语法的影响。
命令格式: git reset [--soft | --mixed | --hard ] [<commit>]
- 使用参数--hard,如: git reset --hard <commit>。会执行上图中的全部动作 ①,②,③,即:
- 替换引用的指向。引用指向新的提交ID。
- 替换暂存区。替换后,暂存区的内容和引用指向的目录树一致。
- 替换工作区。替换后,工作区的内容变得和暂存区一致,也和HEAD 所指向的目录树内容相同。
- 使用参数--soft,如: git reset --soft <commit>。
会执行上图中的操作①,即只更改引用的指向,不改变暂存区和工作区。
- 使用参数 --mixed 或不使用参数 (默认为 --mixed),如: git reset <commit>。 会执行上图中的操作①和操作②。即更改引用的指向及重置暂存区,但是不改变工作区。
下面通过一些示例,看一下重置命令的不同用法。
- 命令:git reset
仅用HEAD 指向的目录树重置暂存区,工作区不会受到影响,相当于将之前用git add 命令更新到暂存区的内容撤出暂存区。引用也未改变,因为引用重置到 HEAD 相当于没有重置。
- 命令:git reset HEAD
同上。
- 命令:git reset --filename
仅将文件filename的改动撤出暂存区,暂存区中其他文件不改变。相当于对命令git add filename 的反向操作。
- 命令:git reset HEAD filename
同上。
- 命令: git reset --soft HEAD^
工作区和暂存区不改变,但是引用向前回退一次。当对最新提交的提交说明或提交的更改不满意时,撤销最新的提交以便重新提交。
在之前曾经介绍过一个修补提交命令 git commit --amend,用于对最新的提交进行重新提交已修补错误的提交说明或错误的提交文件。修补提交命令实际上相当于执行了下面两条命令。(注: 文件 .git/COMMIT_EDITMSG保存了上次的提交日志。)
git reset --soft HEAD^ git commit -e -F .git/COMMIT_EDITMSG
- 命令: git reset HEAD^
工作区不改变,但是暂存区会回退到上一次提交之前,引用也会回退一次。
- 命令:git reset --mixed HEAD^
同上。
- 命令: git reset --hard HEAD^
彻底撤销最近的提交。引用回退到前一次,而且工作区和暂存区都会回退到上一次提交的状态。自上一次以来的提交全部丢失。