黑帽联盟

标题: Git之旅(8):HEAD是啥? [打印本页]

作者: 定位    时间: 2020-4-3 15:00
标题: Git之旅(8):HEAD是啥?
上一篇文章中,我们提到了一个概念,那就是HEAD,但是我们并没有详细的去解释HEAD是个什么东西?这篇文章我们就来聊聊什么是HEAD

为了方便,我仍然使用之前的测试仓库进行测试,你也可以随意的创建一个用于测试的git仓库,然后创建几条分支,以便测试时使用。

我们先来回顾一下前文中测试仓库的状态,如下:
注:下图中使用了前文中创建的allbranch视图
1.png
如上图所示,我们现在有两个分支,master分支和test分支,从上图可以看出,目前我们处于黄色的提交,也就是test分支的"add D to m2",如果我们想要切换回master分支,则可以使用前文中总结的如下命令
/d/workspace/git/test_repo1 (test)
$ git checkout master
Switched to branch 'master'

/d/workspace/git/test_repo1 (master)
$
如上述信息所示,我们已经从test分支切换到了master分支。
假设,我们现在关闭git bash和工作目录,当我们下次再次进入工作目录并且打开git bash时,仍然会显示为当前处于master分支,因为我们上次关闭工作目录之前,已经切换到了master分支,当然,如果你之前处于test分支,那么当你再次打开工作空间,仍然会显示你处于test分支。
那么问题来了,git是怎么知道我们当前该处于哪个分支呢?
git其实就是靠HEAD知道我们该处于哪个分支的,你可以把HEAD理解成一个指针,HEAD指针通常会指向一个分支(或者说指向一个分支指针),分支和分支指针的概念我们在前文中已经说明过,此处不再赘述,你可以把HEAD也理解成一个指针,这个指针通常指向一个分支指针,这样说不太直观,不如看下图(仍然在前文的图片的基础上进行修改):
2.png
如上图所示,由于我们当前处于master分支,所以,HEAD这个指针指向了master分支指针,如果我们现在检出test分支,那么HEAD指针就会指向test指针,也就是说,当我们从master分支检出到test分支时,HEAD指针会由上图中的状态变成下图中的状态:
3.png
所以说,git只要找到HEAD,就能找到我们当前所处的分支(因为我们在切换分支时,会将HEAD指向所在的分支)。

我们可以直观的查看当前仓库的.git目录中的HEAD文件的内容,你会发现,其实.git/HEAD文件的内容就是HEAD指针所指向的分支,如下所示:
/d/workspace/git/test_repo1 (master)
$ cat .git/HEAD
ref: refs/heads/master
从上述返回信息可以看出,当前HEAD指针指向了另一个文件,这个文件就是.git/refs/heads/master,那么我们顺藤摸瓜,看看.git/refs/heads/master这个文件的文件内容
/d/workspace/git/test_repo1 (master)
$ cat .git/refs/heads/master
7406a10efcc169bbab17827aeda189aa20376f7f
可以看到,这个文件的内容是一串哈希码,而这个哈希码正是master分支上最新的提交所对应的哈希码。
聪明如你,肯定已经看出来了,.git/HEAD文件和.git/refs/heads/master文件不正是上图中的HEAD指针和master分支指针么,没错,就是这样的,只不过,在Git中,这些代表了上图中"指针"的文件还有另外一个名字,它们被称之为"引用" (references 或者 refs),其实都是一样的东西,不用纠结于它们的名字。

为了证明我们的想法,我们切换几次分支, 看看.git/HEAD文件内容的变化
/d/workspace/git/test_repo1 (master)
$ git checkout test
Switched to branch 'test'

/d/workspace/git/test_repo1 (test)
$ cat .git/HEAD
ref: refs/heads/test

/d/workspace/git/test_repo1 (test)
$ git checkout master
Switched to branch 'master'

/d/workspace/git/test_repo1 (master)
$ cat .git/HEAD
ref: refs/heads/master
嗯嗯,看来是没错了,跟我们想的一样,HEAD指针通常指向我们所在的分支(的分支指针)。

