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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The ref store read through `Repository`, compared with what git says//! of the same repository, for both backends.
const std = @import("std");const testing = std.testing;const Allocator = std.mem.Allocator;
const Repository = @import("../Repository.zig");const RefStore = @import("../RefStore.zig");const discover = @import("../discover.zig");const fixture = @import("../testing/fixture.zig");const object = @import("../object.zig");
const formats = [_][]const u8{ "files", "reftable" };
fn open(repo: *fixture.Repo, sub: []const u8) !Repository { const start = try std.fs.path.join(testing.allocator, &.{ repo.path, sub }); defer testing.allocator.free(start); return Repository.discoverAndOpen(testing.allocator, testing.io, .{ .start = start, .ownership = if (discover.effectiveUid()) |uid| .{ .uid = uid } else null, }, .{ .environment = &repo.env }, null);}
/// A history with branches, annotated and nested tags, a tag of a tree,/// a remote's symbolic `HEAD`, some refs packed and some loose.fn makeRefs(repo: *fixture.Repo) !void { try repo.commit("one"); try repo.git(&.{ "tag", "-a", "-m", "version one", "v1" }); try repo.git(&.{ "tag", "-a", "-m", "nested", "v1-nested", "v1" }); try repo.git(&.{ "tag", "-a", "-m", "a tree", "tree-tag", "HEAD^{tree}" }); try repo.git(&.{ "tag", "light" }); try repo.git(&.{ "branch", "side" }); try repo.git(&.{ "update-ref", "refs/remotes/origin/main", "HEAD" }); try repo.git(&.{ "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main" }); try repo.git(&.{ "pack-refs", "--all" }); try repo.commit("two"); try repo.git(&.{ "branch", "a/b" }); try repo.git(&.{ "branch", "a-b" }); try repo.git(&.{ "tag", "-a", "-m", "loose", "v2" });}
fn trim(s: []const u8) []const u8 { return std.mem.trimEnd(u8, s, "\n");}
test "refs are listed, resolved and peeled as git does" { const gpa = testing.allocator; for (formats) |ref_format| { var repo = try fixture.Repo.initWith(gpa, testing.io, .{ .ref_format = ref_format }); defer repo.deinit(); try makeRefs(&repo); var r = try open(&repo, "."); defer r.close(testing.io); const store = r.refs();
// What for-each-ref lists. const expected = try repo.output(&.{ "for-each-ref", "--format=%(refname) %(objectname) %(*objectname) %(symref)" }); defer gpa.free(expected); var got: std.Io.Writer.Allocating = .init(gpa); defer got.deinit(); var l = try store.list(testing.io, gpa, "", null); defer l.deinit(); for (l.entries) |e| { var resolved = try store.resolve(testing.io, gpa, e.name, null); defer resolved.deinit(gpa); const d = resolved.outcome.direct; const peeled = try RefStore.peel(testing.io, gpa, r.database.database(), d.id, null); try got.writer.print("{s} {f} ", .{ e.name, d.id }); if (!peeled.id.eql(d.id)) try got.writer.print("{f}", .{peeled.id}); try got.writer.print(" {s}\n", .{if (resolved.isSymbolic()) resolved.last() else ""}); // The storage's own peeled value, when it has one, agrees. if (d.peeled) |p| try testing.expect(p.eql(peeled.id)); } try testing.expectEqualStrings(expected, got.written());
// HEAD, and what `onbranch:` sees. var head = try store.resolve(testing.io, gpa, "HEAD", null); defer head.deinit(gpa); try testing.expectEqualStrings("refs/heads/main", head.last()); const head_id = try repo.output(&.{ "rev-parse", "HEAD" }); defer gpa.free(head_id); var buf: [object.max_hex_length]u8 = undefined; try testing.expectEqualStrings(trim(head_id), head.outcome.direct.id.toHex(&buf)); try testing.expectEqualStrings("main", r.branch().?);
// A prefix lists only what is under it. var tags = try store.list(testing.io, gpa, "refs/tags/", null); defer tags.deinit(); for (tags.entries) |e| try testing.expect(std.mem.startsWith(u8, e.name, "refs/tags/")); try testing.expectEqual(5, tags.entries.len);
// Short names, as for-each-ref shortens them. const shorts = try repo.output(&.{ "for-each-ref", "--format=%(refname:short)" }); defer gpa.free(shorts); got.clearRetainingCapacity(); for (l.entries) |e| try got.writer.print("{s}\n", .{try store.shorten(testing.io, gpa, e.name, true, null)}); try testing.expectEqualStrings(shorts, got.written()); }}
test "symbolic refs are followed as far as git follows them" { const gpa = testing.allocator; for (formats) |ref_format| { var repo = try fixture.Repo.initWith(gpa, testing.io, .{ .ref_format = ref_format }); defer repo.deinit(); try repo.commit("one"); try repo.git(&.{ "symbolic-ref", "refs/heads/s0", "refs/heads/main" }); for (1..6) |i| { var from: [32]u8 = undefined; var to: [32]u8 = undefined; try repo.git(&.{ "symbolic-ref", try std.fmt.bufPrint(&from, "refs/heads/s{d}", .{i}), try std.fmt.bufPrint(&to, "refs/heads/s{d}", .{i - 1}) }); } try repo.git(&.{ "symbolic-ref", "refs/heads/unborn", "refs/heads/nothing" }); try repo.git(&.{ "symbolic-ref", "refs/heads/loop-a", "refs/heads/loop-b" }); try repo.git(&.{ "symbolic-ref", "refs/heads/loop-b", "refs/heads/loop-a" }); var r = try open(&repo, "."); defer r.close(testing.io); const store = r.refs(); for ([_][]const u8{ "refs/heads/s3", "refs/heads/s4", "refs/heads/s5", "refs/heads/unborn", "refs/heads/loop-a", "refs/heads/nope" }) |n| { var resolved = try store.resolve(testing.io, gpa, n, null); defer resolved.deinit(gpa); const o = try repo.runIn(".", &.{ "rev-parse", "-q", "--verify", n }, ""); defer o.deinit(gpa); try testing.expectEqual(o.code == 0, resolved.outcome == .direct); } var loop = try store.resolve(testing.io, gpa, "refs/heads/loop-a", null); defer loop.deinit(gpa); try testing.expectEqual(.cycle, loop.outcome); var unborn = try store.resolve(testing.io, gpa, "refs/heads/unborn", null); defer unborn.deinit(gpa); try testing.expectEqual(.missing, unborn.outcome); try testing.expectEqualStrings("refs/heads/nothing", unborn.last()); }}
test "reflogs read as git reflog shows them" { const gpa = testing.allocator; for (formats) |ref_format| { var repo = try fixture.Repo.initWith(gpa, testing.io, .{ .ref_format = ref_format }); defer repo.deinit(); try repo.commit("one"); try repo.commit("two"); try repo.git(&.{ "branch", "side" }); try repo.git(&.{ "update-ref", "-m", "moved\tby hand", "refs/heads/side", "HEAD~1" }); try repo.git(&.{ "update-ref", "--create-reflog", "refs/heads/quiet", "HEAD" }); try repo.git(&.{ "checkout", "-q", "side" }); var r = try open(&repo, "."); defer r.close(testing.io); for ([_][]const u8{ "HEAD", "refs/heads/main", "refs/heads/side", "refs/heads/quiet" }) |n| { const expected = try repo.output(&.{ "reflog", "show", "--date=raw", "--format=%H %gn <%ge> %gd %gs", n }); defer gpa.free(expected); var log = (try r.refs().reflog(testing.io, gpa, n, null)).?; defer log.deinit(); var got: std.Io.Writer.Allocating = .init(gpa); defer got.deinit(); var i = log.entries.len; while (i > 0) { i -= 1; const e = log.entries[i]; try got.writer.print("{f} {s} <{s}> {s}@{{{f}}} {s}\n", .{ e.new, e.name, e.email, try r.refs().shorten(testing.io, gpa, n, true, null), e.date, e.message }); } try testing.expectEqualStrings(expected, got.written()); } try testing.expect(try r.refs().reflog(testing.io, gpa, "refs/heads/none", null) == null); }}
test "a linked worktree reads its own per-worktree refs and reaches the others" { const gpa = testing.allocator; for (formats) |ref_format| { var repo = try fixture.Repo.initWith(gpa, testing.io, .{ .ref_format = ref_format }); defer repo.deinit(); try repo.commit("one"); try repo.git(&.{ "update-ref", "refs/bisect/main-good", "HEAD" }); try repo.git(&.{ "worktree", "add", "-q", "wt", "-b", "wt-branch" }); try repo.commit("two"); const wt_args = [_][]const u8{ "-C", "wt" }; try repo.git(&(wt_args ++ [_][]const u8{ "update-ref", "refs/bisect/wt-good", "HEAD" })); try repo.git(&(wt_args ++ [_][]const u8{ "update-ref", "refs/worktree/mine", "HEAD" })); for ([_][]const u8{ ".", "wt" }) |where| { var r = try open(&repo, where); defer r.close(testing.io); const store = r.refs(); const expected = try repo.output(&.{ "-C", where, "for-each-ref", "--format=%(refname) %(objectname)" }); defer gpa.free(expected); var got: std.Io.Writer.Allocating = .init(gpa); defer got.deinit(); var l = try store.list(testing.io, gpa, "", null); defer l.deinit(); for (l.entries) |e| try got.writer.print("{s} {f}\n", .{ e.name, e.value.direct.id }); try testing.expectEqualStrings(expected, got.written()); for ([_][]const u8{ "HEAD", "main-worktree/HEAD", "worktrees/wt/HEAD", "main-worktree/refs/bisect/main-good", "worktrees/wt/refs/bisect/wt-good" }) |n| { const id = try repo.output(&.{ "-C", where, "rev-parse", n }); defer gpa.free(id); var resolved = try store.resolve(testing.io, gpa, n, null); defer resolved.deinit(gpa); var buf: [object.max_hex_length]u8 = undefined; try testing.expectEqualStrings(trim(id), resolved.outcome.direct.id.toHex(&buf)); } } }}
test "a reftable stack after many updates, deletions and compaction reads as git's" { const gpa = testing.allocator; var repo = try fixture.Repo.initWith(gpa, testing.io, .{ .ref_format = "reftable" }); defer repo.deinit(); try repo.commit("one"); // Small blocks, so that tables have many blocks and indexes. try repo.git(&.{ "config", "reftable.blockSize", "256" }); var input: std.Io.Writer.Allocating = .init(gpa); defer input.deinit(); for (0..200) |i| try input.writer.print("create refs/heads/branch-{d:0>3} HEAD\n", .{i}); const out1 = try repo.outputWithInput(&.{ "update-ref", "--stdin" }, input.written()); gpa.free(out1); for (0..40) |i| { var buf: [64]u8 = undefined; try repo.git(&.{ "update-ref", "-d", try std.fmt.bufPrint(&buf, "refs/heads/branch-{d:0>3}", .{i * 5}) }); } try repo.commit("two"); for (0..20) |i| { var buf: [64]u8 = undefined; try repo.git(&.{ "update-ref", try std.fmt.bufPrint(&buf, "refs/heads/branch-{d:0>3}", .{i * 7}), "HEAD" }); } for ([_]bool{ false, true }) |compact| { if (compact) try repo.git(&.{"pack-refs"}); var r = try open(&repo, "."); defer r.close(testing.io); const expected = try repo.output(&.{ "for-each-ref", "--format=%(refname) %(objectname)" }); defer gpa.free(expected); var got: std.Io.Writer.Allocating = .init(gpa); defer got.deinit(); var l = try r.refs().list(testing.io, gpa, "", null); defer l.deinit(); for (l.entries) |e| { try got.writer.print("{s} {f}\n", .{ e.name, e.value.direct.id }); var target: std.ArrayList(u8) = .empty; defer target.deinit(gpa); const v = (try r.refs().read(testing.io, gpa, e.name, &target, null)).?; try testing.expect(v.direct.id.eql(e.value.direct.id)); } try testing.expectEqualStrings(expected, got.written()); var target: std.ArrayList(u8) = .empty; defer target.deinit(gpa); try testing.expect(try r.refs().read(testing.io, gpa, "refs/heads/branch-005", &target, null) == null); }}