// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! A ref store: where a repository's refs are found by name, whatever the //! storage behind it — loose refs and `packed-refs` (`refs/Files.zig`) or //! a stack of reftables (`refs/Reftable.zig`). //! //! An implementation answers three questions of its storage, none of //! which follows a symbolic ref: what one name holds (`read`), which refs //! there are under a prefix, in order of name (`list`), and what a ref's //! reflog says (`reflog`). Everything else — following symbolic refs to //! the end of the chain, `HEAD`, peeling tags, short names — is answered //! here, the same way for every implementation, as git answers it. //! //! Worktrees are the implementation's business: a name such as `HEAD` or //! `refs/bisect/bad` is read from the worktree the store was opened for, //! and `main-worktree/HEAD` and `worktrees//HEAD` reach into the //! others, as git-worktree(1) describes. //! //! **Writing** is not here yet. When it comes it will be a ref //! transaction, a second vtable beside this one that an implementation //! may offer, so that a reader need not carry it; nothing in this //! interface assumes refs never change, but every implementation reads a //! snapshot the first time it is asked and answers from it after. const std = @import("std"); const Allocator = std.mem.Allocator; const Io = std.Io; const object = @import("object.zig"); const Id = object.Id; const Diagnostics = @import("Diagnostics.zig"); const ObjectDatabase = @import("ObjectDatabase.zig"); const Tag = @import("tag.zig").Tag; pub const name = @import("refs/name.zig"); const packed_refs = @import("refs/packed.zig"); const reftable = @import("refs/reftable.zig"); const reflog_format = @import("refs/reflog.zig"); const RefStore = @This(); ptr: *anyopaque, vtable: *const VTable, /// What an implementation provides. pub const VTable = struct { /// What `name` holds, without following it if it is symbolic, or null /// if there is no such ref; a symbolic target is written to `target`. read: *const fn (ptr: *anyopaque, io: Io, gpa: Allocator, ref_name: []const u8, target: *std.ArrayList(u8), diag: ?*Diagnostics) ReadError!?Value, /// Every ref whose name begins with `prefix`, in order of name: the /// refs under `refs/`, and whatever else the storage keeps among them /// (`packed-refs` may hold any name), but not the root refs kept in /// their own place, such as the files backend's `HEAD`. list: *const fn (ptr: *anyopaque, io: Io, gpa: Allocator, prefix: []const u8, diag: ?*Diagnostics) ReadError!List, /// The ref's reflog, oldest entry first, or null if it has none. reflog: *const fn (ptr: *anyopaque, io: Io, gpa: Allocator, ref_name: []const u8, diag: ?*Diagnostics) ReadError!?Reflog, }; /// What a ref holds. pub const Value = union(enum) { /// An object ID, and the object it peels to when the storage records /// that (a `packed-refs` peel line, or a reftable's peeled value). direct: Direct, /// The name of another ref. symbolic: []const u8, }; /// A ref holding an object ID. pub const Direct = struct { id: Id, /// What the storage says the object peels to, if it says. peeled: ?Id = null, }; /// One ref found by `list`. pub const Entry = struct { name: []const u8, value: EntryValue, }; /// What a listed ref holds, or why it cannot be used. pub const EntryValue = union(enum) { direct: Direct, symbolic: []const u8, /// A ref git lists only to pass over, or to stop at. broken: Broken, }; /// Why a listed ref cannot be used, each in git's words: pub const Broken = enum { /// Its contents are not a ref: "ignoring broken ref". malformed, /// Its name is not well formed: "ignoring ref with broken name". bad_name, /// Its name, in `packed-refs`, is not safe to use at all: "packed /// refname is dangerous". dangerous_name, }; /// The refs `list` found, and what they live in. pub const List = struct { entries: []Entry, arena: std.heap.ArenaAllocator, /// Frees everything. pub fn deinit(l: *List) void { l.arena.deinit(); l.* = undefined; } }; /// One change to a ref. pub const ReflogEntry = reflog_format.Entry; /// A ref's reflog, oldest entry first. pub const Reflog = reflog_format.Reflog; /// Why the refs could not be read. Besides running out of memory and /// being canceled: /// /// - `RefUnreadable`: a file could not be read; the diagnostic names it /// and the system's error. /// - `MalformedRef`: from `read`, a ref whose contents are not a ref /// ("ignoring broken ref"); `resolve` answers `.malformed` instead. /// - `UnexpectedPackedRefsLine`, `UnterminatedPackedRefsLine`: as /// `refs/packed.zig` says; the diagnostic names `packed-refs`. /// - `MalformedReftable`, `UnsupportedReftableVersion`, /// `ReftableChecksumMismatch`, `ReftableObjectFormatMismatch`: as /// `refs/reftable.zig` says; the diagnostic names the table. /// - `MissingReftable`: `tables.list` names a table that is not there. pub const ReadError = Allocator.Error || Io.Cancelable || packed_refs.Error || reftable.Error || error{ RefUnreadable, MissingReftable, MalformedRef, }; /// The most refs read in following one name: the name and four symbolic /// refs after it. git gives up on a fifth ("ignoring dangling symref"). pub const max_symref_depth = 5; /// What `read` says of a name. pub fn read(s: RefStore, io: Io, gpa: Allocator, ref_name: []const u8, target: *std.ArrayList(u8), diag: ?*Diagnostics) ReadError!?Value { return s.vtable.read(s.ptr, io, gpa, ref_name, target, diag); } /// Every ref whose name begins with `prefix`, unresolved, in git's order: /// by name, byte by byte. Root refs are not listed, unless the storage /// keeps one among the others, as a `packed-refs` file may; `for-each-ref` /// leaves those out (`name.isRootRef`) and `show-ref` does not. pub fn list(s: RefStore, io: Io, gpa: Allocator, prefix: []const u8, diag: ?*Diagnostics) ReadError!List { return s.vtable.list(s.ptr, io, gpa, prefix, diag); } /// The ref's reflog, oldest entry first, or null if it has none. pub fn reflog(s: RefStore, io: Io, gpa: Allocator, ref_name: []const u8, diag: ?*Diagnostics) ReadError!?Reflog { return s.vtable.reflog(s.ptr, io, gpa, ref_name, diag); } /// Where following a name ended. pub const Outcome = union(enum) { /// At an object ID. direct: Direct, /// At a name with no ref: the name asked for, or, at the end of a /// chain of symbolic refs, an unborn branch. missing, /// At a ref whose contents are not a ref. malformed, /// At a symbolic ref naming something that is not a well-formed ref /// name; the bad target is the chain's last name. bad_target, /// After `max_symref_depth` refs without reaching an object ID. too_deep, /// At a name already followed. cycle, }; /// A name followed to the end of its chain of symbolic refs. pub const Resolved = struct { /// Every name read, the one asked for first: a direct ref's chain is /// that one name. Owned. chain: [][]u8, outcome: Outcome, /// Frees the chain. pub fn deinit(r: *Resolved, gpa: Allocator) void { for (r.chain) |n| gpa.free(n); gpa.free(r.chain); r.* = undefined; } /// The last name in the chain: the ref holding the object ID, or /// where following stopped. pub fn last(r: *const Resolved) []const u8 { return r.chain[r.chain.len - 1]; } /// Whether the name asked for is a symbolic ref. pub fn isSymbolic(r: *const Resolved) bool { return r.chain.len > 1; } }; /// Follows `ref_name` through symbolic refs, as git does, to an object ID /// or to where it cannot go on. pub fn resolve(s: RefStore, io: Io, gpa: Allocator, ref_name: []const u8, diag: ?*Diagnostics) ReadError!Resolved { var chain: std.ArrayList([]u8) = .empty; errdefer { for (chain.items) |n| gpa.free(n); chain.deinit(gpa); } var target: std.ArrayList(u8) = .empty; defer target.deinit(gpa); try chain.append(gpa, try gpa.dupe(u8, ref_name)); const outcome: Outcome = while (true) { const current = chain.items[chain.items.len - 1]; if (chain.items.len > max_symref_depth) break .too_deep; target.clearRetainingCapacity(); const value = s.read(io, gpa, current, &target, diag) catch |err| switch (err) { error.MalformedRef => break .malformed, else => |e| return e, } orelse break .missing; switch (value) { .direct => |d| break .{ .direct = d }, .symbolic => |t| { const next = try gpa.dupe(u8, t); try chain.append(gpa, next); if (!name.isValid(next, .{ .allow_onelevel = true })) break .bad_target; const seen_before = for (chain.items[0 .. chain.items.len - 1]) |seen| { if (std.mem.eql(u8, seen, next)) break true; } else false; if (seen_before) break .cycle; }, } }; return .{ .chain = try chain.toOwnedSlice(gpa), .outcome = outcome }; } /// Whether a ref of this name exists in any form, broken or dangling /// included. pub fn exists(s: RefStore, io: Io, gpa: Allocator, ref_name: []const u8, diag: ?*Diagnostics) ReadError!bool { var target: std.ArrayList(u8) = .empty; defer target.deinit(gpa); const v = s.read(io, gpa, ref_name, &target, diag) catch |err| switch (err) { error.MalformedRef => return true, else => |e| return e, }; return v != null; } /// The shortest name that stands for `full` and for nothing else, as /// `%(refname:short)` and `symbolic-ref --short` shorten it: the rules of /// gitrevisions(7) tried from the one leaving the least, each short name /// kept only if no other rule (with `strict`, the default of /// `core.warnAmbiguousRefs`) or no earlier rule (without) turns it into a /// ref that exists. A name nothing shortens is returned whole; either way /// the result is a slice of `full`. pub fn shorten(s: RefStore, io: Io, gpa: Allocator, full: []const u8, strict: bool, diag: ?*Diagnostics) ReadError![]const u8 { var candidate: std.ArrayList(u8) = .empty; defer candidate.deinit(gpa); var i: usize = name.lookup_rules.len; while (i > 1) { i -= 1; const short = name.shortUnderRule(full, i) orelse continue; const ambiguous = for (name.lookup_rules, 0..) |rule, j| { if (j == i or (!strict and j > i)) continue; candidate.clearRetainingCapacity(); try candidate.appendSlice(gpa, rule.prefix); try candidate.appendSlice(gpa, short); try candidate.appendSlice(gpa, rule.suffix); if (!name.isValid(candidate.items, .{ .allow_onelevel = true })) continue; if (try s.exists(io, gpa, candidate.items, diag)) break true; } else false; if (!ambiguous) return short; } return full; } /// An object peeled: the first object that is not a tag, reached through /// annotated tags, and its kind. pub const Peeled = struct { id: Id, kind: object.Kind, }; /// Why an object could not be peeled: it could not be read, or a tag on /// the way is malformed (`MalformedTag`). pub const PeelError = ObjectDatabase.ReadError || error{MalformedTag}; /// The most tags peeled through before giving up; a chain this long can /// only be made on purpose. pub const max_peel_depth = 1000; /// Peels `id` through annotated tags by reading them, as git does for a /// ref whose storage does not record what it peels to. pub fn peel(io: Io, gpa: Allocator, db: ObjectDatabase, id: Id, diag: ?*Diagnostics) PeelError!Peeled { var current = id; for (0..max_peel_depth) |_| { const header = try db.readHeader(io, current, diag); if (header.kind != .tag) return .{ .id = current, .kind = header.kind }; var o = try db.read(io, gpa, current, diag); defer o.deinit(gpa); current = Tag.targetOf(current.object_format, o.data) orelse { if (diag) |d| d.object_id = current; return error.MalformedTag; }; } if (diag) |d| d.object_id = current; return error.MalformedTag; } test { _ = name; _ = packed_refs; _ = reftable; _ = reflog_format; _ = @import("refs/loose.zig"); _ = @import("refs/Files.zig"); _ = @import("refs/Reftable.zig"); _ = @import("refs/tests.zig"); }