- Zig 99.7%
- Shell 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| CLAUDE.md | ||
| context.md | ||
| DESIGN.md | ||
| LICENSE | ||
| README.md | ||
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 (v2–v4)
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
addandreset(unstage) (no directory/glob staging yet) - Commit creation with system timezone detection
- Line-level blame (first-parent walk)
License
MIT