Git detached HEADから抜け出す — 原因とコミットの復旧

読了 5分

過去のコミットを確認しようとコミットハッシュをチェックアウトしたら、見慣れない警告が長々と出力されます。

detached HEADの警告
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 switching back to a branch.

警告の長さから大きな事故のように見えますが、結論から整理します。detached HEADは故障ではなく、HEADがブランチではなくコミットを直接指している状態です。状態そのものは何も壊しません。ただし、この状態でコミットを作って何の対処もなしに離れると、そのコミットを失う可能性があるため、構造と抜け出し方を知っておく必要があります。

HEADが分離されるということの意味 #

普段のHEADはコミットではなくブランチを指していますmainの上でコミットを作るとmainポインタが新しいコミットへ前進し、HEADはmainに従って自動的に一緒に動きます。

ところが、ブランチではなく特定のコミットのハッシュやタグを直接チェックアウトすると、HEADはブランチを経由せずにそのコミットを直接指すようになります。ブランチから離れたという意味でdetached、つまり分離されたHEADと呼びます。

通常の状態とdetachedの状態
[通常 — attached]
HEAD ──▶ main ──▶ C3

[分離 — detached]
HEAD ──────────▶ C1        (ブランチを経由しない)

detached状態に入る経路は、だいたい3つです。

  • 過去のコミットやタグを直接チェックアウトgit checkout 1a2b3c4git checkout v2.1.0、または明示的にgit switch --detach 1a2b3c4
  • bisectの進行中 — 二分探索が中間のコミットを1つずつチェックアウトするため、探索の間はずっとdetached状態です。git bisect resetで終えれば元に戻ります。
  • サブモジュールの内部 — サブモジュールは特定のコミットに固定して参照する構造のため、そのディレクトリの中はデフォルトでdetached状態です。

何が問題か — どのブランチにも属さないコミット #

detached状態でコードを見るだけなら、何の危険もありません。問題はコミットを作ったときです。

ブランチの上で作ったコミットは、ブランチポインタが参照を保ってくれます。ところがdetached状態で作ったコミットは、どのブランチからも指されていません。この状態からgit switch mainで離れた瞬間、作ったばかりのコミットは到達できない状態になり、時間が経てばガベージコレクションの整理対象になります。Gitもこれを知っているため、離れる時点で最後の警告を出力します。

detached状態でコミットした後に離れるときの警告
Warning: you are leaving 2 commits behind, not connected to
any of your branches:

  8f3c2d1 実験: キャッシュサイズ調整
  5e9a7b4 実験: キャッシュ統計を出力

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> 8f3c2d1

警告が解決策まで教えてくれています。コミットにブランチを付ければよいのです。

状況別の抜け出し方 #

detached HEADから抜け出す方法は、そこまでに何をしたかによって3つに分かれます。

眺めただけなら — そのまま戻ります

直前のブランチへ復帰
git switch -

-は直前にいたブランチを意味します。コミットを作っていなければ、この1行で状況は終わりです。

コミットを作って残したいなら — ブランチを付けてから出ます

現在のコミットの上に新しいブランチを作成
git switch -c experiment-cache

いまHEADが指しているコミットの上に新しいブランチができて、HEADもそのブランチに再び接続されます。コミットはブランチが参照している状態になったので、失う心配はありません。

すでに離れてコミットを失ったなら — ハッシュを見つけてブランチを付けます

離れるときに出力された警告にコミットハッシュが書かれているので、そのハッシュからブランチを作ります。

警告に残ったハッシュで復旧
git branch experiment-cache 8f3c2d1

ターミナルを閉じて警告を失った場合は、reflogから探します。detached状態で作ったコミットもreflogにはすべて記録されています。ハッシュを探す手順は削除したGitコミットとブランチをreflogで復旧するで整理しました。

注記
サブモジュールの中のdetached HEADは、抜け出す対象ではなく正常な状態です。外側のリポジトリがサブモジュールを特定のコミットに固定して参照する構造なので、サブモジュールの中で作業するときは、まずgit switch <ブランチ>でブランチに接続してからコミットします。

そもそも安全に眺める習慣 #

過去時点のコードを確認することがあるなら、入る前にブランチを作っておくほうがすっきりします。

ブランチを付けて過去時点を見る
git switch -c temp-look 1a2b3c4
# 確認が終わったら
git switch main
git branch -d temp-look

detached状態そのものを作らないので、警告もコミット消失の可能性もありません。確認だけしてコミットしなかったなら、ブランチの削除で跡形なく片付きます。

まとめ #

この記事の要点は3つです。

  • detached HEADは、HEADがブランチの代わりにコミットを直接指している状態で、それ自体は何も壊しません。
  • 危険はこの状態で作ったコミットだけにあります。離れる前にgit switch -c <ブランチ>でブランチを付ければ安全で、すでに離れたなら警告のハッシュかreflogで復旧します。
  • 過去時点を確認するとき、最初から一時ブランチを作って入れば、この状態にはそもそも出会いません。

警告メッセージは長くて威圧的に読めますが、内容は案内に近いものです。構造を一度理解しておけば、detached HEADは事故ではなく、Gitが提供する自由な探索モードとして使えます。

X