GitトラブルをGetしてしまったら:バージョン管理のお話

非常におかしい題名を笑ってくださってありがとうございます。しかし、おかしくないことがありますが、何か分かりますか。git repoにコミットをプッシュした時で、これをGitHubデスクトップで見ることができます。


Googleを使ってこれが何を意味するか調べてください。

もちろん、いけている人Git Towerを使い、本当にいけている人はコマンドラインのみを使っていることを知っています。我々は本当にいけているので、ここではコマンドラインを使って問題を解決します。実は他に選択肢がないのです。この記事で、git専門のコマンドライン知識が全くないのにも関わらず、コマンドラインを使い、全くプログラムの書き手に非がないのに、突然git repoが壊れてしまう問題を解決する冒険に誘います。少なくとも自分に非がないのに突然壊れてしまった時のパニックの度合いを見ていただけるかと思います。

第1段階は問題を診断することです。しかし、私と同じようにメタ問題も抱えている場合、理解しきれていないツールに頼っているためにその問題が生じているとしたら、診断は難しいでしょう。同情してくれる専門家を見つけて助けてもらうのも一苦労でしょう。もしかしたら、誰にどのような質問を投げかければいいのかも分かっていないかもしれません。いずれにせよ、忍耐強く、几帳面で学ぶ意欲があれば、何がどのように動作し、どのようにすれば自分の抱えているジレンマを解決できるかが分かるでしょう。だからこそ、gitのリファレンスを読むことになり、git-fsckコマンドの発見に至ったのです。これを律儀に自分のレポジトリのルートディレクトリで実行しました。その結果が下記の出力(の切り取り)です。

> git fsck

...

error: object file .git/objects/67/99ddac675cab54060cdfb066dbfadb6708fc3f is empty
error: object file .git/objects/67/99ddac675cab54060cdfb066dbfadb6708fc3f is empty
fatal: loose object 6799ddac675cab54060cdfb066dbfadb6708fc3f (stored in .git/objects/67/99ddac675cab54060cdfb066dbfadb6708fc3f) is corrupt

上記は一体何なのでしょう、そして恐らく、プロジェクトがパックマンのブリンキーを発症していれば見たことがあるのではないでしょうか。これは、破損したレボジトリです。そう。分かりましたが、何をすればいいのでしょうか。人生の意味を知りたい場合は、神様に答えを求めます。この場合は、遥か昔に受信したLinus Torvaldsからのメールを読んでみたところ、同じような状況に対する内容でした。

実はgit repoは、BLOBやツリー、コミットのような異なる種類のバイナリオブジェクトグラフなのです。BLOBオブジェクトは暗号化でハッシュされています。つまり、それぞれのファイルを表すデータのBLOBなのです。これらのBLOBオブジェクトは互いに依存していませんが、ツリーオブジェクトと呼ばれるものでリンク付けされています。これによって効果的にBLOBをグループ化し、ファイルシステムのディレクトリ構造に似た配置することができます。最後にツリーやBLOBの変更を記録するために必要な情報を保持するコミットオブジェクトがあります。コミットオブジェクトも連続してリンク付けされています(期待どおりだと思います)。

Gitはこれらのオブジェクトを.git/objects/に置かれている一連のネストされたディレクトリに格納します。オブジェクトはコミットIDの最初の数文字に応じて置かれています。例えば、オブジェクト6799ddac675cab54060cdfb066dbfadb6708fc3f67/という名のディレクトリにファイル99ddac675cab54060cdfb066dbfadb6708fc3fとして格納されます。つまり、オブジェクトの正式名は格納されているディレクトリとそのディレクトリにある特定のファイルの組み合わせになります。

そのため、格納したコミットオブジェクトの1つが破損してしまうと、コミットのリンク付けが壊れることになるため、レポジトリ全体が全く使えないバイトの山でしかなくなってしまいます。これが悪い方のニュースです。良い方のニュースは、レポジトリが個別ファイルの集まりであるため、オブジェクトの1つが修復不可能なほど破損していたとしても、ファイルを健全な状態に復元することができる可能性があるということです。これはあくまでも十分に正確な手術を実施できればの話です。

まさに私が試みたことです。

Linusの助言どおり、破損したコミットオブジェクトファイルである./git/objects/67/99ddac675cab54060cdfb066dbfadb6708fc3fを別の場所に移動しました。壊れたオブジェクトは好きな場所に置くことができます。最終的にはゴミ箱行きになるとは思いますが。

