git reflog の活用シーン
ブランチ名がひとつしか付いていないブランチを未マージにも関わらず強制削除したり git reset --hard
をしたりすると、そこにあったコミットは「どのブランチにも属していないコミット」になってしまう。このようなコミットを本記事では 迷子コミット と呼ぶことにする。迷子コミットは git log
に --all
オプションを付けても表示されなくなってしまうため消失してしまったかのように見えるが、内部的には残っている。消してはいけないブランチをうっかり消してしまったときなど、このような迷子コミットをサルベージしたい場合には git reflog
を見て復旧するのがセオリーである。
git commit --amend
したときや git rebase
したときにも、実は修正前のコミットやリベース前のコミットが残っており、これらも「どのブランチにも属していないコミット」である。このようなコミットは迷子というよりは脱皮後の抜け殻という感じだろうか。
git reflog のグラフ表示
git reflog
を見ると HEAD が以前に指していたコミットなどがわかるため迷子コミットをサルベージすることができる。しかし git log --graph
によるグラフ表示のように、迷子コミットがどこから生えていたのかを目視で確認したい場合もある。そのような場合には git log
に --reflog
オプションを付ければ、git reflog
に載っているエントリを拾った git log
を表示してくれる。
# 情報量が多くなるので適宜 oneline 表示などを使うとよい
$ git log --all --graph --oneline --reflog
紛らわしいのだが、git reflog show
は git log --walk-reflogs --oneline
のエイリアスである。git log
において --walk-reflogs
オプションは --graph
オプションと同時に使うことができないため、git reflog
に --graph
オプションを付けてもグラフ表示させることはできない。
最強の git log(のバグ修正版)
さきほどの git log --reflog
でも出てこない 激しく迷子な コミットも拾う “最強の git log” なる手法がある。
ただしこちらの記事で紹介されているコマンドでは、もし git fsck
が出力するハッシュ値が大量になるとコマンドライン引数の上限を回避するために xargs
が自動で引数群を分割してしまうため、複数回に分けて git log
が実行されてしまう。この問題を解決するには xargs
を使うのではなく、git log
の --stdin
オプションで標準入力からハッシュ値を読み込むようにすればよい。
#!/bin/sh
git fsck --verbose 2>&1 | grep -o -E '[0-9A-Fa-f]{40}' | grep -v -E '0{40}' | sort -u | git log --stdin "$@"
.gitconfig
で以下のようにエイリアスを設定すれば、git further-log
と書くだけで一発で “最強の git log” を呼び出せるため便利である(便利だが、使わざるを得ない状況には陥りたくないものである)。
[alias]
further-log = "!sh -c 'git fsck --verbose 2>&1 | grep -o -E \"[0-9A-Fa-f]{40}\" | grep -v -E \"0{40}\" | sort -u | git log --stdin \"$@\"' DUMMY"
上のエイリアス設定では sh -c 'ほげほげ' DUMMY
のように末尾に DUMMY
が付いているが、これは必要なダミー引数である。'ほげほげ'
の中で $@
を使っているのだが、sh
を -c
オプションで実行する場合には普通にシェルスクリプトを実行する場合と違ってコマンドライン引数が $1
からではなく $0
から順番に格納される仕様になっているためである。dash や bash などでも同様。ダミーなので別に DUMMY
という文字列である必要はない。本来の用途からすると sh
という文字列にしておくのが正しい気がする。
sh -c [-abCefhimnuvx] [-o option]... [+abCefhimnuvx] [+o option]... command_string [command_name [argument...]]
-c
: Read commands from thecommand_string
operand. Set the value of special parameter0
(see Section 2.5.2, Special Parameters) from the value of thecommand_name
operand and the positional parameters ($1
,$2
, and so on) in sequence from the remaining argument operands. No commands shall be read from the standard input.
-- https://manpages.ubuntu.com/manpages/jammy/en/man1/sh.1posix.html
$ sh -c 'echo "{0: $0}, {@: $@}"'
{0: sh}, {@: }
$ sh -c 'echo "{0: $0}, {@: $@}"' aaa bbb ccc
{0: aaa}, {@: bbb ccc}
$ sh -c 'echo "{0: $0}, {@: $@}"' DUMMY aaa bbb ccc
{0: DUMMY}, {@: aaa bbb ccc}
git log --reflog と “最強の git log” の違い
このセクションの内容はちゃんと調べていないので不正確かもしれない。
git log --reflog
では出てこなくて “最強の git log” では出てくる迷子コミットは、恐らく git fsck --unreachable
で出てくるものだと思われる。このような git fsck
で Unreachable 判定となっている 激しく迷子な コミットは git gc
を実行するとガベージコレクトされ、完全に消滅する(多分)。つまり、風前の灯火な 迷子コミットである。
迷子コミットの完全削除
迷子コミットをまったくサルベージできなくなってしまうため実行する際には注意すること。
例えば GitHub のようなリモートリポジトリに機密情報をアップロードしてしまった事故に際しては、ローカルリポジトリで歴史改変作業を行う過程で必然的に機密情報が迷子コミットとして追いやられるはずだが、リモートリポジトリにおける機密情報の削除が完了するまではローカルリポジトリの迷子コミットを削除してはいけない。すべてが終わるまでは情報保全をしておくこと。
機密情報アップロード事故に際しては以下の記事やドキュメントが参考になるはずである(なお、本記事筆者はこの手の事故対応をしたことがないし以下のリンク先も斜め読みしかしていない)。
一方、すべてが終わった後ならば機密情報がサルベージされないようにするために迷子コミットは完全に削除されるべきだが、そのような重大な事案の場合には上で紹介したウェブページでも書かれているように、ローカルリポジトリを破棄してリモートから clone しなおしたほうが確実だと思われる。
迷子コミットを git reflog
にまだ残っているものも含めて完全に削除するには以下のふたつのコマンドを実行する。
$ git reflog expire --expire-unreachable=now --all $ git gc --prune=now
迷子コミットといえど単に git gc
するだけではそうそう削除されない。そのため、まず git reflog expire
で迷子コミットを reflog から一掃する。そうすると迷子コミットは 風前の灯火な 迷子コミットとなり、ガベージコレクトの対象となる(git gc
する前ならまだ git fsck --unreachable
で出てくる)。