git add しただけのファイルを git reset --hardで消してしまったときの対処法

Git に慣れてくると、`reset --hard` を多用することになりますが、 **表1** のように `add` しただけのファイルは `reset --hard` で消えてしまう、ということに注意が必要です。

**表1:** `add`/`commit` 有無による `reset --hard` 前後でのファイルの状態変化
|`reset --hard`前|`reset --hard`後|
|---|---|
|`add` していない|ワークツリーにそのまま残る|
|`add` している  |ワークツリーから消える|
|`commit` 済み   |ワークツリーから消えるが、`reflog` で辿れる|

:::note info
 「`reflog` で辿れる」と言っても実際に `reflog` の出力から探すのはそれなりに骨の折れる作業です。
`reset --hard` のような破壊的な操作をするときに共通して使えるテクニックとして、そのような操作をする前に `git tag -f backup` などとバックアップ用のタグを打っておく、というものがあります。 `reflog` を頑張って探さなくても `git reset --hard backup` 一発で `reset` 前の状態に戻れるようになります。 (`-f` は force オプションで、 `backup` という名前のタグがすでに存在していても上書きしてタグを作成します)
:::

では `git add` しただけのファイルを `git reset --hard` で消してしまったときにはどうすれば良いのでしょうか。

## 対処法

`reset --hard` で　`add` だけしたファイルを消してしまった場合、次のような操作でファイルを復活できます。

```bash
$ git fsck --lost-found
$ ls $(git rev-parse --git-path lost-found)/other
1dcdad3e568e5462239cc4d3741b8591f79cf379
0b4487101f12b248cc11c12f5be6e66492f4a64e
...
```

この操作を行うと `.git/lost-found/other` ディレクトリに **「どのような参照（ブランチ、タグ等）からもたどり着けないblob(一般ファイル)オブジェクト」** が、 Git のオブジェクトデータベースから取り出されて格納されます。 `add` した時点で Git のオブジェクトデータベースには格納されているので、「`add` だけして `commit` していないファイル」はこれに含まれます。

残念ながらファイル名はやツリー構造は復活できないので、そこには恐ろしげな SHA1 ハッシュ名のファイルがずらりと並ぶことになりますが、 **表2** に示す各種コマンドで対象を絞り込みましょう。

**表2:** 絞り込みに使えるコマンドの例
|コマンド|取得できる情報|
|--|--|
|file|ファイル種別 |
|ls -l|ファイルサイズや実行ビット|
|wc -l|ファイル行数|
|head|ファイル先頭数行|

例えば `file` コマンドを使うと次のような出力を得ることができ、ファイルを探す助けになります。

```bash
$ file $(git rev-parse --git-path lost-found)/other/*
/path/to/.git/lost-found/other/1dcdad3e568e5462239cc4d3741b8591f79cf379: Unicode text, UTF-8 text
/path/to/.git/lost-found/other/0b4487101f12b248cc11c12f5be6e66492f4a64e: C++ source text, Unicode text, UTF-8 text
/path/to/.git/lost-found/other/442608b02c4ccb59b37339b8220cd39fc18586f9: C++ source text, Unicode text, UTF-8 text
/path/to/.git/lost-found/other/05b574a877a28970b4307d6cd64a060494aebaf4: makefile script text, ASCII text
/path/to/.git/lost-found/other/bf09997fdf86a395e4b71ea53bca226edcb53187: C++ source text, Unicode text, UTF-8 text

...

/path/to/.git/lost-found/other/fc201e11ea72e5b77ca371e48148c7962cde2ad3: Unicode text, UTF-8 text
/path/to/.git/lost-found/other/a9b08c2eec97d00672cac752ebced1817e2fc8ea: C++ source text, Unicode text, UTF-8 text
/path/to/.git/lost-found/other/92bdc2cfd82bee18782859e1ff43883338029a02: makefile script text, ASCII text
/path/to/.git/lost-found/other/e2ce39059570040f4db47fc21fba7587dd68decf: makefile script text, ASCII text
/path/to/.git/lost-found/other/5c0a9d26193bbdfb1235415b2cfce9415d4204cd: ASCII text
```

:::note warn
`file` コマンドは意図しない種別を表示することもあるので注意が必要です。
例えば `file` コマンドが認識しない言語のソースコードなどは `Unicode text, UTF-8 text` と表示されたり、その言語がC/C++風の文法を持っている場合は `C++ source text` と誤って表示されることもあります。
探しているファイル形式のファイルが `file` コマンドでどのような表示になるのか、あらかじめ手元にある複数のファイルで確認しておきましょう。
:::

### `lost-found` 内のファイルを探すときのコツ

確実に違うファイルは随時 `rm` していくと探しやすいです。`lost-found` は先ほどの操作で何回でも復活できるので、間違って `rm` してしまった場合は最初からやり直せば良いです。

また、ファイルを編集して `add` するたびに新たにオブジェクトが登録されていくので、同じファイルを複数回 `add` していた場合は `lost-found` にも同じファイルの複数のバージョンが存在することがあります。ひとつそれらしい物を見つけたとしても、一通り全てのファイルをチェックして候補を洗い出すことをお勧めします。最後に最終候補同士を `diff` で比較して、一番新しいと思われるファイルを復活させると良いでしょう。


## 予防方法: `add` だけしたファイルを `reset --hard` 前後で保持するには

そもそも `add` だけしたファイルを `reset --hard` で失ってしまい、 `lost-found` を調べるような事態そのものを予防できないのでしょうか。

### `reset` 前に `status` を確認する

`reset --hard` するときに `add` だけされたファイルがあるのは大抵の場合誤りなので、 `reset --hard` するときは常に `git status -s | egrep '^(M|.M)'` などでチェックすることを習慣づけるのは一つの予防法になります。

```bash
$ git status -s | egrep '^(M|.M)'
M  newfile
```

このように何か表示されたら `add` だけしたファイルが存在していますので、失っても問題ないか確認します。

### `stash` を使う

例えばステージング用の使い捨て統合ブランチ [^throw-away] を再構築するようなケースでは、意図的に `add` だけして `commit` しない変更を保持しておきたいことがあります。
このような場合には `reset --hard` を実行する前に `git stash` しておくと、 `add` だけしたファイルを `stash` 領域に退避できます。例えば次のように操作すると再構築前後で `add` だけしたファイルを保持できます。

[^throw-away]: 使い捨て統合ブランチについての詳細は公式マニュアルの [gitworkflows(7)](https://git-scm.com/docs/gitworkflows#_throw_away_integration) を参照してください


```bash
$ git stash
$ git reset --hard master
$ git merge topic-1
$ git merge topic-2

...

$ git merge topic-n
$ git stash pop
```

なお、再構築し直した変更内容と `stash` した変更内容がコンフリクトしてしまう場合はあります。この場合 `stash pop` コマンドの出力としてコンフリクトの発生が表示され、ワーキングディレクトリのファイルにコンフリクトマーカーが挿入された状態になるので、対象ファイルを `git status` で確認し、必ずコンフリクトを解消しましょう。


## おわりに

`git add` しただけのファイルを `git reset --hard` で消してしまったときの対処法を紹介しました。誤りや、より良い方法などの提案があればコメントしていただけると嬉しいです。

以上です。