Skip to content
zit Artifact [587da48671]
Login

Artifact [587da48671]

Artifact 587da4867130e1415f78a6ad6094ea81b60741599e842496ced1cb42910462d6:


//! The F-set composition a check-in is built from: pure value-in/value-out
//! pieces shared by the working-tree commit (`cmd/checkout.zig`) and the
//! direct-commit core (`checkin.zig`), so the two cannot mint different
//! manifests for the same change. No I/O, no clock, no environment.

const std = @import("std");
const artifact = @import("../artifact.zig");
const cone_mod = @import("../cone.zig");

/// Merge policy for the new check-in's F-set.
///  - `.snapshot`     : the F-set IS `captured` (the managed set is
///                      authoritative; a parent file absent from `captured` is a
///                      DELETION and is dropped — B-commit-8/12, all-files
///                      commit). `parent` is consulted only for the no-op verdict.
///  - `.carry_forward`: `captured` files, plus every `parent` file whose
///                      filename is NOT in `captured`, carried forward at the
///                      parent's card unchanged (B-commit-11 partial commit; a
///                      root partial has an empty `parent`, so this degrades to
///                      `captured`).
pub const ComposePolicy = enum { snapshot, carry_forward };

/// Value inputs to `composeFCards`. `parent` is the parent check-in's F-set
/// (empty for a root); `captured` are the files read + hashed from the working
/// tree this commit; `policy` selects the merge rule above.
pub const ComposeInput = struct {
    parent: []const artifact.FCard,
    captured: []const artifact.FCard,
    policy: ComposePolicy,
    /// Sparse-cone RFC section 4.2: when set, a `.snapshot` compose ALSO
    /// carries forward every parent card the cone does not cover, byte-
    /// verbatim -- the snapshot semantics (deletion by absence) apply only
    /// inside the cone. Null = dense, bit-identical to the shipped compose.
    /// `.carry_forward` needs no cone: it already carries everything
    /// uncaptured, which subsumes the out-of-cone set.
    sparse: ?*const cone_mod.Cone = null,
};

/// The composed F-set and the no-op verdict.
pub const ComposeResult = struct {
    /// Merged F-set, ascending by filename per §4.5.3 (`artifact.compareBytes`),
    /// allocated on the caller's arena. Filenames are unique.
    f: []const artifact.FCard,
    /// True iff `f` is identical to `parent` on the file-set identity (filename +
    /// hash + mode, entrywise, in sorted order) — the child no-op verdict
    /// (B-commit-9). For a root (`parent` empty) this is exactly `f.len == 0`
    /// ("nothing tracked").
    unchanged: bool,
};

/// §4.5.3 ordering predicate: raw-byte ascending by filename (shorter-first on a
/// prefix tie, via `artifact.compareBytes`). Filenames are unique, so a strict
/// `.lt` total order needs no tiebreak.
fn lessByFilename(_: void, a: artifact.FCard, b: artifact.FCard) bool {
    return artifact.compareBytes(a.filename, b.filename) == .lt;
}

