mirror of
https://github.com/cline/cline.git
synced 2026-09-21 05:10:09 +08:00
Merge pull request #1454 from HeavenOSK/cline-main-ignore
feat: Add .clineignore file support for protected files
This commit is contained in:
+7
-1
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user