返回 DeepSeek-Reasonix
gitignore_test.go
根目录 / internal / tool / builtin / gitignore_test.go
1 package builtin
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 fileencoding "reasonix/internal/fileutil/encoding"
10 )
11
12 func writeFileT(t *testing.T, path, body string) {
13 t.Helper()
14 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
15 t.Fatal(err)
16 }
17 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
18 t.Fatal(err)
19 }
20 }
21
22 func mkRepo(t *testing.T) string {
23 t.Helper()
24 dir := t.TempDir()
25 if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
26 t.Fatal(err)
27 }
28 return dir
29 }
30
31 func TestGrepNestedGitignore(t *testing.T) {
32 dir := mkRepo(t)
33 writeFileT(t, filepath.Join(dir, ".gitignore"), "*.log\n")
34 writeFileT(t, filepath.Join(dir, "pkg", ".gitignore"), "secret.txt\n")
35 writeFileT(t, filepath.Join(dir, "pkg", "keep.go"), "NEEDLE\n")
36 writeFileT(t, filepath.Join(dir, "pkg", "secret.txt"), "NEEDLE\n") // ignored by nested
37 writeFileT(t, filepath.Join(dir, "pkg", "app.log"), "NEEDLE\n") // ignored by root
38
39 out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
40 if !strings.Contains(out, "keep.go") {
41 t.Fatalf("kept file must be found: %q", out)
42 }
43 if strings.Contains(out, "secret.txt") {
44 t.Fatalf("a nested .gitignore must apply: %q", out)
45 }
46 if strings.Contains(out, "app.log") {
47 t.Fatalf("an ancestor .gitignore must apply in subdirs: %q", out)
48 }
49 }
50
51 func TestGrepNestedNegationReincludes(t *testing.T) {
52 dir := mkRepo(t)
53 writeFileT(t, filepath.Join(dir, ".gitignore"), "*.log\n")
54 writeFileT(t, filepath.Join(dir, "pkg", ".gitignore"), "!keep.log\n")
55 writeFileT(t, filepath.Join(dir, "pkg", "keep.log"), "NEEDLE\n") // re-included by nested negation
56 writeFileT(t, filepath.Join(dir, "pkg", "drop.log"), "NEEDLE\n") // still ignored by root
57
58 out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
59 if !strings.Contains(out, "keep.log") {
60 t.Fatalf("a nested !negation must re-include: %q", out)
61 }
62 if strings.Contains(out, "drop.log") {
63 t.Fatalf("a non-negated .log must stay ignored: %q", out)
64 }
65 }
66
67 func TestGrepSkipsHidden(t *testing.T) {
68 dir := mkRepo(t)
69 writeFileT(t, filepath.Join(dir, "visible.txt"), "NEEDLE\n")
70 writeFileT(t, filepath.Join(dir, ".env"), "NEEDLE\n")
71 writeFileT(t, filepath.Join(dir, ".github", "ci.yml"), "NEEDLE\n")
72
73 out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
74 if !strings.Contains(out, "visible.txt") {
75 t.Fatalf("a visible file must be found: %q", out)
76 }
77 if strings.Contains(out, ".env") || strings.Contains(out, "ci.yml") {
78 t.Fatalf("hidden files/dirs must be skipped by default: %q", out)
79 }
80 }
81
82 func TestGrepExplicitHiddenRootSearched(t *testing.T) {
83 dir := mkRepo(t)
84 writeFileT(t, filepath.Join(dir, ".github", "ci.yml"), "NEEDLE\n")
85 // Pointing grep straight at a hidden dir searches it in full.
86 out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": filepath.Join(dir, ".github")})
87 if !strings.Contains(out, "ci.yml") {
88 t.Fatalf("an explicitly targeted hidden dir should be searched: %q", out)
89 }
90 }
91
92 // repo scaffolds a fake git repo: a .git marker, a .gitignore, and files both
93 // kept and ignored — enough for the native grep walk to exercise .gitignore.
94 func gitignoreRepo(t *testing.T) string {
95 t.Helper()
96 dir := t.TempDir()
97 if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
98 t.Fatal(err)
99 }
100 writeFileT(t, filepath.Join(dir, ".gitignore"), "ignored.txt\nbuild/\n")
101 writeFileT(t, filepath.Join(dir, "keep.txt"), "NEEDLE in kept file\n")
102 writeFileT(t, filepath.Join(dir, "ignored.txt"), "NEEDLE in ignored file\n")
103 writeFileT(t, filepath.Join(dir, "build", "out.txt"), "NEEDLE in ignored dir\n")
104 writeFileT(t, filepath.Join(dir, "src", "deep.txt"), "NEEDLE deep in tree\n")
105 return dir
106 }
107
108 func TestGrepSkipsGitignored(t *testing.T) {
109 dir := gitignoreRepo(t)
110 out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
111
112 if !strings.Contains(out, "keep.txt") || !strings.Contains(out, "deep.txt") {
113 t.Fatalf("kept files must be searched: %q", out)
114 }
115 if strings.Contains(out, "ignored.txt") {
116 t.Fatalf("a .gitignore'd file must be skipped: %q", out)
117 }
118 if strings.Contains(out, "out.txt") {
119 t.Fatalf("files under a .gitignore'd dir must be skipped: %q", out)
120 }
121 }
122
123 func TestGrepDecodesGB18030Gitignore(t *testing.T) {
124 dir := mkRepo(t)
125 if err := os.WriteFile(filepath.Join(dir, ".gitignore"), fileencoding.Encode("秘密.txt\n", fileencoding.GB18030), 0o644); err != nil {
126 t.Fatal(err)
127 }
128 writeFileT(t, filepath.Join(dir, "秘密.txt"), "NEEDLE ignored\n")
129 writeFileT(t, filepath.Join(dir, "公开.txt"), "NEEDLE kept\n")
130
131 out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
132 if !strings.Contains(out, "公开.txt") {
133 t.Fatalf("kept Chinese file must be searched: %q", out)
134 }
135 if strings.Contains(out, "秘密.txt") {
136 t.Fatalf("GB18030 .gitignore pattern should skip Chinese file: %q", out)
137 }
138 }
139
140 func TestScanGitConfigExcludesDecodesGB18030Path(t *testing.T) {
141 dir := t.TempDir()
142 path := filepath.Join(dir, ".gitconfig")
143 want := filepath.Join(dir, "中文忽略规则.txt")
144 body := "[core]\n\texcludesFile = " + want + "\n"
145 if err := os.WriteFile(path, fileencoding.Encode(body, fileencoding.GB18030), 0o644); err != nil {
146 t.Fatal(err)
147 }
148
149 if got := scanGitConfigExcludes(path); got != want {
150 t.Fatalf("scanGitConfigExcludes = %q, want %q", got, want)
151 }
152 }
153
154 func TestGrepExplicitIgnoredRootStillSearched(t *testing.T) {
155 dir := gitignoreRepo(t)
156 // Pointing grep straight at a gitignored directory still searches it — the
157 // walk root is never pruned.
158 out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": filepath.Join(dir, "build")})
159 if !strings.Contains(out, "out.txt") {
160 t.Fatalf("an explicitly targeted ignored dir should still be searched: %q", out)
161 }
162 }
163
164 func TestGrepNoRepoIgnoresNothing(t *testing.T) {
165 // Same layout but without a .git marker: there is no repository, so
166 // .gitignore is not consulted and every file is searched.
167 dir := t.TempDir()
168 writeFileT(t, filepath.Join(dir, ".gitignore"), "ignored.txt\n")
169 writeFileT(t, filepath.Join(dir, "ignored.txt"), "NEEDLE here\n")
170
171 out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
172 if !strings.Contains(out, "ignored.txt") {
173 t.Fatalf("outside a git repo, .gitignore must not be applied: %q", out)
174 }
175 }
176
177 func TestFindRepoRoot(t *testing.T) {
178 dir := t.TempDir()
179 if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
180 t.Fatal(err)
181 }
182 sub := filepath.Join(dir, "a", "b")
183 if err := os.MkdirAll(sub, 0o755); err != nil {
184 t.Fatal(err)
185 }
186 got, err := filepath.EvalSymlinks(findRepoRoot(sub))
187 if err != nil {
188 t.Fatal(err)
189 }
190 want, _ := filepath.EvalSymlinks(dir)
191 if got != want {
192 t.Fatalf("findRepoRoot(%q) = %q, want %q", sub, got, want)
193 }
194 if rr := findRepoRoot(t.TempDir()); rr != "" {
195 t.Fatalf("a dir with no .git ancestor should return \"\", got %q", rr)
196 }
197 }
198
198 lines GO