Compare commits

...
Author SHA1 Message Date
Daniel Steigman b1971b4720 Merge pull request #1454 from HeavenOSK/cline-main-ignore
feat: Add .clineignore file support for protected files
2025-01-29 13:16:06 -08:00
HeavenOSK 4bc0ee4561 refactor: optimize directory ignore logic to prioritize user-defined patterns 2025-01-30 01:15:10 +09:00
HeavenOSK 505d443b1a feat: implement .clineignore file protection
- Add isClinieIgnoredFile function to check if a file is protected
- Prevent modifications to .clineignore file itself
- Add protection checks in file editing operations
2025-01-26 02:48:42 +09:00
HeavenOSK 3c60fa8c4a feat: add .clineignore support 2025-01-26 01:19:06 +09:00
HeavenOSK c094c0144a 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.
2025-01-26 01:19:01 +09:00
7 changed files with 219 additions and 21 deletions
+7 -1
View File
@@ -45,7 +45,7 @@ import { getApiMetrics } from "../shared/getApiMetrics"
import { HistoryItem } from "../shared/HistoryItem"
import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage"
import { calculateApiCost } from "../utils/cost"
import { fileExistsAtPath } from "../utils/fs"
import { checkClineIgnoreFile, fileExistsAtPath } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
@@ -1587,6 +1587,9 @@ export class Cline {
}
try {
// Check if the file is protected by .clineignore
checkClineIgnoreFile(relPath)
// Construct newContent from diff
let newContent: string
if (diff) {
@@ -1624,6 +1627,9 @@ export class Cline {
break
}
} else if (content) {
// Check if the file is protected by .clineignore
checkClineIgnoreFile(relPath)
newContent = content
// pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini)
+11
View File
@@ -4,8 +4,19 @@ import pdf from "pdf-parse/lib/pdf-parse"
import mammoth from "mammoth"
import fs from "fs/promises"
import { isBinaryFile } from "isbinaryfile"
import { loadIgnorePatterns } from "../../utils/cline-ignore"
export async function extractTextFromFile(filePath: string): Promise<string> {
// Convert file path to relative path
const cwd = path.dirname(filePath)
const relativePath = path.relative(cwd, filePath)
// Load and check .clineignore patterns
const { shouldIgnore } = await loadIgnorePatterns(cwd)
if (shouldIgnore(relativePath)) {
throw new Error(`File is ignored by .clineignore: ${filePath}`)
}
try {
await fs.access(filePath)
} catch (error) {
+24 -18
View File
@@ -2,6 +2,7 @@ import { globby, Options } from "globby"
import os from "os"
import * as path from "path"
import { arePathsEqual } from "../../utils/path"
import { loadIgnorePatterns } from "../../utils/cline-ignore"
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
const absolutePath = path.resolve(dirPath)
@@ -17,24 +18,29 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
return [[homeDir], false]
}
const dirsToIgnore = [
"node_modules",
"__pycache__",
"env",
"venv",
"target/dependency",
"build/dependencies",
"dist",
"out",
"bundle",
"vendor",
"tmp",
"temp",
"deps",
"pkg",
"Pods",
".*", // '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only their contents. This way we are at least aware of the existence of hidden directories.
].map((dir) => `**/${dir}/**`)
const { patterns } = await loadIgnorePatterns(absolutePath)
const dirsToIgnore =
patterns.length > 0
? patterns
: [
"node_modules",
"__pycache__",
"env",
"venv",
"target/dependency",
"build/dependencies",
"dist",
"out",
"bundle",
"vendor",
"tmp",
"temp",
"deps",
"pkg",
"Pods",
".*", // '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only their contents. This way we are at least aware of the existence of hidden directories.
].map((dir) => `**/${dir}/**`)
const options = {
cwd: dirPath,
+4 -2
View File
@@ -3,6 +3,7 @@ import * as childProcess from "child_process"
import * as path from "path"
import * as fs from "fs"
import * as readline from "readline"
import { loadClineIgnoreFile, loadIgnorePatterns } from "../../utils/cline-ignore"
/*
This file provides functionality to perform regex searches on files using ripgrep.
@@ -173,8 +174,9 @@ export async function regexSearchFiles(cwd: string, directoryPath: string, regex
if (currentResult) {
results.push(currentResult as SearchResult)
}
return formatResults(results, cwd)
const { shouldIgnore } = await loadIgnorePatterns(cwd)
const filteredResults = results.filter((result) => !shouldIgnore(result.file.replace(`${cwd}/`, "")))
return formatResults(filteredResults, cwd)
}
function formatResults(results: SearchResult[], cwd: string): string {
+75
View File
@@ -0,0 +1,75 @@
import { describe, it } from "mocha"
import "should"
import { shouldIgnorePath } from "./cline-ignore"
describe("shouldIgnorePath", () => {
it("exact match pattern", () => {
const ignoreContent = "test.txt"
shouldIgnorePath("test.txt", ignoreContent).should.be.true()
shouldIgnorePath("other.txt", ignoreContent).should.be.false()
})
it("wildcard pattern", () => {
const ignoreContent = "*.txt"
shouldIgnorePath("test.txt", ignoreContent).should.be.true()
shouldIgnorePath("test.js", ignoreContent).should.be.false()
})
it("directory pattern", () => {
const ignoreContent = "node_modules/"
shouldIgnorePath("node_modules/package.json", ignoreContent).should.be.true()
shouldIgnorePath("src/node_modules.ts", ignoreContent).should.be.false()
})
it("comments and empty lines", () => {
const ignoreContent = `
# This is a comment
test.txt
# This is also ignored
*.js
`
shouldIgnorePath("test.txt", ignoreContent).should.be.true()
shouldIgnorePath("app.js", ignoreContent).should.be.true()
})
it("negation pattern", () => {
const ignoreContent = `
*.txt
!important.txt
docs/
!docs/README.txt
`
// Matches *.txt but excluded by !important.txt
shouldIgnorePath("test.txt", ignoreContent).should.be.true()
shouldIgnorePath("important.txt", ignoreContent).should.be.false()
// Matches docs/ but excluded by !docs/README.txt
shouldIgnorePath("docs/test.txt", ignoreContent).should.be.true()
shouldIgnorePath("docs/README.txt", ignoreContent).should.be.false()
})
it("complex negation pattern combinations", () => {
const ignoreContent = `
# Ignore all .log files
*.log
# But not debug.log
!debug.log
# However, ignore debug.log in tmp/
tmp/debug.log
`
shouldIgnorePath("error.log", ignoreContent).should.be.true()
shouldIgnorePath("debug.log", ignoreContent).should.be.false()
shouldIgnorePath("tmp/debug.log", ignoreContent).should.be.true()
})
it("negation pattern with reversed order", () => {
const ignoreContent = `
!.env.example
.env*
`
// .env.example should be ignored because .env* comes after !.env.example
shouldIgnorePath(".env.example", ignoreContent).should.be.true()
shouldIgnorePath(".env.local", ignoreContent).should.be.true()
})
})
+87
View File
@@ -0,0 +1,87 @@
import { fileExistsAtPath } from "./fs"
import * as path from "path"
import * as fs from "fs/promises"
/**
* Loads the contents of .clineignore file and returns cache and evaluation function.
* @param cwd Current working directory
* @returns Object containing patterns and evaluation function
*/
function parseIgnorePatterns(clineIgnoreFile: string): string[] {
return clineIgnoreFile
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
}
export async function loadIgnorePatterns(cwd: string): Promise<{
patterns: string[]
shouldIgnore: (path: string) => boolean
}> {
const ignoreContent = await loadClineIgnoreFile(cwd)
const patterns = parseIgnorePatterns(ignoreContent)
return {
patterns,
shouldIgnore: (path: string) => shouldIgnorePath(path, ignoreContent),
}
}
/**
* Filters multiple file paths in batch.
* @param paths Array of paths to filter
* @param ignoreContent Contents of .clineignore file
* @returns Array of filtered paths
*/
export function filterIgnoredPaths(paths: string[], ignoreContent: string): string[] {
return paths.filter((path) => !shouldIgnorePath(path, ignoreContent))
}
export async function loadClineIgnoreFile(cwd: string): Promise<string> {
const filePath = path.join(cwd, ".clineignore")
try {
const fileExists = await fileExistsAtPath(filePath)
if (!fileExists) {
return ""
}
return fs.readFile(filePath, "utf-8")
} catch (error) {
return ""
}
}
function convertGlobToRegExp(pattern: string): string {
// Handle directory pattern
if (pattern.endsWith("/")) {
pattern = pattern + "**"
}
return (
pattern
// Escape special characters
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
// Convert wildcard * to regex pattern
.replace(/\*/g, ".*")
)
}
export function shouldIgnorePath(filePath: string, clineIgnoreFile: string): boolean {
const patterns = parseIgnorePatterns(clineIgnoreFile)
let isIgnored = false
// Evaluate patterns in order
for (const pattern of patterns) {
const isNegation = pattern.startsWith("!")
const actualPattern = isNegation ? pattern.slice(1) : pattern
// Convert pattern to regex
const regexPattern = convertGlobToRegExp(actualPattern)
const regex = new RegExp(`^${regexPattern}$`)
// Check if pattern matches
if (regex.test(filePath)) {
isIgnored = !isNegation
}
}
return isIgnored
}
+11
View File
@@ -45,3 +45,14 @@ export async function fileExistsAtPath(filePath: string): Promise<boolean> {
return false
}
}
/**
* Checks if a file is .clineignore and throws an error if it is
* @param filePath - The path of the file to check
* @throws Error if the file is .clineignore
*/
export function checkClineIgnoreFile(filePath: string): void {
if (path.basename(filePath) === ".clineignore") {
throw new Error("Cannot modify '.clineignore' file as it is protected from modifications.")
}
}