DUICUO

これが本当のGitです - Gitの内部構造

この記事では、具体的な例とアニメーション GIF を使用して、Git の内部動作を紹介します。Git はコードと変更履歴を保存するためのものであること、ファイルが変更されると Git が内部的にどのように変化するか、この方法で Git を実装する利点などについて説明します。

[[317591]]

上記のGIFを例を用いて解説することで、Gitの内部動作を理解しやすくなります。このGIFを既に理解できる方は、以下の内容がより基本的な内容となるでしょう。

序文

近年の急速な技術進歩により、一部の学生は単にコマンドを呼び出すだけの、表面的な学習アプローチをとるようになってしまいました。私たちはしばしば「この技術やフレームワークを使えば大丈夫」という幻想を抱いていますが、実際に問題に直面してみると、物事はそれほど単純ではないことに気づきます。

その後、私は落ち着きを取り戻し、プログラミングを学び始めた頃のことを思い出すようになりました。当時は、新しい概念を学ぶたびに、その根底にある原理を深く掘り下げていました。しかし、これは高度なAPIを習得して使いこなすことが重要ではないという意味ではありません。高度なAPIも同様に重要であり、私たちは日常業務で頻繁に使用しています。それらを素早く習得することは効率的な学習を意味し、開発や本番環境での迅速な応用を可能にします。

場合によっては、基礎となる概念の一部を知っていると、考えが明確になり、実際に何を行っているのかを理解し、Git の多数のコマンドやパラメータで迷うことがなくなります。

Git はどのように情報を保存しますか?

ここでは、簡単な例を使用して、Git がどのように情報を保存するかを視覚的に理解できるようにします。

まず、2つのファイルを作成しましょう。

  1. $ git 初期化
  2. エコー'111' > a.txt
  3. $ echo '222' > b.txt
  4. git で*.txtを追加する

Gitはデータベース全体を.git/ディレクトリに保存します。.git/objectsディレクトリを確認すると、リポジトリに2つの新しいオブジェクトがあることがわかります。

  1. $ ツリー .git/objects
  2. .git/オブジェクト
  3. ├── 58
  4. │ └── c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
  5. ├── c2
  6. │ └─ 00906efd24ec5e783bee7f23b5d7c941b0c12c
  7. ├── 情報
  8. └── パック

興味深いですね、中身を見てみましょう。

  1. $ cat .git/objects/58/c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
  2. xKOR0a044K%

なぜ意味不明な文字列になっているのでしょうか?これは、Gitが情報をバイナリファイルに圧縮しているためです。しかし、心配はいりません。Gitは情報を調べるためのAPIも提供しています。`git cat-file [-t] [-p]`です。`-t`はオブジェクトの型をチェックし、`-p`はオブジェクトに格納されている具体的な内容をチェックします。

  1. `git cat-file -t 58c9`
  2. ブロブ
  3. `git cat-file -p 58c9`
  4. 111

このオブジェクトは BLOB タイプのノードであり、その内容は 111 であることがわかります。つまり、このオブジェクトには a.txt ファイルの内容が格納されていることになります。

ここで、Gitオブジェクトの最初のタイプであるBLOB型に遭遇します。このオブジェクトは、ファイル名などの情報を除き、ファイルの内容のみを保存します。この情報はSHA1ハッシュアルゴリズムを用いてハッシュ化され、対応するハッシュ値58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6cが生成されます。このハッシュ値は、Gitリポジトリ内でこのオブジェクトの一意の識別子として機能します。

つまり、この時点での Git リポジトリは次のようになります。

探索を続けて、コミットを作成します。

  1. `git commit -am '[+] init'`  
  2. $ ツリー .git/objects
  3. .git/オブジェクト
  4. ├── 0c
  5. │ └─ 96bfc59d0f02317d002ebbf8318f46c7e47ab2
  6. ├── 4c
  7. │ └─ aaa1a9ae0b274fba9e3675f9ef071616e5b209
  8. ...

