Gitでdetached HEADのままコミットして迷子になったときの調査・救出方法

概要

今回はGitでdetached HEAD状態のままコミットしてしまい、どのブランチにも属さない「迷子コミット」を作ってしまったときの調査・救出方法について紹介していきます。

過去のコミット地点をgit checkoutで見に行っただけのつもりが、そのままうっかりコミットしてしまうことってありますよね。
ブランチではなくコミットを直接checkoutするとdetached HEAD状態になり、気付かずに作業を続けるとコミットがどのブランチにも属さない状態になってしまいます。

幸いgit reflogを使えば、detached HEAD状態で作ったコミットでも取り戻せます。
今回自分が実際にやった、状態確認から救出ブランチ経由で本来のブランチに取り込むまでの手順を残しておきます。

それではやっていきましょう!

reflogに記録が残っている間(デフォルトでは90日程度)は取り戻せますが、記録が切れる・もしくは`git gc`が走ると復元できなくなる可能性があります。
「あれ、コミットが消えた?」と気付いたら早めに対応するのがお勧めです。

目次

全体の流れ

まずは全体像から。
detached HEADで作った迷子コミットを見つけて、本来取り込みたいブランチに引き渡すまでの流れを図にするとこんな感じです。

迷子コミットの調査・救出フロー(detached HEAD発生→reflogでハッシュ確認→救出ブランチ作成→cherry-pickで取り込み)
Gitでdetached HEAD状態のまま作ったコミットを救出する手順のフロー図。git statusでHEAD detached at xxxxxxxを確認し、git reflogでハッシュを特定、git branchで救出ブランチを作成し、git cherry-pickで取り込み先ブランチへ反映する4ステップ

そもそもなぜdetached HEADになるのか

git checkout <コミットハッシュ>のように、ブランチ名ではなくコミットを直接指定してcheckoutすると、detached HEAD状態になります。

通常のブランチ上での作業は「ブランチ名」がHEADの指す先を追いかけてくれますが、detached HEADではHEADが特定のコミットを直接指してしまうんですよね。
その状態に気付かないままファイルを編集してコミットすると、そのコミットはどのブランチのポインタからも辿れない「迷子コミット」になります。

Step1: detached HEAD状態かどうかを確認する

まずは自分が今detached HEAD状態にいるかどうかを確認します。

コマンド

1
git status

「HEAD detached at xxxxxxx」と表示されれば、detached HEAD状態です。
通常のブランチ上にいる場合は「On branch xxxxxx」のように表示されるので、この一文の違いで見分けられます。

Step2: 迷子コミットのハッシュを確認する

detached HEAD状態だと分かったら、git reflogで自分の操作履歴を辿ります。

コマンド

1
git reflog

reflogにはHEADが移動してきた履歴が新しい順に並んでいるので、先ほどコミットした直後のログを探してハッシュをメモしておきます。
「commit: (コミットメッセージ)」という行が、自分がdetached HEADで作ったコミットに該当します。

`reflog`はローカルリポジトリの操作履歴であり、リモートには存在しません。
別のマシンやクローンし直した環境では過去の`reflog`は引き継がれないため、迷子コミットの救出は同じローカル環境で行う必要があります。

Step3: 救出ブランチを作成する

ハッシュが分かったら、そのコミットを起点に救出用のブランチを作ります。

コマンド

1
2
git branch rescue-branch <コミットハッシュ>
git checkout rescue-branch

これで迷子コミットにブランチ名という「名札」が付き、以降git gcなどでうっかり消えるリスクを避けられます。

Step4: 取り込み先ブランチに移動する

次に、このコミットを本来取り込みたいブランチへ移動します。
状況に応じて、どちらかのコマンドを使います。

  • ローカルに<ブランチ名>がすでにある場合
    • git checkout <ブランチ名>
  • ローカルにない場合(リモート追跡ブランチから作成)
    • git checkout -b <ブランチ名> origin/<ブランチ名>

Step5: コミットを取り込む

取り込み先ブランチに移動できたら、cherry-pickで迷子コミットだけを取り込みます。

コマンド

1
git cherry-pick <コミットハッシュ>

cherry-pickは指定したコミット1つ分の変更だけを、今いるブランチの先端に新しいコミットとして適用してくれるコマンドです。
マージのように履歴全体を持ってくるわけではないので、迷子コミット1個だけを回収したい今回のケースにちょうど合っています。

Step6: 取り込めたか確認する

cherry-pickが終わったら、ちゃんと取り込めているかログで確認します。

コマンド

1
git log --oneline

先頭に、先ほどのコミットメッセージが新しいコミットとして積まれていれば成功です。
ハッシュ自体はcherry-pickによって別物になりますが、変更内容とメッセージは引き継がれます。

Step7: 救出ブランチを削除する

取り込みが確認できたら、役目を終えたrescue-branchは削除して片付けます。

コマンド

1
git branch -d rescue-branch
  • git branch -d
    • マージ済みの場合のみ削除可能。安全な削除方法
  • git branch -D
    • 未マージでも強制的に削除する。内容が不要だと確認できた場合のみ使う

-Dはマージ状況を無視して問答無用で消すため、cherry-pickできているか確認する前に使うと本当に迷子コミットへ戻れなくなります。
基本は-dを使い、削除できないというエラーが出たときに初めて内容を見直す、くらいの慎重さでちょうどいいと思います。

予防策

救出できたとはいえ、そもそもdetached HEADに気付かないまま作業してしまうこと自体を減らしたいですよね。
自分は次の2点を習慣にしています。

  • 作業前にgit statusとgit branchで現在地を確認する
    • ブランチ名が表示されない・「HEAD detached」と出ている場合はそこで気付ける
  • 過去のコミットを覗きたいだけでも、先にブランチを切ってからcheckoutする
    • git checkout -b <新しいブランチ名> <コミットハッシュ>としておけば、そのまま作業してもdetached HEADにならない

checkoutする前にワンクッション置くだけの話ですが、これを習慣にしてからは同じ事故を起こさなくなりました。

締め

reflogという安全装置があるおかげで、detached HEADでのコミットも実質「なくなってはいない」と分かってからは、以前ほど身構えなくなりました。

とはいえreflogの記録がいつまでも残るわけではないので、事故ったら早めに対応するのが一番ですね(^^;
あとは事故らないように気をつける

以上となります。
結構大きい修正だったので、無事救出できてよかった(^^/
それではお疲れさまでした!