Artifact f507bba80b5a276bdb6e5fad8a234247cb5d01433c56ec7725d4b2701115c31c:
- File
src/git_import_refs_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: 35812) [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 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); }