Chapter 7. Git のコンセプト

Table of Contents

オブジェクトDB
索引(index)

Git は少ない数のシンプルだが強力なアイデアで成り立っています。 それらを理解しなくても git を利用することはできますが、 理解することで git をより直感的に理解できます。

最も重要なコンセプトである オブジェクトデータベース索引(index) の説明から開始しましょう、

オブジェクトDB

既に the section called “履歴の理解:コミット” で見てきたように、全てのコミットは 40桁の "オブジェクト名" で格納されています。実際、プロジェクトの履歴を 表現するのに必要な全ての情報は、そのような名前のオブジェクトとして格納されています。 それぞれの名前はオブジェクト内容の SHA-1 ハッシュによって 計算されています。SHA-1ハッシュは暗号学的ハッシュ関数です。 それはつまり、同じ名前を持つ2つの異なるオブジェクトを見つけるのが 不可能であることを意味します。このことは多くの利点を持っています。 とりわけ:

  • Git は2つのオブジェクトが同じであるかどうかを 名前を比較するだけで高速に判断できます。 オブジェクト名が全てのリポジトリに対して同じ方法で計算されるので、 2つのリポジトリに格納した同じ内容は、常に同じ名前で格納されます。
  • オブジェクト名が自身の内容の SHA-1 ハッシュ値と一致しているかを 確認することで、Git はオブジェクトを読み込んだ時に、エラーを検出できます。

(オブジェクトの形式と SHA1 計算の詳細は the section called “オブジェクトの保管形式” を参照してください)

Gitが扱う オブジェクトには4種類あります:"blob", "tree", "commit" そして "tag" です。

  • "blob" オブジェクト はファイルデータを格納するのに使用されます。
  • "tree" オブジェクト は1つ以上の "blob" オブジェクトに リンクし、ディレクトリ構成を作ります。さらに、tree オブジェクトは 他の tree オブジェクトを参照できます。従って、ディレクトリ階層を作成できます。
  • "commit" オブジェクトはディレクトリ階層とリンクし、 リビジョンの 有向非巡回グラフを作ります。— 各コミットは そのコミット時点でのディレクトリ階層を指し示すオブジェクトの名前を 含みます。さらに、commit はそのディレクトリ階層に到った経路を示す "親" のコミットオブジェクトを参照しています。
  • "tag" オブジェクト はあるオブジェクトを特定する シンボルの役目をし、また他のオブジェクトに署名をつける目的でも 利用できます。"tag" オブジェクトは、他のオブジェクトの名前と型、 そして(もちろん)シンボリック名を持ち、時には署名も含んでいます。

オブジェクトタイプの詳細:

Commit オブジェクト

"commit" オブジェクトはツリーの物理的な状態にリンクし、 また、どのようにしてその記述に至ったかの情報も一緒に含んでいます。 —pretty=raw オプション付きで git-show(1) または git-log(1) を 実行すると、特定のコミットの内容を確認できます:

$ git show -s --pretty=raw 2be7fcb476
commit 2be7fcb4764f2dbcee52635b91fedb1b3dcf7ab4
tree fb3a8bdd0ceddd019615af4d57a53f43d8cee2bf
parent 257a84d9d02e90447b149af58b271c19405edb6a
author Dave Watson <[email protected]> 1187576872 -0400
committer Junio C Hamano <[email protected]> 1187591163 -0700

    Fix misspelling of 'suppress' in docs

    Signed-off-by: Junio C Hamano <[email protected]>

