HEAD 到底是什么?detached HEAD 又是怎么回事,在上面提交会怎样?这篇直接看 .git/HEAD 文件在各种状态下的内容,再看 reflog 如何帮你找回 detached 上的提交,最后认识 ORIG_HEAD、MERGE_HEAD 等几个特殊文件。
$ cat .git/HEAD; git symbolic-ref HEAD; git rev-parse HEAD | cut -c1-12; cat .git/refs/heads/main | cut -c1-12 ref: refs/heads/main refs/heads/main e79e434670c8 e79e434670c8
.git/HEAD 在分支上是 ref: refs/heads/main 这样的符号引用,真正的提交 hash 存在 .git/refs/heads/main 里;HEAD 本身并不存提交。
$ git checkout -q 9867bddbd4ae45702c6ed062bec552b684c96d4d; cat .git/HEAD | cut -c1-12; git symbolic-ref HEAD 2>&1; git status | head -2; git branch
9867bddbd4ae
fatal: ref HEAD is not a symbolic ref
HEAD detached at 9867bdd
nothing to commit, working tree clean
* (HEAD detached at 9867bdd)
maingit checkout <commit> 之后 HEAD 里直接是 hash,git symbolic-ref HEAD 报错“不是符号引用”,git status 提示 detached。这时 HEAD 不再跟着任何分支。
$ git log --oneline --all --graph; git branch --contains HEAD 2>&1
* 690e7c4 detached 上的提交
| * e79e434 提交 3
|/
* 9867bdd 提交 2
* 88a540f 提交 1
* (HEAD detached from 9867bdd)
切回 main:git 提醒你正在丢下一个提交
$ git checkout main 2>&1
Warning: you are leaving 1 commit behind, not connected to
any of your branches:
690e7c4 detached 上的提交
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> 690e7c4
Switched to branch 'main'
$ git branch --contains 690e7c485315d1a71ac3e10a948539a48f64a368; echo "(上面没有输出:没有任何分支包含它)"; git reflog | head -5 | cut -c9-
(上面没有输出:没有任何分支包含它)
HEAD@{0}: checkout: moving from 690e7c485315d1a71ac3e10a948539a48f64a368 to main
HEAD@{1}: commit: detached 上的提交
HEAD@{2}: checkout: moving from main to 9867bddbd4ae45702c6ed062bec552b684c96d4d
HEAD@{3}: commit: 提交 3
HEAD@{4}: commit: 提交 2新提交只有 HEAD 指着它,不属于任何分支。切走时 git 会警告,并建议 git branch <名字> <hash>。如果错过警告,git branch --contains 查不到它。
$ git switch -c rescued 690e7c485315d1a71ac3e10a948539a48f64a368 2>&1; git log --oneline rescued Switched to a new branch 'rescued' 690e7c4 detached 上的提交 9867bdd 提交 2 88a540f 提交 1
reflog 记录了 HEAD 的每一次移动,其中包括这个提交的 hash;用它起一个分支就找回来了。这是 reflog 最典型的用途(见后面 reflog 一篇)。
$ git checkout -q main; ls .git | grep -E "^(ORIG_HEAD|FETCH_HEAD|MERGE_HEAD|CHERRY_PICK_HEAD|REVERT_HEAD|REBASE_HEAD)$" || echo "(现在一个都没有)"
(现在一个都没有)
$ git reset -q --hard HEAD~1; ls .git | grep -E "_HEAD$"; git rev-parse ORIG_HEAD | cut -c1-12; git reset -q --hard ORIG_HEAD
ORIG_HEAD
e79e434670c8
$ git merge side 2>&1 | head -3; ls .git | grep -E "_HEAD$"; cat .git/MERGE_HEAD | cut -c1-12; git merge --abort; ls .git | grep -E "MERGE_HEAD" || echo "(abort 之后 MERGE_HEAD 没了)"
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
Automatic merge failed; fix conflicts and then commit the result.
MERGE_HEAD
ORIG_HEAD
6aea679fd5ec
(abort 之后 MERGE_HEAD 没了)
$ git cherry-pick side 2>&1 | head -2; ls .git | grep -E "_HEAD$"; git cherry-pick --abort
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
CHERRY_PICK_HEAD
ORIG_HEAD
$ git remote add o /work/l23 2>/dev/null; git fetch -q o 2>&1; ls .git | grep -E "FETCH_HEAD"
FETCH_HEAD
FETCH_HEAD 的前两行(hash 前 12 位 / 是否可合并 / 来源):
e79e434670c8 not-for-merge branch 'main'
690e7c485315 not-for-merge branch 'rescued'ORIG_HEAD(危险操作之前 HEAD 的位置)、MERGE_HEAD(正在合并的提交)、CHERRY_PICK_HEAD(cherry-pick 冲突)、FETCH_HEAD(最近一次 fetch)都是 .git 里的普通文件,命令结束或中止后有的消失,有的保留。
$ cat .git/HEAD; git status | head -3; git rev-parse HEAD 2>&1 | head -1; git log 2>&1 | head -1 ref: refs/heads/main On branch main No commits yet fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree. fatal: your current branch 'main' does not have any commits yet $ git symbolic-ref HEAD refs/heads/其它名字; cat .git/HEAD; git status | head -2 ref: refs/heads/其它名字 On branch 其它名字 $ git checkout --orphan fresh 2>&1; cat .git/HEAD; git status | head -3; git log 2>&1 | head -1; ls Switched to a new branch 'fresh' ref: refs/heads/fresh On branch fresh No commits yet fatal: your current branch 'fresh' does not have any commits yet a.txt
HEAD 可以指向一个还没有任何提交的分支名:新仓库、git symbolic-ref HEAD refs/heads/xxx、git checkout --orphan 都是这样,git log 和 git rev-parse HEAD 会报“还没有提交”。