前文中说过,当我们在某个分支上创建新的提交时,分支指针总是会指向当前分支的最新提交。
而刚才又说过,HEAD指针通常会指向当前所在分支的分支指针。
那么,结合上述两点,我们可以得出如下结论:
HEAD指针 --------> 分支指针 --------> 最新提交
也就是说,通常情况下,HEAD指针总是通过分支指针,间接的指向了当前分支的最新提交。
单纯的靠上述文字描述可能不够直观,不如通过实际的操作来验证一下,操作如下:
首先,我们切换回test分支,看看当前HEAD指针和分支指针的指向,如下:
/d/workspace/git/test_repo1 (master)
$ git checkout test
Switched to branch 'test'

/d/workspace/git/test_repo1 (test)
$ cat .git/HEAD
ref: refs/heads/test

/d/workspace/git/test_repo1 (test)
$ cat .git/refs/heads/test
30d80b030d1a960bd90f020be2a3efb657c978e9
从上述命令可以看出,切换到test分支以后,HEAD指针指向了test分支指针,而test分支指针指向了30d80b0这个提交,如下图所示
4.png
那么现在,我们来尝试在test分支上创建一个新的提交,看看HEAD指针和test分支指针会有哪些变化,操作如下:
/d/workspace/git/test_repo1 (test)
$ echo E >> m2

/d/workspace/git/test_repo1 (test)
$ git add m2

/d/workspace/git/test_repo1 (test)
$ git commit -m "add E to m2"
[test 35cff8c] add E to m2
1 file changed, 1 insertion(+)

/d/workspace/git/test_repo1 (test)
$ cat .git/HEAD
ref: refs/heads/test

/d/workspace/git/test_repo1 (test)
$ cat .git/refs/heads/test
35cff8cabb71d553ab1abceaf33fa5a046a17bdb
如上所示,我们在test分支上创建了一个新的提交35cff8c,然后查看了.git/HEAD,发现HEAD指针仍然指向了test分支指针,而test分支指针已经指向了最新创建的提交,也就是35cff8c,如下图所示:
5.png
所以说,通常情况下,HEAD指针总是指向了当前分支的最新提交(通过分支指针间接的指向)。


什么是分离头
刚才一直在说,"通常情况下",HEAD总是指向当前所在的分支(即指向当前分支的分支指针)。
既然有"通常情况下",肯定还有"不是那么通常的情况",这种所谓的"不是那么通常的情况",就是我们现在要描述的情况,这种情况被称之为"分离头"(detached HEAD),那么" 分离头"是什么意思呢?其实很简单,分离头的状态其实就是HEAD指针没有指向分支指针,而是直接指向了某个提交,比如下图中的情况
6.png
如上图所示HEAD指针没有指向任何一个分支,而是直接指向了上图中的C3这个提交,这种情况就是"分离头"(detached HEAD)的情况,从字面上理解," 分离头"或者称之为" 头分离"的这种情况就是HEAD指针(头指针)和分支指针分开了,HEAD指针和分支指针分开后,指向了某个提交。
上图的HEAD指针直接指向了C3这个提交,上图只是为了示例,所以随意的指向了一个提交,HEAD指针直接指向任何一个提交的情况都属于分离头,所以,不要纠结于上图的HEAD指针指向了哪个提交。

那么我们进行什么操作时才会进入到分离头的状态呢?很简单,当我们直接检出一个提交(而不是检出某个分支),就可以进出分离头状态,在之前的操作中,我们使用git checkout命令都是检出某个分支,那么现在,我们直接在git checkout命令后面指定某个提交的哈希码,即可进入到分离头的状态。下面我们来动手操作一下,不过在开始之前,我们先来看看各个分支以及HEAD指针的状态,使用"git log --oneline --all --graph"命令可以在命令行中以字符的形式尽量接近图形化的方式展示分支,如下:
7.png
当然,使用上述方式和使用gitk图形化的方式都能查看分支,但是我个人还是习惯使用gitk图形化工具查看分支,毕竟图形化工具相对美观一些,在后文中还是会尽可能的使用图形化的,不过从上图可以更加直观的看出,当前我们处于test分支,HEAD指针指向了test分支。此时,运行gitk命令,看到的分支图如下(下图中使用了前文中创建的allbranch视图):
8.png
在上图中,你可以直观的看到test分支指针和master分支指针,但是你看不到HEAD指针,其实,上图中的黄色提交就是HEAD指针所在的位置(或者你也可以把HEAD指针理解成一个黄色的圆球),也就是说,从上图可以看出,HEAD指针现在指向了test分支。

