Artifact fac80d3baef1452f954859547ede127aaf5144de9a834f7af6ed64a8b0d66481:
- File
src/control_paths.zig
— part of check-in
[503637a369]
at
2026-08-18 21:28:15
on branch trunk
— stash: reserve .zit-stash so it cannot be tracked
The sidecar was absent from the control-path registry, so extras listed it and add accepted it: an ordinary add put the contents of stashed work into a check-in and synced it to every peer, recoverable only by shunning. The stash help already promised "never an artifact"; nothing enforced it.
Authoritative tier, matching what losing the file costs. (user: dmestas, size: 5817) [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; } } /// What losing the file costs -- the property that dictates its format, its /// durability discipline, and who may delete it. The registry below is the /// single authority on which names zit owns at a checkout root; every other /// list (extras exclusions, the ignore-glob pins, the case tables) is derived /// from or tested against it, so a name added here without its glob line or /// case coverage is a TEST FAILURE, not quiet drift. pub const Tier = enum { /// Cannot be rebuilt: losing it loses work (the store, and the checkout /// container carrying the record, managed set, undo image and bisect /// session). authoritative, /// Discardable by construction, rebuilt on demand; deleting it must never /// change an answer (repocache, vstat). advisory, /// Exists only while an operation is in flight; a crash strands one, and a /// stranger must be able to identify and sweep it without understanding it. scratch, }; pub const Reserved = struct { name: []const u8, tier: Tier, /// A prefix entry reserves every root beginning with `name`, not one name. prefix: bool = false, }; /// The whole policy: three names and a prefix. The legacy tier (absorbed /// container siblings, the pre-directory cache/vstat spellings, retired /// scratch spellings, import provenance) was deleted after the consolidation's /// migration release: a leftover in a never-re-opened old checkout is now /// visible to `zit extras` and deletable by `zit clean` — the correct end /// state. A legacy AUTHORITATIVE sibling still migrates losslessly: the /// container's byte-1 dispatch folds `.zit-managed`/`.zit-undo`/`.zit-bisect` /// in on first contact (checkout.zig §8); only the reservation is gone. pub const reserved = [_]Reserved{ .{ .name = ".zit", .tier = .authoritative }, .{ .name = ".zit-checkout", .tier = .authoritative }, .{ .name = ".zit-stash", .tier = .authoritative }, .{ .name = ".zit-cache", .tier = .advisory }, .{ .name = ".zit-precedent", .tier = .advisory }, .{ .name = ".zit-tmp-", .tier = .scratch, .prefix = true }, }; /// 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]; for (reserved) |entry| { if (entry.prefix) { if (asciiStartsWithIgnoreCase(root, entry.name)) return true; } else if (asciiEqlIgnoreCase(root, entry.name)) return true; } return false; } 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" { // Generated from the registry, so a new entry gets its coverage by // existing: the bare name, a child path, a case-flipped copy, and the // near-miss (`name ++ "x"`) -- which for a PREFIX entry must be reserved // rather than refused, that being what a prefix means. inline for (reserved) |entry| { try std.testing.expect(isReservedRoot(entry.name)); try std.testing.expect(isReservedRoot(entry.name ++ "/sub")); var flipped: [entry.name.len]u8 = undefined; for (entry.name, 0..) |c, i| { flipped[i] = if (std.ascii.isLower(c)) std.ascii.toUpper(c) else std.ascii.toLower(c); } try std.testing.expect(isReservedRoot(&flipped)); try std.testing.expectEqual(entry.prefix, isReservedRoot(entry.name ++ "x")); } // Hand-written rows the generator cannot derive: shapes that LOOK like // control names and must not be. const negatives = [_][]const u8{ "", "zit", ".zit-other/data", ".zit-cacheable/data", ".zit-vstats/x", ".zit-checkout-user/file", ".zit-update-txn", ".zit-update-txns/user", "refs-old/heads/main", "sub/.zit/object", // Deleted legacy tier, pinned unreserved: these are ordinary // working-tree names now. ".zit-managed", ".zit-undo", ".zit-bisect", ".zit-vstat", ".zit-pack-tmp", ".zit-update-txn-x", ".zit-import-tmp-x", ".zit-import-map.jsonl", "refs", "refs/heads/main", }; for (negatives) |path| try std.testing.expect(!isReservedRoot(path)); }