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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The `packed-refs` file: many refs in one file, each on a line of its//! own as an object ID, a space and the name, with a `^` line after an//! annotated tag giving the object it peels to.//!//! # pack-refs with: peeled fully-peeled sorted//! 4a298c2be86322f047cf44cef2d85c5426e59474 refs/heads/main//! 23955d658730798c8c89f41fe773f25630d85138 refs/tags/v1//! ^4a298c2be86322f047cf44cef2d85c5426e59474//!//! These are git's observed rules, from files written by hand and read//! back with `git for-each-ref` and `git show-ref -d`://!//! - Only the first line may be the header, `# pack-refs with:` and the//! traits separated by spaces. Any other line beginning `#` is an//! "unexpected line", as is an empty line.//! - A ref line is exactly as many hexadecimal digits as an object ID//! has, one blank (a space, a tab or a carriage return), and the name,//! which is the rest of the line: a CRLF file's names end in a carriage//! return, and are then broken names. A missing name, or anything else//! that is not digits and a blank, is an unexpected line.//! - A peel line is `^` and the digits of an object ID and nothing else,//! straight after a ref line.//! - Every line, the last included, ends in a line feed; one that does//! not is an "unterminated line".//! - A peel line is believed whatever the traits say, and whatever the ref//! names; a ref without one is peeled by reading its object. The traits//! change nothing that can be seen, except `sorted`: git trusts a file//! that says it is sorted and reads it in the order it is in, and sorts//! one that does not say so.//! - Whether a name is well formed is not this file's business: it is//! kept as it is, for the ref store to judge when it comes to the name//! (`name.isSafe`).//!//! Where git reads a single ref from a file it trusts to be sorted, it//! searches the file rather than reading all of it, so it passes over a//! malformed line it does not come to; this reader refuses a malformed//! file whole.
const std = @import("std");const Allocator = std.mem.Allocator;
const object = @import("../object.zig");const Id = object.Id;const Diagnostics = @import("../Diagnostics.zig");
/// The traits a header can declare.pub const Traits = struct { peeled: bool = false, fully_peeled: bool = false, sorted: bool = false,};
/// One ref from the file.pub const Entry = struct { /// The name, as written; a slice of the file's contents. name: []const u8, id: Id, /// The object the `^` line after it says it peels to. peeled: ?Id,};
/// A `packed-refs` file, read. The entries are slices of the contents,/// which must outlive it.pub const PackedRefs = struct { traits: Traits, /// In order of name, unless the file said it was sorted when it was /// not, in which case in the file's order, as git reads it. entries: []Entry,
/// Frees the entries. pub fn deinit(p: *PackedRefs, gpa: Allocator) void { gpa.free(p.entries); p.* = undefined; }
/// The entry named `name`, found as git finds it: by binary search in /// a file that says it is sorted, and by looking at every entry in one /// that does not (which has been sorted, so the search is the same). pub fn find(p: *const PackedRefs, name: []const u8) ?*const Entry { var lo: usize = 0; var hi: usize = p.entries.len; while (lo < hi) { const mid = lo + (hi - lo) / 2; switch (std.mem.order(u8, p.entries[mid].name, name)) { .lt => lo = mid + 1, .gt => hi = mid, .eq => return &p.entries[mid], } } return null; }};
/// Why a `packed-refs` file could not be read:////// - `UnexpectedPackedRefsLine`: "unexpected line in packed-refs"; the/// diagnostic's detail is the line and its `line` the line number./// - `UnterminatedPackedRefsLine`: "unterminated line in packed-refs":/// the last line has no line feed; the detail is the line.pub const Error = Allocator.Error || error{ UnexpectedPackedRefsLine, UnterminatedPackedRefsLine };
const header_prefix = "# pack-refs with:";
/// Reads a `packed-refs` file's contents.pub fn parse(gpa: Allocator, format: object.Format, contents: []const u8, diag: ?*Diagnostics) Error!PackedRefs { var entries: std.ArrayList(Entry) = .empty; errdefer entries.deinit(gpa); var traits: Traits = .{}; var rest = contents; var line_number: u32 = 0; const hex_len = format.hexLength(); // A peel line belongs to the ref line just before it, and to no other. var last_was_ref = false; while (rest.len != 0) { line_number += 1; const nl = std.mem.findScalar(u8, rest, '\n') orelse { if (diag) |d| { d.line = line_number; d.setDetail("{s}", .{rest}); } return error.UnterminatedPackedRefsLine; }; const line = rest[0..nl]; rest = rest[nl + 1 ..]; if (line_number == 1) { if (std.mem.cutPrefix(u8, line, header_prefix)) |words| { last_was_ref = false; var it = std.mem.tokenizeScalar(u8, words, ' '); while (it.next()) |w| { if (std.mem.eql(u8, w, "peeled")) traits.peeled = true; if (std.mem.eql(u8, w, "fully-peeled")) traits.fully_peeled = true; if (std.mem.eql(u8, w, "sorted")) traits.sorted = true; } continue; } } if (line.len != 0 and line[0] == '^') { const id = if (line.len == 1 + hex_len) Id.parseHex(format, line[1..]) catch null else null; if (id == null or !last_was_ref) return unexpected(diag, line, line_number); entries.items[entries.items.len - 1].peeled = id; last_was_ref = false; continue; } if (line.len < hex_len + 2) return unexpected(diag, line, line_number); const id = Id.parseHex(format, line[0..hex_len]) catch return unexpected(diag, line, line_number); switch (line[hex_len]) { ' ', '\t', '\r' => {}, else => return unexpected(diag, line, line_number), } try entries.append(gpa, .{ .name = line[hex_len + 1 ..], .id = id, .peeled = null }); last_was_ref = true; } if (!traits.sorted) { std.sort.block(Entry, entries.items, {}, struct { fn lessThan(_: void, a: Entry, b: Entry) bool { return std.mem.lessThan(u8, a.name, b.name); } }.lessThan); } return .{ .traits = traits, .entries = try entries.toOwnedSlice(gpa) };}
fn unexpected(diag: ?*Diagnostics, line: []const u8, line_number: u32) error{UnexpectedPackedRefsLine} { if (diag) |d| { d.line = line_number; d.setDetail("{s}", .{line}); } return error.UnexpectedPackedRefsLine;}
// -- tests -----------------------------------------------------------------------
const testing = std.testing;
const fuzz_input = @import("../testing/fuzz.zig");
test "a packed-refs file is read as git reads it" { const gpa = testing.allocator; var p = try parse(gpa, .sha1, @embedFile("../fuzz/packed-refs"), null); defer p.deinit(gpa); try testing.expect(p.traits.sorted and p.traits.peeled and p.traits.fully_peeled); try testing.expectEqual(4, p.entries.len); try testing.expectEqualStrings("refs/tags/v1", p.entries[3].name); try testing.expect(p.entries[3].peeled != null); try testing.expect(p.entries[0].peeled == null); try testing.expect(p.find("refs/heads/side") != null); try testing.expect(p.find("refs/heads/nope") == null);}
test "malformed packed-refs files are refused as git refuses them" { const gpa = testing.allocator; const h = "f262b5fdc70bf36bebcf70cc60cf788a294c8126"; const header = "# pack-refs with: peeled fully-peeled sorted \n"; for ([_][]const u8{ header ++ h ++ " refs/heads/main\ngarbage\n", header ++ "# hi\n" ++ h ++ " refs/heads/main\n", header ++ "^" ++ h ++ "\n" ++ h ++ " refs/heads/main\n", header ++ h ++ " refs/heads/main\n^zz\n", header ++ h ++ " refs/heads/main\n^" ++ h ++ "\n^" ++ h ++ "\n", header ++ h ++ " \n", header ++ h ++ "\n", header ++ "\n", h ++ " refs/heads/main\n" ++ header, h ++ "x refs/heads/main\n", }) |c| { var d: Diagnostics = .{}; try testing.expectError(error.UnexpectedPackedRefsLine, parse(gpa, .sha1, c, &d)); } var d: Diagnostics = .{}; try testing.expectError(error.UnterminatedPackedRefsLine, parse(gpa, .sha1, header ++ h ++ " refs/heads/main", &d)); try testing.expectEqualStrings(h ++ " refs/heads/main", d.detail());}
test "unsorted files are sorted, and files that say they are sorted are trusted" { const gpa = testing.allocator; const h = "f262b5fdc70bf36bebcf70cc60cf788a294c8126"; var p = try parse(gpa, .sha1, h ++ " refs/tags/v1\n" ++ h ++ "\trefs/heads/main\n", null); defer p.deinit(gpa); try testing.expectEqualStrings("refs/heads/main", p.entries[0].name); var q = try parse(gpa, .sha1, "# pack-refs with: sorted\n" ++ h ++ " refs/tags/v1\n" ++ h ++ " refs/heads/main\r\n", null); defer q.deinit(gpa); try testing.expectEqualStrings("refs/tags/v1", q.entries[0].name); try testing.expectEqualStrings("refs/heads/main\r", q.entries[1].name);}
test "fuzz: packed-refs" { try fuzz_input.fuzz({}, struct { fn one(_: void, smith: *testing.Smith) anyerror!void { var buffer: [1024]u8 = undefined; const input = @import("../testing/fuzz.zig").bytes(smith, &buffer); for ([_]object.Format{ .sha1, .sha256 }) |f| { var p = parse(testing.allocator, f, input, null) catch continue; defer p.deinit(testing.allocator); for (p.entries) |e| _ = p.find(e.name); } } }.one, .{ .corpus = &.{ @embedFile("../fuzz/packed-refs"), @embedFile("../fuzz/packed-refs-sha256"), } });}