//! 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 repeat = @import("zit").repeat;
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);
}
test "git export e2e: a remembered mirror exports, autopushes, ignores contacts, and re-runs bare" {
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 cwd_abs = try std.process.currentPathAlloc(io, gpa);
defer gpa.free(cwd_abs);
const tmp_abs = try std.fs.path.join(gpa, &.{ cwd_abs, ".zig-cache", "tmp", &tmp.sub_path });
defer gpa.free(tmp_abs);
const remote = try std.fs.path.join(gpa, &.{ tmp_abs, "remote.git" });
defer gpa.free(remote);
const mirror = try std.fs.path.join(gpa, &.{ tmp_abs, "mirror" });
defer gpa.free(mirror);
{
const bare = try runProgram(gpa, io, tmp.dir, &.{ "git", "init", "--quiet", "--bare", "remote.git" });
defer freeResult(gpa, bare);
try expectExit(bare, 0);
}
// The committer gets a contact, which is not an input to the identity.
const user_row = "1700000000 'e2e' pw '' cap 'v' info 'placeholder'";
try writeFile(io, src, "users.txt", std.fmt.comptimePrint("config /user {d}\n{s}\n", .{ user_row.len, user_row }));
{
const merged = try runZit(gpa, io, src, &.{ "config", "merge", "users.txt" });
defer freeResult(gpa, merged);
try expectExit(merged, 0);
const set = try runZit(gpa, io, src, &.{ "users", "contact", "e2e", "E2E Person <[email protected]>" });
defer freeResult(gpa, set);
try expectExit(set, 0);
const shown = try runZit(gpa, io, src, &.{ "users", "contact", "e2e" });
defer freeResult(gpa, shown);
try expectExit(shown, 0);
try testing.expectEqualStrings("E2E Person <[email protected]>\n", shown.stdout);
}
// A password in the autopush URL is refused and nothing is remembered.
{
const refused = try runZit(gpa, io, src, &.{ "git", "export", "../mirror", "--autopush", "https://u:[email protected]/x.git" });
defer freeResult(gpa, refused);
try expectExit(refused, 2);
const status = try runZit(gpa, io, src, &.{ "git", "status" });
defer freeResult(gpa, status);
try expectExit(status, 0);
try testing.expectEqualStrings("mirror none\n", status.stdout);
}
{
const first = try runZit(gpa, io, src, &.{ "git", "export", "../mirror", "--autopush", remote });
defer freeResult(gpa, first);
try expectExit(first, 0);
const want = try std.fmt.allocPrint(gpa, "exported 3 commits into {s}\npushed trunk to {s} as main\n", .{ mirror, remote });
defer gpa.free(want);
try testing.expectEqualStrings(want, first.stdout);
}
{
const authors = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "remote.git", "log", "--format=%an <%ae>", "main" });
defer gpa.free(authors);
try testing.expectEqualStrings(repeat("e2e <[email protected]>\n", 3), authors);
}
// Remembered: a bare re-run from any directory exports what is new and
// pushes it, and status says the mirror is current.
try writeFile(io, src, "adir/alpha.txt", "alpha three\n");
try commitAll(gpa, io, src, "more");
{
const again = try runZit(gpa, io, tmp.dir, &.{ "git", "export", "-R", "src" });
defer freeResult(gpa, again);
try expectExit(again, 0);
try testing.expect(std.mem.startsWith(u8, again.stdout, "exported 1 commits into "));
const count = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "remote.git", "rev-list", "--count", "main" });
defer gpa.free(count);
try testing.expectEqualStrings("4\n", count);
}
{
const status = try runZit(gpa, io, src, &.{ "git", "status" });
defer freeResult(gpa, status);
try expectExit(status, 0);
const want_mirror = try std.fmt.allocPrint(gpa, "mirror {s}\nautopush {s}\npush-branch main\nemail-domain zit.invalid\n", .{ mirror, remote });
defer gpa.free(want_mirror);
try testing.expect(std.mem.startsWith(u8, status.stdout, want_mirror));
try testing.expect(std.mem.indexOf(u8, status.stdout, "exported 4 check-ins, last at ") != null);
try testing.expect(std.mem.endsWith(u8, status.stdout, "up-to-date yes\n"));
}
// Forgetting the autopush stops the pushing.
{
const off = try runZit(gpa, io, src, &.{ "git", "export", "--no-autopush" });
defer freeResult(gpa, off);
try expectExit(off, 0);
try testing.expectEqualStrings("exported 0 commits into ", off.stdout[0..24]);
try testing.expect(std.mem.indexOf(u8, off.stdout, "pushed") == null);
}
}
/// The repository config as the library reads it, for the Git email domains.
fn readConfig(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !struct { synced: []u8, local: []u8 } {
var file = try dir.openFile(io, ".zit", .{ .mode = .read_only });
defer file.close(io);
const st = try file.stat(io);
var fs = zit.storage.FileStorage.init(io, file, st.size);
var log = try zit.logstore.LogStore.openReadOnly(gpa, fs.storageFile());
defer log.deinit(gpa);
var oc = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult;
defer oc.deinit(gpa);
const synced = try gpa.dupe(u8, oc.config.git_email_domain);
errdefer gpa.free(synced);
return .{ .synced = synced, .local = try gpa.dupe(u8, oc.config.git_email_domain_local) };
}
/// Save `domain` the way a store did before the domain was synced: in the
/// local key.
fn saveLocalEmailDomain(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, domain: []const u8) !void {
var file = try dir.openFile(io, ".zit", .{ .mode = .read_write });
defer file.close(io);
const st = try file.stat(io);
var fs = zit.storage.FileStorage.init(io, file, st.size);
var log = try zit.logstore.LogStore.open(gpa, fs.storageFile(), 0);
defer log.deinit(gpa);
var oc = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult;
defer oc.deinit(gpa);
var next = oc.config;
next.git_email_domain_local = domain;
try log.putConfig(next);
try log.commit();
}
/// Rewrite a mirror's state file as version 1, the format written before the
/// identity rule was recorded: drop the two rule bytes after the trees flag
/// (an empty domain) and re-seal the checksum.
fn downgradeStateToV1(gpa: std.mem.Allocator, io: std.Io, mirror: std.Io.Dir) !void {
const name = zit.git_export.state_filename;
const bytes = try mirror.readFileAlloc(io, name, gpa, .limited(1 << 20));
defer gpa.free(bytes);
// magic(8) version(2) key(28) trees(1), then [contacts u8][len u8][domain].
const domain_len: usize = bytes[40];
var v1: std.ArrayList(u8) = .empty;
defer v1.deinit(gpa);
try v1.appendSlice(gpa, bytes[0..39]);
try v1.appendSlice(gpa, bytes[41 + domain_len .. bytes.len - 4]);
std.mem.writeInt(u16, v1.items[8..10], 1, .little);
var crc = zit.crc32.Crc32.init();
crc.update(v1.items);
var tail: [4]u8 = undefined;
std.mem.writeInt(u32, &tail, crc.final(), .little);
try v1.appendSlice(gpa, &tail);
var f = try mirror.createFile(io, name, .{ .truncate = true });
defer f.close(io);
try f.writePositionalAll(io, v1.items, 0);
}
test "git export e2e: both verbs give a check-in one commit ID, whatever the contacts say" {
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 user_row = "1700000000 'e2e' pw '' cap 'v' info 'placeholder'";
try writeFile(io, src, "users.txt", std.fmt.comptimePrint("config /user {d}\n{s}\n", .{ user_row.len, user_row }));
{
const merged = try runZit(gpa, io, src, &.{ "config", "merge", "users.txt" });
defer freeResult(gpa, merged);
try expectExit(merged, 0);
const set = try runZit(gpa, io, src, &.{ "users", "contact", "e2e", "E2E Person <[email protected]>" });
defer freeResult(gpa, set);
try expectExit(set, 0);
}
// `git export` sets the repository's domain; `export-git` reads it.
{
const first = try runZit(gpa, io, src, &.{ "git", "export", "../m1", "--email-domain", "example.org" });
defer freeResult(gpa, first);
try expectExit(first, 0);
const config = try readConfig(gpa, io, src);
defer gpa.free(config.synced);
defer gpa.free(config.local);
try testing.expectEqualStrings("example.org", config.synced);
const second = try runZit(gpa, io, src, &.{ "export-git", ".", "../m2" });
defer freeResult(gpa, second);
try expectExit(second, 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);
const authors = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "m2", "log", "--format=%an <%ae>|%cn <%ce>", "trunk" });
defer gpa.free(authors);
try testing.expectEqualStrings(repeat("e2e <[email protected]>|e2e <[email protected]>\n", 3), authors);
}
test "git export e2e: --email-domain on a mirror with history refuses unless it agrees" {
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 first = try runZit(gpa, io, src, &.{ "export-git", ".", "../m1", "--email-domain", "example.org" });
defer freeResult(gpa, first);
try expectExit(first, 0);
}
const before = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "m1", "for-each-ref" });
defer gpa.free(before);
try writeFile(io, src, "adir/alpha.txt", "alpha three\n");
try commitAll(gpa, io, src, "append");
for ([_][]const []const u8{
&.{ "export-git", ".", "../m1", "--email-domain", "example.net" },
&.{ "git", "export", "../m1", "--email-domain", "example.net" },
}) |argv| {
const refused = try runZit(gpa, io, src, argv);
defer freeResult(gpa, refused);
try expectExit(refused, 1);
try testing.expect(std.mem.indexOf(u8, refused.stderr, "example.org") != null);
try testing.expect(std.mem.indexOf(u8, refused.stderr, "example.net") != null);
const after = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "m1", "for-each-ref" });
defer gpa.free(after);
try testing.expectEqualStrings(before, after);
}
// The refused `git export` remembered nothing.
{
const config = try readConfig(gpa, io, src);
defer gpa.free(config.synced);
defer gpa.free(config.local);
try testing.expectEqualStrings("", config.synced);
}
// Not a domain at all is a usage error.
{
const bad = try runZit(gpa, io, src, &.{ "export-git", ".", "../m9", "--email-domain", "a b" });
defer freeResult(gpa, bad);
try expectExit(bad, 2);
}
// Agreeing, or saying nothing, continues the mirror under its rule, even
// though the repository's domain is still the default.
{
const agreed = try runZit(gpa, io, src, &.{ "export-git", ".", "../m1" });
defer freeResult(gpa, agreed);
try expectExit(agreed, 0);
try testing.expect(std.mem.indexOf(u8, agreed.stdout, "exported 1 commits") != null);
const authors = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "m1", "log", "--format=%ae", "trunk" });
defer gpa.free(authors);
try testing.expectEqualStrings(repeat("[email protected]\n", 4), authors);
}
}
test "git export e2e: a domain a store saved locally becomes the repository's synced domain" {
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);
try saveLocalEmailDomain(gpa, io, src, "legacy.example");
// export-git reads it without moving it.
{
const exported = try runZit(gpa, io, src, &.{ "export-git", ".", "../m1" });
defer freeResult(gpa, exported);
try expectExit(exported, 0);
const config = try readConfig(gpa, io, src);
defer gpa.free(config.synced);
defer gpa.free(config.local);
try testing.expectEqualStrings("", config.synced);
try testing.expectEqualStrings("legacy.example", config.local);
}
// git export moves it to the synced key.
{
const exported = try runZit(gpa, io, src, &.{ "git", "export", "../m2" });
defer freeResult(gpa, exported);
try expectExit(exported, 0);
const config = try readConfig(gpa, io, src);
defer gpa.free(config.synced);
defer gpa.free(config.local);
try testing.expectEqualStrings("legacy.example", config.synced);
try testing.expectEqualStrings("", config.local);
}
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);
const authors = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "m2", "log", "--format=%ae", "trunk" });
defer gpa.free(authors);
try testing.expectEqualStrings(repeat("[email protected]\n", 3), authors);
}
test "git export e2e: a mirror whose state predates the record keeps the rule its commits show" {
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 first = try runZit(gpa, io, src, &.{ "export-git", ".", "../m1" });
defer freeResult(gpa, first);
try expectExit(first, 0);
}
{
var mirror = try tmp.dir.openDir(io, "m1", .{});
defer mirror.close(io);
try downgradeStateToV1(gpa, io, mirror);
}
// The repository has since named a domain; the mirror's commits do not
// use it, and its later ones must not either.
{
const other = try runZit(gpa, io, src, &.{ "git", "export", "../m2", "--email-domain", "example.org" });
defer freeResult(gpa, other);
try expectExit(other, 0);
}
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 authors = try gitOut(gpa, io, tmp.dir, &.{ "git", "-C", "m1", "log", "--format=%ae", "trunk" });
defer gpa.free(authors);
try testing.expectEqualStrings(repeat("[email protected]\n", 4), authors);
// The inferred rule is now recorded: naming the repository's domain is
// refused against it.
try writeFile(io, src, "adir/alpha.txt", "alpha four\n");
try commitAll(gpa, io, src, "again");
{
const refused = try runZit(gpa, io, src, &.{ "export-git", ".", "../m1", "--email-domain", "example.org" });
defer freeResult(gpa, refused);
try expectExit(refused, 1);
try testing.expect(std.mem.indexOf(u8, refused.stderr, "zit.invalid") != null);
}
}