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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The files backend: refs kept as loose refs under `refs/` and the git//! directory, and in `packed-refs`, with reflogs under `logs/`, as//! gitrepository-layout(5) lays them out.//!//! A loose ref overrides a packed one of the same name. Shared refs are//! in the common directory; per-worktree refs (`HEAD` and the other root//! refs, `refs/bisect/`, `refs/worktree/`, `refs/rewritten/`) in the git//! directory of the worktree they belong to, which for the main worktree//! is the common directory and for a linked one is under its//! `worktrees/`. Only the main worktree's per-worktree refs are looked//! for in `packed-refs`.//!//! What git can be seen to do when listing loose refs: a file or//! directory whose name begins with `.` or ends in `.lock` is passed over//! without a word; a file whose name is not a well-formed ref name is a//! broken name; a symbolic link whose target begins `refs/` is a symbolic//! ref to that target, and any other is followed and read as a file. A//! directory where a ref is looked for is no ref at all.//!//! `packed-refs` is read the first time it is wanted and kept; so is its//! absence. Nothing is cached of loose refs, which are read when asked.
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 packed_refs = @import("packed.zig");const reflog_format = @import("reflog.zig");
const Files = @This();
gpa: Allocator,format: object.Format,/// What the paths below are relative to, when relative.base: Io.Dir,/// The current worktree's git directory, as git names it.git_dir: []u8,/// The common directory, as git names it.common_dir: []u8,/// Whether the git directory is a linked worktree's.linked: bool,/// `packed-refs`, once read.packed_state: PackedState = .unread,
const PackedState = union(enum) { unread, absent, read: struct { contents: []u8, refs: packed_refs.PackedRefs },};
/// Where the refs are.pub const Options = struct { format: object.Format, /// What relative paths are relative to. base: Io.Dir, /// The git directory of the worktree whose refs these are. git_dir: []const u8, /// The common directory. common_dir: []const u8, /// Whether `git_dir` is a linked worktree's rather than the common /// directory itself. linked: bool,};
/// A files backend for the refs `options` locates. Nothing is read yet.pub fn init(gpa: Allocator, options: Options) Allocator.Error!Files { 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(f: *Files) void { switch (f.packed_state) { .read => |*r| { r.refs.deinit(f.gpa); f.gpa.free(r.contents); }, else => {}, } f.gpa.free(f.git_dir); f.gpa.free(f.common_dir); f.* = undefined;}
/// The backend as a ref store.pub fn store(f: *Files) RefStore { return .{ .ptr = f, .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(*Files, @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(*Files, @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(*Files, @ptrCast(@alignCast(ptr))).reflog(io, gpa, n, diag); } }.reflog,};
/// `dir` and `name` joined, `./` dropped, as git names files in/// messages.pub fn joinPath(gpa: Allocator, dir: []const u8, file: []const u8) Allocator.Error![]u8 { var d = dir; while (std.mem.startsWith(u8, d, "./")) d = std.mem.trimStart(u8, d[2..], "/"); if (std.mem.eql(u8, d, ".") or d.len == 0) return gpa.dupe(u8, file); d = std.mem.trimEnd(u8, d, "/"); if (d.len == 0) return std.mem.concat(gpa, u8, &.{ "/", file }); return std.mem.concat(gpa, u8, &.{ d, "/", file });}
/// The git directory holding the per-worktree refs of the worktree a/// name reaches, which the caller frees.fn perWorktreeDir(f: *const Files, gpa: Allocator, worktree: name_rules.Scoped.Worktree) Allocator.Error![]u8 { return switch (worktree) { .current => gpa.dupe(u8, f.git_dir), .main => gpa.dupe(u8, f.common_dir), .linked => |w| blk: { const wt = try joinPath(gpa, f.common_dir, "worktrees"); defer gpa.free(wt); break :blk joinPath(gpa, wt, w); }, };}
/// Whether a name reaching `worktree` reads `packed-refs` for its/// per-worktree refs: only the main worktree's are ever packed.fn packsPerWorktree(f: *const Files, worktree: name_rules.Scoped.Worktree) bool { return switch (worktree) { .current => !f.linked, .main => true, .linked => false, };}
pub fn read(f: *Files, 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; // A name that is not a clean path names no file, and no packed ref // git would read. if (!name_rules.isValid(n, .{ .allow_onelevel = true })) return null; const per_worktree = name_rules.isPerWorktree(n); const dir = if (per_worktree) try f.perWorktreeDir(gpa, scoped.worktree) else try gpa.dupe(u8, f.common_dir); defer gpa.free(dir); if (try f.readLoose(io, gpa, dir, n, target, diag)) |v| return v; if (per_worktree and !f.packsPerWorktree(scoped.worktree)) return null; const p = try f.packedRefs(io, diag) orelse return null; const e = p.find(n) orelse return null; return .{ .direct = .{ .id = e.id, .peeled = e.peeled } };}
/// Reads the loose ref `n` in `dir`; null if there is none.fn readLoose(f: *Files, io: Io, gpa: Allocator, dir: []const u8, n: []const u8, target: *std.ArrayList(u8), diag: ?*Diagnostics) RefStore.ReadError!?RefStore.Value { const path = try joinPath(gpa, dir, n); defer gpa.free(path); return switch (try f.readLoosePath(io, gpa, path, target, diag)) { .missing => null, // A malformed loose ref hides a packed one of the same name. .malformed => error.MalformedRef, .value => |v| v, };}
const LooseResult = union(enum) { missing, malformed, value: RefStore.Value,};
fn readLoosePath(f: *Files, io: Io, gpa: Allocator, path: []const u8, target: *std.ArrayList(u8), diag: ?*Diagnostics) RefStore.ReadError!LooseResult { const st = f.base.statFile(io, path, .{ .follow_symlinks = false }) catch |err| switch (err) { error.Canceled => |e| return e, error.FileNotFound, error.NotDir => return .missing, else => return unreadable(diag, path, err), }; switch (st.kind) { .directory => return .missing, .sym_link => { var buffer: [std.fs.max_path_bytes]u8 = undefined; const len = f.base.readLink(io, path, &buffer) catch |err| switch (err) { error.Canceled => |e| return e, error.FileNotFound => return .missing, else => return unreadable(diag, path, err), }; if (std.mem.startsWith(u8, buffer[0..len], "refs/")) { target.clearRetainingCapacity(); try target.appendSlice(gpa, buffer[0..len]); return .{ .value = .{ .symbolic = target.items } }; } }, else => {}, } const contents = f.base.readFileAlloc(io, path, gpa, .limited(1 << 20)) catch |err| switch (err) { error.Canceled => |e| return e, error.OutOfMemory => |e| return e, error.FileNotFound, error.NotDir, error.IsDir => return .missing, else => return unreadable(diag, path, err), }; defer gpa.free(contents); const v = loose.parse(f.format, contents) catch return .malformed; return switch (v) { .direct => |id| .{ .value = .{ .direct = .{ .id = id } } }, .symbolic => |t| blk: { target.clearRetainingCapacity(); try target.appendSlice(gpa, t); break :blk .{ .value = .{ .symbolic = target.items } }; }, };}
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;}
/// `packed-refs`, read the first time it is wanted; null if there is/// none.pub fn packedRefs(f: *Files, io: Io, diag: ?*Diagnostics) RefStore.ReadError!?*const packed_refs.PackedRefs { switch (f.packed_state) { .read => |*r| return &r.refs, .absent => return null, .unread => {}, } const path = try joinPath(f.gpa, f.common_dir, "packed-refs"); defer f.gpa.free(path); const contents = f.base.readFileAlloc(io, path, f.gpa, .unlimited) catch |err| switch (err) { error.Canceled => |e| return e, error.OutOfMemory => |e| return e, error.FileNotFound => { f.packed_state = .absent; return null; }, else => return unreadable(diag, path, err), }; errdefer f.gpa.free(contents); const refs = packed_refs.parse(f.gpa, f.format, contents, diag) catch |err| { if (diag) |d| d.setPath(&.{path}); return err; }; f.packed_state = .{ .read = .{ .contents = contents, .refs = refs } }; return &f.packed_state.read.refs;}
pub fn list(f: *Files, 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 loose_entries: std.ArrayList(RefStore.Entry) = .empty;
// Shared refs from the common directory; in a linked worktree, its // own per-worktree refs from its git directory in place of the main // worktree's. try f.walk(io, a, f.common_dir, "refs", prefix, if (f.linked) .shared else .all, &loose_entries, diag); if (f.linked) { for (name_rules.per_worktree_prefixes) |p| { const top = p[0 .. p.len - 1]; if (!overlaps(prefix, p)) continue; try f.walk(io, a, f.git_dir, top, prefix, .all, &loose_entries, diag); } } std.sort.block(RefStore.Entry, loose_entries.items, {}, entryLessThan);
var entries: std.ArrayList(RefStore.Entry) = .empty; const p = try f.packedRefs(io, diag); const packed_entries = if (p) |pr| pr.entries else &.{}; var i: usize = 0; var j: usize = 0; while (true) { // Packed entries outside the prefix, and in a linked worktree the // main worktree's per-worktree refs, are not listed. A root ref // that has found its way into `packed-refs` is, as git lists it // for `show-ref` (and `for-each-ref` leaves it out). while (j < packed_entries.len) : (j += 1) { const e = packed_entries[j]; if (!std.mem.startsWith(u8, e.name, prefix)) continue; if (f.linked and name_rules.isPerWorktree(e.name)) continue; break; } const have_loose = i < loose_entries.items.len; const have_packed = j < packed_entries.len; if (!have_loose and !have_packed) break; const order: std.math.Order = if (!have_packed) .lt else if (!have_loose) .gt else std.mem.order(u8, loose_entries.items[i].name, packed_entries[j].name); switch (order) { .lt => { try entries.append(a, loose_entries.items[i]); i += 1; }, .eq => { try entries.append(a, loose_entries.items[i]); i += 1; j += 1; }, .gt => { const e = packed_entries[j]; j += 1; const value: RefStore.EntryValue = if (!name_rules.isSafe(e.name)) .{ .broken = .dangerous_name } else if (!name_rules.isValid(e.name, .{ .allow_onelevel = true })) .{ .broken = .bad_name } else .{ .direct = .{ .id = e.id, .peeled = e.peeled } }; try entries.append(a, .{ .name = try a.dupe(u8, e.name), .value = value }); }, } } return .{ .entries = try entries.toOwnedSlice(a), .arena = arena };}
fn entryLessThan(_: void, x: RefStore.Entry, y: RefStore.Entry) bool { return std.mem.lessThan(u8, x.name, y.name);}
/// Whether some name under `dir_prefix` (a directory, ending in `/`) can/// begin with `prefix`.fn overlaps(prefix: []const u8, dir_prefix: []const u8) bool { return std.mem.startsWith(u8, prefix, dir_prefix) or std.mem.startsWith(u8, dir_prefix, prefix);}
const Which = enum { all, shared };
/// Adds the loose refs under `top` in `dir` whose names begin with/// `prefix` to `out`. With `.shared`, the per-worktree directories are/// left out.fn walk(f: *Files, io: Io, a: Allocator, dir: []const u8, top: []const u8, prefix: []const u8, which: Which, out: *std.ArrayList(RefStore.Entry), diag: ?*Diagnostics) RefStore.ReadError!void { // Start as deep as the prefix allows: its whole directories. var start: []const u8 = top; if (std.mem.startsWith(u8, prefix, top) and prefix.len > top.len and prefix[top.len] == '/') { if (std.mem.findScalarLast(u8, prefix, '/')) |slash| start = prefix[0..slash]; } else if (!std.mem.startsWith(u8, top, prefix) and !std.mem.startsWith(u8, prefix, top)) return; var name_buf: std.ArrayList(u8) = .empty; try name_buf.appendSlice(a, start); try f.walkDir(io, a, dir, &name_buf, prefix, which, out, diag);}
fn walkDir(f: *Files, io: Io, a: Allocator, dir: []const u8, ref_name: *std.ArrayList(u8), prefix: []const u8, which: Which, out: *std.ArrayList(RefStore.Entry), diag: ?*Diagnostics) RefStore.ReadError!void { const path = try joinPath(a, dir, ref_name.items); var d = f.base.openDir(io, path, .{ .iterate = true }) catch |err| switch (err) { error.Canceled => |e| return e, error.FileNotFound, error.NotDir => return, else => return unreadable(diag, path, err), }; defer d.close(io); var names: std.ArrayList([]u8) = .empty; var it = d.iterate(); while (it.next(io) catch |err| switch (err) { error.Canceled => |e| return e, else => return unreadable(diag, path, err), }) |entry| { if (entry.name.len == 0 or entry.name[0] == '.' or std.mem.endsWith(u8, entry.name, ".lock")) continue; try names.append(a, try a.dupe(u8, entry.name)); } const len = ref_name.items.len; var target: std.ArrayList(u8) = .empty; for (names.items) |entry_name| { ref_name.shrinkRetainingCapacity(len); try ref_name.append(a, '/'); try ref_name.appendSlice(a, entry_name); const full = ref_name.items; if (which == .shared and name_rules.isPerWorktree(full)) continue; const entry_path = try joinPath(a, dir, full); const st = f.base.statFile(io, entry_path, .{}) catch |err| switch (err) { error.Canceled => |e| return e, error.FileNotFound, error.NotDir => { // A symbolic link to nowhere may still name a ref. try f.addLoose(io, a, entry_path, full, prefix, &target, out, diag); continue; }, else => return unreadable(diag, entry_path, err), }; if (st.kind == .directory) { // A directory is worth entering only if names in it can begin // with the prefix. if (std.mem.startsWith(u8, full, prefix) or std.mem.startsWith(u8, prefix, full)) try f.walkDir(io, a, dir, ref_name, prefix, which, out, diag); continue; } try f.addLoose(io, a, entry_path, full, prefix, &target, out, diag); } ref_name.shrinkRetainingCapacity(len);}
fn addLoose(f: *Files, io: Io, a: Allocator, path: []const u8, full: []const u8, prefix: []const u8, target: *std.ArrayList(u8), out: *std.ArrayList(RefStore.Entry), diag: ?*Diagnostics) RefStore.ReadError!void { if (!std.mem.startsWith(u8, full, prefix)) return; const owned = try a.dupe(u8, full); if (!name_rules.isValid(full, .{})) { try out.append(a, .{ .name = owned, .value = .{ .broken = .bad_name } }); return; } const value: RefStore.EntryValue = switch (try f.readLoosePath(io, a, path, target, diag)) { .missing => return, .malformed => .{ .broken = .malformed }, .value => |v| switch (v) { .direct => |d| .{ .direct = d }, .symbolic => |t| .{ .symbolic = try a.dupe(u8, t) }, }, }; try out.append(a, .{ .name = owned, .value = value });}
pub fn reflog(f: *Files, 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 = if (name_rules.isPerWorktree(n)) try f.perWorktreeDir(gpa, scoped.worktree) else try gpa.dupe(u8, f.common_dir); defer gpa.free(dir); const logs = try joinPath(gpa, dir, "logs"); defer gpa.free(logs); const path = try joinPath(gpa, logs, n); defer gpa.free(path); var arena: std.heap.ArenaAllocator = .init(gpa); errdefer arena.deinit(); const a = arena.allocator(); const contents = f.base.readFileAlloc(io, path, a, .unlimited) catch |err| switch (err) { error.Canceled => |e| return e, error.OutOfMemory => |e| return e, error.FileNotFound, error.NotDir, error.IsDir => { arena.deinit(); return null; }, else => return unreadable(diag, path, err), }; var entries: std.ArrayList(RefStore.ReflogEntry) = .empty; try reflog_format.parseFile(a, f.format, contents, &entries); return .{ .entries = try entries.toOwnedSlice(a), .arena = arena };}