Artifact de80ef5fb1da9dc73577411768e4f9a97b6f65d0087daee1e0cc4e22aa0dc879:
- File
src/git/git_export_e2e.zig
— part of check-in
[0910bc00f5]
at
2026-09-17 13:39:56
on branch trunk
— log: open once, on the zero-parse image
log reached past the open funnel and called Repository.open with the default options, which is the full record replay meant for write verbs, and did so twice for a symbolic name. On pkgsrc that was 0.94s before the first row; on the image path it is 0.05-0.10s, output byte-identical on every route. The open mode has no default any more: every direct caller declares it, so the next verb written past the funnel cannot inherit the expensive one silently. An e2e runs every log route under the derive-footprint seam and fails if any of them replays a record. (user: dmestas, size: 15063) [annotate] [blame] [check-ins using]
//! Real-Git differential tests for `zit export-git`: drive the BUILT zit //! binary and the real `git`, then verify commit by commit against the seam //! — `git ls-tree`/`cat-file` rows compared to the source repository's own //! enumeration, metadata against the manifest's P/C/D cards, refs and HEAD //! against the derived state. Determinism and incrementality are proven by //! OID equality across fresh and appended runs. Never compared against real //! Fossil or `fossil git export` — the oracle is zit's seam plus Git's own //! read-back of what we wrote (DESIGN §2, §11). const std = @import("std"); const build_options = @import("build_options"); const zit = @import("zit"); const testing = std.testing; const zit_exe: []const u8 = build_options.zit_exe; fn runProgram(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, argv: []const []const u8) !std.process.RunResult { return std.process.run(gpa, io, .{ .argv = argv, .cwd = .{ .dir = dir }, .stdout_limit = .limited(1 << 24), .stderr_limit = .limited(1 << 20), }); } fn runZit(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, tail: []const []const u8) !std.process.RunResult { const cwd_abs = try std.process.currentPathAlloc(io, gpa); defer gpa.free(cwd_abs); const abs_exe = try std.fs.path.resolve(gpa, &.{ cwd_abs, zit_exe }); defer gpa.free(abs_exe); var argv_buf: [16][]const u8 = undefined; argv_buf[0] = abs_exe; std.debug.assert(tail.len <= argv_buf.len - 1); for (tail, argv_buf[1..][0..tail.len]) |src, *dst| dst.* = src; return runProgram(gpa, io, dir, argv_buf[0 .. 1 + tail.len]); } fn expectExit(result: std.process.RunResult, want: u8) !void { switch (result.term) { .exited => |code| { if (code != want) { std.debug.print("exit {d} (want {d})\nstdout: {s}\nstderr: {s}\n", .{ code, want, result.stdout, result.stderr }); return error.TestUnexpectedExitCode; } }, else => return error.TestUnexpectedExitCode, } } fn freeResult(gpa: std.mem.Allocator, result: std.process.RunResult) void { gpa.free(result.stdout); gpa.free(result.stderr); } fn writeFile(io: std.Io, dir: std.Io.Dir, sub: []const u8, bytes: []const u8) !void { if (std.fs.path.dirname(sub)) |parent| dir.createDir(io, parent, .default_dir) catch {}; var f = try dir.createFile(io, sub, .{ .truncate = true }); defer f.close(io); try f.writePositionalAll(io, bytes, 0); } fn commitAll(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, message: []const u8) !void { const add = try runZit(gpa, io, dir, &.{"addremove"}); defer freeResult(gpa, add); try expectExit(add, 0); const ci = try runZit(gpa, io, dir, &.{ "commit", "-m", message, "--user", "e2e" }); defer freeResult(gpa, ci); try expectExit(ci, 0); } /// The deterministic fixture: linear history + branch + delete + executable /// file, built through the real zit binary. Returns nothing; the state is on /// disk in `dir`. fn buildFixture(gpa: std.mem.Allocator, io: std.Io, tmp_dir: std.Io.Dir) !std.Io.Dir { tmp_dir.createDir(io, "src", .default_dir) catch {}; const dir = try tmp_dir.openDir(io, "src", .{}); const init = try runZit(gpa, io, dir, &.{"init"}); defer freeResult(gpa, init); try expectExit(init, 0); try writeFile(io, dir, "adir/alpha.txt", "alpha one\n"); try writeFile(io, dir, "bdir/beta.txt", "beta one\n"); try commitAll(gpa, io, dir, "root"); try writeFile(io, dir, "adir/alpha.txt", "alpha two\n"); try writeFile(io, dir, "cdir/gamma.txt", "gamma\n"); try commitAll(gpa, io, dir, "grow"); // A delete: addremove drops the vanished file. dir.deleteFile(io, "bdir/beta.txt") catch {}; try commitAll(gpa, io, dir, "shrink"); return dir; } fn gitOut(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, argv: []const []const u8) ![]u8 { const result = try runProgram(gpa, io, dir, argv); defer gpa.free(result.stderr); errdefer gpa.free(result.stdout); try expectExit(result, 0); return result.stdout; } test "export-git e2e: a real export fscks clean and every commit's tree matches the seam enumeration" { const gpa = testing.allocator; const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); var src = try buildFixture(gpa, io, tmp.dir); defer src.close(io); const exported = try runZit(gpa, io, src, &.{ "export-git", ".", "../mirror" }); defer freeResult(gpa, exported); try expectExit(exported, 0); { const fsck = try runProgram(gpa, io, tmp.dir, &.{ "git", "-C", "mirror", "fsck", "--strict" }); defer freeResult(gpa, fsck); try expectExit(fsck, 0); } // Per-commit differential via the state file's hash/OID pairs. var repo: zit.Repository = undefined; try zit.Repository.open(&repo, gpa, io, src, .{ .mode = .full }); defer repo.deinit(gpa); var state = try zit.git_export.loadState(gpa, io, try tmp.dir.openDir(io, "mirror", .{})); defer state.deinit(); try testing.expectEqual(@as(usize, 3), state.records.len); for (state.records) |record| { const rid = repo.lookup(record.hash) orelse return error.TestUnexpectedResult; var fl = (try repo.files(gpa, rid)) orelse return error.TestUnexpectedResult; defer fl.deinit(gpa); // `git ls-tree -r` rows: path -> blob OID. var lstree_argv_buf: [8][]const u8 = undefined; lstree_argv_buf[0..6].* = .{ "git", "-C", "mirror", "ls-tree", "-r", record.oid }; const listing = try gitOut(gpa, io, tmp.dir, lstree_argv_buf[0..6]); defer gpa.free(listing); var git_paths: std.StringHashMapUnmanaged([]const u8) = .empty; defer git_paths.deinit(gpa); var lines = std.mem.splitScalar(u8, listing, '\n'); while (lines.next()) |line| { if (line.len == 0) continue; // `<mode> blob <oid>\t<path>` const tab = std.mem.indexOfScalar(u8, line, '\t') orelse return error.TestUnexpectedResult; const meta = line[0..tab]; const path = line[tab + 1 ..]; var fields = std.mem.tokenizeScalar(u8, meta, ' '); _ = fields.next(); // mode _ = fields.next(); // type const oid = fields.next() orelse return error.TestUnexpectedResult; try git_paths.put(gpa, path, oid); } // Every seam file appears with matching CONTENT (via cat-file). var count: usize = 0; var it = fl.iterator(); while (it.next()) |card| { count += 1; const git_oid = git_paths.get(card.filename) orelse { std.debug.print("missing path in git tree: {s}\n", .{card.filename}); return error.TestUnexpectedResult; }; var cat_argv: [6][]const u8 = .{ "git", "-C", "mirror", "cat-file", "blob", git_oid }; const git_bytes = try gitOut(gpa, io, tmp.dir, &cat_argv); defer gpa.free(git_bytes); const zit_bytes = (try repo.blob(gpa, card.hash.?)) orelse return error.TestUnexpectedResult; defer gpa.free(zit_bytes); try testing.expectEqualSlices(u8, zit_bytes, git_bytes); } try testing.expectEqual(count, git_paths.count()); } } test "export-git e2e: two fresh exports are OID-identical, and incremental equals fresh" { const gpa = testing.allocator; const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); var src = try buildFixture(gpa, io, tmp.dir); defer src.close(io); // Fresh export twice: identical refs. for ([_][]const u8{ "../m1", "../m2" }) |dest| { const exported = try runZit(gpa, io, src, &.{ "export-git", ".", dest }); defer freeResult(gpa, exported); try expectExit(exported, 0); } const refs1 = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "m1", "for-each-ref" }); defer gpa.free(refs1); const refs2 = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "m2", "for-each-ref" }); defer gpa.free(refs2); try testing.expectEqualStrings(refs1, refs2); // Append a commit; incremental m1 must equal a fresh m3. try writeFile(io, src, "adir/alpha.txt", "alpha three\n"); try commitAll(gpa, io, src, "append"); { const incr = try runZit(gpa, io, src, &.{ "export-git", ".", "../m1" }); defer freeResult(gpa, incr); try expectExit(incr, 0); try testing.expect(std.mem.indexOf(u8, incr.stdout, "exported 1 commits") != null); } { const fresh = try runZit(gpa, io, src, &.{ "export-git", ".", "../m3" }); defer freeResult(gpa, fresh); try expectExit(fresh, 0); } const refs_incr = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "m1", "for-each-ref" }); defer gpa.free(refs_incr); const refs_fresh = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "m3", "for-each-ref" }); defer gpa.free(refs_fresh); try testing.expectEqualStrings(refs_incr, refs_fresh); } test "export-git e2e: a DEST inside an enclosing git repository gets its own repo; the enclosing refs never move" { const gpa = testing.allocator; const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); // The enclosing repository — the role the CI workspace checkout played // when this class of failure was caught: DEST is a fresh subdirectory // inside it, and `rev-parse --git-dir` resolves upward to HERE. for ([_][]const []const u8{ &.{ "git", "init", "-q", "-b", "trunk", "." }, &.{ "git", "-c", "user.email=e@x", "-c", "user.name=e", "commit", "--allow-empty", "-q", "-m", "enclosing" }, }) |argv| { const r = try runProgram(gpa, io, tmp.dir, argv); defer freeResult(gpa, r); try expectExit(r, 0); } const enclosing_tip = try gitOut(gpa, io, tmp.dir, &.{ "git", "rev-parse", "refs/heads/trunk" }); defer gpa.free(enclosing_tip); var src = try buildFixture(gpa, io, tmp.dir); defer src.close(io); const exported = try runZit(gpa, io, src, &.{ "export-git", ".", "../mirror" }); defer freeResult(gpa, exported); try expectExit(exported, 0); // The mirror is its own repository with the fixture's three commits… const mirror_gitdir = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "mirror", "rev-parse", "--git-dir" }); defer gpa.free(mirror_gitdir); try testing.expectEqualStrings(".git", std.mem.trim(u8, mirror_gitdir, " \r\n")); // …and the enclosing repository's trunk did not move. const after = try gitOut(gpa, io, tmp.dir, &.{ "git", "rev-parse", "refs/heads/trunk" }); defer gpa.free(after); try testing.expectEqualStrings(enclosing_tip, after); } test "export-git e2e: a foreign commit on a mirror ref refuses with exit 1 and unchanged state" { const gpa = testing.allocator; const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); var src = try buildFixture(gpa, io, tmp.dir); defer src.close(io); { const exported = try runZit(gpa, io, src, &.{ "export-git", ".", "../mirror" }); defer freeResult(gpa, exported); try expectExit(exported, 0); } // Foreign work: a commit made directly in the mirror moves trunk. for ([_][]const []const u8{ &.{ "git", "-C", "mirror", "checkout", "-q", "trunk" }, &.{ "git", "-C", "mirror", "-c", "user.email=f@x", "-c", "user.name=f", "commit", "--allow-empty", "-q", "-m", "foreign" }, }) |argv| { const r = try runProgram(gpa, io, tmp.dir, argv); defer freeResult(gpa, r); try expectExit(r, 0); } const state_before = try tmp.dir.readFileAlloc(io, "mirror/" ++ zit.git_export.state_filename, gpa, .limited(1 << 20)); defer gpa.free(state_before); try writeFile(io, src, "adir/alpha.txt", "post-foreign\n"); try commitAll(gpa, io, src, "post-foreign"); const refused = try runZit(gpa, io, src, &.{ "export-git", ".", "../mirror" }); defer freeResult(gpa, refused); try expectExit(refused, 1); try testing.expect(std.mem.indexOf(u8, refused.stderr, "moved since the last export") != null); const state_after = try tmp.dir.readFileAlloc(io, "mirror/" ++ zit.git_export.state_filename, gpa, .limited(1 << 20)); defer gpa.free(state_after); try testing.expectEqualSlices(u8, state_before, state_after); } test "export-git e2e: a .git path segment in the source refuses with exit 1 naming the path" { const gpa = testing.allocator; const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); // Author the hostile manifest through the library (the CLI itself refuses // to add such paths, which is the point — imported histories may not). { var raw = try tmp.dir.createFile(io, ".zit", .{ .read = true }); var fs = zit.storage.FileStorage.init(io, raw, 0); var log = try zit.logstore.LogStore.open(gpa, fs.storageFile(), 0); try log.putConfig(.{ .hash_policy = 0, .default_branch = "trunk", .project_id = "p" }); var digest: [32]u8 = undefined; std.crypto.hash.sha3.Sha3_256.hash("evil\n", &digest, .{}); _ = try log.put(&digest, "evil\n"); var blob_hex: [64]u8 = undefined; const digits = "0123456789abcdef"; for (&digest, 0..) |b, i| { blob_hex[i * 2] = digits[b >> 4]; blob_hex[i * 2 + 1] = digits[b & 0x0f]; } var m: zit.artifact.Manifest = .{ .arena_state = std.heap.ArenaAllocator.init(gpa), .kind = .manifest, .a = null, .b = null, .c = .{ .comment = "hostile" }, .d = .{ .timestamp = "2020-01-01T00:00:00" }, .e = null, .g = null, .h = null, .i = null, .k = null, .l = null, .n = null, .r = null, .u = .{ .login = "u" }, .z = null, .w = null, .f = &.{.{ .filename = ".git/hooks/pwn", .hash = &blob_hex, .perm = null, .old_name = null, .mode = .regular }}, .j = &.{}, .m = &.{}, .p = &.{}, .q = &.{}, .t = &.{.{ .sign = .propagate, .name = "branch", .target = null, .value = "trunk" }}, }; defer m.deinit(); const bytes = try zit.artifact.create(&m, gpa); defer gpa.free(bytes); var mdigest: [32]u8 = undefined; std.crypto.hash.sha3.Sha3_256.hash(bytes, &mdigest, .{}); _ = try log.put(&mdigest, bytes); try log.commit(); log.deinit(gpa); raw.close(io); } const refused = try runZit(gpa, io, tmp.dir, &.{ "export-git", ".", "mirror" }); defer freeResult(gpa, refused); try expectExit(refused, 1); try testing.expect(std.mem.indexOf(u8, refused.stderr, ".git/hooks/pwn") != null); }