// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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); } }