Simple Git plumbing library for Zig.
  • Zig 99.7%
  • Shell 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker ee5e77f173 Repository: discover a repo from any path inside it, and read a path at a ref
Two gaps an embedder hits immediately, both of which every consumer would
otherwise fill in for itself.

open() takes a work-tree ROOT and looks for .git directly under it. An
application almost never has that. It has the path of a document the user
opened, somewhere below the root, and it needs two things from it: the
repository, and that path spelled relative to the work tree - which is the
form lookupPath, addFile and blameFile all consume. discover() walks up
the way git does and returns both.

It honours a .git FILE as well as a directory. Without that, a linked
worktree or a submodule walks straight past its own repository into the
parent and answers confidently about the wrong one. The path need not
exist yet either, only its directory, so a Save As target discovers the
same repository the eventual file will live in.

readFileAtRef collapses the five-step walk to a document's bytes at a
commit: resolve, read commit, parse, look the path up in the tree, read
the blob. This library already contained that walk twice, in unstageFile
and in blame. A diff against HEAD is the third caller and the first one
outside the library. Absent from the tree returns null rather than an
error, because "not in HEAD" is the ordinary answer for a file the user
just created.

Six tests: nested and root-relative discovery, a not-yet-written path, a
missing directory kept distinct from a missing repository, the .git file
redirection, and a blob read through a nested tree that stays put when
the working tree moves on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxMhgcSjjDa9sWuVzWPDQA
2026-09-04 10:45:14 +02:00
scripts setup-tools: detect checkouts with rev-parse, not [ -d .git ] 2026-07-25 23:33:03 +02:00
src Repository: discover a repo from any path inside it, and read a path at a ref 2026-09-04 10:45:14 +02:00
tests fix: audit findings — refs, index, status, write path, blame 2026-07-19 01:03:02 +02:00
.gitignore diff: indent heuristic + extensive git-parity tests 2026-06-28 09:46:10 +02:00
build.zig tests: integration suite against system git (zig build test-integration) 2026-07-12 17:39:08 +02:00
build.zig.zon Port to Zig 0.16 2026-04-30 07:25:17 +02:00
CLAUDE.md docs: repoint links to git.urverk.org 2026-07-25 11:41:29 +02:00
context.md docs: record audit session 2026-07-19 01:03:19 +02:00
DESIGN.md docs: record fuzz harnesses and wrinkle-hunt results 2026-07-18 21:28:03 +02:00
LICENSE Initial project: git plumbing library with object, commit, tree, and refs modules 2026-03-03 12:35:21 +01:00
README.md Port to Zig 0.16 2026-04-30 07:25:17 +02:00

plumbuz

A Git plumbing library for Zig. Pure Zig, no subprocesses, no C dependencies.

Designed for embedding in applications — editors, TUIs, dev tools — where you need direct access to Git internals without shelling out to git. Comes with a small CLI for testing and exploration.

Features

Object store — read and write loose objects (blob, tree, commit, tag) with SHA-1 and SHA-256 support. Pack file reading with index caching.

Refs — resolve symbolic refs, read HEAD, list branches

Index — parse, mutate, and write .git/index (v2v4)

Staging & commits — full git add + git commit workflow with automatic timezone detection, git reset (unstage) support

Diff — tree-to-tree comparison and line-level Myers diff

Log — commit walker with priority queue (reverse chronological, dedup)

Status — working tree status: staged changes, unstaged modifications, untracked files

Blame — line-level attribution via first-parent walk and diff mapping

.gitignore — hierarchical pattern matching (.git/info/exclude, root, per-directory)

Library usage

Add plumbuz as a Zig dependency. plumbuz follows Zig 0.16's explicit-I/O pattern: pass a std.Io (typically from std.process.Init.io) into Repository.open, after which the handle threads it through internally.

const std = @import("std");
const plumbuz = @import("plumbuz");

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const allocator = init.gpa;

    // Open a repository
    var repo = try plumbuz.Repository.open(allocator, io, ".");
    defer repo.close();

    // Stage and unstage files
    try repo.addFile("src/main.zig");
    try repo.unstageFile("src/main.zig");

    // Get status
    var status = try repo.getStatus();
    defer status.deinit();

    for (status.files) |f| {
        std.debug.print("{s}: index={s} worktree={s}\n", .{
            f.path,
            @tagName(f.index_status),
            @tagName(f.worktree_status),
        });
    }

    // Walk commit history
    var walker = try repo.logFrom("HEAD");
    defer walker.deinit();

    while (try walker.next()) |*entry| {
        defer entry.deinit();
        var hex_buf: [64]u8 = undefined;
        const hex = entry.oid.toHex(&hex_buf);
        std.debug.print("{s} {s}\n", .{ hex[0..7], entry.commit.message });
    }

    // Blame a file
    var result = try repo.blameFile("src/main.zig");
    defer result.deinit();

    for (result.lines) |line| {
        var hex_buf: [64]u8 = undefined;
        const hex = line.oid.toHex(&hex_buf);
        std.debug.print("{s} ({s} {d}) {s}\n", .{
            hex[0..7], line.author.name, line.line_no, line.line,
        });
    }

    // Read objects directly
    const oid = try repo.resolveRef("HEAD");
    var obj = try repo.readObject(oid);
    defer obj.deinit();
}

CLI

$ zig build
$ ./zig-out/bin/plumbuz help
Usage: plumbuz <command> [args]

Commands:
  add        Stage a file
  reset      Unstage a file
  commit     Record changes to the repository
  status     Show working tree status
  log        Show commit history
  branch     List branches
  diff       Show changes between HEAD~1 and HEAD
  blame      Show line-by-line attribution
  cat-file   Display object contents
  help       Show this help

Examples

$ plumbuz status
On branch main
Changes not staged for commit:
  modified:   src/index.zig

Untracked files:
  notes.txt

$ plumbuz add src/index.zig

$ plumbuz log -n3
commit a1b2c3d4...
Author: Alice <alice@example.com>
Date:   Mon Mar 3 10:30:00 2026 +0100

    Add staging workflow

$ plumbuz branch
* main
  feature/parser

$ plumbuz blame src/main.zig
a1b2c3d (Alice 1) const std = @import("std");
f8e9d0c (Bob   2) const plumbuz = @import("plumbuz");
a1b2c3d (Alice 3) ...

$ plumbuz reset src/index.zig
Unstaged 'src/index.zig'

$ plumbuz cat-file a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
commit (size 231)
tree 4b825dc642cb6eb9a060e54bf899d15644e68cb5
parent ...

Building

Requires Zig 0.16.

zig build        # build CLI
zig build test   # run test suite

Status

Early development. Read path is solid, write path covers staging and commits. Currently supports:

  • SHA-1 (default) and SHA-256 object formats (auto-detected from .git/config)
  • Loose object read/write and pack file reading (OFS_DELTA/REF_DELTA)
  • Hierarchical .gitignore (.git/info/exclude, root, per-directory)
  • Single-file add and reset (unstage) (no directory/glob staging yet)
  • Commit creation with system timezone detection
  • Line-level blame (first-parent walk)

License

MIT