/// The one genuinely deep, RFC-normative unit of `commit`, extracted pure so it
/// is reusable and unit-tested without a subprocess: it merges already-hashed
/// `FCard` values into the new check-in's F-set and reports the no-op verdict.
/// No I/O, no clock, no env, no hashing — it rearranges values and allocates
/// only the result array on `arena` (value-in/value-out, so it lifts verbatim
/// into a future deep `checkin.compose`). (B-commit-8/9/11/12, §4.5.3.)
///
/// Algorithm: (1) gather `captured`, plus — under `.carry_forward` — every
/// `parent` card whose filename is neither a captured filename nor a captured
/// artifact §4.7.6 old-name provenance. A carried rename consumes that one-shot
/// provenance by clearing `old_name` and its regular-file `w` placeholder;
/// untouched parent files carry forward otherwise, then (2) sort ascending by
/// filename (§4.5.3); (3) in a safety build, drive `artifact.FCardAdmission`
/// over the sorted filenames to assert strict §4.5.2-r1 order with no reserved
/// names — a dup or mis-sort is a bug, never shipped; (4) the no-op verdict
/// compares the result to `parent` entrywise on filename+hash+mode (§4.5.3-
/// sorted parent from a parsed manifest, so the comparison is aligned).
/// Captured rename tokens remain intact; the verdict keys on filename+hash+mode,
/// so a rename changes the filename and is never a no-op.
pub fn composeFCards(arena: std.mem.Allocator, in: ComposeInput) error{OutOfMemory}!ComposeResult {
    var list: std.ArrayList(artifact.FCard) = .empty;
    errdefer list.deinit(arena);
    try list.appendSlice(arena, in.captured);
    if (in.policy == .carry_forward or in.sparse != null) {
        for (in.parent) |pc| {
            if (in.policy == .snapshot) {
                // Sparse snapshot: only OUT-OF-CONE parent cards carry; an
                // in-cone parent file absent from `captured` is a deletion,
                // exactly as in a dense snapshot (RFC section 4.2).
                if (in.sparse.?.covers(pc.filename)) continue;
            }
            var captured_here = false;
            for (in.captured) |cc| {
                if (std.mem.eql(u8, pc.filename, cc.filename) or
                    (cc.old_name != null and std.mem.eql(u8, pc.filename, cc.old_name.?)))
                {
                    captured_here = true;
                    break;
                }
            }
            if (!captured_here) {
                var carried = pc;
                if (carried.old_name != null) {
                    carried.old_name = null;
                    if (carried.mode == .regular and
                        carried.perm != null and
                        std.mem.eql(u8, carried.perm.?, "w"))
                    {
                        carried.perm = null;
                    }
                }
                try list.append(arena, carried);
            }
        }
    }
    std.mem.sort(artifact.FCard, list.items, {}, lessByFilename);

    // Postcondition (§4.5.2-r1 order + §4.6.3 admission): every card is admitted
    // — strictly ascending, no reserved name among managed paths. Safety-gated
    // so it costs nothing in ReleaseFast; a violation is a compose bug, not a
    // wire condition, so a trip here is correct.
    if (std.debug.runtime_safety) {
        var adm: artifact.FCardAdmission = .{};
        for (list.items) |card| {
            const kept = adm.admit(card.filename) catch |e|
                std.debug.panic("composeFCards produced out-of-order F cards: {s}", .{@errorName(e)});
            std.debug.assert(kept);
        }
    }

    const unchanged = blk: {
        if (list.items.len != in.parent.len) break :blk false;
        for (list.items, in.parent) |a, b| {
            if (!std.mem.eql(u8, a.filename, b.filename)) break :blk false;
            if (artifact.compareOptional(a.hash, b.hash) != .eq) break :blk false;
            if (a.mode != b.mode) break :blk false;
        }
        break :blk true;
    };
    return .{ .f = try list.toOwnedSlice(arena), .unchanged = unchanged };
}

/// The serialized size an F card contributes to a manifest: letter, spaces,
/// tokens, LF. An estimate only in the sense that §4.3 escapes can lengthen
/// a filename — identically in both runs being compared, so the comparison
/// is exact where it matters.
pub fn fCardCost(c: artifact.FCard) usize {
    var n: usize = 2 + c.filename.len + 1; // "F " + name + LF
    if (c.hash) |h| n += 1 + h.len;
    if (c.perm) |p| n += 1 + p.len;
    if (c.old_name) |o| n += 1 + o.len;
    return n;
}

/// A delta manifest's baseline: its raw F run and its name.
pub const DialectBase = struct {
    raw: []const artifact.FCard,
    hash: []const u8,
};

/// The baseline a delta manifest built on `parent` would name: the parent
/// when it is flat, or the parent's OWN baseline when it is a delta (§4.7.3:
/// never a delta of a delta). Null when the dialect does not continue: a tree
/// parent (§4.4: B and S never mix), a parent whose baseline is itself a delta
/// (foreign history the writer must not extend), or a flat parent in a
/// repository that has never stored a delta manifest (`seen_delta` false).
/// `baseline` is the parent's parsed baseline, present iff `parent.b` is.
pub fn dialectBase(
    parent: *const artifact.Manifest,
    baseline: ?*const artifact.Manifest,
    parent_hash: []const u8,
    seen_delta: bool,
) ?DialectBase {
    std.debug.assert((parent.b == null) == (baseline == null));
    if (parent.s.len != 0) return null;
    if (parent.b) |b| {
        if (baseline.?.b != null) return null;
        return .{ .raw = baseline.?.f, .hash = b.hash };
    }
    if (!seen_delta) return null;
    return .{ .raw = parent.f, .hash = parent_hash };
}

