Skip to content
zit Artifact [4c74d9ffa1]
Login

Artifact [4c74d9ffa1]

Artifact 4c74d9ffa166673b08ff775af05596c553eddb6ad1cea69e5f548ebcae363610:


const std = @import("std");
const zit = @import("zit");
const artifact = zit.artifact;
const editor = zit.submanifest_editor;
const fast_export = zit.git_fast_export;
const git_import = zit.git_import;
const logstore = zit.logstore;
const storage = zit.storage;
const update = zit.update;
const submanifest = zit.submanifest;

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

/// A real append-only LogStore needs only this memory-backed StorageFile in
/// tests. TreeStore then uses the same log, so blobs, submanifests, and
/// check-ins exercise their production persistence boundary together.
const MemoryFile = struct {
    gpa: std.mem.Allocator,
    bytes: std.ArrayList(u8) = .empty,
    append_count: usize = 0,
    fail_append_ordinal: ?usize = null,

    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,
        .refresh_end = refreshEnd,
    };

    /// One writer by construction, so there is no other writer to exclude and
    /// no other writer's appends to re-read the length for.
    fn refreshEnd(_: *anyopaque) storage.StorageError!void {}

    fn append(ctx: *anyopaque, data: []const u8) storage.StorageError!void {
        const self: *MemoryFile = @ptrCast(@alignCast(ctx));
        self.append_count += 1;
        if (self.fail_append_ordinal) |ordinal| {
            if (self.append_count == ordinal) return error.Io;
        }
        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,
    tree_loads: usize = 0,

    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));
        self.tree_loads += 1;
        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));
        // Width-dispatched, so this stand-in store accepts whatever algorithm
        // the import names artifacts under.
        if (!artifact.verify(name, bytes)) 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| {
            // `verify` reads the algorithm off the name's own width, so this
            // stand-in store follows whatever the import mints rather than
            // pinning SHA-1 and rejecting every SHA3-256 name.
            if (!artifact.verify(node.name, node.bytes)) 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);
    }
};

const OwnedManifest = struct {
    bytes: []u8,
    manifest: artifact.Manifest,

    fn deinit(self: *OwnedManifest, gpa: std.mem.Allocator) void {
        self.manifest.deinit();
        gpa.free(self.bytes);
        self.* = undefined;
    }
};

fn loadManifest(fixture: *Fixture, gpa: std.mem.Allocator, hash: []const u8) !OwnedManifest {
    const rid = fixture.lookupName(hash) orelse return error.TestUnexpectedResult;
    const bytes = (try fixture.log.content(gpa, rid)) orelse return error.TestUnexpectedResult;
    errdefer gpa.free(bytes);
    const manifest = try artifact.parse(gpa, bytes);
    return .{ .bytes = bytes, .manifest = manifest };
}

fn blob(mark: u64, original_oid: ?[]const u8, data: []const u8) fast_export.Event {
    return .{ .blob = .{ .mark = mark, .original_oid = original_oid, .data = data } };
}

fn commit(
    mark: u64,
    original_oid: ?[]const u8,
    from: ?fast_export.ObjectRef,
    merges: []const fast_export.ObjectRef,
    commands: []const fast_export.FileCommand,
    message: []const u8,
) fast_export.Event {
    return .{ .commit = .{
        .ref = "refs/heads/main",
        .mark = mark,
        .original_oid = original_oid,
        .author = identity,
        .committer = identity,
        .message = message,
        .from = from,
        .merges = merges,
        .commands = commands,
    } };
}

const CommandSet = struct {
    gpa: std.mem.Allocator,
    paths: std.ArrayList([]u8) = .empty,
    commands: std.ArrayList(fast_export.FileCommand) = .empty,

    fn deinit(self: *CommandSet) void {
        for (self.paths.items) |path| self.gpa.free(path);
        self.paths.deinit(self.gpa);
        self.commands.deinit(self.gpa);
        self.* = undefined;
    }
};

/// k=5 groups at least eight files. The ninth selected pathname has five zero
/// bits, therefore forms a stable leaf boundary; a one-file child change in
/// the first leaf must preserve the second leaf's artifact name.
fn twoLeafCommands(gpa: std.mem.Allocator, first_mark: u64, second_mark: u64) !CommandSet {
    var set = CommandSet{ .gpa = gpa };
    errdefer set.deinit();

    var candidate: usize = 0;
    while (set.paths.items.len < 17) : (candidate += 1) {
        const count = set.paths.items.len;
        try set.paths.ensureUnusedCapacity(gpa, 1);
        try set.commands.ensureUnusedCapacity(gpa, 1);
        const path = try std.fmt.allocPrint(gpa, "p{d:0>8}.txt", .{candidate});
        if (count == 8 and artifact.zbits(path) < 5) {
            gpa.free(path);
            continue;
        }
        set.paths.appendAssumeCapacity(path);
        set.commands.appendAssumeCapacity(.{ .modify = .{
            .mode = .regular,
            .data = .{ .mark = if (count % 2 == 0) first_mark else second_mark },
            .path = path,
        } });
    }
    return set;
}

/// The name the IMPORT would give these bytes. Tests that predict a stored
/// artifact's name must follow the import's algorithm, not a fixed one.
fn nameFor(bytes: []const u8, buffer: *[64]u8) []const u8 {
    return artifact.artifactName(git_import.import_hash_algo, bytes, buffer);
}

fn rootView(root: editor.RootRun) editor.RootView {
    return switch (root) {
        .flat => |cards| .{ .flat = cards },
        .tree => |cards| .{ .tree = cards },
    };
}

fn expectRootEqual(expected: editor.RootRun, actual: *const artifact.Manifest) !void {
    switch (expected) {
        .flat => |cards| {
            try std.testing.expectEqual(@as(usize, 0), actual.s.len);
            try std.testing.expectEqual(cards.len, actual.f.len);
            for (cards, actual.f) |want, got| {
                try std.testing.expectEqualStrings(want.filename, got.filename);
                try std.testing.expectEqualStrings(want.hash.?, got.hash.?);
                try std.testing.expectEqual(want.mode, got.mode);
            }
        },
        .tree => |cards| {
            try std.testing.expectEqual(@as(usize, 0), actual.f.len);
            try std.testing.expectEqual(cards.len, actual.s.len);
            for (cards, actual.s) |want, got| {
                try std.testing.expectEqualStrings(want.filename, got.filename);
                try std.testing.expectEqualStrings(want.hash, got.hash);
            }
        },
    }
}

fn expectSharedSubmanifest(before: *const artifact.Manifest, after: *const artifact.Manifest) !void {
    try std.testing.expect(before.s.len >= 2);
    try std.testing.expect(after.s.len >= 2);
    for (before.s) |old| for (after.s) |new| {
        if (std.mem.eql(u8, old.hash, new.hash)) return;
    };
    return error.TestUnexpectedResult;
}

fn expectParents(manifest: *const artifact.Manifest, expected: []const []const u8) !void {
    try std.testing.expectEqual(@as(usize, 1), manifest.p.len);
    const parents = manifest.p[0];
    try std.testing.expectEqual(expected.len, parents.count);
    var it = parents.hashes();
    for (expected) |want| {
        const got = it.next() orelse return error.TestUnexpectedResult;
        try std.testing.expectEqualStrings(want, got);
    }
    try std.testing.expect(it.next() == null);
}

fn findFile(manifest: *const artifact.Manifest, path: []const u8) !artifact.FCard {
    for (manifest.f) |card| {
        if (std.mem.eql(u8, card.filename, path)) return card;
    }
    return error.TestUnexpectedResult;
}

/// Keeps every resolver result alive for the duration of a `flattenTree` call:
/// the artifact API borrows the slices it is handed.
const SubTrees = struct {
    gpa: std.mem.Allocator,
    fixture: *Fixture,
    loaded: std.ArrayList(OwnedManifest) = .empty,

    fn deinit(self: *SubTrees) void {
        for (self.loaded.items) |*item| item.deinit(self.gpa);
        self.loaded.deinit(self.gpa);
        self.* = undefined;
    }

    fn resolve(ctx: *anyopaque, hash: []const u8) ?artifact.SubManifestView {
        const self: *SubTrees = @ptrCast(@alignCast(ctx));
        var loaded = loadManifest(self.fixture, self.gpa, hash) catch return null;
        self.loaded.append(self.gpa, loaded) catch {
            loaded.deinit(self.gpa);
            return null;
        };
        const item = &self.loaded.items[self.loaded.items.len - 1];
        return .{ .files = item.manifest.f, .subs = item.manifest.s };
    }
};

/// Every file in a check-in, whatever root shape the importer chose. `subs`
/// must outlive the returned slice.
fn flatFiles(manifest: *artifact.Manifest, subs: *SubTrees) ![]const artifact.FCard {
    if (manifest.s.len == 0) return manifest.f;
    try manifest.flattenTree(.{ .ctx = subs, .resolveFn = SubTrees.resolve });
    return manifest.flattened.?;
}

fn expectNoFile(files: []const artifact.FCard, path: []const u8) !void {
    for (files) |card| {
        if (std.mem.eql(u8, card.filename, path)) return error.TestUnexpectedResult;
    }
}

test "git import and update share reserved path admission" {
    const Case = struct { path: []const u8, reserved: bool };
    const cases = [_]Case{
        .{ .path = ".ZiT-Checkout", .reserved = true },
        .{ .path = ".ZiT-CaChE/repo", .reserved = true },
        .{ .path = ".zit-tmp-import-user/file", .reserved = true },
        .{ .path = ".zit-checkout-user/file", .reserved = false },
        // The deleted legacy tier: ordinary admissible import content now.
        .{ .path = ".zit-import-map.jsonl", .reserved = false },
        .{ .path = "ReFs/heads/main", .reserved = false },
        .{ .path = ".zit-update-txn-user/file", .reserved = false },
        .{ .path = ".zit-update-txn", .reserved = false },
        .{ .path = ".zit-update-txns/user", .reserved = false },
    };
    for (cases) |case_| {
        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(blob(1, null, "reserved"));
        const before = fixture.log.entries.items.len;
        const commands = [_]fast_export.FileCommand{.{ .modify = .{
            .mode = .regular,
            .data = .{ .mark = 1 },
            .path = case_.path,
        } }};
        const update_rejected = blk: {
            update.validateWorktreePath(case_.path) catch |err| switch (err) {
                error.ReservedPath => break :blk true,
                else => return err,
            };
            break :blk false;
        };
        const importer_rejected = blk: {
            importer.ingest(commit(10, null, null, &.{}, &commands, "reserved")) catch |err| {
                try std.testing.expectEqual(error.ReservedPath, err);
                break :blk true;
            };
            break :blk false;
        };
        try std.testing.expectEqual(case_.reserved, update_rejected);
        try std.testing.expectEqual(update_rejected, importer_rejected);
        if (importer_rejected) {
            try std.testing.expectEqual(before, fixture.log.entries.items.len);
        } else {
            try std.testing.expect(fixture.log.entries.items.len > before);
        }
    }
}

test "git import rejects every malformed update worktree path before artifacts or mappings" {
    const cases = [_][]const u8{
        ".",
        "..",
        "a//b",
        "a/./b",
        "a/../b",
        "a\\b",
        "/absolute",
    };
    for (cases) |path| {
        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(blob(1, null, "malformed"));
        const before = fixture.log.entries.items.len;
        const commands = [_]fast_export.FileCommand{.{ .modify = .{
            .mode = .regular,
            .data = .{ .mark = 1 },
            .path = path,
        } }};

        try std.testing.expectError(error.InvalidPath, update.validateWorktreePath(path));
        try std.testing.expectError(error.InvalidPath, importer.ingest(commit(10, null, null, &.{}, &commands, "malformed")));
        try std.testing.expectEqual(before, fixture.log.entries.items.len);
        try std.testing.expectError(error.UnresolvedMark, importer.objectForMark(10));
    }
}

test "git import work counters report observed tree height" {
    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(commit(10, null, null, &.{}, &.{}, "flat"));
        try std.testing.expectEqual(@as(u8, 0), importer.workCounters().max_tree_height);
    }

    var heights: [2]u8 = undefined;
    for ([_]u4{ 5, 1 }, &heights) |k, *height| {
        var fixture = try Fixture.init(gpa);
        defer fixture.deinit();
        var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{
            .submanifest = .{ .tree = k },
        });
        defer importer.deinit();
        try importer.ingest(blob(1, null, "first"));
        try importer.ingest(blob(2, null, "second"));
        var commands = try twoLeafCommands(gpa, 1, 2);
        defer commands.deinit();
        try importer.ingest(commit(10, null, null, &.{}, commands.commands.items, "tree"));
        height.* = importer.workCounters().max_tree_height;
        try std.testing.expect(height.* != 0);
        try importer.ingest(commit(11, null, .{ .mark = 10 }, &.{}, &.{}, "unchanged tree"));
        try std.testing.expectEqual(height.*, importer.workCounters().max_tree_height);
    }
    try std.testing.expect(heights[1] > heights[0]);
}

test "git import stores blobs before check-ins and canonically reuses an unchanged submanifest" {
    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();

    var commands = try twoLeafCommands(gpa, 1, 2);
    defer commands.deinit();
    try importer.ingest(blob(1, oid_a, "first blob bytes"));
    try importer.ingest(blob(2, oid_b, "second blob bytes"));
    try importer.ingest(commit(10, oid_c, null, &.{}, commands.commands.items, "root"));

    const root_hash = try importer.objectForMark(10);
    var root = try loadManifest(&fixture, gpa, root_hash);
    defer root.deinit(gpa);
    try std.testing.expectEqual(artifact.ArtifactType.manifest, root.manifest.kind);
    try std.testing.expect(root.manifest.z != null);
    try std.testing.expect(root.manifest.s.len >= 2);

    var first_blob_name_buf: [64]u8 = undefined;
    var second_blob_name_buf: [64]u8 = undefined;
    const first_blob_hash = nameFor("first blob bytes", &first_blob_name_buf);
    const second_blob_hash = nameFor("second blob bytes", &second_blob_name_buf);
    try std.testing.expectEqualStrings(first_blob_hash, try importer.objectForMark(1));
    try std.testing.expectEqualStrings(second_blob_hash, try importer.objectForOid(oid_b));
    try std.testing.expect((fixture.lookupName(first_blob_hash) orelse return error.TestUnexpectedResult) <
        (fixture.lookupName(root_hash) orelse return error.TestUnexpectedResult));
    for (root.manifest.s) |sub| {
        try std.testing.expect((fixture.lookupName(sub.hash) orelse return error.TestUnexpectedResult) <
            (fixture.lookupName(root_hash) orelse return error.TestUnexpectedResult));
    }

    var expected_fixture = try Fixture.init(gpa);
    defer expected_fixture.deinit();
    var expected_editor = try editor.TreeEditor.init(gpa, expected_fixture.treeStore(), .{
        // The oracle must name artifacts the way the import does, or it
        // compares two different address spaces.
        .hash_algo = git_import.import_hash_algo,
        .shape = .{ .tree = .{ .k = 5, .height_max = 16 } },
    }, 1 << 20);
    defer expected_editor.deinit();
    var expected_commands: std.ArrayList(editor.TreeCommand) = .empty;
    defer expected_commands.deinit(gpa);
    for (commands.paths.items, 0..) |path, index| {
        try expected_commands.append(gpa, .{ .upsert = .{
            .filename = path,
            .hash = if (index % 2 == 0) first_blob_hash else second_blob_hash,
            .perm = null,
            .old_name = null,
            .mode = .regular,
        } });
    }
    var expected_root = try expected_editor.apply(.{ .flat = &.{} }, expected_commands.items);
    defer expected_root.deinit();
    try expectRootEqual(expected_root.root, &root.manifest);

    const child_command = [_]fast_export.FileCommand{.{ .modify = .{
        .mode = .regular,
        .data = .{ .mark = 2 },
        .path = commands.paths.items[0],
    } }};
    try importer.ingest(commit(11, null, .{ .mark = 10 }, &.{}, &child_command, "child changes one file"));
    const child_hash = try importer.objectForMark(11);
    var child = try loadManifest(&fixture, gpa, child_hash);
    defer child.deinit(gpa);
    try expectParents(&child.manifest, &.{root_hash});
    try expectSharedSubmanifest(&root.manifest, &child.manifest);

    var expected_child = try expected_editor.apply(rootView(expected_root.root), &.{.{ .upsert = .{
        .filename = commands.paths.items[0],
        .hash = second_blob_hash,
        .perm = null,
        .old_name = null,
        .mode = .regular,
    } }});
    defer expected_child.deinit();
    try expectRootEqual(expected_child.root, &child.manifest);
}