このように、コミットは次のように定義されています:

  • tree: ある時点のディレクトリの中身を表現する ツリーオブジェクト(以下で定義)の SHA-1 名。
  • parent(s): プロジェクト履歴内のすぐ1つ前の状態を表している 複数のコミットの SHA-1 名。上記例では1つの parent があります; マージコミットでは、1つ以上の場合があります。 parent のないコミットは "root" コミットと呼ばれ、 プロジェクトの初期リビジョンを表します。各プロジェクトは最低1つの root を 持つ必要があります。プロジェクトは複数の root を持つこともできますが、 あまり一般的ではありません(良いアイデアではありません)
  • author: この変更に対する責任者の名前と日付。
  • committer: このコミットを実際に作成した担当者と日付。 committer は author とは一致しないかもしれません、例えば、 author がパッチを作成しそれを E-Mailで別の人がそのパッチをコミットする ことがあります。
  • このコメントに関するコメント。

注意:コミット自身は実際にどのような変更がされたかの情報を持っていません; 全ての変更はコミットが参照しているツリーとparents から連想されるツリーとの比較 によって計算されます。特に、git はファイル名の変更を明示的には記録しようと しません。しかし、同じデータをもつファイルが存在する場合に名前変更であると 認識する方法があります。(例えば、git-diff(1) の -M オプションを参照)

コミットは通常 git-commit(1) によって作成されます。 デフォルトではその親を現在のHEADとしたコミットが作成され、 そのツリーは現在の索引に格納されている内容が使用されます。

Tree オブジェクト

git-show(1) コマンドは tree オブジェクトに対しても 使用することができますが、git-ls-tree(1) の方が より詳細な情報を表示します:

$ git ls-tree fb3a8bdd0ce
100644 blob 63c918c667fa005ff12ad89437f2fdc80926e21c    .gitignore
100644 blob 5529b198e8d14decbe4ad99db3f7fb632de0439d    .mailmap
100644 blob 6ff87c4664981e4397625791c8ea3bbb5f2279a3    COPYING
040000 tree 2fb783e477100ce076f6bf57e4a6f026013dc745    Documentation
100755 blob 3c0032cec592a765692234f1cba47dfdcc3a9200    GIT-VERSION-GEN
100644 blob 289b046a443c0647624607d471289b2c7dcd470b    INSTALL
100644 blob 4eb463797adc693dc168b926b6932ff53f17d0b1    Makefile
100644 blob 548142c327a6790ff8821d67c2ee1eff7a656b52    README
...

このように、tree オブジェクトは名前順でソートされたエントリの一覧を含んでおり、 各エントリは mode, オブジェクトタイプ、SHA-1名、名前を持っています。 tree は 1つのディレクトリツリーの中身を表現します。

オブジェクトタイプが blob の場合はファイルデータであることを表し、 tree である場合は、サブディレクトリであることを表しています。 tree と blob は他のオブジェクトと同じようにその中身の SHA-1値によって 名前が付けられていて、その中身が(全てのサブディレクトリの内容も含めて)同じ場合 にのみ同じ SHA-1 名となります。この仕組みにより git は2つの関連する tree オブジェクト間の差分を高速に調べることができます。

(注意:サブモジュールが存在する場合、tree は commit をエントリに 持つことがあります。Chapter 8, サブモジュール のドキュメントを参照。)

注意:ファイルは全て 644 または 755 のモードとなります:実際、git は 実行パーミッションだけを管理しています。

Blob オブジェクト

git-show(1) を使用すると blob の内容を参照できます; 例として、上記ツリーの blob エントリ "COPYING" を確認します:

$ git show 6ff87c4664

 Note that the only valid version of the GPL as far as this project
 is concerned is _this_ particular version of the license (ie v2, not
 v2.2 or v3.x or whatever), unless explicitly otherwise stated.
...

"blob" オブジェクトはバイナリの blob データであるにすぎません。 参照や属性といったものは持っていません。

blob はそれ自身のデータによって完全に定義されるため、 ディレクトリツリー内(またはリポジトリ内の異なるバージョン)に 2つのファイルがあり、それらが同じ内容であるなら、同じ blob オブジェクトを 共有します。オブジェクトはディレクトリツリーの位置に完全に独立しており、 ファイル名を変更してもそれに対応するオブジェクトは変更されません。

