Artifact b6088c525eda4dac7e30453ada82ca6ee20ac0bfa11a1b331bba4db01f845869:
- File
src/update_test.zig
— part of check-in
[7fcc9b5ee1]
at
2026-08-24 10:16:17
on branch update-dirty-tree-135
— update: carry uncommitted work across the move instead of refusing it
A dirty tree is now three-way merged (current check-in, working file, target) rather than rejected, so a branch tip that advanced under uncommitted work can be joined. Previously update, merge and stash all refused that state, leaving commit --allow-fork as the only exit -- the guard that keeps forks deliberate was manufacturing them. The fork guard itself is unchanged.
applyCheckinWith scans the tree once and calls back for the merge policy; handing carries in instead would scan every clean update twice, which cost a 20k-file no-op update 63%.
Paths that cannot merge either way (type change, edited here / deleted there) are named and refused before anything mutates.
Fixes #135 (user: dmestas, size: 52976) [annotate] [blame] [check-ins using] [more...]
//! Private `update.applyCheckin` contract tests. //! //! Tests that require update.zig file-private helpers or fault-injection seams //! remain inline with that implementation. const std = @import("std"); const artifact = @import("artifact.zig"); const checkout = @import("checkout.zig"); const logstore = @import("logstore.zig"); const managed = @import("managed.zig"); const storage = @import("storage.zig"); const update = @import("update.zig"); const ApplyError = update.ApplyError; const applyCheckin = update.applyCheckin; fn decodeValidatedName(name: []const u8, out: *[logstore.max_hash_len]u8) ApplyError![]const u8 { if (!artifact.validateName(name)) return error.InvalidCheckinId; const raw = out[0 .. name.len / 2]; _ = std.fmt.hexToBytes(raw, name) catch return error.InvalidCheckinId; return raw; } fn completeTestArtifact(gpa: std.mem.Allocator, body: []const u8) ![]u8 { var checksum_buf: [32]u8 = undefined; const checksum = artifact.selfChecksum(body, &checksum_buf); return std.fmt.allocPrint(gpa, "{s}Z {s}\n", .{ body, checksum }); } fn putTestArtifact(log: *logstore.LogStore, name: []const u8, bytes: []const u8) !void { var digest: [logstore.max_hash_len]u8 = undefined; const raw = try decodeValidatedName(name, &digest); _ = try log.put(raw, bytes); } fn putCompleteTestArtifact( gpa: std.mem.Allocator, log: *logstore.LogStore, body: []const u8, id_buf: *[64]u8, ) ![]const u8 { const bytes = try completeTestArtifact(gpa, body); defer gpa.free(bytes); const id = artifact.artifactName(.sha1, bytes, id_buf); try putTestArtifact(log, id, bytes); return id; } fn putTestPayload( log: *logstore.LogStore, bytes: []const u8, id_buf: *[64]u8, ) ![]const u8 { const id = artifact.artifactName(.sha1, bytes, id_buf); try putTestArtifact(log, id, bytes); return id; } fn putDeltaTestTarget( gpa: std.mem.Allocator, log: *logstore.LogStore, baseline_id: []const u8, id_buf: *[64]u8, ) ![]const u8 { const body = try std.fmt.allocPrint(gpa, "B {s}\nD 2007-07-21T14:10:00\nF next {s}\n", .{ baseline_id, "a" ** 40, }); defer gpa.free(body); return putCompleteTestArtifact(gpa, log, body, id_buf); } fn writeFixturePayload( io: std.Io, root: std.Io.Dir, path: []const u8, bytes: []const u8, mode: artifact.FileMode, ) !void { var file = try root.createFile(io, path, .{ .read = false, .exclusive = true, .permissions = switch (mode) { .regular => .default_file, .executable => .executable_file, .symlink => unreachable, }, .resolve_beneath = true, }); defer file.close(io); try file.writePositionalAll(io, bytes, 0); try file.sync(io); } fn modeMatches(stat: std.Io.File.Stat, mode: artifact.FileMode) bool { if (comptime std.Io.File.Permissions.has_executable_bit) { const executable = stat.permissions.toMode() & 0o111 != 0; return switch (mode) { .regular => !executable, .executable => executable, .symlink => false, }; } return mode != .symlink; } fn expectFileState( gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir, path: []const u8, expected_bytes: []const u8, expected_mode: artifact.FileMode, ) !void { const stat = try worktree.statFile(io, path, .{ .follow_symlinks = false }); try std.testing.expectEqual(.file, stat.kind); try std.testing.expect(modeMatches(stat, expected_mode)); var file = try worktree.openFile(io, path, .{ .allow_directory = false, .follow_symlinks = false, .resolve_beneath = true, }); defer file.close(io); const bytes = try gpa.alloc(u8, stat.size); defer gpa.free(bytes); const n = try file.readPositionalAll(io, bytes, 0); try std.testing.expectEqual(expected_bytes.len, n); try std.testing.expectEqualStrings(expected_bytes, bytes); } fn expectSymlinkTarget(io: std.Io, worktree: std.Io.Dir, path: []const u8, expected_target: []const u8) !void { const stat = try worktree.statFile(io, path, .{ .follow_symlinks = false }); try std.testing.expectEqual(.sym_link, stat.kind); var target: [1024]u8 = undefined; const n = try worktree.readLink(io, path, &target); try std.testing.expectEqualStrings(expected_target, target[0..n]); } const RootEntrySet = struct { names: std.ArrayList([]u8) = .empty, fn deinit(self: *RootEntrySet, gpa: std.mem.Allocator) void { for (self.names.items) |name| gpa.free(name); self.names.deinit(gpa); self.* = undefined; } fn contains(self: *const RootEntrySet, name: []const u8) bool { for (self.names.items) |candidate| { if (std.mem.eql(u8, candidate, name)) return true; } return false; } }; fn snapshotRootEntries(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir) !RootEntrySet { var entries = RootEntrySet{}; errdefer entries.deinit(gpa); var root = try worktree.openDir(io, ".", .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); defer root.close(io); var iterator = root.iterate(); while (try iterator.next(io)) |entry| { const name = try gpa.dupe(u8, entry.name); entries.names.append(gpa, name) catch |err| { gpa.free(name); return err; }; } return entries; } fn expectSameRootEntries(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir, expected: *const RootEntrySet) !void { var actual = try snapshotRootEntries(gpa, io, worktree); defer actual.deinit(gpa); try std.testing.expectEqual(expected.names.items.len, actual.names.items.len); for (expected.names.items) |name| try std.testing.expect(actual.contains(name)); } fn seedUntouchedWorktree(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir) !void { try checkout.write(io, worktree, .{ .version = "old-checkout", .branch = "old-branch", .merge_parent = "", .special_op = "", .manifest_flags = "" }); var old_managed = managed.ManagedSet.init(gpa); defer old_managed.deinit(); _ = try old_managed.insert("old-file"); try old_managed.write(io, worktree); try writeFixturePayload(io, worktree, "old-file", "old executable bytes", .executable); try writeFixturePayload(io, worktree, "witness", "unmanaged witness bytes", .regular); } fn expectUntouchedWorktree(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir) !void { var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, worktree, &checkout_buf)).?; try std.testing.expectEqualStrings("old-checkout", state.version); try std.testing.expectEqualStrings("old-branch", state.branch); var current_managed = try managed.ManagedSet.read(gpa, io, worktree); defer current_managed.deinit(); try std.testing.expectEqual(@as(usize, 1), current_managed.iterate().len); try std.testing.expect(current_managed.contains("old-file")); try expectFileState(gpa, io, worktree, "old-file", "old executable bytes", .executable); try expectFileState(gpa, io, worktree, "witness", "unmanaged witness bytes", .regular); } fn setupRollbackFailureFixture( gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir, log: *logstore.LogStore, target_id_buf: *[64]u8, old_id_buf: *[64]u8, ) ![]const u8 { var payload_id_buf: [64]u8 = undefined; const payload_id = artifact.artifactName(.sha1, "", &payload_id_buf); try putTestArtifact(log, payload_id, ""); const old_body = try std.fmt.allocPrint(gpa, "D 2007-07-21T14:09:59\nF old {s}\n", .{payload_id}); defer gpa.free(old_body); const old_bytes = try completeTestArtifact(gpa, old_body); defer gpa.free(old_bytes); const old_id = artifact.artifactName(.sha1, old_bytes, old_id_buf); try putTestArtifact(log, old_id, old_bytes); const target_body = try std.fmt.allocPrint(gpa, "D 2007-07-21T14:10:00\nF new {s}\n", .{payload_id}); defer gpa.free(target_body); const target_bytes = try completeTestArtifact(gpa, target_body); defer gpa.free(target_bytes); const target_id = artifact.artifactName(.sha1, target_bytes, target_id_buf); try putTestArtifact(log, target_id, target_bytes); try checkout.write(io, worktree, .{ .version = old_id, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" }); var old_managed = managed.ManagedSet.init(gpa); defer old_managed.deinit(); _ = try old_managed.insert("old"); try old_managed.write(io, worktree); try writeFixturePayload(io, worktree, "old", "", .regular); return target_id; } fn retainedTransactionContainsPrevious(io: std.Io, worktree: std.Io.Dir) !bool { var root = try worktree.openDir(io, ".", .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); defer root.close(io); var iterator = root.iterate(); while (try iterator.next(io)) |entry| { if (entry.kind != .directory or !std.mem.startsWith(u8, entry.name, ".zit-tmp-txn-")) continue; var transaction = try root.openDir(io, entry.name, .{ .access_sub_paths = true, .follow_symlinks = false }); defer transaction.close(io); var previous = transaction.openDir(io, "previous", .{ .access_sub_paths = true, .follow_symlinks = false }) catch continue; defer previous.close(io); const old = previous.statFile(io, "old", .{ .follow_symlinks = false }) catch continue; if (old.kind == .file) return true; } return false; } test "update strict target and baseline failures leave the checkout untouched" { const t = std.testing; const Case = struct { kind: enum { target_missing, target_phantom, target_misnamed, target_non_manifest, baseline_missing, baseline_phantom, baseline_misnamed, baseline_non_manifest, baseline_delta, }, expected: ApplyError, }; const cases = [_]Case{ .{ .kind = .target_missing, .expected = error.CheckinNotFound }, .{ .kind = .target_phantom, .expected = error.CheckinContentMissing }, .{ .kind = .target_misnamed, .expected = error.CheckinNameMismatch }, .{ .kind = .target_non_manifest, .expected = error.TargetNotManifest }, .{ .kind = .baseline_missing, .expected = error.BaselineNotFound }, .{ .kind = .baseline_phantom, .expected = error.BaselineContentMissing }, .{ .kind = .baseline_misnamed, .expected = error.BaselineNameMismatch }, .{ .kind = .baseline_non_manifest, .expected = error.BaselineNotManifest }, .{ .kind = .baseline_delta, .expected = error.BaselineIsDelta }, }; for (cases) |case| { var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); try seedUntouchedWorktree(t.allocator, io, tmp.dir); var root_entries = try snapshotRootEntries(t.allocator, io, tmp.dir); defer root_entries.deinit(t.allocator); var target_id_buf: [64]u8 = undefined; var target_id: []const u8 = undefined; switch (case.kind) { .target_missing => target_id = "a" ** 40, .target_phantom => { target_id = "b" ** 40; var digest: [logstore.max_hash_len]u8 = undefined; _ = try log.intern(try decodeValidatedName(target_id, &digest)); }, .target_misnamed => { const expected_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:09:59\nF expected aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n"); defer t.allocator.free(expected_bytes); target_id = artifact.artifactName(.sha1, expected_bytes, &target_id_buf); const actual_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:10:00\nF actual bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n"); defer t.allocator.free(actual_bytes); try putTestArtifact(&log, target_id, actual_bytes); }, .target_non_manifest => { target_id = try putCompleteTestArtifact(t.allocator, &log, "D 2007-07-21T14:09:59\nT +branch aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa trunk\nU alice\n", &target_id_buf); }, .baseline_missing => { target_id = try putDeltaTestTarget(t.allocator, &log, "c" ** 40, &target_id_buf); }, .baseline_phantom => { const baseline_id = "d" ** 40; var digest: [logstore.max_hash_len]u8 = undefined; _ = try log.intern(try decodeValidatedName(baseline_id, &digest)); target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf); }, .baseline_misnamed => { const expected_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:09:59\nF expected aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n"); defer t.allocator.free(expected_bytes); var baseline_id_buf: [64]u8 = undefined; const baseline_id = artifact.artifactName(.sha1, expected_bytes, &baseline_id_buf); const actual_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:10:00\nF actual bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n"); defer t.allocator.free(actual_bytes); try putTestArtifact(&log, baseline_id, actual_bytes); target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf); }, .baseline_non_manifest => { var baseline_id_buf: [64]u8 = undefined; const baseline_id = try putCompleteTestArtifact(t.allocator, &log, "D 2007-07-21T14:09:59\nT +branch aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa trunk\nU alice\n", &baseline_id_buf); target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf); }, .baseline_delta => { var baseline_id_buf: [64]u8 = undefined; const baseline_body = try std.fmt.allocPrint(t.allocator, "B {s}\nD 2007-07-21T14:09:59\nF inherited aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", .{"e" ** 40}); defer t.allocator.free(baseline_body); const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf); target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf); }, } try t.expectError(case.expected, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .inherit)); try expectUntouchedWorktree(t.allocator, io, tmp.dir); try expectSameRootEntries(t.allocator, io, tmp.dir, &root_entries); } } test "update delta applies inherited replacements and deletions across multiple entries" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var inherited_id_buf: [64]u8 = undefined; const inherited_id = try putTestPayload(&log, "inherited bytes", &inherited_id_buf); var original_id_buf: [64]u8 = undefined; const original_id = try putTestPayload(&log, "original bytes", &original_id_buf); var removed_id_buf: [64]u8 = undefined; const removed_id = try putTestPayload(&log, "removed bytes", &removed_id_buf); var replacement_id_buf: [64]u8 = undefined; const replacement_id = try putTestPayload(&log, "replacement bytes", &replacement_id_buf); const baseline_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF inherited {s}\nF removed {s}\nF replaced {s}\n", .{ inherited_id, removed_id, original_id, }); defer t.allocator.free(baseline_body); var baseline_id_buf: [64]u8 = undefined; const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf); const target_body = try std.fmt.allocPrint(t.allocator, "B {s}\nD 2007-07-21T14:10:00\nF removed\nF replaced {s}\nF unmatched\n", .{ baseline_id, replacement_id, }); defer t.allocator.free(target_body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .inherit); try expectFileState(t.allocator, io, tmp.dir, "inherited", "inherited bytes", .regular); try expectFileState(t.allocator, io, tmp.dir, "replaced", "replacement bytes", .regular); try t.expectError(error.FileNotFound, tmp.dir.statFile(io, "removed", .{ .follow_symlinks = false })); try t.expectError(error.FileNotFound, tmp.dir.statFile(io, "unmatched", .{ .follow_symlinks = false })); var current_managed = try managed.ManagedSet.read(t.allocator, io, tmp.dir); defer current_managed.deinit(); try t.expectEqual(@as(usize, 2), current_managed.iterate().len); try t.expect(current_managed.contains("inherited")); try t.expect(current_managed.contains("replaced")); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(target_id, state.version); } test "update touches only the diff: an unchanged file is not rewritten, and the tree still equals a fresh materialization" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var stable_id_buf: [64]u8 = undefined; const stable_id = try putTestPayload(&log, "stable bytes", &stable_id_buf); var v1_id_buf: [64]u8 = undefined; const v1_id = try putTestPayload(&log, "moving v1", &v1_id_buf); var v2_id_buf: [64]u8 = undefined; const v2_id = try putTestPayload(&log, "moving v2", &v2_id_buf); const baseline_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF moving {s}\nF stable {s}\n", .{ v1_id, stable_id }); defer t.allocator.free(baseline_body); var baseline_id_buf: [64]u8 = undefined; const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf); const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF moving {s}\nF stable {s}\n", .{ v2_id, stable_id }); defer t.allocator.free(target_body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, baseline_id, .inherit, .inherit); const before = try tmp.dir.statFile(io, "stable", .{ .follow_symlinks = false }); try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .inherit); // The canary that the skip actually fired: materialization writes a fresh // file and moves it into place, so a rewritten `stable` would carry a new // inode. The SAME inode proves the unchanged path was never staged, // deleted, or rewritten -- the point of the diff. const after = try tmp.dir.statFile(io, "stable", .{ .follow_symlinks = false }); try t.expectEqual(before.inode, after.inode); // And the oracle that skipping never changes the result: the updated tree // is byte-identical to what a from-scratch materialization of the target // produces. A wrong diff -- an entry skipped that should have moved -- // fails here as a content mismatch, not silently. try expectFileState(t.allocator, io, tmp.dir, "stable", "stable bytes", .regular); try expectFileState(t.allocator, io, tmp.dir, "moving", "moving v2", .regular); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(target_id, state.version); } test "update refuses a dirtied or deleted file that the diff would have skipped" { const t = std.testing; const io = t.io; // The invariant #52 requires pinned BEFORE any skip optimization: a file // that is byte-identical between baseline and target -- exactly the one // markUnchanged marks and the sidecar plan carries -- must still be // repaired-or-refused when the working tree disagrees with the baseline. // Both mutations must die in `requireClean`, before any mark is computed. for ([_]enum { dirty, delete }{ .dirty, .delete }) |mutation| { var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var stable_id_buf: [64]u8 = undefined; const stable_id = try putTestPayload(&log, "stable bytes", &stable_id_buf); var v1_id_buf: [64]u8 = undefined; const v1_id = try putTestPayload(&log, "moving v1", &v1_id_buf); var v2_id_buf: [64]u8 = undefined; const v2_id = try putTestPayload(&log, "moving v2", &v2_id_buf); const baseline_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF moving {s}\nF stable {s}\n", .{ v1_id, stable_id }); defer t.allocator.free(baseline_body); var baseline_id_buf: [64]u8 = undefined; const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf); const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF moving {s}\nF stable {s}\n", .{ v2_id, stable_id }); defer t.allocator.free(target_body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, baseline_id, .inherit, .inherit); switch (mutation) { .dirty => { try tmp.dir.deleteFile(io, "stable"); try writeFixturePayload(io, tmp.dir, "stable", "tampered", .regular); }, .delete => try tmp.dir.deleteFile(io, "stable"), } try t.expectError(error.WorktreeNotClean, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .inherit)); // Refused means REFUSED: the moving file is still at v1, the record // still names the baseline. try expectFileState(t.allocator, io, tmp.dir, "moving", "moving v1", .regular); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(baseline_id, state.version); } } test "update behaves identically with the sidecar plan, without it, and with a stale one" { const t = std.testing; const io = t.io; // Three second-updates over identical repositories: sidecar intact (the // no-flatten fast path), sidecar deleted (the flatten fallback), and // sidecar rebound to a WRONG check-in id (must be ignored, not trusted). // All three must land the same tree and the same record -- the sidecar is // an accelerator, never an input. for ([_]enum { intact, absent, stale }{ .intact, .absent, .stale }) |variant| { var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var stable_id_buf: [64]u8 = undefined; const stable_id = try putTestPayload(&log, "stable bytes", &stable_id_buf); var v1_id_buf: [64]u8 = undefined; const v1_id = try putTestPayload(&log, "moving v1", &v1_id_buf); var v2_id_buf: [64]u8 = undefined; const v2_id = try putTestPayload(&log, "moving v2", &v2_id_buf); const baseline_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF moving {s}\nF stable {s}\n", .{ v1_id, stable_id }); defer t.allocator.free(baseline_body); var baseline_id_buf: [64]u8 = undefined; const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf); const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF moving {s}\nF stable {s}\n", .{ v2_id, stable_id }); defer t.allocator.free(target_body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, baseline_id, .inherit, .inherit); switch (variant) { .intact => {}, .absent => try tmp.dir.deleteFile(io, ".zit-cache/vstat"), .stale => { // Rebind the sidecar to a different id by rewriting the header // byte-for-byte EXCEPT the checkin field, keeping the CRC // honest -- a syntactically perfect sidecar for the wrong // check-in, the exact shape trust must refuse. var f = try tmp.dir.openFile(io, ".zit-cache/vstat", .{}); defer f.close(io); const st = try f.stat(io); const buf = try t.allocator.alloc(u8, @intCast(st.size)); defer t.allocator.free(buf); _ = try f.readPositionalAll(io, buf, 0); // magic(8) + version(2) + checkin_len(2) then the id bytes. buf[12] ^= 0x01; // flip one id byte const body = buf[0 .. buf.len - 4]; var crc = @import("crc32.zig").Crc32.init(); crc.update(body); std.mem.writeInt(u32, buf[buf.len - 4 ..][0..4], crc.final(), .little); var rw = try tmp.dir.createFile(io, ".zit-cache/vstat", .{ .truncate = true }); defer rw.close(io); try rw.writePositionalAll(io, buf, 0); }, } try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .inherit); try expectFileState(t.allocator, io, tmp.dir, "stable", "stable bytes", .regular); try expectFileState(t.allocator, io, tmp.dir, "moving", "moving v2", .regular); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(target_id, state.version); } } test "update stages the full tree when the baseline is unborn, unchanged marks notwithstanding" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var one_id_buf: [64]u8 = undefined; const one_id = try putTestPayload(&log, "only bytes", &one_id_buf); const body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF only {s}\n", .{one_id}); defer t.allocator.free(body); var id_buf: [64]u8 = undefined; const id = try putCompleteTestArtifact(t.allocator, &log, body, &id_buf); // An unborn baseline has an EMPTY plan, so nothing can be marked unchanged // and open-style materialization is exactly what it always was. try applyCheckin(t.allocator, io, tmp.dir, &log, id, .inherit, .inherit); try expectFileState(t.allocator, io, tmp.dir, "only", "only bytes", .regular); } test "update non-ancestor control tags use persisted frame order over RID order" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var target_payload_id_buf: [64]u8 = undefined; const target_payload_id = try putTestPayload(&log, "target bytes", &target_payload_id_buf); const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF target {s}\n", .{target_payload_id}); defer t.allocator.free(target_body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf); const RID_first_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nT +branch {s} rid-first\nU alice\n", .{target_id}); defer t.allocator.free(RID_first_body); const RID_first_bytes = try completeTestArtifact(t.allocator, RID_first_body); defer t.allocator.free(RID_first_bytes); var RID_first_id_buf: [64]u8 = undefined; const RID_first_id = artifact.artifactName(.sha1, RID_first_bytes, &RID_first_id_buf); var RID_first_digest: [logstore.max_hash_len]u8 = undefined; const RID_first = try log.intern(try decodeValidatedName(RID_first_id, &RID_first_digest)); const persisted_first_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nT +branch {s} persisted-first\nU alice\n", .{target_id}); defer t.allocator.free(persisted_first_body); const persisted_first_bytes = try completeTestArtifact(t.allocator, persisted_first_body); defer t.allocator.free(persisted_first_bytes); var persisted_first_id_buf: [64]u8 = undefined; const persisted_first_id = artifact.artifactName(.sha1, persisted_first_bytes, &persisted_first_id_buf); try putTestArtifact(&log, persisted_first_id, persisted_first_bytes); var persisted_first_digest: [logstore.max_hash_len]u8 = undefined; const persisted_first = log.lookup(try decodeValidatedName(persisted_first_id, &persisted_first_digest)).?; try t.expect(RID_first < persisted_first); try putTestArtifact(&log, RID_first_id, RID_first_bytes); try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .inherit); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings("persisted-first", state.branch); } test "update applyCheckin rejects a non-manifest before checkout mutation" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); const payload = "ordinary blob, not a structural artifact"; var id_buf: [64]u8 = undefined; const target_id = artifact.artifactName(.sha1, payload, &id_buf); var digest: [logstore.max_hash_len]u8 = undefined; const raw_digest = try decodeValidatedName(target_id, &digest); _ = try log.put(raw_digest, payload); try checkout.write(io, tmp.dir, .{ .version = "old", .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" }); try t.expectError(error.TargetNotManifest, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .inherit)); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings("old", state.version); try t.expectEqualStrings("trunk", state.branch); } test "update retains the staged rollback tree when inverse rollback fails" { const t = std.testing; const io = t.io; var probe_tmp = t.tmpDir(.{}); defer probe_tmp.cleanup(); var probe_raw_file = try probe_tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer probe_raw_file.close(io); var probe_storage = storage.FileStorage.init(io, probe_raw_file, 0); var probe_log = try logstore.LogStore.open(t.allocator, probe_storage.storageFile(), 0); defer probe_log.deinit(t.allocator); var probe_target_id_buf: [64]u8 = undefined; var probe_old_id_buf: [64]u8 = undefined; const probe_target_id = try setupRollbackFailureFixture(t.allocator, io, probe_tmp.dir, &probe_log, &probe_target_id_buf, &probe_old_id_buf); var probe_allocator = std.testing.FailingAllocator.init(t.allocator, .{}); try applyCheckin(probe_allocator.allocator(), io, probe_tmp.dir, &probe_log, probe_target_id, .inherit, .inherit); const allocation_count = probe_allocator.alloc_index; try t.expect(allocation_count > 0); // The rollback-arming allocation is no longer the run's LAST: the stat // sidecar writes (and allocates, swallowed) after the commit point, so // "fail the final allocation" now lands harmlessly inside it and the // update simply succeeds. Walk the fail index downward from the probe // count until the injected failure genuinely reaches the rollback path -- // a successful attempt is discarded with its tree, a fresh fixture built, // and the walk is bounded by the probe count itself. var fail_index: usize = allocation_count; const found = while (fail_index > 0) : (fail_index -= 1) { var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var target_id_buf: [64]u8 = undefined; var old_id_buf: [64]u8 = undefined; const target_id = try setupRollbackFailureFixture(t.allocator, io, tmp.dir, &log, &target_id_buf, &old_id_buf); var failing_allocator = std.testing.FailingAllocator.init(t.allocator, .{ .fail_index = fail_index - 1 }); const result = applyCheckin(failing_allocator.allocator(), io, tmp.dir, &log, target_id, .inherit, .inherit); if (result) |_| continue else |err| { if (err != error.RollbackFailed) continue; try t.expect(failing_allocator.has_induced_failure); try t.expect(try retainedTransactionContainsPrevious(io, tmp.dir)); try t.expect((try tmp.dir.statFile(io, "old", .{ .follow_symlinks = false })).kind == .file); try t.expectError(error.FileNotFound, tmp.dir.statFile(io, "new", .{ .follow_symlinks = false })); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(old_id_buf[0..40], state.version); var restored_managed = try managed.ManagedSet.read(t.allocator, io, tmp.dir); defer restored_managed.deinit(); try t.expectEqual(@as(usize, 1), restored_managed.iterate().len); try t.expect(restored_managed.contains("old")); break true; } } else false; try t.expect(found); } test "update applyCheckin treats a present-but-unborn checkout record as a clean tree" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); const payload = "unborn-tree payload\n"; var payload_id_buf: [64]u8 = undefined; const payload_id = try putTestPayload(&log, payload, &payload_id_buf); const body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF only.txt {s}\n", .{payload_id}); defer t.allocator.free(body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, body, &target_id_buf); // The exact bootstrap `init` writes and `clone` inherits: a checkout record // that is PRESENT but names no check-in, with no managed set beside it. The // e2e proves this state as clone actually produces it; this pins the // semantics at the layer that owns them, so a regression fails fast and // locally instead of only through a networked clone. try checkout.write(io, tmp.dir, .{ .version = "", .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" }); try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .inherit); try expectFileState(t.allocator, io, tmp.dir, "only.txt", payload, .regular); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(target_id, state.version); } test "update applyCheckin still refuses an unborn record that has a managed set" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); const payload = "unborn-tree payload\n"; var payload_id_buf: [64]u8 = undefined; const payload_id = try putTestPayload(&log, payload, &payload_id_buf); const body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF only.txt {s}\n", .{payload_id}); defer t.allocator.free(body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, body, &target_id_buf); // An unborn record is only "clean" when nothing is managed. A record that // names no check-in but claims managed paths is incoherent state, not a // fresh clone — the tolerance added for clone must NOT swallow it. try checkout.write(io, tmp.dir, .{ .version = "", .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" }); var set = managed.ManagedSet.init(t.allocator); defer set.deinit(); _ = try set.insert("stray.txt"); try set.write(io, tmp.dir); try t.expectError(error.WorktreeNotClean, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .inherit)); } test "update applyCheckin refuses an unborn record that claims a pending operation" { const t = std.testing; const io = t.io; // The other half of the unborn coherence rule: a record naming no check-in // cannot also carry a merge parent or a special operation, because both // describe work in progress against a check-in the record does not name. // An empty managed set alone does not make such a record a fresh clone. const cases = [_]checkout.CheckoutState{ .{ .version = "", .branch = "trunk", .merge_parent = "b" ** 64, .special_op = "", .manifest_flags = "" }, .{ .version = "", .branch = "trunk", .merge_parent = "", .special_op = "merge", .manifest_flags = "" }, }; for (cases) |state| { var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); const payload = "pending-op payload\n"; var payload_id_buf: [64]u8 = undefined; const payload_id = try putTestPayload(&log, payload, &payload_id_buf); const body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF only.txt {s}\n", .{payload_id}); defer t.allocator.free(body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, body, &target_id_buf); try checkout.write(io, tmp.dir, state); try t.expectError( error.CurrentStateInvalid, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .inherit), ); } } test "update materializes symlink targets and transitions them with regular files" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var link_target_id_buf: [64]u8 = undefined; const link_target_id = try putTestPayload(&log, "../actual-target", &link_target_id_buf); var regular_id_buf: [64]u8 = undefined; const regular_id = try putTestPayload(&log, "regular replacement\n", ®ular_id_buf); const link_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF entry {s} l\n", .{link_target_id}); defer t.allocator.free(link_body); var link_id_buf: [64]u8 = undefined; const link_id = try putCompleteTestArtifact(t.allocator, &log, link_body, &link_id_buf); const regular_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF entry {s}\n", .{regular_id}); defer t.allocator.free(regular_body); var regular_checkin_id_buf: [64]u8 = undefined; const regular_checkin_id = try putCompleteTestArtifact(t.allocator, &log, regular_body, ®ular_checkin_id_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, link_id, .inherit, .inherit); try expectSymlinkTarget(io, tmp.dir, "entry", "../actual-target"); try applyCheckin(t.allocator, io, tmp.dir, &log, regular_checkin_id, .inherit, .inherit); try expectFileState(t.allocator, io, tmp.dir, "entry", "regular replacement\n", .regular); try applyCheckin(t.allocator, io, tmp.dir, &log, link_id, .inherit, .inherit); try expectSymlinkTarget(io, tmp.dir, "entry", "../actual-target"); } /// A `CarryResolver` that records what the scan handed it and answers with a /// fixed carry set. Lets a test observe the divergence list on the real path /// the CLI uses, rather than through a second entry point that exists only for /// tests to call. const RecordingResolver = struct { gpa: std.mem.Allocator, seen: std.ArrayList(update.Divergence) = .empty, answer: []const update.Carry = &.{}, refuse: bool = false, fn deinit(self: *RecordingResolver) void { for (self.seen.items) |d| self.gpa.free(d.path); self.seen.deinit(self.gpa); } fn resolver(self: *RecordingResolver) update.CarryResolver { return .{ .ctx = self, .resolve = resolve }; } fn resolve( ctx: *anyopaque, divergences: []const update.Divergence, ) error{ OutOfMemory, Refused }![]const update.Carry { const self: *RecordingResolver = @ptrCast(@alignCast(ctx)); for (divergences) |d| { try self.seen.append(self.gpa, .{ .path = try self.gpa.dupe(u8, d.path), .kind = d.kind, }); } if (self.refuse) return error.Refused; return self.answer; } }; test "a carrying update sees every kind of divergence instead of stopping at the first" { const t = std.testing; const io = t.io; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var a_buf: [64]u8 = undefined; const a_id = try putTestPayload(&log, "alpha", &a_buf); var b_buf: [64]u8 = undefined; const b_id = try putTestPayload(&log, "bravo", &b_buf); var c_buf: [64]u8 = undefined; const c_id = try putTestPayload(&log, "charlie", &c_buf); const body = try std.fmt.allocPrint( t.allocator, "D 2007-07-21T14:09:59\nF edited {s}\nF gone {s}\nF untouched {s}\n", .{ a_id, b_id, c_id }, ); defer t.allocator.free(body); var checkin_buf: [64]u8 = undefined; const checkin = try putCompleteTestArtifact(t.allocator, &log, body, &checkin_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, checkin, .inherit, .inherit); // One of each: content drift, a deleted managed file, and a path the // checkout manages that the check-in has never heard of. The refusing scan // stops at whichever it reaches first; this one must report all three. try tmp.dir.deleteFile(io, "edited"); try writeFixturePayload(io, tmp.dir, "edited", "alpha, revised", .regular); try tmp.dir.deleteFile(io, "gone"); try writeFixturePayload(io, tmp.dir, "extra", "not in the check-in", .regular); { var set = try managed.ManagedSet.read(t.allocator, io, tmp.dir); defer set.deinit(); _ = try set.insert("extra"); try set.write(io, tmp.dir); } var rec = RecordingResolver{ .gpa = t.allocator, .refuse = true }; defer rec.deinit(); // Refusing keeps the tree untouched, so the assertions below are about the // scan alone. try t.expectError(error.WorktreeNotClean, update.applyCheckinWith(t.allocator, io, tmp.dir, &log, checkin, .inherit, .inherit, null, rec.resolver())); try t.expectEqual(@as(usize, 3), rec.seen.items.len); // Ascending by path, so a caller that reports the list needs no sort. try t.expectEqualStrings("edited", rec.seen.items[0].path); try t.expectEqual(update.DivergenceKind.modified, rec.seen.items[0].kind); try t.expectEqualStrings("extra", rec.seen.items[1].path); try t.expectEqual(update.DivergenceKind.added, rec.seen.items[1].kind); try t.expectEqualStrings("gone", rec.seen.items[2].path); try t.expectEqual(update.DivergenceKind.missing, rec.seen.items[2].kind); // "untouched" is clean and therefore absent: the list is divergences, not // an inventory. for (rec.seen.items) |d| try t.expect(!std.mem.eql(u8, d.path, "untouched")); } test "a clean tree never reaches the resolver, so carrying costs it nothing" { const t = std.testing; const io = t.io; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var v1_buf: [64]u8 = undefined; const v1 = try putTestPayload(&log, "version one", &v1_buf); var v2_buf: [64]u8 = undefined; const v2 = try putTestPayload(&log, "version two", &v2_buf); const base_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF moving {s}\n", .{v1}); defer t.allocator.free(base_body); var base_buf: [64]u8 = undefined; const base = try putCompleteTestArtifact(t.allocator, &log, base_body, &base_buf); const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF moving {s}\n", .{v2}); defer t.allocator.free(target_body); var target_buf: [64]u8 = undefined; const target = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, base, .inherit, .inherit); // The resolver is a policy for divergence. A clean tree has none, so it is // never consulted -- the property that keeps the one extra scan from // costing a clean update anything. var rec = RecordingResolver{ .gpa = t.allocator, .refuse = true }; defer rec.deinit(); try update.applyCheckinWith(t.allocator, io, tmp.dir, &log, target, .inherit, .inherit, null, rec.resolver()); try t.expectEqual(@as(usize, 0), rec.seen.items.len); try expectFileState(t.allocator, io, tmp.dir, "moving", "version two", .regular); } test "a carried update materializes the carried bytes and still moves the checkout" { const t = std.testing; const io = t.io; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var v1_buf: [64]u8 = undefined; const v1 = try putTestPayload(&log, "version one", &v1_buf); var v2_buf: [64]u8 = undefined; const v2 = try putTestPayload(&log, "version two", &v2_buf); const base_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF moving {s}\n", .{v1}); defer t.allocator.free(base_body); var base_buf: [64]u8 = undefined; const base = try putCompleteTestArtifact(t.allocator, &log, base_body, &base_buf); const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF moving {s}\n", .{v2}); defer t.allocator.free(target_body); var target_buf: [64]u8 = undefined; const target = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, base, .inherit, .inherit); try tmp.dir.deleteFile(io, "moving"); try writeFixturePayload(io, tmp.dir, "moving", "locally edited", .regular); // Without a resolver this is exactly the refusal the clean path has always // made — the guard is not gone, it is opted out of per call. try t.expectError( error.WorktreeNotClean, applyCheckin(t.allocator, io, tmp.dir, &log, target, .inherit, .inherit), ); const carry = [_]update.Carry{.{ .path = "moving", .after = .{ .bytes = "merged result" }, .before = .{ .bytes = "locally edited" }, }}; var rec = RecordingResolver{ .gpa = t.allocator, .answer = &carry }; defer rec.deinit(); try update.applyCheckinWith(t.allocator, io, tmp.dir, &log, target, .inherit, .inherit, null, rec.resolver()); // The carry wins over the target's own content for that path... try expectFileState(t.allocator, io, tmp.dir, "moving", "merged result", .regular); // ...and the checkout still moved, which is the difference between this and // a merge: no merge parent, just a new version. var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(target, state.version); try t.expectEqualStrings("", state.merge_parent); // The stat sidecar must not claim the carried file matches the target's // digest, or the next scan would call a locally-edited file clean. var rec2 = RecordingResolver{ .gpa = t.allocator, .refuse = true }; defer rec2.deinit(); try t.expectError(error.WorktreeNotClean, update.applyCheckinWith(t.allocator, io, tmp.dir, &log, target, .inherit, .inherit, null, rec2.resolver())); try t.expectEqual(@as(usize, 1), rec2.seen.items.len); try t.expectEqualStrings("moving", rec2.seen.items[0].path); try t.expectEqual(update.DivergenceKind.modified, rec2.seen.items[0].kind); }