zerogit 0.2.0

A lightweight, pure Rust Git client library
Documentation

zerogit

Pure Rust製の軽量Gitクライアントライブラリ。最小限の依存でGitリポジトリの読み書きを実現します。

Crates.io Documentation License

特徴

  • Pure Rust: Cバインディングなし、クロスコンパイルが容易
  • 最小依存: miniz_oxide(zlib解凍)のみに依存
  • 軽量: 必要な機能だけを実装したシンプルな設計
  • 学習向け: Git内部構造の理解に役立つクリーンな実装

インストール

Cargo.toml に以下を追加:

[dependencies]
zerogit = "0.2"

必要環境

  • Rust 1.70.0 以上
  • Linux / macOS / Windows

クイックスタート

リポジトリを開いてログを表示

use zerogit::{Repository, Result};

fn main() -> Result<()> {
    // カレントディレクトリから.gitを探索
    let repo = Repository::discover(".")?;
    
    // 最新10件のコミットを表示
    for commit in repo.log()?.take(10) {
        let commit = commit?;
        println!("{} {}", commit.oid().short(), commit.summary());
    }
    
    Ok(())
}

ステータスを確認

use zerogit::{Repository, FileStatus, Result};

fn main() -> Result<()> {
    let repo = Repository::open(".")?;
    
    for entry in repo.status()? {
        let marker = match entry.status() {
            FileStatus::Untracked => "??",
            FileStatus::Modified => " M",
            FileStatus::Added => "A ",
            FileStatus::Deleted => " D",
            _ => "  ",
        };
        println!("{} {}", marker, entry.path().display());
    }
    
    Ok(())
}

特定コミットの詳細を取得

use zerogit::{Repository, Result};

fn main() -> Result<()> {
    let repo = Repository::discover(".")?;
    
    // 短縮形式でもOK
    let commit = repo.commit("abc1234")?;
    
    println!("Commit:  {}", commit.oid());
    println!("Author:  {} <{}>", commit.author().name(), commit.author().email());
    println!("Message: {}", commit.summary());
    
    Ok(())
}

ブランチ一覧

use zerogit::{Repository, Result};

fn main() -> Result<()> {
    let repo = Repository::discover(".")?;
    let head = repo.head()?;
    
    for branch in repo.branches()? {
        let marker = if head.branch().map(|b| b.name()) == Some(branch.name()) {
            "* "
        } else {
            "  "
        };
        println!("{}{}", marker, branch.name());
    }
    
    Ok(())
}

API概要

主要な型

説明
Repository リポジトリ操作のエントリーポイント
Commit コミット情報(author, message, parents等)
Tree ディレクトリ構造
Blob ファイル内容
Oid オブジェクトID(SHA-1ハッシュ)
Branch ブランチ情報
Head HEAD参照(ブランチまたはdetached)

Repository メソッド

// リポジトリを開く
Repository::open(path)?;      // 指定パス
Repository::discover(path)?;  // 親ディレクトリを探索

// 読み取り操作
repo.head()?;                 // HEAD取得
repo.branches()?;             // ブランチ一覧
repo.log()?;                  // コミット履歴(Iterator)
repo.status()?;               // ワーキングツリー状態
repo.commit("sha")?;          // コミット取得
repo.tree("sha")?;            // ツリー取得
repo.blob("sha")?;            // Blob取得
repo.index()?;                // インデックス取得

// 書き込み操作
repo.add(path)?;              // ファイルをステージ
repo.add_all()?;              // 全変更をステージ
repo.reset(path)?;            // ステージを解除
repo.create_commit(msg, author, email)?;  // コミット作成
repo.create_branch(name, target)?;        // ブランチ作成
repo.delete_branch(name)?;                // ブランチ削除
repo.checkout(target)?;                   // ブランチ切り替え

詳細は APIドキュメント を参照してください。

使用例

ファイル内容の取得

let repo = Repository::discover(".")?;
let head = repo.head()?;
let commit = repo.commit(&head.oid().to_hex())?;
let tree = repo.tree(&commit.tree().to_hex())?;

if let Some(entry) = tree.get("README.md") {
    let blob = repo.blob(&entry.oid().to_hex())?;
    println!("{}", blob.content_str()?);
}

コミット間の差分ファイル一覧

let repo = Repository::discover(".")?;
let commits: Vec<_> = repo.log()?.take(2).collect::<Result<_, _>>()?;

if commits.len() == 2 {
    let tree1 = repo.tree(&commits[0].tree().to_hex())?;
    let tree2 = repo.tree(&commits[1].tree().to_hex())?;

    // ツリーを比較して差分を検出
    // ...
}

ファイルをステージしてコミット

use zerogit::{Repository, Result};

fn main() -> Result<()> {
    let repo = Repository::discover(".")?;

    // ファイルをステージ
    repo.add("src/main.rs")?;

    // または全変更をステージ
    repo.add_all()?;

    // コミット作成
    let oid = repo.create_commit(
        "Add new feature",
        "Your Name",
        "your@email.com"
    )?;

    println!("Created commit: {}", oid.short());
    Ok(())
}

ブランチ操作

use zerogit::{Repository, Result};

