|
4 | 4 | import org.junit.jupiter.api.io.TempDir; |
5 | 5 |
|
6 | 6 | import java.nio.file.Path; |
| 7 | +import java.util.Map; |
7 | 8 |
|
8 | 9 | import static org.assertj.core.api.Assertions.assertThat; |
9 | 10 | import static org.assertj.core.api.Assertions.assertThatCode; |
| 11 | +import static org.junit.jupiter.api.Assumptions.assumeTrue; |
10 | 12 |
|
11 | 13 | /** |
12 | 14 | * Unit tests for {@link RepositoryIdentity}. |
13 | 15 | * Validates graceful degradation when git metadata is unavailable. |
| 16 | + * |
| 17 | + * <p>Git invocations in these tests are hermetic: we override every global-config |
| 18 | + * setting that can cause {@code git commit} / {@code git init} to fail silently |
| 19 | + * on a developer machine but pass on CI (or vice-versa) — most notably |
| 20 | + * {@code commit.gpgsign}, {@code tag.gpgsign}, {@code core.hooksPath}, |
| 21 | + * {@code init.templateDir}, and {@code core.autocrlf}. We also scrub |
| 22 | + * {@code GIT_CONFIG_*} / {@code GIT_DIR} / {@code GIT_WORK_TREE} env vars so the |
| 23 | + * invoked processes inherit no ambient git state from the parent. |
14 | 24 | */ |
15 | 25 | class RepositoryIdentityTest { |
16 | 26 |
|
@@ -109,31 +119,92 @@ void record_nullFieldsAllowed() { |
109 | 119 | // Helpers |
110 | 120 | // ------------------------------------------------------------------ |
111 | 121 |
|
| 122 | + /** |
| 123 | + * Pre-flight: skip git-dependent tests when the {@code git} binary is not available. |
| 124 | + * Treat absence-of-git as an environment gap, not a product bug — still covered by the |
| 125 | + * non-git tests above, and by the {@code RepositoryIdentity.runGit} swallow-all-errors path. |
| 126 | + */ |
| 127 | + private static void requireGit() { |
| 128 | + try { |
| 129 | + Process p = new ProcessBuilder("git", "--version") |
| 130 | + .redirectErrorStream(true).start(); |
| 131 | + boolean ok = p.waitFor() == 0; |
| 132 | + assumeTrue(ok, "git binary not available on PATH"); |
| 133 | + } catch (Exception e) { |
| 134 | + assumeTrue(false, "git binary not available on PATH: " + e.getMessage()); |
| 135 | + } |
| 136 | + } |
| 137 | + |
112 | 138 | private static void initGitRepo(Path dir) throws Exception { |
113 | | - run(dir, "git", "init"); |
| 139 | + requireGit(); |
| 140 | + // -c overrides are applied to THIS invocation only and cannot be shadowed by a user's |
| 141 | + // global gitconfig (unlike `git config` writes into the new .git/config). |
| 142 | + run(dir, "git", |
| 143 | + "-c", "init.defaultBranch=main", |
| 144 | + "-c", "init.templateDir=", |
| 145 | + "init"); |
114 | 146 | run(dir, "git", "config", "user.email", "test@test.com"); |
115 | 147 | run(dir, "git", "config", "user.name", "Test"); |
| 148 | + // Kill every global knob that can make `git commit` fail on an otherwise-clean repo. |
| 149 | + run(dir, "git", "config", "commit.gpgsign", "false"); |
| 150 | + run(dir, "git", "config", "tag.gpgsign", "false"); |
| 151 | + run(dir, "git", "config", "core.hooksPath", "/dev/null"); |
| 152 | + run(dir, "git", "config", "core.autocrlf", "false"); |
116 | 153 | } |
117 | 154 |
|
118 | 155 | private static void makeInitialCommit(Path dir) throws Exception { |
119 | 156 | Path readme = dir.resolve("README.md"); |
120 | 157 | java.nio.file.Files.writeString(readme, "# Test"); |
121 | 158 | run(dir, "git", "add", "."); |
122 | | - run(dir, "git", "commit", "-m", "init"); |
| 159 | + // --no-gpg-sign is belt-and-braces over the repo-local commit.gpgsign=false set above; |
| 160 | + // --allow-empty-message keeps the test robust if a commit.template hook injects content. |
| 161 | + run(dir, "git", "-c", "commit.gpgsign=false", "commit", |
| 162 | + "--no-gpg-sign", "-m", "init"); |
123 | 163 | } |
124 | 164 |
|
125 | 165 | private static String runGit(Path dir, String... args) throws Exception { |
| 166 | + requireGit(); |
126 | 167 | var cmd = new java.util.ArrayList<String>(); |
127 | 168 | cmd.add("git"); |
128 | 169 | cmd.addAll(java.util.Arrays.asList(args)); |
129 | | - var proc = new ProcessBuilder(cmd).directory(dir.toFile()).start(); |
130 | | - String out = new String(proc.getInputStream().readAllBytes()).trim(); |
| 170 | + var pb = new ProcessBuilder(cmd).directory(dir.toFile()); |
| 171 | + scrubGitEnv(pb.environment()); |
| 172 | + var proc = pb.start(); |
| 173 | + String out; |
| 174 | + try (var is = proc.getInputStream()) { |
| 175 | + out = new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8).trim(); |
| 176 | + } |
131 | 177 | proc.waitFor(); |
132 | 178 | return out; |
133 | 179 | } |
134 | 180 |
|
| 181 | + /** |
| 182 | + * Execute a git sub-command and assert it exited 0. Fails the test loudly (not silently) |
| 183 | + * if setup cannot complete — preferred over ignoring the exit code, which is what let the |
| 184 | + * GPG-signing failure slip through before. |
| 185 | + */ |
135 | 186 | private static void run(Path dir, String... cmd) throws Exception { |
136 | | - new ProcessBuilder(cmd).directory(dir.toFile()) |
137 | | - .redirectErrorStream(true).start().waitFor(); |
| 187 | + var pb = new ProcessBuilder(cmd).directory(dir.toFile()).redirectErrorStream(true); |
| 188 | + scrubGitEnv(pb.environment()); |
| 189 | + Process proc = pb.start(); |
| 190 | + String stderr; |
| 191 | + try (var is = proc.getInputStream()) { |
| 192 | + stderr = new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); |
| 193 | + } |
| 194 | + int exit = proc.waitFor(); |
| 195 | + if (exit != 0) { |
| 196 | + throw new IllegalStateException( |
| 197 | + "Command failed (exit " + exit + "): " + String.join(" ", cmd) |
| 198 | + + "\n" + stderr); |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + /** |
| 203 | + * Remove every ambient git env var that could leak the parent shell's context into the |
| 204 | + * child process (most commonly {@code GIT_DIR}/{@code GIT_WORK_TREE} in a worktree-based |
| 205 | + * setup, or {@code GIT_CONFIG_*} injected by CI runners). |
| 206 | + */ |
| 207 | + private static void scrubGitEnv(Map<String, String> env) { |
| 208 | + env.keySet().removeIf(k -> k.startsWith("GIT_")); |
138 | 209 | } |
139 | 210 | } |
0 commit comments