Artifact 75d951bcbbc1834e1399d6ca7c00e2507cdcb1e752f2b551ef9b3aa0114ecec5:
- File
src/git_fast_export.zig
— part of check-in
[dc0aa2f858]
at
2026-07-31 02:56:07
on branch sub-manifest
— git-import: translate a Git fast-export stream into a zit history
`zit import-git SOURCE DEST` reads a Git repository through `git fast- export` and builds a zit repository from it. The destination appears only after the export, artifact creation and publication have all completed, so a failure anywhere leaves no half-published repository behind; this is the first caller of the logstore artifact batch added earlier on this branch, which is what makes a check-in's blobs arrive as a unit.
Mode 160000 now parses as a gitlink instead of error.UnsupportedMode. That completes the split begun in the previous check-in: reporting what the stream said is the parser's job, and deciding that zit has no submodule concept is the importer's. The refusal still happens, but it now explains itself instead of failing as a parse error on input git legitimately emits.
Signed commits are refused by default rather than imported with the signature silently dropped. Fossil has no signature card, so importing one is a loss the operator should choose; --drop-signatures accepts it explicitly.
The sub-manifest parameter is analyzed, not guessed. Preflight measures the source's own name set and pins its measured optimum, and --submanifest-k overrides that only to match another repository's existing pin, which is what lets two repositories share sub-manifests by content address. On a 100,000 file corpus the analyzed value cuts the objective by roughly 3.4x against the fixed default it replaces.
Correctness is checked differentially against real Git rather than against fixtures we wrote: every materialized tree is compared to `git ls-tree` for the corresponding commit, refs and annotated tags are compared to the source, and both comparators now assert that a gitlink is absent rather than merely tolerating it, so the omission is a contract instead of an accident.
The rest is wiring. extras learns the import provenance names, root.zig exports the importer modules across the executable and library boundary, tidy and the CI step guard learn the new test roots and build steps, and the e2e help check learns that a verb spelled import_git in Zig is import-git on the command line. (user: dmestas, size: 30137) [annotate] [blame] [check-ins using]
const std = @import("std"); pub const Limits = struct { max_line: usize, max_data: u64, max_path: usize, max_merges_per_commit: usize = 1_024, max_events: u64 = 1_000_000, max_total_data: u64 = 1 << 40, }; /// Floor on the bytes one retained file command occupies: its slot in the /// per-commit command list plus the shortest path it can carry. `parsePathToken` /// rejects the empty path, so one byte is that floor. Nothing smaller is /// representable, which is what makes it safe to divide a byte budget by. pub const min_retained_bytes_per_file_command: u64 = @sizeOf(FileCommand) + 1; /// The per-commit file-command ceiling, derived rather than declared. /// /// `max_total_data` is the parser's authorization for bytes it may hold in an /// event arena, but `admitData` charges only `data <n>` payloads against it. /// A file command's list slot and duplicated path are arena bytes no budget /// sees, so an unbounded command loop grows the arena without ever touching /// `max_total_data`. The ceiling closes that gap by restating the same /// authorization in commands: the most whose minimum retention still fits /// inside the budget. It moves when the byte budget moves and at no other time. pub fn fileCommandCeiling(limits: Limits) usize { const ceiling = limits.max_total_data / min_retained_bytes_per_file_command; return std.math.cast(usize, ceiling) orelse std.math.maxInt(usize); } /// The numbers a `FileCommandLimitExceeded` diagnostic must name. The parser /// has no output channel, so it retains them and the caller — which knows the /// flag that raises the budget — does the formatting. pub const FileCommandOverrun = struct { /// The refused commit carried at least this many commands: the ceiling /// plus the one that did not fit. Parsing stops there, so the true count /// is unknown and this is a lower bound, never an estimate. observed_at_least: usize, ceiling: usize, payload_budget_bytes: u64, min_bytes_per_command: u64 = min_retained_bytes_per_file_command, }; pub const Error = error{ ActiveLease, Malformed, DataLimitExceeded, EventLimitExceeded, FileCommandLimitExceeded, LineLimitExceeded, MergeLimitExceeded, PathLimitExceeded, MissingDone, UnexpectedEof, TrailingData, UnsupportedFeature, UnknownMode, OutOfMemory, Io, }; pub const ObjectRef = union(enum) { mark: u64, oid: []const u8, }; /// Modes a Git tree entry can carry. `gitlink` is mode 160000 -- a recorded /// submodule commit. Zit has no equivalent, but that is an import-policy /// question, not a parsing one: the parser reports what the stream said and /// the importer decides what to do with it. Only modes Git never emits reach /// `error.UnknownMode`. pub const FileMode = enum { regular, executable, symlink, gitlink, }; pub const FileCommand = union(enum) { modify: struct { mode: FileMode, data: ObjectRef, path: []const u8, }, delete: []const u8, rename: struct { old: []const u8, new: []const u8, }, copy: struct { old: []const u8, new: []const u8, }, delete_all, }; pub const Identity = struct { name: []const u8, email: []const u8, timestamp: i64, timezone_offset_minutes: i16, }; pub const Blob = struct { mark: u64, original_oid: ?[]const u8, data: []const u8, }; pub const Commit = struct { ref: []const u8, mark: u64, original_oid: ?[]const u8, author: Identity, committer: Identity, message: []const u8, from: ?ObjectRef, merges: []const ObjectRef, commands: []const FileCommand, }; pub const Reset = struct { ref: []const u8, target: ?ObjectRef, }; pub const Tag = struct { name: []const u8, mark: ?u64, target: ObjectRef, original_oid: ?[]const u8, tagger: Identity, message: []const u8, }; pub const Event = union(enum) { blob: Blob, commit: Commit, reset: Reset, tag: Tag, done, }; pub const EventLease = struct { value: Event, reader: *Reader, released: bool = false, pub fn deinit(self: *EventLease) void { if (self.released) return; self.released = true; self.reader.releaseLease(); } }; pub const Reader = struct { gpa: std.mem.Allocator, input: std.Io.Reader, limits: Limits, line: std.ArrayList(u8), event_arena: ?std.heap.ArenaAllocator = null, active_lease: bool = false, feature_done_seen: bool = false, records_started: bool = false, done_seen: bool = false, eof_seen: bool = false, pending_top_level: ?[]u8 = null, events_seen: u64 = 0, data_bytes_seen: u64 = 0, /// Retained by the one bound whose error name cannot carry its own /// numbers. Null until `FileCommandLimitExceeded` is returned. file_command_overrun: ?FileCommandOverrun = null, pub fn init(gpa: std.mem.Allocator, input: std.Io.Reader, limits: Limits) Reader { return .{ .gpa = gpa, .input = input, .limits = limits, .line = .empty, }; } pub fn deinit(self: *Reader) void { if (self.event_arena) |arena| arena.deinit(); self.event_arena = null; self.active_lease = false; self.line.deinit(self.gpa); if (self.pending_top_level) |line| self.gpa.free(line); self.pending_top_level = null; } pub fn next(self: *Reader) Error!?EventLease { if (self.active_lease) return error.ActiveLease; if (self.eof_seen) return null; if (self.done_seen) { try self.requireEof(); self.eof_seen = true; return null; } while (true) { const line = try self.readTopLevelLine(); if (std.mem.startsWith(u8, line, "feature ")) { if (self.records_started or self.feature_done_seen) return error.Malformed; if (!std.mem.eql(u8, line, "feature done")) return error.UnsupportedFeature; self.feature_done_seen = true; continue; } if (std.mem.startsWith(u8, line, "progress ")) { if (!self.feature_done_seen) return error.Malformed; try self.admitEvent(); continue; } if (!self.feature_done_seen) return error.Malformed; const is_event = std.mem.eql(u8, line, "blob") or std.mem.startsWith(u8, line, "commit ") or std.mem.startsWith(u8, line, "reset ") or std.mem.startsWith(u8, line, "tag ") or std.mem.eql(u8, line, "done"); if (!is_event) return error.Malformed; self.records_started = true; try self.admitEvent(); self.event_arena = std.heap.ArenaAllocator.init(self.gpa); errdefer { self.event_arena.?.deinit(); self.event_arena = null; } const allocator = self.eventAllocator(); const event: Event = if (std.mem.eql(u8, line, "blob")) .{ .blob = try self.parseBlob(allocator) } else if (std.mem.startsWith(u8, line, "commit ")) .{ .commit = try self.parseCommit(allocator, line["commit ".len..]) } else if (std.mem.startsWith(u8, line, "reset ")) .{ .reset = try self.parseReset(allocator, line["reset ".len..]) } else if (std.mem.startsWith(u8, line, "tag ")) .{ .tag = try self.parseTag(allocator, line["tag ".len..]) } else if (std.mem.eql(u8, line, "done")) blk: { self.done_seen = true; break :blk .done; } else unreachable; return self.lease(event); } } fn lease(self: *Reader, value: Event) EventLease { self.active_lease = true; return .{ .value = value, .reader = self }; } fn releaseLease(self: *Reader) void { std.debug.assert(self.active_lease); self.event_arena.?.deinit(); self.event_arena = null; self.active_lease = false; } fn eventAllocator(self: *Reader) std.mem.Allocator { return self.event_arena.?.allocator(); } fn admitEvent(self: *Reader) Error!void { const next_events_seen = std.math.add(u64, self.events_seen, 1) catch return error.EventLimitExceeded; if (next_events_seen > self.limits.max_events) return error.EventLimitExceeded; self.events_seen = next_events_seen; } fn admitData(self: *Reader, size: u64) Error!void { const next_data_bytes_seen = std.math.add(u64, self.data_bytes_seen, size) catch return error.DataLimitExceeded; if (next_data_bytes_seen > self.limits.max_total_data) return error.DataLimitExceeded; self.data_bytes_seen = next_data_bytes_seen; } fn readTopLevelLine(self: *Reader) Error![]const u8 { if (self.pending_top_level) |owned| { defer self.gpa.free(owned); self.pending_top_level = null; self.line.clearRetainingCapacity(); self.line.appendSlice(self.gpa, owned) catch return error.OutOfMemory; return self.line.items; } return self.readLine() catch |err| switch (err) { error.EndOfStream => return error.MissingDone, else => |e| return e, }; } fn requiredLine(self: *Reader) Error![]const u8 { return self.readLine() catch |err| switch (err) { error.EndOfStream => return error.UnexpectedEof, else => |e| return e, }; } fn readLine(self: *Reader) (Error || error{EndOfStream})![]const u8 { self.line.clearRetainingCapacity(); while (true) { const byte = self.input.takeByte() catch |err| switch (err) { error.EndOfStream => return error.EndOfStream, error.ReadFailed => return error.Io, }; if (byte == '\n') return self.line.items; if (self.line.items.len == self.limits.max_line) return error.LineLimitExceeded; self.line.append(self.gpa, byte) catch return error.OutOfMemory; } } fn requireEof(self: *Reader) Error!void { _ = self.input.takeByte() catch |err| switch (err) { error.EndOfStream => return, error.ReadFailed => return error.Io, }; return error.TrailingData; } fn duplicate(_: *Reader, allocator: std.mem.Allocator, bytes: []const u8) Error![]const u8 { return allocator.dupe(u8, bytes) catch return error.OutOfMemory; } fn parseBlob(self: *Reader, allocator: std.mem.Allocator) Error!Blob { const mark = try parseMarkLine(try self.requiredLine()); var original_oid: ?[]const u8 = null; while (true) { const line = try self.requiredLine(); if (std.mem.startsWith(u8, line, "original-oid ")) { if (original_oid != null) return error.Malformed; original_oid = try parseOid(allocator, line["original-oid ".len..]); continue; } if (std.mem.startsWith(u8, line, "data ")) { return .{ .mark = mark, .original_oid = original_oid, .data = try self.readData(allocator, line), }; } return error.Malformed; } } fn parseCommit(self: *Reader, allocator: std.mem.Allocator, ref_text: []const u8) Error!Commit { const ref = try self.parseRef(allocator, ref_text); const mark = try parseMarkLine(try self.requiredLine()); var original_oid: ?[]const u8 = null; var author: ?Identity = null; var committer: ?Identity = null; var message: ?[]const u8 = null; while (true) { const line = try self.requiredLine(); if (std.mem.startsWith(u8, line, "original-oid ")) { if (original_oid != null) return error.Malformed; original_oid = try parseOid(allocator, line["original-oid ".len..]); } else if (std.mem.startsWith(u8, line, "author ")) { if (author != null) return error.Malformed; author = try parseIdentity(self, allocator, line["author ".len..]); } else if (std.mem.startsWith(u8, line, "committer ")) { if (committer != null) return error.Malformed; committer = try parseIdentity(self, allocator, line["committer ".len..]); } else if (std.mem.startsWith(u8, line, "data ")) { message = try self.readCommitData(allocator, line); break; } else return error.Malformed; } const parsed_author = author orelse return error.Malformed; const parsed_committer = committer orelse return error.Malformed; const parsed_message = message orelse return error.Malformed; var from: ?ObjectRef = null; var merges: std.ArrayList(ObjectRef) = .empty; var commands: std.ArrayList(FileCommand) = .empty; const file_command_ceiling = fileCommandCeiling(self.limits); // Exact-count data may have an optional trailing LF. Only the first // following empty line is ambiguous with the commit terminator. var first_command_line = true; command_loop: while (true) { var line = try self.requiredLine(); if (line.len == 0) { if (!first_command_line) break; line = self.readLine() catch |err| switch (err) { error.EndOfStream => break :command_loop, else => |e| return e, }; if (line.len == 0) break; if (isTopLevelRecord(line)) { self.pending_top_level = self.gpa.dupe(u8, line) catch return error.OutOfMemory; break; } } first_command_line = false; if (std.mem.startsWith(u8, line, "from ")) { if (from != null) return error.Malformed; from = try parseObjectRef(allocator, line["from ".len..]); } else if (std.mem.startsWith(u8, line, "merge ")) { if (merges.items.len >= self.limits.max_merges_per_commit) return error.MergeLimitExceeded; const merge = try parseObjectRef(allocator, line["merge ".len..]); merges.append(allocator, merge) catch return error.OutOfMemory; } else { if (commands.items.len >= file_command_ceiling) { self.file_command_overrun = .{ .observed_at_least = file_command_ceiling +| 1, .ceiling = file_command_ceiling, .payload_budget_bytes = self.limits.max_total_data, }; return error.FileCommandLimitExceeded; } const command = try self.parseFileCommand(allocator, line); commands.append(allocator, command) catch return error.OutOfMemory; } } return .{ .ref = ref, .mark = mark, .original_oid = original_oid, .author = parsed_author, .committer = parsed_committer, .message = parsed_message, .from = from, .merges = merges.toOwnedSlice(allocator) catch return error.OutOfMemory, .commands = commands.toOwnedSlice(allocator) catch return error.OutOfMemory, }; } fn isTopLevelRecord(line: []const u8) bool { return std.mem.eql(u8, line, "blob") or std.mem.startsWith(u8, line, "commit ") or std.mem.startsWith(u8, line, "reset ") or std.mem.startsWith(u8, line, "tag ") or std.mem.startsWith(u8, line, "progress ") or std.mem.eql(u8, line, "done"); } fn parseReset(self: *Reader, allocator: std.mem.Allocator, ref_text: []const u8) Error!Reset { const ref = try self.parseRef(allocator, ref_text); const line = try self.requiredLine(); if (line.len == 0) return .{ .ref = ref, .target = null }; if (isTopLevelRecord(line)) { self.pending_top_level = self.gpa.dupe(u8, line) catch return error.OutOfMemory; return .{ .ref = ref, .target = null }; } if (!std.mem.startsWith(u8, line, "from ")) return error.Malformed; const target = try parseObjectRef(allocator, line["from ".len..]); const separator = try self.requiredLine(); if (separator.len == 0) return .{ .ref = ref, .target = target }; if (!isTopLevelRecord(separator)) return error.Malformed; self.pending_top_level = self.gpa.dupe(u8, separator) catch return error.OutOfMemory; return .{ .ref = ref, .target = target }; } fn parseTag(self: *Reader, allocator: std.mem.Allocator, name_text: []const u8) Error!Tag { const name = try self.parseRef(allocator, name_text); var mark: ?u64 = null; var target: ?ObjectRef = null; var original_oid: ?[]const u8 = null; var tagger: ?Identity = null; while (true) { const line = try self.requiredLine(); if (std.mem.startsWith(u8, line, "mark ")) { if (mark != null) return error.Malformed; mark = try parseMarkLine(line); } else if (std.mem.startsWith(u8, line, "from ")) { if (target != null) return error.Malformed; target = try parseObjectRef(allocator, line["from ".len..]); } else if (std.mem.startsWith(u8, line, "original-oid ")) { if (original_oid != null) return error.Malformed; original_oid = try parseOid(allocator, line["original-oid ".len..]); } else if (std.mem.startsWith(u8, line, "tagger ")) { if (tagger != null) return error.Malformed; tagger = try parseIdentity(self, allocator, line["tagger ".len..]); } else if (std.mem.startsWith(u8, line, "data ")) { return .{ .name = name, .mark = mark, .target = target orelse return error.Malformed, .original_oid = original_oid, .tagger = tagger orelse return error.Malformed, .message = try self.readData(allocator, line), }; } else return error.Malformed; } } fn parseFileCommand(self: *Reader, allocator: std.mem.Allocator, line: []const u8) Error!FileCommand { if (std.mem.eql(u8, line, "deleteall")) return .delete_all; if (std.mem.startsWith(u8, line, "M ")) return self.parseModify(allocator, line[2..]); if (std.mem.startsWith(u8, line, "D ")) return .{ .delete = try self.parseSinglePath(allocator, line[2..]) }; if (std.mem.startsWith(u8, line, "R ")) return self.parsePair(allocator, line[2..], .rename); if (std.mem.startsWith(u8, line, "C ")) return self.parsePair(allocator, line[2..], .copy); return error.Malformed; } fn parseModify(self: *Reader, allocator: std.mem.Allocator, body: []const u8) Error!FileCommand { const mode_end = std.mem.indexOfScalar(u8, body, ' ') orelse return error.Malformed; const mode = try parseMode(body[0..mode_end]); var index = mode_end + 1; const data_text = takeBareToken(body, &index) orelse return error.Malformed; const data = try parseObjectRef(allocator, data_text); if (index >= body.len or body[index] != ' ') return error.Malformed; index += 1; const path = try self.parsePathToken(allocator, body, &index); if (index != body.len) return error.Malformed; return .{ .modify = .{ .mode = mode, .data = data, .path = path } }; } fn parseSinglePath(self: *Reader, allocator: std.mem.Allocator, body: []const u8) Error![]const u8 { var index: usize = 0; const path = try self.parsePathToken(allocator, body, &index); if (index != body.len) return error.Malformed; return path; } fn parsePair(self: *Reader, allocator: std.mem.Allocator, body: []const u8, comptime kind: enum { rename, copy }) Error!FileCommand { var index: usize = 0; const old = try self.parsePathToken(allocator, body, &index); if (index >= body.len or body[index] != ' ') return error.Malformed; index += 1; const new = try self.parsePathToken(allocator, body, &index); if (index != body.len) return error.Malformed; return switch (kind) { .rename => .{ .rename = .{ .old = old, .new = new } }, .copy => .{ .copy = .{ .old = old, .new = new } }, }; } fn parsePathToken(self: *Reader, allocator: std.mem.Allocator, input: []const u8, index: *usize) Error![]const u8 { if (index.* >= input.len) return error.Malformed; const start = index.*; if (input[start] == '"') { index.* += 1; var escaped = false; while (index.* < input.len) : (index.* += 1) { const byte = input[index.*]; if (escaped) { escaped = false; continue; } if (byte == '\\') { escaped = true; continue; } if (byte == '"') { index.* += 1; return self.decodeQuotedPath(allocator, input[start + 1 .. index.* - 1]); } } return error.Malformed; } while (index.* < input.len and input[index.*] != ' ') : (index.* += 1) {} return self.copyPath(allocator, input[start..index.*]); } fn copyPath(self: *Reader, allocator: std.mem.Allocator, path: []const u8) Error![]const u8 { if (path.len == 0 or std.mem.indexOfScalar(u8, path, 0) != null) return error.Malformed; if (path.len > self.limits.max_path) return error.PathLimitExceeded; return self.duplicate(allocator, path); } fn decodeQuotedPath(self: *Reader, allocator: std.mem.Allocator, encoded: []const u8) Error![]const u8 { var decoded: std.ArrayList(u8) = .empty; var index: usize = 0; while (index < encoded.len) { const byte = encoded[index]; index += 1; if (byte != '\\') { try appendPathByte(self, &decoded, byte); continue; } if (index == encoded.len) return error.Malformed; const escape = encoded[index]; index += 1; const value: u8 = switch (escape) { '"', '\\' => escape, 'a' => '\x07', 'b' => '\x08', 'f' => '\x0c', 'n' => '\n', 'r' => '\r', 't' => '\t', 'v' => '\x0b', '0'...'7' => blk: { if (index + 2 > encoded.len) return error.Malformed; const second = encoded[index]; const third = encoded[index + 1]; if (second < '0' or second > '7' or third < '0' or third > '7') return error.Malformed; index += 2; const octal: u16 = @as(u16, escape - '0') * 64 + @as(u16, second - '0') * 8 + @as(u16, third - '0'); if (octal > @as(u16, std.math.maxInt(u8))) return error.Malformed; break :blk @intCast(octal); }, else => return error.Malformed, }; try appendPathByte(self, &decoded, value); } if (decoded.items.len == 0) return error.Malformed; return decoded.toOwnedSlice(allocator) catch return error.OutOfMemory; } fn parseRef(self: *Reader, allocator: std.mem.Allocator, text: []const u8) Error![]const u8 { if (text.len == 0 or std.mem.indexOfScalar(u8, text, ' ') != null or std.mem.indexOfScalar(u8, text, 0) != null) return error.Malformed; return self.duplicate(allocator, text); } fn readData(self: *Reader, allocator: std.mem.Allocator, line: []const u8) Error![]const u8 { const data = try self.readDataExact(allocator, line); const terminator = self.input.takeByte() catch |err| switch (err) { error.EndOfStream => return error.UnexpectedEof, error.ReadFailed => return error.Io, }; if (terminator != '\n') return error.Malformed; return data; } fn readCommitData(self: *Reader, allocator: std.mem.Allocator, line: []const u8) Error![]const u8 { return self.readDataExact(allocator, line); } fn readDataExact(self: *Reader, allocator: std.mem.Allocator, line: []const u8) Error![]const u8 { if (!std.mem.startsWith(u8, line, "data ")) return error.Malformed; const size = try parseUnsigned(line["data ".len..]); if (size > self.limits.max_data or size > @as(u64, std.math.maxInt(usize))) return error.DataLimitExceeded; try self.admitData(size); const count: usize = @intCast(size); const data = allocator.alloc(u8, count) catch return error.OutOfMemory; self.input.readSliceAll(data) catch |err| switch (err) { error.EndOfStream => return error.UnexpectedEof, error.ReadFailed => return error.Io, }; return data; } }; fn appendPathByte(reader: *Reader, list: *std.ArrayList(u8), byte: u8) Error!void { if (byte == 0) return error.Malformed; if (list.items.len == reader.limits.max_path) return error.PathLimitExceeded; list.append(reader.eventAllocator(), byte) catch return error.OutOfMemory; } fn parseMarkLine(line: []const u8) Error!u64 { if (!std.mem.startsWith(u8, line, "mark :")) return error.Malformed; const mark = try parseUnsigned(line["mark :".len..]); if (mark == 0) return error.Malformed; return mark; } fn parseObjectRef(allocator: std.mem.Allocator, text: []const u8) Error!ObjectRef { if (text.len == 0) return error.Malformed; if (text[0] == ':') return .{ .mark = try parseMark(text) }; return .{ .oid = try parseOid(allocator, text) }; } fn parseMark(text: []const u8) Error!u64 { if (text.len < 2 or text[0] != ':') return error.Malformed; const mark = try parseUnsigned(text[1..]); if (mark == 0) return error.Malformed; return mark; } fn parseOid(allocator: std.mem.Allocator, text: []const u8) Error![]const u8 { if (text.len != 40 and text.len != 64) return error.Malformed; for (text) |byte| if (!std.ascii.isHex(byte)) return error.Malformed; return allocator.dupe(u8, text) catch return error.OutOfMemory; } fn parseUnsigned(text: []const u8) Error!u64 { if (text.len == 0) return error.Malformed; var value: u64 = 0; for (text) |byte| { if (byte < '0' or byte > '9') return error.Malformed; const digit: u64 = byte - '0'; if (value > (std.math.maxInt(u64) - digit) / 10) return error.Malformed; value = value * 10 + digit; } return value; } fn parseSigned(text: []const u8) Error!i64 { if (text.len == 0) return error.Malformed; const negative = text[0] == '-'; const digits = if (negative or text[0] == '+') text[1..] else text; const unsigned = try parseUnsigned(digits); if (negative) { const minimum_magnitude = @as(u64, std.math.maxInt(i64)) + 1; if (unsigned > minimum_magnitude) return error.Malformed; if (unsigned == minimum_magnitude) return std.math.minInt(i64); return -@as(i64, @intCast(unsigned)); } if (unsigned > std.math.maxInt(i64)) return error.Malformed; return @intCast(unsigned); } fn parseIdentity(reader: *Reader, allocator: std.mem.Allocator, text: []const u8) Error!Identity { const close = std.mem.lastIndexOfScalar(u8, text, '>') orelse return error.Malformed; const open = std.mem.lastIndexOfScalar(u8, text[0..close], '<') orelse return error.Malformed; if (open == 0 or text[open - 1] != ' ' or open + 1 == close or close + 1 >= text.len or text[close + 1] != ' ') return error.Malformed; const name = text[0 .. open - 1]; const email = text[open + 1 .. close]; if (name.len == 0 or email.len == 0) return error.Malformed; const stamp_and_zone = text[close + 2 ..]; const separator = std.mem.indexOfScalar(u8, stamp_and_zone, ' ') orelse return error.Malformed; const timestamp = try parseSigned(stamp_and_zone[0..separator]); const zone = stamp_and_zone[separator + 1 ..]; if (zone.len != 5 or (zone[0] != '+' and zone[0] != '-')) return error.Malformed; const hours = try parseUnsigned(zone[1..3]); const minutes = try parseUnsigned(zone[3..5]); if (hours > 23 or minutes > 59) return error.Malformed; const raw_offset: i16 = @intCast(hours * 60 + minutes); return .{ .name = try reader.duplicate(allocator, name), .email = try reader.duplicate(allocator, email), .timestamp = timestamp, .timezone_offset_minutes = if (zone[0] == '-') -raw_offset else raw_offset, }; } fn parseMode(text: []const u8) Error!FileMode { if (std.mem.eql(u8, text, "100644")) return .regular; if (std.mem.eql(u8, text, "100755")) return .executable; if (std.mem.eql(u8, text, "120000")) return .symlink; if (std.mem.eql(u8, text, "160000")) return .gitlink; return error.UnknownMode; } fn takeBareToken(input: []const u8, index: *usize) ?[]const u8 { if (index.* >= input.len) return null; const start = index.*; while (index.* < input.len and input[index.*] != ' ') : (index.* += 1) {} if (start == index.*) return null; return input[start..index.*]; }