注意:全ての tree と blob オブジェクトは git-show(1) に <revision>:<path> の引数を付けて実行することができます。 これにより、現在チェックアウトしていないツリーの中身を ブラウズすることができます。

Trust

あるソースから blob の SHA-1値と(信頼できないかもしれない)ソースの中身 を受け取ったとします。この場合でも SHA-1値が一致する限りはその内容が 正しいと信頼することができます。何故なら SHA-1値は同じハッシュ値を生成 する異なるファイルを見つけることが困難なように設計されているからです。

同様に、トップレベルのツリーオブジェクトの SHA-1値を信頼するだけで それが参照する全ディレクトリの内容を信頼することができます。 また、信頼するソースからコミットのSHA-1値を受け取ったなら、 そのコミットの親から到達可能なコミットの全履歴とコミットが参照する ツリーの内容全てを容易に信頼することができます。

従ってトップレベルのコミット名を含む 一つの 特定のノートに デジタル署名するだけで、システムに信頼性を持たせることができます。 デジタル署名はあなたがそのコミットを信頼していることを示し、 コミット履歴の不変性は全ての履歴が信頼できることを示しています。

言い換えると、トップレベルのコミットのSHA-1値を伝えるメールを作成し、 GPG/PGPでデジタル署名するだけで、容易に全ての履歴の妥当性を示す ことができるということです。

この仕組みを支援するため、gitはtagオブジェクトも用意しています。

タグ オブジェクト

tag オブジェクトはオブジェクトとオブジェクトタイプ、タグ名、 タグの作成者、メッセージ(これには署名が付けられることがあります)を 含んでいます。このことは git-cat-file(1) で確認できます:

$ git cat-file tag v1.5.0
object 437b1b20df4b356c9342dac8d38849f24ef44f27
type commit
tag v1.5.0
tagger Junio C Hamano <[email protected]> 1171411200 +0000

GIT 1.5.0
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.6 (GNU/Linux)

iD8DBQBF0lGqwMbZpPMRm5oRAuRiAJ9ohBLd7s2kqjkKlq1qqC57SbnmzQCdG4ui
nLE/L9aUXdWeTFPron96DLA=
=2E+0
-----END PGP SIGNATURE-----

タグの作成方法と検証方法は git-tag(1) コマンドを参照してください。 (注意: git-tag(1) は'軽量'タグを作成することもできます。 これは、tagオブジェクトとは全く異なるもので、'refs/tags/'で始まる 単なる参照です)

gitがどのようにオブジェクトを効率的に保管するか: packファイル

新規作成されたオブジェクトは最初はオブジェクトの SHA-1ハッシュ値の 名前でファイルとして保存されます(.git/objects 内に保管されます)。

残念なことに、プロジェクトに大量のオブジェクトが作成されると、 この仕組みでは不十分になります。古いプロジェクトで以下を実行 してみてください:

$ git count-objects
6930 objects, 47620 kilobytes

最初の数字はファイルとして保管されているオブジェクト数です。 二つ目の数字はこれらの "遊離した" オブジェクトによって消費される 容量の合計値です。

これら遊離したオブジェクトを "packファイル" に移動することで ディスク容量を節約し、またgitを高速化することができます。 pack ファイルはオブジェクトのグループを十分に圧縮した形式で保管します: packファイル形式の詳細は technical/pack-format.txt を参照してください。

遊離したオブジェクトを pack に移動するには、git repack を実行するだけです:

$ git repack
Generating pack...
Done counting 6020 objects.
Deltifying 6020 objects.
 100% (6020/6020) done
Writing 6020 objects.
 100% (6020/6020) done
Total 6020, written 6020 (delta 4070), reused 0 (delta 0)
Pack pack-3e54ad29d5b2e05838c75df582c65257b8d08e1c created.

その後、

$ git prune

