feature image

2023年4月27日 | ブログ記事

Gitを作ってみようとした話

この記事は新歓ブログリレー2023の50日目の記事です。
こんにちは、SysAd班21Bのmehm8128です。
今回はタイトルにもあるように、Gitを自作してみようとした話をしようと思います。

Gitとは?

新入生の皆さんはGitが何なのかを知らない人が多いと思うので、軽く説明します。
Gitは分散型バージョン管理システムで、プログラムのソースコードを管理・共有するために使います。
例えば新しい機能を追加したときに、別の場所でバグが発生してしまったとします。そのとき、バージョン管理をしていなければ機能追加前のコードに戻せなくなってしまいますが、Gitを使えば機能を追加する前の状態で保存してあれば、元に戻ってどこからバグが発生してしまったのかを調査することができます(GitHubというサービスを利用すれば見やすく確認することができます)。

今回はそのバージョン管理システムであるGitの内部の仕組みに軽く触れてみて、一部の機能を自作してみたり自作しようとしてみたりしたので、紹介しようと思います。ただし、実装の仕方の紹介はほとんどせずに僕が理解している範囲で内部の仕組みを説明する方に重点を置こうと思います。

Gitの仕組み

Gitで現在のソースコードの状態を保存するには主に2つのステップが必要になります。

  1. git addというコマンドを使って保存するファイルを選択する
  2. git commitというコマンドを使って選択したファイルたちの保存を確定させる

この2つの話をメインにしていきます。前者は8割くらい実装できて、実際にVSCodeのGUIが認識してくれるくらいまで動くようにはなっています(ただし、不備があって上手く動かないケースがあります)。後者は実装力が足りなくて諦めました(そもそも実装方針に問題があるかも?)。

注意:ここより先は実際に勉強して実装してから2ヶ月くらい経ってから書いているので間違いがあるかもしれないことにご注意ください。

git add

ではgit addで行っていることを説明します。git addでは主に以下の手順で処理が進みます。

  1. stageされているファイルの中身の先頭にblob <ファイルサイズ><NULL文字>をつけたものを用意
  2. 1.で作ったものをzlib圧縮する
  3. 1.で作ったものをハッシュ化した文字列の先頭2文字をディレクトリ名、残りをファイル名にして2.で作ったものをobjectsディレクトリに保存
  4. .git/indexをフォーマットに従って更新

.git/indexの中身のフォーマットは大体ここらに書いてある通りになっています。
https://qiita.com/noshishi/items/60a6fe7c63097950911b#インデックスを解体してみる
indexファイルを更新する際に、1.から3.で作ったblobオブジェクトが必要になります。
gitにはblobオブジェクト、treeオブジェクト、commitオブジェクトというものがあります(tagオブジェクトもあるらしいですが今回は関係ないので飛ばします)。blobオブジェクトは1. ~3.で作ったような形式で、git管理されているファイルを示すものです。treeオブジェクトはgit commitのときに出てくるのですが、ファイルではなくてディレクトリを示すもので、そのディレクトリに入っているオブジェクトの情報が入っています。commitオブジェクトもgit commitのときに出てくるのですが、コミットの情報が入っています。

なんたらオブジェクトとかindexファイルとか言われても分からないと思うので、ハンズオン形式で軽く実際に見てみましょう(ファイル名やファイルに書き込む内容は同じものにしてください。でないとハッシュ値がずれてしまってこの通りになりません)。

$ mkdir git-handson
$ cd git-handson
$ touch aaa.txt
$ echo oisu- > aaa.txt
$ git init
$ git add .
$ git ls-files --stage

ここまでやって

100644 72943a16fb2c8f38f9dde202b7a70ccc19c52f34 0       aaa.txt

のような表示になりましたでしょうか。
左から順にファイルの権限、ハッシュ値、なんだっけこれ、ファイル名、です。
git ls-files --stageすることで、indexファイルの中身を解読してstageされているファイルの一覧を見ることができます。
さらに続いて

$ cd .git/objects/72
$ ls

すると

943a16fb2c8f38f9dde202b7a70ccc19c52f34

と出てきたと思います。
これはディレクトリ名と合わせると、ls-filesしたときに出てきたハッシュ値と一致しますね。上で説明した通り、ファイルのハッシュ値の先頭2文字がディレクトリ名、残りがファイル名となってgit addしたファイルの中身が保存されています。
最初のコマンドの72はどこから出てきたんだと言われたら.git/objectsディレクトリのファイル一覧を見てずるしてるのですが、許してください(もしくは、状態によってはgit commitのときに紹介する方法で辿ることはできます)。

では本当にこのファイルにaaa.txtの中身が保存されているのでしょうか?確認してみましょう。

git cat-file -p 72943a16fb2c8f38f9dde202b7a70ccc19c52f34

無事ファイルの中身である、

oisu-

が表示されましたでしょうか。cat-fileコマンドを使うことでblobオブジェクトの中身を確認することができます。ちなみに「おいすー」はtraP部員がよく使う挨拶です。