fn main() -> Result<()> {
    let repo = Repository::discover(".")?;

    // 新しいブランチを作成
    repo.create_branch("feature/new-feature", None)?;

    // ブランチに切り替え
    repo.checkout("feature/new-feature")?;

    // 作業後、mainに戻る
    repo.checkout("main")?;

    // ブランチを削除
    repo.delete_branch("feature/new-feature")?;

    Ok(())
}

ロードマップ

Phase 1: 読み取り操作(MVP)✅

ローカルリポジトリの読み取り機能を提供します。

  • オブジェクト読み取り(blob/tree/commit)
  • 参照解決(HEAD/branches/tags)
  • コミット履歴イテレータ
  • ワーキングツリーステータス
  • インデックス読み取り

Phase 2: 書き込み操作

ローカルリポジトリへの書き込み機能を提供します。

  • add / reset - ステージング操作
  • commit - コミット作成
  • branch - ブランチ作成・削除
  • checkout - ブランチ切り替え

Phase 3: 差分・マージ・Packfile

差分計算とPackfile対応を提供します。

機能 説明 難易度
Tree diff 2つのTree間の差分計算
Blob diff ファイル内容の行単位差分(Myers算法)
Packfile読み取り .git/objects/pack/*.pack の読み取り
Packfileインデックス .idx ファイルによる高速検索
Delta復元 ofs_delta / ref_delta の展開
3-way merge 共通祖先ベースのマージ

想定API:

// 差分取得
let diff = repo.diff_trees(&tree1, &tree2)?;
for delta in diff.deltas() {
    println!("{:?} {}", delta.status(), delta.path());
}

// Packfile対応(内部的に自動処理)
let obj = repo.object("abc123")?;  // looseまたはpackから透過的に取得

Phase 4: リモート操作(別crate: zerogit-remote

ネットワーク操作は依存関係が増えるため、別crateとして提供予定です。

なぜ別crateなのか?

観点 zerogit (コア) zerogit-remote
依存 miniz_oxide のみ rustls, russh, ureq
ビルド時間 高速 TLS/SSH依存で増加
WASM対応 △(制限あり)
組み込み用途

サポート予定プロトコル

プロトコル URL形式 認証方式 優先度
HTTPS https://github.com/... Basic / Bearer Token
SSH git@github.com:... SSH鍵
Git git://... なし(読み取り専用)

想定API

use zerogit::Repository;
use zerogit_remote::{Remote, Credentials};

// クローン
let repo = Remote::clone(
    "https://github.com/user/repo.git",
    "./local-repo",
    Credentials::token("ghp_xxxx"),
)?;

// フェッチ
let remote = repo.remote("origin")?;
remote.fetch(&Credentials::ssh_key("~/.ssh/id_ed25519"))?;

// プッシュ
remote.push("main", &Credentials::token("ghp_xxxx"))?;

技術的な実装要素

Smart HTTP Protocol:
┌─────────┐                              ┌─────────┐
│ Client  │  GET /info/refs              │ Server  │
│         │ ───────────────────────────> │         │
│         │  200 OK (refs + capabilities)│         │
│         │ <─────────────────────────── │         │
│         │                              │         │
│         │  POST /git-upload-pack       │         │
│         │  (want/have negotiation)     │         │
│         │ ───────────────────────────> │         │
│         │  200 OK (packfile)           │         │
│         │ <─────────────────────────── │         │
└─────────┘                              └─────────┘

代替アプローチ

リモート操作が必要だが zerogit-remote を待てない場合、システムのgitコマンドと連携できます:

use std::process::Command;

fn fetch_with_git(repo_path: &str, remote: &str) -> std::io::Result {
    Command::new("git")
        .args(["-C", repo_path, "fetch", remote])
        .status()?;
    Ok(())
}

将来の検討事項

  • Worktree対応: 複数のワーキングツリー
  • Submodule対応: サブモジュールの読み取り
  • Sparse checkout: 部分的なチェックアウト
  • Shallow clone: 履歴を限定したクローン

貢献

コントリビューションを歓迎します!

開発環境のセットアップ

git clone https://github.com/siska-tech/zerogit
cd zerogit

# テスト用フィクスチャの準備
cd tests/fixtures
bash create_fixtures.sh
cd ../..

# テスト実行
cargo test

# フォーマットとLint
cargo fmt
cargo clippy

プルリクエスト

  1. Issueを作成して変更内容を議論
  2. フォークしてfeatureブランチを作成
  3. 変更を実装(テスト必須)
  4. cargo fmtcargo clippy を実行
  5. プルリクエストを送信

コーディング規約

  • cargo fmt でフォーマット
  • cargo clippy の警告をゼロに
  • 公開APIには必ずドキュメントコメント
  • 新機能にはテストを追加

設計ドキュメント

詳細な設計については以下を参照:

関連プロジェクト

  • gitoxide - フル機能のPure Rust Git実装
  • git2-rs - libgit2のRustバインディング

zerogitは学習目的と軽量な用途に特化しています。フル機能が必要な場合は上記のプロジェクトを検討してください。

ライセンス

本プロジェクトはデュアルライセンスです:

お好きな方を選択してください。

謝辞

  • Git - オリジナル実装とドキュメント
  • Pro Git Book - Git内部構造の解説
  • gitoxide - Pure Rust実装の参考