Skip to content
zit Artifact [f507bba80b]
Login

Artifact [f507bba80b]

Artifact f507bba80b5a276bdb6e5fad8a234247cb5d01433c56ec7725d4b2701115c31c:


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);
}