BLOBオブジェクトの67a45ac2f58a444fa4db11cd9ab7e024a8e35dcfの時と同じエラーメッセージが出たので、これも移動しました。そこで私はファイルシステムチェックを再度実行しました。

> git fsck

Checking object directories: 100% (256/256), done.
Checking objects: 100% (8970/8970), done.
broken link from tree 03a88f876eb3f6157f76461a3ae6cb18bbb86561
to blob 67a45ac2f58a444fa4db11cd9ab7e024a8e35dcf
dangling commit 76814e15074b540bc2f7e78daf3f5175a8759523
missing commit 6799ddac675cab54060cdfb066dbfadb6708fc3f
missing blob 67a45ac2f58a444fa4db11cd9ab7e024a8e35dcf
dangling blob 2a60520000698ad964e4e61fab31f9b862763550
dangling commit 41634cd81964068acb153bfa355d63bd80fc7cef
dangling commit 5bf415e2bdbc47822ae99b64c2a0f6b4f288eefb

注意点は、Linusはgit fsck --fullを使うことを提案していますが、現在は規定の動作となっていることです。

「dangling commit」メッセージは無視しますが、「リンク切れ」メッセージはツリーオブジェクトが移動されたBLOBオブジェクトを指していることを知らせてくれています。この情報を見せる趣旨で意図的にリンクを外しました。ツリーオブジェクトの03a88f876eb3f6157f76461a3ae6cb18bbb86561は、BLOBオブジェクトの67a45ac2f58a444fa4db11cd9ab7e024a8e35dcfを指すべきですが、BLOBが存在しません。コミットオブジェクトの6799ddac675cab54060cdfb066dbfadb6708fc3fも移動したので、存在しない旨のメッセージが出ます。ここまでは順調です。

Linusの助言どおりに進めると、上記で呼び出したツリーオブジェクトの内容をリストアップするためのgit-ls-treeコマンドを使用する際に必要な情報が十分揃いました。

> git ls-tree 03a88f876eb3f6157f76461a3ae6cb18bbb86561

100644 blob 312d8994f1005a9563a9410c592b27000c201101 building-test.js
100644 blob f84006fd14c6d4b2ccc3ef22b2fe02abf535bd1a folds-test.js
100644 blob 67a45ac2f58a444fa4db11cd9ab7e024a8e35dcf index.js
100644 blob 8b1f47bce7ec989dff7e936279d63d1d02f6a92d indexing-test.js
100644 blob 3f2b45f8cd9dfd486c8e821ee672ed66a34768df inf-test.js
100644 blob 0e6d1985aa59d17e2115bc6c7936d2ac88b00457 list-test.js
100644 blob 40310b5df53691d0e1ba4118c0e3ab66ed766990 reducing-test.js
100644 blob 8244d5fb2768ad5c7c33890ee26c797c2df6262b searching-test.js
100644 blob ff34a2f15faf6eec7d9c9635e79d2a0abdadfb42 sub-test.js
100644 blob ac0c029cc64870c7445c4bdd9d7fe20646b5cc33 trans-test.js
100644 blob 99781d303e90b7aa4de8d630c1053a42f87e8331 zip-test.js

上記のリストに目を通すと、問題となるBLOB67a45ac2f58a444fa4db11cd9ab7e024a8e35dcfと関連ファイルのindex.jsを見つけました。これで、問題の原因が分かりましたが、そもそもどのバージョンのファイルが問題を起こしたのかが分かっていませんでした。コマンドラインに戻りましょう。

> git log --raw --all

commit cf63a71497e027d96614cfff6ba1d297f1a1a26e
Author: Steven Syrek <steven.syrek@example.com>
Date: Mon Jul 18 11:55:40 2016 -0400

Add tests for set operations on lists

:100644 100644 67a45ac... c1c2f99... M test/list/index.js
:000000 100644 0000000... 23c47fe... A test/list/set-test.js

commit f3bc2c55b22deb889f99cdd45663c20a8e8e79c1
Author: Steven Syrek <steven.syrek@example.com>
Date: Mon Jul 18 11:14:13 2016 -0400

Add tests for list zipping and unzipping functions and remove exponentiation operator from tests and examples

