第四节《Git检出》
使用过Git的朋友们都谁知道git reset可以达到重置效果,不知道的小伙伴们可以看下上一篇博客,重置命令的一个用途就是修改引用的游标指向,实际上在执行重置命令的时候没有使用任何参数对所要重置的分支名进行设置,这是因为重置命令实际上所针对的是头指针HEAD,之所以没有改变头指针HEAD的内容,是因为HEAD指向了一个引用refs/heads/master,所以重置命令体现为分支“游标”的变更,HEAD本身一直指向的是refs/heads/master,并没有在重置时改变。那么git checkout命令实质就是修改HEAD本身的指向,该命令不会影响分支“游标”。
HEAD可以理解为“头指针”,当执行提交时,HEAD指向的提交将作为新提交的父提交,我们看一下当前的HEAD指向:
[root@git demo]# cat .git/HEAD
ref: refs/heads/master
可以看出HEAD指向了分支master。现在执行git branch会看到当前出于master分支:
[root@git demo]# git branch -v
* master 8ea40df touch new-commit.txt
现在使用git checkout命令检出该ID的父提交看看是什么效果?
[root@git demo]# git checkout 8ea40df^
Note: checking out '8ea40df^'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b new_branch_name
HEAD is now at 28353c5... version 2 welcome
翻译:正在检出'8ea40df^'
您现在处于‘分离头指针’状态。您可以检查、测试和提交,而不影响任何分支。
通过执行另外一个checkout检出指令会丢弃在此状态下的修改和提交。
如果想保留在此状态下的修改和提交,使用-b参数调用checkout检出指令以创建新的跟踪分支,如:
git checkout -b new_branch_name
头指针现在指向28353c5...,提交说明为version 2 welcome。
那么什么叫分离头指针状态呢?就这个问题我们来看一下HEAD的内容。
[root@git demo]# cat .git/HEAD
28353c5910d0e97fd70ee1c9cd0e4973a66618b6
原来"分离头指针"状态指的就是HEAD头指针指向了一个具体提交的ID,而不是一个分支。
查看最新提交的reflog也可以看到当针对提交执行git checkout命令时,HEAD头指针被更改了:由指向master分支变成了指向一个提交ID。
[root@git demo]# git reflog -l
28353c5 HEAD@{0}: checkout: moving from master to 8ea40df^ //注意这个是HEAD头指针的变迁记录,而不是master分支。
我们再来查看一下HEAD和master对应的提交ID,会发现现在它们指向的不一样:
[root@git demo]# git rev-parse HEAD master
28353c5910d0e97fd70ee1c9cd0e4973a66618b6
8ea40df385b3dfe69bee04928ca65f9a7b049fb6
前一个是HEAD头指针的指向,后一个是master分支的指向。而且还可以看到执行git checkout命令与执行git reset命令不同,分支的指向并没有变动,仍旧指向原来提交的ID。
现在版本库的HEAD是指向28353c5提交的。再做一次提交,HEAD会如何变化?
<1>先做一次修改:创建一个新文件dateched-commit.txt,添加到暂存区中。
[root@git demo]# touch dateched-commit.txt
[root@git demo]# git add dateched-commit.txt
<2>查看状态,会发现其中有“当前不处于任何分支”的字样,显然这是因为HEAD处于“分离头指针”模式。
[root@git demo]# git status
# Not currently on any branch.
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: dateched-commit.txt
<3>执行提交。在提交输出中也会出现[dateched HEAD...]的标识,这也是对用户的警告。
[root@git demo]# git commit -m "commit in dateched HEAD mode"
[detached HEAD 3beb511] commit in dateched HEAD mode
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dateched-commit.txt
<4>此时头指针指向了新的提交。
[root@git demo]# cat .git/HEAD
3beb511e60196f91664b8b67aefdaf42867ee01c
<5>再查看一下日志会发现新的提交是建立在之前的提交基础上的。
[root@git demo]# git log --graph --pretty=oneline
* 3beb511e60196f91664b8b67aefdaf42867ee01c commit in dateched HEAD mode
* 28353c5910d0e97fd70ee1c9cd0e4973a66618b6 version 2 welcome
* f9d075c2a02c6faf14efeec5a7b1c70f00c7d126 initwelcome
记下新的提交ID(3beb511),然后以master分支名作为参数执行checkout命令,会切换到master分支上。
<1>切换到master分支上,再没有之前大段的文字警告。
[root@git demo]# git checkout master
Previous HEAD position was 3beb511... commit in dateched HEAD mode
Switched to branch 'master'
<2>因为HEAD头指针重新指向了分支,而不是处于分离头指针模式。
[root@git demo]# cat .git/HEAD
ref: refs/heads/master
<3>切换之后,之前本地创建的dateched.commit.txt不见了。
[root@git demo]# ls
new-commit.txt welcome.txt
<4>看一下提交日志应该也不存在了。
[root@git demo]# git log --graph --pretty=oneline
* 8ea40df385b3dfe69bee04928ca65f9a7b049fb6 touch new-commit.txt
* 28353c5910d0e97fd70ee1c9cd0e4973a66618b6 version 2 welcome
* f9d075c2a02c6faf14efeec5a7b1c70f00c7d126 initwelcome
那么刚才的提交还存在于版本库中吗,看一下刚才记下的提交ID。
[root@git demo]# git show 3beb511
commit 3beb511e60196f91664b8b67aefdaf42867ee01c
Author: Gao Yue <dgy@89mc.com>
Date: Sun Aug 27 20:06:48 2017 +0800
commit in dateched HEAD mode
diff --git a/dateched-commit.txt b/dateched-commit.txt
new file mode 100644
index 0000000..e69de29
可以看出这个提交现在仍存在于版本库中。由于这个提交没有被任何分支跟踪到,因此并不能保证这个提交会永久存在。实际上当reflog中含有该提交的日志过期后,这个提交随时都会从版本库中彻底清除。
在“分离头指针”模式下进行的测试提交除了使用提交ID(3beb511)访问之外,不能通过master分支或其他引用访问到。如果这个提交是master分支所需要的呢,我们看看应该怎么做。答案是使用合并操作(git merge)。
下面的操作会把提交“3beb511”合并到master分支中来:
<1>确定当前出于master分支。
[root@git demo]# git branch -v
* master 8ea40df touch new-commit.txt
<2>执行合并操作
[root@git demo]# git merge 3beb511
Merge made by recursive.
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dateched-commit.txt
<3>工作区中多了一个dateched-commit.txt文件
[root@git demo]# ls
dateched-commit.txt new-commit.txt welcome.txt
<4>查看日志,会看到不一样的分支图。
[root@git demo]# git log --graph --pretty=oneline
* c0f4bf10925743f95149b5fe4ee3f239ef3c110d Merge commit '3beb511'
|\
| * 3beb511e60196f91664b8b67aefdaf42867ee01c commit in dateched HEAD mode
* | 8ea40df385b3dfe69bee04928ca65f9a7b049fb6 touch new-commit.txt
|/
* 28353c5910d0e97fd70ee1c9cd0e4973a66618b6 version 2 welcome
* f9d075c2a02c6faf14efeec5a7b1c70f00c7d126 initwelcome
<5>仔细看看最新的提交,会看待这个提交有两个父提交,这就是合并的奥秘所在。
[root@git demo]# git cat-file -p HEAD
tree c79ab951fd87acc40d0efec20b401ceef4183736
parent 8ea40df385b3dfe69bee04928ca65f9a7b049fb6
parent 3beb511e60196f91664b8b67aefdaf42867ee01c
author Gao Yue <dgy@89mc.com> 1503839183 +0800
committer Gao Yue <dgy@89mc.com> 1503839183 +0800
Merge commit '3beb511'
检出命令(git checkout)是Git最常用的命令之一,同时也是一个很危险的命令,因为这个命令会重写工作区,检出命令用法如下:
<1>git checkout [-q] [<commit>] [--] <paths>...
<2>git checkout [<branch>]
<3>git checkout [-m] [[-b|--orphan] <new_branch>] [<start_point>]
第一种和第二种区别在于,第一种用法在命令中包含路径<paths>。为了避免路径和引用同名而发生冲突,可以在<paths>前用两个连续的短线作为分割。
第一种用法的<commit>是可选项,如果省略则相当于从暂存区进行检出。这个和重置命令大不相同:重置的默认值是HEAD,而检出的默认值是暂存区。因此重置一般用于重置暂存区,而检出命令主要是覆盖工作区。
第一种用法不会改变HEAD头指针,主要是用于指定版本的文件覆盖工作区中对应的文件。如果省略<commit>,则会用暂存区的文件覆盖工作区的文件,否则用指定提交中的文件覆盖暂存区和工作区中对应的文件。
第二种用法则会改变HEAD头指针。之所以后面的参数写作<branch>,是因为只有HEAD切换到一个分支才可以对提交进行跟踪,否则仍然会进入“分离头指针”的状态。在“分离头指针”状态下的提交不能被引用关联到,从而可能丢失。所以用法二最主要的作用就是切换分支。如果省略<branch>则相当于对工作区进行状态检查。
第三种用法主要是创建和切换到新的分支,新的分支从<start_point>指定的提交开始创建。新分支和我们熟悉的master分支没有什么实质的不同,都是在refs/heads命名空间下的引用。
下面通过一些示例来具体看下检出命令的不通用法:
<1>命令:git checkout branch //检出branch分支,更新HEAD以指向branch分支,以及用branch指向的树更新暂存区和工作区。
<2>命令:git checkout //汇总显示工作区、暂存区与HEAD的差异。
<3>命令:git checkout HEAD //同上
<4>命令:git checkout -- filename //用暂存区中filename文件来覆盖工作区中的filename文件,相当于取消自上次执行git add filename以来的本地修改。
<5>命令:git checkout branch -- filename //维持HEAD指向不变,用branch所指向的提交中的filename替换暂存区和工作区中相应的文件