Artifact e47af5da4ad926e3c00ee0eb9cb24b19550d54803f7a7a91e41ae115314061bd:
- File
src/git_fast_export_test.zig
— part of check-in
[85d510d361]
at
2026-07-31 02:28:47
on branch sub-manifest
— fast-export: derive the per-commit file-command ceiling
Limits.max_file_commands_per_commit was a standalone number, and the caller pinned it to the width of our own benchmark corpus. That is a fixture size promoted to policy: no headroom, no way to raise it without a rebuild, and it refused legitimate histories. `git fast-export` lists an entire tree for a root commit and for any commit whose first parent was not exported, so the ceiling is reached by ordinary repositories whose initial commit lands a whole codebase at once.
The bound itself is necessary. admitData is the parser's only byte admission and it sees the byte count in a `data` header alone; every file command additionally allocates a list slot and a duplicated path into the per- commit arena with nothing charging for them, so the command loop is otherwise unbounded against a subprocess pipe.
It is now computed: fileCommandCeiling(limits) = max_total_data divided by min_retained_bytes_per_file_command, where that constant is @sizeOf(FileCommand) plus one -- a command's list slot plus the shortest path it can carry, the empty path being rejected. Nothing smaller is representable, which is what makes it safe to divide a byte budget by. The ceiling restates the arena authorization the caller already granted, in commands rather than bytes, and moves when that authorization moves.
Refusal retains the observed count, the ceiling, and the budget that produced it, so the diagnostic names all three rather than only its own error. The remaining limits gain distinct names -- Data, Event, FileCommand, Line, Merge and Path -- in place of one LimitExceeded that could not say which bound was hit. Exact-count data may carry an optional trailing LF, so only the first following empty line is ambiguous with the commit terminator; readDataExact and readCommitData split that decision instead of guessing. (user: dmestas, size: 27244) [annotate] [blame] [check-ins using]
const std = @import("std"); const fast_export = @import("git_fast_export.zig"); const Limits = fast_export.Limits; const Reader = fast_export.Reader; const Event = fast_export.Event; const oid = "0123456789abcdef0123456789abcdef01234567"; const second_oid = "89abcdef0123456789abcdef0123456789abcdef"; var stream_bytes: []const u8 = &.{}; var stream_split: usize = 0; var stream_offset: usize = 0; fn splitStream(_: *std.Io.Reader, writer: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { if (stream_offset == stream_bytes.len) return error.EndOfStream; const boundary = if (stream_offset < stream_split) stream_split else stream_bytes.len; const available = boundary - stream_offset; const count = @min(available, @intFromEnum(limit)); if (count == 0) return error.EndOfStream; try writer.writeAll(stream_bytes[stream_offset..][0..count]); stream_offset += count; return count; } fn initSplitInput(bytes: []const u8, split: usize, buffer: *[1]u8) std.Io.Reader { stream_bytes = bytes; stream_split = split; stream_offset = 0; return .{ .vtable = &.{ .stream = splitStream }, .buffer = buffer, .seek = 0, .end = 0, }; } fn defaultLimits() Limits { return .{ .max_line = 4096, .max_data = 4096, .max_path = 4096, }; } fn expectBlobThenDone(reader: *Reader) !void { var blob_lease = (try reader.next()).?; defer blob_lease.deinit(); switch (blob_lease.value) { .blob => |blob| { try std.testing.expectEqual(@as(u64, 1), blob.mark); try std.testing.expectEqualStrings(oid, blob.original_oid.?); try std.testing.expectEqualSlices(u8, "a\n\x00bc", blob.data); }, else => return error.TestUnexpectedResult, } blob_lease.deinit(); var done_lease = (try reader.next()).?; defer done_lease.deinit(); switch (done_lease.value) { .done => {}, else => return error.TestUnexpectedResult, } done_lease.deinit(); try std.testing.expect((try reader.next()) == null); } test "git fast-export blob framing survives every split position" { const stream = "feature done\n" ++ "blob\n" ++ "mark :1\n" ++ "original-oid " ++ oid ++ "\n" ++ "data 5\n" ++ "a\n\x00bc\n" ++ "done\n"; for (0..stream.len + 1) |split| { var input_buffer: [1]u8 = undefined; var reader = Reader.init(std.testing.allocator, initSplitInput(stream, split, &input_buffer), defaultLimits()); defer reader.deinit(); try expectBlobThenDone(&reader); } } test "git fast-export leases block advancement and release event storage" { const stream = "feature done\nblob\nmark :1\ndata 1\nx\ndone\n"; const input = std.Io.Reader.fixed(stream); var reader = Reader.init(std.testing.allocator, input, defaultLimits()); defer reader.deinit(); var lease = (try reader.next()).?; try std.testing.expectError(error.ActiveLease, reader.next()); lease.deinit(); var done_lease = (try reader.next()).?; defer done_lease.deinit(); switch (done_lease.value) { .done => {}, else => return error.TestUnexpectedResult, } } test "git fast-export skips top-level progress directives" { const stream = "feature done\n" ++ "progress before blob\n" ++ "blob\nmark :1\ndata 0\n\n" ++ "progress after blob\n" ++ "done\n"; const input = std.Io.Reader.fixed(stream); var reader = Reader.init(std.testing.allocator, input, defaultLimits()); defer reader.deinit(); var blob_lease = (try reader.next()).?; switch (blob_lease.value) { .blob => {}, else => return error.TestUnexpectedResult, } blob_lease.deinit(); var done_lease = (try reader.next()).?; defer done_lease.deinit(); switch (done_lease.value) { .done => {}, else => return error.TestUnexpectedResult, } done_lease.deinit(); try std.testing.expect((try reader.next()) == null); } test "git fast-export commit decodes identities modes paths and ordered commands" { const stream = "feature done\n" ++ "commit refs/heads/main\n" ++ "mark :2\n" ++ "original-oid " ++ oid ++ "\n" ++ "author Alice Example <[email protected]> 1700000000 -0230\n" ++ "committer Bob Example <[email protected]> 1700000001 +0530\n" ++ "data 4\n" ++ "msg\n" ++ "from :1\n" ++ "merge " ++ second_oid ++ "\n" ++ "R \"old\\\\path\" \"space path\\t\\377\"\n" ++ "M 100755 :2 \"space path\\t\\377\"\n" ++ "M 100644 :1 plain\n" ++ "M 120000 :3 link\n" ++ "D -leading-dash\n" ++ "D \"line\\nfeed\"\n" ++ "D \"bells\\a\\v\"\n" ++ "C plain copy\n" ++ "deleteall\n" ++ "\n" ++ "done\n"; const input = std.Io.Reader.fixed(stream); var reader = Reader.init(std.testing.allocator, input, defaultLimits()); defer reader.deinit(); var lease = (try reader.next()).?; defer lease.deinit(); switch (lease.value) { .commit => |commit| { try std.testing.expectEqualStrings("refs/heads/main", commit.ref); try std.testing.expectEqual(@as(u64, 2), commit.mark); try std.testing.expectEqualStrings(oid, commit.original_oid.?); try std.testing.expectEqualStrings("Alice Example", commit.author.name); try std.testing.expectEqualStrings("[email protected]", commit.author.email); try std.testing.expectEqual(@as(i64, 1700000000), commit.author.timestamp); try std.testing.expectEqual(@as(i16, -150), commit.author.timezone_offset_minutes); try std.testing.expectEqualStrings("Bob Example", commit.committer.name); try std.testing.expectEqual(@as(i16, 330), commit.committer.timezone_offset_minutes); try std.testing.expectEqualSlices(u8, "msg\n", commit.message); try std.testing.expectEqual(@as(usize, 1), commit.merges.len); try std.testing.expectEqualStrings(second_oid, commit.merges[0].oid); try std.testing.expectEqual(@as(usize, 9), commit.commands.len); switch (commit.commands[0]) { .rename => |rename| { try std.testing.expectEqualStrings("old\\path", rename.old); try std.testing.expectEqualSlices(u8, "space path\t\xff", rename.new); }, else => return error.TestUnexpectedResult, } switch (commit.commands[1]) { .modify => |modify| { try std.testing.expectEqual(.executable, modify.mode); try std.testing.expectEqual(@as(u64, 2), modify.data.mark); try std.testing.expectEqualSlices(u8, "space path\t\xff", modify.path); }, else => return error.TestUnexpectedResult, } switch (commit.commands[5]) { .delete => |path| try std.testing.expectEqualSlices(u8, "line\nfeed", path), else => return error.TestUnexpectedResult, } switch (commit.commands[6]) { .delete => |path| try std.testing.expectEqualSlices(u8, "bells\x07\x0b", path), else => return error.TestUnexpectedResult, } switch (commit.commands[8]) { .delete_all => {}, else => return error.TestUnexpectedResult, } }, else => return error.TestUnexpectedResult, } } test "git fast-export accepts an empty reset directly before a commit" { const stream = "feature done\n" ++ "reset refs/heads/main\n" ++ "commit refs/heads/main\n" ++ "mark :2\n" ++ "author A <[email protected]> 1 +0000\n" ++ "committer C <[email protected]> 2 +0000\n" ++ "data 0\n\n" ++ "done\n"; const input = std.Io.Reader.fixed(stream); var reader = Reader.init(std.testing.allocator, input, defaultLimits()); defer reader.deinit(); var reset_lease = (try reader.next()).?; switch (reset_lease.value) { .reset => |reset| { try std.testing.expectEqualStrings("refs/heads/main", reset.ref); try std.testing.expect(reset.target == null); }, else => return error.TestUnexpectedResult, } reset_lease.deinit(); var commit_lease = (try reader.next()).?; defer commit_lease.deinit(); switch (commit_lease.value) { .commit => |commit| try std.testing.expectEqual(@as(u64, 2), commit.mark), else => return error.TestUnexpectedResult, } } test "git fast-export accepts a reset target directly before a commit" { const stream = "feature done\n" ++ "reset refs/heads/main\n" ++ "from :2\n" ++ "commit refs/heads/main\n" ++ "mark :3\n" ++ "author A <[email protected]> 1 +0000\n" ++ "committer C <[email protected]> 2 +0000\n" ++ "data 0\n\n" ++ "done\n"; const input = std.Io.Reader.fixed(stream); var reader = Reader.init(std.testing.allocator, input, defaultLimits()); defer reader.deinit(); var reset_lease = (try reader.next()).?; switch (reset_lease.value) { .reset => |reset| try std.testing.expectEqual(@as(u64, 2), reset.target.?.mark), else => return error.TestUnexpectedResult, } reset_lease.deinit(); var commit_lease = (try reader.next()).?; defer commit_lease.deinit(); switch (commit_lease.value) { .commit => |commit| try std.testing.expectEqual(@as(u64, 3), commit.mark), else => return error.TestUnexpectedResult, } } test "git fast-export reset and tag retain metadata" { const stream = "feature done\n" ++ "reset refs/heads/main\n" ++ "from :2\n\n" ++ "tag v1.0\n" ++ "mark :3\n" ++ "from :2\n" ++ "original-oid " ++ oid ++ "\n" ++ "tagger Release Bot <[email protected]> 1700000002 +0000\n" ++ "data 3\n" ++ "tag\n" ++ "done\n"; const input = std.Io.Reader.fixed(stream); var reader = Reader.init(std.testing.allocator, input, defaultLimits()); defer reader.deinit(); var reset_lease = (try reader.next()).?; defer reset_lease.deinit(); switch (reset_lease.value) { .reset => |reset| { try std.testing.expectEqualStrings("refs/heads/main", reset.ref); try std.testing.expectEqual(@as(u64, 2), reset.target.?.mark); }, else => return error.TestUnexpectedResult, } reset_lease.deinit(); var tag_lease = (try reader.next()).?; defer tag_lease.deinit(); switch (tag_lease.value) { .tag => |tag| { try std.testing.expectEqualStrings("v1.0", tag.name); try std.testing.expectEqual(@as(u64, 3), tag.mark.?); try std.testing.expectEqual(@as(u64, 2), tag.target.mark); try std.testing.expectEqualStrings(oid, tag.original_oid.?); try std.testing.expectEqualStrings("Release Bot", tag.tagger.name); try std.testing.expectEqualSlices(u8, "tag", tag.message); }, else => return error.TestUnexpectedResult, } } fn expectNextError(bytes: []const u8, limits: Limits, expected: anyerror) !void { const input = std.Io.Reader.fixed(bytes); var reader = Reader.init(std.testing.allocator, input, limits); defer reader.deinit(); try std.testing.expectError(expected, reader.next()); } test "git fast-export rejects malformed framing and configured bounds" { try expectNextError("feature done\nblob\nmark :1\ndata -1\n", defaultLimits(), error.Malformed); try expectNextError("feature done\nblob\nmark :1\ndata 18446744073709551616\n", defaultLimits(), error.Malformed); try expectNextError("feature done\nblob\nmark :1\ndata 1\nx", defaultLimits(), error.UnexpectedEof); try expectNextError("feature done\nblob\nmark :1\ndata 1\nxx", defaultLimits(), error.Malformed); try expectNextError("feature done\nblob\nmark :x\ndata 0\n\n", defaultLimits(), error.Malformed); try expectNextError("feature relative-marks\n", defaultLimits(), error.UnsupportedFeature); try expectNextError("feature done\nblob\nmark :1\ndata 2\nxx\n", .{ .max_line = 64, .max_data = 1, .max_path = 64 }, error.DataLimitExceeded); try expectNextError("feature done\n", .{ .max_line = 3, .max_data = 64, .max_path = 64 }, error.LineLimitExceeded); try expectNextError( "feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\nD \"\\400\"\n\n", defaultLimits(), error.Malformed, ); try expectNextError( "feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\nD four\n\n", .{ .max_line = 128, .max_data = 64, .max_path = 3 }, error.PathLimitExceeded, ); const trailing = "feature done\ndone\ntrailing"; const input = std.Io.Reader.fixed(trailing); var reader = Reader.init(std.testing.allocator, input, defaultLimits()); defer reader.deinit(); var done_lease = (try reader.next()).?; done_lease.deinit(); try std.testing.expectError(error.TrailingData, reader.next()); } test "git fast-export requires a terminal done followed by eof" { const stream = "feature done\nblob\nmark :1\ndata 0\n\n"; const input = std.Io.Reader.fixed(stream); var reader = Reader.init(std.testing.allocator, input, defaultLimits()); defer reader.deinit(); var blob_lease = (try reader.next()).?; blob_lease.deinit(); try std.testing.expectError(error.MissingDone, reader.next()); } test "git fast-export rejects invalid commit semantics at the grammar boundary" { const prefix = "feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n"; try expectNextError(prefix ++ "M 160000 :1 submodule\n\n", defaultLimits(), error.UnsupportedMode); try expectNextError(prefix ++ "M 100600 :1 unknown\n\n", defaultLimits(), error.UnknownMode); try expectNextError(prefix ++ "M 100644 nonsense file\n\n", defaultLimits(), error.Malformed); try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 999999999999999999999999 +0000\n", defaultLimits(), error.Malformed); try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +2460\n", defaultLimits(), error.Malformed); try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\n\n", defaultLimits(), error.Malformed); try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n", defaultLimits(), error.UnexpectedEof); try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 4\nabc", defaultLimits(), error.UnexpectedEof); } test "git fast-export leaves mark declaration and reference resolution to the importer" { const stream = "feature done\n" ++ "blob\nmark :1\ndata 0\n\n" ++ "blob\nmark :1\ndata 0\n\n" ++ "commit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\nfrom :999\n\n" ++ "done\n"; const input = std.Io.Reader.fixed(stream); var reader = Reader.init(std.testing.allocator, input, defaultLimits()); defer reader.deinit(); for (0..2) |_| { var blob_lease = (try reader.next()).?; switch (blob_lease.value) { .blob => {}, else => return error.TestUnexpectedResult, } blob_lease.deinit(); } var commit_lease = (try reader.next()).?; defer commit_lease.deinit(); switch (commit_lease.value) { .commit => |commit| try std.testing.expectEqual(@as(u64, 999), commit.from.?.mark), else => return error.TestUnexpectedResult, } } test "git fast-export accepts exact per-commit merge and derived file command limits" { const commit_prefix = "feature done\n" ++ "commit refs/heads/main\n" ++ "mark :1\n" ++ "author A <[email protected]> 1 +0000\n" ++ "committer C <[email protected]> 2 +0000\n" ++ "data 0\n"; var merge_limits = defaultLimits(); merge_limits.max_merges_per_commit = 2; const merge_stream = commit_prefix ++ "merge :2\nmerge :3\n\n"; { const input = std.Io.Reader.fixed(merge_stream); var reader = Reader.init(std.testing.allocator, input, merge_limits); defer reader.deinit(); var lease = (try reader.next()).?; defer lease.deinit(); switch (lease.value) { .commit => |commit| try std.testing.expectEqual(@as(usize, 2), commit.merges.len), else => return error.TestUnexpectedResult, } } try expectNextError( commit_prefix ++ "merge :2\nmerge :3\nmerge :4\n\n", merge_limits, error.MergeLimitExceeded, ); // The file-command ceiling is derived, so the way to pin it at two is to // authorize exactly two commands' worth of minimum retention. No count // appears in the limits at all. var command_limits = defaultLimits(); command_limits.max_total_data = 2 * fast_export.min_retained_bytes_per_file_command; try std.testing.expectEqual( @as(usize, 2), fast_export.fileCommandCeiling(command_limits), ); const command_stream = commit_prefix ++ "D one\nD two\n\n"; { const input = std.Io.Reader.fixed(command_stream); var reader = Reader.init(std.testing.allocator, input, command_limits); defer reader.deinit(); var lease = (try reader.next()).?; defer lease.deinit(); switch (lease.value) { .commit => |commit| try std.testing.expectEqual(@as(usize, 2), commit.commands.len), else => return error.TestUnexpectedResult, } try std.testing.expect(reader.file_command_overrun == null); } { const input = std.Io.Reader.fixed(commit_prefix ++ "D one\nD two\nD three\n\n"); var reader = Reader.init(std.testing.allocator, input, command_limits); defer reader.deinit(); try std.testing.expectError(error.FileCommandLimitExceeded, reader.next()); // The refusal retains the numbers its diagnostic must name. const overrun = reader.file_command_overrun.?; try std.testing.expectEqual(@as(usize, 3), overrun.observed_at_least); try std.testing.expectEqual(@as(usize, 2), overrun.ceiling); try std.testing.expectEqual(command_limits.max_total_data, overrun.payload_budget_bytes); try std.testing.expectEqual( fast_export.min_retained_bytes_per_file_command, overrun.min_bytes_per_command, ); } // Raising the byte budget raises the ceiling with it: nothing else does. var doubled = command_limits; doubled.max_total_data *= 2; try std.testing.expectEqual(@as(usize, 4), fast_export.fileCommandCeiling(doubled)); } test "git fast-export imports a commit far above the retired 200,000 file wall" { // The retired ceiling was the benchmark corpus width promoted to policy. // A root commit that drops a whole monorepo in at once is an ordinary // source shape, and `git fast-export` lists the entire tree for it, so a // commit past that number must import rather than fail. const retired_ceiling: usize = 200_000; const command_count: usize = retired_ceiling + 1; var stream: std.ArrayList(u8) = .empty; defer stream.deinit(std.testing.allocator); try stream.appendSlice(std.testing.allocator, "feature done\n" ++ "commit refs/heads/main\n" ++ "mark :1\n" ++ "author A <[email protected]> 1 +0000\n" ++ "committer C <[email protected]> 2 +0000\n" ++ "data 0\n"); var line_buffer: [32]u8 = undefined; for (0..command_count) |index| { const line = try std.fmt.bufPrint(&line_buffer, "D f{d}\n", .{index}); try stream.appendSlice(std.testing.allocator, line); } try stream.appendSlice(std.testing.allocator, "\ndone\n"); var limits = defaultLimits(); // The production payload budget, not a value picked to make this pass. limits.max_total_data = @as(u64, 4) * 1024 * 1024 * 1024; try std.testing.expect(fast_export.fileCommandCeiling(limits) > command_count); const input = std.Io.Reader.fixed(stream.items); var reader = Reader.init(std.testing.allocator, input, limits); defer reader.deinit(); var lease = (try reader.next()).?; switch (lease.value) { .commit => |commit| try std.testing.expectEqual(command_count, commit.commands.len), else => return error.TestUnexpectedResult, } lease.deinit(); try std.testing.expect(reader.file_command_overrun == null); var done_lease = (try reader.next()).?; defer done_lease.deinit(); switch (done_lease.value) { .done => {}, else => return error.TestUnexpectedResult, } } test "git fast-export bounds total events and payload bytes before allocation" { var event_limits = defaultLimits(); event_limits.max_events = 4; // `feature done` configures the stream; progress, blobs, and done each // consume one event slot. const event_stream = "feature done\n" ++ "progress before blobs\n" ++ "blob\nmark :1\ndata 0\n\n" ++ "blob\nmark :2\ndata 0\n\n" ++ "done\n"; { const input = std.Io.Reader.fixed(event_stream); var reader = Reader.init(std.testing.allocator, input, event_limits); defer reader.deinit(); for (0..2) |_| { var lease = (try reader.next()).?; switch (lease.value) { .blob => {}, else => return error.TestUnexpectedResult, } lease.deinit(); } var done_lease = (try reader.next()).?; switch (done_lease.value) { .done => {}, else => return error.TestUnexpectedResult, } done_lease.deinit(); try std.testing.expect((try reader.next()) == null); } { const overflow_stream = "feature done\n" ++ "progress before blobs\n" ++ "blob\nmark :1\ndata 0\n\n" ++ "blob\nmark :2\ndata 0\n\n" ++ "blob\nmark :3\ndata 0\n\n" ++ "done\n"; const input = std.Io.Reader.fixed(overflow_stream); var reader = Reader.init(std.testing.allocator, input, event_limits); defer reader.deinit(); for (0..3) |_| { var lease = (try reader.next()).?; lease.deinit(); } try std.testing.expectError(error.EventLimitExceeded, reader.next()); } var payload_limits = defaultLimits(); payload_limits.max_events = 3; payload_limits.max_total_data = 3; const payload_stream = "feature done\n" ++ "blob\nmark :1\ndata 2\nab\n" ++ "blob\nmark :2\ndata 1\nc\n" ++ "done\n"; for (0..payload_stream.len + 1) |split| { var input_buffer: [1]u8 = undefined; var reader = Reader.init( std.testing.allocator, initSplitInput(payload_stream, split, &input_buffer), payload_limits, ); defer reader.deinit(); for (0..2) |_| { var lease = (try reader.next()).?; switch (lease.value) { .blob => {}, else => return error.TestUnexpectedResult, } lease.deinit(); } var done_lease = (try reader.next()).?; switch (done_lease.value) { .done => {}, else => return error.TestUnexpectedResult, } done_lease.deinit(); try std.testing.expect((try reader.next()) == null); } { const input = std.Io.Reader.fixed( "feature done\nblob\nmark :1\ndata 2\nab\nblob\nmark :2\ndata 2\ncd\n", ); var reader = Reader.init(std.testing.allocator, input, payload_limits); defer reader.deinit(); var first_blob = (try reader.next()).?; first_blob.deinit(); try std.testing.expectError(error.DataLimitExceeded, reader.next()); } } test "git fast-export commit data framing accepts both legal forms across every split" { const prefix = "feature done\n" ++ "commit refs/heads/main\n" ++ "mark :1\n" ++ "author A <[email protected]> 1 +0000\n" ++ "committer C <[email protected]> 2 +0000\n"; const suffix = "from :2\n\ndone\n"; const Form = struct { stream: []const u8, message: []const u8, }; const forms = [_]Form{ .{ .stream = prefix ++ "data 3\nabc\n" ++ suffix, .message = "abc" }, .{ .stream = prefix ++ "data 4\nabc\n" ++ suffix, .message = "abc\n" }, .{ .stream = prefix ++ "data 0\n" ++ suffix, .message = "" }, }; for (forms) |form| { const stream = form.stream; for (0..stream.len + 1) |split| { var input_buffer: [1]u8 = undefined; var reader = Reader.init(std.testing.allocator, initSplitInput(stream, split, &input_buffer), defaultLimits()); defer reader.deinit(); var commit_lease = (try reader.next()).?; switch (commit_lease.value) { .commit => |commit| { try std.testing.expectEqualStrings(form.message, commit.message); try std.testing.expectEqual(@as(u64, 2), commit.from.?.mark); }, else => return error.TestUnexpectedResult, } commit_lease.deinit(); var done_lease = (try reader.next()).?; defer done_lease.deinit(); switch (done_lease.value) { .done => {}, else => return error.TestUnexpectedResult, } } } } test "git fast-export commit data count permits an immediately following command" { const stream = "feature done\n" ++ "commit refs/heads/main\n" ++ "mark :1\n" ++ "author A <[email protected]> 1 +0000\n" ++ "committer C <[email protected]> 2 +0000\n" ++ "data 1\nxfrom :2\n\n" ++ "done\n"; const input = std.Io.Reader.fixed(stream); var reader = Reader.init(std.testing.allocator, input, defaultLimits()); defer reader.deinit(); var lease = (try reader.next()).?; defer lease.deinit(); switch (lease.value) { .commit => |commit| { try std.testing.expectEqualStrings("x", commit.message); try std.testing.expectEqual(@as(u64, 2), commit.from.?.mark); }, else => return error.TestUnexpectedResult, } }