Skip to content
zit Artifact [cc7e940368]
Login

Artifact [cc7e940368]

Artifact cc7e9403682a06fb373d085f1faae5aff338f45d1e87376c03c4e4182d01b0f6:


//! `zit import-fossil` — bring a fossil repository's history into zit, with
//! the compatibility consequence CHOSEN AT IMPORT TIME (#61). Two modes, no
//! default: the difference is permanent and the operator must pick.
//!
//!   --compatible   The artifact corpus is preserved verbatim: same hashes,
//!                  same project code, flat manifests. The result stays a
//!                  first-class member of the origin's sync world. This is
//!                  the clone machinery wearing its import name — fossil's
//!                  sync protocol IS the import path; no SQLite is read.
//!                  (Dispatched in main.zig straight onto the clone runner.)
//!
//!   --submanifest  REBIRTH: every check-in re-mints as a sub-manifest tree
//!                  under a NEW project code — uniform S-tree history from
//!                  the first commit, and a permanent break with the origin
//!                  (never syncs with it again; signatures cannot survive
//!                  rehashing and are stripped and counted).
//!
//! This file owns the submanifest arm: the #41 evidence gate first — replay
//! the WHOLE history through the real tree builder, print the costs, and
//! REFUSE a conversion the evidence says would be worse than flat unless
//! --force — then the conversion itself. The staged-publication discipline
//! comes from `publication.zig` (the #61 step-1 extraction).

const std = @import("std");
const zit = @import("zit");
const cli = zit.cli;
const artifact = zit.artifact;
const Repository = zit.Repository;
const convert = @import("fossil_convert.zig");

pub const Mode = enum { compatible, submanifest };

/// The submanifest arm: SOURCE is a local store (a directory holding `.zit`
/// or the `.zit` file itself) already populated by `zit clone` from the
/// fossil origin. The gate runs unconditionally; the conversion runs only
/// when the gate passes (or --force overrides its refusal).
pub fn runSubmanifestImport(
    gpa: std.mem.Allocator,
    io: std.Io,
    arena: std.mem.Allocator,
    source: []const u8,
    destination: []const u8,
    k_override: ?u8,
    force: bool,
    streams: cli.Streams,
) cli.Exit {
    _ = destination; // The conversion pass consumes this; the gate does not.

    const root = (cli.resolveRoot(arena, io, source) catch return oom(streams)) orelse {
        streams.err.print("zit import-fossil: '{s}' is not a zit repository (clone the fossil origin first: 'zit clone URL DIR')\n", .{source}) catch {};
        return .failure;
    };
    var root_dir = root.dir;
    defer root_dir.close(io);

    var repo: Repository = undefined;
    Repository.open(&repo, gpa, io, root_dir, .{}) catch {
        streams.err.writeAll("zit import-fossil: cannot read the source repository\n") catch {};
        return .failure;
    };
    defer repo.deinit(gpa);

    const verdict = gate(gpa, &repo, k_override, streams) orelse return .failure;
    if (!verdict.passes and !force) {
        streams.err.writeAll(
            "zit import-fossil: the full-history replay says a sub-manifest conversion\n" ++
                "would cost more than flat manifests for this history; refusing. Pass --force\n" ++
                "to convert anyway, or import with --compatible instead.\n",
        ) catch {};
        return .failure;
    }

    const corpus = convert.census(gpa, &repo) catch {
        streams.err.writeAll("zit import-fossil: walking the source corpus failed\n") catch {};
        return .failure;
    };
    convert.renderCensus(&corpus, streams);
    if (corpus.phantoms != 0) {
        streams.err.writeAll("zit import-fossil: the source holds phantoms — an incomplete corpus cannot re-mint; pull to convergence first\n") catch {};
        return .failure;
    }

    // The conversion pass lands next on this branch: reference remap over
    // the corpus, S-tree re-mint through the submanifest editor, new
    // project code, staged publication, differential verification.
    streams.err.writeAll(
        "zit import-fossil: the conversion pass is not built yet; the gate above is\n" ++
            "its decision input. Track issue #61.\n",
    ) catch {};
    return .failure;
}

const Verdict = struct {
    passes: bool,
    k: u4,
};