を実行すると pack に含まれる全ての "遊離した" オブジェクトは削除されます。 この操作は参照されていないオブジェクト(例えば、"git reset" によって コミットを削除した場合などに作成される)も全て削除します 遊離したオブジェクトが削除されたことは .git/objects ディレクトリを 見るか、次のコマンドを実行することで確認できます。

$ git count-objects
0 objects, 0 kilobytes

オブジェクトファイルが削除されても、そのオブジェクトを参照する 全てのコマンドは以前と同じように動作します。

The git-gc(1) コマンドは repack、prune などを実行してくれるので 通常は高レベルであるこのコマンドのみ使用します。

Dangling オブジェクト

git-fsck(1) コマンドは dangling オブジェクトに関する メッセージを表示することがありますが、これは問題ではありません。

dangling オブジェクトが作成される主な原因は ブランチをリベースした場合や、 他のユーザがリベースしたブランチを pull した場合です。 — Chapter 5, 履歴を再編集し、一連のパッチを管理する 参照。この場合、ブランチの古い head は まだ存在していて、head が参照していたオブジェクトも全て残っています。 ブランチのポインタは、他の場所に移しかえられているので、存在しませんが。

dangling オブジェクトが作成される他の例もあります。 例えば、ファイルを "git add" したが、そのファイルに別の変更を加えて コミットしたような場合です。 — この場合、もともと add していた内容は、どのコミットとツリーにも 参照されず、dangling blob オブジェクトとなります。

同様に、"再帰的に" マージを実行した際に、マージ内容が複雑で 複数のマージベースが存在するような場合(あまり発生することはありませんが)には、 中間状態のツリーが一時的に作成されます。これら中間状態の作成時に オブジェクトが作成されますが、それらは最終的なマージ結果では 参照されることがありません。従ってこれらも "dangling" となります。

一般に、dangling オブジェクトが存在しても心配する必要はありません。 どちらかといえば、それらは役に立つものです:何か操作間違いをした時に、 dangling オブジェクトを使用すると元の状態に戻すことができます。 (リベースした後に誤りに気が付いた時、 ある古い dangling の状態に head をリセットすることができます)

commit の場合は、次のようにします:

$ gitk <dangling-commit-sha-goes-here> --not --all

これは、指定したコミットから到達可能だが他のブランチやタグ、その他の参照からは 到達できない履歴すべてを表示します。そのうちのどれかが望むものであるなら、 新しい参照を作成します、例えば次のように。

$ git branch recovered-branch <dangling-commit-sha-goes-here>

blob と tree の場合は、同じようにはできませんが、 次のようにして確認することができます。

$ git show <dangling-blob/tree-sha-goes-here>

これは、blob の中身が何であるか(ツリーの場合、ディレクトリの "ls" の内容) を表示するので、その dangling オブジェクトを残すのにどのような操作が 必要となるかを教えてくれるでしょう。

通常、dangling blob と tree はあまり必要になることはありません。 大抵はコンフリクトマーカのついたマージの途中状態であるか、 "git fetch" を Ctrl+C や何かで中断した際に作成されたもので、 宙ぶらりんで役に立ちません。

いずれにしろ、dangling 状態に興味がないことを確認したのなら、 到達できないオブジェクト全てを破棄することができます:

$ git prune

"git prune" は必ず休止状態にあるリポジトリでだけ実行してください。 — これはファイルシステムの fsck によるリカバリのようなものです: ファイルシステムがマウントされている時には行なうべきではありません。

("git fsck" についてもこれと同じことが言えます。ところで、 git fsck は実際に決してリポジトリを変更することはなく、 検出されたものを報告するだけで、実行しても危険なものではありません。 誰かがリポジトリを変更している最中に実行した場合、 紛らわしく恐ろしいメッセージが表示されますが、悪いことは行なわれません。 それとは反対に "git prune" を他のユーザがリポジトリを変更している最中に 実行するのは 悪い アイデアです。)

リポジトリの破損からの復旧

