どうもこんにちは.RCC副執行委員長の3^5です.
11月の終わりごろに執筆しているのですが無事25日連続投稿が成功して延長線としてこの記事が公開されることを願っております.締め切りを遵守した1~2回生は数名と聞いているのでとても心配ですが…w
(12/22追記: どうやら数人の失踪によりこの記事は24日目になるみたいですね.今年は1回生のアドカレ担当者が足りない分の記事を錬成しなかったらしい)
以下,今回の目次です.
目次
- Rustで作るGit入門
- .gitの中身を見る
- HEADについて
- refsについて
- indexについて
- objectsについて
- Gitオブジェクトとその種類
- blobオブジェクト
- treeオブジェクト
- commitオブジェクト
- 配管コマンド
- cat-file
- hash-object
- 磁器コマンドを配管コマンドで再現
- add
- commit
- .gitの中身を見る
- 終わりに
Rustで作るGit入門
さて本題ですが,今回の話は先日TwitterでRustのGit自作本を見かけたので読んでみたという話です.
公開しました!
— うじまる (@uzimaru0000) October 30, 2020
最初の方は無料なので読んでみてください🙇♂️
RustでつくるGit入門|うじまる https://t.co/hp2gf3zTJD #zenn
実装内容とか詳細をこの記事に書いちゃうと元記事(有料500円)のネタバレになっちゃうのでここではGitのディレクトリ構造とかGitオブジェクトの話とかを中心にしていきます.(実装に関しても元記事の6章くらいまでしかやってないけど)
なお,実装内容はGitHubで公開しています.
.gitの中身を見る
.git
ディレクトリの中をじっくり見たことはありますか?実はこんな感じになっています.(見づらい画像でごめんなさい)
この中で以降出てくるものは
- HEAD
- index
- objects
- refs
の4つになります.
それぞれ詳しくみていきましょう.
HEADについて
HEADはgitにおいては基本的には現在いるブランチを指すエイリアスになっています.
例えば,今develop
ブランチにいるならばgit push origin develop
と書くところを代わりにgit push origin HEAD
と書けるわけです.間違いが減ってありがたいですね.
このHEAD
は.git
内に直接ファイルが存在していて,現在いるブランチへの参照となっています.実はブランチは特定(最新)のコミットを指す概念であって,それぞれに分かれた枝においてのコミットの列を指しているわけではないことに注意が必要です.この記事とかが多分参考になります.
ご自身の環境にある適当なGit管理されているプロジェクトの.git/HEAD
を見てみてください.そこにはおそらくref: refs/heads/<branch名>
と書かれていることでしょう.ブランチは.git/refs
内に情報が格納されているのです.
refsについて
refs
の中を覗いてみると大体の雰囲気はつかめると思います.なんとなくheads
の中にローカルのブランチがあって,remotes
にリモートリポジトリのブランチがあるんだな,みたいなことがわかります.
実際にはこのファイルになっている部分(上画像で言えばheads/master
など)が,そのブランチの最新のコミット(前項で説明したようにブランチは特定のコミットを指すのでそのブランチの最新のコミットという表現は適切ではありませんが)の参照になっています.
indexについて
.git/index
は現在追跡中のファイルが記されています.
index
はバイナリファイルですが,git ls-files --stage
コマンドで中身を表示することもできます.
100644 95ba71b8e7c2d70965d9a5086a9fabd58e87d259 0 .gitignore
100644 960a0c393e5e493e911ac24ac42d6eb125409263 0 Makefile
100644 9d7f6b8213c452a8e698ccb5557cdf5d36ccf25c 0 README.md
一番左の100644というのはこのファイルが通常ファイルであることを表しています.これはtreeオブジェクトの実装の際にももう一度出てくるのですが,対象が実行可能ファイルだったりtreeオブジェクトだったりするとこの値が変わってきます.
その次のよく分からない値はハッシュ値です..git/objectsの中にそのファイル名のオブジェクトが存在します.ただし最初の2桁がディレクトリになっていて残りの桁が名前になったファイルがその中に配置されているという形で保管されています.ここら辺は次の項でも出てきます.
次の数値はステージと呼ばれる値で0~3の値をとり,マージ時に使われます.Gitのマージコンフリクトの解消は一方のブランチと他方のブランチ,そして二つの親ブランチの3バージョンを比較して行うために0の他に3種類のフラグを要するわけです.
objectsについて
.git/objects
にはGitオブジェクトが配置されています.それぞれのオブジェクトは前述したようにそのハッシュ値の上二桁を除いたものがファイル名になっており,上二桁の名前のディレクトリ内に配置されています.
Gitオブジェクトはすべてこの.git/objects
に配置されますが,その中でも役割に応じて3つの種類に分かれています.
Gitオブジェクトとその種類
Gitオブジェクトにはblob, tree, commitの3種類が存在します.
前項で見たようにGitオブジェクトはすべて.git/objects
に格納されています.格納場所や方法にタイプによる違いはなく,blobだろうとそうでなかろうと前述の方法で保存されています.
各オブジェクトはどれもheaderとbodyから構成されており,headerのフォーマットは"<object-type> <byte-size>"
で統一されています.bodyのフォーマットは各オブジェクトのタイプで異なります.
それぞれの役割とRustでの簡単な実装例(ほぼ定義のみ)をみていきましょう.
blobオブジェクト
一つのファイルを表すGitオブジェクトです.
実装としてはデータとその長さをメンバに持つ構造体を定義して,メソッドとしてバイト列から構造体を作ったり逆にバイト列として出力したりする関数やオブジェクトのファイル名となるハッシュ値を計算するメソッドを作っておけばよいでしょう.
treeオブジェクト
treeはディレクトリ構造を保存するオブジェクトです.
名前の通り木構造になっており,blobオブジェクトやtreeオブジェクトを保有します.この木構造がそのままコミット時のディレクトリ構造を示しています.
実装については構造体のメンバとしてFileを表す構造体の配列を持つようにします.File構造体はファイル名・ハッシュ値・ファイルの種類(通常ファイルやtreeオブジェクトを表すフラグ)を持つことでtreeが木構造を表現しています.
commitオブジェクト
commitオブジェクトは名の通りコミットを表現するオブジェクトです.
あるcommitオブジェクトは下図のようにコミット時点でのtreeオブジェクトを指します.
メンバとしては5つの情報を持っており,指しているtreeオブジェクトのハッシュ値,一個前のコミットオブジェクトのハッシュ値,author/commiter情報,そしてコミットメッセージです.
配管コマンド
Gitには普段よく使うgit add
やgit commit
,checkout, branch, remote
のようなユーザフレンドリーなコマンドがある一方で,git ls-files
のような低レベルの処理を行うためのコマンドもたくさんあります.これら後者のコマンドは配管コマンドと呼ばれていて,これをUNIX流につなぎ合わせることで前者磁器コマンドが実装できます.
cat-file
git cat-file -p <見たいblobオブジェクトのハッシュ値>
を実行することでそのファイルをcatすることができます.これも配管コマンドの一つです.
ls-filesで出てきたハッシュ値を与えて実行するとそのファイルの中身が標準出力に出力されるでしょう.当然ですが与えたハッシュ値のオブジェクトがblobオブジェクトである必要があります.
このcat-fileが行う作業は以下のようになります.
- ハッシュ値からblobオブジェクトのパスを生成
- blobオブジェクトのデータを読み取る
- データを解凍する
- 出力
実装はかなり簡単です..git/objects
の中のハッシュ値上二桁のディレクトリ内で残りのハッシュ値のファイルをオープンし,内容を読み取ってzlibで解凍するだけです.
hash-object
git hash-object <ファイル名>
とすることでそのファイルをblobオブジェクトにすることができます.基本的には標準出力にハッシュ値を表示しますが-w
オプションでそれを直接.git/objects
に保存することもできます.cat-fileの逆みたいな感じですね.これも実装は簡単です.
- ファイルを開いて中身を読み取る
- 読み取ったバイト列を使ってblobオブジェクトを生成して返却する
磁器コマンドを配管コマンドで再現
こっからは実装できてないので先ほど出てきたcat-file
やhash-object
に加え公式のGitに含まれている配管コマンドも使って話を進めていきます.
配管コマンドだけで磁器コマンドであるadd
とcommit
を実行してみましょう.
git add
git add
がやっていることは大きく分けて2つです.
- 引数のファイルのGitオブジェクトを生成
index
ファイルを更新
前者はhash-object
を使うことで.git/objects
に新しいblobオブジェクトを生成できますね?
git hash-object -w <file-name>
後者はupdate-index
という配管コマンドを使います.名前の通りindex
を更新できますがいくつか引数とオプションを与える必要があります.具体的には以下の通りです.
git update-index --add --cacheinfo <mode> <hash> <file-name>
--add
オプションはindex
に追加されていないファイルをadd
する際に利用します.
--cacheinfo
はindex
に記録される値を記述します.引数としてtreeオブジェクトの際に実装したFile
構造体の持っている情報と同じものを渡しています.mode
のところにファイルの種類が入ります.残りの二つはそのままです.
git commit
git commitがやっていることは大きく分けて3つです.
- treeオブジェクトの生成
- commitオブジェクトの生成
ref
に書かれているcommitへの参照を更新
まずgit write-tree
でindex
の情報をもとにtreeオブジェクトを生成します.
次に,作成したtreeオブジェクトのハッシュ値を使ってcommitオブジェクトを生成します.これはcommit-tree
コマンドを使います.いつも通り-m
でコミットメッセージがつけられます.
最後にref
に記述されているcommitへの参照を更新してやります.これはupdate-ref
コマンドを用います.引数としてref
内のブランチのパスとcommitオブジェクトのハッシュ値を渡します.
git write-tree
git commit-tree <hash> -m "commit message"
git update-ref refs/heads/<branch> <hash>
終わりに
やった内容はこんな感じです.最初に載せた記事の方はまだまだ続きがあるので皆さん買ってやりましょう.
感想ですが,Gitの内部構造をなんとなく理解できたおかげでとログの見方が分かったり誤った処理をやったときの対処などが想像できるようになってとてもよかったです.今回は参考記事をほとんどそのまま写経していきましたがもう実際の動きは分かっているのでもうちょっとこだわった実装で書き直してみても面白いかもですね.とくにtreeオブジェクトを実際に木構造で書いてみたさがあります.
それでは長文になりましたがこれで失礼します.皆さんよいお年をお迎えください~~~!