Artifact beb6e26b27b0d0d7b85d1f8e52f0e29458e503eab6aae7b63358af7e26f78324:
- File
src/control_paths.zig
— part of check-in
[8797f53546]
at
2026-08-10 13:41:20
on branch sub-manifest
— update: cost tracks the diff, not the width
Three fixes, each found by measuring the previous one's residue.
markUnchanged: a path byte-identical between baseline and target -- same digest, same mode, with requireClean having already proven the on-disk file matches -- is skipped by every mutation phase: staging both directions, the undo journal, deletion and rewrite. An update used to delete and rewrite the entire tree regardless of how little changed.
.zit-vstat: the stat sidecar written at materialization is now the BASELINE PLAN -- version-bound, CRC'd, carrying mode and plan order -- so requireClean rebuilds the baseline with zero tree flattens when it describes the current check-in, and proves most files clean by inode/size/mtime instead of reconstructing their content out of the log. Advisory throughout: any mismatch, staleness or corruption falls back to the flatten and the content compare, which stay authoritative.
ManagedSet.fromSortedPaths: building the 66k-entry managed set through insert was quadratic -- an O(n) sorted-position memmove plus the LIVE sortedUnique assert per call -- and measured at 23.9 SECONDS of a pkgsrc update, larger than every real phase combined. The plan's own order is already sorted and unique, so the set is now built in one pass, order enforced per adjacent pair, invariant asserted once.
Measured, same store and targets as #52's gate: sqlite (2.2k files): 117.8s flat -> 0.6s adjacent / 1.2s distant pkgsrc (66k files): 42.2s flat -> 6.8s adjacent / 8.9s distant pkgsrc user CPU: ~25s -> 1.3s Residue is stat I/O (two ~66k-file sweeps), not computation.
The lockstep flatten prune #52 proposed was never built: the flatten measured 178ms at 66k files once the real costs fell.
Tests: dirty AND delete of a skipped-diff file both refuse with the tree untouched; sidecar intact/absent/rebound-to-wrong-check-in all produce identical trees; an unchanged file provably keeps its inode; an updated tree equals a from-scratch materialization. Closes #52. (user: dmestas, size: 3703) [annotate] [blame] [check-ins using]
const std = @import("std"); pub const WorktreePathError = error{ InvalidPath, ReservedPath, }; /// Valid relative working-tree paths use `/` separators, contain no empty or /// dot components, and never claim a reserved root. Import and update share /// this check so neither can admit a path the other would later reject. pub fn validateWorktreePath(path: []const u8) WorktreePathError!void { if (path.len == 0 or std.fs.path.isAbsolute(path)) return error.InvalidPath; if (std.mem.indexOfScalar(u8, path, '\\') != null) return error.InvalidPath; var components = std.mem.splitScalar(u8, path, '/'); const root = components.next() orelse return error.InvalidPath; if (root.len == 0 or std.mem.eql(u8, root, ".") or std.mem.eql(u8, root, "..")) return error.InvalidPath; if (isReservedRoot(path)) return error.ReservedPath; while (components.next()) |component| { if (component.len == 0 or std.mem.eql(u8, component, ".") or std.mem.eql(u8, component, "..")) return error.InvalidPath; } } /// Root-level checkout metadata names that imported and materialized paths /// must never own. Inspects only the first slash-delimited path component. pub fn isReservedRoot(path: []const u8) bool { const slash = std.mem.indexOfScalar(u8, path, '/') orelse path.len; const root = path[0..slash]; const names = [_][]const u8{ ".zit", ".zit-checkout", ".zit-managed", ".zit-undo", ".zit-bisect", ".zit-cache", ".zit-vstat", ".zit-pack-tmp", ".zit-import-map.jsonl", "refs", }; for (names) |reserved| if (asciiEqlIgnoreCase(root, reserved)) return true; return asciiStartsWithIgnoreCase(root, ".zit-update-txn-"); } fn asciiEqlIgnoreCase(a: []const u8, b: []const u8) bool { if (a.len != b.len) return false; for (a, b) |left, right| { if (std.ascii.toLower(left) != std.ascii.toLower(right)) return false; } return true; } fn asciiStartsWithIgnoreCase(value: []const u8, prefix: []const u8) bool { return value.len >= prefix.len and asciiEqlIgnoreCase(value[0..prefix.len], prefix); } test "control path policy reserves complete roots case-insensitively" { const Case = struct { path: []const u8, reserved: bool }; const cases = [_]Case{ .{ .path = ".zit", .reserved = true }, .{ .path = ".ZiT/object", .reserved = true }, .{ .path = ".zit-checkout", .reserved = true }, .{ .path = ".ZiT-Checkout/state", .reserved = true }, .{ .path = ".zit-managed/entries", .reserved = true }, .{ .path = ".zit-undo/transaction", .reserved = true }, .{ .path = ".zit-bisect/state", .reserved = true }, .{ .path = ".zit-cache/data", .reserved = true }, .{ .path = ".zit-pack-tmp/scratch", .reserved = true }, .{ .path = ".zit-import-map.jsonl/sidecar", .reserved = true }, .{ .path = "ReFs/heads/main", .reserved = true }, .{ .path = ".zit-update-txn-user/file", .reserved = true }, .{ .path = ".ZiT-UpDaTe-TxN-user/file", .reserved = true }, .{ .path = "", .reserved = false }, .{ .path = "zit", .reserved = false }, .{ .path = ".zit-other/data", .reserved = false }, .{ .path = ".zit-cacheable/data", .reserved = false }, .{ .path = ".zit-checkout-user/file", .reserved = false }, .{ .path = ".zit-update-txn", .reserved = false }, .{ .path = ".zit-update-txns/user", .reserved = false }, .{ .path = "refs-old/heads/main", .reserved = false }, .{ .path = "sub/.zit/object", .reserved = false }, }; for (cases) |case_| try std.testing.expectEqual(case_.reserved, isReservedRoot(case_.path)); }