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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Ref names: which are well formed, which are root refs, pseudorefs and//! per-worktree refs, how a name reaches into another worktree, and the//! two pattern dialects commands list refs with.//!//! The rules for a well-formed name are git-check-ref-format(1)'s, as//! `git check-ref-format` can be seen to apply them://!//! - no component begins with `.` or ends with `.lock`, and none is empty//! (so no leading, trailing or doubled `/`);//! - at least one `/`, unless one-level names are allowed;//! - no `..`, no `@{`, and not the single character `@`;//! - no byte below 0x20, no DEL, and none of space, `~`, `^`, `:`, `?`,//! `[`, `\`, or `*` (one `*` is allowed in a refspec pattern);//! - it does not end with `.`.//!//! Bytes from 0x80 up are allowed whatever they spell, as git allows//! them. `--normalize` drops leading slashes and collapses runs of them//! before checking; a trailing slash is still refused.//!//! Everything here works on bytes the caller has; nothing allocates//! except `normalize`, which writes into a buffer it is given.
const std = @import("std");const wildmatch = @import("../wildmatch.zig");
/// How strictly a name is checked.pub const CheckOptions = struct { /// Allow a name with no `/`, such as `HEAD` (`--allow-onelevel`). allow_onelevel: bool = false, /// Allow one `*`, as a refspec's pattern may have /// (`--refspec-pattern`). refspec_pattern: bool = false,};
/// What makes a name ill formed: the first rule it breaks, scanning from/// the start.pub const Problem = enum { /// The name, or a component of it, is empty: a leading, trailing or /// doubled `/`, or nothing at all. empty_component, /// A component begins with `.`. leading_dot, /// A component ends with `.lock`. lock_suffix, /// The name ends with `.`. trailing_dot, /// Two dots in a row. double_dot, /// `@{`. at_brace, /// The name is `@`. lone_at, /// A control character, or one of space, `~`, `^`, `:`, `?`, `[`, `\`. forbidden_character, /// A `*` where none is allowed, or a second one in a pattern. wildcard, /// No `/`, and one-level names are not allowed. one_level,};
/// The first rule `name` breaks, or null if it is a well-formed ref name.pub fn check(name: []const u8, options: CheckOptions) ?Problem { if (name.len == 0) return .empty_component; if (std.mem.eql(u8, name, "@")) return .lone_at; var stars: usize = 0; var components: usize = 0; var it = std.mem.splitScalar(u8, name, '/'); while (it.next()) |component| { components += 1; if (component.len == 0) return .empty_component; if (component[0] == '.') return .leading_dot; var prev: u8 = 0; for (component) |c| { switch (c) { 0...0x1f, 0x7f, ' ', '~', '^', ':', '?', '[', '\\' => return .forbidden_character, '*' => { stars += 1; if (!options.refspec_pattern or stars > 1) return .wildcard; }, '.' => if (prev == '.') return .double_dot, '{' => if (prev == '@') return .at_brace, else => {}, } prev = c; } if (std.mem.endsWith(u8, component, ".lock")) return .lock_suffix; } if (name[name.len - 1] == '.') return .trailing_dot; if (components < 2 and !options.allow_onelevel) return .one_level; return null;}
/// Whether `name` is a well-formed ref name.pub fn isValid(name: []const u8, options: CheckOptions) bool { return check(name, options) == null;}
/// `name` as `--normalize` makes it before checking: leading slashes/// dropped and runs of slashes collapsed to one, written into `buffer`,/// which must be at least as long as `name`.pub fn normalize(name: []const u8, buffer: []u8) []u8 { std.debug.assert(buffer.len >= name.len); var n: usize = 0; var prev_slash = true; for (name) |c| { if (c == '/') { if (prev_slash) continue; prev_slash = true; } else prev_slash = false; buffer[n] = c; n += 1; } return buffer[0..n];}
/// Whether `name` has the shape of a root ref, a ref outside `refs/`:/// nothing but upper-case letters and underscores. `show-ref --verify`/// accepts such a name as well as one beginning `refs/`.pub fn hasRootRefSyntax(name: []const u8) bool { if (name.len == 0) return false; for (name) |c| if (!(std.ascii.isUpper(c) or c == '_')) return false; return true;}
/// The root refs gitglossary(7) lists as not following the rule that a/// root ref is `HEAD` or ends in `_HEAD`; the list is closed.pub const irregular_root_refs = [_][]const u8{ "AUTO_MERGE", "BISECT_EXPECTED_REV", "NOTES_MERGE_PARTIAL", "NOTES_MERGE_REF", "MERGE_AUTOSTASH",};
/// Whether `name` is a root ref: of root ref syntax and either `HEAD`,/// ending in `_HEAD`, or one of `irregular_root_refs`. Listing refs leaves/// these out unless asked for them.pub fn isRootRef(name: []const u8) bool { if (!hasRootRefSyntax(name)) return false; if (std.mem.eql(u8, name, "HEAD") or std.mem.endsWith(u8, name, "_HEAD")) return true; for (irregular_root_refs) |r| if (std.mem.eql(u8, name, r)) return true; return false;}
/// Whether `name` is one of the pseudorefs gitglossary(7) names,/// `FETCH_HEAD` and `MERGE_HEAD`: refs that may hold several object IDs/// with more besides, kept as files in the git directory whatever the ref/// storage, and read only for the first object ID they hold.pub fn isPseudoref(name: []const u8) bool { return std.mem.eql(u8, name, "FETCH_HEAD") or std.mem.eql(u8, name, "MERGE_HEAD");}
/// The prefixes under `refs/` whose refs each worktree has its own of.pub const per_worktree_prefixes = [_][]const u8{ "refs/bisect/", "refs/worktree/", "refs/rewritten/" };
/// Whether each worktree has its own ref of this name: everything outside/// `refs/` (`HEAD` and the other root refs), and what is under the/// prefixes in `per_worktree_prefixes`. Every other ref is shared by all/// worktrees. The name must not have a worktree prefix (see `Scoped`).pub fn isPerWorktree(name: []const u8) bool { if (!std.mem.startsWith(u8, name, "refs/")) return true; for (per_worktree_prefixes) |p| if (std.mem.startsWith(u8, name, p)) return true; return false;}
/// A ref name with any worktree prefix taken off: which worktree's refs/// it names, and the name within them.pub const Scoped = struct { /// Whose refs. worktree: Worktree, /// The name within that worktree's refs. name: []const u8,
/// Whose refs a name reaches. pub const Worktree = union(enum) { /// The worktree the repository was opened in. current, /// `main-worktree/`: the main worktree's. main, /// `worktrees/<name>/`: the linked worktree of that name, as it is /// named under the common directory's `worktrees/`. linked: []const u8, };
/// Splits `main-worktree/<name>` and `worktrees/<worktree>/<name>` /// into the worktree and the name; any other name is the current /// worktree's. pub fn parse(full: []const u8) Scoped { if (std.mem.cutPrefix(u8, full, "main-worktree/")) |rest| { if (rest.len != 0) return .{ .worktree = .main, .name = rest }; } if (std.mem.cutPrefix(u8, full, "worktrees/")) |rest| { if (std.mem.findScalar(u8, rest, '/')) |slash| { if (slash != 0 and slash + 1 < rest.len) return .{ .worktree = .{ .linked = rest[0..slash] }, .name = rest[slash + 1 ..] }; } } return .{ .worktree = .current, .name = full }; }};
/// Whether a name read from `packed-refs` is safe to treat as a ref at/// all; git stops ("packed refname is dangerous") at one that is not. A/// name under `refs/` must be a clean path — no empty, `.` or `..`/// component and no trailing slash — though it may break the other rules,/// which only make it a broken name to pass over; any other name must be/// well formed as a one-level name may be.pub fn isSafe(name: []const u8) bool { if (std.mem.cutPrefix(u8, name, "refs/")) |rest| { if (rest.len == 0) return false; var it = std.mem.splitScalar(u8, rest, '/'); while (it.next()) |c| { if (c.len == 0 or std.mem.eql(u8, c, ".") or std.mem.eql(u8, c, "..")) return false; } return true; } return isValid(name, .{ .allow_onelevel = true });}
/// Whether `for-each-ref` shows `name` for `pattern`: a pattern with a/// wildcard is a glob matched with wildmatch's pathname rule, so `*` and/// `?` stop at `/` while `**` crosses it; one without is a literal that/// matches the whole name, or its start up to a `/`.////// (`?` does not match `/` here: `refs/heads/a?b/c` matches nothing of/// `refs/heads/a/b/c` for `git for-each-ref`.)pub fn matchesForEachRefPattern(pattern: []const u8, name: []const u8) bool { if (wildmatch.hasWildcards(pattern)) return wildmatch.match(pattern, name, .{ .pathname = true }); if (!std.mem.startsWith(u8, name, pattern)) return false; return name.len == pattern.len or name[pattern.len] == '/' or (pattern.len != 0 and pattern[pattern.len - 1] == '/');}
/// Whether `show-ref` shows `name` for `pattern`: the pattern matches/// whole components at the end of the name, so `main` matches/// `refs/heads/main` and `refs/remotes/origin/main` but not/// `refs/heads/mymain`. There are no wildcards.pub fn matchesShowRefPattern(pattern: []const u8, name: []const u8) bool { if (!std.mem.endsWith(u8, name, pattern)) return false; if (pattern.len == name.len) return true; return name[name.len - pattern.len - 1] == '/';}
/// The ways a short name is tried as a full one, in the order/// gitrevisions(7) gives: as it is, then under `refs/`, `refs/tags/`,/// `refs/heads/`, `refs/remotes/`, and as a remote's `HEAD`.pub const lookup_rules = [_]struct { prefix: []const u8, suffix: []const u8 }{ .{ .prefix = "", .suffix = "" }, .{ .prefix = "refs/", .suffix = "" }, .{ .prefix = "refs/tags/", .suffix = "" }, .{ .prefix = "refs/heads/", .suffix = "" }, .{ .prefix = "refs/remotes/", .suffix = "" }, .{ .prefix = "refs/remotes/", .suffix = "/HEAD" },};
/// The short name `full` has under lookup rule `rule`, or null if the/// rule cannot produce it.pub fn shortUnderRule(full: []const u8, rule: usize) ?[]const u8 { const r = lookup_rules[rule]; if (!std.mem.startsWith(u8, full, r.prefix) or !std.mem.endsWith(u8, full, r.suffix)) return null; if (full.len <= r.prefix.len + r.suffix.len) return null; return full[r.prefix.len .. full.len - r.suffix.len];}
/// Strips `n` components from the front of `name` as/// `%(refname:lstrip=n)` does: for a negative `n`, as many as leave `-n`./// Too few components give an empty name for a positive `n` and the whole/// name for a negative one.pub fn lstrip(name: []const u8, n: i64) []const u8 { const count = std.mem.countScalar(u8, name, '/') + 1; var strip: u64 = undefined; if (n >= 0) { strip = @intCast(n); if (strip >= count) return if (strip == 0) name else ""; } else { const keep: u64 = @abs(n); if (keep >= count) return name; strip = count - keep; } var rest = name; for (0..strip) |_| rest = rest[(std.mem.findScalar(u8, rest, '/') orelse return "") + 1 ..]; return rest;}
/// Strips `n` components from the back of `name` as/// `%(refname:rstrip=n)` does, with `lstrip`'s rules for negative and/// excessive counts.pub fn rstrip(name: []const u8, n: i64) []const u8 { const count = std.mem.countScalar(u8, name, '/') + 1; var strip: u64 = undefined; if (n >= 0) { strip = @intCast(n); if (strip >= count) return if (strip == 0) name else ""; } else { const keep: u64 = @abs(n); if (keep >= count) return name; strip = count - keep; } var rest = name; for (0..strip) |_| rest = rest[0 .. std.mem.findScalarLast(u8, rest, '/') orelse return ""]; return rest;}
// -- tests -----------------------------------------------------------------------
const testing = std.testing;
const fuzz_input = @import("../testing/fuzz.zig");
test "names are checked by git-check-ref-format's rules" { const one: CheckOptions = .{ .allow_onelevel = true }; const pat: CheckOptions = .{ .refspec_pattern = true }; try testing.expectEqual(null, check("refs/heads/main", .{})); try testing.expectEqual(.one_level, check("main", .{})); try testing.expectEqual(null, check("HEAD", one)); try testing.expectEqual(.lone_at, check("@", one)); try testing.expectEqual(null, check("refs/@", .{})); try testing.expectEqual(.empty_component, check("foo//bar", .{})); try testing.expectEqual(.empty_component, check("foo/", .{})); try testing.expectEqual(.leading_dot, check("foo/.bar", .{})); try testing.expectEqual(.lock_suffix, check("foo/bar.lock", .{})); try testing.expectEqual(.trailing_dot, check("foo/bar.", .{})); try testing.expectEqual(.double_dot, check("foo/a..b", .{})); try testing.expectEqual(.at_brace, check("foo/a@{b", .{})); try testing.expectEqual(null, check("foo/a{b", .{})); try testing.expectEqual(.forbidden_character, check("foo/a b", .{})); try testing.expectEqual(.forbidden_character, check("foo/a\x7fb", .{})); try testing.expectEqual(.wildcard, check("foo/*", .{})); try testing.expectEqual(null, check("foo/*", pat)); try testing.expectEqual(.wildcard, check("foo/*/*", pat)); try testing.expectEqual(null, check("foo/\xc3\xa9", .{})); try testing.expectEqual(null, check("foo/a]b", .{}));}
test "normalizing drops leading slashes and collapses runs" { var buffer: [32]u8 = undefined; try testing.expectEqualStrings("foo/bar", normalize("//foo///bar", &buffer)); try testing.expectEqualStrings("foo/", normalize("foo/", &buffer));}
test "root refs, pseudorefs and per-worktree refs are told apart" { try testing.expect(isRootRef("HEAD")); try testing.expect(isRootRef("ORIG_HEAD")); try testing.expect(isRootRef("MERGE_AUTOSTASH")); try testing.expect(!isRootRef("FOO")); try testing.expect(hasRootRefSyntax("FOO")); try testing.expect(!hasRootRefSyntax("FOO1")); try testing.expect(isPseudoref("FETCH_HEAD")); try testing.expect(isPerWorktree("HEAD")); try testing.expect(isPerWorktree("refs/bisect/good")); try testing.expect(!isPerWorktree("refs/heads/main")); const s = Scoped.parse("worktrees/wt/refs/bisect/bad"); try testing.expectEqualStrings("wt", s.worktree.linked); try testing.expectEqualStrings("refs/bisect/bad", s.name); try testing.expectEqual(.main, Scoped.parse("main-worktree/HEAD").worktree); try testing.expectEqual(.current, Scoped.parse("worktrees/x").worktree);}
test "packed names are safe or dangerous as git finds them" { try testing.expect(isSafe("refs/heads/a..b")); try testing.expect(!isSafe("refs/heads/../x")); try testing.expect(!isSafe("refs/heads/x/")); try testing.expect(!isSafe("refs/")); try testing.expect(isSafe("foo")); try testing.expect(isSafe("x/y")); try testing.expect(!isSafe(" refs/heads/main")); try testing.expect(!isSafe("x.lock"));}
test "for-each-ref and show-ref patterns match as git's do" { try testing.expect(matchesForEachRefPattern("refs/heads", "refs/heads/main")); try testing.expect(matchesForEachRefPattern("refs/heads/", "refs/heads/main")); try testing.expect(!matchesForEachRefPattern("refs/hea", "refs/heads/main")); try testing.expect(matchesForEachRefPattern("refs/heads/*", "refs/heads/main")); try testing.expect(!matchesForEachRefPattern("refs/heads/*", "refs/heads/a/b")); try testing.expect(!matchesForEachRefPattern("refs/heads/a?b", "refs/heads/a/b")); try testing.expect(matchesForEachRefPattern("refs/**/c", "refs/heads/a/b/c")); try testing.expect(matchesShowRefPattern("main", "refs/heads/main")); try testing.expect(!matchesShowRefPattern("main", "refs/heads/mymain")); try testing.expect(matchesShowRefPattern("heads/main", "refs/heads/main"));}
test "components are stripped as %(refname:lstrip) and rstrip do" { try testing.expectEqualStrings("foo", lstrip("refs/tags/foo", 2)); try testing.expectEqualStrings("tags/foo", lstrip("refs/tags/foo", -2)); try testing.expectEqualStrings("", lstrip("refs/tags/foo", 5)); try testing.expectEqualStrings("refs/tags/foo", lstrip("refs/tags/foo", -5)); try testing.expectEqualStrings("refs", rstrip("refs/tags/foo", 2)); try testing.expectEqualStrings("refs", rstrip("refs/tags/foo", -1)); try testing.expectEqualStrings("refs/tags/foo", rstrip("refs/tags/foo", 0));}
fn fuzzCheck(_: void, smith: *testing.Smith) anyerror!void { var buffer: [256]u8 = undefined; const name = @import("../testing/fuzz.zig").bytes(smith, &buffer); var normalized: [256]u8 = undefined; for ([_]bool{ false, true }) |onelevel| for ([_]bool{ false, true }) |pattern| { const options: CheckOptions = .{ .allow_onelevel = onelevel, .refspec_pattern = pattern }; if (check(name, options) == null) { // A valid name stays valid when normalized, and is its own // normal form. try testing.expectEqualStrings(name, normalize(name, normalized[0..name.len])); } }; _ = isSafe(name); _ = Scoped.parse(name); _ = lstrip(name, 2); _ = rstrip(name, -1);}
test "fuzz: ref names" { try fuzz_input.fuzz({}, fuzzCheck, .{ .corpus = &.{ @embedFile("../fuzz/refname"), "refs/heads/main", "refs/heads/a..b", "//foo/bar*", "@", "refs/heads/x.lock", } });}