ちなみにls-filescat-fileは配管コマンドと呼ばれ、これも自分で実装してみました。git addの実装よりはかなり手軽に実装できるので、やってみてください。最後に参考にした記事をまとめて載せるので、そこを参考にするといいと思います。

さて、ここまで分かったらあとは実装するだけです。上で挙げた手順に従って処理をするように実装をしてみましょう。最初に上手く動かないケースがあると言ったのは、git addされたときにindexファイルをstagingエリアにあるファイルだけで完全に上書きしてしまっていたのですが、実は今までgit addされたものは残しながら更新しないといけなかったらしいみたいな話があります。2ヶ月前のことなのであんまり記憶がないのですが、git commitのときに上手くいかないとか何とかが理由だったような気がします。

一応VSCodeで動いているところを動画撮ったのでご覧ください。

ではここまででgit addが完成したということにして、次にgit commitについて見ていきましょう。

git commit

ではgit commitで行っていることを説明します。git commitでは主に以下の手順で処理が進みます。

  1. 現在のディレクトリ構造とindexファイルからtreeオブジェクトを生成し、commitオブジェクトを生成
  2. 1.で作ったcommitオブジェクトをrefs/heads以下が指すようにする

git addのときよりざっくりした手順になってしまいましたが、1つずつ説明します。
treeオブジェクトというのは上でも書いた通りディレクトリを示すもので、ディレクトリに入っているオブジェクトの一覧が入っています。commitオブジェクトはそのコミット時のルートのtreeオブジェクトのハッシュ値やコミットしたユーザーの情報、タイムスタンプなどが含まれています。
では先ほどの続きから実際に手を動かして確認していきましょう。

$ git commit -m 'first commit'
$ git cat-file -t 3d0dff9c5d6f9b1fd844ddc7cc74cfcada495a32
$ git cat-file -p 3d0dff9c5d6f9b1fd844ddc7cc74cfcada495a32

2つ目のコマンドで

tree

3つ目のコマンドで

100644 blob 6299ee48c1967e011cb8cec0cabef203df5e96b0    aaa.txt

という表示になりましたでしょうか。これでコミットしたことによってtreeオブジェクトが生成され、その中にはaaa.txtを示すblobオブジェクトが入っていることが確認できます。
左から順に権限、オブジェクトの種類、ハッシュ値、ファイル名(ディレクトリ名)です。
このtreeオブジェクトは現在コマンドを実行しているディレクトリのtreeオブジェクトです。なので、aaa.txtをさらに深い階層のディレクトリに入れたらその分何回もtreeオブジェクトを辿らないといけなくなります(ディレクトリの中にディレクトリが入っている場合、blobと書いてあるところがtreeになることもあります)。

続けてcommitファイルを確認します。

$ git cat-file -t 1147583dcfd01266a21c23428eb4f8ff3eab0973
$ git cat-file -p 1147583dcfd01266a21c23428eb4f8ff3eab0973

1つ目のコマンドで

commit

2つ目のコマンドで

tree 3d0dff9c5d6f9b1fd844ddc7cc74cfcada495a32
author {ユーザー名} <{メールアドレス}> {タイムスタンプ} +0900
committer {ユーザー名} <{メールアドレス}> {タイムスタンプ} +0900
gpgsig -----BEGIN PGP SIGNATURE-----
 
 {PGP SIGNATURE}
 -----END PGP SIGNATURE-----

first commit

という表示になりましたでしょうか。{}で囲ってある部分は人によって違うと思います。また、僕はGPG署名の設定をしてあるのでgpgsigとかいうのがありますが、設定していない人はcommitterのすぐ下にfirst commitというコミットメッセージが表示されると思います(2ヶ月前に勉強してたときはなかったのにその間にGPG署名の設定したので表示されるようになっていて驚きました)。
commitオブジェクトはtreeオブジェクトのハッシュ値を1つだけ保存しています。このtreeオブジェクトはさっき見た、今いるディレクトリのハッシュ値になります。このハッシュ値だけ分かっていれば、このtreeオブジェクトの中身をみてその中にあるtreeオブジェクトをさらに辿って...としてディレクトリ全体を再現することができるからです。

まだ続きます。

$ echo oisi- > aaa.txt
$ git add .
$ git commit -m 'second commit'
$ git cat-file -p 60553e5e46bfa5ccebc635cb8df4a805ba31d4de

2つ目のコミットを打って、そのcommitオブジェクトを見ています(一番下に表示されるコミットメッセージがsecond commitとなっているはずです)。ちなみに、このgit addでは最初のgit addblobオブジェクトにしたファイルも更新された状態で再びblobファイルとして保存されます。これが「gitは差分ではなくスナップショットで保存される」みたいな話ですね(古いものはpackというディレクトリに圧縮されて差分等で保存されるらしいですが)。
cat-fileの結果を上から2行分だけ取り出して見ていきましょう。

