Skip to content

Commit c7d966b

Browse files
committed
fix: prevent .gitignore from filtering file mentions (fixes #5944)
- Added --no-ignore-vcs flag to ripgrep command in executeRipgrepForFiles - This ensures files in .gitignore can still be mentioned with @ syntax - Only .rooignore should control file access in Roo Code - Added comprehensive unit tests to verify the fix
1 parent e28fad1 commit c7d966b

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import * as childProcess from "child_process"
3+
import * as path from "path"
4+
import * as vscode from "vscode"
5+
import * as readline from "readline"
6+
import { executeRipgrepForFiles } from "../file-search"
7+
import { getBinPath } from "../../ripgrep"
8+
9+
vi.mock("vscode", () => ({
10+
env: {
11+
appRoot: "/mock/app/root",
12+
},
13+
}))
14+
15+
vi.mock("../../ripgrep", () => ({
16+
getBinPath: vi.fn(),
17+
}))
18+
19+
vi.mock("child_process", () => ({
20+
spawn: vi.fn(),
21+
}))
22+
23+
vi.mock("readline", () => ({
24+
createInterface: vi.fn(),
25+
}))
26+
27+
describe("file-search", () => {
28+
const mockRgPath = "/mock/path/to/rg"
29+
const mockWorkspacePath = "/mock/workspace"
30+
31+
beforeEach(() => {
32+
vi.mocked(getBinPath).mockResolvedValue(mockRgPath)
33+
})
34+
35+
afterEach(() => {
36+
vi.clearAllMocks()
37+
})
38+
39+
describe("executeRipgrepForFiles", () => {
40+
it("should include --no-ignore-vcs flag to bypass .gitignore", async () => {
41+
// Create mock readline interface
42+
const mockRl = {
43+
on: vi.fn(),
44+
close: vi.fn(),
45+
}
46+
47+
// Set up readline mock to emit lines
48+
mockRl.on.mockImplementation((event, callback) => {
49+
if (event === "line") {
50+
// Simulate file output
51+
callback("/mock/workspace/file1.txt")
52+
callback("/mock/workspace/ignored-by-git.txt")
53+
} else if (event === "close") {
54+
// Simulate readline close
55+
setTimeout(() => callback(), 0)
56+
}
57+
return mockRl
58+
})
59+
60+
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any)
61+
62+
// Create a mock ripgrep process
63+
const mockStderr = {
64+
on: vi.fn(),
65+
}
66+
67+
const mockProcess = {
68+
stdout: {},
69+
stderr: mockStderr,
70+
on: vi.fn((event, callback) => {
71+
if (event === "close") {
72+
// Delay to ensure readline processes all lines
73+
setTimeout(() => callback(0), 10)
74+
}
75+
return mockProcess
76+
}),
77+
kill: vi.fn(),
78+
}
79+
80+
vi.mocked(childProcess.spawn).mockReturnValue(mockProcess as any)
81+
82+
// Call the function
83+
const results = await executeRipgrepForFiles(mockWorkspacePath)
84+
85+
// Verify ripgrep was called with correct arguments
86+
expect(childProcess.spawn).toHaveBeenCalledWith(mockRgPath, [
87+
"--files",
88+
"--follow",
89+
"--hidden",
90+
"--no-ignore-vcs", // This is the key flag we added
91+
"-g",
92+
"!**/node_modules/**",
93+
"-g",
94+
"!**/.git/**",
95+
"-g",
96+
"!**/out/**",
97+
"-g",
98+
"!**/dist/**",
99+
mockWorkspacePath,
100+
])
101+
102+
// Verify results include files that might be in .gitignore
103+
expect(results).toContainEqual({
104+
path: "file1.txt",
105+
type: "file",
106+
label: "file1.txt",
107+
})
108+
expect(results).toContainEqual({
109+
path: "ignored-by-git.txt",
110+
type: "file",
111+
label: "ignored-by-git.txt",
112+
})
113+
})
114+
115+
it("should handle ripgrep errors gracefully", async () => {
116+
// Create mock readline interface
117+
const mockRl = {
118+
on: vi.fn(),
119+
close: vi.fn(),
120+
}
121+
122+
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any)
123+
124+
const mockProcess = {
125+
stdout: {},
126+
stderr: {
127+
on: vi.fn((event, callback) => {
128+
if (event === "data") {
129+
callback(Buffer.from("Error: something went wrong"))
130+
}
131+
}),
132+
},
133+
on: vi.fn((event, callback) => {
134+
if (event === "error") {
135+
callback(new Error("ripgrep failed"))
136+
}
137+
return mockProcess
138+
}),
139+
kill: vi.fn(),
140+
}
141+
142+
vi.mocked(childProcess.spawn).mockReturnValue(mockProcess as any)
143+
144+
// Should reject with error
145+
await expect(executeRipgrepForFiles(mockWorkspacePath)).rejects.toThrow(
146+
"ripgrep process error: ripgrep failed",
147+
)
148+
})
149+
150+
it("should respect the file limit", async () => {
151+
// Create mock readline interface
152+
const mockRl = {
153+
on: vi.fn(),
154+
close: vi.fn(),
155+
}
156+
157+
let lineCount = 0
158+
mockRl.on.mockImplementation((event, callback) => {
159+
if (event === "line") {
160+
// Simulate many files
161+
for (let i = 1; i <= 10; i++) {
162+
if (lineCount < 5) {
163+
// Respect the limit
164+
callback(`/mock/workspace/file${i}.txt`)
165+
lineCount++
166+
}
167+
}
168+
} else if (event === "close") {
169+
setTimeout(() => callback(), 0)
170+
}
171+
return mockRl
172+
})
173+
174+
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any)
175+
176+
const mockStderr = {
177+
on: vi.fn(),
178+
}
179+
180+
const mockProcess = {
181+
stdout: {},
182+
stderr: mockStderr,
183+
on: vi.fn((event, callback) => {
184+
if (event === "close") {
185+
setTimeout(() => callback(0), 10)
186+
}
187+
return mockProcess
188+
}),
189+
kill: vi.fn(),
190+
}
191+
192+
vi.mocked(childProcess.spawn).mockReturnValue(mockProcess as any)
193+
194+
// Call with a limit of 5
195+
const results = await executeRipgrepForFiles(mockWorkspacePath, 5)
196+
197+
// Should only return 5 files
198+
expect(results.filter((r) => r.type === "file")).toHaveLength(5)
199+
})
200+
})
201+
})

src/services/search/file-search.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export async function executeRipgrepForFiles(
9393
"--files",
9494
"--follow",
9595
"--hidden",
96+
"--no-ignore-vcs", // Don't respect .gitignore - only .rooignore should control file access
9697
"-g",
9798
"!**/node_modules/**",
9899
"-g",

0 commit comments

Comments
 (0)