git: どのブランチにも属していない迷子のコミットに関する備忘録

LICENSE: CC BY 4.0[^ccby], 0BSD[^zerobsd]

# git reflog の活用シーン

ブランチ名がひとつしか付いていないブランチを未マージにも関わらず強制削除したり `git reset --hard` をしたりすると、そこにあったコミットは「どのブランチにも属していないコミット」になってしまう。このようなコミットを本記事では *迷子コミット* と呼ぶことにする。迷子コミットは `git log` に `--all` オプションを付けても表示されなくなってしまうため消失してしまったかのように見えるが、内部的には残っている。消してはいけないブランチをうっかり消してしまったときなど、このような迷子コミットをサルベージしたい場合には `git reflog` を見て復旧するのがセオリーである。

https://qiita.com/getty104/items/cfd98f5f0ea89ef07bf0

:::note info
`git commit --amend` したときや `git rebase` したときにも、実は修正前のコミットやリベース前のコミットが残っており、これらも「どのブランチにも属していないコミット」である。このようなコミットは迷子というよりは脱皮後の抜け殻という感じだろうか。
:::

## git reflog のグラフ表示

`git reflog` を見ると HEAD が以前に指していたコミットなどがわかるため迷子コミットをサルベージすることができる。しかし `git log --graph` によるグラフ表示のように、迷子コミットがどこから生えていたのかを目視で確認したい場合もある。そのような場合には `git log` に `--reflog` オプションを付ければ、`git reflog` に載っているエントリを拾った `git log` を表示してくれる。

```shell
# 情報量が多くなるので適宜 oneline 表示などを使うとよい
$ git log --all --graph --oneline --reflog
```

:::note info
紛らわしいのだが、`git reflog show` は `git log --walk-reflogs --oneline` のエイリアスである。`git log` において `--walk-reflogs` オプションは `--graph` オプションと同時に使うことができないため、`git reflog` に `--graph` オプションを付けてもグラフ表示させることはできない。
:::

# 最強の git log（のバグ修正版）

さきほどの `git log --reflog` でも出てこない *激しく迷子な* コミットも拾う “最強の git log” なる手法がある。

https://qiita.com/querykuma/items/ac6287002e752535d7ae

ただしこちらの記事で紹介されているコマンドでは、もし `git fsck` が出力するハッシュ値が大量になるとコマンドライン引数の上限を回避するために `xargs` が自動で引数群を分割してしまうため、複数回に分けて `git log` が実行されてしまう。この問題を解決するには `xargs` を使うのではなく、`git log` の `--stdin` オプションで標準入力からハッシュ値を読み込むようにすればよい。

```shell:further-git-log.sh
#!/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” を呼び出せるため便利である（便利だが、使わざるを得ない状況には陥りたくないものである）。

```text:.gitconfig
[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"
```

:::note warn
上のエイリアス設定では `sh -c 'ほげほげ' DUMMY` のように末尾に `DUMMY` が付いているが、これは必要なダミー引数である。`'ほげほげ'` の中で `$@` を使っているのだが、`sh` を `-c` オプションで実行する場合には普通にシェルスクリプトを実行する場合と違ってコマンドライン引数が `$1` からではなく `$0` から順番に格納される仕様になっているためである。dash や bash などでも同様。ダミーなので別に `DUMMY` という文字列である必要はない。本来の用途からすると `sh` という文字列にしておくのが正しい気がする。

> ```text
> sh -c [-abCefhimnuvx] [-o option]...
>       [+abCefhimnuvx] [+o option]...
>       command_string [command_name [argument...]]
> ```
> `-c` : Read commands from the `command_string` operand. Set the value of special parameter `0` (see Section 2.5.2, Special Parameters) from the value of the `command_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.
> -- <em>https://manpages.ubuntu.com/manpages/jammy/en/man1/sh.1posix.html</em>

```shell
$ 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” の違い

:::note warn
このセクションの内容はちゃんと調べていないので不正確かもしれない。
:::

`git log --reflog` では出てこなくて “最強の git log” では出てくる迷子コミットは、恐らく `git fsck --unreachable` で出てくるものだと思われる。このような `git fsck` で Unreachable 判定となっている *激しく迷子な* コミットは `git gc` を実行するとガベージコレクトされ、完全に消滅する（多分）。つまり、*風前の灯火な* 迷子コミットである。

# 迷子コミットの完全削除

:::note warn
**迷子コミットをまったくサルベージできなくなってしまう**ため実行する際には注意すること。
:::

:::note alert
例えば GitHub のようなリモートリポジトリに機密情報をアップロードしてしまった事故に際しては、ローカルリポジトリで歴史改変作業を行う過程で必然的に機密情報が迷子コミットとして追いやられるはずだが、リモートリポジトリにおける機密情報の削除が完了するまではローカルリポジトリの迷子コミットを削除してはいけない。すべてが終わるまでは情報保全をしておくこと。

機密情報アップロード事故に際しては以下の記事やドキュメントが参考になるはずである（なお、本記事筆者はこの手の事故対応をしたことがないし以下のリンク先も斜め読みしかしていない）。

- [GitHub 上の sensitive data を削除するための手順と道のり | メルカリエンジニアリング](https://engineering.mercari.com/blog/entry/20211207-removing-sensitive-data-from-github/)
- [リポジトリからの機微なデータの削除 - GitHub Docs](https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository)

一方、すべてが終わった後ならば機密情報がサルベージされないようにするために迷子コミットは完全に削除されるべきだが、そのような重大な事案の場合には上で紹介したウェブページでも書かれているように、ローカルリポジトリを破棄してリモートから clone しなおしたほうが確実だと思われる。
:::

迷子コミットを `git reflog` に**まだ残っているものも含めて完全に削除する**には以下のふたつのコマンドを実行する。

> ```shell
> $ git reflog expire --expire-unreachable=now --all
> $ git gc --prune=now
> ```
> -- <em>https://stackoverflow.com/questions/3765234/listing-and-deleting-git-commits-that-are-under-no-branch-dangling</em>

迷子コミットといえど単に `git gc` するだけではそうそう削除されない。そのため、まず `git reflog expire` で迷子コミットを reflog から一掃する。そうすると迷子コミットは *風前の灯火な* 迷子コミットとなり、ガベージコレクトの対象となる（`git gc` する前ならまだ `git fsck --unreachable` で出てくる）。

<!-- ** END OF ARTICLE ** -->

[^ccby]: 本記事のライセンスは [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.ja) とします。
[^zerobsd]: 前記1に関わらず、引用部分を除き、本記事内のコードスニペットのライセンスは [0BSD](https://licenses.opensource.jp/0BSD/0BSD.html) とします。