现在,我们想要做的是造成所谓的" 分离头"状态,也就是说,我们想让HEAD指针指向某个提交,而不是指向某个分支指针。
我们随便检出到某个提交,都可以进入分离头状态,操作如下:
/d/workspace/git/test_repo1 (test)
$ git checkout cbd3348
Note: checking out 'cbd3348'.

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 cbd3348 add 2 in m1

/d/workspace/git/test_repo1 ((cbd3348...))
$
从上述信息可以看出,我检出了哈希码为cbd3348的提交,也就是"add 2 in m1"对应的提交,当我执行上述命令后,git的返回信息也在提示我们,You are in 'detached HEAD' state.(你现在处于'分离头'状态),我们检出了'cbd3348'这个提交,现在,当前目录就处于'cbd3348'这个提交对应的状态,也就是说,目录中所有文件的内容都与'cbd3348'这个状态对应,执行上述命令后,命令提示符中也显示了当前提交的哈希码。
那么现在,我们再次执行 "git log --oneline --all --graph"命令,看看现在HEAD指针所处的位置,如下:
9.png
从上图可以看出,HEAD指针目前没有指向任何一个分支指针,而是直接指向了'cbd3348'这个提交,这就是所谓的分离头状态。
如果此时你使用gitk图形化界面,会发现'cbd3348'这个提交变成了黄色,如下:
10.png
也就是说,HEAD指针目前处于'cbd3348'这个提交,并没有指向任何分支。
如果此时在命令行中执行'git status'命令,同样会提示你,你当前处于分离头状态,如下
/d/workspace/git/test_repo1 ((cbd3348...))
$ git status
HEAD detached at cbd3348
nothing to commit, working tree clean
分离头状态的使用场景
那么分离头状态有什么用呢?该怎么用呢?
其实,从上文的返回信息中就能找到答案。在我们执行' git checkout cbd3348'这条命令时,git就返回了很长一段信息,返回信息如下:
Note: checking out 'cbd3348'.

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 cbd3348 add 2 in m1
注:我先大概的描述一下上述返回信息,后面会有实际的示例,所以,不明白上述返回信息的意思也不用纠结,看到最后自然就会明白。
从上述返回信息我们可以得知,我们当前检出了'cbd3348'这个提交,现在我们处于分离头的状态,git建议我们,在分离头的状态下,我们可以随便看看,可以按照我们的想法,对当前目录中的文件进行一些实验性的修改,并且将这些实验性的修改创建成一些提交(其实这些提交会组成一条匿名分支),如果你最后后悔了,觉得实验不成功,修改后的结果并不是你想要的,那么我们可以在不影响任何其他分支和提交的情况下,丢弃这些实验性的提交(丢弃这条匿名分支),如果你觉得这些实验性的提交让你很满意,那么你就可以创建一个新的分支(其实是给这个匿名分支一个固定的名字),来永久性的保存这些提交。

只是通过语言描述的方式,可能并不是特别容易理解分离头状态的用处,不如,我们就在分离头的状态下随便做些修改,然后创建一些提交,通过实际操作来认识分离头状态的使用方法吧,操作如下:
首先,看看当前目录中各个文件的状态
/d/workspace/git/test_repo1 ((cbd3348...))
$ git status
HEAD detached at cbd3348
nothing to commit, working tree clean

/d/workspace/git/test_repo1 ((cbd3348...))
$ cat m1
1
2

