前言

我们在git使用时,很可能会遇到git命令行弹出的一些消息,其中的一些术语我们可能并不知道,其中就包括了一个分离头指针(detached head)的消息。

分离头指针实践

假设我们在切换分支时,不小心输入了commit的id,就会出现分离头指针的警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ git branch -av
checkout 5bc7fdf rename test
* master 5bc7fdf [ahead 2] rename test
temp e1514cf test1
remotes/origin/master b390c28 add modified html css

G:\mygitea\GitLearn\learn01   master 
$ git log --oneline --graph --all
* e1514cf (temp) test1
* a1909d9 add 5bc7fdf file
* 5bc7fdf (HEAD -> master, checkout) rename test
* 012ae46 e1
* b390c28 (origin/master) add modified html css
* 58b4bfd add js
* 1d40e3a add css
* 1580123 add html images file
* 3cd7fd8 add readme
* df647fe Initial commit

G:\mygitea\GitLearn\learn01   master 
$ git checkout 012ae46
Note: switching to '012ae46'.
# 目前正处于分离头指针的状态
You are in 'detached HEAD' state.
# 你可以做一些变更,然后产生commit,你也可以把你产生的commit丢弃掉,这并不会对你任何分支文件造成影响
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 switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

git switch -c <new-branch-name>

Or undo this operation with:

git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 012ae46 e1

即在分离头指针情况下,你可以继续做开发,继续产生commit,而且不会影响其他分支。

本质上,分离头指针表示我们现在正工作在没有分支的情况下。如果你在分离头指针的情况下做了很多commit产生了很多变更,接着你又切换回分支上,此时你之前做的没有与分支挂钩的变更最后很可能会被git当作垃圾清除掉。

即,如果你想做变更,首先得与某个分支挂钩,在这个分支的基础上对分支进行变更。这样的commit,git是不会清除掉的。

分离头指针的实践场景

在想做一些变更,但这些变更都是尝试性变更,在做这些变更后发现结果并不好,你想把这些变更丢弃,则只需要切换到一个新的分支上就可以了。

下面是实践演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 G:\mygitea\GitLearn\learn01   HEAD detached at 012ae46 ± 
$ git status
HEAD detached at 012ae46 # 头指针是基于012xxx commit做的,没有挂靠任何分支
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: styles/style.css

no changes added to commit (use "git add" and/or "git commit -a")

G:\mygitea\GitLearn\learn01   HEAD detached at 012ae46 ± 
$ git add .

G:\mygitea\GitLearn\learn01   HEAD detached at 012ae46 ± 
$ git commit -m "change background color"
[detached HEAD 9fa98d6] change background color
1 file changed, 1 insertion(+), 1 deletion(-)

G:\mygitea\GitLearn\learn01   HEAD detached at 9fa98d6 
$ git log
commit 9fa98d6eb27abdf05064e5ced0c4400a6ca847b5 (HEAD)
Author: Jabari <innocenfox@gmail.com>
Date: Tue May 10 19:41:34 2022 +0800

change background color

可以看到,此时我们commit的HEAD并没有和任何分支挂靠,而我们在[[05git log查看版本历史]]那一节中的commit日志都是有和分支挂靠的。

此时,我们来做一个分离头指针切换分支后文件丢失的案例:checkout到master分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 G:\mygitea\GitLearn\learn01   HEAD detached at 9fa98d6 
$ git checkout master
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

9fa98d6 change background color

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

git branch <new-branch-name> 9fa98d6

Switched to branch 'master'
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)

可以看到,此时有一个报警,询问你是否需要创建一个新分支来存储在分离头指针状态下的commit记录。如果我们不创建,则文件会丢失,我们gitk --all查看下,我们在分离头指针的commit是否会在历史树中出现。

1
2
 G:\mygitea\GitLearn\learn01   master 
$ gitk --all

可以看到分支树中并没有我们之前在分离头指针状态提交的commit。即在git眼中,这些没和分支绑定的commit都是不重要的,都是日后要清除的。

接下来,我们为先前在分离头指针创建的文件进行创建分支:

1
2
3
4
5
 G:\mygitea\GitLearn\learn01   master 