コミット後、Gitリポジトリに2つの新しいオブジェクトが追加されます。`cat-file`コマンドを使って、それらの型と内容を確認してみましょう。

  1. `git cat-file -t 4caaa1`
  2. `git cat-file -p 4caaa1`
  3. 100644 ブロブ 58c9bdf9d017fcd178dc8c0... a.txt
  4. 100644 ブロブ c200906efd24ec5e783bee7... b.txt

ここで、2つ目のGitオブジェクトタイプであるツリーが登場します。ツリーは現在のディレクトリ構造のスナップショットを取得します。ツリーに格納されている内容から、ディレクトリ構造(フォルダに類似)に加え、各ファイル(またはサブフォルダ)のパーミッション、タイプ、対応する識別子(SHA1値)、ファイル名が格納されていることがわかります。

この時点での Git リポジトリは次のようになります。


  1. `git cat-file -t 0c96bf`
  2. 専念 
  3. `git cat-file -p 0c96bf`
  4. 木 4caaa1a9ae0b274fba9e3675f9ef071616e5b209
  5. 著者: ルザン・リー・ゼファン 1573302343 +0800
  6. コミッター Lzane Li Zefan 1573302343 +0800
  7. [+] 初期化

次に、3つ目のGitオブジェクトタイプであるコミットについて発見しました。コミットに関する情報が格納されており、対応するディレクトリ構造のスナップショットツリーのハッシュ値、前のコミットのハッシュ値(これは最初のコミットなので親ノードはありません。マージコミットには複数の親ノードが存在します)、コミットの作成者、コミットの具体的な時刻、そして最後にコミットに関する情報が含まれます。

この時点で、Git リポジトリは次のようになります。

Gitがコミット情報をどのように保存するかは分かりました。では、普段目にするブランチ情報はどこに保存されるのかと疑問に思う生徒もいるかもしれません。

  1. $ cat .git/HEAD
  2. 参照: refs/heads/master
  3.  
  4. $ cat .git/refs/heads/master
  5. 0c96bfc59d0f02317d002ebbf8318f46c7e47ab2

Git リポジトリでは、HEAD、ブランチ、および通常のタグは、対応するコミットの SHA1 値を指すポインタとして簡単に理解できます。

実は、Gitオブジェクトには4つ目の種類、タグがあります。これは、コメント付きのタグを追加(git tag -a)した際に作成されます。ここでは詳細は説明しませんが、興味のある方は上記の方法でさらに詳しく調べることができます。

これでGitとは何か、つまりファイルの内容、ディレクトリ構造、コミット情報、ブランチをどのように保存するかが分かりました。基本的には、キーバリューデータベースとMerkle木を組み合わせて有向非巡回グラフ(DAG)を形成します。ブロックチェーンのデータ構造にもMerkle木が使われているため、この関連性はブロックチェーンの普及にも当てはまります。

Gitの3つのパーティション

次に、Gitの3つのパーティション(作業ディレクトリ、インデックス領域、Gitリポジトリ)と、Gitの変更ログがどのように形成されるかを見てみましょう。これらの3つのパーティションとGitチェーンの内部動作を理解することで、Gitの多くのコマンドを「視覚的に」理解し、混乱を防ぐことができます。

上記の例を続けると、現在の倉庫のステータスは次のようになります。

ここには 3 つの領域があり、保存される情報は次のとおりです。

作業ディレクトリ: すべてのコード開発と編集が行われるオペレーティング システム上のファイル。

インデックス (またはステージング領域): これは、次のコミットでコードが Git リポジトリにコミットされる一時的な保存領域として理解できます。

Git リポジトリは、Git オブジェクトによって記録された各コミットのスナップショットであり、コミットの変更履歴を記録するチェーン構造です。

ファイルの内容を更新するプロセス中に何が起こるかを見てみましょう。

`echo "333" > a.txt` を実行すると、`a.txt` の内容が 111 から 333 に変更されます。上の画像に示すように、インデックス領域と git リポジトリは変更されません。

