diff --git a/.mise.toml b/.mise.toml index 01afb8d..4c58f64 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,5 +1,6 @@ [tools] prek = "0.4.1" +pre-commit = "4.6.0" zig = "0.16.0" zls = "0.16.0" "ubi:DonIsaac/zlint" = "0.7.9" diff --git a/src/portfolio_loader.zig b/src/portfolio_loader.zig index 7a1d34d..e782032 100644 --- a/src/portfolio_loader.zig +++ b/src/portfolio_loader.zig @@ -672,8 +672,57 @@ test "loadFromBytes: all-empty bytes returns empty portfolio" { // Kept minimal (one happy-path, one missing-file case); the // bulk of the logic is in `loadFromBytes`, covered above. +/// Build an environment map from the parent process with the GIT_* +/// variables stripped out. +/// +/// When these tests run inside a git hook (pre-commit, prek, etc.), +/// the hook runner sets `GIT_INDEX_FILE`, `GIT_DIR`, and +/// `GIT_WORK_TREE` to point at the outer repo's staging state. Git +/// inherits those env vars unconditionally - `git -C ` +/// changes the CWD but does NOT clear these env vars. The result is +/// that `git init` in our temp dir succeeds but subsequent `git +/// add`/`git commit` operate against the OUTER repo's index, with +/// blob references that don't exist in our temp dir's object store +/// ("invalid object 100644 for ''"). +/// +/// The hook runner can't fix this for us; per upstream guidance, +/// hooks (and code shelled out by hooks) that run git against a +/// different repo must explicitly opt out of the inherited env. See +/// https://github.com/j178/prek/issues/1786 for the prek-specific +/// instance and https://pre-commit.com/ for the analogous pre-commit +/// guidance. +fn buildScrubbedEnv(allocator: std.mem.Allocator) !std.process.Environ.Map { + var map = try testing.environ.createMap(allocator); + errdefer map.deinit(); + + // Strip every GIT_* variable. Iterating-while-removing isn't + // supported on the underlying ArrayHashMap, so collect first + // then remove. Keys are duped because `swapRemove` frees the + // map's owned key buffer, which would otherwise leave us with + // dangling pointers in `keys_to_remove`. + var keys_to_remove: std.ArrayList([]u8) = .empty; + defer { + for (keys_to_remove.items) |k| allocator.free(k); + keys_to_remove.deinit(allocator); + } + + var it = map.iterator(); + while (it.next()) |entry| { + if (std.mem.startsWith(u8, entry.key_ptr.*, "GIT_")) { + try keys_to_remove.append(allocator, try allocator.dupe(u8, entry.key_ptr.*)); + } + } + for (keys_to_remove.items) |key| _ = map.swapRemove(key); + + return map; +} + /// Run a one-shot git command in `cwd` for test setup. Panics on -/// failure — these tests can't proceed without a working repo. +/// failure - these tests can't proceed without a working repo. +/// +/// Uses `buildScrubbedEnv` to drop inherited `GIT_*` env vars; see +/// that function's doc comment for why this matters under git +/// hooks. fn gitInTestRepo(allocator: std.mem.Allocator, cwd: []const u8, argv: []const []const u8) !void { const full_argv = try allocator.alloc([]const u8, argv.len + 3); defer allocator.free(full_argv); @@ -681,8 +730,13 @@ fn gitInTestRepo(allocator: std.mem.Allocator, cwd: []const u8, argv: []const [] full_argv[1] = "-C"; full_argv[2] = cwd; @memcpy(full_argv[3..], argv); + + var env_map = try buildScrubbedEnv(allocator); + defer env_map.deinit(); + const result = try std.process.run(allocator, testing.io, .{ .argv = full_argv, + .environ_map = &env_map, .stdout_limit = .limited(64 * 1024), }); defer allocator.free(result.stdout);