diff --git a/src/git.zig b/src/git.zig index 74e9960..14ca4fd 100644 --- a/src/git.zig +++ b/src/git.zig @@ -16,6 +16,7 @@ const std = @import("std"); const Date = @import("Date.zig"); +const test_git = @import("testutil/git.zig"); // ── Types ──────────────────────────────────────────────────── @@ -1009,40 +1010,82 @@ test "resolveCommitRange: --since with no earlier commit -> NoCommitAtOrBefore" try std.testing.expectError(error.NoCommitAtOrBefore, result); } -test "commitThatAdded finds the introducing commit and ignores later edits" { - // Uses the ambient zfin checkout: `build.zig` was added once and modified many - // times since, which is exactly the shape `--diff-filter=A` must see through. +test "commitThatAdded returns the introducing commit, not a later edit" { + // Builds its own two-commit history rather than reading the ambient checkout. + // + // The first version of this test asserted against `build.zig` in the zfin repo, + // reasoning that a long-lived file's add-commit must differ from the newest + // commit touching it. True locally, false in CI: `actions/checkout@v4` defaults + // to `fetch-depth: 1`, so the whole repo is ONE commit and add == newest. The + // test was coupled to clone depth, which is not a property of the code under + // test. A purpose-built repo makes the history explicit and deterministic. const allocator = std.testing.allocator; + if (!test_git.available(allocator)) return; + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const dir_len = try tmp.dir.realPathFile(std.testing.io, ".", &path_buf); + const dir = path_buf[0..dir_len]; + + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "snap.srf", .data = "v1\n" }); + try test_git.run(allocator, dir, null, &.{ "init", "-q" }); + try test_git.run(allocator, dir, null, &.{ "config", "user.email", "test@example.com" }); + try test_git.run(allocator, dir, null, &.{ "config", "user.name", "Test" }); + try test_git.run(allocator, dir, null, &.{ "config", "commit.gpgsign", "false" }); + try test_git.run(allocator, dir, null, &.{ "add", "snap.srf" }); + try test_git.run(allocator, dir, "2026-08-22T12:00:00", &.{ "commit", "-q", "-m", "add snapshot" }); + var env = gitTestEnv(allocator); defer env.deinit(); - const info = findRepo(std.testing.io, allocator, &env, "build.zig") catch return; - defer allocator.free(info.root); - defer allocator.free(info.rel_path); - const add_sha = (commitThatAdded(std.testing.io, allocator, &env, info.root, info.rel_path) catch return) orelse return; + const add_sha = (try commitThatAdded(std.testing.io, allocator, &env, dir, "snap.srf")).?; defer allocator.free(add_sha); try std.testing.expect(add_sha.len == 40 or add_sha.len == 64); for (add_sha) |c| try std.testing.expect(std.ascii.isHex(c)); - // The add-commit must be at-or-older than the newest commit touching the file. - // If `--diff-filter=A` were being ignored these would be equal for a file with - // any edit history, which `build.zig` certainly has. - const newest = (commitAtOrBeforeDate(std.testing.io, allocator, &env, info.root, &.{info.rel_path}, "2099-01-01") catch return) orelse return; + // Rewrite the file and commit again. This is the case that matters: one real + // commit restated 97 historical snapshots at once, and the anchor must ignore it. + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "snap.srf", .data = "v2 restated\n" }); + try test_git.run(allocator, dir, null, &.{ "add", "snap.srf" }); + try test_git.run(allocator, dir, "2026-08-29T12:00:00", &.{ "commit", "-q", "-m", "restate snapshot" }); + + // The add-commit is unchanged by the rewrite... + const add_again = (try commitThatAdded(std.testing.io, allocator, &env, dir, "snap.srf")).?; + defer allocator.free(add_again); + try std.testing.expectEqualStrings(add_sha, add_again); + + // ...while the newest commit touching the file has moved. Without + // `--diff-filter=A` these would now agree, which is the regression this guards. + const newest = (try commitAtOrBeforeDate(std.testing.io, allocator, &env, dir, &.{"snap.srf"}, "2099-01-01")).?; defer allocator.free(newest); try std.testing.expect(!std.mem.eql(u8, add_sha, newest)); } test "commitThatAdded returns null for a path never committed" { const allocator = std.testing.allocator; + if (!test_git.available(allocator)) return; + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const dir_len = try tmp.dir.realPathFile(std.testing.io, ".", &path_buf); + const dir = path_buf[0..dir_len]; + + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "snap.srf", .data = "v1\n" }); + try test_git.run(allocator, dir, null, &.{ "init", "-q" }); + try test_git.run(allocator, dir, null, &.{ "config", "user.email", "test@example.com" }); + try test_git.run(allocator, dir, null, &.{ "config", "user.name", "Test" }); + try test_git.run(allocator, dir, null, &.{ "config", "commit.gpgsign", "false" }); + try test_git.run(allocator, dir, null, &.{ "add", "snap.srf" }); + try test_git.run(allocator, dir, null, &.{ "commit", "-q", "-m", "initial" }); + var env = gitTestEnv(allocator); defer env.deinit(); - const info = findRepo(std.testing.io, allocator, &env, "build.zig") catch return; - defer allocator.free(info.root); - defer allocator.free(info.rel_path); // Null, not an error: `resolveSpec` relies on this to fall back to the date // behaviour for a portfolio kept without a committed history directory. - const sha_opt = commitThatAdded(std.testing.io, allocator, &env, info.root, "history/1970-01-01-portfolio.srf") catch return; + const sha_opt = try commitThatAdded(std.testing.io, allocator, &env, dir, "history/1970-01-01-portfolio.srf"); try std.testing.expect(sha_opt == null); }