/// The #41 evidence gate: static k sweep over the tip file set names the
/// candidate, a WHOLE-history replay through the real tree builder and
/// delta encoder prices it, and the verdict compares steady-state full-form
/// cost against the flat arm. Prints what it measures; null = could not
/// measure (already diagnosed).
fn gate(
    gpa: std.mem.Allocator,
    repo: *Repository,
    k_override: ?u8,
    streams: cli.Streams,
) ?Verdict {
    const tip = repo.resolve("tip", .checkin) catch {
        streams.err.writeAll("zit import-fossil: the source repository has no check-in to measure\n") catch {};
        return null;
    };
    var fl = (repo.files(gpa, tip) catch null) orelse {
        streams.err.writeAll("zit import-fossil: cannot read the source tip check-in\n") catch {};
        return null;
    };
    defer fl.deinit(gpa);

    var arena_state = std.heap.ArenaAllocator.init(gpa);
    defer arena_state.deinit();
    const aa = arena_state.allocator();
    var entries: std.ArrayList(artifact.FCard) = .empty;
    var it = fl.iterator();
    while (it.next()) |f| {
        entries.append(aa, .{
            .filename = aa.dupe(u8, f.filename) catch return null,
            .hash = if (f.hash) |h| aa.dupe(u8, h) catch return null else null,
            .perm = if (f.perm) |p| aa.dupe(u8, p) catch return null else null,
            .old_name = null,
            .mode = f.mode,
        }) catch return null;
    }
    if (entries.items.len == 0) {
        streams.err.writeAll("zit import-fossil: the source tip has no files to measure\n") catch {};
        return null;
    }
    streams.out.print("gate: measuring {d} names at the source tip\n", .{entries.items.len}) catch {};

    // Static sweep names the candidate k (the operator's --k overrides it).
    // The measurement algo follows the source identity space: 40-hex = SHA-1.
    const algo: artifact.HashAlgo = if (repo.hashOf(tip).len == 40) .sha1 else .sha3_256;
    var costs: [artifact.max_k - artifact.min_k + 1]zit.submanifest.Cost = undefined;
    var n: usize = 0;
    var k: u8 = artifact.min_k;
    while (k <= artifact.max_k) : (k += 1) {
        costs[n] = zit.submanifest.cost(gpa, entries.items, @intCast(k), algo) catch return null;
        n += 1;
    }
    const candidate: u4 = if (k_override) |kk| @intCast(kk) else blk: {
        const w = zit.submanifest.best(costs[0..n]) orelse {
            streams.out.writeAll("gate: no k partitions this file set into a tree — every commit would stay flat\n") catch {};
            return .{ .passes = false, .k = 0 };
        };
        break :blk w.k;
    };

    // Whole-history replay of the candidate and its in-range neighbours.
    var ks_buf: [3]u4 = undefined;
    var n_ks: usize = 0;
    if (candidate > artifact.min_k) {
        ks_buf[n_ks] = candidate - 1;
        n_ks += 1;
    }
    ks_buf[n_ks] = candidate;
    n_ks += 1;
    if (candidate < artifact.max_k) {
        ks_buf[n_ks] = candidate + 1;
        n_ks += 1;
    }
    var rep = zit.submanifest.replay(gpa, repo, std.math.maxInt(usize), ks_buf[0..n_ks]) catch {
        streams.err.writeAll("zit import-fossil: replaying the source history failed\n") catch {};
        return null;
    };
    defer rep.deinit(gpa);
    if (rep.commits < 2) {
        streams.out.print("gate: only {d} replayable commit(s) — too little history to refuse on; proceeding is --force territory either way\n", .{rep.commits}) catch {};
        return .{ .passes = false, .k = candidate };
    }
    streams.out.print(
        "gate: replayed all {d} commits — flat store {d} bytes, flat full-form {d} bytes\n",
        .{ rep.commits, rep.flat_store, rep.flat_full },
    ) catch {};
    for (rep.arms) |arm| {
        if (arm.all_flat) continue;
        streams.out.print(
            "gate: k={d} steady-state full-form {d} bytes (flat {d}), store {d} bytes (flat {d}), bootstrap {d}\n",
            .{ arm.k, arm.steady_full, rep.flat_full_steady, arm.steady_store, rep.flat_store_steady, arm.bootstrap_store },
        ) catch {};
    }
    const winner = rep.bestArm() orelse {
        streams.out.writeAll("gate: every replayed k built flat — this history gives sub-manifests nothing to share\n") catch {};
        return .{ .passes = false, .k = candidate };
    };
    const passes = winner.steady_full < rep.flat_full_steady;
    streams.out.print(
        "gate verdict: k={d} {s}\n",
        .{ winner.k, if (passes) "beats flat on steady-state full-form cost over the whole history" else "does not beat flat over the whole history" },
    ) catch {};
    return .{ .passes = passes, .k = winner.k };
}

fn oom(streams: cli.Streams) cli.Exit {
    streams.err.writeAll("zit import-fossil: out of memory\n") catch {};
    return .failure;
}