$ git branch fix_css 9fa98d6 # 给分离头指针所处的commit分配一个名为fix_css的branch

G:\mygitea\GitLearn\learn01   master 
$ gitk --all

此时,我们再来看看:

可以看到,此时commit分支就出现了。

总结

如果临时想基于某个commit做变更,试试新方案是否可行,就可以采用分离头指针的方式。测试后发现新方案不成熟,直接reset回其他分支即可。省却了建、删分支的麻烦了。

git checkout commitId:会出现分离头指针的情况,这种情况下比较危险,因为这个时候你提交的代码没有和分支对应起来,当切换到其他分支的时候(比如master分支),容易丢失代码; 但是分离头指针也有它的应用场景,就是在自己做尝试或者测试的时候可以分离头指针,当尝试完毕没有用的时候可以随时丢弃,但是如果觉得尝试有用,那么可以新建一个分支,使用 git branch <新分支的名称> commitId

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 G:\mygitea\GitLearn\learn03 
$ git log --oneline
41f7a2f (HEAD -> master) Second commit
dab1693 Second commit
8653890 First commit

G:\mygitea\GitLearn\learn03 
$ git checkout dab1693
Note: switching to 'dab1693'.
You are in 'detached HEAD' state. You can look around, make experimental...

G:\mygitea\GitLearn\learn03 
$ git status
HEAD detached at dab1693
nothing to commit, working tree clean

G:\mygitea\GitLearn\learn03 
$ vim readme

G:\mygitea\GitLearn\learn03   HEAD detached at dab1693 ± 
$ git add . && git commit -m"older-x-young"
[detached HEAD 8fbb99f] older-x-young
1 file changed, 1 insertion(+)

G:\mygitea\GitLearn\learn03   HEAD detached at 8fbb99f 
$ git branch older-x-young 8fbb99f

G:\mygitea\GitLearn\learn03   master 
$ git log --all --oneline --graph
* 8fbb99f (older-x-young) older-x-young
| * 41f7a2f (HEAD -> master) Second commit
|/
* dab1693 Second commit
* 8653890 First commit


测试git diff HEAD HEAD~,参考[[10进一步理解HEAD和branch]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
 G:\mygitea\GitLearn\learn03   master 
$ git log --all --oneline --graph
* 8fbb99f (older-x-young) older-x-young
| * 41f7a2f (HEAD -> master) Second commit # 当前所处分支
|/
* dab1693 Second commit
* 8653890 First commit

G:\mygitea\GitLearn\learn03   master 
$ cat readme
This is the oldest branch.
This is the older branch.
This is the young branch.

G:\mygitea\GitLearn\learn03   master 
$ git diff HEAD HEAD~
diff --git a/readme b/readme
index a5a8dc1..b7c8a93 100644
--- a/readme
+++ b/readme
@@ -1,3 +1,2 @@
This is the oldest branch.
This is the older branch.
-This is the young branch.

G:\mygitea\GitLearn\learn03   master 
$ git diff HEAD HEAD~1
diff --git a/readme b/readme
index a5a8dc1..b7c8a93 100644
--- a/readme
+++ b/readme
@@ -1,3 +1,2 @@
This is the oldest branch.
This is the older branch.
-This is the young branch.

G:\mygitea\GitLearn\learn03   master 
$ git diff HEAD HEAD~~
diff --git a/readme b/readme
index a5a8dc1..fbfe1a7 100644
--- a/readme
+++ b/readme
@@ -1,3 +1 @@
This is the oldest branch.
-This is the older branch.
-This is the young branch.

G:\mygitea\GitLearn\learn03   master 
$ git diff HEAD HEAD~2
diff --git a/readme b/readme
index a5a8dc1..fbfe1a7 100644
--- a/readme
+++ b/readme
@@ -1,3 +1 @@
This is the oldest branch.
-This is the older branch.
-This is the young branch.

G:\mygitea\GitLearn\learn03   master 
$ git diff HEAD HEAD~3
fatal: ambiguous argument 'HEAD~3': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

通过上面测试可知,HEAD~可以追溯到同分支上的起始到当前的所有父节点,但是无法追溯到分支上的节点。