Something went wrong. Try again.
Reading and writing git repositories in Zig, without running git.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// 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/<name>/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");}