Artifact ba3d652f3f480bb89d40800a75ee5e9e920cd0fe3ed2474237c2fec6adabf83c:
- File
src/git_import_refs_test.zig
— part of check-in
[8ae7041c2c]
at
2026-07-31 20:55:36
on branch sub-manifest
— import-git: pack the refs, and split the budget they shared with payload
zit now imports sqlite at defaults: 36,416 commits, 2,109 refs, 44m35s, no flags. Two independent walls stood in front of that.
Refs (issue 21). One file per ref made a ref's name a path, so the filesystem got a vote on which repositories are representable. sqlite has refs/remotes/origin/EQP-improvements and .../eqp-improvements at different commits -- legal, distinct git refs -- and on APFS the second landed on the first as a bare PathAlreadyExists, raised during publication after the whole import had run. Case was only the reachable instance; path length, reserved device names and each platform's own Unicode fold table are the same bug in other clothes.
They are one sorted file now, "refs", whose lines pair a canonical name with its target. Nothing reads it back -- "refs" is a reserved root the working-tree scan skips, and branches resolve through repostate's sym- tags -- so it is interop output and its job is to be legible. Sorting makes it byte-identical for a given ref set, so two imports of one repository can be compared.
This supersedes the ASCII-fold refusal from the previous commit. That was the right interim while a ref was a filename; with one file the collision is representable, so refusing it would be gratuitous. What survives is the genuine collision -- distinct sources normalizing to a byte-identical canonical name, refs/heads/./release against refs/heads/release -- which no layout can hold, and which now names both sources instead of only failing.
Budget (issue 20). The 4 GiB payload default refused sqlite's 12.2 GiB stream, and could not simply be raised: fileCommandCeiling divided the same number, so a larger allowance for a whole history was also a larger allowance for one commit's retained command list. Those bound different things and only one is memory -- payload streams and is released per event (sqlite's import held 21 MiB), while a commit's command list is held whole. The arena has max_command_arena_bytes of its own now and keeps the old 4 GiB; the cumulative total goes to 1 TiB.
The file-command ceiling is therefore structural: about 104 million commands in one commit, and no flag raises it. Its diagnostic says so rather than pointing at a knob that no longer moves it. The help text claimed the payload flag fixed both; it only ever fixed one. (user: dmestas, size: 38537) [annotate] [blame] [check-ins using]
const std = @import("std"); const artifact = @import("artifact.zig"); const editor = @import("submanifest_editor.zig"); const fast_export = @import("git_fast_export.zig"); const git_import = @import("git_import.zig"); const logstore = @import("logstore.zig"); const storage = @import("storage.zig"); const oid_a = "1111111111111111111111111111111111111111"; const oid_b = "2222222222222222222222222222222222222222"; const oid_c = "3333333333333333333333333333333333333333"; const identity = fast_export.Identity{ .name = "Import Test", .email = "[email protected]", .timestamp = 1_700_000_000, .timezone_offset_minutes = 0, }; const MemoryFile = struct { gpa: std.mem.Allocator, bytes: std.ArrayList(u8) = .empty, fn init(gpa: std.mem.Allocator) MemoryFile { return .{ .gpa = gpa }; } fn deinit(self: *MemoryFile) void { self.bytes.deinit(self.gpa); self.* = undefined; } fn storageFile(self: *MemoryFile) storage.StorageFile { return .{ .context = self, .vtable = &vtable }; } const vtable: storage.StorageFile.VTable = .{ .append = append, .pread = pread, .sync = sync, .size = size, .truncate = truncate, }; fn append(ctx: *anyopaque, data: []const u8) storage.StorageError!void { const self: *MemoryFile = @ptrCast(@alignCast(ctx)); self.bytes.appendSlice(self.gpa, data) catch return error.Io; } fn pread(ctx: *anyopaque, offset: u64, out: []u8) storage.StorageError!usize { const self: *MemoryFile = @ptrCast(@alignCast(ctx)); if (offset >= self.bytes.items.len) return 0; const start: usize = @intCast(offset); const n = @min(out.len, self.bytes.items.len - start); @memcpy(out[0..n], self.bytes.items[start..][0..n]); return n; } fn sync(_: *anyopaque) storage.StorageError!void {} fn size(ctx: *anyopaque) storage.StorageError!u64 { const self: *MemoryFile = @ptrCast(@alignCast(ctx)); return self.bytes.items.len; } fn truncate(ctx: *anyopaque, length: u64) storage.StorageError!void { const self: *MemoryFile = @ptrCast(@alignCast(ctx)); const end: usize = @intCast(length); if (end > self.bytes.items.len) return error.Io; self.bytes.shrinkRetainingCapacity(end); } }; fn rawHashName(name: []const u8, buffer: *[32]u8) ?[]const u8 { if (name.len != 40 and name.len != 64) return null; const raw = buffer[0 .. name.len / 2]; _ = std.fmt.hexToBytes(raw, name) catch return null; return raw; } const Fixture = struct { gpa: std.mem.Allocator, file: *MemoryFile, log: logstore.LogStore, fn init(gpa: std.mem.Allocator) !Fixture { const file = try gpa.create(MemoryFile); errdefer gpa.destroy(file); file.* = MemoryFile.init(gpa); errdefer file.deinit(); const log = try logstore.LogStore.open(gpa, file.storageFile(), 0); return .{ .gpa = gpa, .file = file, .log = log }; } fn deinit(self: *Fixture) void { self.log.deinit(self.gpa); self.file.deinit(); self.gpa.destroy(self.file); self.* = undefined; } fn treeStore(self: *Fixture) editor.TreeStore { return .{ .ctx = self, .load_fn = loadTreeNode, .put_fn = putTreeNode, .put_batch_fn = putTreeNodes }; } fn loadTreeNode(ctx: *anyopaque, allocator: std.mem.Allocator, name: []const u8) editor.StoreError!editor.LoadedBytes { const self: *Fixture = @ptrCast(@alignCast(ctx)); const rid = self.lookupName(name) orelse return error.MissingNode; const bytes = self.log.content(allocator, rid) catch return error.StorageFailure; return .{ .bytes = bytes orelse return error.MissingNode, .allocator = allocator }; } fn putTreeNode(ctx: *anyopaque, name: []const u8, bytes: []const u8) editor.StoreError!void { const self: *Fixture = @ptrCast(@alignCast(ctx)); var expected_buf: [64]u8 = undefined; const expected = artifact.artifactName(.sha1, bytes, &expected_buf); if (!std.mem.eql(u8, name, expected)) return error.StorageFailure; var raw_name_buf: [32]u8 = undefined; const raw_name = rawHashName(name, &raw_name_buf) orelse return error.StorageFailure; _ = self.log.putArtifact(raw_name, bytes, null) catch return error.StorageFailure; } fn putTreeNodes(ctx: *anyopaque, nodes: []const editor.StoreBatchNode) editor.StoreError!void { const self: *Fixture = @ptrCast(@alignCast(ctx)); var digests: std.ArrayList([32]u8) = .empty; defer digests.deinit(self.gpa); var items: std.ArrayList(logstore.LogStore.ArtifactBatchItem) = .empty; defer items.deinit(self.gpa); digests.ensureTotalCapacity(self.gpa, nodes.len) catch return error.OutOfMemory; items.ensureTotalCapacity(self.gpa, nodes.len) catch return error.OutOfMemory; for (nodes) |node| { var expected_buf: [64]u8 = undefined; const expected = artifact.artifactName(.sha1, node.bytes, &expected_buf); if (!std.mem.eql(u8, node.name, expected)) return error.StorageFailure; var digest: [32]u8 = undefined; const raw = rawHashName(node.name, &digest) orelse return error.StorageFailure; digests.appendAssumeCapacity(digest); items.appendAssumeCapacity(.{ .hash = digests.items[digests.items.len - 1][0..raw.len], .bytes = node.bytes, }); } self.log.putArtifactBatch(items.items) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.StorageFailure, }; } fn lookupName(self: *const Fixture, name: []const u8) ?logstore.Rid { var raw_name_buf: [32]u8 = undefined; const raw_name = rawHashName(name, &raw_name_buf) orelse return null; return self.log.lookup(raw_name); } }; // Wished API: `Importer.finalizeRefs(options) !FinalizedRefs` returns // caller-owned `controls: [][]u8`, `provenance_jsonl: []u8`, `refs` // ({ source, canonical, target }), and `primary_branch: []u8`. // `deinit(allocator)` frees all returned storage; finalization neither publishes // to a filesystem nor mutates the imported check-ins. fn task9Commit( ref: []const u8, mark: u64, original_oid: ?[]const u8, author: fast_export.Identity, committer: fast_export.Identity, from: ?fast_export.ObjectRef, merges: []const fast_export.ObjectRef, commands: []const fast_export.FileCommand, message: []const u8, ) fast_export.Event { return .{ .commit = .{ .ref = ref, .mark = mark, .original_oid = original_oid, .author = author, .committer = committer, .message = message, .from = from, .merges = merges, .commands = commands, } }; } fn task9ExpectRef(finalization: anytype, source: []const u8, canonical: []const u8, target: []const u8) !void { for (finalization.refs) |mapping| { if (!std.mem.eql(u8, source, mapping.source)) continue; try std.testing.expectEqualStrings(canonical, mapping.canonical); try std.testing.expectEqualStrings(target, mapping.target); return; } return error.TestUnexpectedResult; } fn task9HasPropagatingBranchProperty( gpa: std.mem.Allocator, finalization: anytype, target: []const u8, branch: []const u8, ) !bool { for (finalization.controls) |bytes| { var control = try artifact.parse(gpa, bytes); defer control.deinit(); try std.testing.expectEqual(artifact.ArtifactType.control, control.kind); for (control.t) |card| { const card_target = card.target orelse continue; const card_value = card.value orelse continue; if (card.sign == .propagate and std.mem.eql(u8, card.name, "branch") and std.mem.eql(u8, card_target, target) and std.mem.eql(u8, card_value, branch)) return true; } } return false; } fn task9HasSymbolicControl( gpa: std.mem.Allocator, finalization: anytype, name: []const u8, target: []const u8, ) !bool { for (finalization.controls) |bytes| { var control = try artifact.parse(gpa, bytes); defer control.deinit(); try std.testing.expectEqual(artifact.ArtifactType.control, control.kind); for (control.t) |card| { const card_target = card.target orelse continue; if (card.sign == .add and std.mem.eql(u8, card.name, name) and std.mem.eql(u8, card_target, target)) return true; } } return false; } test "git import finalizes fork merge refs into symbolic controls without duplicate check-ins" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); // Only this first feature commit changes branch context from its first // parent. Its feature child and the main merge must not gain another // propagating branch property. try importer.ingest(task9Commit("refs/heads/feature", 20, oid_b, identity, identity, .{ .mark = 10 }, &.{}, &.{}, "feature root")); try importer.ingest(task9Commit("refs/heads/feature", 21, null, identity, identity, .{ .mark = 20 }, &.{}, &.{}, "feature child")); try importer.ingest(task9Commit( "refs/heads/main", 30, oid_c, identity, identity, .{ .mark = 10 }, &.{.{ .mark = 21 }}, &.{}, "merge feature", )); try importer.ingest(.{ .reset = .{ .ref = "refs/heads/merge-alias", .target = .{ .mark = 30 } } }); try importer.ingest(.{ .reset = .{ .ref = "refs/heads/old-main", .target = .{ .mark = 10 } } }); try importer.ingest(.{ .reset = .{ .ref = "refs/tags/lightweight", .target = .{ .mark = 30 } } }); try importer.ingest(.{ .tag = .{ .name = "annotated", .mark = null, .target = .{ .mark = 21 }, .original_oid = "4444444444444444444444444444444444444444", .tagger = identity, .message = "annotated release", } }); const root_hash = try importer.objectForMark(10); const feature_root_hash = try importer.objectForMark(20); const feature_tip_hash = try importer.objectForMark(21); const merge_hash = try importer.objectForMark(30); const imported_entries = fixture.log.entries.items.len; var finalization = try importer.finalizeRefs(.{}); defer finalization.deinit(gpa); try std.testing.expectEqual(imported_entries, fixture.log.entries.items.len); try task9ExpectRef(&finalization, "refs/heads/main", "sym-heads/main", merge_hash); try task9ExpectRef(&finalization, "refs/heads/feature", "sym-heads/feature", feature_tip_hash); try task9ExpectRef(&finalization, "refs/heads/merge-alias", "sym-heads/merge-alias", merge_hash); try task9ExpectRef(&finalization, "refs/heads/old-main", "sym-heads/old-main", root_hash); try task9ExpectRef(&finalization, "refs/tags/lightweight", "sym-tags/lightweight", merge_hash); try task9ExpectRef(&finalization, "refs/tags/annotated", "sym-tags/annotated", feature_tip_hash); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-heads/main", merge_hash)); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-main", merge_hash)); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-heads/feature", feature_tip_hash)); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-heads/merge-alias", merge_hash)); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-heads/old-main", root_hash)); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-tags/lightweight", merge_hash)); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-tags/annotated", feature_tip_hash)); try std.testing.expect(try task9HasPropagatingBranchProperty(gpa, &finalization, feature_root_hash, "feature")); try std.testing.expect(!(try task9HasPropagatingBranchProperty(gpa, &finalization, feature_tip_hash, "feature"))); try std.testing.expect(!(try task9HasPropagatingBranchProperty(gpa, &finalization, merge_hash, "main"))); } test "git import finalizes remote refs with symbolic controls and provenance" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try importer.ingest(.{ .reset = .{ .ref = "refs/remotes/origin/main", .target = .{ .mark = 10 } } }); const target = try importer.objectForMark(10); var finalization = try importer.finalizeRefs(.{}); defer finalization.deinit(gpa); try task9ExpectRef(&finalization, "refs/remotes/origin/main", "sym-refs/remotes/origin/main", target); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-refs/remotes/origin/main", target)); try std.testing.expect(std.mem.indexOf( u8, finalization.provenance_jsonl, "\"source\":\"refs/remotes/origin/main\",\"canonical\":\"sym-refs/remotes/origin/main\"", ) != null); } test "git import rejects traversal and collisions in remote refs" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try std.testing.expectError( error.InvalidRef, importer.ingest(.{ .reset = .{ .ref = "refs/remotes/origin/../main", .target = .{ .mark = 10 } } }), ); try importer.ingest(.{ .reset = .{ .ref = "refs/remotes/origin/main", .target = .{ .mark = 10 } } }); try importer.ingest(.{ .reset = .{ .ref = "refs/remotes/origin/./main", .target = .{ .mark = 10 } } }); try std.testing.expectError(error.CanonicalRefCollision, importer.finalizeRefs(.{})); } test "git import keeps branch and tag leaves distinct while rejecting normalized branch collisions" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try importer.ingest(.{ .reset = .{ .ref = "refs/heads/release", .target = .{ .mark = 10 } } }); try importer.ingest(.{ .reset = .{ .ref = "refs/tags/release", .target = .{ .mark = 10 } } }); var distinct = try importer.finalizeRefs(.{}); defer distinct.deinit(gpa); const target = try importer.objectForMark(10); try task9ExpectRef(&distinct, "refs/heads/release", "sym-heads/release", target); try task9ExpectRef(&distinct, "refs/tags/release", "sym-tags/release", target); var collision_fixture = try Fixture.init(gpa); defer collision_fixture.deinit(); var collision_importer = try git_import.Importer.init(gpa, &collision_fixture.log, collision_fixture.treeStore(), .{}); defer collision_importer.deinit(); try collision_importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try collision_importer.ingest(.{ .reset = .{ .ref = "refs/heads/release", .target = .{ .mark = 10 } } }); try collision_importer.ingest(.{ .reset = .{ .ref = "refs/heads/./release", .target = .{ .mark = 10 } } }); try std.testing.expectError(error.CanonicalRefCollision, collision_importer.finalizeRefs(.{})); } test "git import keeps both refs when they differ only by case" { // sqlite carries refs/remotes/origin/EQP-improvements and // .../eqp-improvements at different commits. Both are legal, distinct git // refs, and nothing about them is ambiguous to zit: the canonical names // differ. Only the old one-file-per-ref layout could not hold both, and // the packed file removes that constraint, so this must import. const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try importer.ingest(.{ .reset = .{ .ref = "refs/remotes/origin/EQP-improvements", .target = .{ .mark = 10 } } }); try importer.ingest(.{ .reset = .{ .ref = "refs/remotes/origin/eqp-improvements", .target = .{ .mark = 10 } } }); var finalization = try importer.finalizeRefs(.{}); defer finalization.deinit(gpa); const target = try importer.objectForMark(10); try task9ExpectRef(&finalization, "refs/remotes/origin/EQP-improvements", "sym-refs/remotes/origin/EQP-improvements", target); try task9ExpectRef(&finalization, "refs/remotes/origin/eqp-improvements", "sym-refs/remotes/origin/eqp-improvements", target); try std.testing.expect(importer.ref_collision == null); } test "git import names both sources when two refs share one canonical name" { // The genuine collision: distinct sources normalizing to a byte-identical // canonical name. No layout can hold both, because there is only one name. // The error must still say which two refs produced it. const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try importer.ingest(.{ .reset = .{ .ref = "refs/heads/release", .target = .{ .mark = 10 } } }); try importer.ingest(.{ .reset = .{ .ref = "refs/heads/./release", .target = .{ .mark = 10 } } }); try std.testing.expectError(error.CanonicalRefCollision, importer.finalizeRefs(.{})); const collision = importer.ref_collision.?; try std.testing.expectEqualStrings("refs/heads/release", collision.first_source); try std.testing.expectEqualStrings("refs/heads/./release", collision.second_source); try std.testing.expectEqualStrings("sym-heads/release", collision.canonical); } test "git import finalization honors the default primary branch override" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try importer.ingest(task9Commit("refs/heads/trunk", 20, oid_b, identity, identity, .{ .mark = 10 }, &.{}, &.{}, "trunk")); const trunk_hash = try importer.objectForMark(20); var finalization = try importer.finalizeRefs(.{ .default_branch = "trunk" }); defer finalization.deinit(gpa); try std.testing.expectEqualStrings("trunk", finalization.primary_branch); try task9ExpectRef(&finalization, "refs/heads/trunk", "sym-heads/trunk", trunk_hash); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-trunk", trunk_hash)); } test "git import finalization emits deterministic fixed-order valid provenance JSONL" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); const author = fast_export.Identity{ .name = "Alice \"Author\"", .email = "[email protected]", .timestamp = 1_700_000_001, .timezone_offset_minutes = 60, }; const committer = fast_export.Identity{ .name = "Bob Committer", .email = "[email protected]", .timestamp = 1_700_000_002, .timezone_offset_minutes = -120, }; const tagger = fast_export.Identity{ .name = "Cara Tagger", .email = "[email protected]", .timestamp = 1_700_000_003, .timezone_offset_minutes = 0, }; try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, author, committer, null, &.{}, &.{}, "identity metadata")); try importer.ingest(.{ .reset = .{ .ref = "refs/tags/lightweight", .target = .{ .mark = 10 } } }); try importer.ingest(.{ .tag = .{ .name = "annotated", .mark = 20, .target = .{ .mark = 10 }, .original_oid = "4444444444444444444444444444444444444444", .tagger = tagger, .message = "release\nnotes", } }); const checkin_hash = try importer.objectForMark(10); var first = try importer.finalizeRefs(.{}); defer first.deinit(gpa); var second = try importer.finalizeRefs(.{}); defer second.deinit(gpa); try std.testing.expectEqualStrings(first.provenance_jsonl, second.provenance_jsonl); const expected = try std.fmt.allocPrint( gpa, \\{{"kind":"commit","oid":"{s}","zit":"{s}","author":{{"name":"Alice \"Author\"","email":"[email protected]","timestamp":1700000001,"timezone_offset_minutes":60}},"committer":{{"name":"Bob Committer","email":"[email protected]","timestamp":1700000002,"timezone_offset_minutes":-120}},"warning":null}} \\{{"kind":"ref","source":"refs/heads/main","canonical":"sym-heads/main","zit":"{s}","warning":null}} \\{{"kind":"ref","source":"refs/tags/lightweight","canonical":"sym-tags/lightweight","zit":"{s}","warning":"lightweight tag has no annotated metadata"}} \\{{"kind":"tag","source":"refs/tags/annotated","canonical":"sym-tags/annotated","name":"annotated","oid":"4444444444444444444444444444444444444444","target_zit":"{s}","tagger":{{"name":"Cara Tagger","email":"[email protected]","timestamp":1700000003,"timezone_offset_minutes":0}},"message":"release\nnotes","warning":null}} \\ , .{ oid_a, checkin_hash, checkin_hash, checkin_hash, checkin_hash }, ); defer gpa.free(expected); try std.testing.expectEqualStrings(expected, first.provenance_jsonl); var lines = std.mem.splitScalar(u8, first.provenance_jsonl[0 .. first.provenance_jsonl.len - 1], '\n'); while (lines.next()) |line| { var parsed = try std.json.parseFromSlice(std.json.Value, gpa, line, .{}); defer parsed.deinit(); try std.testing.expect(parsed.value == .object); } } fn task9Blob(mark: u64, original_oid: []const u8, data: []const u8) fast_export.Event { return .{ .blob = .{ .mark = mark, .original_oid = original_oid, .data = data } }; } fn task9ExpectControlsEqual(left: anytype, right: anytype) !void { try std.testing.expectEqual(left.controls.len, right.controls.len); for (left.controls, right.controls) |left_bytes, right_bytes| { try std.testing.expectEqualSlices(u8, left_bytes, right_bytes); } } test "git import provenance records concrete blob and commit mappings" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Blob(1, oid_b, "blob provenance")); const commands = [_]fast_export.FileCommand{.{ .modify = .{ .mode = .regular, .data = .{ .mark = 1 }, .path = "tracked.txt", } }}; try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &commands, "commit provenance")); const blob_hash = try importer.objectForMark(1); const checkin_hash = try importer.objectForMark(10); var finalization = try importer.finalizeRefs(.{}); defer finalization.deinit(gpa); const blob_record = try std.fmt.allocPrint(gpa, "\"kind\":\"blob\",\"oid\":\"{s}\",\"zit\":\"{s}\"", .{ oid_b, blob_hash }); defer gpa.free(blob_record); const commit_record = try std.fmt.allocPrint(gpa, "\"kind\":\"commit\",\"oid\":\"{s}\",\"zit\":\"{s}\"", .{ oid_a, checkin_hash }); defer gpa.free(commit_record); try std.testing.expect(std.mem.indexOf(u8, finalization.provenance_jsonl, blob_record) != null); try std.testing.expect(std.mem.indexOf(u8, finalization.provenance_jsonl, commit_record) != null); try std.testing.expect(std.mem.indexOf(u8, finalization.provenance_jsonl, "\"canonical\":\"sym-tags/annotated\"") == null); } test "git import finalization rejects invalid UTF-8 identity and message before output" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); const invalid = [_]u8{0xff}; const invalid_identity = fast_export.Identity{ .name = invalid[0..], .email = "[email protected]", .timestamp = 1_700_000_000, .timezone_offset_minutes = 0, }; try importer.ingest(task9Commit( "refs/heads/main", 10, oid_a, invalid_identity, invalid_identity, null, &.{}, &.{}, invalid[0..], )); try std.testing.expectError(error.InvalidUtf8, importer.finalizeRefs(.{})); } fn task9FinalizeOomNoLeak(gpa: std.mem.Allocator) !void { var fixture = try Fixture.init(std.testing.allocator); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try importer.ingest(.{ .reset = .{ .ref = "refs/tags/lightweight", .target = .{ .mark = 10 } } }); try importer.ingest(.{ .tag = .{ .name = "annotated", .mark = 20, .target = .{ .mark = 10 }, .original_oid = "4444444444444444444444444444444444444444", .tagger = identity, .message = "release", } }); var finalization = try importer.finalizeRefs(.{}); defer finalization.deinit(gpa); } test "git import finalization frees controls and ref tag ownership across allocation failures" { try std.testing.checkAllAllocationFailures(std.testing.allocator, task9FinalizeOomNoLeak, .{}); } test "git import finalization deterministically replays two independent fixtures" { const gpa = std.testing.allocator; var left_fixture = try Fixture.init(gpa); defer left_fixture.deinit(); var right_fixture = try Fixture.init(gpa); defer right_fixture.deinit(); var left = try git_import.Importer.init(gpa, &left_fixture.log, left_fixture.treeStore(), .{}); defer left.deinit(); var right = try git_import.Importer.init(gpa, &right_fixture.log, right_fixture.treeStore(), .{}); defer right.deinit(); const events = [_]fast_export.Event{ task9Blob(1, oid_b, "same blob"), task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{.{ .modify = .{ .mode = .regular, .data = .{ .mark = 1 }, .path = "tracked.txt", } }}, "same commit"), .{ .reset = .{ .ref = "refs/tags/lightweight", .target = .{ .mark = 10 } } }, .{ .tag = .{ .name = "annotated", .mark = 20, .target = .{ .mark = 10 }, .original_oid = "4444444444444444444444444444444444444444", .tagger = identity, .message = "same release", } }, }; for (events) |event| { try left.ingest(event); try right.ingest(event); } var left_finalization = try left.finalizeRefs(.{}); defer left_finalization.deinit(gpa); var right_finalization = try right.finalizeRefs(.{}); defer right_finalization.deinit(gpa); try std.testing.expectEqualSlices(u8, left_fixture.file.bytes.items, right_fixture.file.bytes.items); try task9ExpectControlsEqual(&left_finalization, &right_finalization); try std.testing.expectEqualStrings(left_finalization.provenance_jsonl, right_finalization.provenance_jsonl); } test "git import finalizes refs from exact first-parent identities when translated commits deduplicate" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); const author_main: fast_export.Identity = .{ .name = "Main Author", .email = "[email protected]", .timestamp = 1_700_000_001, .timezone_offset_minutes = 0, }; const author_feature: fast_export.Identity = .{ .name = "Feature Author", .email = "[email protected]", .timestamp = 1_700_000_002, .timezone_offset_minutes = 0, }; // These parents differ in Git identity and source ref, but their chosen // committer attribution makes their Zit check-ins byte-identical. try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, author_main, identity, null, &.{}, &.{}, "shared parent")); try importer.ingest(task9Commit("refs/heads/feature", 20, oid_b, author_feature, identity, null, &.{}, &.{}, "shared parent")); const main_parent_hash = try importer.objectForMark(10); const feature_parent_hash = try importer.objectForMark(20); try std.testing.expectEqualStrings(main_parent_hash, feature_parent_hash); // These distinct Git commits also deduplicate, yet their source refs and // exact first parents are crossed. Both branch controls must survive. try importer.ingest(task9Commit("refs/heads/feature", 30, oid_c, author_feature, identity, .{ .mark = 10 }, &.{}, &.{}, "shared child")); try importer.ingest(task9Commit( "refs/heads/main", 40, "dddddddddddddddddddddddddddddddddddddddd", author_main, identity, .{ .mark = 20 }, &.{}, &.{}, "shared child", )); const feature_child_hash = try importer.objectForMark(30); const main_child_hash = try importer.objectForMark(40); try std.testing.expectEqualStrings(feature_child_hash, main_child_hash); var finalization = try importer.finalizeRefs(.{}); defer finalization.deinit(gpa); try std.testing.expect(try task9HasPropagatingBranchProperty( gpa, &finalization, feature_child_hash, "feature", )); try std.testing.expect(try task9HasPropagatingBranchProperty( gpa, &finalization, main_child_hash, "main", )); } test "git import commit OOM leaves exact-identity mappings unpublished before retry" { const retry_oid = "dddddddddddddddddddddddddddddddddddddddd"; var fail_index: usize = 0; var saw_commit_failure = false; while (true) : (fail_index += 1) { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = fail_index }); const gpa = failing.allocator(); var fixture = try Fixture.init(std.testing.allocator); defer fixture.deinit(); var importer = git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}) catch |err| { try std.testing.expectEqual(error.OutOfMemory, err); continue; }; defer importer.deinit(); if (importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root"))) |_| {} else |err| { try std.testing.expectEqual(error.OutOfMemory, err); continue; } const entries_before = importer.retainedStateEntries(); const retry = task9Commit("refs/heads/feature", 20, retry_oid, identity, identity, .{ .mark = 10 }, &.{}, &.{}, "child"); if (importer.ingest(retry)) |_| { if (!failing.has_induced_failure) break; return error.TestUnexpectedResult; } else |err| { try std.testing.expectEqual(error.OutOfMemory, err); failing.fail_index = std.math.maxInt(usize); saw_commit_failure = true; try std.testing.expectEqual(entries_before, importer.retainedStateEntries()); try std.testing.expectError(error.UnresolvedMark, importer.objectForMark(20)); try std.testing.expectError(error.UnresolvedMark, importer.objectForOid(retry_oid)); try importer.ingest(retry); _ = try importer.objectForMark(20); _ = try importer.objectForOid(retry_oid); } } try std.testing.expect(saw_commit_failure); } fn task9BlobOidOwnershipOom(gpa: std.mem.Allocator) !void { var fixture = try Fixture.init(std.testing.allocator); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); // The OID must be duplicated into importer-owned state before the // translated artifact hash is duplicated. Every failure point in that // hand-off must leave neither ownership path leaked. try importer.ingest(task9Blob(1, oid_b, "ownership after OID duplication")); } test "git import blob OID ingestion releases ownership across allocation failures" { try std.testing.checkAllAllocationFailures(std.testing.allocator, task9BlobOidOwnershipOom, .{}); } test "git import propagates branch when a feature's first commit is a merge" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try importer.ingest(task9Commit("refs/heads/side", 11, oid_b, identity, identity, .{ .mark = 10 }, &.{}, &.{}, "side parent")); try importer.ingest(task9Commit( "refs/heads/feature", 20, oid_c, identity, identity, .{ .mark = 10 }, &.{.{ .mark = 11 }}, &.{}, "feature merge root", )); const feature_merge_hash = try importer.objectForMark(20); var finalization = try importer.finalizeRefs(.{}); defer finalization.deinit(gpa); try std.testing.expect(try task9HasPropagatingBranchProperty( gpa, &finalization, feature_merge_hash, "feature", )); } test "git import finalization rejects a missing default branch selection" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try std.testing.expectError(error.InvalidPrimaryBranch, importer.finalizeRefs(.{ .default_branch = "trunk" })); } test "git import finalization retains a parentless non-primary branch root control" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "main root")); try importer.ingest(task9Commit("refs/heads/orphan", 20, oid_b, identity, identity, null, &.{}, &.{}, "orphan root")); const orphan_hash = try importer.objectForMark(20); var finalization = try importer.finalizeRefs(.{}); defer finalization.deinit(gpa); try task9ExpectRef(&finalization, "refs/heads/orphan", "sym-heads/orphan", orphan_hash); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-heads/orphan", orphan_hash)); try std.testing.expect(try task9HasPropagatingBranchProperty(gpa, &finalization, orphan_hash, "orphan")); } test "git import finalization resolves nested annotated tags to their check-in" { const gpa = std.testing.allocator; var fixture = try Fixture.init(gpa); defer fixture.deinit(); var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{}); defer importer.deinit(); try importer.ingest(task9Commit("refs/heads/main", 10, oid_a, identity, identity, null, &.{}, &.{}, "root")); try importer.ingest(.{ .tag = .{ .name = "v1", .mark = 20, .target = .{ .mark = 10 }, .original_oid = "4444444444444444444444444444444444444444", .tagger = identity, .message = "first tag", } }); try importer.ingest(.{ .tag = .{ .name = "v2", .mark = 21, .target = .{ .mark = 20 }, .original_oid = "5555555555555555555555555555555555555555", .tagger = identity, .message = "nested tag", } }); const checkin_hash = try importer.objectForMark(10); var finalization = try importer.finalizeRefs(.{}); defer finalization.deinit(gpa); try task9ExpectRef(&finalization, "refs/tags/v1", "sym-tags/v1", checkin_hash); try task9ExpectRef(&finalization, "refs/tags/v2", "sym-tags/v2", checkin_hash); try std.testing.expect(try task9HasSymbolicControl(gpa, &finalization, "sym-tags/v2", checkin_hash)); try std.testing.expect(std.mem.indexOf(u8, finalization.provenance_jsonl, "\"source\":\"refs/tags/v2\",\"canonical\":\"sym-tags/v2\"") != null); try std.testing.expect(std.mem.indexOf(u8, finalization.provenance_jsonl, "\"target_zit\":\"") != null); }