「全部消えてしまった」(ネタバレ: たぶん消えていません)
まずは深呼吸してください。失ったと思っている作業が一度でもコミットされていたなら -- たとえ間違ったブランチ上でも、後から「削除した」コミットの中でも -- それはほぼ間違いなく、今この瞬間もリポジトリの中に残っています。Git は物事を失うことよりも保持することのほうがずっと得意です。そして、この先 10 分ほど読み進めれば、コードはおそらく画面に戻ってきます。
Git の内部動作について、安心できる事実をお伝えします。Git がデータを即座に削除することはほとんどありません。作成したすべてのコミットは .git ディレクトリにオブジェクトとして保存され、SHA で識別されます。git reset --hard を実行しても、ブランチを削除しても、rebase で履歴を書き換えても、Git はそれらのコミットオブジェクトを消去しません。動かすのはポインタ -- ブランチ名と HEAD -- だけで、それらがコミットを指さなくなるだけです。コミットは「到達不能 (unreachable)」になります。つまり、どのブランチやタグからもたどれなくなるだけで、オブジェクト自体は、ガベージコレクションが手を付けることを検討し始めるまで、何週間もディスク上に残り続けます。
つまり問題は、コミットが消えたことではありません。コミットを指す名前がなくなったことです。必要なのは SHA です。そして Git には、リポジトリが訪れたすべての SHA を静かに記録し続けてきたジャーナルがあります。それが reflog です。
reflog とは実際のところ何なのか
reflog (reference log) は、HEAD と各ブランチの先端がこれまでどこを指してきたかを記録する、マシンごとのローカルなジャーナルです。HEAD が動くたびに -- コミット、checkout、マージ、rebase、reset、amend のたびに -- Git はこのジャーナルに 1 行を追記します。いわばリポジトリのフライトレコーダーです。
確認するには、次を実行します。
git reflog
出力は次のようになります。
e4f5a6b HEAD@{0}: reset: moving to HEAD~3
a1b2c3d HEAD@{1}: commit: add payment validation
9f8e7d6 HEAD@{2}: commit: refactor checkout flow
3c4d5e6 HEAD@{3}: checkout: moving from main to feature/payments
上から下へ「新しい順」に読みます。各行には、HEAD が指していた SHA、HEAD@{1} のような位置参照、HEAD を動かした操作、そして短い説明が表示されます。HEAD@{n} という構文は「n 回前の移動のときに HEAD がいた場所」を意味します。HEAD@{0} は現在地、HEAD@{1} は 1 操作前、という具合です。これらの参照は、コミットを受け取るあらゆるコマンドで使えます。救出作業がこれほど直接的に行えるのは、まさにこのおかげです。
上の例では、何が起きたかは明快です。2 つのコミット (HEAD@{2} と HEAD@{1}) を作った後、reset が HEAD を 3 コミット分戻しました。この 2 つのコミットは失われていません。a1b2c3d にそのまま存在しています。
ログを持つ参照は HEAD だけではありません。各ブランチも自分のログを持っています。
git reflog show feature/payments
これは feature/payments の先端が占めてきたすべての位置を表示します。これまで行ったあらゆる checkout のノイズを除いて、1 つのブランチの履歴だけを見たいときに便利です。
救出シナリオ、ステップバイステップ
1. git reset --hard で戻しすぎた
1 コミットだけ取り消すつもりが 3 つ消してしまった、あるいはまったく違う場所に reset してしまった。reflog は reset の直前にいた位置を記録しており、その位置が HEAD@{1} です。
git reset --hard HEAD@{1}
修正はこれだけです。reset 自体も単なる HEAD の移動の 1 つにすぎないので、HEAD を直前の位置に戻せばすべてが復元されます。誤った reset の後に他の操作をしている場合は、まず git reflog を実行し、reset のエントリの 1 つ前の行を見つけて、その SHA に reset してください。取り消し戦略の全体像については、Git のコミットを取り消す方法 をご覧ください。
2. ブランチを削除してしまった
ブランチの削除で消えるのはポインタであって、コミットではありません。そのブランチをこのマシンで最近 checkout したりコミットしたりしていれば、その先端は HEAD の reflog にあります。探してみましょう。
git reflog | grep "feature/payments"
そのブランチで最後に作ったコミット、または最後の checkout: moving from feature/payments エントリを探します。その行の SHA がブランチの先端です。あとは、それを指すブランチを再作成するだけです。
git branch feature/payments a1b2c3d
ブランチは、削除された瞬間とまったく同じ状態で戻ります。おまけに、Git はブランチ削除時に先端の SHA を出力します (Deleted branch feature/payments (was a1b2c3d))。このメッセージがまだターミナルのスクロールバックに残っていれば、reflog を探す手間すら省けます。
3. rebase が失敗した
rebase はコミットを書き換える操作です。コンフリクトだらけの rebase の途中では、ブランチが修復不能なほど壊れたように感じられるかもしれません。そんなことはありません。rebase 前の状態は reflog にあります。rebase がまだ進行中なら、最もクリーンな脱出方法は git rebase --abort です。すでに完了していて結果が気に入らないなら、開始前のエントリを見つけてください。
git reflog
# "rebase (start)" を探します -- その「すぐ下」のエントリが
# rebase 前にブランチがいた場所です
git reset --hard HEAD@{5}
({5} は、あなたの reflog で rebase 前のエントリが占めている位置に置き換えてください。) これでブランチは元どおりです。rebase で頻繁にこの状況に陥るなら、怖くないインタラクティブ rebase のガイドで、rebase をリスクではなく日常の作業にする方法を解説しています。
4. --amend の後にコミットが消えた
git commit --amend はコミットを編集するのではなく、新しいコミットを作ってブランチをそちらに移動させます。元のコミットは到達不能になりますが無傷で、amend の直後なら HEAD@{1} にあります。中身を確認するには次のとおりです。
git show HEAD@{1}
間違ったコミットを amend してしまった場合や、元のコミットを取り戻したい場合は、そこへ reset するか、必要な場所へ cherry-pick してください。
5. detached HEAD でコミットした後、別の場所へ移動した
特定のコミットやタグを checkout し、detached HEAD 状態でコミットを作り、その後ブランチを checkout したら、コミットが蒸発したように見える。Git は移動するときにこの状況を警告し、孤立した SHA を出力までしてくれます。いずれにせよ、reflog には残っています。
git reflog
# "checkout: moving from <sha> to <branch>" の
# エントリより前に作った最後のコミットを探します
git branch rescued-work e4f5a6b
detached 状態のコミットは、これで本物のブランチ上に移りました。必要な場所へマージや rebase ができます。
復元の前に中身を確認する
復元した SHA に何かを向ける前に、まず中身を見てください。思っているとおりのコミットかどうかを確認します。
git show a1b2c3d
これでコミットメッセージ、作者、日付、完全な diff が表示されます。1 行ずつのエントリではなく、コミットの詳細つきで reflog を眺めたい場合は、log の reflog モードを使います。
git log -g
そして、何より安全な習慣がこれです。現在のブランチを復元したコミットに reset するのではなく、まずそこに一時ブランチを作ってください。
git branch rescue a1b2c3d
これで復元した作業に、到達可能で恒久的な名前が付きました。現在のブランチには何の変化もなく、ガベージコレクションが救出したコミットに触れることは二度とありません。落ち着いて中身を確認し、diff を取り、マージできます。ブランチはタダです。100% の確信がないときは、いつでもブランチを作りましょう。
限界について、正直に
reflog は素晴らしいセーフティネットですが、現実的な限界もあります。必要になる前に知っておくべきことは次のとおりです。
- 完全にローカルです。 reflog はあなたのマシン上にあり、push、pull、clone のいずれでも転送されません。新しい clone の reflog は空の状態から始まります。ノートパソコン上でコミットを失ったなら、重要なのはそのノートパソコン上の reflog です。同僚の clone も、サーバー側のコピーも助けにはなりません。
- エントリには期限があります。 デフォルトでは、ブランチから到達可能なコミットの reflog エントリは 90 日後に、到達不能なコミットのエントリは 30 日後に期限切れになります。期限切れの後は、ガベージコレクションが到達不能なオブジェクトを本当に削除できるようになります。実際にはこれは十分な時間ですが、reflog が救えるのは先月のミスであって、去年のミスではない、ということでもあります。
- コミットされなかったものは復元できません。
git reset --hardやgit checkout -- <file>で破壊された未コミットの変更は、そもそも Git オブジェクトになっていないため、どのジャーナルにも記録されていません。唯一の部分的な例外として、少なくともgit addで staging されていたファイルは blob オブジェクトとして存在しており、git fsck --lost-foundでそうした宙ぶらりんの blob を.git/lost-found/に掘り出せます。ファイル名も履歴もない中身だけですが、何もないよりはましです。これは最後の手段であって、ワークフローではありません。 - drop した stash には専用の脱出口があります。 drop または clear した stash は HEAD の reflog にはありませんが、stash のコミットは宙ぶらりんのオブジェクトとして生き残っていることがよくあります。
git fsck --unreachable | grep commitと、候補に対するgit showで見つけられます。stash を多用するなら、git stash のガイド で、より安全なワークフローを解説しています。
予防: 将来のパニックを退屈なものにする
上のすべてのシナリオには、たった 1 つの前提条件がありました。作業がコミットされていたことです。ここから導かれる、本当に意味のある唯一の予防策はこれです。早めにコミットし、こまめにコミットする。小さくて雑な、作業途中のコミットにコストはかかりません。後から squash したり、reword したり、並べ替えたりできます。コミットされた瞬間、それはオブジェクトデータベースに入り、reflog が見守ってくれます。コミットされていない間は、運に頼っているだけです。
コミットするには重すぎると感じる素早いコンテキスト切り替えには、未コミットの変更を checkout 間で持ち回すのではなく、stash を使ってください。stash もまた、Git が後から見つけ直せる本物のオブジェクトです。
GitSquid の出番 (と CLI が勝つ場面)
正直に言うと、reflog はコマンドラインこそが正しいツールである領域の 1 つです。GitSquid に reflog ブラウザはなく、git reflog が必要なときは、ターミナルを開いて使うべきです。この記事のコマンドが復旧への道筋です。
GitSquid が代わりに行うのは、周辺のワークフローを安全にして、そもそも reflog に手を伸ばす機会を減らすことです。ファイルタイムライン (任意のファイルを右クリックして Show history を選択) は、ファイル単位の視覚的な履歴を提供し、「あのコードはどこへ行った?」という疑問に、復旧作業なしで答えてくれることがよくあります。reset やブランチ削除のような破壊的操作は、コミットグラフ上の明示的なコンテキストメニューを経由するため、実行する前に、これから何をしようとしているのかが正確に見えます。そして「たった今、何が起きたのか」を再構築するときには、コマンドログ (Cmd/Ctrl+Shift+L) が、GitSquid がリポジトリ上で実行したすべての Git コマンドを、完全な引数と終了コード付きで表示します。インシデントの経緯をつなぎ合わせる際に reflog の隣に置きたい証拠は、まさにこれです。
クイックリファレンス
| 状況 | コマンド |
|---|---|
| HEAD がたどってきた場所を見る | git reflog |
| ブランチ先端がたどってきた場所を見る | git reflog show <branch> |
誤った reset --hard を取り消す |
git reset --hard HEAD@{1} |
| 削除したブランチを復元する | git branch <name> <sha> |
| 完了済みの rebase を取り消す | git reset --hard HEAD@{n} (rebase 前のエントリ) |
| 復元したコミットを確認する | git show <sha> |
| 詳細つきで reflog を閲覧する | git log -g |
| 救出した作業を安全に退避する | git branch rescue <sha> |
| staging 済みだが未コミットのファイルの最終手段 | git fsck --lost-found |
reflog は「全部消えてしまった」を「ポインタを 10 分間置き忘れただけ」に変えてくれます。こまめにコミットし、パニックになる前に reflog を確認し、復元したものは他に手を付ける前に一時ブランチへ退避しましょう。そして、破壊的操作を明示的に見せ、実行したすべてのコマンドの完全なログを残す Git クライアントが欲しければ、GitSquid を無料でダウンロード してください。reflog の代わりにはなりませんが、reflog が必要になる場面を減らす助けにはなります。