/// A delta manifest's B card and F run.
pub const DeltaPatch = struct {
    b: artifact.BCard,
    f: []const artifact.FCard,
};

/// The delta patch of `composed` (the new check-in's complete, sorted F-set)
/// against `base`, or null when the patch would not be smaller than the flat
/// run. A path present in the baseline and absent now becomes a delete
/// marker (an F card with no hash, §4.7.6); a path equal to the baseline's
/// card is omitted.
///
/// The size guard is in BYTES rather than cards (a delete marker is a dozen
/// bytes, a full card ~80 — counting them equal suppresses the dialect on
/// exactly the small patches deltas exist for): a patch whose F run is not
/// smaller than the flat one carries no benefit, so the flat form is kept and
/// the dialect left to the next commit.
pub fn deltaPatch(
    arena: std.mem.Allocator,
    composed: []const artifact.FCard,
    base: DialectBase,
) error{OutOfMemory}!?DeltaPatch {
    var cards: std.ArrayList(artifact.FCard) = .empty;
    var ci: usize = 0;
    var bi: usize = 0;
    while (ci < composed.len or bi < base.raw.len) {
        const order: std.math.Order = if (ci >= composed.len)
            .gt
        else if (bi >= base.raw.len)
            .lt
        else
            artifact.compareBytes(composed[ci].filename, base.raw[bi].filename);
        switch (order) {
            .lt => {
                try cards.append(arena, composed[ci]);
                ci += 1;
            },
            .gt => {
                try cards.append(arena, .{
                    .filename = base.raw[bi].filename,
                    .hash = null,
                    .perm = null,
                    .old_name = null,
                    .mode = .regular,
                });
                bi += 1;
            },
            .eq => {
                const c = composed[ci];
                const bl = base.raw[bi];
                const same = c.old_name == null and c.mode == bl.mode and
                    std.mem.eql(u8, c.hash orelse "", bl.hash orelse "") and
                    std.mem.eql(u8, c.perm orelse "", bl.perm orelse "");
                if (!same) try cards.append(arena, c);
                ci += 1;
                bi += 1;
            },
        }
    }
    var delta_bytes: usize = 0;
    for (cards.items) |c| delta_bytes += fCardCost(c);
    var flat_bytes: usize = 0;
    for (composed) |c| flat_bytes += fCardCost(c);
    if (delta_bytes >= flat_bytes) return null;
    return .{ .b = .{ .hash = base.hash }, .f = cards.items };
}

test "deltaPatch: a one-card change against a wide baseline is a patch; a rewrite is not" {
    var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    const h1_buf: [64]u8 = @splat('1');
    const h2_buf: [64]u8 = @splat('2');
    const h1: []const u8 = &h1_buf;
    const h2: []const u8 = &h2_buf;
    const base = [_]artifact.FCard{
        .{ .filename = "a", .hash = h1, .perm = null, .old_name = null, .mode = .regular },
        .{ .filename = "b", .hash = h1, .perm = null, .old_name = null, .mode = .regular },
        .{ .filename = "c", .hash = h1, .perm = null, .old_name = null, .mode = .regular },
    };
    const now = [_]artifact.FCard{
        .{ .filename = "a", .hash = h1, .perm = null, .old_name = null, .mode = .regular },
        .{ .filename = "b", .hash = h2, .perm = null, .old_name = null, .mode = .regular },
    };
    const got = (try deltaPatch(arena, &now, .{ .raw = &base, .hash = "base" })).?;
    try std.testing.expectEqualStrings("base", got.b.hash);
    try std.testing.expectEqual(@as(usize, 2), got.f.len);
    try std.testing.expectEqualStrings("b", got.f[0].filename);
    try std.testing.expectEqualStrings(h2, got.f[0].hash.?);
    try std.testing.expectEqualStrings("c", got.f[1].filename);
    try std.testing.expect(got.f[1].hash == null);

    const rewrite = [_]artifact.FCard{
        .{ .filename = "a", .hash = h2, .perm = null, .old_name = null, .mode = .regular },
    };
    try std.testing.expect(try deltaPatch(arena, &rewrite, .{ .raw = &base, .hash = "base" }) == null);
}