test "git import de-duplicates matching blobs and rejects conflicting or unresolved identities" {
    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(blob(1, oid_a, "identical bytes"));
    try importer.ingest(blob(2, oid_b, "identical bytes"));
    const hash_one = try importer.objectForMark(1);
    try std.testing.expectEqualStrings(hash_one, try importer.objectForMark(2));
    try std.testing.expectEqualStrings(hash_one, try importer.objectForOid(oid_a));
    try std.testing.expectEqualStrings(hash_one, try importer.objectForOid(oid_b));
    try std.testing.expectEqual(@as(usize, 1), fixture.log.entries.items.len);

    try importer.ingest(blob(3, oid_c, "first OID body"));
    try std.testing.expectError(error.ConflictingOid, importer.ingest(blob(4, oid_c, "different OID body")));
    try std.testing.expectError(error.DuplicateMark, importer.ingest(blob(1, null, "duplicate mark")));

    try importer.ingest(commit(10, null, null, &.{}, &.{}, "root"));
    try std.testing.expectError(error.DuplicateMark, importer.ingest(commit(10, null, null, &.{}, &.{}, "duplicate commit mark")));
    const unresolved_blob = [_]fast_export.FileCommand{.{ .modify = .{
        .mode = .regular,
        .data = .{ .mark = 999 },
        .path = "missing.txt",
    } }};
    try std.testing.expectError(error.UnresolvedMark, importer.ingest(commit(11, null, null, &.{}, &unresolved_blob, "missing blob")));
    try std.testing.expectError(error.UnresolvedMark, importer.ingest(commit(12, null, .{ .mark = 998 }, &.{}, &.{}, "missing parent")));
}

test "git import preserves ordered parents while empty commits retain the canonical root" {
    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(commit(100, null, null, &.{}, &.{}, "root metadata"));
    try importer.ingest(commit(101, null, .{ .mark = 100 }, &.{}, &.{}, "empty child metadata"));
    try importer.ingest(commit(102, null, .{ .mark = 100 }, &.{}, &.{}, "other parent metadata"));
    try importer.ingest(commit(103, null, .{ .mark = 101 }, &.{.{ .mark = 102 }}, &.{}, "merge metadata"));
    try importer.ingest(commit(104, null, .{ .mark = 103 }, &.{ .{ .mark = 101 }, .{ .mark = 102 }, .{ .mark = 100 } }, &.{}, "octopus metadata"));

    const root_hash = try importer.objectForMark(100);
    const child_hash = try importer.objectForMark(101);
    const other_hash = try importer.objectForMark(102);
    const merge_hash = try importer.objectForMark(103);
    const octopus_hash = try importer.objectForMark(104);
    var root = try loadManifest(&fixture, gpa, root_hash);
    defer root.deinit(gpa);
    var child = try loadManifest(&fixture, gpa, child_hash);
    defer child.deinit(gpa);
    var merge = try loadManifest(&fixture, gpa, merge_hash);
    defer merge.deinit(gpa);
    var octopus = try loadManifest(&fixture, gpa, octopus_hash);
    defer octopus.deinit(gpa);

    try std.testing.expect(!std.mem.eql(u8, root.bytes, child.bytes));
    try std.testing.expect(!std.mem.eql(u8, child.bytes, merge.bytes));
    try expectRootEqual(.{ .flat = root.manifest.f }, &child.manifest);
    try expectParents(&child.manifest, &.{root_hash});
    try expectParents(&merge.manifest, &.{ child_hash, other_hash });
    try expectParents(&octopus.manifest, &.{ merge_hash, child_hash, other_hash, root_hash });
}

test "git import translates Git file modes and classifies a submodule as a gitlink" {
    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 std.testing.expectEqual(fast_export.FileMode.regular, try git_import.modeFromGit(0o100644));
    try std.testing.expectEqual(fast_export.FileMode.executable, try git_import.modeFromGit(0o100755));
    try std.testing.expectEqual(fast_export.FileMode.symlink, try git_import.modeFromGit(0o120000));
    // A submodule is classified, not refused. Dropping it is import policy and
    // lives in the translator; only modes Git never emits are errors here.
    try std.testing.expectEqual(fast_export.FileMode.gitlink, try git_import.modeFromGit(0o160000));
    try std.testing.expectError(error.UnknownMode, git_import.modeFromGit(0o040000));

    try importer.ingest(blob(1, null, "regular"));
    try importer.ingest(blob(2, null, "executable"));
    try importer.ingest(blob(3, null, "symlink"));
    const commands = [_]fast_export.FileCommand{
        .{ .modify = .{ .mode = .regular, .data = .{ .mark = 1 }, .path = "regular.txt" } },
        .{ .modify = .{ .mode = .executable, .data = .{ .mark = 2 }, .path = "run" } },
        .{ .modify = .{ .mode = .symlink, .data = .{ .mark = 3 }, .path = "link" } },
        // No blob is ever declared for this OID: a submodule entry must be
        // omitted without its target being resolved at all.
        .{ .modify = .{ .mode = .gitlink, .data = .{ .oid = oid_a }, .path = "nested" } },
    };
    try importer.ingest(commit(10, null, null, &.{}, &commands, "modes"));
    var manifest = try loadManifest(&fixture, gpa, try importer.objectForMark(10));
    defer manifest.deinit(gpa);
    try std.testing.expectEqual(artifact.FileMode.regular, (try findFile(&manifest.manifest, "regular.txt")).mode);
    try std.testing.expectEqual(artifact.FileMode.executable, (try findFile(&manifest.manifest, "run")).mode);
    try std.testing.expectEqual(artifact.FileMode.symlink, (try findFile(&manifest.manifest, "link")).mode);
    try expectNoFile(manifest.manifest.f, "nested");
    try std.testing.expectEqual(@as(u64, 1), importer.workCounters().skipped_submodule_entries);
}

test "git import accepts a later delete, rename, or copy of an omitted submodule path" {
    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(blob(1, null, "kept"));
    const seeded = [_]fast_export.FileCommand{
        .{ .modify = .{ .mode = .regular, .data = .{ .mark = 1 }, .path = "kept.txt" } },
        .{ .modify = .{ .mode = .gitlink, .data = .{ .oid = oid_a }, .path = "nested" } },
    };
    try importer.ingest(commit(10, null, null, &.{}, &seeded, "submodule added"));

    // Git dropping the submodule names a path the zit tree never had. That is
    // not a missing path; it is the omission this import already made.
    const removed = [_]fast_export.FileCommand{.{ .delete = "nested" }};
    try importer.ingest(commit(11, null, .{ .mark = 10 }, &.{}, &removed, "submodule removed"));

    // Same for a move or a copy whose source is the omitted path: the
    // destination inherits the omission instead of failing.
    const moved = [_]fast_export.FileCommand{.{ .rename = .{ .old = "nested", .new = "moved" } }};
    try importer.ingest(commit(12, null, .{ .mark = 10 }, &.{}, &moved, "submodule moved"));
    const copied = [_]fast_export.FileCommand{.{ .copy = .{ .old = "nested", .new = "copied" } }};
    try importer.ingest(commit(13, null, .{ .mark = 10 }, &.{}, &copied, "submodule copied"));

    for ([_]u64{ 11, 12, 13 }) |mark| {
        var manifest = try loadManifest(&fixture, gpa, try importer.objectForMark(mark));
        defer manifest.deinit(gpa);
        try std.testing.expectEqual(@as(usize, 1), manifest.manifest.f.len);
        _ = try findFile(&manifest.manifest, "kept.txt");
    }

    // The tolerance reaches submodule paths only. A delete of a path nothing
    // ever declared still fails, so a genuine missing-path bug still surfaces.
    const bogus = [_]fast_export.FileCommand{.{ .delete = "never-existed" }};
    try std.testing.expectError(
        error.MissingPath,
        importer.ingest(commit(20, null, .{ .mark = 10 }, &.{}, &bogus, "bogus delete")),
    );

    // One entry omitted at :10, one re-omitted at each of the move and copy
    // destinations.
    try std.testing.expectEqual(@as(u64, 3), importer.workCounters().skipped_submodule_entries);
}

test "git import writes a path from the commit where it stops being a submodule" {
    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(blob(1, null, "kept"));
    try importer.ingest(blob(2, null, "no longer a submodule"));
    const seeded = [_]fast_export.FileCommand{
        .{ .modify = .{ .mode = .regular, .data = .{ .mark = 1 }, .path = "kept.txt" } },
        .{ .modify = .{ .mode = .gitlink, .data = .{ .oid = oid_a }, .path = "nested" } },
    };
    try importer.ingest(commit(10, null, null, &.{}, &seeded, "submodule added"));

    const replaced = [_]fast_export.FileCommand{
        .{ .modify = .{ .mode = .regular, .data = .{ .mark = 2 }, .path = "nested" } },
    };
    try importer.ingest(commit(11, null, .{ .mark = 10 }, &.{}, &replaced, "submodule became a file"));

    var after = try loadManifest(&fixture, gpa, try importer.objectForMark(11));
    defer after.deinit(gpa);
    var name_buf: [64]u8 = undefined;
    try std.testing.expectEqualStrings(
        nameFor("no longer a submodule", &name_buf),
        (try findFile(&after.manifest, "nested")).hash.?,
    );

    // And once it is a real file, deleting it is a real delete: the recorded
    // submodule path must not swallow a delete of the file that replaced it.
    const deleted = [_]fast_export.FileCommand{.{ .delete = "nested" }};
    try importer.ingest(commit(12, null, .{ .mark = 11 }, &.{}, &deleted, "file removed"));
    var last = try loadManifest(&fixture, gpa, try importer.objectForMark(12));
    defer last.deinit(gpa);
    try std.testing.expectEqual(@as(usize, 1), last.manifest.f.len);
    _ = try findFile(&last.manifest, "kept.txt");
}

test "git import removes a file that a later commit turns into a submodule" {
    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(blob(1, null, "kept"));
    try importer.ingest(blob(2, null, "a real file for now"));
    const seeded = [_]fast_export.FileCommand{
        .{ .modify = .{ .mode = .regular, .data = .{ .mark = 1 }, .path = "kept.txt" } },
        .{ .modify = .{ .mode = .regular, .data = .{ .mark = 2 }, .path = "nested" } },
    };
    try importer.ingest(commit(10, null, null, &.{}, &seeded, "file added"));

    const submoduled = [_]fast_export.FileCommand{
        .{ .modify = .{ .mode = .gitlink, .data = .{ .oid = oid_a }, .path = "nested" } },
    };
    try importer.ingest(commit(11, null, .{ .mark = 10 }, &.{}, &submoduled, "file became a submodule"));

    var after = try loadManifest(&fixture, gpa, try importer.objectForMark(11));
    defer after.deinit(gpa);
    try expectNoFile(after.manifest.f, "nested");
    _ = try findFile(&after.manifest, "kept.txt");
    try std.testing.expectEqual(@as(u64, 1), importer.workCounters().skipped_submodule_entries);
}

test "git import removes a submodule path out of a tree-shaped parent root" {
    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();

    // A tree-shaped root is the case where the parent lookup has to read stored
    // sub-manifests, so it is the one that proves the lookup is bound to a live
    // store rather than the previous commit's staging store.
    try importer.ingest(blob(1, null, "first"));
    try importer.ingest(blob(2, null, "second"));
    var set = try twoLeafCommands(gpa, 1, 2);
    defer set.deinit();
    try importer.ingest(commit(10, null, null, &.{}, set.commands.items, "wide root"));

    var seeded = try loadManifest(&fixture, gpa, try importer.objectForMark(10));
    defer seeded.deinit(gpa);
    try std.testing.expect(seeded.manifest.s.len != 0);

    const target = set.paths.items[0];
    const submoduled = [_]fast_export.FileCommand{
        .{ .modify = .{ .mode = .gitlink, .data = .{ .oid = oid_a }, .path = target } },
    };
    try importer.ingest(commit(11, null, .{ .mark = 10 }, &.{}, &submoduled, "one path became a submodule"));

    var after = try loadManifest(&fixture, gpa, try importer.objectForMark(11));
    defer after.deinit(gpa);
    var subs = SubTrees{ .gpa = gpa, .fixture = &fixture };
    defer subs.deinit();
    const files = try flatFiles(&after.manifest, &subs);
    try std.testing.expectEqual(set.paths.items.len - 1, files.len);
    try expectNoFile(files, target);

    // Deleting the now-omitted path out of the same tree-shaped root still has
    // to be accepted.
    const deleted = [_]fast_export.FileCommand{.{ .delete = target }};
    try importer.ingest(commit(12, null, .{ .mark = 11 }, &.{}, &deleted, "submodule removed"));
    try std.testing.expectEqual(@as(u64, 1), importer.workCounters().skipped_submodule_entries);
}

test "git import accepts a real Git no-from reset before its first commit" {
    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();

    // `git fast-export --all` emits this reset without a blank separator before
    // the first commit.  Reader must replay that commit, rather than consume it
    // as reset metadata, and the importer must accept the resulting null reset.
    const stream =
        "feature done\n" ++
        "blob\nmark :1\noriginal-oid debade3ceb7f902612ff347334ca7dc4357c7292\ndata 12\nlinear root\n\n" ++
        "blob\nmark :2\noriginal-oid 100b93820ade4c16225673b4ca62bb3ade63c313\ndata 6\nREADME\n" ++
        "blob\nmark :3\noriginal-oid 9aaec01376f8c39683df3f230b972ff6871ad293\ndata 19\n#!/bin/sh\necho zit\n\n" ++
        "blob\nmark :4\noriginal-oid fa48ac16de6179989a8ab8fa2b82d4d10cba9cb9\ndata 13\nunusual path\n\n" ++
        "reset refs/heads/main\n" ++
        "commit refs/heads/main\nmark :5\noriginal-oid ed9b38a10070b504cecfacd892507af78fcb24a4\n" ++
        "author Git Author <[email protected]> 981173106 +0000\n" ++
        "committer Git Committer <[email protected]> 981173106 +0000\n" ++
        "data 12\nlinear root\n" ++
        "M 120000 :2 README.link\nM 100644 :1 README\nM 100755 :3 bin/run\nM 100644 :4 \"odd/name with space\\tand-tab\"\n\n" ++
        "done\n";
    try importer.ingestFastExport(stream, .{ .max_line = 1024, .max_data = 1024, .max_path = 1024, .max_events = std.math.maxInt(u64) });
    _ = try importer.objectForMark(5);
}

test "git import skips a worktree pseudo-ref and still refuses a malformed one" {
    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();

    // `git fast-export --all` emits `reset worktrees/<name>/HEAD` for every
    // detached-HEAD linked worktree in the source -- Git's own bookkeeping,
    // not a ref of the history. Before this was skipped, one `git worktree
    // add --detach` in the source aborted the entire import with InvalidRef
    // after all the real work had already been done.
    const stream =
        "feature done\n" ++
        "blob\nmark :1\ndata 12\nlinear root\n\n" ++
        "commit refs/heads/main\nmark :2\n" ++
        "author Git Author <[email protected]> 981173106 +0000\n" ++
        "committer Git Committer <[email protected]> 981173106 +0000\n" ++
        "data 4\nroot\n" ++
        "M 100644 :1 README\n\n" ++
        "reset worktrees/wt-test/HEAD\nfrom :2\n\n" ++
        "done\n";
    const limits = fast_export.Limits{ .max_line = 1024, .max_data = 1024, .max_path = 1024, .max_events = std.math.maxInt(u64) };
    try importer.ingestFastExport(stream, limits);
    _ = try importer.objectForMark(2);
    try std.testing.expectEqual(@as(u64, 1), importer.workCounters().skipped_foreign_refs);

    // The skip is on the `refs/` prefix alone. A well-formed ref that escapes
    // its own namespace is malformed, not foreign, and must still abort --
    // otherwise the skip would have widened `canonicalRef`'s traversal
    // refusal into silence.
    var strict = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{});
    defer strict.deinit();
    try std.testing.expectError(error.InvalidRef, strict.ingest(.{ .reset = .{
        .ref = "refs/heads/../../evil",
        .target = null,
    } }));
    try std.testing.expectEqual(@as(u64, 0), strict.workCounters().skipped_foreign_refs);
}