tree 3901d5eb8422b3cdede6e5bd733c09564163f701
parent 1147583dcfd01266a21c23428eb4f8ff3eab0973

さっきの1つ目のcommitオブジェクトにはなかった、parentというものがありますね。そしてこのハッシュ値は1つ目のコミットのcommitオブジェクトを示しています。gitではこのようにして、新しいcommitオブジェクトに前のcommitオブジェクトのハッシュ値をつけておくことによって、現在のcommitオブジェクトのハッシュ値だけ分かれば過去の状態を遡ることができるようになっています。これが高パフォーマンスでバージョン管理ができるようになっている要因の1つなのですね。

もう少しで終わります。

$ cat .git/HEAD
$ cat .git/refs/heads/master

1つ目のコマンドで

ref: refs/heads/master

2つ目のコマンドで

60553e5e46bfa5ccebc635cb8df4a805ba31d4de

が表示されましたでしょうか。
既に普段からgitを使っている人はgit reset --hard HEAD^みたいにしてHEADというものを使ったことがある人がいるかもしれませんが、これは単にrefs/heads/masterにあるハッシュ値を指しているのですね(masterブランチにいる場合)。そしてそこにあるハッシュ値60553e5e46bfa5ccebc635cb8df4a805ba31d4deは、さっき見た2つ目のcommitオブジェクトのハッシュ値です。gitのブランチは最新のオブジェクトを指しているだけ、という話を聞いたことがあるかもしれませんが、それはこのことです。masterブランチはハッシュ値60553e5e46bfa5ccebc635cb8df4a805ba31d4decommitオブジェクトを指しているだけで、それより前のcommitオブジェクトはこのcommitオブジェクトのparentのハッシュ値を辿っていけば全て見れます。すごいですよね。興味のある人は別のブランチを作ってrefs/heads/<ブランチ名>とかを確認してみるといいと思います。

これでgit commitについての説明は一通り終えたつもりです。実装はできていないので動画などはありません。

git log

おまけです。
実は過去にこんな勉強会があったらしいです。
https://www.youtube.com/watch?v=-4rcs6SgT0o&ab_channel=CAMPHOR-
ここでgit logを実装していたので、git add等の実装の参考にしようとしてgitの仕組みを勉強した後に最初にこれを追って実装していました。実装の雰囲気を掴んでみたい人はやってみるといいと思います。ただし、僕は前半しか見ていません。後半は先ほど少しだけ触れたpackディレクトリの話になって難しそうだったのでやめました。

最後に

僕は最初はgitがかなり苦手だったのですが、SysAd班で色々使っているうちに内部の仕組みに興味が持てるくらいまで苦手を克服できました。
興味を持ってくれた人がいれば是非さらに勉強して、余力があれば実装までやってみてほしいです。
また、traPでは毎年春にgit講習会というものを開催していて、git初心者がgitのいくつかの基本的なコマンドを使い、Pull Requestを出せるところまでサポートしています。gitを実際に使えるようになりたい人がいれば是非受講してみてください!

ちなみに、一応GitHubでリポジトリは公開しているのですが完成度がかなり微妙なので敢えてリンクは置きませんどうしても気になる人は探し出してみてください。

参考記事

gitの仕組みについてしっかり説明している記事は多くなかったのですが、参考にさせていただいた記事の中でも特によかったものを紹介させていただきます。僕のこの記事もgitの仕組みについて興味を持った人々の役に立つことができればと思います。

既に紹介したやつ

https://www.youtube.com/watch?v=-4rcs6SgT0o&ab_channel=CAMPHOR-
https://qiita.com/noshishi/items/60a6fe7c63097950911b

メルカリエンジニアブログ

https://engineering.mercari.com/blog/entry/2015-09-14-175300/
https://engineering.mercari.com/blog/entry/2016-02-08-173000/
https://engineering.mercari.com/blog/entry/2017-04-06-171430/

UUUMエンジニアブログ

https://system.blog.uuum.jp/entry/2020/09/17/110000
https://system.blog.uuum.jp/entry/2020/09/24/110000

リプセンスエンジニアブログ

https://made.livesense.co.jp/entry/2017/08/22/080000

git公式ドキュメント

https://git-scm.com/book/ja/v2

明日の担当は鵜崎くんとH1ronoくんです、お楽しみに!

mehm8128 icon
この記事を書いた人
mehm8128

21BJC。SysAd班で色々やってます

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年4月17日
ポケモンを飼いたい夢を叶える
tqk icon tqk
2023年4月25日
【驚愕】作曲4年目だった男が大学3年間ゲームサウンドに関わった末路...【ゲームサウンドのお仕事について】
tenya icon tenya
2023年3月20日
traPグラフィック班の活動紹介(Ver.2023)
NABE icon NABE
2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2023年4月27日
Vulkanのデバイスドライバを自作してみた
kegra icon kegra
記事一覧 タグ一覧 Google アナリティクスについて