Artifact a6a76e050129508a2c8ecec9050232fc90215f024999023f4696516188ab4e43:
- File
src/git_fast_export_test.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: 28580) [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 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 reports a submodule entry as a gitlink mode" { // Mode 160000 is a real thing a healthy repository exports. Refusing it is // an import decision, so the parser hands it to the importer intact. const stream = "feature done\n" ++ "commit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n" ++ "M 160000 " ++ oid ++ " nested\nM 100644 :1 keep\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 => |parsed| { try std.testing.expectEqual(@as(usize, 2), parsed.commands.len); const gitlink = parsed.commands[0].modify; try std.testing.expectEqual(fast_export.FileMode.gitlink, gitlink.mode); try std.testing.expectEqualStrings("nested", gitlink.path); try std.testing.expectEqualStrings(oid, gitlink.data.oid); // The rest of the commit still parses normally around it. try std.testing.expectEqual(fast_export.FileMode.regular, parsed.commands[1].modify.mode); try std.testing.expectEqualStrings("keep", parsed.commands[1].modify.path); }, else => return error.TestUnexpectedResult, } } 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, } }