test "git import retains exact commit identity while blob marks cannot become parents" {
    const gpa = std.testing.allocator;
    const resolved = try git_import.resolveOptions(.{});
    try std.testing.expectEqual(@as(u4, 5), resolved.shape.tree.k);

    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(blob(1, null, "a blob is not a commit parent"));
    try std.testing.expectError(
        error.UnresolvedMark,
        importer.ingest(commit(2, null, .{ .mark = 1 }, &.{}, &.{}, "blob parent")),
    );

    const same_commit = fast_export.Commit{
        .ref = "refs/heads/main",
        .mark = 10,
        .original_oid = null,
        .author = identity,
        .committer = identity,
        .message = "same check-in on two source refs",
        .from = null,
        .merges = &.{},
        .commands = &.{},
    };
    try importer.ingest(.{ .commit = same_commit });
    var feature_commit = same_commit;
    feature_commit.ref = "refs/heads/feature";
    feature_commit.mark = 11;
    try importer.ingest(.{ .commit = feature_commit });
    try std.testing.expectEqualStrings(
        try importer.objectForMark(10),
        try importer.objectForMark(11),
    );

    try importer.ingest(.{ .commit = .{
        .ref = "refs/heads/main",
        .mark = 12,
        .original_oid = null,
        .author = identity,
        .committer = identity,
        .message = "main follows the feature identity",
        .from = .{ .mark = 11 },
        .merges = &.{},
        .commands = &.{},
    } });
    const child_hash = try importer.objectForMark(12);
    const root_hash = try importer.objectForMark(10);

    var refs = try importer.finalizeRefs(.{});
    defer refs.deinit(gpa);
    // A branch control marks a segment's ROOT; every check-in above it inherits
    // the branch by propagation (repo-state §5.4). `main` here runs from the
    // parentless check-in the two source refs share up to the child, so its one
    // control targets that root — not the child, which merely continued the
    // branch under a ref fast-export happened to switch back to.
    var found_root_branch = false;
    var found_child_branch = false;
    for (refs.controls) |bytes| {
        var control = try artifact.parse(gpa, bytes);
        defer control.deinit();
        for (control.t) |card| {
            if (!std.mem.eql(u8, card.name, "branch")) continue;
            // `trunk`, not `main`: the default branch is relabelled on import,
            // so the branch card carries the destination's name. Which SOURCE
            // ref it came from is recorded separately, by the canonical tag.
            if (!std.mem.eql(u8, card.value.?, "trunk")) continue;
            if (std.mem.eql(u8, card.target.?, root_hash)) found_root_branch = true;
            if (std.mem.eql(u8, card.target.?, child_hash)) found_child_branch = true;
        }
    }
    try std.testing.expect(found_root_branch);
    try std.testing.expect(!found_child_branch);
}

// Task 8 blockers: attribution, identity canonicalization, tag-mark
// reservation, pre-epoch dates, and failure atomicity at the persistence edge.
test "git import attributes D and U cards to the selected identity" {
    const gpa = std.testing.allocator;
    const author = fast_export.Identity{
        .name = "Author",
        .email = "[email protected]",
        .timestamp = 111,
        .timezone_offset_minutes = 0,
    };
    const committer = fast_export.Identity{
        .name = "Committer",
        .email = "[email protected]",
        .timestamp = 222,
        .timezone_offset_minutes = 0,
    };

    var author_fixture = try Fixture.init(gpa);
    defer author_fixture.deinit();
    var author_importer = try git_import.Importer.init(gpa, &author_fixture.log, author_fixture.treeStore(), .{
        .attribution = .author,
    });
    defer author_importer.deinit();
    try author_importer.ingest(.{ .commit = .{
        .ref = "refs/heads/main",
        .mark = 10,
        .original_oid = null,
        .author = author,
        .committer = committer,
        .message = "author attribution",
        .from = null,
        .merges = &.{},
        .commands = &.{},
    } });
    var authored = try loadManifest(&author_fixture, gpa, try author_importer.objectForMark(10));
    defer authored.deinit(gpa);

    var default_fixture = try Fixture.init(gpa);
    defer default_fixture.deinit();
    var default_importer = try git_import.Importer.init(gpa, &default_fixture.log, default_fixture.treeStore(), .{});
    defer default_importer.deinit();
    try default_importer.ingest(.{ .commit = .{
        .ref = "refs/heads/main",
        .mark = 10,
        .original_oid = null,
        .author = author,
        .committer = committer,
        .message = "committer attribution",
        .from = null,
        .merges = &.{},
        .commands = &.{},
    } });
    var committed = try loadManifest(&default_fixture, gpa, try default_importer.objectForMark(10));
    defer committed.deinit(gpa);

    try std.testing.expectEqualStrings("Author", authored.manifest.u.?.login);
    try std.testing.expectEqualStrings("Committer", committed.manifest.u.?.login);
    try std.testing.expect(!std.mem.eql(u8, authored.manifest.d.?.timestamp, committed.manifest.d.?.timestamp));
}

test "git import canonicalizes OID spelling before deduplication and conflict detection" {
    const gpa = std.testing.allocator;
    const lower = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    const upper = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
    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(blob(1, lower, "same OID bytes"));
    try std.testing.expectEqualStrings(try importer.objectForOid(lower), try importer.objectForOid(upper));
    try std.testing.expectError(error.ConflictingOid, importer.ingest(blob(2, upper, "different bytes")));
}

test "git import reserves annotated tag marks in the shared object namespace" {
    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(commit(10, null, null, &.{}, &.{}, "tag target"));
    try importer.ingest(.{ .tag = .{
        .name = "v1",
        .mark = 20,
        .target = .{ .mark = 10 },
        .original_oid = null,
        .tagger = identity,
        .message = "release",
    } });
    try std.testing.expectError(error.DuplicateMark, importer.ingest(blob(20, null, "tag mark collision")));
    try std.testing.expectError(error.DuplicateMark, importer.ingest(commit(20, null, null, &.{}, &.{}, "tag mark collision")));
}

test "git import serializes pre-epoch timestamps distinctly from the epoch" {
    const gpa = std.testing.allocator;
    const before_epoch = fast_export.Identity{
        .name = "Before Epoch",
        .email = "[email protected]",
        .timestamp = -1,
        .timezone_offset_minutes = 0,
    };
    const at_epoch = fast_export.Identity{
        .name = "At Epoch",
        .email = "[email protected]",
        .timestamp = 0,
        .timezone_offset_minutes = 0,
    };
    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(.{ .commit = .{
        .ref = "refs/heads/main",
        .mark = 10,
        .original_oid = null,
        .author = before_epoch,
        .committer = before_epoch,
        .message = "before epoch",
        .from = null,
        .merges = &.{},
        .commands = &.{},
    } });
    try importer.ingest(.{ .commit = .{
        .ref = "refs/heads/main",
        .mark = 11,
        .original_oid = null,
        .author = at_epoch,
        .committer = at_epoch,
        .message = "at epoch",
        .from = null,
        .merges = &.{},
        .commands = &.{},
    } });
    const before_hash = try importer.objectForMark(10);
    const epoch_hash = try importer.objectForMark(11);
    var before = try loadManifest(&fixture, gpa, before_hash);
    defer before.deinit(gpa);
    var epoch = try loadManifest(&fixture, gpa, epoch_hash);
    defer epoch.deinit(gpa);

    try std.testing.expectEqualStrings("1969-12-31T23:59:59", before.manifest.d.?.timestamp);
    try std.testing.expectEqualStrings("1970-01-01T00:00:00", epoch.manifest.d.?.timestamp);
    try std.testing.expect(!std.mem.eql(u8, before_hash, epoch_hash));
}

test "git import rejects conflicting commit OIDs before persisting a candidate tree or check-in" {
    const gpa = std.testing.allocator;
    const shared_oid = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
    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(blob(1, null, "first tree body"));
    try importer.ingest(blob(2, null, "conflicting tree body"));
    var root_commands = try twoLeafCommands(gpa, 1, 2);
    defer root_commands.deinit();
    try importer.ingest(commit(10, shared_oid, null, &.{}, root_commands.commands.items, "original commit"));
    const entries_before = fixture.log.entries.items.len;
    const end_before = fixture.log.end;
    const frames_before = fixture.log.frame_count;
    const tail_before = fixture.log.last_frame_offset;

    const conflicting_commands = [_]fast_export.FileCommand{.{ .modify = .{
        .mode = .regular,
        .data = .{ .mark = 2 },
        .path = root_commands.paths.items[0],
    } }};
    try std.testing.expectError(error.ConflictingOid, importer.ingest(commit(
        11,
        shared_oid,
        null,
        &.{},
        &conflicting_commands,
        "conflicting commit",
    )));
    try std.testing.expectEqual(entries_before, fixture.log.entries.items.len);
    try std.testing.expectEqual(end_before, fixture.log.end);
    try std.testing.expectEqual(frames_before, fixture.log.frame_count);
    try std.testing.expectEqual(tail_before, fixture.log.last_frame_offset);
}
fn expectMainRef(importer: *git_import.Importer, gpa: std.mem.Allocator, expected: []const u8) !void {
    var refs = try importer.finalizeRefs(.{});
    defer refs.deinit(gpa);
    for (refs.refs) |mapping| {
        if (std.mem.eql(u8, mapping.source, "refs/heads/main")) {
            try std.testing.expectEqualStrings(expected, mapping.target);
            return;
        }
    }
    return error.TestUnexpectedResult;
}

fn expectNoPublishedMainRef(importer: *git_import.Importer, gpa: std.mem.Allocator) !void {
    var refs = importer.finalizeRefs(.{}) catch |err| {
        // A failed first commit has no branch to choose as primary. This
        // intentional finalization precondition proves no main ref published.
        try std.testing.expectEqual(error.InvalidPrimaryBranch, err);
        return;
    };
    defer refs.deinit(gpa);
    try std.testing.expectEqual(@as(usize, 0), refs.refs.len);
    return error.TestUnexpectedResult;
}

fn visibleCheckinCount(fixture: *Fixture, gpa: std.mem.Allocator) !usize {
    var count: usize = 0;
    for (1..fixture.log.entries.items.len + 1) |ordinal| {
        const rid: logstore.Rid = @intCast(ordinal);
        const bytes = (try fixture.log.content(gpa, rid)) orelse continue;
        defer gpa.free(bytes);
        var manifest = artifact.parse(gpa, bytes) catch continue;
        defer manifest.deinit();
        if (manifest.kind == .manifest) count += 1;
    }
    return count;
}

test "git import does not consume a tag mark when its target is unresolved" {
    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 std.testing.expectError(error.UnresolvedMark, importer.ingest(.{ .tag = .{
        .name = "unresolved",
        .mark = 20,
        .target = .{ .mark = 999 },
        .original_oid = null,
        .tagger = identity,
        .message = "must not reserve",
    } }));
    try importer.ingest(blob(20, null, "the failed tag mark remains available"));
    _ = try importer.objectForMark(20);
}

test "git import tag-mark allocation failures leave the mark reusable" {
    var fail_index: usize = 0;
    var saw_tag_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(commit(10, null, null, &.{}, &.{}, "tag target"))) |_| {} else |err| {
            try std.testing.expectEqual(error.OutOfMemory, err);
            continue;
        }

        const tag = fast_export.Event{ .tag = .{
            .name = "v1",
            .mark = 20,
            .target = .{ .mark = 10 },
            .original_oid = null,
            .tagger = identity,
            .message = "release",
        } };
        if (importer.ingest(tag)) |_| {
            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_tag_failure = true;
            try std.testing.expectError(error.UnresolvedMark, importer.objectForMark(20));
            try importer.ingest(blob(20, null, "the OOM tag mark remains available"));
        }
    }
    try std.testing.expect(saw_tag_failure);
}

test "git import setRef replacement OOM retains the old target and retry replaces it" {
    var fail_index: usize = 0;
    var saw_replacement_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(commit(10, null, null, &.{}, &.{}, "old main"))) |_| {} else |err| {
            try std.testing.expectEqual(error.OutOfMemory, err);
            continue;
        }
        if (importer.ingest(.{ .commit = .{
            .ref = "refs/heads/other",
            .mark = 11,
            .original_oid = null,
            .author = identity,
            .committer = identity,
            .message = "new target",
            .from = null,
            .merges = &.{},
            .commands = &.{},
        } })) |_| {} else |err| {
            try std.testing.expectEqual(error.OutOfMemory, err);
            continue;
        }
        const old_hash = try importer.objectForMark(10);
        const new_hash = try importer.objectForMark(11);
        const replacement = fast_export.Event{ .reset = .{
            .ref = "refs/heads/main",
            .target = .{ .mark = 11 },
        } };
        if (importer.ingest(replacement)) |_| {
            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_replacement_failure = true;
            try expectMainRef(&importer, gpa, old_hash);
            try importer.ingest(replacement);
            try expectMainRef(&importer, gpa, new_hash);
        }
    }
    try std.testing.expect(saw_replacement_failure);
}

test "git import OOM before commit publication leaves the staged tree unpublished and retry succeeds" {
    const commit_oid = "cccccccccccccccccccccccccccccccccccccccc";
    var fail_index: usize = 0;
    var saw_candidate_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(blob(1, null, "staged tree first body"))) |_| {} else |err| {
            try std.testing.expectEqual(error.OutOfMemory, err);
            continue;
        }
        if (importer.ingest(blob(2, null, "staged tree second body"))) |_| {} else |err| {
            try std.testing.expectEqual(error.OutOfMemory, err);
            continue;
        }
        const entries_before_candidate = fixture.log.entries.items.len;
        var commands = twoLeafCommands(gpa, 1, 2) catch |err| {
            try std.testing.expectEqual(error.OutOfMemory, err);
            continue;
        };
        defer commands.deinit();
        const candidate = commit(10, commit_oid, null, &.{}, commands.commands.items, "staged tree commit");
        if (importer.ingest(candidate)) |_| {
            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);
            try std.testing.expectError(error.UnresolvedMark, importer.objectForMark(10));
            try std.testing.expectError(error.UnresolvedMark, importer.objectForOid(commit_oid));
            try expectNoPublishedMainRef(&importer, gpa);
            const entries_after_failure = fixture.log.entries.items.len;
            try std.testing.expectEqual(entries_before_candidate, entries_after_failure);
            try std.testing.expectEqual(@as(usize, 0), try visibleCheckinCount(&fixture, gpa));
            try importer.ingest(candidate);
            const committed_hash = try importer.objectForMark(10);
            try std.testing.expectEqualStrings(committed_hash, try importer.objectForOid(commit_oid));
            try expectMainRef(&importer, gpa, committed_hash);
            var checkin = try loadManifest(&fixture, gpa, committed_hash);
            defer checkin.deinit(gpa);
            try std.testing.expect(checkin.manifest.s.len >= 2);
            try std.testing.expectEqual(
                entries_before_candidate + checkin.manifest.s.len + 1,
                fixture.log.entries.items.len,
            );
            saw_candidate_failure = true;
        }
    }
    try std.testing.expect(saw_candidate_failure);
}

test "git import batch storage failures publish no candidate artifact or mapping before retry" {
    const gpa = std.testing.allocator;
    const commit_oid = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
    for (1..4) |failure_offset| {
        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(blob(1, null, "batch storage first body"));
        try importer.ingest(blob(2, null, "batch storage second body"));
        var commands = try twoLeafCommands(gpa, 1, 2);
        defer commands.deinit();
        const candidate = commit(10, commit_oid, null, &.{}, commands.commands.items, "batch storage commit");
        const entries_before = fixture.log.entries.items.len;
        const end_before = fixture.log.end;
        const frames_before = fixture.log.frame_count;
        fixture.file.fail_append_ordinal = fixture.file.append_count + failure_offset;
        try std.testing.expectError(error.StorageFailure, importer.ingest(candidate));
        fixture.file.fail_append_ordinal = null;
        try std.testing.expectError(error.UnresolvedMark, importer.objectForMark(10));
        try std.testing.expectError(error.UnresolvedMark, importer.objectForOid(commit_oid));
        try expectNoPublishedMainRef(&importer, gpa);
        try std.testing.expectEqual(entries_before, fixture.log.entries.items.len);
        try std.testing.expectEqual(end_before, fixture.log.end);
        try std.testing.expectEqual(frames_before, fixture.log.frame_count);
        try std.testing.expectEqual(@as(usize, 0), try visibleCheckinCount(&fixture, gpa));
        try importer.ingest(candidate);
        const committed_hash = try importer.objectForMark(10);
        try std.testing.expectEqualStrings(committed_hash, try importer.objectForOid(commit_oid));
        try expectMainRef(&importer, gpa, committed_hash);
        var checkin = try loadManifest(&fixture, gpa, committed_hash);
        defer checkin.deinit(gpa);
        try std.testing.expectEqual(
            entries_before + checkin.manifest.s.len + 1,
            fixture.log.entries.items.len,
        );
        try std.testing.expectEqual(@as(usize, 1), try visibleCheckinCount(&fixture, gpa));
    }
}

fn stagedCommitOomNoLeak(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(blob(1, null, "staged tree first body"));
    try importer.ingest(blob(2, null, "staged tree second body"));
    var commands = try twoLeafCommands(gpa, 1, 2);
    defer commands.deinit();
    try importer.ingest(commit(
        10,
        "dddddddddddddddddddddddddddddddddddddddd",
        null,
        &.{},
        commands.commands.items,
        "staged tree leak check",
    ));
}

test "git import staged commit unwinds every deterministic allocation failure" {
    var no_grow = @import("zit").NoGrowAllocator.init(std.testing.allocator);
    try std.testing.checkAllAllocationFailures(no_grow.allocator(), stagedCommitOomNoLeak, .{});
}

/// `n` distinct paths spread over directories, so the name set resembles a
/// real tree rather than one flat run of near-identical strings.
fn syntheticNames(arena: std.mem.Allocator, n: usize) ![]const []const u8 {
    const names = try arena.alloc([]const u8, n);
    for (names, 0..) |*name, index| {
        name.* = try std.fmt.allocPrint(arena, "src/dir{d}/file{d}.zig", .{ index % 97, index });
    }
    return names;
}

