返回 DeepSeek-Reasonix
git_config_isolation_test.go
根目录 / internal / worktree / git_config_isolation_test.go
1 package worktree
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9 )
10
11 // hostileHooksPath builds the machine that made the hook tests destructive: a
12 // user-level git config pointing core.hooksPath at a directory the developer
13 // owns. The hook planted there records that it ran and exits 0, so it changes
14 // nothing except leave evidence - a hook that failed would be indistinguishable
15 // from an unrelated broken commit.
16 //
17 // Returns the hook's path and the marker it writes when git runs it.
18 func hostileHooksPath(t *testing.T) (hook, marker string) {
19 t.Helper()
20 dir := t.TempDir()
21 hook = filepath.Join(dir, "pre-commit")
22 marker = filepath.Join(t.TempDir(), "ambient-hook-ran")
23 script := "#!/bin/sh\nprintf ran > '" + marker + "'\nexit 0\n"
24 if err := os.WriteFile(hook, []byte(script), 0o755); err != nil {
25 t.Fatal(err)
26 }
27 config := filepath.Join(t.TempDir(), "gitconfig")
28 if err := os.WriteFile(config, []byte("[core]\n\thooksPath = "+dir+"\n"), 0o644); err != nil {
29 t.Fatal(err)
30 }
31 // Set before requireGit, exactly as the ambient environment would be, so
32 // the isolation has to win rather than merely arrive first.
33 t.Setenv("GIT_CONFIG_GLOBAL", config)
34 return hook, marker
35 }
36
37 // The write side. Any test that asks git where the pre-commit hook lives and
38 // then writes one must be answered with a path inside its own repository. This
39 // asserts the property rather than the two call sites that rely on it, so a
40 // third one cannot be added outside its cover.
41 func TestHookPathResolvesInsideTheTestRepository(t *testing.T) {
42 hook, _ := hostileHooksPath(t)
43 before, err := os.ReadFile(hook)
44 if err != nil {
45 t.Fatal(err)
46 }
47
48 requireGit(t)
49 repo := initRepo(t)
50
51 resolved := gitTest(t, repo, "rev-parse", "--git-path", "hooks/pre-commit")
52 if !filepath.IsAbs(resolved) {
53 resolved = filepath.Join(repo, resolved)
54 }
55 if !strings.HasPrefix(resolved, repo) {
56 t.Fatalf("hook path resolved outside the test repository: %s", resolved)
57 }
58
59 after, err := os.ReadFile(hook)
60 if err != nil {
61 t.Fatalf("the developer's hook is gone: %v", err)
62 }
63 if string(after) != string(before) {
64 t.Fatalf("the developer's hook was rewritten:\n%s", after)
65 }
66 }
67
68 // The read side, and the reason the hook tests could pass for the wrong reason.
69 // The package spawns git through runGit, which inherits os.Environ(), so the
70 // isolation has to reach the code under test and not only the helpers: a hook
71 // configured outside the repository must not run during the commits these
72 // tests make.
73 func TestCodeUnderTestDoesNotRunAnAmbientHook(t *testing.T) {
74 _, marker := hostileHooksPath(t)
75
76 requireGit(t)
77 repo := initRepo(t)
78 if err := os.WriteFile(filepath.Join(repo, "change.txt"), []byte("change\n"), 0o644); err != nil {
79 t.Fatal(err)
80 }
81 if _, stderr, err := runGit(context.Background(), opened(t, repo), "add", "change.txt"); err != nil {
82 t.Fatalf("git add: %v%s", err, stderrSuffix(stderr))
83 }
84 if _, stderr, err := runGit(context.Background(), opened(t, repo),
85 "-c", "user.name=Reasonix Test", "-c", "user.email=reasonix@example.invalid",
86 "commit", "-m", "change"); err != nil {
87 t.Fatalf("git commit: %v%s", err, stderrSuffix(stderr))
88 }
89
90 if _, err := os.Stat(marker); !os.IsNotExist(err) {
91 t.Fatalf("a hook outside the repository ran during the test's own commit, stat err = %v", err)
92 }
93 }
94
94 lines GO