Skip to content

Commit 5eacb76

Browse files
committed
Add cline-ignore utility and tests
- Implemented `loadClineIgnoreFile` to read and return contents of `.clineignore`. - Created `shouldIgnorePath` function to determine if a file path should be ignored based on patterns in the ignore file. - Added tests for `shouldIgnorePath` covering exact matches, wildcards, directory patterns, comments, and negation patterns. - Introduced new files: `src/utils/cline-ignore.ts` and `src/utils/__tests__/cline-ignore.test.ts`. Enhance cline-ignore utility with additional functions and documentation - Added `parseIgnorePatterns` function to streamline parsing of .clineignore file. - Implemented `loadIgnorePatterns` to load patterns and provide an evaluation function. - Introduced `filterIgnoredPaths` for batch filtering of file paths based on ignore patterns. - Improved documentation with JSDoc comments for better clarity on function usage.
1 parent 34205d9 commit 5eacb76

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { shouldIgnorePath } from "../cline-ignore"
2+
3+
describe("shouldIgnorePath", () => {
4+
test("exact match pattern", () => {
5+
const ignoreContent = "test.txt"
6+
expect(shouldIgnorePath("test.txt", ignoreContent)).toBe(true)
7+
expect(shouldIgnorePath("other.txt", ignoreContent)).toBe(false)
8+
})
9+
10+
test("wildcard pattern", () => {
11+
const ignoreContent = "*.txt"
12+
expect(shouldIgnorePath("test.txt", ignoreContent)).toBe(true)
13+
expect(shouldIgnorePath("test.js", ignoreContent)).toBe(false)
14+
})
15+
16+
test("directory pattern", () => {
17+
const ignoreContent = "node_modules/"
18+
expect(shouldIgnorePath("node_modules/package.json", ignoreContent)).toBe(true)
19+
expect(shouldIgnorePath("src/node_modules.ts", ignoreContent)).toBe(false)
20+
})
21+
22+
test("comments and empty lines", () => {
23+
const ignoreContent = `
24+
# This is a comment
25+
test.txt
26+
27+
# This is also ignored
28+
*.js
29+
`
30+
expect(shouldIgnorePath("test.txt", ignoreContent)).toBe(true)
31+
expect(shouldIgnorePath("app.js", ignoreContent)).toBe(true)
32+
})
33+
34+
test("negation pattern", () => {
35+
const ignoreContent = `
36+
*.txt
37+
!important.txt
38+
docs/
39+
!docs/README.txt
40+
`
41+
// Matches *.txt but excluded by !important.txt
42+
expect(shouldIgnorePath("test.txt", ignoreContent)).toBe(true)
43+
expect(shouldIgnorePath("important.txt", ignoreContent)).toBe(false)
44+
45+
// Matches docs/ but excluded by !docs/README.txt
46+
expect(shouldIgnorePath("docs/test.txt", ignoreContent)).toBe(true)
47+
expect(shouldIgnorePath("docs/README.txt", ignoreContent)).toBe(false)
48+
})
49+
50+
test("complex negation pattern combinations", () => {
51+
const ignoreContent = `
52+
# Ignore all .log files
53+
*.log
54+
# But not debug.log
55+
!debug.log
56+
# However, ignore debug.log in tmp/
57+
tmp/debug.log
58+
`
59+
expect(shouldIgnorePath("error.log", ignoreContent)).toBe(true)
60+
expect(shouldIgnorePath("debug.log", ignoreContent)).toBe(false)
61+
expect(shouldIgnorePath("tmp/debug.log", ignoreContent)).toBe(true)
62+
})
63+
64+
test("negation pattern with reversed order", () => {
65+
const ignoreContent = `
66+
!.env.example
67+
.env*
68+
`
69+
// .env.example should be ignored because .env* comes after !.env.example
70+
expect(shouldIgnorePath(".env.example", ignoreContent)).toBe(true)
71+
expect(shouldIgnorePath(".env.local", ignoreContent)).toBe(true)
72+
})
73+
})

src/utils/cline-ignore.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { fileExistsAtPath } from "./fs"
2+
import * as path from "path"
3+
import * as fs from "fs/promises"
4+
5+
/**
6+
* Loads the contents of .clineignore file and returns cache and evaluation function.
7+
* @param cwd Current working directory
8+
* @returns Object containing patterns and evaluation function
9+
*/
10+
function parseIgnorePatterns(clineIgnoreFile: string): string[] {
11+
return clineIgnoreFile
12+
.split("\n")
13+
.map((line) => line.trim())
14+
.filter((line) => line && !line.startsWith("#"))
15+
}
16+
17+
export async function loadIgnorePatterns(cwd: string): Promise<{
18+
patterns: string[]
19+
shouldIgnore: (path: string) => boolean
20+
}> {
21+
const ignoreContent = await loadClineIgnoreFile(cwd)
22+
const patterns = parseIgnorePatterns(ignoreContent)
23+
return {
24+
patterns,
25+
shouldIgnore: (path: string) => shouldIgnorePath(path, ignoreContent),
26+
}
27+
}
28+
29+
/**
30+
* Filters multiple file paths in batch.
31+
* @param paths Array of paths to filter
32+
* @param ignoreContent Contents of .clineignore file
33+
* @returns Array of filtered paths
34+
*/
35+
export function filterIgnoredPaths(paths: string[], ignoreContent: string): string[] {
36+
return paths.filter((path) => !shouldIgnorePath(path, ignoreContent))
37+
}
38+
39+
export async function loadClineIgnoreFile(cwd: string): Promise<string> {
40+
const filePath = path.join(cwd, ".clineignore")
41+
try {
42+
const fileExists = await fileExistsAtPath(filePath)
43+
if (!fileExists) {
44+
return ""
45+
}
46+
return fs.readFile(filePath, "utf-8")
47+
} catch (error) {
48+
return ""
49+
}
50+
}
51+
52+
function convertGlobToRegExp(pattern: string): string {
53+
// Handle directory pattern
54+
if (pattern.endsWith("/")) {
55+
pattern = pattern + "**"
56+
}
57+
58+
return (
59+
pattern
60+
// Escape special characters
61+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
62+
// Convert wildcard * to regex pattern
63+
.replace(/\*/g, ".*")
64+
)
65+
}
66+
67+
export function shouldIgnorePath(filePath: string, clineIgnoreFile: string): boolean {
68+
const patterns = parseIgnorePatterns(clineIgnoreFile)
69+
let isIgnored = false
70+
71+
// Evaluate patterns in order
72+
for (const pattern of patterns) {
73+
const isNegation = pattern.startsWith("!")
74+
const actualPattern = isNegation ? pattern.slice(1) : pattern
75+
76+
// Convert pattern to regex
77+
const regexPattern = convertGlobToRegExp(actualPattern)
78+
const regex = new RegExp(`^${regexPattern}$`)
79+
80+
// Check if pattern matches
81+
if (regex.test(filePath)) {
82+
isIgnored = !isNegation
83+
}
84+
}
85+
86+
return isIgnored
87+
}

0 commit comments

Comments
 (0)