test "git import analyzes a sub-manifest pin that beats the former fixed default" {
    const gpa = std.testing.allocator;
    var arena_state = std.heap.ArenaAllocator.init(gpa);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    // Too small to partition at all: §6.2 step 2's flat outcome must surface
    // as "no opinion", never as a manufactured pin.
    try std.testing.expectEqual(@as(?u4, null), try git_import.analyzeSubmanifestK(gpa, &.{}, .sha1));
    try std.testing.expectEqual(@as(?u4, null), try git_import.analyzeSubmanifestK(gpa, &.{"only"}, .sha1));

    // The optimum grows with the file count — the property that makes a single
    // hardcoded default wrong at both ends of the range.
    const small = (try git_import.analyzeSubmanifestK(gpa, try syntheticNames(arena, 64), .sha1)).?;
    const large = (try git_import.analyzeSubmanifestK(gpa, try syntheticNames(arena, 16_384), .sha1)).?;
    try std.testing.expect(large > small);

    // And on a set far larger than the old constant was tuned for, the pin it
    // chooses is genuinely cheaper under §6.3's own objective than 5 is.
    const names = try syntheticNames(arena, 16_384);
    const entries = try arena.alloc(artifact.FCard, names.len);
    std.mem.sort([]const u8, @constCast(names), {}, struct {
        fn less(_: void, a: []const u8, b: []const u8) bool {
            return artifact.compareBytes(a, b) == .lt;
        }
    }.less);
    for (entries, names) |*entry, name| {
        var hash_buf: [64]u8 = undefined;
        entry.* = .{
            .filename = name,
            .hash = try arena.dupe(u8, artifact.artifactName(.sha1, name, &hash_buf)),
            .perm = null,
            .old_name = null,
            .mode = .regular,
        };
    }
    const analyzed = try submanifest.cost(gpa, entries, large, .sha1);
    const former_default = try submanifest.cost(gpa, entries, 5, .sha1);
    try std.testing.expect(analyzed.total <= former_default.total);
}

/// The two facts `labelBranches` decides per commit, spelled out so a failing
/// expectation names the commit rather than an index into two parallel slices.
const ExpectedLabel = struct {
    branch: []const u8,
    is_segment_root: bool,
};

fn expectLabels(expected: []const ExpectedLabel, actual: []const git_import.BranchLabel) !void {
    try std.testing.expectEqual(expected.len, actual.len);
    for (expected, actual, 0..) |want, got, index| {
        std.testing.expectEqualStrings(want.branch, got.branch) catch |err| {
            std.debug.print("commit {d}: branch label mismatch\n", .{index});
            return err;
        };
        std.testing.expectEqual(want.is_segment_root, got.is_segment_root) catch |err| {
            std.debug.print("commit {d}: segment-root mismatch on branch {s}\n", .{ index, got.branch });
            return err;
        };
    }
}

test "git import labels the primary segment with the DESTINATION name, matching on the source name" {
    const gpa = std.testing.allocator;
    // The rename that makes a Git import openable: the head still publishes
    // `master`, which is how the primary segment is FOUND, but every check-in
    // on it is labelled `trunk`, which is what this format calls its main line
    // and therefore the only name a fresh checkout or `zit open` will look for.
    const first_parents = [_]?usize{ null, 0, 1 };
    const heads = [_]git_import.BranchHead{.{ .name = "master", .commit_index = 2 }};

    const labels = try git_import.labelBranches(gpa, &first_parents, &heads, "master", "trunk");
    defer gpa.free(labels);

    try expectLabels(&.{
        .{ .branch = "trunk", .is_segment_root = true },
        .{ .branch = "trunk", .is_segment_root = false },
        .{ .branch = "trunk", .is_segment_root = false },
    }, labels);
}

test "git import renames only the primary branch, never a side branch" {
    const gpa = std.testing.allocator;
    // A side branch keeps the name the source gave it. Only the main line is
    // renamed, because only the main line has a name this format assumes.
    const first_parents = [_]?usize{ null, 0, 0 };
    const heads = [_]git_import.BranchHead{
        .{ .name = "master", .commit_index = 1 },
        .{ .name = "feature", .commit_index = 2 },
    };

    const labels = try git_import.labelBranches(gpa, &first_parents, &heads, "master", "trunk");
    defer gpa.free(labels);

    try expectLabels(&.{
        .{ .branch = "trunk", .is_segment_root = true },
        .{ .branch = "trunk", .is_segment_root = false },
        .{ .branch = "feature", .is_segment_root = true },
    }, labels);
}

test "git import refuses to rename the primary onto a name another head already holds" {
    const gpa = std.testing.allocator;
    // The collision `finalizeRefs` exists to avoid: if the caller relabelled the
    // primary `trunk` while a SIDE branch already published `trunk`, two
    // segments would claim one name. `labelBranches` catches it rather than
    // producing a branch graph with two roots under one label — which is why
    // `finalizeRefs` keeps the source's own name in that case instead.
    const first_parents = [_]?usize{ null, 0, 0 };
    const heads = [_]git_import.BranchHead{
        .{ .name = "trunk", .commit_index = 1 },
        .{ .name = "trunk", .commit_index = 2 },
    };
    try std.testing.expectError(
        error.DuplicateBranchHead,
        git_import.labelBranches(gpa, &first_parents, &heads, "trunk", "trunk"),
    );
}

