チタン アーマー:さまざまな障害からの復旧

Nicola Paolucci
Nicola Paolucci
リストに戻る

git は高度なツールです。git の哲学は、私が大切にしている考え方でもあります。開発者を賢明で責任感のある人として扱うことです。つまり、git は多くのパワーを人に授けます。同時に、このパワーは災いを招くもとになります。皆さんはおそらくチタン アーマーで防御しているでしょうが、それでも自らを傷つけることがあります。

この記事のトピックは、私の同僚、Charles O'Farrell がよく読まれている「Git が選ばれる理由」で以前説明していた内容を再度述べています。

[...] Git は実際、すべての DVCS オプションの中で最も安全なものです。上で確認したとおり、Git は実際には何も変更せず、新しいオブジェクトを作成するだけです。

[...] Git は実際に行ったすべての変更を追跡し、それを reflog に保存します。すべてのコミットは一意で不変であるため、reflog ではそれに対する参照を保存するだけです。これによって危険から守られ、コードが常に保持されることになりますが、コードを元に戻すために何らかの処理が必要になる場合もあります。

実際のトラブルから回復する方法を、単純なものから重度のものまで、いくつか実例を挙げてみましょう。

目次

  1. (ソフト) リセットを取り消し、削除されたファイルを復元する方法
  2. 対話型リベース中にコミットが失われた場合
  3. 変更をステージングしただけの場合に、reset --hard を元に戻す方法

(ソフト) リセットを取り消し、削除されたファイルを復元する方法

git rm を使用してファイルを削除し、そのすぐ後に作業ディレクトリを git reset した場合 (これを soft リセットと呼びます)、以下のようにファイルは失われ、作業ディレクトリがダーティな状態で表示されます。

 On branch main Changes not staged for commit: (use "git add/rm file..." to update what will be committed) (use "git checkout -- file..." to discard changes in working directory) deleted: test.txt

このファイルを元に戻す簡単な方法が 2 つあります。1 つは git reset --hard を使用する方法で、失われたファイルを元に戻しますが、作業ディレクトリで行ったローカルの変更をすべて削除します

もう 1 つは git checkout test.txt を使って自分でファイルを再チェック アウトすることです。

少し異なるシナリオでは、以前のコミットでファイルが削除された場合、ファイルが削除されたコミットを書き留めておき、その参照を使用して git checkout commit_id~ -- test.txt 直前のコミットを選択して復元できます。

ここで、~ (チルダ) はこの記号より前のものを指します。

対話型リベース中にコミットが失われた場合

対話型リベースは git arsenal で最も便利なツールの1 つであり、対話的にコミットを編集、スカッシュ、削除することができます。

個人的には、コミットを他の人と共有する前に、コミットを整理して合理化すべきと考えています。コミットはそれぞれ、理解しやすく論理的なものであるとよいでしょう。開発の最中、あるいはローカルのプライベート ブランチでは、私のコミットはやや過剰になるかもしれませんが、ある程度満足できるものになれば、じっくり履歴をクリーンアップしていきます。

ただし、対話型のリベース セッション中、誤ってコミット行を削除し、それに気付かず保存した場合はどうなるでしょうか。

この状況は、履歴からコミットを削除した直後だと少し面倒になります。

rebase を開始する前に、使い捨てブランチを作成しているのであれば (いつもそうしているべきです)、問題なくプロセスをすべてやり直すことができます。しかし、そうしていなかった場合はどうでしょう。

大丈夫です。git が備えをしてくれています。何も失われていません。これは [reflog][6 を活用するよい機会です。relog とは、すべてのブランチの先端があった場所を過去 30 日間分自動的に記録する仕組みです。

rebase の開始時に、以下のようにコミットが 4 つあったとします。

 $ git log cfdf880 [2 seconds ago] (HEAD, main) wrote fifth change ab446e6 [34 seconds ago] changed one line 6e1a130 [25 minutes ago] third change 6566977 [26 minutes ago] second change 5feeb33 [26 minutes ago] initial commit

私は最新の 4 つのコミットを rebase し、誤って 1 行削除してしまいます。

$ git rebase -i HEAD~4

エディターが開き、以下のように表示されます。

pick 6566977 second change pick 6e1a130 third change pick ab446e6 changed one line pick cfdf880 wrote fifth change Rebase 5feeb33..cfdf880 onto 5feeb33

Commands:

  • p、pick = コミットを使用する
  • r, reword = use commit, but edit the commit message
  • e、edit = コミットを使用するが、修正のために停止する
  • s、squash = コミットを使用するが、前のコミットに融合する
  • f、fixup = "squash" に似ているが、このコミットのログ メッセージを破棄する
  • x、exec = シェルを使用してコマンド (残りの行) を実行する

これらの行は並べ替えることができ、上から下に実行されます。

If you remove a line here THAT COMMIT WILL BE LOST.

However, if you remove everything, the rebase will be aborted.

空のコミットはコメント アウトされることに注意してください。

コミット行を 1 行 (pick ab446e6 changed one line) 削除し、保存すると次のように表示されました。

 Successfully rebased and updated refs/heads/master.

ここでコミットのリストをチェックすると、1 つ減ったことがわかります。

 $ git log 3dd6845 [6 minutes ago] (HEAD, main) wrote fifth change 6e1a130 [32 minutes ago] third change 6566977 [32 minutes ago] second change 5feeb33 [32 minutes ago] initial commit

しかし、 reflog には失われたコミットが残っています。

 $ git reflog 3dd6845 HEAD@{0}: rebase -i (finish): returning to refs/heads/main 3dd6845 HEAD@{1}: rebase -i (pick): wrote fifth change 6e1a130 HEAD@{2}: checkout: moving from main to 6e1a130 cfdf880 HEAD@{3}: commit: wrote fifth change ----}} ab446e6 HEAD@{4}: commit: changed one line [...]