:100644 100644 01af47b... 3b1bf35... M source/list/zip.js
:100644 100644 21206e2... 67a45ac... M test/list/index.js
:000000 100644 0000000... 99781d3... A test/list/zip-test.js

注釈
Add tests for set operations on lists 一覧の集合演算用テストを追加
Add tests for list zipping and unzipping functions and remove exponentiation operator from tests and examples 圧縮機能や解凍機能用テストを追加し、指数演算子をテストや事例から削除

git-logコマンドと–rawオプションと–allオプションでレポジトリのコミット歴を表示することができます。ここでは、必要な部分しかお見せしません。オブジェクト21206e20386e0365bc6f15d0ccd372b1c72b5667があるのが分かります。そしてその後に破損したオブジェクト67a45ac2f58a444fa4db11cd9ab7e024a8e35dcfがあり、次にオブジェクトc1c2f99072ef41aca89e963cfb0143f897e0de78の後続のコミット(逆順にリストされます)があります。

どのファイルバージョンが壊れたコミットの前後にあるか判明したため、この時点で完了だとLinusは言いました。

> これができれば、git hash-object -wを使って行方不明のオブジェクトを再び作成することができ、レポジトリを回復することができるよ。

残念ながら、これを実行した後もレポジトリを回復することができませんでした。この辺りで異変が生じました。過去にもらったLinusからの助言を使い果たしてしまいました。現在のLinus(過去のLinusでもありますが)は、私を助けてくれることよりもやるべきことがあるでしょう。そのため、私にはプロの開発者が毎日使っている賢明なトラブルシューティングプロセスを使う道しか残されていませんでした。

Google。Google。

スタックのオーバーフローです。落ちてしまいました。

このとにかく試してみるといったような進め方をしているうちに、git-diffコマンドを初め、いくつかのことを行なってみました。行方不明のオブジェクトを自動的に再作成できないのであれば、手動であればできるかもしれないと必死に思い込みました。

> git diff 206e20386e0365bc6f15d0ccd372b1c72b5667..c2f99072ef41aca89e963cfb0143f897e0de78

fatal: ambiguous argument '206e20386e0365bc6f15d0ccd372b1c72b5667..c2f99072ef41aca89e963cfb0143f897e0de78': unknown revision or path not in the working tree.

おっと。先頭の文字を忘れてしまいました。

> git diff 21206e20386e0365bc6f15d0ccd372b1c72b5667..c1c2f99072ef41aca89e963cfb0143f897e0de78

diff --git a/21206e20386e0365bc6f15d0ccd372b1c72b5667..c1c2f99072ef41aca89e963cfb0143f897e0de78 b/c1c2f99072ef41aca89e963cfb0143f897e0de78
index 21206e2..c1c2f99 100644
--- a/21206e20386e0365bc6f15d0ccd372b1c72b5667..c1c2f99072ef41aca89e963cfb0143f897e0de78
+++ b/c1c2f99072ef41aca89e963cfb0143f897e0de78
@@ -25,3 +25,7 @@ export * from './sub-test';
export * from './searching-test';

export * from './indexing-test';
+
+export * from './zip-test';
+
+export * from './set-test';

上記はindex.js内で破損したコミットの両側の2つのコミットの間で変更が生じた行です。違いが+で記され、さらに関連を明記するために行が追加されています。削除された行があった場合は、-で記されます。全く同じファイルがハッシュされた場合、全く同じハッシュキーが作成されるはずなので、変更された行を削除し、手作業でコミットを再作成する強硬手段を講じました。

> git hash-object -w ./test/list/index.js

2a60520000698ad964e4e61fab31f9b862763550

ダメです。+の行のみを削除してもう一度試してみましょう。

> git hash-object -w ./test/list/index.js

21206e20386e0365bc6f15d0ccd372b1c72b5667

ダメですが、面白い結果が出ました。どうにか、破損コミットが生じる前のオブジェクトの元の状態を再作成することができましたが、本当は正しい中間状態のオブジェクトを再作成しようとしていたのではないかと思います。いつになく正しいgitの健全化フェーズ経験中だったため、幸運にも可能性は多くはありませんでした。そのため、ファイルを再び変更し、深みにはまる前に追加した覚えのある+の行のみを上記に追加しました。

git hash-object -w ./test/list/index.js

67a45ac2f58a444fa4db11cd9ab7e024a8e35dcf

できました。

> git fsck