test "git import labels a branchless single-branch history with one segment root at its root" {
    const gpa = std.testing.allocator;
    // Shape A: three commits, one head. The whole point is that
    // the primary branch is labelled at all — before the claim pass this shape
    // imported with zero branch controls and every check-in untagged.
    const first_parents = [_]?usize{ null, 0, 1 };
    const heads = [_]git_import.BranchHead{.{ .name = "main", .commit_index = 2 }};

    const labels = try git_import.labelBranches(gpa, &first_parents, &heads, "main", "main");
    defer gpa.free(labels);

    try expectLabels(&.{
        .{ .branch = "main", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
        .{ .branch = "main", .is_segment_root = false },
    }, labels);
}

test "git import labels every sibling branch of a merge-free fork with its own segment root" {
    const gpa = std.testing.allocator;
    // Shape B: one base commit, three feature branches off it, and a second
    // commit on the primary. The base belongs to the primary because the
    // primary claims before any other head, not because the export stream
    // happened to emit it under some feature ref.
    const first_parents = [_]?usize{ null, 0, 0, 0, 0 };
    const heads = [_]git_import.BranchHead{
        .{ .name = "feat2", .commit_index = 2 },
        .{ .name = "main", .commit_index = 4 },
        .{ .name = "feat1", .commit_index = 1 },
        .{ .name = "feat3", .commit_index = 3 },
    };

    const labels = try git_import.labelBranches(gpa, &first_parents, &heads, "main", "main");
    defer gpa.free(labels);

    try expectLabels(&.{
        .{ .branch = "main", .is_segment_root = true },
        .{ .branch = "feat1", .is_segment_root = true },
        .{ .branch = "feat2", .is_segment_root = true },
        .{ .branch = "feat3", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
    }, labels);
}

test "git import lands merge check-ins on their first parent's segment" {
    const gpa = std.testing.allocator;
    // Shape C: five topic branches, each merged back into the primary with a
    // real merge commit. Every merge is a primary-chain commit reached through
    // its first parent, so no topic may claim one and no merge may root a
    // segment — that is what keeps the §7.2 same-branch leaf test from seeing
    // a severed child chain.
    const first_parents = [_]?usize{
        null, // 0  base
        0, //    1  topic1 work
        0, //    2  merge topic1
        2, //    3  topic2 work
        2, //    4  merge topic2
        4, //    5  topic3 work
        4, //    6  merge topic3
        6, //    7  topic4 work
        6, //    8  merge topic4
        8, //    9  topic5 work
        8, //   10  merge topic5
    };
    const heads = [_]git_import.BranchHead{
        .{ .name = "main", .commit_index = 10 },
        .{ .name = "topic1", .commit_index = 1 },
        .{ .name = "topic2", .commit_index = 3 },
        .{ .name = "topic3", .commit_index = 5 },
        .{ .name = "topic4", .commit_index = 7 },
        .{ .name = "topic5", .commit_index = 9 },
    };

    const labels = try git_import.labelBranches(gpa, &first_parents, &heads, "main", "main");
    defer gpa.free(labels);

    try expectLabels(&.{
        .{ .branch = "main", .is_segment_root = true },
        .{ .branch = "topic1", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
        .{ .branch = "topic2", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
        .{ .branch = "topic3", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
        .{ .branch = "topic4", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
        .{ .branch = "topic5", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
    }, labels);
}

test "git import roots an orphan branch at its own parentless commit" {
    const gpa = std.testing.allocator;
    // A head whose history shares no commit with the primary: its walk bottoms
    // out at its own parentless root, which is where its control belongs.
    const first_parents = [_]?usize{ null, 0, null, 2 };
    const heads = [_]git_import.BranchHead{
        .{ .name = "main", .commit_index = 1 },
        .{ .name = "docs", .commit_index = 3 },
    };

    const labels = try git_import.labelBranches(gpa, &first_parents, &heads, "main", "main");
    defer gpa.free(labels);

    try expectLabels(&.{
        .{ .branch = "main", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
        .{ .branch = "docs", .is_segment_root = true },
        .{ .branch = "docs", .is_segment_root = false },
    }, labels);
}

test "git import gives an unclaimed orphan root the primary label and its own control" {
    const gpa = std.testing.allocator;
    // Commit 2 roots a chain that was merged into the primary and whose branch
    // git deleted, so no head reaches it by first parent. Left unlabelled the
    // chain would carry no propagating tag at all; labelling it primary invents
    // no branch name and makes its merged tip a non-leaf by the §7.2 merge-edge
    // rule. Commit 4 is the merge (first parent 1, second parent 3).
    const first_parents = [_]?usize{ null, 0, null, 2, 1 };
    const heads = [_]git_import.BranchHead{.{ .name = "main", .commit_index = 4 }};

    const labels = try git_import.labelBranches(gpa, &first_parents, &heads, "main", "main");
    defer gpa.free(labels);

    try expectLabels(&.{
        .{ .branch = "main", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
        .{ .branch = "main", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
        .{ .branch = "main", .is_segment_root = false },
    }, labels);
}

test "git import claims a shallower head's segment before a head built on top of it" {
    const gpa = std.testing.allocator;
    // `release` points into the middle of `feature`'s first-parent chain. Claim
    // order is tip depth ascending, so `release` takes its own commit and
    // `feature` takes only what sits above it — both keep a segment root.
    // Claiming in stream order instead would let `feature` swallow commit 2 and
    // leave `release` with no control at all.
    const first_parents = [_]?usize{ null, 0, 0, 2, 3 };
    const heads = [_]git_import.BranchHead{
        .{ .name = "main", .commit_index = 1 },
        .{ .name = "feature", .commit_index = 4 },
        .{ .name = "release", .commit_index = 2 },
    };

    const labels = try git_import.labelBranches(gpa, &first_parents, &heads, "main", "main");
    defer gpa.free(labels);

    try expectLabels(&.{
        .{ .branch = "main", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
        .{ .branch = "release", .is_segment_root = true },
        .{ .branch = "feature", .is_segment_root = true },
        .{ .branch = "feature", .is_segment_root = false },
    }, labels);
}

test "git import breaks a two-heads-on-one-commit tie by name, independent of head order" {
    const gpa = std.testing.allocator;
    // Two non-primary heads on the same commit: exactly one segment exists to
    // claim, so the lexicographically first name wins and the loser gets no
    // branch control (it stays reachable through its own symbolic control).
    // Feeding the heads in both orders pins that the outcome is a property of
    // the names, not of export order.
    const first_parents = [_]?usize{ null, 0 };
    const forward = [_]git_import.BranchHead{
        .{ .name = "main", .commit_index = 0 },
        .{ .name = "beta", .commit_index = 1 },
        .{ .name = "alpha", .commit_index = 1 },
    };
    const reversed = [_]git_import.BranchHead{
        .{ .name = "alpha", .commit_index = 1 },
        .{ .name = "beta", .commit_index = 1 },
        .{ .name = "main", .commit_index = 0 },
    };

    const expected = [_]ExpectedLabel{
        .{ .branch = "main", .is_segment_root = true },
        .{ .branch = "alpha", .is_segment_root = true },
    };

    const forward_labels = try git_import.labelBranches(gpa, &first_parents, &forward, "main", "main");
    defer gpa.free(forward_labels);
    try expectLabels(&expected, forward_labels);

    const reversed_labels = try git_import.labelBranches(gpa, &first_parents, &reversed, "main", "main");
    defer gpa.free(reversed_labels);
    try expectLabels(&expected, reversed_labels);
}

test "git import claims the primary chain ahead of a deeper head that shares it" {
    const gpa = std.testing.allocator;
    // The primary's tip is shallower than nothing here — `topic` is the shallow
    // head — yet the base still belongs to the primary. Priority is primary
    // first, then depth, and this pins the first half of that rule.
    const first_parents = [_]?usize{ null, 0, 1 };
    const heads = [_]git_import.BranchHead{
        .{ .name = "topic", .commit_index = 1 },
        .{ .name = "main", .commit_index = 2 },
    };

    const labels = try git_import.labelBranches(gpa, &first_parents, &heads, "main", "main");
    defer gpa.free(labels);

    // `topic` sits on the primary's own first-parent chain, so it claims
    // nothing and contributes no branch control.
    try expectLabels(&.{
        .{ .branch = "main", .is_segment_root = true },
        .{ .branch = "main", .is_segment_root = false },
        .{ .branch = "main", .is_segment_root = false },
    }, labels);
}

test "git import rejects unlabelable branch input instead of asserting about it" {
    const gpa = std.testing.allocator;
    // Every one of these facts is about data a foreign repository's export
    // stream produced, so each is a rejection rather than an assert: under the
    // shipped ReleaseSafe build an assert would panic on a malformed source,
    // and under `--release=fast` it would vanish and let the bad index through.
    const first_parents = [_]?usize{ null, 0 };
    const heads = [_]git_import.BranchHead{.{ .name = "main", .commit_index = 1 }};

    try std.testing.expectError(error.InvalidPrimaryBranch, git_import.labelBranches(gpa, &first_parents, &heads, "", ""));
    try std.testing.expectError(error.InvalidPrimaryBranch, git_import.labelBranches(gpa, &first_parents, &heads, "trunk", "trunk"));

    const nameless = [_]git_import.BranchHead{.{ .name = "", .commit_index = 0 }};
    try std.testing.expectError(error.InvalidBranchHead, git_import.labelBranches(gpa, &first_parents, &nameless, "main", "main"));

    const out_of_range = [_]git_import.BranchHead{.{ .name = "main", .commit_index = 2 }};
    try std.testing.expectError(error.InvalidBranchHead, git_import.labelBranches(gpa, &first_parents, &out_of_range, "main", "main"));

    // A child ahead of its own first parent cannot be walked at all.
    const inverted = [_]?usize{ 1, null };
    try std.testing.expectError(error.InvalidCommitOrder, git_import.labelBranches(gpa, &inverted, &heads, "main", "main"));

    const duplicate = [_]git_import.BranchHead{
        .{ .name = "main", .commit_index = 1 },
        .{ .name = "main", .commit_index = 0 },
    };
    try std.testing.expectError(error.DuplicateBranchHead, git_import.labelBranches(gpa, &first_parents, &duplicate, "main", "main"));
}

test "git import refuses two heads publishing one branch name rather than emitting two roots for it" {
    const gpa = std.testing.allocator;
    // One chain with two heads on it both calling themselves "feat". Nothing
    // rejects it and the labelling itself "works": main claims the root, the
    // shallower "feat" claims 1-2, and the deeper "feat" claims 3-5 and stops
    // where it meets that claim — so the segment root at 3 has a FIRST PARENT
    // (2) that is also labelled "feat". `makeBranchControl` would then compose
    // a `*branch feat` card whose cancel targets the very `*sym-feat` that same
    // card establishes, which `artifact.composeCommitTags` asserts against: a
    // ReleaseSafe panic on repository input, and with that assert compiled out
    // under `--release=fast`, two `*branch feat` cards where repo-state §5.2's
    // UNIQUE (rid, tag) admits one. One name, one segment: rejected here so no
    // later function has to be right about it.
    //
    //   0 ← 1 ← 2 (feat) ← 3 ← 4 ← 5 (feat)
    const first_parents = [_]?usize{ null, 0, 1, 2, 3, 4 };
    const heads = [_]git_import.BranchHead{
        .{ .name = "main", .commit_index = 0 },
        .{ .name = "feat", .commit_index = 2 },
        .{ .name = "feat", .commit_index = 5 },
    };
    try std.testing.expectError(error.DuplicateBranchHead, git_import.labelBranches(gpa, &first_parents, &heads, "main", "main"));

    // …and the same shape with distinct names is accepted, so the rejection is
    // about the collision and not about the topology.
    const distinct = [_]git_import.BranchHead{
        .{ .name = "main", .commit_index = 0 },
        .{ .name = "feat", .commit_index = 2 },
        .{ .name = "feat2", .commit_index = 5 },
    };
    const labels = try git_import.labelBranches(gpa, &first_parents, &distinct, "main", "main");
    defer gpa.free(labels);
    try std.testing.expectEqualSlices(git_import.BranchLabel, &[_]git_import.BranchLabel{
        .{ .branch = "main", .is_segment_root = true },
        .{ .branch = "feat", .is_segment_root = true },
        .{ .branch = "feat", .is_segment_root = false },
        .{ .branch = "feat2", .is_segment_root = true },
        .{ .branch = "feat2", .is_segment_root = false },
        .{ .branch = "feat2", .is_segment_root = false },
    }, labels);
    // …and the parent of each of those segment roots carries a DIFFERENT
    // branch, which is precisely the fact `makeBranchControl` relies on and the
    // duplicate above breaks.
    try std.testing.expect(!std.mem.eql(u8, labels[1].branch, labels[0].branch));
    try std.testing.expect(!std.mem.eql(u8, labels[3].branch, labels[2].branch));
}

/// Labelling allocates three times — the result, the depth scratch, and the
/// claim-order scratch — and only the first outlives a success.
fn labelBranchesOomNoLeak(gpa: std.mem.Allocator) !void {
    const first_parents = [_]?usize{ null, 0, 0, 2 };
    const heads = [_]git_import.BranchHead{
        .{ .name = "main", .commit_index = 1 },
        .{ .name = "feat", .commit_index = 3 },
    };
    const labels = try git_import.labelBranches(gpa, &first_parents, &heads, "main", "main");
    gpa.free(labels);
}

test "git import branch labelling releases every allocation it makes under allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, labelBranchesOomNoLeak, .{});
}

// ---------------------------------------------------------------------------
// Fossil Git-mirror metadata classification
// ---------------------------------------------------------------------------
//
// Every case below drives the real importer over a synthetic mirror commit and
// then asks one question of the check-in it produced: are the candidate root
// files in the tree or not? The classifier has no test-only entry point, so a
// gate that stops working shows up as a file appearing or disappearing from a
// real imported history — which is the only thing that actually matters.

/// The line fossil appends to the exported `manifest`. Spelled out here rather
/// than imported so the test pins the bytes independently of the writer.
const test_sterilizer = "# Remove this line to create a well-formed Fossil manifest.\n";
/// `identity.timestamp` (1_700_000_000) as a Fossil D card.
const test_instant = "2023-11-14T22:13:20";

const MirrorFile = struct {
    path: []const u8,
    content: []const u8,
    mode: artifact.FileMode = .regular,
};

/// One deliberate defect in an otherwise perfect mirror. Each one must retain
/// EVERY present candidate; none may retain a strict subset.
const MirrorMutation = enum {
    none,
    /// Fossil's sterilizing line is missing, so the exported bytes ARE the
    /// artifact. `manifest.uuid` still names them, so this stays valid.
    sterilizer_absent,
    sterilizer_doubled,
    sterilizer_leading,
    uuid_uppercase,
    uuid_extra_newline,
    uuid_no_newline,
    uuid_truncated,
    uuid_names_other_artifact,
    manifest_unparseable,
    manifest_is_a_cluster,
    manifest_extra_file,
    manifest_missing_file,
    manifest_wrong_perm,
    manifest_wrong_content,
    manifest_duplicate_file,
    manifest_unsorted_files,
    manifest_lists_candidate,
    timestamp_next_second,
    timestamp_fractional,
    timestamp_trailing_junk,
    parent_card_on_root,
    tags_no_tag_line,
    tags_unknown_keyword,
    tags_missing_trailing_lf,
    tags_bare_branch_name,
};

const MirrorSpec = struct {
    algo: artifact.HashAlgo = .sha3_256,
    /// The subset a mirror's `manifest` setting exports, as
    /// `checkout.manifest_selectors` letters.
    parts: []const u8 = "rut",
    mode: artifact.FileMode = .regular,
    mutation: MirrorMutation = .none,
};

/// The exported bytes of one mirror commit's metadata. Any member may be
/// absent: `manifest` is a per-repository setting, not a fixed pair.
const MirrorParts = struct {
    gpa: std.mem.Allocator,
    manifest: ?[]u8 = null,
    uuid: ?[]u8 = null,
    tags: ?[]u8 = null,
    /// The artifact name the mirror's own `manifest.uuid` carries, so a child
    /// commit can build a P card that refers to this one.
    name: []u8,

    fn deinit(self: *MirrorParts) void {
        if (self.manifest) |bytes| self.gpa.free(bytes);
        if (self.uuid) |bytes| self.gpa.free(bytes);
        if (self.tags) |bytes| self.gpa.free(bytes);
        self.gpa.free(self.name);
        self.* = undefined;
    }
};

fn mirrorPermToken(mode: artifact.FileMode) ?[]const u8 {
    return switch (mode) {
        .regular => null,
        .executable => "x",
        .symlink => "l",
    };
}

fn mirrorExportMode(mode: artifact.FileMode) fast_export.FileMode {
    return switch (mode) {
        .regular => .regular,
        .executable => .executable,
        .symlink => .symlink,
    };
}

fn mirrorFCard(arena: std.mem.Allocator, algo: artifact.HashAlgo, file: MirrorFile) !artifact.FCard {
    var buf: [64]u8 = undefined;
    return .{
        .filename = file.path,
        .hash = try arena.dupe(u8, artifact.artifactName(algo, file.content, &buf)),
        .perm = mirrorPermToken(file.mode),
        .old_name = null,
        .mode = file.mode,
    };
}

fn emptyMirrorManifest(gpa: std.mem.Allocator) artifact.Manifest {
    return .{
        .arena_state = std.heap.ArenaAllocator.init(gpa),
        .kind = .manifest,
        .a = null,
        .b = null,
        .c = .{ .comment = "mirror" },
        .d = .{ .timestamp = test_instant },
        .e = null,
        .g = null,
        .h = null,
        .i = null,
        .k = null,
        .l = null,
        .n = null,
        .r = null,
        .u = .{ .login = "mirror" },
        .z = null,
        .w = null,
        .f = &.{},
        .j = &.{},
        .m = &.{},
        .p = &.{},
        .q = &.{},
        .s = &.{},
        .t = &.{},
    };
}

/// Builds the metadata a `fossil git export` mirror injects for a commit whose
/// tree is `files`, then applies `spec.mutation`. `files` must already be in
/// canonical order.
fn buildMirrorParts(
    gpa: std.mem.Allocator,
    spec: MirrorSpec,
    files: []const MirrorFile,
    parent_names: []const []const u8,
) !MirrorParts {
    var arena_state = std.heap.ArenaAllocator.init(gpa);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    var cards: std.ArrayList(artifact.FCard) = .empty;
    for (files) |file| try cards.append(arena, try mirrorFCard(arena, spec.algo, file));

    switch (spec.mutation) {
        .manifest_extra_file => try cards.append(arena, try mirrorFCard(arena, spec.algo, .{
            .path = "zz-phantom.txt",
            .content = "phantom",
        })),
        .manifest_missing_file => _ = cards.pop(),
        .manifest_wrong_perm => cards.items[0].perm = "x",
        .manifest_wrong_content => cards.items[0].hash = (try mirrorFCard(arena, spec.algo, .{
            .path = files[0].path,
            .content = "not what the tree holds",
        })).hash,
        .manifest_duplicate_file => try cards.append(arena, cards.items[cards.items.len - 1]),
        .manifest_unsorted_files => std.mem.swap(artifact.FCard, &cards.items[0], &cards.items[cards.items.len - 1]),
        .manifest_lists_candidate => try cards.append(arena, try mirrorFCard(arena, spec.algo, .{
            .path = "manifest.tags",
            .content = "branch trunk\ntag trunk\n",
        })),
        else => {},
    }
    if (spec.mutation == .manifest_wrong_perm) cards.items[0].mode = .executable;

    var manifest = emptyMirrorManifest(gpa);
    defer manifest.deinit();
    manifest.f = cards.items;
    switch (spec.mutation) {
        .timestamp_next_second => manifest.d = .{ .timestamp = "2023-11-14T22:13:21" },
        .timestamp_fractional => manifest.d = .{ .timestamp = "2023-11-14T22:13:20.500" },
        .timestamp_trailing_junk => manifest.d = .{ .timestamp = "2023-11-14T22:13:20 and a half" },
        else => {},
    }
    var effective_parents = parent_names;
    var synthetic: [1][]const u8 = undefined;
    if (spec.mutation == .parent_card_on_root and parent_names.len == 0) {
        var buf: [64]u8 = undefined;
        synthetic[0] = try arena.dupe(u8, artifact.artifactName(spec.algo, "no such parent", &buf));
        effective_parents = synthetic[0..1];
    }
    var parent_card: artifact.PCard = undefined;
    if (effective_parents.len != 0) {
        parent_card = .{
            .parents = try std.mem.join(arena, " ", effective_parents),
            .count = effective_parents.len,
        };
        manifest.p = (&parent_card)[0..1];
    }
    var cluster_card: artifact.MCard = undefined;
    if (spec.mutation == .manifest_is_a_cluster) {
        // A syntactically valid artifact that is not a check-in. The card set,
        // not a field, is what makes it one, so the whole shape is replaced.
        var buf: [64]u8 = undefined;
        cluster_card = .{ .hash = try arena.dupe(u8, artifact.artifactName(.sha1, "clustered", &buf)) };
        manifest.kind = .cluster;
        manifest.c = null;
        manifest.d = null;
        manifest.u = null;
        manifest.f = &.{};
        manifest.p = &.{};
        manifest.m = (&cluster_card)[0..1];
    }

    const artifact_bytes = try artifact.create(&manifest, gpa);
    defer gpa.free(artifact_bytes);

    var name_buf: [64]u8 = undefined;
    const name = artifact.artifactName(spec.algo, artifact_bytes, &name_buf);

    var parts = MirrorParts{ .gpa = gpa, .name = try gpa.dupe(u8, name) };
    errdefer parts.deinit();

    if (std.mem.indexOfScalar(u8, spec.parts, 'r') != null) {
        parts.manifest = switch (spec.mutation) {
            .sterilizer_absent => try gpa.dupe(u8, artifact_bytes),
            .sterilizer_doubled => try std.mem.concat(gpa, u8, &.{ artifact_bytes, test_sterilizer, test_sterilizer }),
            .sterilizer_leading => try std.mem.concat(gpa, u8, &.{ test_sterilizer, artifact_bytes }),
            .manifest_unparseable => try std.mem.concat(gpa, u8, &.{ "not an artifact at all\n", test_sterilizer }),
            else => try std.mem.concat(gpa, u8, &.{ artifact_bytes, test_sterilizer }),
        };
    }
    if (std.mem.indexOfScalar(u8, spec.parts, 'u') != null) {
        parts.uuid = switch (spec.mutation) {
            .uuid_uppercase => blk: {
                const upper = try gpa.dupe(u8, name);
                defer gpa.free(upper);
                for (upper) |*byte| byte.* = std.ascii.toUpper(byte.*);
                break :blk try std.fmt.allocPrint(gpa, "{s}\n", .{upper});
            },
            .uuid_extra_newline => try std.fmt.allocPrint(gpa, "{s}\n\n", .{name}),
            .uuid_no_newline => try gpa.dupe(u8, name),
            .uuid_truncated => try std.fmt.allocPrint(gpa, "{s}\n", .{name[0 .. name.len - 1]}),
            .uuid_names_other_artifact => blk: {
                var other_buf: [64]u8 = undefined;
                break :blk try std.fmt.allocPrint(gpa, "{s}\n", .{
                    artifact.artifactName(spec.algo, "a different artifact entirely", &other_buf),
                });
            },
            else => try std.fmt.allocPrint(gpa, "{s}\n", .{name}),
        };
    }
    if (std.mem.indexOfScalar(u8, spec.parts, 't') != null) {
        parts.tags = try gpa.dupe(u8, switch (spec.mutation) {
            .tags_no_tag_line => "branch trunk\n",
            .tags_unknown_keyword => "branch trunk\nclosed trunk\n",
            .tags_missing_trailing_lf => "branch trunk\ntag trunk",
            .tags_bare_branch_name => "trunk\ntag trunk\n",
            else => "branch trunk\ntag trunk\n",
        });
    }
    return parts;
}

/// Ingests one mirror commit: a blob per tree file, a blob per exported
/// candidate, and the commit naming all of them.
fn ingestMirrorCommit(
    gpa: std.mem.Allocator,
    importer: *git_import.Importer,
    mark_base: u64,
    commit_mark: u64,
    from: ?fast_export.ObjectRef,
    merges: []const fast_export.ObjectRef,
    files: []const MirrorFile,
    parts: *const MirrorParts,
    candidate_mode: artifact.FileMode,
) !void {
    var commands: std.ArrayList(fast_export.FileCommand) = .empty;
    defer commands.deinit(gpa);
    var mark = mark_base;
    for (files) |file| {
        try importer.ingest(blob(mark, null, file.content));
        try commands.append(gpa, .{ .modify = .{
            .mode = mirrorExportMode(file.mode),
            .data = .{ .mark = mark },
            .path = file.path,
        } });
        mark += 1;
    }
    const candidates = [_]struct { path: []const u8, bytes: ?[]const u8 }{
        .{ .path = "manifest", .bytes = parts.manifest },
        .{ .path = "manifest.uuid", .bytes = parts.uuid },
        .{ .path = "manifest.tags", .bytes = parts.tags },
    };
    for (candidates) |candidate| {
        const bytes = candidate.bytes orelse continue;
        try importer.ingest(blob(mark, null, bytes));
        try commands.append(gpa, .{ .modify = .{
            .mode = mirrorExportMode(candidate_mode),
            .data = .{ .mark = mark },
            .path = candidate.path,
        } });
        mark += 1;
    }
    // A deterministic original ID per commit, so a mirror fixture exercises
    // the same provenance records a real `git fast-export --show-original-ids`
    // stream produces.
    var oid_buf: [40]u8 = undefined;
    const commit_oid = std.fmt.bufPrint(&oid_buf, "{x:0>40}", .{commit_mark}) catch unreachable;
    try importer.ingest(commit(commit_mark, commit_oid, from, merges, commands.items, "mirror commit"));
}

/// The complete file set of the check-in `mark` produced, whatever root shape
/// the builder chose.
const ImportedTree = struct {
    gpa: std.mem.Allocator,
    checkin: OwnedManifest,
    subs: SubTrees,
    files: []const artifact.FCard,

    fn deinit(self: *ImportedTree) void {
        self.subs.deinit();
        self.checkin.deinit(self.gpa);
        self.* = undefined;
    }

    fn has(self: *const ImportedTree, path: []const u8) bool {
        for (self.files) |card| {
            if (std.mem.eql(u8, card.filename, path)) return true;
        }
        return false;
    }
};

fn importedTree(
    gpa: std.mem.Allocator,
    fixture: *Fixture,
    importer: *git_import.Importer,
    mark: u64,
) !ImportedTree {
    const hash = try importer.objectForMark(mark);
    var tree = ImportedTree{
        .gpa = gpa,
        .checkin = try loadManifest(fixture, gpa, hash),
        .subs = .{ .gpa = gpa, .fixture = fixture },
        .files = &.{},
    };
    errdefer tree.deinit();
    tree.files = try flatFiles(&tree.checkin.manifest, &tree.subs);
    return tree;
}

const mirror_tree_files = [_]MirrorFile{
    .{ .path = "alpha.txt", .content = "alpha body" },
    .{ .path = "bin/tool.sh", .content = "#!/bin/sh\n", .mode = .executable },
    .{ .path = "docs/readme.md", .content = "readme body" },
};

const MirrorCase = struct {
    name: []const u8,
    spec: MirrorSpec,
    omit: bool,
};

test "git import strips the sterilizer from both positions fossil writes it" {
    const gpa = std.testing.allocator;
    const marker = test_sterilizer;
    const sig =
        "-----BEGIN PGP SIGNATURE-----\n" ++
        "iEYEARECAAYFAk507VMACgkQoxKgR168RlEAVgCfXvTVgVXWPGlRVI4ZDt0OzYbj\n" ++
        "=LZzm\n" ++
        "-----END PGP SIGNATURE-----\n";
    const cards = "C probe\nD 2009-11-06T03:22:55\nU shaneh\n";

    // 1. Appended at the end -- an ordinary manifest. A truncation.
    {
        const bytes = try std.mem.concat(gpa, u8, &.{ cards, marker });
        defer gpa.free(bytes);
        try std.testing.expectEqualStrings(cards, git_import.stripMirrorSterilizer(bytes));
    }

    // 2. Spliced before the signature block -- a PGP-clearsigned manifest.
    //    This is NOT a truncation, and a strip that only handled case 1 left
    //    the marker in place, so the bytes never hashed to the origin's name
    //    and the pair could never be proven. On a real sqlite mirror that was
    //    1,076 check-ins whose mirror metadata was retained forever.
    {
        const bytes = try std.mem.concat(gpa, u8, &.{ cards, marker, sig });
        defer gpa.free(bytes);
        const want = try std.mem.concat(gpa, u8, &.{ cards, sig });
        defer gpa.free(want);
        try std.testing.expectEqualStrings(want, git_import.stripMirrorSterilizer(bytes));
    }

    // 3. Anywhere else is left alone: fossil never writes it there, and a blob
    //    that merely contains the line must not be reshaped into one that
    //    proves.
    {
        const bytes = try std.mem.concat(gpa, u8, &.{ marker, cards });
        defer gpa.free(bytes);
        const want = try gpa.dupe(u8, bytes);
        defer gpa.free(want);
        try std.testing.expectEqualStrings(want, git_import.stripMirrorSterilizer(bytes));
    }

    // 4. Absent: returned whole.
    {
        const bytes = try gpa.dupe(u8, cards);
        defer gpa.free(bytes);
        try std.testing.expectEqualStrings(cards, git_import.stripMirrorSterilizer(bytes));
    }
}

test "git import mirror pair omits only a fully proven candidate set" {
    const gpa = std.testing.allocator;
    const cases = [_]MirrorCase{
        // The two supported Fossil hash policies, and the subsets the
        // `manifest` setting can export.
        .{ .name = "sha3 rut", .spec = .{}, .omit = true },
        .{ .name = "sha1 rut", .spec = .{ .algo = .sha1 }, .omit = true },
        .{ .name = "ru", .spec = .{ .parts = "ru" }, .omit = true },
        .{ .name = "sha1 ru", .spec = .{ .algo = .sha1, .parts = "ru" }, .omit = true },
        // A dependent or lone member is never dropped on its own.
        .{ .name = "r alone", .spec = .{ .parts = "r" }, .omit = false },
        .{ .name = "u alone", .spec = .{ .parts = "u" }, .omit = false },
        .{ .name = "t alone", .spec = .{ .parts = "t" }, .omit = false },
        .{ .name = "rt without uuid", .spec = .{ .parts = "rt" }, .omit = false },
        .{ .name = "ut without manifest", .spec = .{ .parts = "ut" }, .omit = false },
        // Fossil's sterilizing line is accepted present or absent, and
        // nowhere else.
        .{ .name = "sterilizer absent", .spec = .{ .mutation = .sterilizer_absent }, .omit = true },
        .{ .name = "sterilizer doubled", .spec = .{ .mutation = .sterilizer_doubled }, .omit = false },
        .{ .name = "sterilizer leading", .spec = .{ .mutation = .sterilizer_leading }, .omit = false },
        // UUID syntax.
        .{ .name = "uuid uppercase", .spec = .{ .mutation = .uuid_uppercase }, .omit = false },
        .{ .name = "uuid extra newline", .spec = .{ .mutation = .uuid_extra_newline }, .omit = false },
        .{ .name = "uuid without newline", .spec = .{ .mutation = .uuid_no_newline }, .omit = true },
        .{ .name = "uuid truncated", .spec = .{ .mutation = .uuid_truncated }, .omit = false },
        .{ .name = "uuid names another artifact", .spec = .{ .mutation = .uuid_names_other_artifact }, .omit = false },
        // Manifest shape.
        .{ .name = "manifest unparseable", .spec = .{ .mutation = .manifest_unparseable }, .omit = false },
        .{ .name = "manifest is a cluster artifact", .spec = .{ .mutation = .manifest_is_a_cluster }, .omit = false },
        // Tree equality, in both directions and on every tuple member.
        .{ .name = "manifest lists an extra file", .spec = .{ .mutation = .manifest_extra_file }, .omit = false },
        .{ .name = "manifest omits a file", .spec = .{ .mutation = .manifest_missing_file }, .omit = false },
        .{ .name = "manifest disagrees on permission", .spec = .{ .mutation = .manifest_wrong_perm }, .omit = false },
        .{ .name = "manifest disagrees on content", .spec = .{ .mutation = .manifest_wrong_content }, .omit = false },
        .{ .name = "manifest duplicates a path", .spec = .{ .mutation = .manifest_duplicate_file }, .omit = false },
        .{ .name = "manifest is unsorted", .spec = .{ .mutation = .manifest_unsorted_files }, .omit = false },
        .{ .name = "manifest lists a candidate", .spec = .{ .mutation = .manifest_lists_candidate }, .omit = false },
        // Parents and instant.
        .{ .name = "root commit with a P card", .spec = .{ .mutation = .parent_card_on_root }, .omit = false },
        // Adopted, not refused. The origin's instant is proven authentic by
        // gate 3, and Git's is the lossy carrier of it -- fossil's export
        // truncates a floating-point Julian Day, so a whole-second origin
        // timestamp lands a second EARLY about half the time, which is this
        // case exactly. The import mints the origin's instant, so the manifest
        // is redundant and drops. Before adoption this retained, and on a real
        // sqlite mirror that was 4,538 commits.
        .{ .name = "instant one second later", .spec = .{ .mutation = .timestamp_next_second }, .omit = true },
        .{ .name = "instant with fossil's fraction", .spec = .{ .mutation = .timestamp_fractional }, .omit = true },
        .{ .name = "instant with trailing text", .spec = .{ .mutation = .timestamp_trailing_junk }, .omit = false },
        // The dependent member's own grammar.
        .{ .name = "tags without a tag line", .spec = .{ .mutation = .tags_no_tag_line }, .omit = false },
        .{ .name = "tags with an unknown keyword", .spec = .{ .mutation = .tags_unknown_keyword }, .omit = false },
        .{ .name = "tags without a trailing LF", .spec = .{ .mutation = .tags_missing_trailing_lf }, .omit = false },
        .{ .name = "tags with a bare branch name", .spec = .{ .mutation = .tags_bare_branch_name }, .omit = false },
        // A candidate that is not an ordinary file is not a candidate.
        .{ .name = "executable candidates", .spec = .{ .mode = .executable }, .omit = false },
        .{ .name = "symlink candidates", .spec = .{ .mode = .symlink }, .omit = false },
    };

    for (cases) |case| {
        errdefer std.debug.print("mirror case: {s}\n", .{case.name});
        var fixture = try Fixture.init(gpa);
        defer fixture.deinit();
        var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{ .submanifest = .flat });
        defer importer.deinit();

        var parts = try buildMirrorParts(gpa, case.spec, &mirror_tree_files, &.{});
        defer parts.deinit();
        try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &mirror_tree_files, &parts, case.spec.mode);

        var tree = try importedTree(gpa, &fixture, &importer, 100);
        defer tree.deinit();

        // Project content is never touched by any outcome.
        for (mirror_tree_files) |file| try std.testing.expect(tree.has(file.path));

        // All-or-nothing: no outcome may drop a strict subset.
        var present: usize = 0;
        var expected: usize = 0;
        inline for (.{ "manifest", "manifest.uuid", "manifest.tags" }, .{ 'r', 'u', 't' }) |path, letter| {
            if (std.mem.indexOfScalar(u8, case.spec.parts, letter) != null) {
                expected += 1;
                if (tree.has(path)) present += 1;
            } else {
                try std.testing.expect(!tree.has(path));
            }
        }
        try std.testing.expectEqual(if (case.omit) @as(usize, 0) else expected, present);

        const census = importer.mirrorCensus();
        try std.testing.expectEqual(@as(u64, 1), census.commits_with_candidates);
        try std.testing.expectEqual(@as(u64, if (case.omit) 1 else 0), census.commits_validated);
        try std.testing.expectEqual(@as(u64, if (case.omit) 0 else 1), census.commits_retained);
    }
}

test "git import sub-manifest mode drops on the pair proof, not on reproduction" {
    const gpa = std.testing.allocator;
    // Sub-manifest mode restructures the file set behind S cards, so its
    // check-in is a different artifact from the origin's by construction and
    // reproduces nothing. Gates 5 to 7 prove reproduction, so requiring them
    // here only strands metadata the mode has no use for -- 5,906 commits'
    // worth on a real sqlite mirror, and every one of libfossil's 2,065.
    //
    // These four fail ONLY those gates. Each retains under flat mode above.
    const reproduction_only = [_]MirrorCase{
        .{ .name = "manifest disagrees on content", .spec = .{ .mutation = .manifest_wrong_content }, .omit = true },
        .{ .name = "root commit with a P card", .spec = .{ .mutation = .parent_card_on_root }, .omit = true },
    };
    // ...while gate 3's cryptographic tie still governs, because it is what
    // tells mirror metadata apart from a project file that merely happens to
    // be called `manifest`. Relaxing the gates above must not reach it.
    const proof_governs = [_]MirrorCase{
        // Both rejected by the PARSE rather than by reproduction: a D card
        // with trailing text is not a timestamp, and fossil's F run is
        // sorted, so an unsorted one is not a manifest to begin with.
        .{ .name = "instant with trailing text", .spec = .{ .mutation = .timestamp_trailing_junk }, .omit = false },
        .{ .name = "manifest is unsorted", .spec = .{ .mutation = .manifest_unsorted_files }, .omit = false },
        .{ .name = "uuid names another artifact", .spec = .{ .mutation = .uuid_names_other_artifact }, .omit = false },
        .{ .name = "uuid truncated", .spec = .{ .mutation = .uuid_truncated }, .omit = false },
        .{ .name = "sterilizer leading", .spec = .{ .mutation = .sterilizer_leading }, .omit = false },
        .{ .name = "tags with a bare branch name", .spec = .{ .mutation = .tags_bare_branch_name }, .omit = false },
        .{ .name = "manifest without a uuid", .spec = .{ .parts = "rt" }, .omit = false },
    };

    for (reproduction_only ++ proof_governs) |case| {
        errdefer std.debug.print("sub-manifest mirror case: {s}\n", .{case.name});
        var fixture = try Fixture.init(gpa);
        defer fixture.deinit();
        // Default options: sub-manifest mode is the default.
        var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{});
        defer importer.deinit();

        var parts = try buildMirrorParts(gpa, case.spec, &mirror_tree_files, &.{});
        defer parts.deinit();
        try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &mirror_tree_files, &parts, case.spec.mode);

        var tree = try importedTree(gpa, &fixture, &importer, 100);
        defer tree.deinit();

        for (mirror_tree_files) |file| try std.testing.expect(tree.has(file.path));

        var present: usize = 0;
        var expected: usize = 0;
        inline for (.{ "manifest", "manifest.uuid", "manifest.tags" }, .{ 'r', 'u', 't' }) |path, letter| {
            if (std.mem.indexOfScalar(u8, case.spec.parts, letter) != null) {
                expected += 1;
                if (tree.has(path)) present += 1;
            } else {
                try std.testing.expect(!tree.has(path));
            }
        }
        try std.testing.expectEqual(if (case.omit) @as(usize, 0) else expected, present);
    }
}

test "git import mirror pair keeps a genuine project file named manifest" {
    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();

    // A project whose own top-level `manifest` is ordinary text, alongside
    // files whose names merely resemble the candidates.
    const files = [_]MirrorFile{
        .{ .path = "alpha.txt", .content = "alpha body" },
        .{ .path = "manifest", .content = "name = demo\nversion = 1\n" },
        .{ .path = "manifest.uuid", .content = "not a hash\n" },
        .{ .path = "src/manifest", .content = "nested and never a candidate" },
    };
    var commands: std.ArrayList(fast_export.FileCommand) = .empty;
    defer commands.deinit(gpa);
    for (files, 1..) |file, mark| {
        try importer.ingest(blob(@intCast(mark), null, file.content));
        try commands.append(gpa, .{ .modify = .{
            .mode = .regular,
            .data = .{ .mark = @intCast(mark) },
            .path = file.path,
        } });
    }
    try importer.ingest(commit(100, null, null, &.{}, commands.items, "ordinary project"));

    var tree = try importedTree(gpa, &fixture, &importer, 100);
    defer tree.deinit();
    for (files) |file| try std.testing.expect(tree.has(file.path));

    const census = importer.mirrorCensus();
    try std.testing.expectEqual(@as(u64, 1), census.commits_with_candidates);
    try std.testing.expectEqual(@as(u64, 0), census.commits_validated);
    try std.testing.expectEqual(@as(u64, 1), census.commits_retained);
    // The pair is present and syntactically plausible; only the cryptographic
    // gate rejects it.
    try std.testing.expectEqual(@as(u64, 1), census.reason(.uuid_syntax));

    const drops = try importer.mirrorDropNames(gpa);
    defer gpa.free(drops);
    try std.testing.expectEqual(@as(usize, 0), drops.len);
}

test "git import mirror pair inherits an unchanged tags member and re-proves each commit" {
    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(), .{ .submanifest = .flat });
    defer importer.deinit();

    // Commit 1: a full export.
    var first_parts = try buildMirrorParts(gpa, .{}, &mirror_tree_files, &.{});
    defer first_parts.deinit();
    try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &mirror_tree_files, &first_parts, .regular);

    // Commit 2: one file changes; `manifest.tags` is unchanged and therefore
    // NOT re-exported, so its presence in the Git tree is inherited alone.
    const second_files = [_]MirrorFile{
        .{ .path = "alpha.txt", .content = "alpha body, revised" },
        .{ .path = "bin/tool.sh", .content = "#!/bin/sh\n", .mode = .executable },
        .{ .path = "docs/readme.md", .content = "readme body" },
    };
    var second_parts = try buildMirrorParts(gpa, .{ .parts = "ru" }, &second_files, &.{first_parts.name});
    defer second_parts.deinit();
    try ingestMirrorCommit(gpa, &importer, 20, 200, .{ .mark = 100 }, &.{}, &second_files, &second_parts, .regular);

    var second = try importedTree(gpa, &fixture, &importer, 200);
    defer second.deinit();
    try std.testing.expect(!second.has("manifest"));
    try std.testing.expect(!second.has("manifest.uuid"));
    try std.testing.expect(!second.has("manifest.tags"));
    try std.testing.expect(second.has("alpha.txt"));

    // Commit 3: the mirror's own manifest lies about the tree, so everything
    // present comes back — including the tags member inherited two commits
    // earlier, which must be restored into the emitted tree.
    const third_files = [_]MirrorFile{
        .{ .path = "alpha.txt", .content = "alpha body, revised again" },
        .{ .path = "bin/tool.sh", .content = "#!/bin/sh\n", .mode = .executable },
        .{ .path = "docs/readme.md", .content = "readme body" },
    };
    var third_parts = try buildMirrorParts(
        gpa,
        .{ .parts = "ru", .mutation = .manifest_missing_file },
        &third_files,
        &.{second_parts.name},
    );
    defer third_parts.deinit();
    try ingestMirrorCommit(gpa, &importer, 40, 300, .{ .mark = 200 }, &.{}, &third_files, &third_parts, .regular);

    var third = try importedTree(gpa, &fixture, &importer, 300);
    defer third.deinit();
    try std.testing.expect(third.has("manifest"));
    try std.testing.expect(third.has("manifest.uuid"));
    try std.testing.expect(third.has("manifest.tags"));

    // Commit 4: valid again, and its P card names commit 3 — which RETAINED,
    // for a reason of its own (`tree_mismatch`) that says nothing about whether
    // its UUID is correct. Gate 3 established commit 3's identity
    // cryptographically before that later gate kept it, so commit 4's parent
    // proof holds and commit 4 DROPS.
    //
    // This assertion used to read the other way, and its comment said so: "which
    // was RETAINED, so its UUID is not proven and this commit is inconclusive."
    // That is the defect written down as intent — it makes a
    // child's proof depend on its parent's unrelated gates, so one retain
    // anywhere poisons every descendant. On a real 36,432-commit sqlite mirror
    // it produced zero drops and 36,252 `parent_unproven` retentions.
    var fourth_parts = try buildMirrorParts(gpa, .{ .parts = "ru" }, &third_files, &.{third_parts.name});
    defer fourth_parts.deinit();
    try ingestMirrorCommit(gpa, &importer, 60, 400, .{ .mark = 300 }, &.{}, &third_files, &fourth_parts, .regular);

    var fourth = try importedTree(gpa, &fixture, &importer, 400);
    defer fourth.deinit();
    try std.testing.expect(!fourth.has("manifest"));
    try std.testing.expect(!fourth.has("manifest.tags"));

    const census = importer.mirrorCensus();
    try std.testing.expectEqual(@as(u64, 4), census.commits_with_candidates);
    // Three drop now, not two: commit 4 is no longer held back by commit 3's
    // unrelated failure.
    try std.testing.expectEqual(@as(u64, 3), census.commits_validated);
    // Commit 3 still retains on its OWN merits — attestation changes what a
    // child may rely on, never whether this commit was droppable.
    try std.testing.expectEqual(@as(u64, 1), census.reason(.tree_mismatch));
    try std.testing.expectEqual(@as(u64, 0), census.reason(.parent_unproven));
}

test "git import mirror pair proves ordered parents across 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(), .{ .submanifest = .flat });
    defer importer.deinit();

    const base_files = [_]MirrorFile{.{ .path = "alpha.txt", .content = "base" }};
    var base = try buildMirrorParts(gpa, .{ .parts = "ru" }, &base_files, &.{});
    defer base.deinit();
    try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &base_files, &base, .regular);

    const side_files = [_]MirrorFile{.{ .path = "alpha.txt", .content = "side" }};
    var side = try buildMirrorParts(gpa, .{ .parts = "ru" }, &side_files, &.{base.name});
    defer side.deinit();
    try ingestMirrorCommit(gpa, &importer, 10, 200, .{ .mark = 100 }, &.{}, &side_files, &side, .regular);

    const main_files = [_]MirrorFile{.{ .path = "alpha.txt", .content = "main" }};
    var main = try buildMirrorParts(gpa, .{ .parts = "ru" }, &main_files, &.{base.name});
    defer main.deinit();
    try ingestMirrorCommit(gpa, &importer, 20, 300, .{ .mark = 100 }, &.{}, &main_files, &main, .regular);

    const merged_files = [_]MirrorFile{.{ .path = "alpha.txt", .content = "merged" }};

    // Correct order: first parent then merge parent.
    var ordered = try buildMirrorParts(gpa, .{ .parts = "ru" }, &merged_files, &.{ main.name, side.name });
    defer ordered.deinit();
    try ingestMirrorCommit(gpa, &importer, 30, 400, .{ .mark = 300 }, &.{.{ .mark = 200 }}, &merged_files, &ordered, .regular);
    var ordered_tree = try importedTree(gpa, &fixture, &importer, 400);
    defer ordered_tree.deinit();
    try std.testing.expect(!ordered_tree.has("manifest"));

    // Reversed order: the same two proven UUIDs, in the wrong sequence.
    var reversed = try buildMirrorParts(gpa, .{ .parts = "ru" }, &merged_files, &.{ side.name, main.name });
    defer reversed.deinit();
    try ingestMirrorCommit(gpa, &importer, 40, 500, .{ .mark = 300 }, &.{.{ .mark = 200 }}, &merged_files, &reversed, .regular);
    var reversed_tree = try importedTree(gpa, &fixture, &importer, 500);
    defer reversed_tree.deinit();
    try std.testing.expect(reversed_tree.has("manifest"));

    // One parent short.
    var short = try buildMirrorParts(gpa, .{ .parts = "ru" }, &merged_files, &.{main.name});
    defer short.deinit();
    try ingestMirrorCommit(gpa, &importer, 50, 600, .{ .mark = 300 }, &.{.{ .mark = 200 }}, &merged_files, &short, .regular);
    var short_tree = try importedTree(gpa, &fixture, &importer, 600);
    defer short_tree.deinit();
    try std.testing.expect(short_tree.has("manifest"));

    const census = importer.mirrorCensus();
    try std.testing.expectEqual(@as(u64, 2), census.reason(.parent_unproven));
}

test "git import mirror pair drop set holds candidate-only artifacts and spares shared ones" {
    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();

    var parts = try buildMirrorParts(gpa, .{}, &mirror_tree_files, &.{});
    defer parts.deinit();

    // The same bytes fossil put in `manifest.tags` are also a real project
    // file, so that artifact is referenced by a retained tree entry.
    const files = mirror_tree_files ++ [_]MirrorFile{
        .{ .path = "zz-copy-of-tags.txt", .content = "branch trunk\ntag trunk\n" },
    };
    var full = try buildMirrorParts(gpa, .{}, &files, &.{});
    defer full.deinit();
    try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &files, &full, .regular);

    var tree = try importedTree(gpa, &fixture, &importer, 100);
    defer tree.deinit();
    try std.testing.expect(!tree.has("manifest"));
    try std.testing.expect(tree.has("zz-copy-of-tags.txt"));

    const drops = try importer.mirrorDropNames(gpa);
    defer {
        for (drops) |name| gpa.free(name);
        gpa.free(drops);
    }
    var tags_name_buf: [64]u8 = undefined;
    const tags_name = nameFor(full.tags.?, &tags_name_buf);
    var manifest_name_buf: [64]u8 = undefined;
    const manifest_name = nameFor(full.manifest.?, &manifest_name_buf);
    var uuid_name_buf: [64]u8 = undefined;
    const uuid_name = nameFor(full.uuid.?, &uuid_name_buf);

    var saw_manifest = false;
    var saw_uuid = false;
    for (drops) |name| {
        // The shared artifact is retained by the ordinary file that also
        // names it, so it must not be droppable.
        try std.testing.expect(!std.mem.eql(u8, name, tags_name));
        if (std.mem.eql(u8, name, manifest_name)) saw_manifest = true;
        if (std.mem.eql(u8, name, uuid_name)) saw_uuid = true;
    }
    try std.testing.expect(saw_manifest);
    try std.testing.expect(saw_uuid);
    try std.testing.expectEqual(@as(usize, 2), drops.len);
}

test "git import mirror pair retains a commit whose candidate arrived by rename" {
    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();

    var parts = try buildMirrorParts(gpa, .{}, &mirror_tree_files, &.{});
    defer parts.deinit();
    try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &mirror_tree_files, &parts, .regular);
    var first = try importedTree(gpa, &fixture, &importer, 100);
    defer first.deinit();
    try std.testing.expect(!first.has("manifest"));

    // A commit that renames a candidate out of the way. The bookkeeping does
    // not model where the content went, so the whole set comes back — and the
    // renamed destination has to hold the restored bytes.
    try importer.ingest(commit(200, null, .{ .mark = 100 }, &.{}, &.{
        .{ .rename = .{ .old = "manifest", .new = "kept-manifest" } },
    }, "rename out"));
    var second = try importedTree(gpa, &fixture, &importer, 200);
    defer second.deinit();
    try std.testing.expect(second.has("kept-manifest"));
    try std.testing.expect(!second.has("manifest"));
    try std.testing.expect(second.has("manifest.uuid"));
    try std.testing.expect(second.has("manifest.tags"));

    const census = importer.mirrorCensus();
    try std.testing.expectEqual(@as(u64, 1), census.reason(.unsupported_command));
    // The safety property stated directly: commit 200's tree references all
    // three artifacts, so none of them may be droppable. Without the retained
    // path's reference accounting this stays green on every other assertion
    // above while the drop set names three live artifacts.
    const drops = try importer.mirrorDropNames(gpa);
    defer {
        for (drops) |name| gpa.free(name);
        gpa.free(drops);
    }
    try std.testing.expectEqual(@as(usize, 0), drops.len);
}

