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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The contents of a loose ref: an object ID, or `ref: ` and the name of//! another ref. These are git's observed rules, from files written by//! hand and read back with `git rev-parse`, `git symbolic-ref` and//! `git for-each-ref`://!//! - The contents end at their first NUL, if they have one.//! - `ref:` begins a symbolic ref. Blank space after it is skipped and//! blank space at the end dropped; what is left is the target, line//! feeds within it and all. A target that is not a well-formed ref name//! (one-level names allowed) makes the ref dangling, not unreadable.//! - Otherwise the contents must begin with exactly as many hexadecimal//! digits as an object ID has, in either case, followed by the end or//! by blank space; anything after that is ignored, which is how//! `FETCH_HEAD`'s extra columns and a CRLF line ending are passed over.//! Anything else — too few digits, too many, a leading space, other//! bytes after the digits — is a broken ref ("ignoring broken ref").//!//! Blank space is git's: space, tab, line feed and carriage return; not//! vertical tab or form feed.
const std = @import("std");
const object = @import("../object.zig");const name_rules = @import("name.zig");
/// What a loose ref holds.pub const Value = union(enum) { /// An object ID. direct: object.Id, /// The name of another ref; a slice of the contents. symbolic: []const u8,};
/// Why loose ref contents could not be read: they are neither an object/// ID nor `ref: ` and a name.pub const Error = error{MalformedRef};
/// Reads the contents of a loose ref file.pub fn parse(format: object.Format, contents: []const u8) Error!Value { const text = contents[0 .. std.mem.findScalar(u8, contents, 0) orelse contents.len]; if (std.mem.cutPrefix(u8, text, "ref:")) |rest| { return .{ .symbolic = std.mem.trim(u8, rest, whitespace) }; } const n = format.hexLength(); if (text.len < n) return error.MalformedRef; if (text.len > n and !isSpace(text[n])) return error.MalformedRef; return .{ .direct = object.Id.parseHex(format, text[0..n]) catch return error.MalformedRef };}
/// Whether a symbolic ref's target can be followed at all: a target that/// is not a well-formed name makes git call the ref dangling.pub fn isValidTarget(target: []const u8) bool { return name_rules.isValid(target, .{ .allow_onelevel = true });}
/// git's blank space.pub const whitespace = " \t\n\r";
fn isSpace(c: u8) bool { return std.mem.findScalar(u8, whitespace, c) != null;}
// -- tests -----------------------------------------------------------------------
const testing = std.testing;
const fuzz_input = @import("../testing/fuzz.zig");
test "loose refs are read as git reads them" { const hex = "f262b5fdc70bf36bebcf70cc60cf788a294c8126"; const id = try object.Id.parseHex(.sha1, hex); for ([_][]const u8{ hex, hex ++ "\n", hex ++ "\r\n", hex ++ " garbage\n", hex ++ "\tx", hex ++ "\x00x", "F262B5FDC70BF36BEBCF70CC60CF788A294C8126\n" }) |c| { try testing.expect((try parse(.sha1, c)).direct.eql(id)); } for ([_][]const u8{ "", hex[0..39], hex ++ "0", hex ++ "x", " " ++ hex, hex ++ "\x0bjunk", "REF: refs/heads/main" }) |c| { try testing.expectError(error.MalformedRef, parse(.sha1, c)); } try testing.expectEqualStrings("refs/heads/main", (try parse(.sha1, "ref: refs/heads/main\n")).symbolic); try testing.expectEqualStrings("refs/heads/main", (try parse(.sha1, "ref:refs/heads/main")).symbolic); try testing.expectEqualStrings("refs/heads/main", (try parse(.sha1, "ref:\t refs/heads/main \r\n")).symbolic); try testing.expectEqualStrings("refs/heads/main\nextra", (try parse(.sha1, "ref: refs/heads/main\nextra\n")).symbolic); try testing.expect(!isValidTarget((try parse(.sha1, "ref: refs/heads/main\nextra\n")).symbolic)); try testing.expect(!isValidTarget((try parse(.sha1, "ref: refs/heads/main\x0b")).symbolic)); try testing.expect(isValidTarget("HEAD")); try testing.expect(!isValidTarget(""));}
test "fuzz: loose refs" { try fuzz_input.fuzz({}, struct { fn one(_: void, smith: *testing.Smith) anyerror!void { var buffer: [256]u8 = undefined; const input = @import("../testing/fuzz.zig").bytes(smith, &buffer); for ([_]object.Format{ .sha1, .sha256 }) |f| { const v = parse(f, input) catch continue; switch (v) { .direct => |id| try testing.expectEqual(f, id.object_format), .symbolic => |t| _ = isValidTarget(t), } } } }.one, .{ .corpus = &.{ @embedFile("../fuzz/head-symref"), @embedFile("../fuzz/loose-ref"), @embedFile("../fuzz/loose-ref-sha256"), } });}