GoでGit自作してみた<後編>
はじめに
Gitの仕組みを理解しようと、Go言語でGitもどきを自作してみました。
その名も 「Goit」 です。
前回の記事では、Gitの仕組みについて説明しました。
執筆段階(2023/06)で実装したコマンドには以下のようなものがあります。
-
init
- initialize Goit, make .goit directory where you init -
add
- make goit object and register to index -
commit
- make commit object -
branch
- manipulate branches -
switch
- switch branches -
restore
- restore files -
log
- show commit history -
config
- set config. e.x.) name, email -
cat-file
- show goit object data -
ls-files
- show index -
hash-object
- show hash of file -
rev-parse
- show hash of reference such as branch, HEAD -
update-ref
- update reference -
write-tree
- write tree object -
version
- show version of Goit
これら全ての実装内容を書くのは大変なので、Gitを使っていておそらく一番馴染み深い「add」して「commit」するまでの流れに沿って実装部分の説明をしていきたいと思います。
それでは早速いきましょう。
add
addコマンドを実装しているファイルはこちらです。
add
コマンドの大まかな処理の流れは以下の通りです。
- 引数として指定されたファイルが存在するかを確かめる
- Ignore対象でないかを確かめる
- 指定されたファイルからBlobオブジェクトを作成する
- インデックスを更新
- オブジェクトをファイルに書き込む
それでは一つずつ見ていきましょう。
1. 引数として指定されたファイルが存在するかを確かめる
forループで各引数を見て、その引数として渡されたファイル名が存在するかどうかを確かめています。
ただし、削除したファイルに対してgit add
する場合もあり得るので、ファイルが存在しない場合、インデックスにそのファイルが登録されていないかを確かめます。インデックスにも登録されていない場合は、引数が誤りであるためエラーを返します。
2. Ignore対象でないかを確かめる
client.Ignore.IsIncluded
関数に渡しています。
ignore対象となるパスを正規表現として格納しておき、引数として渡したパスがその正規表現にマッチした場合はignore対象と判断しています。
3. 指定されたファイルからBlobオブジェクトを作成する
まずは指定されたファイルの中身を取得します。
そしてobjectパッケージのNewObject関数に、BlobObjectというobject typeと取得したデータをパラメーターとして渡します。
object.NewObject(object.BlobObject, data)
NewObjectの実装は以下の通りです。
object構造体はtype
、hash
、size
、data
をフィールドに持っているので、それぞれのフィールドに値をセットしていくことで、objectを生成します。
4. インデックスを更新
ここではまずインデックスを更新します。更新の必要ない場合は、後続の処理を行う必要がないので、スキップします。
インデックスの更新部分は次の通りです。
インデックスには、各エントリ毎のファイルパスとハッシュ値が格納されています。新規追加ファイルと同じパス、ハッシュ値を持つエントリが既にインデックスに登録されている場合、インデックスを登録する必要がないため、returnします。インデックスを更新する場合でも
- 新規追加ファイル
- インデックスには登録されているけど、中身が変わった(ハッシュが変わった)ことによりインデックスを更新する必要のあるファイル
とでは処理が少し異なります。1の場合は単純にエントリを格納しているスライスに追加するだけで良いですが、2の場合は既存のエントリを一度消してから、新しいエントリを追加しています。
そして、新規エントリ追加後はインデックスをファイルパスでソートし、更新したインデックスをファイルに書き込みます。
5. オブジェクトをファイルに書き込む
object.Write
の実装部分は次のようになっています。
まず、オブジェクトのデータをzlibで圧縮しています。その後適当なパスに対してファイルを作成し、圧縮したデータを書き込みます。
ここまでの流れでgit add
の実装は終了です。それでは次にgit commit
について見ていきましょう。
commit
commit
コマンドを実装しているファイルはこちらです。
commit
コマンドの大まかな流れは以下の通りです。
- configの設定がされているか確認
- コミットの必要があるか(インデックスが更新されているか)の確認
- ツリーオブジェクトの作成
- コミットオブジェクトの作成
- ブランチの更新
- log書き出し
- HEADの更新
1. configの設定がされているか確認
IsUserSetの実装部分は次の通りです。 ここでは、local(.goit/.goitconfig)あるいはglobal(~/.goitconfig)のconfigファイルにuser(name & email)が設定されているかを確認しています。
ユーザー情報(name & email)はコミットに含まれるCommitterとAuthorで参照するので設定されている必要があるため、ここで確認しています。
2. コミットの必要があるか(インデックスが更新されているか)の確認
isCommitNecessary
関数の実装は次のとおりです。
コミットの必要がある、コミットに意味がある場合というのはインデックスが更新されている場合です。インデックスの更新有無検知はひとつ前のコミット(のツリーオブジェクト)と比較することで実現します。ということで、ここではHEADが指しているコミットオブジェクトが内包しているツリーオブジェクトとインデックスを比較しています。
3. ツリーオブジェクトの作成
writeTreeObject
の実装は次のとおりです。
少し複雑な実装になっていますが、内容自体はそれほど複雑ではありません。
インデックスに登録されているエントリを一つずつ走査し、エントリがBlobオブジェクトであれば、そのまま追加し、エントリがTreeオブジェクトであれば、writeTreeObjectを再帰的に呼び出してTreeオブジェクトを作成します。
ただ、インデックスのエントリは全てBlobオブジェクトとして登録されているので、エントリのパスからそのエントリがサブディレクトリに含まれているかを確認します。例えばエントリのパスがtmp/test.txt
であれば、test.txtはtmpディレクトリ内にあるとして、「test.txt」Blobオブジェクトを持つ「tmp」Treeオブジェクトを作ります。
4. コミットオブジェクトの作成
NewObject
メソッドは上で説明したので省略します。
NewCommit
メソッドは次のとおりです。
Commitデータを1行ずつ読み込んで、適当なフィールドに値をセットしていきます。
Commitデータは
$ git cat-file -p 8ae3a823e4fc675afb9772408129d483687d6e0c
tree c23b8eb57ad94e6eb0acefee6abbb7ad37adac10
parent fb1155f1745d4848600d47315bd0b3663a7d9a50
author test taro <test@example.com> 1686202540 +0900
committer test taro <test@example.com> 1686202540 +0900
delete main
といったようにtree
parent
author
committer
message
で構成されています。
commit.Write
に関してはcommit内部でobjectのWriteメソッドを呼び出しています。Writeメソッドに関しても上で説明したので省略します。
5. ブランチの更新
ブランチがすでに存在する場合とそうでない場合で場合分けをしています。
既に存在する場合は、ブランチのハッシュ値のみ更新します。
branchのハッシュを書き換えて、branchファイルを上書きします。存在しない場合は、新しくブランチを追加します。
RefsのHeadsに新規ブランチを追加して、ソートします。6. log書き出し
Gitでは.git/logs配下でログファイルを管理しています。commitでは.git/logs/HEADのHEADファイルと.git/logs/refs/heads/<branch name>のブランチファイルにログを残します。
7. HEADの更新
Head.Update
は以下のとおりに実装しています。
HEADファイルを書き換えて、HEADのコミットをHeadに登録しています。
まとめ
前編と後編に分けて、GitをGoで自作した話について書きました。
この記事がGitを理解する、Gitを自作する上で何かしらの役に立つのであれば幸いです。
また、今後も「Goit」の開発を進めるていくつもりなので、レポジトリにスターを付けてくださると励みになります。
参考資料
<Gitについて>
<Gitの仕組みについて>
- Git の仕組み (1)
- Git の仕組み (2) - コミット・ブランチ・タグ
- Gitのインデックスの中身
- Gitのブランチの実装
- Gitのオブジェクトの中身
- GitのHEADとは何者なのか
- たぶんもう怖くないGit ~Git内部の仕組み~
- コミットはスナップショットであり差分ではない
- コマンドを使わずに理解するGit
- Gitを作ってみる(理解編)
- Gitが連想配列記憶装置であることを低レイヤーな操作を通して体感しよう!
- Git が内部でデータを取り扱う方法
- Gitの内側
- Git Internals part 1: The git object model
- What does the git index contain EXACTLY?
- What is the internal format of a Git tree object?
Discussion