test "git import mirror pair takes a candidate submodule from Git truth, not the emitted tree" {
    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();

    var parts = try buildMirrorParts(gpa, .{}, &mirror_tree_files, &.{});
    defer parts.deinit();
    try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &mirror_tree_files, &parts, .regular);
    var first = try importedTree(gpa, &fixture, &importer, 100);
    defer first.deinit();
    try std.testing.expect(!first.has("manifest"));

    // Git replaces the candidate file with a submodule. The validated parent
    // emitted no `manifest` at all, so a delete decided by looking in the
    // emitted tree would be skipped -- and the stale shadow entry would be
    // restored into this retained commit as a regular file Git does not have.
    try importer.ingest(commit(200, null, .{ .mark = 100 }, &.{}, &.{
        .{ .modify = .{ .mode = .gitlink, .data = .{ .mark = 100 }, .path = "manifest" } },
    }, "submodule at manifest"));
    var second = try importedTree(gpa, &fixture, &importer, 200);
    defer second.deinit();
    try std.testing.expect(!second.has("manifest"));
    try std.testing.expect(second.has("manifest.uuid"));
    try std.testing.expect(second.has("manifest.tags"));

    const census = importer.mirrorCensus();
    try std.testing.expectEqual(@as(u64, 1), census.commits_validated);
    try std.testing.expectEqual(@as(u64, 1), census.reason(.incomplete_pair));
}

