CH 18 / 75第3部 Gitの内部構造 ── なぜそう動くのか

HEAD とは何か・detached HEAD

このページで分かること

  • HEAD が「今いる場所」を指す名前だと分かる
  • HEAD → ブランチ → コミットという2段の指し方を説明できる
  • detached HEAD がどういう状態かが分かり、慌てずに戻れる
  • detached HEAD で作ったコミットを失わずに済む方法が分かる
このページの目次8 items

第2部の14では、HEAD を「今いる位置、つまり最新のコミットを指す名前」として使いました。このページでは、その正体を開けて確かめます。HEAD が分かると、第4部のブランチの操作が「名前を付け替えているだけ」だと見えてきます。

HEAD はファイル1つ#

HEAD の実体は、.git の中にある HEAD という名前のテキストファイルです。

text5 lines
cd ~/git-practice
cat .git/HEAD

# 出力:
# ref: refs/heads/main

中身は1行だけで、「refs/heads/main を見よ」と書いてあります。refs/heads/main は main ブランチのことで、その正体は第3部の19で開けます。ここでは「HEAD はコミットを直接指しているのではなく、ブランチを指している」ことだけ押さえます。

上段はふだんの状態で、HEAD が main を指し、main がコミット C を指す。下段は detached HEAD で、HEAD が main を通さずコミット B を直接指し、main は C を指したまま

上段がふだんの状態です。HEAD は main を指し、main が最新のコミットを指します。HEAD と main のどちらで尋ねても、行き着くコミットは同じです。

text5 lines
git rev-parse HEAD
git rev-parse main

# 出力(2つとも同じ):
# b505e8e8f92abb8142a6ca486600de9100f98e42

git rev-parse は、名前がどのコミットを指しているかを ID で答えるコマンドです。

コミットすると何が動くのか#

この2段の指し方には意味があります。コミットすると、Git はHEAD が指しているブランチを、新しいコミットへ進めます。HEAD のファイルの中身は ref: refs/heads/main のまま変わりません。HEAD は main を指し続け、main だけが1つ先へ動きます。

「今どのブランチで作業しているか」を HEAD が覚えていて、記録はそのブランチに積み上がる、という仕組みです。第4部の22でブランチを切り替えるときに動くのは、この HEAD の中身です。

detached HEAD:HEAD がコミットを直接指す状態#

過去のコミットの状態を見たいとき、そのコミットに移動できます。

text4 lines
git switch --detach HEAD~2

# 出力:
# HEAD is now at 35b12cd やることに郵便局を追加

git switch は今いる場所を切り替えるコマンドで、--detach は「ブランチを通さず、コミットに直接移る」という指定です。HEAD の中身を見ると、変わっています。

text4 lines
cat .git/HEAD

# 出力:
# 35b12cd6e2f8655e722088f0cc635c3686230db0

ref: … ではなく、コミットの ID が直接書かれています。これが detached HEAD(切り離された HEAD)です。図の下段の状態で、HEAD がブランチから切り離されて、コミットを直接指しています。

text5 lines
git status

# 出力:
# HEAD detached at 35b12cd
# nothing to commit, working tree clean

ワークツリーのファイルは、そのコミットの時点の内容に変わっています。todo.txt を開くと「郵便局に行く」の行があり、recipes フォルダはありません。main ブランチのコミットは何も変わっていません。 見る場所を移しただけです。

元に戻る#

見終わったら、ブランチに戻ります。

text5 lines
git switch main
cat .git/HEAD

# 出力:
# ref: refs/heads/main

HEAD が再び main を指し、ワークツリーも main の最新のコミットの状態に戻りました。detached HEAD はエラーではなく、ただの状態です。慌てる必要はありません。

注意:detached HEAD でコミットすると#

detached HEAD のままでもコミットはできます。ただ、そのコミットを指すブランチがありません。

text11 lines
git switch --detach HEAD~2
echo "あとで消える行" >> todo.txt
git commit -a -m "迷子のコミット"
git switch main

# 出力:
# Warning: you are leaving 1 commit behind, not connected to
# any of your branches:
#
#   4d25345 迷子のコミット
# (以下略)

main に戻るとき、Git は「どのブランチにもつながっていないコミットを1つ置いていく」と警告します。git log には出てこなくなり、第2部の15で見たとおり、reflog からしかたどれなくなります。

残しておきたい場合は、detached HEAD のうちに、そのコミットにブランチの名前を付けます。

text1 lines
git switch -c rescue

git switch -c 名前 は、今いるコミットに新しいブランチを作って、そこに移るコマンドです。ブランチの作り方は第4部の22で扱います。もう戻ってしまったあとでも、警告に表示された ID(ここでは 4d25345)や reflog を使って、git switch -c rescue 4d25345 のように救えます。

今回の「迷子のコミット」は練習なので、救わずにそのままにしておいて構いません。

よくある疑問#

Q. HEAD~1 と HEAD@{1} は何が違いますか?
A. HEAD~1 は「今のコミットの1つ前(親)」、HEAD@{1} は「HEAD が1回前にいた場所」です。前者は履歴のつながりを、後者は第2部の15の reflog をたどります。reset や switch のあとでは、2つが別のコミットを指すことがあります。

Q. detached HEAD は、どんなときに使うのですか?
A. 過去のある時点のファイルを丸ごと確かめたいときです。第5部の32の bisect(不具合が入ったコミットを探す機能)でも、Git が自動で detached HEAD を使います。

まとめ#

  • HEAD は .git/HEAD というファイルで、ふだんはブランチの名前を指している
  • コミットすると、HEAD が指しているブランチが新しいコミットへ進む
  • detached HEAD は、HEAD がブランチを通さずコミットを直接指している状態
  • git switch ブランチ名 で戻れる。detached HEAD はエラーではない
  • detached HEAD で作ったコミットは、git switch -c でブランチを付けて残す

次のページ#

19. 参照(refs)とブランチの実体 ── HEAD が指していた refs/heads/main を開けて、ブランチの正体が41バイトのファイルであることを確かめます。