/d/workspace/git/test_repo1 ((cbd3348...))
$ cat m2
A
由于刚才我们直接检出了'cbd3348'这个提交,所以,我们处于分离头状态,而且当前仓库中各个文件的内容也回到了'cbd3348'这个提交对应的状态,假设,此时我想对m1这个文件进行一些修改,于是,我进行了如下一系列的操作
/d/workspace/git/test_repo1 ((cbd3348...))
$ echo 3test >> m1

/d/workspace/git/test_repo1 ((cbd3348...))
$ git add m1

/d/workspace/git/test_repo1 ((cbd3348...))
$ git commit -m "add 3test in m1"
[detached HEAD c22af40] add 3test in m1
1 file changed, 1 insertion(+)

/d/workspace/git/test_repo1 ((c22af40...))
$ echo 4test >> m1

/d/workspace/git/test_repo1 ((c22af40...))
$ git add m1

/d/workspace/git/test_repo1 ((c22af40...))
$ git commit -m "add 4test in m1"
[detached HEAD dca15df] add 4test in m1
1 file changed, 1 insertion(+)

/d/workspace/git/test_repo1 ((dca15df...))
$
从上述命令可以看出,我先在m1文件中加了一行"3test",并且为这次修改创建了一个新的提交,新提交的哈希码是c22af40,当新提交c22af40被创建以后,命令提示符中的哈希码就变成了c22af40,然后,我又在m1文件中加了一行"4test",并且为这次修改也创建了一个新提交,新提交的哈希码为dca15df,同样,这个提交被创建以后,命令提示符中的哈希码就变成了最新提交的哈希码,好了,现在我们已经在分离头的状态下创建了两个新提交,那么此时,我们来看看各个分支的状态以及HEAD指针的位置,打开gitk图形化界面,如下:
注:此处使用"gitk --all"命令打开gitk图形化界面,"--all"参数表示显示所有分支,你也可以打开gitk以后,选择前文中保存过的视图来查看所有分支。
11.png
从上图可以看出,HEAD指针已经指向了最新的提交,这个提交就是我们刚才创建的"add 4test in m1",而且从上图可以看出,从"add 2 in m1"分出了一条分支,"add 2 in m1"是一个分叉点,这个分叉点就是上文中的'cbd3348'提交,我们直接检出了'cbd3348'这个提交,进入到了分离头的状态,然后在分离头的状态下,创建了两个新提交,这两个新提交就是"add 3test in m1"和"add 4test in m1",这两个新提交与分叉点之前的提交组成了一条匿名分支,没有任何一个分支标签指向这个匿名分支。

我们现在有两个选择,如下:
选择一:丢弃这个匿名分支
选择二:保留这个匿名分支
上述两种选择分别对应了不同的操作,我们后续再进行实际的示例。

看到这里,我似乎明白了分离头的使用场景,说白了,分离头的状态不就是让我们可以随意的检出某个提交,然后基于这个提交创建一些新提交么,这些新创建的提交是偏实验性质的,为什么这么说呢?因为我们可能会保存这些提交,也有可能放弃这些提交,你可以根据最终的结果判断是否保存这些提交,这些新创建的提交可以理解成一条匿名分支。

好了,刚才说过,我们可以选择放弃这个匿名分支,也可以选择保留它,那么具体该怎么操作呢?一个一个聊

先说说怎样放弃这些提交,如果你对这些实验性的提交不满意,你可以直接检出到任何一个别的分支,就相当于放弃了这些提交,就是这么简单。比如,我们当前处于分离头状态,现在我们直接检出到test分支,示例操作如下:
/d/workspace/git/test_repo1 ((dca15df...))
$ git checkout test
Warning: you are leaving 2 commits behind, not connected to
any of your branches:

dca15df add 4test in m1
c22af40 add 3test in m1

If you want to keep them by creating a new branch, this may be a good time
to do so with:

git branch <new-branch-name> dca15df

Switched to branch 'test'