ここにある ab446e6 は、失ったコミットの sha-1 です。この ID があれば、ファイルを cherry-pick するだけで、コードの中に取り戻すことができます。

 $ git cherry-pick ab446e6 [main 032c6a6] changed one line 1 file changed, 1 insertion(+), 1 deletion(-)

ご覧のように、コミットは履歴に戻っています。

 $ git log 032c6a6 [12 minutes ago] (HEAD, main) changed one line 3dd6845 [12 minutes ago] wrote fifth change 6e1a130 [37 minutes ago] third change 6566977 [38 minutes ago] second change 5feeb33 [38 minutes ago] initial commit

変更をステージングしただけの場合に、reset --hard を元に戻す方法

短期間だけ使用する非公開のブランチであれ、共有のブランチであれ、皆さんが頻繁にコミットして、問題の山から身を守っていることは言うまでもないでしょう。

しかし、今回はコミットしていなくて、以下のようなシナリオになっているとしましょう。作業を完了し、まだコミットはしていませんが、git add でステージング エリアに追加してあります。

ここで悲劇が起こります。あなたは git reset --hard を入力した直後に、ローカルの変更をすべて取り消して、(完全に) 前のコミットの状態に戻してしまったことに気付きます。

厳しい状況になってしまいました。作業をどのように復元できるでしょうか。少なくとも、可能なのでしょうか。もちろん可能です。このソリューションには低レベルの検索が含まれており、アプローチできる方法がいくつかあります。

何かがステージング エリアに追加されると、git はすぐに .git/objects フォルダーに新しいオブジェクトを作成するため、そのフォルダーを探せば、直近に作成されたオブジェクトを見つけることができます。

OSX (および BSD システム) では、次のことが可能です。

find .git/objects/ -type f | xargs stat -f "%Sm %N" | sort | tail -5

Linux(GNU ツール使用)では、次のコマンドが使えます。

find .git/objects/-type f -printf '%TY-%Tm-%Td %TT %p\n' | sort | tail -5

リポジトリの規模が非常に大きい場合、tail -n を使用することによって、最後の n 行だけ表示できます。

結果は次のようになるはずです。

Apr 2 12:26:33 2013 .git/objects//pack/pack-4cf657357973915f6fb6f90a41f69a88c0b08bb7.pack Apr 2 12:29:56 2013 .git/objects//3d/d6845a69cd59dac0851e1ae0bd69dde950c46b Apr 2 12:29:56 2013 .git/objects//68/c983f0cefb4bee2f753516ab6352ee2f2b7d29 Apr 2 12:35:23 2013 .git/objects//03/2c6a653cc00c302020293e020d8d68326b112e Apr 2 15:34:55 2013 .git/objects//df/485610889e98ff773b4440a032ea2ede1338b9

私の例では、サンプル ファイルはちょうど 15:34 頃に失われたので、次のコマンドでファイルを調べて、取り戻すことができます。

git show df485610889e98ff773b4440a032ea2ede1338b9

フォルダーは sha-1 参照の一部であるため、正しい ID を求めるには、/ スラッシュを除くことを忘れないでください。

git には、宙ぶらりんの状態になったオブジェクトを調べ、接続性を確認する特定の低レベルのコマンドが用意されています。それが git fsck です。fsck を使用してこの問題を解決する別の方法については、スタック オーバーフローに関する回答でわかりやすく説明していますのでこちらをご覧ください。

結論

このテーマで取り扱うべき実例やシナリオは数多くあるので、今度また、もっと興味深いケースをお話しする機会があると思います。

今回の内容でぜひ覚えておいていただきたいのは、git を使用しているときに、データを駄目にしたり、失ったりすることはほとんどないという感覚です。では、今回のところはこの辺で。

Git ロッキングの詳細については、@Bitbucket チームと私 @durdn をフォローしてください。

Git を学習する準備はできていますか?

この対話式チュートリアルを利用しましょう。

今すぐ始める