Something went wrong. Try again.
Reading and writing git repositories in Zig, without running git.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! What a repository's own configuration says about how to read it: its//! format version, its `extensions.*`, and the `core.bare` and//! `core.worktree` that decide whether it has a working tree.//!//! gitrepository-layout(5) defines the versions: a version 1 repository//! with an extension the reader does not implement must not be read, and//! git-config(1) lists the extensions git knows. What git does at the//! edges was found by writing configurations by hand and running//! `git rev-parse` on them://!//! - The version is `core.repositoryformatversion`, the last one in the//! file, read as an integer with its units; no version is 0. Above 1,//! git stops: "Expected git repo version <= 1, found N".//! - Under version 1 every `extensions.*` key must be known, or git stops//! naming each unknown one, once for each time it appears, in file//! order. Under version 0 unknown keys are ignored, but the known keys//! git calls v1-only stop it: "repo version is 0, but v1-only extension//! found". `noop`, `preciousObjects`, `partialClone` and//! `worktreeConfig` are honored under either version. Under a negative//! version git honors no extension at all and complains of none.//! - Values are checked as each line is read, so the first bad one in the//! file is the one git names, before any version or unknown-key check://! an object format or ref storage git does not know, and a missing//! value, are "bad config line N"; a bad boolean names only the key; a//! bad version names the file.//! - Only the repository's own `config` is read for this, and//! `config.worktree` (when `extensions.worktreeConfig` is on) only for//! `core.bare` and `core.worktree`: includes are not followed, and an//! `extensions.*` key in `config.worktree` means nothing.//!//! The extensions git 2.56 knows, and what this library makes of each://!//! | Extension | Versions | Here |//! | --- | --- | --- |//! | `objectFormat` | 1 | supported: `sha1` and `sha256` |//! | `compatObjectFormat` | 1 | refused: `CompatObjectFormatUnsupported` |//! | `refStorage` | 1 | `files` and `reftable` supported; a payload refused |//! | `worktreeConfig` | 0 and 1 | supported: `config.worktree` is read |//! | `preciousObjects` | 0 and 1 | supported: only writing cares |//! | `partialClone` | 0 and 1 | tolerated: objects the promisor remote kept are missing, and missing objects are not fetched |//! | `relativeWorktrees` | 1 | supported: worktrees' paths are read whichever way they are written |//! | `submodulePathConfig` | 1 | supported: only finding a submodule's git directory from its superproject cares, which is not done here |//! | `noop`, `noop-v1` | 0 and 1; 1 | supported: they change nothing |
const std = @import("std");const Allocator = std.mem.Allocator;
const object = @import("object.zig");const Diagnostics = @import("Diagnostics.zig");const syntax = @import("config/syntax.zig");const value = @import("config/value.zig");
const RepositoryFormat = @This();
/// `core.repositoryformatversion`.version: i32 = 0,/// `extensions.objectFormat`, or SHA-1.object_format: object.Format = .sha1,/// `extensions.compatObjectFormat`, which this library refuses.compat_object_format: ?object.Format = null,/// `extensions.refStorage`.ref_storage: RefStorage = .files,/// Whether `extensions.refStorage` gave a payload after `://`: refs kept/// somewhere other than the common directory, which this library/// refuses.ref_storage_payload: bool = false,/// `extensions.worktreeConfig`: whether `config.worktree` is read.worktree_config: bool = false,/// `extensions.preciousObjects`.precious_objects: bool = false,/// `extensions.partialClone`: the promisor remote's name.partial_clone: ?[]const u8 = null,/// `extensions.relativeWorktrees`.relative_worktrees: bool = false,/// `extensions.submodulePathConfig`.submodule_path_config: bool = false,/// `core.bare` as it applies to this worktree, if set.bare: ?bool = null,/// `core.bare` as the last file read sets it, whether or not it applies/// to this worktree's working tree: what `rev-parse/// --is-bare-repository` says of a worktree's git directory with no/// working tree.bare_anywhere: ?bool = null,/// `core.worktree` as it applies to this worktree, if set: relative to/// the git directory, or absolute.worktree: ?[]const u8 = null,
/// How a repository keeps its refs.pub const RefStorage = enum { files, reftable,
/// The name `--show-ref-format` prints. pub fn name(r: RefStorage) []const u8 { return @tagName(r); }};
/// Which file is being read.pub const Role = enum { /// The common directory's `config`, for the main worktree: everything /// in it counts. main, /// The common directory's `config` read for a linked worktree: its /// `core.bare` and `core.worktree` count only with /// `extensions.worktreeConfig`. common, /// `config.worktree`: only `core.bare` and `core.worktree` count. worktree,};
/// Why a repository's format cannot be read, each in git's words./// `Diagnostics` gives the file as git names it, the line, and a detail.pub const Error = error{ /// "bad config line N in file P". BadConfigLine, /// "error: missing value for '<key>'" then "bad config line"; the /// detail is the key. MissingValue, /// "error: invalid value for '<key>': '<value>'" then "bad config /// line"; the detail is `<key>=<value>`. InvalidExtensionValue, /// "bad boolean config value '<value>' for '<key>'"; the detail is /// `<key>=<value>`. BadBoolean, /// "bad numeric config value '<value>' for /// 'core.repositoryformatversion' in file P: invalid unit"; the /// detail is the value. VersionInvalidUnit, /// The same, "out of range". VersionOutOfRange, /// "Expected git repo version <= 1, found N"; the detail is N. UnsupportedVersion, /// "unknown repository extension(s) found:", then each name on a line /// of its own after a tab; the detail is the names, one to a line. UnknownExtensions, /// "repo version is 0, but v1-only extension(s) found:", the same way. V1OnlyExtensions, /// Ours: `extensions.compatObjectFormat`, which git accepts and this /// library does not implement. CompatObjectFormatUnsupported, /// Ours: `extensions.refStorage` with a payload, refs kept elsewhere; /// the detail is the value. RefStoragePayloadUnsupported,} || Allocator.Error;
/// Reads a repository's format from its configuration files, one at a/// time in the order git reads them: the common directory's `config`,/// then `config.worktree` if `worktree_config` is set.pub const Reader = struct { arena: std.heap.ArenaAllocator, format: RepositoryFormat = .{}, /// Every `extensions.*` key, lowered, in file order, with whether it /// is known and honored under version 0. extensions: std.ArrayList(Extension) = .empty, /// `core.bare` and `core.worktree` from the common `config` of a /// linked worktree, which count only with `extensions.worktreeConfig`. common_bare: ?bool = null, common_worktree: ?[]const u8 = null,
const Extension = struct { name: []const u8, known: ?Known }; const Known = enum { any_version, v1_only };
/// A reader with nothing read. pub fn init(gpa: Allocator) Reader { return .{ .arena = .init(gpa) }; }
/// Frees everything, the format's strings included. pub fn deinit(r: *Reader) void { r.arena.deinit(); r.* = undefined; }
fn fail(diag: ?*Diagnostics, path: []const u8, line: ?u32, comptime fmt: []const u8, args: anytype) void { const d = diag orelse return; d.reset(); d.setPath(&.{path}); d.line = line; d.setDetail(fmt, args); }
/// Reads one file, `contents`, named `path` in messages. pub fn addFile(r: *Reader, contents: []const u8, path: []const u8, role: Role, diag: ?*Diagnostics) Error!void { const a = r.arena.allocator(); var p: syntax.Parser = .init(contents); defer p.deinit(a); while (p.next(a) catch |err| switch (err) { error.OutOfMemory => |e| return e, error.BadConfigLine => { fail(diag, path, p.line, "", .{}); return error.BadConfigLine; }, }) |e| try r.entry(e, path, role, diag); }
fn entry(r: *Reader, e: syntax.Entry, path: []const u8, role: Role, diag: ?*Diagnostics) Error!void { const a = r.arena.allocator(); const f = &r.format; if (std.mem.eql(u8, e.key, "core.bare")) { const b = value.parseBool(e.value) orelse { fail(diag, path, e.line, "core.bare={s}", .{e.value.?}); return error.BadBoolean; }; if (role == .common) r.common_bare = b else f.bare = b; f.bare_anywhere = b; return; } if (std.mem.eql(u8, e.key, "core.worktree")) { const v = e.value orelse { fail(diag, path, e.line, "core.worktree", .{}); return error.MissingValue; }; const kept = try a.dupe(u8, v); if (role == .common) r.common_worktree = kept else f.worktree = kept; return; } if (role == .worktree) return; if (std.mem.eql(u8, e.key, "core.repositoryformatversion")) { f.version = value.parseInt(i32, e.value orelse "") catch |err| { fail(diag, path, e.line, "{s}", .{e.value orelse ""}); return switch (err) { error.InvalidUnit => error.VersionInvalidUnit, error.OutOfRange => error.VersionOutOfRange, }; }; return; } const prefix = "extensions."; if (!std.mem.startsWith(u8, e.key, prefix)) return; const name = e.key[prefix.len..]; const known = try r.extension(name, e, path, diag); try r.extensions.append(a, .{ .name = try a.dupe(u8, name), .known = known }); }
/// Checks and keeps one extension's value; says whether git knows it. fn extension(r: *Reader, name: []const u8, e: syntax.Entry, path: []const u8, diag: ?*Diagnostics) Error!?Known { const a = r.arena.allocator(); const f = &r.format; const Name = enum { noop, @"noop-v1", objectformat, compatobjectformat, refstorage, worktreeconfig, preciousobjects, partialclone, relativeworktrees, submodulepathconfig, }; const n = std.meta.stringToEnum(Name, name) orelse return null; switch (n) { .noop => return .any_version, .@"noop-v1" => return .v1_only, .objectformat, .compatobjectformat => { const v = try requireValue(e, path, diag); const format = object.Format.fromName(v) orelse { fail(diag, path, e.line, "{s}={s}", .{ e.key, v }); return error.InvalidExtensionValue; }; if (n == .objectformat) f.object_format = format else f.compat_object_format = format; return .v1_only; }, .refstorage => { const v = try requireValue(e, path, diag); const sep = std.mem.find(u8, v, "://"); const format_name = if (sep) |s| v[0..s] else v; f.ref_storage = std.meta.stringToEnum(RefStorage, format_name) orelse { fail(diag, path, e.line, "{s}={s}", .{ e.key, v }); return error.InvalidExtensionValue; }; f.ref_storage_payload = if (sep) |s| v.len > s + 3 else false; return .v1_only; }, .partialclone => { f.partial_clone = try a.dupe(u8, try requireValue(e, path, diag)); return .any_version; }, .worktreeconfig, .preciousobjects, .relativeworktrees, .submodulepathconfig => { const b = value.parseBool(e.value) orelse { fail(diag, path, e.line, "{s}={s}", .{ e.key, e.value.? }); return error.BadBoolean; }; switch (n) { .worktreeconfig => { f.worktree_config = b; return .any_version; }, .preciousobjects => { f.precious_objects = b; return .any_version; }, .relativeworktrees => f.relative_worktrees = b, else => f.submodule_path_config = b, } return .v1_only; }, } }
fn requireValue(e: syntax.Entry, path: []const u8, diag: ?*Diagnostics) Error![]const u8 { return e.value orelse { fail(diag, path, e.line, "{s}", .{e.key}); return error.MissingValue; }; }
/// Whether `config.worktree` is to be read: `extensions.worktreeConfig` /// under any version that is not negative. pub fn worktreeConfig(r: *const Reader) bool { return r.format.version >= 0 and r.format.worktree_config; }
/// Checks the version and the extensions against what git accepts and /// this library implements, and returns the format, whose strings live /// as long as the reader. `path` names the common `config` in /// messages. pub fn finish(r: *Reader, path: []const u8, diag: ?*Diagnostics) Error!RepositoryFormat { var f = r.format; if (f.version > 1) { fail(diag, path, null, "{d}", .{f.version}); return error.UnsupportedVersion; } if (f.version < 0) { // Nothing is honored: every extension is as if unset. f = .{ .version = f.version, .bare = f.bare, .bare_anywhere = f.bare_anywhere, .worktree = f.worktree }; } else { const want: Known = if (f.version == 1) .any_version else .v1_only; var names: std.ArrayList(u8) = .empty; const a = r.arena.allocator(); var count: usize = 0; for (r.extensions.items) |x| { const bad = if (f.version == 1) x.known == null else x.known == want; if (!bad) continue; if (count != 0) try names.append(a, '\n'); try names.appendSlice(a, x.name); count += 1; } if (count != 0) { fail(diag, path, null, "{s}", .{names.items}); return if (f.version == 1) error.UnknownExtensions else error.V1OnlyExtensions; } if (f.version == 0) { // Honored under version 0 only as they would be under 1. f.object_format = .sha1; f.compat_object_format = null; f.ref_storage = .files; f.ref_storage_payload = false; } } if (f.compat_object_format != null) { fail(diag, path, null, "", .{}); return error.CompatObjectFormatUnsupported; } if (f.ref_storage_payload) { fail(diag, path, null, "", .{}); return error.RefStoragePayloadUnsupported; } if (f.version >= 0 and f.worktree_config) { if (f.bare == null) f.bare = r.common_bare; if (f.worktree == null) f.worktree = r.common_worktree; } return f; }};
// -- tests ---------------------------------------------------------------------
const testing = std.testing;
fn expectFormat(contents: []const u8) !RepositoryFormat { var r: Reader = .init(testing.allocator); defer r.deinit(); try r.addFile(contents, "config", .main, null); var f = try r.finish("config", null); f.partial_clone = null; f.worktree = null; return f;}
fn expectError(expected: anyerror, contents: []const u8, detail: []const u8) !void { var r: Reader = .init(testing.allocator); defer r.deinit(); var d: Diagnostics = .{}; const result = blk: { r.addFile(contents, "config", .main, &d) catch |err| break :blk err; _ = r.finish("config", &d) catch |err| break :blk err; return error.TestExpectedError; }; try testing.expectEqual(expected, result); try testing.expectEqualStrings(detail, d.detail());}
test "the version and the extensions git accepts" { try testing.expectEqual(object.Format.sha1, (try expectFormat("")).object_format); const f = try expectFormat("[core]\n\trepositoryformatversion = 1\n[extensions]\n\tobjectFormat = sha256\n\trefStorage = reftable\n"); try testing.expectEqual(object.Format.sha256, f.object_format); try testing.expectEqual(RefStorage.reftable, f.ref_storage); // Version 0 ignores what it does not know and honors what it may. const v0 = try expectFormat("[extensions]\n\tfoo = bar\n\tworktreeConfig\n\tnoop = x\n"); try testing.expect(v0.worktree_config); // A negative version honors nothing. const neg = try expectFormat("[core]\n\trepositoryformatversion = -1\n[extensions]\n\tobjectformat = sha256\n\tfoo\n"); try testing.expectEqual(object.Format.sha1, neg.object_format);}
test "what git refuses, as it refuses it" { try expectError(error.UnknownExtensions, "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tfoo = 1\n\tFOO\n[extensions \"Sub\"]\n\tx\n", "foo\nfoo\nSub.x"); try expectError(error.V1OnlyExtensions, "[extensions]\n\tobjectformat = sha1\n\tnoop-v1\n", "objectformat\nnoop-v1"); try expectError(error.UnsupportedVersion, "[core]\n\trepositoryformatversion = 2\n", "2"); try expectError(error.UnsupportedVersion, "[core]\n\trepositoryformatversion = 1\n\trepositoryformatversion = 1k\n", "1024"); try expectError(error.VersionInvalidUnit, "[core]\n\trepositoryformatversion = junk\n", "junk"); try expectError(error.InvalidExtensionValue, "[extensions]\n\tobjectformat = SHA1\n\tfoo\n", "extensions.objectformat=SHA1"); try expectError(error.MissingValue, "[extensions]\n\tpartialclone\n", "extensions.partialclone"); try expectError(error.BadBoolean, "[extensions]\n\tpreciousobjects = maybe\n\tobjectformat = md5\n", "extensions.preciousobjects=maybe"); try expectError(error.CompatObjectFormatUnsupported, "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tcompatObjectFormat = sha256\n", ""); try expectError(error.RefStoragePayloadUnsupported, "[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = files:///elsewhere\n", "");}
fn fuzzOne(_: void, smith: *testing.Smith) anyerror!void { var buffer: [4096]u8 = undefined; const input = @import("testing/fuzz.zig").bytes(smith, &buffer); var r: Reader = .init(testing.allocator); defer r.deinit(); var d: Diagnostics = .{}; r.addFile(input, "config", .main, &d) catch return; r.addFile(input, "config.worktree", .worktree, &d) catch return; _ = r.finish("config", &d) catch return;}
test "fuzz: repository format" { try testing.fuzz({}, fuzzOne, .{ .corpus = &.{ @embedFile("fuzz/repository-format"), @embedFile("fuzz/repository-format-v0"), } });}