Skip to content

Commit 6e71b3f

Browse files
authored
feat: Add !include .file directive support for .clineignore (RooCodeInc#1777)
* feat: Add `!include .file` directive support for `.clineignore` * changeset * add warning * fix * revert * reduce diff * reduce diff
1 parent a198f71 commit 6e71b3f

File tree

3 files changed

+111
-2
lines changed

3 files changed

+111
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": patch
3+
---
4+
5+
Add !include .file directive support for .clineignore

src/core/ignore/ClineIgnoreController.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,52 @@ describe("ClineIgnoreController", () => {
242242
result.should.be.true()
243243
})
244244
})
245+
246+
describe("Include Directive", () => {
247+
it("should load patterns from an included file", async () => {
248+
// Create a .gitignore file with patterns "*.log" and "debug/"
249+
await fs.writeFile(path.join(tempDir, ".gitignore"), ["*.log", "debug/"].join("\n"))
250+
251+
// Create a .clineignore file that includes .gitignore and adds an extra pattern "secret.txt"
252+
await fs.writeFile(path.join(tempDir, ".clineignore"), ["!include .gitignore", "secret.txt"].join("\n"))
253+
254+
// Initialize the controller to load the updated .clineignore
255+
controller = new ClineIgnoreController(tempDir)
256+
await controller.initialize()
257+
258+
// "server.log" should be ignored due to the "*.log" pattern from .gitignore
259+
controller.validateAccess("server.log").should.be.false()
260+
// "debug/app.js" should be ignored due to the "debug/" pattern from .gitignore
261+
controller.validateAccess("debug/app.js").should.be.false()
262+
// "secret.txt" should be ignored as specified directly in .clineignore
263+
controller.validateAccess("secret.txt").should.be.false()
264+
// Other files should be allowed
265+
controller.validateAccess("app.js").should.be.true()
266+
})
267+
268+
it("should handle non-existent included file gracefully", async () => {
269+
// Create a .clineignore file that includes a non-existent file
270+
await fs.writeFile(path.join(tempDir, ".clineignore"), ["!include missing-file.txt"].join("\n"))
271+
272+
// Initialize the controller
273+
controller = new ClineIgnoreController(tempDir)
274+
await controller.initialize()
275+
276+
// Validate access to a regular file; it should be allowed because the missing include should not break everything
277+
controller.validateAccess("regular-file.txt").should.be.true()
278+
})
279+
280+
it("should handle non-existent included file gracefully alongside a valid pattern", async () => {
281+
// Test with an include directive for a non-existent file alongside a valid pattern ("*.tmp")
282+
await fs.writeFile(path.join(tempDir, ".clineignore"), ["!include non-existent.txt", "*.tmp"].join("\n"))
283+
284+
controller = new ClineIgnoreController(tempDir)
285+
await controller.initialize()
286+
287+
// "file.tmp" should be ignored because of the "*.tmp" pattern
288+
controller.validateAccess("file.tmp").should.be.false()
289+
// Files that do not match "*.tmp" should be allowed
290+
controller.validateAccess("file.log").should.be.true()
291+
})
292+
})
245293
})

src/core/ignore/ClineIgnoreController.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export class ClineIgnoreController {
5858
}
5959

6060
/**
61-
* Load custom patterns from .clineignore if it exists
61+
* Load custom patterns from .clineignore if it exists.
62+
* Supports "!include <filename>" to load additional ignore patterns from other files.
6263
*/
6364
private async loadClineIgnore(): Promise<void> {
6465
try {
@@ -68,7 +69,7 @@ export class ClineIgnoreController {
6869
if (await fileExistsAtPath(ignorePath)) {
6970
const content = await fs.readFile(ignorePath, "utf8")
7071
this.clineIgnoreContent = content
71-
this.ignoreInstance.add(content)
72+
await this.processIgnoreContent(content)
7273
this.ignoreInstance.add(".clineignore")
7374
} else {
7475
this.clineIgnoreContent = undefined
@@ -79,6 +80,61 @@ export class ClineIgnoreController {
7980
}
8081
}
8182

83+
/**
84+
* Process ignore content and apply all ignore patterns
85+
*/
86+
private async processIgnoreContent(content: string): Promise<void> {
87+
// Optimization: first check if there are any !include directives
88+
if (!content.includes("!include ")) {
89+
this.ignoreInstance.add(content)
90+
return
91+
}
92+
93+
// Process !include directives
94+
const combinedContent = await this.processClineIgnoreIncludes(content)
95+
this.ignoreInstance.add(combinedContent)
96+
}
97+
98+
/**
99+
* Process !include directives and combine all included file contents
100+
*/
101+
private async processClineIgnoreIncludes(content: string): Promise<string> {
102+
let combinedContent = ""
103+
const lines = content.split(/\r?\n/)
104+
105+
for (const line of lines) {
106+
const trimmedLine = line.trim()
107+
108+
if (!trimmedLine.startsWith("!include ")) {
109+
combinedContent += "\n" + line
110+
continue
111+
}
112+
113+
// Process !include directive
114+
const includedContent = await this.readIncludedFile(trimmedLine)
115+
if (includedContent) {
116+
combinedContent += "\n" + includedContent
117+
}
118+
}
119+
120+
return combinedContent
121+
}
122+
123+
/**
124+
* Read content from an included file specified by !include directive
125+
*/
126+
private async readIncludedFile(includeLine: string): Promise<string | null> {
127+
const includePath = includeLine.substring("!include ".length).trim()
128+
const resolvedIncludePath = path.join(this.cwd, includePath)
129+
130+
if (!(await fileExistsAtPath(resolvedIncludePath))) {
131+
console.debug(`[ClineIgnore] Included file not found: ${resolvedIncludePath}`)
132+
return null
133+
}
134+
135+
return await fs.readFile(resolvedIncludePath, "utf8")
136+
}
137+
82138
/**
83139
* Check if a file should be accessible to the LLM
84140
* @param filePath - Path to check (relative to cwd)

0 commit comments

Comments
 (0)