Dev Study
← 解説「detached HEAD と「消えたコミット」— 参照されないだけで残っている」に戻る

サンプルコードで身につける: detached HEAD と「消えたコミット」— 参照されないだけで残っている

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1detached HEAD に入って、抜ける

コミットハッシュを直接checkoutすると起きる状態です。警告文が長くて身構えますが、要は「今いる場所に名前が無い」だけです。名前を付ければ普通のブランチに戻ります。

$ git checkout dd2818b
Note: switching to 'dd2818b'.

You are in 'detached HEAD' state. ...

# HEADがブランチではなくコミットを直接指している
$ cat .git/HEAD
dd2818b6f87da96757d1cd70f43e293660aece49    ← 'ref:' が無い

$ git status
HEAD detached at dd2818b

# ここに名前を付ければ、普通のブランチとして続けられる
$ git switch -c experiment
Switched to a new branch 'experiment'

$ cat .git/HEAD
ref: refs/heads/experiment                   ← 通常状態に戻った

# 何もせず元に戻るだけなら
$ git switch -                               # 直前のブランチへ

2なぜコミットが「消えた」ように見えるのか

detached HEADのままコミットすると、更新されるブランチファイルが存在しません。そのままcheckoutで離れると、そのコミットを指す名前がどこにも無くなります。その流れを再現します。

Python

3reflogで迷子のコミットを救出する

「コミットが消えた」の多くは名前を失っただけです。reflogにはHEADが過去に指した先が残っているので、そこからハッシュを見つけて名前を付け直せます。実際の救出手順です。

# detached HEAD でコミットしたあと、うっかり main に戻ってしまった
$ git log --oneline -1
dd2818b READMEに使い方を追加      ← さっきの作業が見当たらない

# reflog には HEAD が指した先が全部記録されている
$ git reflog
dd2818b HEAD@{0}: checkout: moving from aa11bb2 to main
aa11bb2 HEAD@{1}: commit: 大事な実験の成果     ← これだ
dd2818b HEAD@{2}: checkout: moving from main to dd2818b

# 名前を付ければ復活する
$ git branch rescued aa11bb2
$ git log --oneline rescued -1
aa11bb2 大事な実験の成果

# 参照の無いオブジェクトを一覧することもできる
$ git fsck --lost-found
dangling commit aa11bb2...

4detached HEAD が役に立つ場面

困った状態としてだけ覚えると損です。「過去の状態を一時的に見たいだけ」のときは、ブランチを作らずに済む正しい使い方です。ビルドの調査などで実際によく使います。

# 3つ前のコミットの時点でテストが通るか確かめたい
$ git checkout HEAD~3
$ npm test

# 確認が済んだら元に戻るだけ(何も残さない)
$ git switch -

# バグが入った地点を二分探索で特定する(内部でdetached HEADを使う)
$ git bisect start
$ git bisect bad                  # 今は壊れている
$ git bisect good dd2818b         # ここでは動いていた
Bisecting: 3 revisions left to test after this (roughly 2 steps)
$ npm test && git bisect good || git bisect bad
...
$ git bisect reset                # 終わったら元のブランチへ

5オブジェクトはいつ本当に消えるのか

参照されないオブジェクトも、しばらくは残ります。git gc が走り、かつreflogの保持期間を過ぎたときに初めて回収されます。「すぐには消えない」ことを知っていると落ち着いて対処できます。

# 参照されていないオブジェクトを確認する
$ git fsck --unreachable
unreachable commit aa11bb2...

# reflog の既定の保持期間
$ git config --get gc.reflogExpire || echo '90 days (既定)'
90 days (既定)

# 到達不能なものは既定で30日保持される
$ git config --get gc.pruneExpire || echo '2 weeks (既定)'
2 weeks (既定)

# → つまり「消えた」と思っても、多くの場合しばらくは救出できる
#   逆に、本当に消したいときは明示的に実行する必要がある
$ git reflog expire --expire=now --all && git gc --prune=now