💻

GoでGitを自作してみた <前編>

2023/05/30に公開

はじめに

Gitの仕組みを理解しようと、Go言語でGitもどきを自作してみました。

その名も 「Goit」 です。

https://github.com/JunNishimura/Goit

今回自作しようと思った理由としては、

  • Gitの仕組みを理解したかった
  • Goで何かツールを作りたかった

などが挙げられます。

Goitの実装内容について記事を書こうと思っていたのですが、Gitの仕組みを解説するだけで記事のボリュームが大きくなってしまったので、この記事ではGitの概要、仕組みについてのみを書こうと思います。

Goitの実装内容については後日記事にして公開したいと思います。

この記事で取り扱う内容

  • Gitとは何か
  • Gitの仕組み(Gitオブジェクト、リファレンス、インデックス

この記事で取り扱わない内容

  • Gitコマンドの紹介
  • Gitコマンドの使い方

この記事の対象者

  • Gitを何となくで使っているけど、内部がどのようになっているのか分かっていない人
  • Gitのようなバージョン管理ツールを自分でも作ってみたい人

Gitとは何か?

概要

Gitはリーナス・トーバルズ(Linux開発者)によって開発された分散型バージョン管理システムです。

バージョン管理システム

バージョン管理システムとは、ソースコードなどのファイルの変更履歴を記録、追跡することができるシステムのことです。

「バージョン管理」という言葉だけを聞くと、堅苦しく感じますが、誰しもが経験したことのある話です。学生時代にレポートや論文を書く時に、「課題_山田花子_ver1.docx」、「課題_山田花子_ver2.docx」という風に、バージョン毎にファイルを切り分けたことのある人は多いのではないでしょうか。そのバージョン管理を上手くやってくれるのがバージョン管理システムです。

編集対象にあるファイルの数が1つや2つで、ファイルの編集・閲覧に関わる人が自分だけの場合は、バージョン管理システムの必要性は感じないかもしれません。

バージョン管理システムの恩恵を受けるのは、ファイルの数が膨大で、かつ複数人にわたってファイルを編集、閲覧するような場合です。複数人でファイル編集を行う場合、「誰が」「いつ」「どこに」「どのような」変更をしたのかが記録され、共有されていなければ、色々なトラブルが発生することが予想されます。

分散型

Gitは分散型バージョン管理システムでDVCS(Distributed Version Control System)と呼ばれます。一方でSubversionのような集中型バージョン管理システムはCVCS(Centrlized Version Control System)と呼ばれます。

CVCSでは、一つのリポジトリのみが存在し、1つのサーバーでそのリポジトリが管理されます。作業者は、そのサーバーからファイルを取得して作業することができます。CVCSの弱点として、サーバーがないと機能しない点が挙げられます。リポジトリが単一サーバーで管理されているので、サーバーに接続できないような環境では、最新のファイルの取得や版管理をすることができません。

一方、DVCSでは各作業者は自分のローカル環境にリポジトリをミラーリングして作業することができます。各作業者の元にリポジトリが分散している様が分散型と呼ばれる所以なのではないでしょうか。各作業者は、各自のローカル環境で開発を進めて、ローカルのリポジトリで版管理をして、ある程度作業が進んだ段階で、リモートリポジトリに反映することができます。またローカルリポジトリは自分専用のリポジトリであるため、ブランチ作成やコミットがCVCSと比べて気軽に行えます。

GitHubとは

Gitと関連してよく目にする言葉として「GitHub」というものがあります。GitHubはGitで管理されているレポジトリのホスティングサービスです。

GitHubを通じてレポジトリを公開することで、自分のレポジトリを他者が見ることができるようになり、また自分も他者が公開したレポジトリを見ることができるようになります。

GitHubを用いることで、複数人がコードレビューなどをしながら協働して開発できるようになるのです。

Gitの仕組み

Gitを理解する上で3つの重要な概念があります。それは、

  1. Gitオブジェクト
  2. リファレンス
  3. インデックス

です。

それでは、これら3つの概念についてもう少し詳しく見ていきましょう。

Gitオブジェクト

Gitでは、管理するファイルやディレクトリ、それのメタデータであるコミットやタグを「オブジェクト」として扱います。このGitオブジェクトはBlob Tree Commit Tagの4種類に分けることができます。

Commit Tagは文字通りCommitとTagを指します。それ以外のBlob Treeに関しては、Blobがファイルに、Treeがディレクトリに対応しています。

Gitオブジェクトは.git/objectsにて管理されています。

画像を見てもらえれば分かる通り、Gitオブジェクトは単なるファイルです。

各Gitオブジェクトには40文字の16進数で表現されたIDが付与されています。このIDはSHA1というハッシュ関数より生成されるハッシュ値です。

IDの先頭2文字がディレクトリ名になり、残りの38文字がファイル名になります。上記の画像を例にすると、3d97823c91b0bfa9713714c8aef2f3ee0897e969というGitオブジェクトは、.git/objects/3d/97823c91b0bfa9713714c8aef2f3ee0897e969というパスに存在することがわかります。

Gitオブジェクトの中身については、zlibと呼ばれるデータ圧縮ライブラリで圧縮されたデータが入っています。

ここまでで、Gitオブジェクトの中身はzlibで、IDはSHA1で生成されていることが分かりました。では次に、zlibとSHA1に渡す入力について説明します。

zlibとSHA1にわたす入力はオブジェクトの種類 + [スペース] + データサイズ + [\0](Nullバイト) + データになっています。オブジェクトの種類は上で述べたblob tree commit tagの4種類です。

以下のsample.txtの場合

sample.txt
This is a sample text file.

入力は

blob 27\0This is a sample text file.

になります。

gitにはhash-objectという引数に渡したファイルのSHA1ハッシュを計算して表示してくれるコマンドがあります。

git hash-object test.txt
4aa5f8f4820c61edeed2230683b472358d54598d

sample.txtには4aa5f8f4820c61edeed2230683b472358d54598dというIDが振られたことが分かります。

リファレンス

GitではコンテンツがGitオブジェクトとして管理されていて、各GitオブジェクトにはID(SHA1ハッシュ)が振られているのでした。そして、SHA1ハッシュが衝突する確率は極めて低いので、SHA1ハッシュをキーにして、一意にオブジェクトを特定・検索することが可能になっています。

しかし、SHA1ハッシュは40文字の英数字によって表現されていることもあり、人間がオブジェクトのハッシュ値を覚えておくことは面倒であり、SHA1ハッシュをそのまま使うのはあまり実用的ではありません。

その問題を解決する仕組みとして、Gitにはリファレンスというものがあります。それではリファレンスについて見ていきましょう。

ブランチ

リファレンスは日本語で「参照」という意味です。Gitでのリファレンスの対象は1つ前のセクションで話したコミットオブジェクトです。

リファレンスはファイルで管理されていて、.git/refs/ディレクトリ内に存在します。
`

リファレンスにも色々種類がありますが、その中の一つのheadsを取りあげてみましょう。例えば、heads/mainを見てみると、ファイルにはコミットオブジェクトのSHA1ハッシュが書かれていることが分かります。

cat .git/refs/heads/main
ccf5cbfb63f1ee509923f968f306bbc552f73742

ファイル名mainに見覚えがありますね。そうです、このheadsディレクトリ配下で管理されているファイルは一般的にブランチと呼ばれるものに当たります。

ブランチという言葉だけを聞くと、枝全体のことを指しているように思えますが、実際のブランチは、枝の末端にあるコミットオブジェクトのハッシュを指しているのです。

あなたが開発者であるならば、ブランチがないことを考えてみてください。わざわざコミットオブジェクトのハッシュ値を覚えておかないと思うと、いかにブランチの存在がありがたいかが分かるかと思います。

リファレンスにはブランチの他に、HEADというものがあります。HEADは分かりやすく言うと、Gitツリー全体の中で、今自分がいる地点を指し示すものです。

HEADは.git/HEADファイルに書かれています。

HEADはGitツリー全体の中で、今自分がいる地点を指し示すものと言いましたが、実体はブランチのポインタです。


HEADファイルの中身を見てみると、ブランチが書かれていることが分かります。

ブランチがコミットオブジェクトのポインタであることを考えれば、HEADはコミットオブジェクトに対するポインタポインタだと言えます。

インデックス

Gitの仕組みの最後としてインデックスについて説明します。

インデックスはステージングエリアとも呼ばれています。

インデックスがどういった役割なのかを理解するために、gitで管理されているレポジトリの状態を整理します。

この画像のように、ワーキングディレクトリ インデックス コミットツリーの3つの領域に分けることができます。ワーキングディレクトリは作業しているディレクトリでコミットツリーはコミットの履歴です。

インデックスはワーキングディレクトリとコミットツリーの架け橋のような役割を果たしています。

インデックスはgit addする前であれば、HEADが指しているコミットのスナップショットを表しており、git addで修正・追加したファイルを登録した後であれば、次のコミットで反映したいスナップショットを表していると言えます。

そんなインデックスの中身はどうなっているのか気になりますよね。インデックスの中身はgit ls-files -sで見ることができます。インデックスの中身を覗いてみると、

  • インデックスに登録されているのはBlobオブジェクトだけである
  • SHA1ハッシュと共にファイルパスが保存されている
    という点に気が付きます。

インデックスの役割、中身については理解できましたが、インデックスがあることでどんな良いことがあるのでしょうか?

理由の一つとして差分検知が楽になるというのが考えられます。

インデックスがなければ、差分検出はブランチが指しているコミットとワーキングディレクトリを比較することになると思うのですが、そうなるとGitオブジェクト(コミット、コミットに含まれるツリーオブジェクト)をzlibで解凍して、バイナリデータを読み込む必要があります。これは少しコストがかかりますよね。

そこでインデックスがあれば、仮に新しいファイルを追加したとすると、その新規ファイルのパスはインデックスに登録されていないので、新しくファイルが追加されたという差分検出が簡単にできます。また、既存ファイルの修正の場合でも、ファイルを修正すればBlobオブジェクトに紐づいたSHA1ハッシュは変わるので、インデックスのパスは同じだけどハッシュ値が異なる行がヒットし、ファイルに修正が加えられたことが検知できるので

まとめ

自作Gitの実装内容についての解説に先立ち、Gitの概要と仕組みについて書きました。

Goitの実装内容については記事にして後日公開する予定なので、もしよかったらそちらも読んでみてください。

また、今後も「Goit」の開発を進めるていくつもりなので、レポジトリにスターを付けてくださると励みになります。

https://github.com/JunNishimura/Goit

参考資料

<Gitについて>

<Gitの仕組みについて>
<Gitの実装について>

Discussion