test "git import mirror pair names a commit that deleted every candidate no_candidates" {
    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();

    var parts = try buildMirrorParts(gpa, .{}, &mirror_tree_files, &.{});
    defer parts.deinit();
    try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &mirror_tree_files, &parts, .regular);

    try importer.ingest(commit(200, null, .{ .mark = 100 }, &.{}, &.{
        .{ .delete = "manifest" },
        .{ .delete = "manifest.uuid" },
        .{ .delete = "manifest.tags" },
    }, "mirror turned off"));
    var second = try importedTree(gpa, &fixture, &importer, 200);
    defer second.deinit();
    try std.testing.expect(!second.has("manifest"));
    try std.testing.expect(!second.has("manifest.uuid"));
    try std.testing.expect(!second.has("manifest.tags"));

    // The reason is per commit, in the ledger: the census deliberately does not
    // count this commit at all, so only the ledger can say what it decided.
    var finalization = try importer.finalizeRefs(.{});
    defer finalization.deinit(gpa);
    const record = try ledgerCommit(&finalization, 1);
    try std.testing.expectEqual(git_import.MirrorDecisionKind.retained, record.decision);
    try std.testing.expectEqual(git_import.MirrorRetainReason.no_candidates, record.reason);

    // A tree with no candidates has no pair to be incomplete, and it is not a
    // commit "with candidates" either -- so the census still balances.
    const census = importer.mirrorCensus();
    try std.testing.expectEqual(@as(u64, 0), census.reason(.incomplete_pair));
    try std.testing.expectEqual(@as(u64, 1), census.commits_with_candidates);
    try std.testing.expectEqual(@as(u64, 1), census.commits_validated);
    try std.testing.expectEqual(@as(u64, 0), census.commits_retained);
}

test "git import mirror pair leaves an ordinary import untouched" {
    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(blob(1, null, "first body"));
    try importer.ingest(blob(2, null, "second body"));
    var commands = try twoLeafCommands(gpa, 1, 2);
    defer commands.deinit();
    try importer.ingest(commit(10, null, null, &.{}, commands.commands.items, "ordinary"));
    try importer.ingest(commit(11, null, .{ .mark = 10 }, &.{}, &.{}, "empty follow-up"));

    const census = importer.mirrorCensus();
    try std.testing.expectEqual(@as(u64, 0), census.commits_with_candidates);
    try std.testing.expectEqual(@as(u64, 0), census.f_cards_compared);
    const drops = try importer.mirrorDropNames(gpa);
    defer gpa.free(drops);
    try std.testing.expectEqual(@as(usize, 0), drops.len);
}

/// The drop-set capture, under allocation failure.
///
/// `capture_drop_set` is a publication boundary with its own fault test, but
/// the work that boundary names is `mirrorDropNames` -- one owned name per
/// droppable candidate plus the slice holding them -- and that had no
/// allocation coverage at all. It is the last allocating step before the pack
/// reads the filter, so a partial failure here would either leak the names or
/// hand packing a truncated set.
///
/// The fixture is deliberately one whose drop set is NON-EMPTY, asserted below:
/// a run that captured nothing would allocate nothing, leak nothing, and prove
/// nothing.
fn mirrorDropNamesOomNoLeak(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();
    var parts = try buildMirrorParts(gpa, .{}, &mirror_tree_files, &.{});
    defer parts.deinit();
    try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &mirror_tree_files, &parts, .regular);

    const drops = try importer.mirrorDropNames(gpa);
    defer {
        for (drops) |name| gpa.free(name);
        gpa.free(drops);
    }
    if (drops.len == 0) return error.TestUnexpectedResult;
}

test "git import mirror drop-set capture releases every allocation it makes under allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, mirrorDropNamesOomNoLeak, .{});
}

fn mirrorClassifierOomNoLeak(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();
    var parts = try buildMirrorParts(gpa, .{}, &mirror_tree_files, &.{});
    defer parts.deinit();
    try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &mirror_tree_files, &parts, .regular);
    // A second commit whose classification fails, so the discarded speculative
    // build and its replacement both unwind.
    var broken = try buildMirrorParts(gpa, .{ .mutation = .manifest_missing_file }, &mirror_tree_files, &.{parts.name});
    defer broken.deinit();
    try ingestMirrorCommit(gpa, &importer, 20, 200, .{ .mark = 100 }, &.{}, &mirror_tree_files, &broken, .regular);
}

test "git import mirror classification releases every allocation it makes under allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, mirrorClassifierOomNoLeak, .{});
}

// ---------------------------------------------------------------------------
// Normalized-tree and provenance digests
// ---------------------------------------------------------------------------

/// One commit's ledger entry, by position in stream order.
fn ledgerCommit(finalization: *const git_import.FinalizedRefs, index: usize) !git_import.LedgerCommit {
    if (index >= finalization.ledger.commits.len) return error.TestUnexpectedResult;
    return finalization.ledger.commits[index];
}

fn expectDigest(expected: []const u8, actual: git_import.Digest) !void {
    var buffer: [64]u8 = undefined;
    try std.testing.expectEqualStrings(expected, git_import.formatDigest(actual, &buffer));
}

const DigestFile = struct {
    path: []const u8,
    content: []const u8,
    mode: fast_export.FileMode = .regular,
};

/// Imports one commit holding exactly `files` and returns its ledger entry.
/// The importer and its finalization stay with the caller, because every
/// digest in the ledger borrows from them.
fn digestCommands(
    gpa: std.mem.Allocator,
    importer: *git_import.Importer,
    files: []const DigestFile,
) !std.ArrayList(fast_export.FileCommand) {
    var commands: std.ArrayList(fast_export.FileCommand) = .empty;
    errdefer commands.deinit(gpa);
    for (files, 1..) |file, mark| {
        try importer.ingest(blob(@intCast(mark), null, file.content));
        try commands.append(gpa, .{ .modify = .{
            .mode = file.mode,
            .data = .{ .mark = @intCast(mark) },
            .path = file.path,
        } });
    }
    return commands;
}

/// The normalized-tree digest of a single-commit import of `files`.
fn treeDigestOf(gpa: std.mem.Allocator, files: []const DigestFile) !git_import.Digest {
    var fixture = try Fixture.init(gpa);
    defer fixture.deinit();
    var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{});
    defer importer.deinit();
    var commands = try digestCommands(gpa, &importer, files);
    defer commands.deinit(gpa);
    try importer.ingest(commit(100, oid_a, null, &.{}, commands.items, "digest fixture"));
    var finalization = try importer.finalizeRefs(.{});
    defer finalization.deinit(gpa);
    return (try ledgerCommit(&finalization, 0)).tree_digest;
}

test "git import normalized digest pins its domain-framed encoding" {
    const gpa = std.testing.allocator;

    // Independently reproducible vectors. The empty tree is the domain alone:
    //   printf 'zit-import-git-tree-v1\0' | shasum -a 256
    // The three-entry vector is the same domain followed, per entry in
    // canonical filename order, by a big-endian u64 path length, the path, one
    // permission byte (regular=0, executable=1, symlink=2), a big-endian u64
    // content length, and the content's raw SHA-256. Neither value may drift:
    // Task 6 verifies a packed generation against them, and Task 8 compares
    // flat and tree censuses by them.
    const empty_digest = "eedb9cfd37cfbbb728fd28f6b8ba2cf538b17a841a6189d157e8c7827cc7cece";
    const three_entry_digest = "fffd82fbea60f7408b84470f59e582e978aed06cc354a0e9cefc5bf2240d5115";

    try expectDigest(empty_digest, try treeDigestOf(gpa, &.{}));

    const files = [_]DigestFile{
        .{ .path = "a.txt", .content = "alpha body" },
        .{ .path = "b.sh", .content = "#!/bin/sh\n", .mode = .executable },
        .{ .path = "c.lnk", .content = "a.txt", .mode = .symlink },
    };
    try expectDigest(three_entry_digest, try treeDigestOf(gpa, &files));
}

test "git import normalized digest separates path, content, and permission" {
    const gpa = std.testing.allocator;
    const base = [_]DigestFile{
        .{ .path = "a.txt", .content = "alpha body" },
        .{ .path = "b.sh", .content = "#!/bin/sh\n", .mode = .executable },
    };
    const renamed = [_]DigestFile{
        .{ .path = "a2.txt", .content = "alpha body" },
        .{ .path = "b.sh", .content = "#!/bin/sh\n", .mode = .executable },
    };
    const rewritten = [_]DigestFile{
        .{ .path = "a.txt", .content = "alpha body." },
        .{ .path = "b.sh", .content = "#!/bin/sh\n", .mode = .executable },
    };
    const rechmodded = [_]DigestFile{
        .{ .path = "a.txt", .content = "alpha body" },
        .{ .path = "b.sh", .content = "#!/bin/sh\n", .mode = .regular },
    };
    // Content that merely moves between paths is a different tree, so the
    // digest cannot be a hash of the concatenated contents alone.
    const swapped = [_]DigestFile{
        .{ .path = "a.txt", .content = "#!/bin/sh\n" },
        .{ .path = "b.sh", .content = "alpha body", .mode = .executable },
    };

    const want = try treeDigestOf(gpa, &base);
    try std.testing.expectEqual(want, try treeDigestOf(gpa, &base));
    for ([_][]const DigestFile{ &renamed, &rewritten, &rechmodded, &swapped }) |variant| {
        try std.testing.expect(!std.mem.eql(u8, &want, &(try treeDigestOf(gpa, variant))));
    }
}