/d/workspace/git/test_repo1 (test)
如上例所示,当我们直接从分离头状态检出到其他分支时,git会提示我们,你落下了2个提交在后面,这两个提交没有关联到任何分支,这两个提交是......(你创建的提交信息),如果你想要保存它们,那么你可以......(git在这时其实就已经给出了保留这个匿名分支的命令,不过这个命令我们放到后面再聊),如上例所示,我们从分离头状态已经检出到了别的分支(test分支),这就相当于放弃了那些实验性的提交(匿名分支),此时运行" gitk --all"命令,查看HEAD以及分支的状况,如下图所示
12.png
你会发现,那条匿名分支已经不见了,因为没有任何一个指针指向那些提交,所以,那些实验性的提交会在过一段时间后被git的垃圾回收机制清除,因为我们不想要它们了,所以不用理会它们,它们就好像不存在一样,也不会对其他分支和提交造成影响。上述操作就是丢弃这些实验性提交的方法,是不是很简单?

如果我们想要永久的保存那些在分离头状态下创建的提交该怎么办呢?
其实也很简单,我们只需要将这些提交创建成一条新的分支就行了(或者也可以理解成为匿名分支命名),为了方便演示,我再次检出到dca15df这个提交(这个提交是之前在演示分离头状态时创建的最新的提交),检出dca15df提交是为了回到之前的分离头状态,以便演示怎样在分离头的状态下保存那些实验性的提交,检出dca15df提交的命令如下:
/d/workspace/git/test_repo1 (test)
$ git checkout dca15df
Note: checking out 'dca15df'.

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 dca15df add 4test in m1

/d/workspace/git/test_repo1 ((dca15df...))
从上述命令可以看出,当我们从test分支检出到'dca15df'提交以后,git又从"通常状态"进入到了"分离头状态",并且git再次提示我们,你可以通过" git checkout -b <new-branch-name>"命令创建一个新的分支以便来保存这些在分离头状态下创建的提交,假设,我们想要创建一个名为newtest的分支来保存这些提交,那么可以直接执行如下命令:
git checkout -b newtest
执行上述命令后,新创建的newtest分支指针就会指向我们当前所在的提交(即dca15df提交),这样,我们就能永久的保存这些提交了。
当然,除了" git checkout -b newtest"命令,你也可以使用如下类似的命令来创建一个分支来保存分离头状态下创建的提交,比如如下命令
git branch newtest dca15df
上述命令的意思是在dca15df提交的位置创建一个名为newtest的分支(此命令不会自动检出到新创建的分支,会停留在原位置,但是新的分支指针会在指定的提交处创建),细心如你肯定已经发现了,在之前的示例过程中,当我们从dca15df提交的分离头状态检出到test分支时,git同样会提示你使用"git branch <new-branch-name> dca15df"命令来保存分离头状态下创建的那些提交,其实,命令虽然不同,但是本质上的目标都是创建一个分支来保存分离头状态下的提交(换一种说法就是给分离头状态下产生的匿名分支命名)。

说了这么多,你肯定明白了分离头指针的使用场景,当你想要基于某个提交进行一些实验或者测试,可以直接检出这个提交,立马在上面开始实验/测试工作,如果结果满意,就保留,如果结果不满意,就丢弃。你可能会说,我直接基于指定的提交创建一个新的分支,然后在新的分支上进行实验不是也可以吗?是的,完全可以,没有任何问题,只是,如果实验结果不满意,你可能还想要删除这条用于实验的分支,而使用分离头,就可以先实验,再判断是否保留这些提交,避免了在不满意的情况下创建分支或者删除分支的操作。


随便聊聊
还记得我们之前使用git reflog查看过git的提交历史么?其实,git reflog中记录的就是各种指针的常见操作,对分支指针、HEAD指针等各种" 指针"(即"引用")的常见操作都会记录在这里,所以,在了解了分支指针和HEAD指针的概念以后,有没有觉得更加深入的理解了git reflog呢?


好了,这篇文章就先写到这里,希望能够对你有所帮助,加油~共勉~






欢迎光临 黑帽联盟 (https://bbs.cnblackhat.com/) Powered by Discuz! X2.5