Something went wrong. Try again.
A Zig library that reads git repositories without running git, with log, show and diff output byte-identical to git's.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The reftable backend: refs and reflogs kept in stacks of reftables,//! each stack a `reftable/` directory whose `tables.list` names its//! tables oldest first, as the reftable document describes.//!//! A newer table's record for a name overrides every older table's, and//! a deletion record hides the name altogether. The common directory's//! stack holds the shared refs and the main worktree's own; a linked//! worktree's git directory holds a stack of its own for its//! per-worktree refs. `FETCH_HEAD` and `MERGE_HEAD` are not in any//! stack: git keeps them as files in the git directory whatever the ref//! storage, and they are read as loose refs.//!//! Each stack is read whole the first time it is wanted and kept: the//! tables are immutable, so this is a snapshot of the refs at that moment,//! as the document's readers take one.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const object = @import("../object.zig");const Diagnostics = @import("../Diagnostics.zig");const RefStore = @import("../RefStore.zig");const Config = @import("../Config.zig");const name_rules = @import("name.zig");const loose = @import("loose.zig");const reftable = @import("reftable.zig");const reflog_format = @import("reflog.zig");const Files = @import("Files.zig");
const Reftable = @This();
gpa: Allocator,format: object.Format,base: Io.Dir,git_dir: []u8,common_dir: []u8,linked: bool,/// Stacks read so far, by the git directory they are in.stacks: std.StringArrayHashMapUnmanaged(*Stack) = .empty,
/// One stack of tables, read.const Stack = struct { tables: []Loaded,
const Loaded = struct { path: []u8, bytes: []u8, table: reftable.Table, };
fn deinit(s: *Stack, gpa: Allocator) void { for (s.tables) |t| { gpa.free(t.path); gpa.free(t.bytes); } gpa.free(s.tables); }};
/// A reftable backend for the refs `options` locates. Nothing is read/// yet.pub fn init(gpa: Allocator, options: Files.Options) Allocator.Error!Reftable { const git_dir = try gpa.dupe(u8, options.git_dir); errdefer gpa.free(git_dir); return .{ .gpa = gpa, .format = options.format, .base = options.base, .git_dir = git_dir, .common_dir = try gpa.dupe(u8, options.common_dir), .linked = options.linked, };}
/// Frees everything.pub fn deinit(r: *Reftable) void { for (r.stacks.keys(), r.stacks.values()) |k, s| { r.gpa.free(k); s.deinit(r.gpa); r.gpa.destroy(s); } r.stacks.deinit(r.gpa); r.gpa.free(r.git_dir); r.gpa.free(r.common_dir); r.* = undefined;}
/// The backend as a ref store.pub fn store(r: *Reftable) RefStore { return .{ .ptr = r, .vtable = &vtable };}
const vtable: RefStore.VTable = .{ .read = struct { fn read(ptr: *anyopaque, io: Io, gpa: Allocator, n: []const u8, target: *std.ArrayList(u8), diag: ?*Diagnostics) RefStore.ReadError!?RefStore.Value { return @as(*Reftable, @ptrCast(@alignCast(ptr))).read(io, gpa, n, target, diag); } }.read, .list = struct { fn list(ptr: *anyopaque, io: Io, gpa: Allocator, prefix: []const u8, diag: ?*Diagnostics) RefStore.ReadError!RefStore.List { return @as(*Reftable, @ptrCast(@alignCast(ptr))).list(io, gpa, prefix, diag); } }.list, .reflog = struct { fn reflog(ptr: *anyopaque, io: Io, gpa: Allocator, n: []const u8, diag: ?*Diagnostics) RefStore.ReadError!?RefStore.Reflog { return @as(*Reftable, @ptrCast(@alignCast(ptr))).reflog(io, gpa, n, diag); } }.reflog,};
/// The git directory whose stack holds `n` as the name reaches it, which/// the caller frees.fn stackDir(r: *const Reftable, gpa: Allocator, scoped: name_rules.Scoped) Allocator.Error![]u8 { if (!name_rules.isPerWorktree(scoped.name)) return gpa.dupe(u8, r.common_dir); return switch (scoped.worktree) { .current => gpa.dupe(u8, r.git_dir), .main => gpa.dupe(u8, r.common_dir), .linked => |w| blk: { const wt = try Files.joinPath(gpa, r.common_dir, "worktrees"); defer gpa.free(wt); break :blk Files.joinPath(gpa, wt, w); }, };}
/// The stack in the git directory `dir`, read the first time it is/// wanted. A directory with no `tables.list` has an empty stack.fn stack(r: *Reftable, io: Io, dir: []const u8, diag: ?*Diagnostics) RefStore.ReadError!*Stack { if (r.stacks.get(dir)) |s| return s; const gpa = r.gpa; const reftable_dir = try Files.joinPath(gpa, dir, "reftable"); defer gpa.free(reftable_dir); const list_path = try Files.joinPath(gpa, reftable_dir, "tables.list"); defer gpa.free(list_path); const listing = r.base.readFileAlloc(io, list_path, gpa, .limited(1 << 24)) catch |err| switch (err) { error.Canceled => |e| return e, error.OutOfMemory => |e| return e, error.FileNotFound => try gpa.dupe(u8, ""), else => return unreadable(diag, list_path, err), }; defer gpa.free(listing); var tables: std.ArrayList(Stack.Loaded) = .empty; errdefer { for (tables.items) |t| { gpa.free(t.path); gpa.free(t.bytes); } tables.deinit(gpa); } var lines = std.mem.tokenizeScalar(u8, listing, '\n'); while (lines.next()) |line| { const path = try Files.joinPath(gpa, reftable_dir, line); errdefer gpa.free(path); const bytes = r.base.readFileAlloc(io, path, gpa, .unlimited) catch |err| switch (err) { error.Canceled => |e| return e, error.OutOfMemory => |e| return e, error.FileNotFound => { if (diag) |d| d.setPath(&.{path}); return error.MissingReftable; }, else => return unreadable(diag, path, err), }; errdefer gpa.free(bytes); const table = reftable.Table.init(bytes, r.format, diag) catch |err| { if (diag) |d| d.setPath(&.{path}); return err; }; try tables.append(gpa, .{ .path = path, .bytes = bytes, .table = table }); } const s = try gpa.create(Stack); errdefer gpa.destroy(s); s.* = .{ .tables = try tables.toOwnedSlice(gpa) }; errdefer s.deinit(gpa); const key = try gpa.dupe(u8, dir); errdefer gpa.free(key); try r.stacks.put(gpa, key, s); // The table structs point into their bytes, which are owned now; the // tables themselves hold only slices, so nothing moved. return s;}
fn unreadable(diag: ?*Diagnostics, path: []const u8, err: anyerror) error{RefUnreadable} { if (diag) |d| { d.setPath(&.{path}); d.setDetail("{s}", .{Config.reason(err)}); } return error.RefUnreadable;}
fn withPath(diag: ?*Diagnostics, path: []const u8, err: RefStore.ReadError) RefStore.ReadError { if (diag) |d| d.setPath(&.{path}); return err;}
pub fn read(r: *Reftable, io: Io, gpa: Allocator, full: []const u8, target: *std.ArrayList(u8), diag: ?*Diagnostics) RefStore.ReadError!?RefStore.Value { const scoped = name_rules.Scoped.parse(full); const n = scoped.name; if (!name_rules.isValid(n, .{ .allow_onelevel = true })) return null; const dir = try r.stackDir(gpa, scoped); defer gpa.free(dir); if (name_rules.isPseudoref(n)) return readPseudoref(r, io, gpa, dir, n, target, diag); const s = try r.stack(io, dir, diag); var i = s.tables.len; while (i > 0) { i -= 1; const t = &s.tables[i]; var it: reftable.RefIterator = undefined; const found = t.table.findRef(gpa, n, &it, diag) catch |err| { it.deinit(); return withPath(diag, t.path, err); }; defer it.deinit(); const rec = found orelse continue; switch (rec.value) { .deletion => return null, .direct => |id| return .{ .direct = .{ .id = id } }, .peeled => |p| return .{ .direct = .{ .id = p.id, .peeled = p.peeled } }, .symbolic => |sym| { target.clearRetainingCapacity(); try target.appendSlice(gpa, sym); return .{ .symbolic = target.items }; }, } } return null;}
fn readPseudoref(r: *Reftable, io: Io, gpa: Allocator, dir: []const u8, n: []const u8, target: *std.ArrayList(u8), diag: ?*Diagnostics) RefStore.ReadError!?RefStore.Value { const path = try Files.joinPath(gpa, dir, n); defer gpa.free(path); const contents = r.base.readFileAlloc(io, path, gpa, .limited(1 << 24)) catch |err| switch (err) { error.Canceled => |e| return e, error.OutOfMemory => |e| return e, error.FileNotFound, error.NotDir, error.IsDir => return null, else => return unreadable(diag, path, err), }; defer gpa.free(contents); const v = loose.parse(r.format, contents) catch return error.MalformedRef; return switch (v) { .direct => |id| .{ .direct = .{ .id = id } }, .symbolic => |t| blk: { target.clearRetainingCapacity(); try target.appendSlice(gpa, t); break :blk .{ .symbolic = target.items }; }, };}
/// Adds every live ref of the stack in `dir` whose name begins with/// `prefix` and passes `keep` to `out`, newer tables overriding older.fn collect(r: *Reftable, io: Io, a: Allocator, dir: []const u8, prefix: []const u8, per_worktree: ?bool, out: *std.StringArrayHashMapUnmanaged(RefStore.EntryValue), diag: ?*Diagnostics) RefStore.ReadError!void { const s = try r.stack(io, dir, diag); for (s.tables) |*t| { var it = t.table.refs(a); defer it.deinit(); while (it.next(diag) catch |err| return withPath(diag, t.path, err)) |rec| { if (!std.mem.startsWith(u8, rec.name, prefix)) continue; if (name_rules.isRootRef(rec.name)) continue; if (per_worktree) |want| if (name_rules.isPerWorktree(rec.name) != want) continue; const value: ?RefStore.EntryValue = switch (rec.value) { .deletion => null, .direct => |id| .{ .direct = .{ .id = id } }, .peeled => |p| .{ .direct = .{ .id = p.id, .peeled = p.peeled } }, .symbolic => |sym| .{ .symbolic = try a.dupe(u8, sym) }, }; if (value) |v| { const gop = try out.getOrPut(a, rec.name); if (!gop.found_existing) gop.key_ptr.* = try a.dupe(u8, rec.name); gop.value_ptr.* = if (name_rules.isValid(rec.name, .{ .allow_onelevel = true })) v else .{ .broken = .bad_name }; } else _ = out.swapRemove(rec.name); } }}
pub fn list(r: *Reftable, io: Io, gpa: Allocator, prefix: []const u8, diag: ?*Diagnostics) RefStore.ReadError!RefStore.List { var arena: std.heap.ArenaAllocator = .init(gpa); errdefer arena.deinit(); const a = arena.allocator(); var refs: std.StringArrayHashMapUnmanaged(RefStore.EntryValue) = .empty; try r.collect(io, a, r.common_dir, prefix, if (r.linked) false else null, &refs, diag); if (r.linked) try r.collect(io, a, r.git_dir, prefix, true, &refs, diag); const entries = try a.alloc(RefStore.Entry, refs.count()); for (refs.keys(), refs.values(), entries) |k, v, *e| e.* = .{ .name = k, .value = v }; std.sort.block(RefStore.Entry, entries, {}, struct { fn lessThan(_: void, x: RefStore.Entry, y: RefStore.Entry) bool { return std.mem.lessThan(u8, x.name, y.name); } }.lessThan); return .{ .entries = entries, .arena = arena };}
pub fn reflog(r: *Reftable, io: Io, gpa: Allocator, full: []const u8, diag: ?*Diagnostics) RefStore.ReadError!?RefStore.Reflog { const scoped = name_rules.Scoped.parse(full); const n = scoped.name; if (!name_rules.isValid(n, .{ .allow_onelevel = true })) return null; const dir = try r.stackDir(gpa, scoped); defer gpa.free(dir); const s = try r.stack(io, dir, diag); var arena: std.heap.ArenaAllocator = .init(gpa); errdefer arena.deinit(); const a = arena.allocator(); const Keyed = struct { update_index: u64, entry: RefStore.ReflogEntry }; var seen: std.AutoHashMapUnmanaged(u64, void) = .empty; var found: std.ArrayList(Keyed) = .empty; var i = s.tables.len; while (i > 0) { i -= 1; const t = &s.tables[i]; var it = t.table.logs(gpa); defer it.deinit(); while (it.next(diag) catch |err| return withPath(diag, t.path, err)) |rec| { if (!std.mem.eql(u8, rec.name, n)) continue; const gop = try seen.getOrPut(a, rec.update_index); if (gop.found_existing) continue; switch (rec.value) { .deletion => {}, .entry => |e| { var message = e.message; if (message.len != 0 and message[message.len - 1] == '\n') message = message[0 .. message.len - 1]; try found.append(a, .{ .update_index = rec.update_index, .entry = .{ .old = e.old, .new = e.new, .name = try a.dupe(u8, e.name), .email = try a.dupe(u8, e.email), .date = .{ .seconds = std.math.cast(i64, e.time) orelse 0, .zone = reflog_format.zoneFromMinutes(e.tz_offset), }, .message = try a.dupe(u8, message), } }); }, } } } if (found.items.len == 0) { arena.deinit(); return null; } std.sort.block(Keyed, found.items, {}, struct { fn lessThan(_: void, x: Keyed, y: Keyed) bool { return x.update_index < y.update_index; } }.lessThan); const entries = try a.alloc(RefStore.ReflogEntry, found.items.len); for (found.items, entries) |k, *e| e.* = k.entry; return .{ .entries = entries, .arena = arena };}