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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Reflog entries, and the lines the files backend keeps them in under//! `logs/`, one line per change, oldest first://!//! <old object ID> SP <new object ID> SP <identity line> [TAB <message>] LF//!//! as git-update-ref(1) describes them. The identity is read with//! `Identity`, and the message is everything after the tab. git passes//! over a line it cannot read rather than failing — one with a malformed//! object ID, no email address or no date — and over a last line with no//! line feed, and so does this.
const std = @import("std");const Allocator = std.mem.Allocator;
const object = @import("../object.zig");const Id = object.Id;const Identity = @import("../Identity.zig");
/// One change to a ref, whichever backend recorded it.pub const Entry = struct { /// The value before; all zeros when the ref was created. old: Id, /// The value after; all zeros when the ref was deleted. new: Id, /// Who made the change. name: []const u8, /// Their email address, without angle brackets. email: []const u8, /// When, and in what time zone. date: Identity.Date, /// Why, without a trailing line feed; empty when none was given. message: []const u8,};
/// A ref's reflog, oldest entry first.pub const Reflog = struct { entries: []Entry, /// What the entries' strings live in. arena: std.heap.ArenaAllocator,
/// Frees everything. pub fn deinit(r: *Reflog) void { r.arena.deinit(); r.* = undefined; }};
/// Reads one line of a reflog file, without its line feed, or returns/// null for a line git passes over. The entry's strings are slices of/// `line`.pub fn parseLine(format: object.Format, line: []const u8) ?Entry { const n = format.hexLength(); if (line.len < 2 * n + 2 or line[n] != ' ' or line[2 * n + 1] != ' ') return null; const old = Id.parseHex(format, line[0..n]) catch return null; const new = Id.parseHex(format, line[n + 1 .. 2 * n + 1]) catch return null; const rest = line[2 * n + 2 ..]; const tab = std.mem.findScalar(u8, rest, '\t'); const identity = Identity.parse(rest[0 .. tab orelse rest.len]) orelse return null; const date = identity.date() orelse return null; return .{ .old = old, .new = new, .name = identity.name, .email = identity.email, .date = date, .message = if (tab) |t| rest[t + 1 ..] else "", };}
/// Reads a whole reflog file, oldest entry first, into `entries`; the/// entries' strings are slices of `contents`.pub fn parseFile(gpa: Allocator, format: object.Format, contents: []const u8, entries: *std.ArrayList(Entry)) Allocator.Error!void { var rest = contents; while (std.mem.findScalar(u8, rest, '\n')) |nl| { if (parseLine(format, rest[0..nl])) |e| try entries.append(gpa, e); rest = rest[nl + 1 ..]; }}
/// The time zone a reftable log record keeps in minutes east of UTC, as/// `Identity.Date` keeps it: hours times a hundred plus minutes.pub fn zoneFromMinutes(minutes: i16) i32 { const m: i32 = minutes; const a: i32 = @intCast(@abs(m)); const z = @divTrunc(a, 60) * 100 + @mod(a, 60); return if (m < 0) -z else z;}
// -- tests -----------------------------------------------------------------------
const testing = std.testing;
const fuzz_input = @import("../testing/fuzz.zig");
test "reflog lines are read, and the ones git passes over passed over" { const gpa = testing.allocator; var entries: std.ArrayList(Entry) = .empty; defer entries.deinit(gpa); try parseFile(gpa, .sha1, @embedFile("../fuzz/reflog"), &entries); try testing.expectEqual(2, entries.items.len); try testing.expectEqualStrings("branch: Created from main", entries.items[0].message); try testing.expectEqualStrings("C O Mitter", entries.items[1].name); try testing.expectEqual(-500, entries.items[1].date.zone); const z = "0000000000000000000000000000000000000000"; const h = "f262b5fdc70bf36bebcf70cc60cf788a294c8126"; try testing.expect(parseLine(.sha1, "garbage") == null); try testing.expect(parseLine(.sha1, z ++ " " ++ h ++ " A a@x 1790000000 +0100\tm") == null); try testing.expect(parseLine(.sha1, z ++ " " ++ h ++ " A <a@x>\tm") == null); try testing.expect(parseLine(.sha1, z ++ " " ++ h ++ " A <a@x> 1790000000 +0100\tm") == null); const e = parseLine(.sha1, z ++ " " ++ h ++ " A <a@x> 1790000000 +0100\tmsg\twith tab\r").?; try testing.expectEqualStrings("msg\twith tab\r", e.message); try testing.expectEqualStrings("", parseLine(.sha1, z ++ " " ++ h ++ " A <a@x> 1790000000 +0100").?.message); entries.clearRetainingCapacity(); try parseFile(gpa, .sha1, z ++ " " ++ h ++ " A <a@x> 1 +0000\tno newline", &entries); try testing.expectEqual(0, entries.items.len);}
test "reftable time zones become git's" { try testing.expectEqual(-800, zoneFromMinutes(-480)); try testing.expectEqual(230, zoneFromMinutes(150)); try testing.expectEqual(0, zoneFromMinutes(0));}
test "fuzz: reflog files" { 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); var entries: std.ArrayList(Entry) = .empty; defer entries.deinit(testing.allocator); for ([_]object.Format{ .sha1, .sha256 }) |f| try parseFile(testing.allocator, f, input, &entries); } }.one, .{ .corpus = &.{@embedFile("../fuzz/reflog")} });}