git は意図的にデータを慎重に扱います。しかし、git 自身にバグがなかったとしても ハードウェアやオペレーティングシステムのエラーによりデータが壊れることがあります。

そのような問題に対する第1の防御はバックアップです。 clone コマンドを使用するか cp, tar または他のバックアップメカニズムを 使用して git ディレクトリをバックアップすることができます。

最後の手段は、破損したオブジェクトを探し出して手作業で置き換えることです。 破損しかかっている最中であっても、作業をする前にリポジトリを バックアップしてください。

1つの blob が紛失または破損している場合を考えます。 そのような場合は時に解決できることがあります。 (ツリー、そして特にコミットの紛失から復旧する場合はより困難です)

開始する前に、破損している箇所を git-fsck(1) を使用して 確認します;これには多大な時間を必要とするかもしれません。

次のように出力されたとします:

$ git fsck --full
broken link from    tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
              to    blob 4b9458b3786228369c63936db65827de3cc06200
missing blob 4b9458b3786228369c63936db65827de3cc06200

(通常、これらと一緒に "dangling オブジェクト" のメッセージも表示 されますが、これらは興味深いものではありません)

4b9458b3 の blob が紛失しており、2d9263c6 のツリーが それを参照していることがわかります。その紛失したblobのコピーを 他のリポジトリなどから見つけることができるなら、それを .git/objects/4b/9458b3… に移動すれば修復は完了です。もしそれが できない場合でも、git-ls-tree(1) を用いて、それが何を指し示して いるかを確認することができます:

$ git ls-tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
100644 blob 8d14531846b95bfa3564b58ccfb7913a034323b8    .gitignore
100644 blob ebf9bf84da0aab5ed944264a5db2a65fe3a3e883    .mailmap
100644 blob ca442d313d86dc67e0a2e5d584b465bd382cbf5c    COPYING
...
100644 blob 4b9458b3786228369c63936db65827de3cc06200    myfile
...

これにより、紛失した blob が "myfile" という名前のファイルデータであることが わかります。そして、それがあるディレクトリも特定できたとします— "somedirectory" とします。運よくチェックアウトした作業ツリー内の "somedirectory/myfile" にそれと同じコピーがあるのなら、 git-hash-object(1) を用いてそれが正しいかどうかをテストできます。

$ git hash-object -w somedirectory/myfile

これにより、somedirectory/myfile の内容をもった blob オブジェクトを 作成して格納し、そのオブジェクトの SHA-1 を表示します。 その値が 4b9458b3786228369c63936db65827de3cc06200 であったなら あなたは非常に幸運です。この場合、あなたの推測は正しく、破損は 修復されました!

一致しなかった場合、より詳しい情報が必要になります。 そのファイルのどのバージョンを無くしたかを確認します。

最も簡単な方法は、次のとおりです:

$ git log --raw --all --full-history -- somedirectory/myfile

生データを出力しているため、次のような出力が得られます

commit abc
Author:
Date:
...
:100644 100644 4b9458b... newsha... M somedirectory/myfile


commit xyz
Author:
Date:

...
:100644 100644 oldsha... 4b9458b... M somedirectory/myfile

これはすぐ前のファイルのバージョンが "newsha" であり、すぐ後のバージョンが "oldsha1" であることを示しています。 また、oldsha から 4b9458b の変更点と 4b9458b から newsha での変更点 のコミットメッセージについても知ることができます。

変更内容が十分小さい場合、4b9458b の状態の内容を再作成することが できるかもしれません。

もしそれができたのなら、紛失したオブジェクトを次のようにして 再作成することができます。

$ git hash-object -w <recreated-file>

これにより、リポジトリは正常に戻ります!

(ところで、fsck を無視することもできます。次のようにして、

$ git log --raw --all

紛失したオブジェクト(4b9458b..)の sha を探し出します。 - git はたくさんの情報を持っていますが、紛失したのは1つの特定の blob バージョンであるにすぎません)