0%

2022-03-28-Git detached head 分析

博客迁移:不恰饭的小站
https://blog.buqia.fun/2022/03/28/2022-03-28-Git%20detach%20head%20%E5%88%86%E6%9E%90/

1. Git detached head 的含义#

detached head 是一种 HEAD 指针指向了某一个具体的 commit id,而不是分支名称(master、…)的情况。

2. Git detached head 的产生、操作与消亡#

  1. 产生:
    git checkout 到一个 commit ID

  2. 操作:

    1. git checkout -b foo (1)
      creates a new branch foo, which refers to commit f, and then updates HEAD to refer to branch foo. In other words, we’ll no longer be in detached HEAD state after this command.

    2. git branch foo (2)
      similarly creates a new branch foo, which refers to commit f, but leaves HEAD detached.

    3. git tag foo (3)
      creates a new tag foo, which refers to commit f, leaving HEAD detached.

  3. 消亡:
    detached head 会由 Git 垃圾回收机制回收

3. Git detached head 的 man 手册#

HEAD normally refers to a named branch (e.g. master). Meanwhile, each branch refers to a specific commit. Let’s look at a repo with three commits, one of them tagged, and with branch master checked out:

1
2
3
4
5
6
7
           HEAD (refers to branch 'master')
|
v
a---b---c branch 'master' (refers to commit 'c')
^
|
tag 'v2.0' (refers to commit 'b')

When a commit is created in this state, the branch is updated to refer to the new commit. Specifically, git commit creates a new commit d, whose parent is commit c, and then updates branch master to refer to new commit d. HEAD still refers to branch master and so indirectly now refers to commit d:

1
2
3
4
5
6
7
8
$ edit; git add; git commit
HEAD (refers to branch 'master')
|
v
a---b---c---d branch 'master' (refers to commit 'd')
^
|
tag 'v2.0' (refers to commit 'b')

It is sometimes useful to be able to checkout a commit that is not at the tip of any named branch, or even to create a new commit that is not referenced by a named branch. Let’s look at what happens when we checkout commit b (here we show two ways this may be done):

1
2
3
4
5
6
7
8
9
10
$ git checkout v2.0  # or
$ git checkout master^^

HEAD (refers to commit 'b')
|
v
a---b---c---d branch 'master' (refers to commit 'd')
^
|
tag 'v2.0' (refers to commit 'b')

Notice that regardless of which checkout command we use, HEAD now refers directly to commit b. This is known as being in detached HEAD state. It means simply that HEAD refers to a specific commit, as opposed to referring to a named branch. Let’s see what happens when we create a commit:

1
2
3
4
5
6
7
8
9
10
11
12
$ edit; git add; git commit

HEAD (refers to commit 'e')
|
v
e
/

a---b---c---d branch 'master' (refers to commit 'd')
^
|
tag 'v2.0' (refers to commit 'b')

There is now a new commit e, but it is referenced only by HEAD. We can of course add yet another commit in this state:

1
2
3
4
5
6
7
8
9
10
11
12
$ edit; git add; git commit

HEAD (refers to commit 'f')
|
v
e---f
/

a---b---c---d branch 'master' (refers to commit 'd')
^
|
tag 'v2.0' (refers to commit 'b')

In fact, we can perform all the normal Git operations. But, let’s look at what happens when we then checkout master:

1
2
3
4
5
6
7
8
9
10
$ git checkout master

HEAD (refers to branch 'master')
e---f |
/ v

a---b---c---d branch 'master' (refers to commit 'd')
^
|
tag 'v2.0' (refers to commit 'b')

It is important to realize that at this point nothing refers to commit f. Eventually commit f (and by extension commit e) will be deleted by the routine Git garbage collection process, unless we create a reference before that happens. If we have not yet moved away from commit f, any of these will create a reference to it:

1
2
3
$ git checkout -b foo (1)
$ git branch foo (2)
$ git tag foo (3)

creates a new branch foo, which refers to commit f, and then updates HEAD to refer to branch foo. In other words, we’ll no longer be in detached HEAD state after this command.

similarly creates a new branch foo, which refers to commit f, but leaves HEAD detached.

creates a new tag foo, which refers to commit f, leaving HEAD detached.

If we have moved away from commit f, then we must first recover its object name (typically by using git reflog), and then we can create a reference to it. For example, to see the last two commits to which HEAD referred, we can use either of these commands:

1
2
$ git reflog -2 HEAD # or
$ git log -g -2 HEAD

4. 应用场景分析#

  1. 临时看一下某个提交的内容, 不做修改
  2. 利用 游离状态的版本号创建一个新的分支

5. 实践#

  1. 创建初始
1
git init
  1. 创建三次提交
1
2
3
4
5
6
7
8
touch a
git add . && git commit -m a

touch b
git add . && git commit -m b

touch c
git add . && git commit -m c
  1. git log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
commit 76617d0b28d7d02516f195851fddcb7654c40a19 (HEAD -> master)
Author: lxmuyu <lixmuyu@126.com>
Date: Mon Mar 28 17:15:18 2022 +0800

c

commit e0fb0f1c9ab9bdfcd3e4f60b2321d152926b3b40
Author: lxmuyu <lixmuyu@126.com>
Date: Mon Mar 28 17:15:03 2022 +0800

b

commit 8acaa8c70b40ed5426c3fbbd4c596dda6c6b1514
Author: lxmuyu <lixmuyu@126.com>
Date: Mon Mar 28 17:14:48 2022 +0800

a

  1. 产生 detached HEAD
    git checout e0fb0f1c9ab9bdfcd3e4f60b2321d152926b3b40
1
2
3
4
5
6
7
8
9
10
11
12
Note: checking out 'e0fb0f1c9ab9bdfcd3e4f60b2321d152926b3b40'.

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 e0fb0f1 b
  1. 以此创建新分支,并消除 detached Head
1
2
3
4
5
git checkout -b new


touch d
git add . && git commit -m d
  1. 切换到 master
1
2
3
git checkout master
touch e
git add . && git commit -m e
  1. 在主线
    git log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
commit 671f275294d84d322fffed7f52fd412223a69f07 (HEAD -> master)
Author: lxmuyu <lixmuyu@126.com>
Date: Mon Mar 28 17:19:48 2022 +0800

e

commit 76617d0b28d7d02516f195851fddcb7654c40a19
Author: lxmuyu <lixmuyu@126.com>
Date: Mon Mar 28 17:15:18 2022 +0800

c

commit e0fb0f1c9ab9bdfcd3e4f60b2321d152926b3b40
Author: lxmuyu <lixmuyu@126.com>
Date: Mon Mar 28 17:15:03 2022 +0800

b

commit 8acaa8c70b40ed5426c3fbbd4c596dda6c6b1514
Author: lxmuyu <lixmuyu@126.com>
Date: Mon Mar 28 17:14:48 2022 +0800

a

git reflog

1
2
3
4
5
6
7
8
671f275 (HEAD -> master) HEAD@{0}: commit: e
76617d0 HEAD@{1}: checkout: moving from new to master
059cf58 (new) HEAD@{2}: commit: d
e0fb0f1 HEAD@{3}: checkout: moving from e0fb0f1c9ab9bdfcd3e4f60b2321d152926b3b40 to new
e0fb0f1 HEAD@{4}: checkout: moving from master to e0fb0f1c9ab9bdfcd3e4f60b2321d152926b3b40
76617d0 HEAD@{5}: commit: c
e0fb0f1 HEAD@{6}: commit: b
8acaa8c HEAD@{7}: commit (initial): a