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