分支游标master的探秘
先来看看当有新的提交发生时,文件.git/refs/heads/master
的内容如何改变。首先在工作区创建一个新文件,姑且叫做new-commit.txt
,然后提交到版本库中。
1 | $ touch new-commit.txt |
此时工作目录下会有两个文件,其中文件new-commit.txt
是新增的。
1 | $ ls |
来看看master分支指向的提交ID是否改变了。
- 先看看在版本库引用空间(.git/refs/目录)下的
master
文件内容的确更改了,指向了新的提交。
1 | $ cat .git/refs/heads/master |
- 再用
git log
查看一下提交日志,可以看到刚刚完成的提交。
1 | $ gilt log --graph --oneline |
引用refs/heads/master
就好像是一个游标,在有新的提交发生的时候指向了新的提交。可是如果只可上、不可下,就不能称为“游标”。
Git提供了git reset
命令,可以将“游标”指向任意一个存在的提交ID。下面就尝试人为更改游标。
1 | $ git reset --hard HEAD^ |
注意,上面的命令中使用了--hard
参数,会破坏工作区未提交的改动,要慎用。这条命令将master重置到了上一个老的提交上,来看下master文件内容是否更改了。
1 | $ cat .git/refs/heads/master |
果然master分支的引用文件的指向更改为前一次提交的ID了。而且通过下面的命令可以看到新添加的文件new-commit.txt
也丢失了。
1 | $ ls |
重置命令不仅可以重置到前一次提交,还可以直接使用提交ID重置到任何一次提交。
- 通过
git log
查询到最早的提交ID。
1 | $ git log --graph --oneline |
- 然后重置到最早的一次提交。
1 | $ git reset --hard d3e0684 |
-重置后会发现welcome.txt
也回退到了最原始版本库,曾经的修改都丢失了。
1 | $ cat welcome.txt |
使用重置命令很危险,会彻底的丢弃历史。那么还能够通过浏览提交历史的办法找到丢弃的提交ID,再使用重置命令恢复历史吗?
不可能!!!因为重置让提交历史也改变了。
1 | $ git log |
使用reflog挽救错误的重置
如果没有记下重置前master分支指向的提交ID,想要重置回原来的提交真的是一件麻烦的事情(去对象库中一个个地找)。幸好Git提供了一个挽救机制,
通过.git/logs
目录下日志文件记录了分支的变更。默认非裸版本库(带有工作区)都提供分支日志功能,这是因为带有工作区的八本库都有如下设置:
1 | $ git config core.logallrefupdates |
查看一下master分支的日志文件.git/logs/refs/heads/master
中的内容。下面命令显示了该文件的最后几行。
1 | $ tail -5 .git/logs/refs/heads/master |
可以看出这个文件记录了master分支指向的变迁,最新的改变追加到文件的末尾因此最后出现。最后一行可以看出因执行git reset --hard
命令,
指向的提交ID由a67c6f
改变为d3e0684
。
Git提供了一个git reflog
命令,对这个文件进行操作。使用show
子命令可以显示此文件的内容。
1 | $ git reflog show master | head -5 |
使用git reflog
的输出和直接查看日志文件最大的不同在于显示顺序不同,即最新改变放在了最前面显示,而且只显示每次改变的最终的SHA1哈希值。
还有个区别在于使用git reflog
的输出中还提供了一个方便易记的表达式:<refname>@{<n>}
。这个表达式的含义是引用<refname>
之前第
那么将引用master切换到两次变更之前的值,可以使用如下命令:
- 重置master为两次改变之前的值。
1 | $ git reset --hard master@{2} |
- 重置后工作区中文件
new-commit.txt
又回来了。
1 | $ ls |
- 提交的历史也回来了。
1 | $ git log --oneline |
此时如果再用git reflog
查看,会看到恢复master的操作也记录在了日志中。
1 | $ git reflog show master | head -5 |
深入了解git reset
命令
重置命令git reset
是Git最常用的命令之一,也是最危险,最容易误用的命令。用法如下:
1 | 用法一:git reset [-q] [<commit>] [--] <paths>… |
上面列出的用法中,
上面列出的两种用法的区别在于,第一种用法在命令中包含路径<paths>
。为了避免路径和引用(或者提交ID)同名而冲突,可以在<paths>
前用两个连续的短线作为分隔。
第一种用法不会重置引用,更不会改变工作区,而是用指定提交状态(
例如命令git reset HEAD <paths>
相当于取消之前执行的git add <paths>
命令时改变的暂存区。
第二种用法(不使用路径<paths>
的用法)则会重置引用。根据不同的选项,可以对暂存区或者工作区进行重置。
参照下面的版本库模型图,来看一看不同的参数对第二种重置语法的影响。
命令格式:git reset [–soft | –mixed | –hard] [
- 使用参数
--hard
,如:git reset --hard <commit>
会执行上图中的1、2、3全部的三个动作。即:- 替换引用的指向。引用指向新的提交ID。
- 替换暂存区。替换后,暂存区的内容和引用指向的目录树一致。
- 替换工作区。替换后,工作区的内容变得和暂存区一致,也和HEAD指向的目录树内容相同。
- 使用参数
--soft
,如:git reset --soft <commit>
,会执行上图中的操作1.即只更改引用的指向,不改变暂存区和工作区。 - 使用参数
--mixed
或者不使用参数(缺省为--mixed
),如:git reset <commit>
,会执行上图中的操作1和操作2。即更改引用的指向以及重置暂存区,但不改变工作区。
下面通过一些示例,看一下重置命令的不同用法。
命令:
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
保存了上次的提交日志)1
2$ git reset --soft HEAD^
$ git commit -e -F .git/COMMIT_EDITMSG- 命令:
命令:
git reset HEAD^
。工作区不改变,但是暂存区会回退到上一次提交之前,引用也会回退一次。命令:
git reset --mixed HEAD^
。同上。命令:
git reset --hard HEAD^
。彻底撤销最近的提交。引用会回退到前一次,而且工作区和暂存区都会回退到上一次提交的状态。自上一次以来的提交全部丢失。