`git add a.txt` を実行すると、`a.txt` がインデックスに追加されます。上の画像に示すように、Git はリポジトリ内に新しい BLOB オブジェクトを作成し、新しいファイルの内容を保存します。また、インデックスも更新され、`a.txt` は新しく作成された BLOB オブジェクトを参照するようになります。

変更をコミットするには、`git commit -m 'update'` を実行します。(上記の画像を参照してください。)

  • Git はまず、現在のインデックスに基づいてツリー オブジェクトを生成します。これは、新しいコミットのスナップショットとして機能します。
  • このコミットの情報を保存するための新しいコミット オブジェクトを作成し、その親が前のコミットを指すようにして、変更履歴を記録するチェーンを形成します。
  • マスター ブランチのポインタを新しいコミット ノードに移動します。

これで、Git の 3 つのパーティションとは何か、それぞれの機能、そして履歴チェーンがどのように構築されるかがわかりました。**基本的に、ほとんどの Git コマンドはこれら 3 つのパーティションと履歴チェーンを操作します。** さまざまな Git コマンドについて考え、上の図でそれらを「視覚化」できるかどうか試してみてください。これは非常に重要なので、ぜひ試してみてください。

日常的に使用するコマンドを効果的に視覚化できない場合は、Git の図解ガイドを読むことをお勧めします。

興味深い質問

興味のある学生は読み続けてください。この部分は記事の主な内容ではありません。

質問 1: ファイルの権限とファイル名を、BLOB オブジェクトではなくツリー オブジェクトに保存するのはなぜですか?

ファイル名を変更することを想像してください。

ファイル名がBLOBに保存されている場合、Gitは元のコンテンツをコピーして新しいBLOBオブジェクトを作成します。しかし、Gitのアプローチでは、新しいツリーオブジェクトを作成し、対応するファイル名を変更するだけで済みます。元のBLOBオブジェクトは再利用できるため、スペースを節約できます。

質問 2: 各コミットごとに、Git はファイルの完全に新しいスナップショットを保存しますか、それとも変更された部分だけを保存しますか?

上記の例からわかるように、Git はファイルの変更記録ではなく、ファイルの完全に新しいスナップショットを保存します。つまり、ファイルに 1 行だけ追加しただけでも、Git は全く新しい BLOB オブジェクトを作成します。これはスペースの無駄遣いのように思えますか?

これは実際にはGitにおけるスペースと時間のトレードオフです。コミットのチェックアウトや2つのコミットの差分比較を考えてみましょう。Gitが変更をアンケート形式で保存した場合、コミットの内容を取得するには、最初のコミットから開始し、対象のコミットまで変更を継続的に計算する必要があり、非常に長い時間がかかります。一方、Gitは新しいファイルのスナップショットを保存するため、この操作は非常に高速です。スナップショットから直接内容を取得できます。

もちろん、ネットワーク転送が関係する場合や Git リポジトリが非常に大きい場合、Git にはガベージ コレクション メカニズム (gc) があり、不要なオブジェクトを削除するだけでなく、既存の同様のオブジェクトをパッケージ化して圧縮します。

質問 3: Git はどのようにして履歴が不変であることを保証するのでしょうか?

これは、SHA1ハッシュアルゴリズムとハッシュツリーによって保証されます。変更履歴内のファイルの内容を秘密裏に変更すると、そのBLOBオブジェクトのSHA1ハッシュ値が変更され、関連するツリーオブジェクトのSHA1ハッシュ値も変更される必要があります。コミットのSHA1ハッシュも変更され、その後のすべてのコミットのSHA1ハッシュ値も変更される必要があります。さらに、Gitは分散システムであるため、誰もがGitリポジトリの履歴の完全なコピーを持っているため、誰でも簡単に問題を発見できます。

この記事がお役に立てば幸いです。次の記事では、日々の業務で役立つGitのヒント、よくある質問、そしてインシデントへの対処法についてご紹介します。