test "git import tree digest is structural over the S-run, flat digest stays per-file" {
    const gpa = std.testing.allocator;
    var digests: [2]git_import.Digest = undefined;
    var had_subs: [2]bool = .{ false, false };
    var structural: git_import.Digest = undefined;

    for ([_]git_import.SubmanifestChoice{ .flat, .{ .tree = 5 } }, 0..) |choice, index| {
        var fixture = try Fixture.init(gpa);
        defer fixture.deinit();
        var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{
            .submanifest = choice,
        });
        defer importer.deinit();
        try importer.ingest(blob(1, null, "first body"));
        try importer.ingest(blob(2, null, "second body"));
        var commands = try twoLeafCommands(gpa, 1, 2);
        defer commands.deinit();
        try importer.ingest(commit(100, oid_a, null, &.{}, commands.commands.items, "shape fixture"));

        var tree = try importedTree(gpa, &fixture, &importer, 100);
        defer tree.deinit();
        had_subs[index] = tree.checkin.manifest.s.len != 0;
        if (had_subs[index]) structural = git_import.structuralTreeDigest(tree.checkin.manifest.s);

        var finalization = try importer.finalizeRefs(.{});
        defer finalization.deinit(gpa);
        digests[index] = (try ledgerCommit(&finalization, 0)).tree_digest;
    }

    // The two runs really did choose different roots. A flat root digests
    // per-file; a tree root digests its own S-run (the S hashes are
    // content addresses, so the run commits to the whole set transitively at
    // O(root-width) instead of O(files)). The shapes therefore digest
    // differently by design, and the tree value is pinned to the shared
    // structural helper the verifier recomputes with.
    try std.testing.expect(!had_subs[0]);
    try std.testing.expect(had_subs[1]);
    try std.testing.expect(!std.mem.eql(u8, &digests[0], &digests[1]));
    try std.testing.expectEqual(structural, digests[1]);
}

test "git import buildTree oracle reproduces the production root and every reachable S node" {
    const gpa = std.testing.allocator;

    // The flat import supplies the canonical entry list the batch builder
    // takes; the tree import supplies what the incremental writer produced
    // from the same commit. Neither is derived from the other.
    var flat_fixture = try Fixture.init(gpa);
    defer flat_fixture.deinit();
    var flat_importer = try git_import.Importer.init(gpa, &flat_fixture.log, flat_fixture.treeStore(), .{
        .submanifest = .flat,
    });
    defer flat_importer.deinit();
    try flat_importer.ingest(blob(1, null, "first body"));
    try flat_importer.ingest(blob(2, null, "second body"));
    var flat_commands = try twoLeafCommands(gpa, 1, 2);
    defer flat_commands.deinit();
    try flat_importer.ingest(commit(100, oid_a, null, &.{}, flat_commands.commands.items, "oracle fixture"));
    var flat_tree = try importedTree(gpa, &flat_fixture, &flat_importer, 100);
    defer flat_tree.deinit();
    try std.testing.expectEqual(@as(usize, 0), flat_tree.checkin.manifest.s.len);

    var tree_fixture = try Fixture.init(gpa);
    defer tree_fixture.deinit();
    var tree_importer = try git_import.Importer.init(gpa, &tree_fixture.log, tree_fixture.treeStore(), .{
        .submanifest = .{ .tree = 5 },
    });
    defer tree_importer.deinit();
    try tree_importer.ingest(blob(1, null, "first body"));
    try tree_importer.ingest(blob(2, null, "second body"));
    var tree_commands = try twoLeafCommands(gpa, 1, 2);
    defer tree_commands.deinit();
    try tree_importer.ingest(commit(100, oid_a, null, &.{}, tree_commands.commands.items, "oracle fixture"));
    var produced = try loadManifest(&tree_fixture, gpa, try tree_importer.objectForMark(100));
    defer produced.deinit(gpa);

    // Same algorithm as the import, or the oracle names a different tree.
    var oracle = try artifact.buildTree(gpa, flat_tree.checkin.manifest.f, 5, git_import.import_hash_algo);
    defer oracle.deinit();
    try std.testing.expect(!oracle.isFlat());

    // The root's own S run, card for card.
    try std.testing.expectEqual(oracle.s.len, produced.manifest.s.len);
    for (oracle.s, produced.manifest.s) |want, got| {
        try std.testing.expectEqualStrings(want.filename, got.filename);
        try std.testing.expectEqualStrings(want.hash, got.hash);
    }

    // And every sub-manifest the root reaches, byte for byte out of the log.
    try std.testing.expect(oracle.subs.len >= oracle.s.len);
    for (oracle.subs) |sub| {
        const rid = tree_fixture.lookupName(sub.name) orelse return error.TestUnexpectedResult;
        const stored = (try tree_fixture.log.content(gpa, rid)) orelse return error.TestUnexpectedResult;
        defer gpa.free(stored);
        try std.testing.expectEqualSlices(u8, sub.bytes, stored);
    }
}

const digest_oid_d = "4444444444444444444444444444444444444444";
const digest_author = fast_export.Identity{
    .name = "Digest Author",
    .email = "[email protected]",
    .timestamp = 1_700_000_100,
    .timezone_offset_minutes = 60,
};
const digest_committer = fast_export.Identity{
    .name = "Digest Committer",
    .email = "[email protected]",
    .timestamp = 1_700_000_200,
    .timezone_offset_minutes = -120,
};

const ProvenanceVariant = enum {
    base,
    extra_ref,
    /// `extra_ref` with the same ref count and a different ref name, so the
    /// digest has to cover what the refs ARE rather than how many there are.
    renamed_ref,
    annotated_tag,
    /// `annotated_tag` with the same tag count and a different message, for
    /// the same reason.
    retagged_message,
    author_attribution,
};

const ProvenanceSample = struct {
    commit: git_import.Digest,
    repository: git_import.Digest,
};

fn provenanceSample(gpa: std.mem.Allocator, variant: ProvenanceVariant) !ProvenanceSample {
    var fixture = try Fixture.init(gpa);
    defer fixture.deinit();
    var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{
        .attribution = if (variant == .author_attribution) .author else .committer,
    });
    defer importer.deinit();
    try importer.ingest(blob(1, oid_b, "provenance body"));
    const commands = [_]fast_export.FileCommand{.{ .modify = .{
        .mode = .regular,
        .data = .{ .mark = 1 },
        .path = "tracked.txt",
    } }};
    try importer.ingest(.{ .commit = .{
        .ref = "refs/heads/main",
        .mark = 10,
        .original_oid = oid_a,
        .author = digest_author,
        .committer = digest_committer,
        .message = "provenance fixture",
        .from = null,
        .merges = &.{},
        .commands = &commands,
    } });
    if (variant == .extra_ref or variant == .renamed_ref) {
        try importer.ingest(.{ .reset = .{
            .ref = if (variant == .extra_ref) "refs/heads/release" else "refs/heads/staging",
            .target = .{ .mark = 10 },
        } });
    }
    if (variant == .annotated_tag or variant == .retagged_message) {
        try importer.ingest(.{ .tag = .{
            .name = "v1",
            .mark = 20,
            .target = .{ .mark = 10 },
            .original_oid = digest_oid_d,
            .tagger = digest_author,
            .message = if (variant == .annotated_tag) "release" else "release candidate",
        } });
    }
    var finalization = try importer.finalizeRefs(.{});
    defer finalization.deinit(gpa);
    return .{
        .commit = (try ledgerCommit(&finalization, 0)).provenance_digest,
        .repository = finalization.ledger.provenance_digest,
    };
}

test "git import provenance digest separates attribution from refs and tags" {
    const gpa = std.testing.allocator;
    const base = try provenanceSample(gpa, .base);
    try std.testing.expectEqual(base, try provenanceSample(gpa, .base));

    // A ref or a tag is repository scope: the commit itself did not change,
    // and its own digest must say so, while the import as a whole did.
    for ([_]ProvenanceVariant{ .extra_ref, .renamed_ref, .annotated_tag, .retagged_message }) |variant| {
        const sample = try provenanceSample(gpa, variant);
        try std.testing.expectEqual(base.commit, sample.commit);
        try std.testing.expect(!std.mem.eql(u8, &base.repository, &sample.repository));
    }

    // And the mappings themselves are covered, not merely counted: each pair
    // below has the same number of refs and tags as the other and differs
    // only in what they say.
    const extra_ref = try provenanceSample(gpa, .extra_ref);
    const renamed_ref = try provenanceSample(gpa, .renamed_ref);
    try std.testing.expect(!std.mem.eql(u8, &extra_ref.repository, &renamed_ref.repository));
    const tagged = try provenanceSample(gpa, .annotated_tag);
    const retagged = try provenanceSample(gpa, .retagged_message);
    try std.testing.expect(!std.mem.eql(u8, &tagged.repository, &retagged.repository));

    // The selected attribution is commit scope, and reaches the repository
    // digest only through the commit's.
    const attributed = try provenanceSample(gpa, .author_attribution);
    try std.testing.expect(!std.mem.eql(u8, &base.commit, &attributed.commit));
    try std.testing.expect(!std.mem.eql(u8, &base.repository, &attributed.repository));
}

/// The provenance digest of the merge commit in a three-parent history whose
/// merge parents are listed in `order`.
fn mergeProvenanceDigest(gpa: std.mem.Allocator, order: [2]u64) !git_import.Digest {
    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(commit(10, oid_a, null, &.{}, &.{}, "first root"));
    try importer.ingest(commit(11, oid_b, null, &.{}, &.{}, "second root"));
    try importer.ingest(commit(12, oid_c, null, &.{}, &.{}, "third root"));
    const merges = [_]fast_export.ObjectRef{ .{ .mark = order[0] }, .{ .mark = order[1] } };
    try importer.ingest(commit(13, digest_oid_d, .{ .mark = 10 }, &merges, &.{}, "merge"));
    var finalization = try importer.finalizeRefs(.{});
    defer finalization.deinit(gpa);
    return (try ledgerCommit(&finalization, 3)).provenance_digest;
}

test "git import provenance digest preserves ordered Git parents" {
    const gpa = std.testing.allocator;
    const forward = try mergeProvenanceDigest(gpa, .{ 11, 12 });
    try std.testing.expectEqual(forward, try mergeProvenanceDigest(gpa, .{ 11, 12 }));
    // Same parent set, different order: a different history, therefore a
    // different digest.
    const reversed = try mergeProvenanceDigest(gpa, .{ 12, 11 });
    try std.testing.expect(!std.mem.eql(u8, &forward, &reversed));
}

const MirrorProvenance = struct {
    tree: git_import.Digest,
    provenance: git_import.Digest,
    decision: git_import.MirrorDecisionKind,
    reason: git_import.MirrorRetainReason,
    candidates: [3]?git_import.LedgerCandidate,
    candidate_artifacts: usize,
    dropped: usize,
};

/// Imports one mirror commit and reports what the ledger says about it.
/// `tags_override`, when given, replaces the exported `manifest.tags` body
/// without touching the pair that proves the commit.
fn mirrorProvenance(
    gpa: std.mem.Allocator,
    spec: MirrorSpec,
    tags_override: ?[]const u8,
) !MirrorProvenance {
    var fixture = try Fixture.init(gpa);
    defer fixture.deinit();
    var importer = try git_import.Importer.init(gpa, &fixture.log, fixture.treeStore(), .{});
    defer importer.deinit();
    var parts = try buildMirrorParts(gpa, spec, &mirror_tree_files, &.{});
    defer parts.deinit();
    if (tags_override) |body| {
        if (parts.tags) |old| gpa.free(old);
        parts.tags = try gpa.dupe(u8, body);
    }
    try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &mirror_tree_files, &parts, spec.mode);
    var finalization = try importer.finalizeRefs(.{});
    defer finalization.deinit(gpa);
    const entry = try ledgerCommit(&finalization, 0);
    var dropped: usize = 0;
    for (finalization.ledger.candidates) |candidate| {
        if (candidate.dropped) dropped += 1;
    }
    return .{
        .tree = entry.tree_digest,
        .provenance = entry.provenance_digest,
        .decision = entry.decision,
        .reason = entry.reason,
        .candidates = entry.candidates,
        .candidate_artifacts = finalization.ledger.candidates.len,
        .dropped = dropped,
    };
}

test "git import provenance digest records the mirror decision and every candidate including tags" {
    const gpa = std.testing.allocator;

    const valid = try mirrorProvenance(gpa, .{}, null);
    try std.testing.expectEqual(git_import.MirrorDecisionKind.omitted, valid.decision);
    // All three exported candidates are recorded, the dependent tags member
    // among them, and all three are droppable here because nothing else in
    // the import names their content.
    // Stored under the import's own algorithm, so the expected width follows
    // it rather than a literal that only held while that algorithm was SHA-1.
    var width_buf: [64]u8 = undefined;
    const name_width = nameFor("", &width_buf).len;
    for (valid.candidates, git_import.mirror_candidate_names) |slot, path| {
        const candidate = slot orelse return error.TestUnexpectedResult;
        try std.testing.expectEqualStrings(path, candidate.path);
        try std.testing.expectEqual(name_width, candidate.name.len);
    }
    try std.testing.expectEqual(@as(usize, 3), valid.candidate_artifacts);
    try std.testing.expectEqual(@as(usize, 3), valid.dropped);

    // Changing only `manifest.tags` leaves the emitted tree byte-identical --
    // the candidates are not in it -- so the tree digest cannot see it. The
    // provenance digest must, or the tags member's decision would be
    // unrecorded and a verifier could not tell the two imports apart.
    const other_tags = try mirrorProvenance(gpa, .{}, "branch release\ntag release\n");
    try std.testing.expectEqual(git_import.MirrorDecisionKind.omitted, other_tags.decision);
    try std.testing.expectEqual(valid.tree, other_tags.tree);
    try std.testing.expect(!std.mem.eql(u8, &valid.provenance, &other_tags.provenance));

    // A retained commit records why, and its candidates stay in the tree.
    const retained = try mirrorProvenance(gpa, .{ .mutation = .uuid_names_other_artifact }, null);
    try std.testing.expectEqual(git_import.MirrorDecisionKind.retained, retained.decision);
    try std.testing.expectEqual(git_import.MirrorRetainReason.uuid_mismatch, retained.reason);
    try std.testing.expectEqual(@as(usize, 0), retained.candidate_artifacts);
    try std.testing.expect(!std.mem.eql(u8, &valid.tree, &retained.tree));
    try std.testing.expect(!std.mem.eql(u8, &valid.provenance, &retained.provenance));
}

test "git import ledger and provenance JSONL agree on candidates, references, and the drop set" {
    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();

    // The bytes fossil put in `manifest.tags` are also an ordinary project
    // file in the same commit, so exactly one candidate artifact is shared
    // with a retained tree entry and must survive the drop set.
    const files = mirror_tree_files ++ [_]MirrorFile{
        .{ .path = "zz-copy-of-tags.txt", .content = "branch trunk\ntag trunk\n" },
    };
    var parts = try buildMirrorParts(gpa, .{}, &files, &.{});
    defer parts.deinit();
    // Named once with an original ID, so the provenance JSONL carries a blob
    // record for exactly this content.
    try importer.ingest(blob(90, oid_b, "branch trunk\ntag trunk\n"));
    try ingestMirrorCommit(gpa, &importer, 1, 100, null, &.{}, &files, &parts, .regular);

    var finalization = try importer.finalizeRefs(.{});
    defer finalization.deinit(gpa);

    const drops = try importer.mirrorDropNames(gpa);
    defer {
        for (drops) |name| gpa.free(name);
        gpa.free(drops);
    }

    // Every candidate artifact appears once in the ledger, and the ledger's
    // dropped flags are exactly the drop set the packer will be handed.
    var ledger_dropped: usize = 0;
    for (finalization.ledger.candidates) |candidate| {
        try std.testing.expectEqual(candidate.retained_references == 0, candidate.dropped);
        if (!candidate.dropped) continue;
        ledger_dropped += 1;
        var found = false;
        for (drops) |name| {
            if (std.mem.eql(u8, name, candidate.name)) found = true;
        }
        try std.testing.expect(found);
    }
    try std.testing.expectEqual(drops.len, ledger_dropped);
    // The shared `manifest.tags` content is a candidate that is not dropped.
    try std.testing.expectEqual(@as(usize, 3), finalization.ledger.candidates.len);
    try std.testing.expectEqual(@as(usize, 2), ledger_dropped);

    // And the JSONL says the same thing about the blob that carries the
    // shared bytes: a candidate, referenced once, and therefore not dropped.
    var tags_name_buf: [64]u8 = undefined;
    const tags_name = nameFor(parts.tags.?, &tags_name_buf);
    const shared_record = try std.fmt.allocPrint(
        gpa,
        "\"zit\":\"{s}\",\"retained_references\":1,\"mirror_candidate\":true,\"dropped\":false",
        .{tags_name},
    );
    defer gpa.free(shared_record);
    try std.testing.expect(std.mem.indexOf(u8, finalization.provenance_jsonl, shared_record) != null);
    try std.testing.expect(std.mem.indexOf(u8, finalization.provenance_jsonl, "\"decision\":\"omitted\"") != null);
    try std.testing.expect(std.mem.indexOf(u8, finalization.provenance_jsonl, "\"kind\":\"summary\"") != null);

    // Every line stays valid JSON with the new fields in place.
    var lines = std.mem.splitScalar(u8, finalization.provenance_jsonl[0 .. finalization.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);
    }
}