Checking object directories: 100% (256/256), done.
Checking objects: 100% (8970/8970), done.
dangling commit 76814e15074b540bc2f7e78daf3f5175a8759523
missing commit 6799ddac675cab54060cdfb066dbfadb6708fc3f
dangling blob 2a60520000698ad964e4e61fab31f9b862763550
dangling commit 41634cd81964068acb153bfa355d63bd80fc7cef
dangling commit 5bf415e2bdbc47822ae99b64c2a0f6b4f288eefb

そう、これで健全なBLOBになりしたが、これを指すコミットオブジェクトがまだ行方不明です。どうすればいいのでしょうか。git-gcに助けてもらえるかもしれません。

> git gc

error: Could not read 6799ddac675cab54060cdfb066dbfadb6708fc3f
error: Could not read 6799ddac675cab54060cdfb066dbfadb6708fc3f
warning: reflog of 'HEAD' references pruned commits
warning: reflog of 'refs/heads/restructure' references pruned commits
error: Could not read 6799ddac675cab54060cdfb066dbfadb6708fc3f
fatal: Failed to traverse parents of commit b267a6a8264c0cdc72d047049610fc91e9f7c06f
error: failed to run repack

助けてもらえませんでしたね。全てのごみをガーベジコレクションできるはずでした。そして全てを修復できるはずだったのですが。なぜ、こうなると思ったのかは不明ですが、期待したかったのだと思います。本当に願いを込めていました。そして呪いをかけてしまいました。上記の「reflog」メッセージに気付き、私は次のすばらしいアイデアを検討していました。

> git reflog expire --all --stale-fix

error: Could not read 6799ddac675cab54060cdfb066dbfadb6708fc3f
fatal: Failed to traverse parents of commit b267a6a8264c0cdc72d047049610fc91e9f7c06f

誰もが知っているとおり、コマンドラインツールを使用する際に、オプションを追加すればするほど、男らしくなります。どのように機能するか知らなくてもいいのです。真の男はmanページを読まず、さっさといじって壊すのです。しかも、私にはレポジトリを再reflogするのがいいと思えました。しかしダメでした。これも思いどおりの結果にはなりませんでした。

既に多くのものを試し深みにはまっていました
ので、何でも試す覚悟でいましたが、センス計測は考慮していませんでした。ログを再度確認することにしましたが、いつもログ確認は負けを認め、静かに涙を流す場所を探すための第一歩に感じられまました。しかし、自分だけが見落とし、他の人には見えていた解決策が奇跡的に現れるのではないかとの思いもありました。

> git log 6799ddac675cab54060cdfb066dbfadb6708fc3f

fatal: bad object 6799ddac675cab54060cdfb066dbfadb6708fc3f

ダメです。

> git ls-tree 6799ddac675cab54060cdfb066dbfadb6708fc3f

fatal: not a tree object

ダメではなく、そうすればよかったのです。どうにかレポジトリのrestructureブランチのみのログだけを調べるというすばらしいアイデアに気付くことができました。restructureブランチは致命的なカーソル点滅が現れる前まで作業していた場所です。

> tail -n 40 .git/logs/refs/heads/restructure

...

44dc22e706fb029a9c96f3bd125755fd55ac882b 6799ddac675cab54060cdfb066dbfadb6708fc3f Steven Syrek <steven.syrek@example.com> 1468788351 -0400 commit: Replace isEq function in all tests with should.eql
6799ddac675cab54060cdfb066dbfadb6708fc3f b267a6a8264c0cdc72d047049610fc91e9f7c06f Steven Syrek <steven.syrek@example.com> 1468789759 -0400 commit: Separate out functions in Ord tests

...

なるほど。『もしかしたら、BLOBオブジェクトに実行したdiffコマンドと同じ様な事をコミットオブジェクトに行えばいいのかもしれない』と思いました。なので、下記を実行しました。

> git diff b267a6a8264c0cdc72d047049610fc91e9f7c06f..44dc22e706fb029a9c96f3bd125755fd55ac882b

...

(bunch of irrelevant stuff)

注釈bunch of irrelevant stuff 無関係なものがたくさん

やっぱり違いました。でも少なくとも、まだ44dc22e706fb029a9c96f3bd125755fd55ac882bというコミットのハッシュタグは残っているので、何かはできるでしょう。私の目の前を真っ暗にする強敵6799ddac675cab54060cdfb066dbfadb6708fc3fの前に立ちはだかる、最後の味方です。ドキュメントを調べました。インターネットも参照しました。そしてもう一手、策を講じてみたのです。

