- バージョン管理システムの内部構造を理解するために、Gitに似たシステムを自分で実装してみた
- SHA-256ハッシュとzstd圧縮を使って、GitのSHA-1とzlibを置き換え、
.tvcディレクトリ構造でリポジトリを構成
- Rustで書かれており、ファイルのハッシュ・圧縮・コミット・チェックアウト機能を段階的に実装
- コミットオブジェクトにはツリーハッシュ、親コミット、作者、メッセージが含まれ、同一ファイルはハッシュ重複防止によって再保存されない
- Gitがコンテンツアドレスベースのファイルストレージであることを自ら体験し、構造化データフォーマットの重要性を強調
ハッシュ化と圧縮方式
- GitはすべてのオブジェクトをSHA-1ハッシュで識別するが、このプロジェクトではSHA-256を使用
- SHA-1は古く安全性の面で脆弱だが、このプロジェクトでは単にファイル内容を識別する用途なので、セキュリティは重要ではない
- Gitのzlibの代わりにFacebookのzstd圧縮ライブラリを採用
- zstdのほうが効率的だと判断し、Git互換性は目標ではなかった
- プロジェクト名は**「tvc (Tony’s Version Control)」**で、
.tvcと.tvcignoreファイルをGitの対応する構造として使用
実装ステップ
- 実装手順はコマンド引数の読み取り → 無視ルールの読み取り → ファイル一覧の出力 → ハッシュ化と圧縮 → ツリー・コミット生成 → HEAD管理 → コミットのチェックアウトの順
- Rustで書かれており、
lsコマンドは.tvcignoreルールを適用して非無視ファイルを再帰的に探索し、各ファイルのSHA-256ハッシュを出力
- zstdライブラリを用いてファイルの圧縮および展開機能を簡単に実装
コミット構造
- コミットオブジェクトには次の情報を含む
- オブジェクト種別(“commit”)
- その時点のファイルシステム状態(ツリーハッシュ)
- 前のコミット(HEAD)
- 作者(author)
- コミットメッセージ
- Gitとは異なり、作者とコミッターの区別を省略し、マージやリベース機能は実装していない
- コミット生成時にツリーオブジェクトを生成・ハッシュ化・圧縮して
.tvc/objects/に保存し、HEADファイルを更新
- 同一ファイルはハッシュが同じなら再保存されないため、重複保存の防止が可能
ツリーオブジェクトとチェックアウト
generate_tree()関数はディレクトリを巡回し、各ファイルをハッシュ化・圧縮・保存し、ファイル名とハッシュを文字列として構成
- 下位ディレクトリは再帰的に処理してツリー構造を形成
- コミットとツリーオブジェクトを構造体(
Commit, Tree)としてパースし、メモリ上で扱いやすいように構成
generate_fs()関数はツリー構造に基づいてファイルシステムを再生成し、指定パスにチェックアウトを実行
プロジェクトの教訓
- Gitはコンテンツアドレスベースの(key-value)ファイルストレージであることを自ら体験
- 最も難しかった部分はオブジェクトフォーマットのパースで、次はYAMLやJSONのような明確なフォーマットを使う予定
- 全コードはGitHubリポジトリ(tonystr/t-version-control)で公開
1件のコメント
Hacker Newsの意見
Gitが recursive merge strategy をサポートする唯一のSCMだという点が興味深い
この方式は過去の競合解決履歴を自動で覚えてくれるので非常に便利
多くの人はいまだにrebaseを好むが、mergeを実装するなら必ず 競合解決履歴の保存メカニズム を入れるべき
関連参考: Merge made by recursive strategy
参考: Git Tools - Rerere
リンク
git mergeには “null” 戦略がないすでに競合を解決したので単にmergeの記録だけ残したいときでも、Gitはわざわざ 助けようとする試み をする
インデックスやワークツリーに触れず、単にmergeした事実だけを記録するオプションがあればよいのにと思う
たとえば Pijul はそうしている
複数コミットでの試行錯誤が見えず、元に戻すのも難しく、すでにmergeされたブランチで追加作業を続けるのもやりにくい
複数のPRが1つのパズルのピースなら、単純なmergeのほうがずっと良いと思う
毎日使うツールの内部を学ぶのはいつでも楽しい
とくに Git from the Bottom Up は、Gitの内部構造を明快に説明してくれる素晴らしい文章だ
20分ほどでGitコマンドの 不透明な動作原理 を理解できる
cat-fileコマンドでハッシュIDを直接確認できると今になって知ったが、かなり面白いコーディングエージェントがどうやって計画を立てるのか気になるなら、こういう記事こそ彼らの 訓練データ だ
ただし著者がLLMの助けを借りていたなら、循環的な状況になるかもしれない
おそらく 公開リポジトリをかき集めるボット が存在するのだろう
自分のコードがLLM学習に使われると考えると妙な気分だ
記事そのものにはLLMの出力はないが、Rustコードの慣習やアルゴリズム比較の助言をもらうときにはChatGPTを使った
CodeCraftersの “Build your own Git” チュートリアルは本当に素晴らしい
Rustで直接実装する Jon Gjengsetのライブ動画 もおすすめ
自分も、バージョン管理が ソフトウェア以外の分野 でももっと使われてほしいと思う
GotVC は、E2E暗号化、並列import、大容量ファイル対応の構造を備えた興味深いプロジェクトだ
結局は元のプログラムで開いて比較するしかない
この記事を見て ugit: DIY Git in Python を思い出した
Gitの内部を深く掘り下げつつ、追いやすく説明した最高の資料のひとつだ
MetaのMercurialフォークである Sapling VCS はZstd dictionary圧縮を使っている
説明文書 でGitのdelta-compressed packfileと比較できる
小さなリポジトリではGitのdelta圧縮のほうが効率的だが、大規模リポジトリでは パスベースのdictionary圧縮 のほうが優れている
最近のGitにも似た “path-walk” 機能が追加された
自分も似たような試みをしたことがあり、プロジェクト名は “shit” だ
GitHubリンク
昔SPAフレームワークを作ろうとして、隠れた複雑さ に驚いた記憶がある
ReactやAngularの開発者たちもこういう 底なし沼 を経験したのだろうと思う
Gitも同じように複雑さをうまく隠している
PHPで書かれたGitクライアントを見たが、packfileとreftableを読み、LCSベースのdiffにも対応している
gipht-horse
そして
@をHEADの代わりに使えると初めて知ったが、文法的にもかなり理にかなっている