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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Resolve-undo: the `REUC` extension of the index, read from its bytes.//!//! gitformat-index(5) describes a record for each path whose conflict was//! resolved: the path and a NUL, three modes in ASCII octal each ending//! in a NUL (0 for a stage the conflict did not have), and an object ID//! for each stage whose mode is not 0. What git makes of the rest, seen//! through `git ls-files --resolve-undo` on extensions written by hand://!//! - A mode is read as C's `strtoul` reads base 8: blank space, then a//! sign, then at least one octal digit, and nothing else before the NUL.//! A negative one wraps, one too big for 64 bits is the largest there//! is, and either is cut to 32 bits.//! - A record whose three modes are all 0 is invalid.//! - The path may be empty; git shows it as `./`.//! - The records are kept sorted by path, whatever order they came in, and//! a later record for a path replaces an earlier one.//!//! Anything else wrong makes git drop the whole extension with "Index//! records invalid resolve-undo information", and go on.
const std = @import("std");const Allocator = std.mem.Allocator;
const object = @import("../object.zig");const Format = object.Format;const Id = object.Id;
/// What one path's conflict was before it was resolved.pub const Record = struct { path: []const u8, /// The mode of stages 1, 2 and 3; 0 for a stage the conflict did not /// have. modes: [3]u32, /// The object IDs of stages 1, 2 and 3; null where the mode is 0. ids: [3]?Id,};
/// Every path's record, sorted by path.pub const ResolveUndo = struct { records: []Record,
/// The record for `path`, if there is one. pub fn find(r: *const ResolveUndo, path: []const u8) ?*const Record { const i = std.sort.binarySearch(Record, r.records, path, struct { fn order(p: []const u8, rec: Record) std.math.Order { return std.mem.order(u8, p, rec.path); } }.order) orelse return null; return &r.records[i]; }};
/// Why a `REUC` extension could not be read: git's "Index records invalid/// resolve-undo information".pub const Error = error{InvalidResolveUndo} || Allocator.Error;
/// Reads the `REUC` extension's contents, in `format`, allocating with/// `arena`.pub fn parse(arena: Allocator, format: Format, data: []const u8) Error!ResolveUndo { var records: std.ArrayList(Record) = .empty; var pos: usize = 0; const n = format.byteLength(); while (pos < data.len) { const nul = std.mem.findScalar(u8, data[pos..], 0) orelse return error.InvalidResolveUndo; var r: Record = .{ .path = data[pos..][0..nul], .modes = undefined, .ids = .{ null, null, null } }; pos += nul + 1; for (&r.modes) |*m| { const end = std.mem.findScalar(u8, data[pos..], 0) orelse return error.InvalidResolveUndo; m.* = parseMode(data[pos..][0..end]) orelse return error.InvalidResolveUndo; pos += end + 1; } if (r.modes[0] == 0 and r.modes[1] == 0 and r.modes[2] == 0) return error.InvalidResolveUndo; for (r.modes, &r.ids) |m, *id| { if (m == 0) continue; if (data.len - pos < n) return error.InvalidResolveUndo; id.* = .fromBytes(format, data[pos..][0..n]); pos += n; } try records.append(arena, r); } // Sorted, a later record for a path replacing an earlier one: a // stable sort keeps them in order, and the last of each run is kept. std.mem.sort(Record, records.items, {}, struct { fn lessThan(_: void, a: Record, b: Record) bool { return std.mem.order(u8, a.path, b.path) == .lt; } }.lessThan); var out: usize = 0; for (records.items, 0..) |r, i| { if (i + 1 < records.items.len and std.mem.eql(u8, records.items[i + 1].path, r.path)) continue; records.items[out] = r; out += 1; } records.shrinkRetainingCapacity(out); return .{ .records = try records.toOwnedSlice(arena) };}
/// Reads a mode as `strtoul(text, &end, 8)` would, requiring it to take/// all of `text`, and cuts it to 32 bits.fn parseMode(text: []const u8) ?u32 { var p: usize = 0; while (p < text.len and std.ascii.isWhitespace(text[p])) p += 1; var negative = false; if (p < text.len and (text[p] == '+' or text[p] == '-')) { negative = text[p] == '-'; p += 1; } const start = p; var v: u64 = 0; var overflow = false; while (p < text.len and text[p] >= '0' and text[p] <= '7') : (p += 1) { const shifted = @shlWithOverflow(v, 3); if (shifted[1] != 0) overflow = true; v = shifted[0] | (text[p] - '0'); } if (p == start or p != text.len) return null; if (overflow) v = std.math.maxInt(u64) else if (negative) v = 0 -% v; return @truncate(v);}
// -- tests -----------------------------------------------------------------------
const testing = std.testing;
const fuzz_input = @import("../testing/fuzz.zig");
test "modes are read as strtoul reads them" { try testing.expectEqual(0o100644, parseMode("100644")); try testing.expectEqual(0o100644, parseMode(" +100644")); try testing.expectEqual(0xffff_ffff, parseMode("-1")); try testing.expectEqual(0xffff_ffff, parseMode("77777777777")); try testing.expectEqual(0xffff_ffff, parseMode("7777777777777777777777777")); try testing.expectEqual(null, parseMode("")); try testing.expectEqual(null, parseMode("8")); try testing.expectEqual(null, parseMode("644x")); try testing.expectEqual(null, parseMode("0x10"));}
test "records are kept sorted, a later one for a path replacing an earlier" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const id: [20]u8 = @splat(0x12); const data = "q\x000\x000\x00100644\x00" ++ id ++ "p\x00100644\x000\x000\x00" ++ id ++ "p\x000\x000\x00100755\x00" ++ id; const r = try parse(arena.allocator(), .sha1, data); try testing.expectEqual(2, r.records.len); try testing.expectEqualStrings("p", r.records[0].path); try testing.expectEqual(0o100755, r.find("p").?.modes[2]); try testing.expect(r.find("p").?.ids[0] == null); try testing.expect(r.find("x") == null); for ([_][]const u8{ "p\x000\x000\x000\x00", "p\x00100644\x000\x000\x00" ++ id[0..10], "p", "p\x00\x000\x00100644\x00" ++ id, }) |bad| try testing.expectError(error.InvalidResolveUndo, parse(arena.allocator(), .sha1, bad));}
test "fuzz: resolve-undo" { 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 ([_]Format{ .sha1, .sha256 }) |format| { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const r = parse(arena.allocator(), format, input) catch continue; for (r.records) |rec| try testing.expect(r.find(rec.path) != null); } } }.one, .{ .corpus = &.{ @embedFile("../fuzz/index-reuc"), @embedFile("../fuzz/index-reuc-sha256"), } });}