> git branch -l rewrite-tests 44dc22e706fb029a9c96f3bd125755fd55ac882b

ここでやったのは、スタートのポイントで44dc22e706fb029a9c96f3bd125755fd55ac882b、つまり最後の味方を使ってrewrite-testsと呼ばれる新しいブランチを作成したのです。git-branch docsの中で特定されたgit branch [--set-upstream | --track | --no-track] [-l] [-f] []というスタートパターンと一致させました。-1というオプションが何なのか、あるいは本当に必要かどうかさえ正直に言えば分かりません。ただ、誰かにそれを使えと言われたので、使ってみました。

それから全てのファイルをレポジトリから出し、次のうちのどれか1つを実行しました。

git checkout rewrite-tests

git-checkoutコマンドはHEADを特定のブランチに配置します。言い換えれば、gitにrewrite-testsというブランチで作業をしたいと言ったのです。それから私は、全てのファイルのバックアップをとり、再度コミットし、restructureブランチが衰退・消滅するまで放っておきました。

驚いたことに、それでできてしまいました。最悪の部分は過ぎたので、何をするにも早すぎるということはありません。ハッシュキーを見ていきましょう。発展させるべき新しいブランチがあり、(間にあったコミットのいくつかは早期に消えたにも関わらず)私の仕事は何ひとつ失われませんでした。結果的に、私は全てをmasterに詰め込みました。しかし今では、どんなレポジトリでもイベントでも、このような騒動が再び起こりそうなそのブランチで直接作業することはなるべく避けています。

私は今でも時々、彼女のウインクを見てニンマリしています。嘘です。そんなことはしません。でも皆さんは、よかったら私がせっせと修復を繰り返したレポジトリを見てみてください。そこには私のmaryamyriameliamurphies.jsプロジェクトがあります。私が全て独りで作った、相当な量のコードがあります。それをダメにしたかもしれないと思った時の気持ちは、想像がつくでしょう。修復方法を見つけた時も気持ちも分かりますね。

この記事の最初で、破損したgitのレポジトリを提案しました。これはバラバラのオブジェクトで構成されるので、注意深い外科手術のような処置でよみがえる可能性があるのです。私はそのような手技を、2つ見つけました。1つ目は、破損したBLOBに対するものですが、害となっているBLOB(たち)を切り離し、それからオリジナルのファイルを作り直すことで傷口を縫合するというものです。2つ目は、1つ目の方法がうまくいかなかった場合(あるいは問題がBLOBではなく、破損したコミットのオブジェクトだった場合)、行き詰った箇所で損傷したブランチを切断して、切り株に新しいブランチを接ぎ木し、この手順の被害者であったいずれかのファイルを再コミットします。

この2つは異なる解決法ですが、どちらも、ファイルの操作だけだったとしても、比較的低いレベルでデータ構造の修復を伴うという点では似通っています。実際のところ、修復の操作が可能なのは、はっきり言えばgitレポジトリが一連のファイルとして保存されるおかげです。ファイルシステムがインターフェースやコマンドラインを伴う大きなデータ構造である限りは、git repoもインターフェースやgitコマンド、様々なサブコマンドや選択肢を持つファイルシステムのようなデータ構造になります。つまり、コマンドラインからファイルシステムを使う方法を学べば、gitの使い方を学べるということです。

この記事が、あなたがいつか遭遇するかもしれない問題を解決するための処方箋になることを願います。残念ながら、私はありふれた激励の言葉しか持ち合わせていませんが、「きっとできます! 」このようなエラーを起こす原因を探るのはとても大変です。修復する最善の方法については言うまでもありませんが、一般化するのも大変です。みなさんにできるのは、問題を掘り下げ、エントロピーと戦うことです。ツールを恐れるのではなく、ツールのことを知りましょう。経験だと思っていろいろ挑戦してみましょう。そして科学者風の友達に助けを求める前に、バックアップをとることを忘れずに!

きちんとした道徳的な判断ができれば、これまでにぶつかった恐ろしく残虐なステータスバー、あるいは「コミットは早期に、そして頻繁に」と訴える鈍いコマンドラインより、修復はもっともっと簡単に、そして費用も少なく抑えられるでしょう。結局のところ、予防的治療が最良の薬なのです。