Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/codebase-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export const codebaseIndexConfigSchema = z.object({
codebaseIndexBedrockProfile: z.string().optional(),
// OpenRouter specific fields
codebaseIndexOpenRouterSpecificProvider: z.string().optional(),
// Gitignore behavior
respectGitIgnore: z.boolean().optional(),
})

export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,7 @@ export interface WebviewMessage {
codebaseIndexSearchMaxResults?: number
codebaseIndexSearchMinScore?: number
codebaseIndexOpenRouterSpecificProvider?: string // OpenRouter provider routing
respectGitIgnore?: boolean // Whether to respect .gitignore when listing files

// Secret settings
codeIndexOpenAiKey?: string
Expand Down
3 changes: 2 additions & 1 deletion src/core/environment/getEnvironmentDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
if (maxFiles === 0) {
details += "(Workspace files context disabled. Use list_files to explore if needed.)"
} else {
const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles)
const respectGitIgnore = state?.codebaseIndexConfig?.respectGitIgnore ?? true
const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles, respectGitIgnore)
const { showRooIgnoredFiles = false } = state ?? {}

const result = formatResponse.formatFilesList(
Expand Down
6 changes: 4 additions & 2 deletions src/core/tools/ListFilesTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ export class ListFilesTool extends BaseTool<"list_files"> {
const absolutePath = path.resolve(task.cwd, relDirPath)
const isOutsideWorkspace = isPathOutsideWorkspace(absolutePath)

const [files, didHitLimit] = await listFiles(absolutePath, recursive || false, 200)
const { showRooIgnoredFiles = false } = (await task.providerRef.deref()?.getState()) ?? {}
const state = await task.providerRef.deref()?.getState()
const respectGitIgnore = state?.codebaseIndexConfig?.respectGitIgnore ?? true
const [files, didHitLimit] = await listFiles(absolutePath, recursive || false, 200, respectGitIgnore)
const { showRooIgnoredFiles = false } = state ?? {}

const result = formatResponse.formatFilesList(
absolutePath,
Expand Down
2 changes: 2 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2211,6 +2211,7 @@ export class ClineProvider
codebaseIndexBedrockRegion: codebaseIndexConfig?.codebaseIndexBedrockRegion,
codebaseIndexBedrockProfile: codebaseIndexConfig?.codebaseIndexBedrockProfile,
codebaseIndexOpenRouterSpecificProvider: codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider,
respectGitIgnore: codebaseIndexConfig?.respectGitIgnore,
},
// Only set mdmCompliant if there's an actual MDM policy
// undefined means no MDM policy, true means compliant, false means non-compliant
Expand Down Expand Up @@ -2450,6 +2451,7 @@ export class ClineProvider
codebaseIndexBedrockProfile: stateValues.codebaseIndexConfig?.codebaseIndexBedrockProfile,
codebaseIndexOpenRouterSpecificProvider:
stateValues.codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider,
respectGitIgnore: stateValues.codebaseIndexConfig?.respectGitIgnore,
},
profileThresholds: stateValues.profileThresholds ?? {},
includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,
Expand Down
1 change: 1 addition & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2543,6 +2543,7 @@ export const webviewMessageHandler = async (
codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults,
codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore,
codebaseIndexOpenRouterSpecificProvider: settings.codebaseIndexOpenRouterSpecificProvider,
respectGitIgnore: settings.respectGitIgnore,
}

// Save global state first
Expand Down
9 changes: 8 additions & 1 deletion src/integrations/workspace/WorkspaceTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ class WorkspaceTracker {
return
}
const tempCwd = this.cwd
const [files, _] = await listFiles(tempCwd, true, MAX_INITIAL_FILES)
let respectGitIgnore = true
try {
const state = await this.providerRef.deref()?.getState()
respectGitIgnore = state?.codebaseIndexConfig?.respectGitIgnore ?? true
} catch {
// Fall back to default (respect .gitignore) if state is not available
}
const [files, _] = await listFiles(tempCwd, true, MAX_INITIAL_FILES, respectGitIgnore)
if (this.prevWorkSpacePath !== tempCwd) {
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ describe("WorkspaceTracker", () => {
vitest.runAllTimers()

// Should initialize file paths for new workspace
expect(listFiles).toHaveBeenCalledWith("/test/new-workspace", true, 1000)
expect(listFiles).toHaveBeenCalledWith("/test/new-workspace", true, 1000, true)
vitest.runAllTimers()
})

Expand Down
9 changes: 9 additions & 0 deletions src/services/code-index/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class CodeIndexConfigManager {
private qdrantApiKey?: string
private searchMinScore?: number
private searchMaxResults?: number
private respectGitIgnore: boolean = true

constructor(private readonly contextProxy: ContextProxy) {
// Initialize with current configuration to avoid false restart triggers
Expand Down Expand Up @@ -86,6 +87,7 @@ export class CodeIndexConfigManager {
this.qdrantApiKey = qdrantApiKey ?? ""
this.searchMinScore = codebaseIndexSearchMinScore
this.searchMaxResults = codebaseIndexSearchMaxResults
this.respectGitIgnore = codebaseIndexConfig.respectGitIgnore ?? true

// Validate and set model dimension
const rawDimension = codebaseIndexConfig.codebaseIndexEmbedderModelDimension
Expand Down Expand Up @@ -194,6 +196,7 @@ export class CodeIndexConfigManager {
openRouterSpecificProvider: this.openRouterOptions?.specificProvider ?? "",
qdrantUrl: this.qdrantUrl ?? "",
qdrantApiKey: this.qdrantApiKey ?? "",
respectGitIgnore: this.respectGitIgnore,
}

// Refresh secrets from VSCode storage to ensure we have the latest values
Expand Down Expand Up @@ -410,6 +413,12 @@ export class CodeIndexConfigManager {
return true
}

// respectGitIgnore change
const prevRespectGitIgnore = prev?.respectGitIgnore ?? true
if (prevRespectGitIgnore !== this.respectGitIgnore) {
return true
}

return false
}

Expand Down
1 change: 1 addition & 0 deletions src/services/code-index/interfaces/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ export type PreviousConfigSnapshot = {
openRouterSpecificProvider?: string
qdrantUrl?: string
qdrantApiKey?: string
respectGitIgnore?: boolean
}
38 changes: 25 additions & 13 deletions src/services/code-index/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,20 +312,31 @@ export class CodeIndexManager {
return
}

// Create .gitignore instance
const ignorePath = path.join(workspacePath, ".gitignore")
// Read the respectGitIgnore setting from the codebaseIndexConfig
let respectGitIgnore = true
try {
const content = await fs.readFile(ignorePath, "utf8")
ignoreInstance.add(content)
ignoreInstance.add(".gitignore")
} catch (error) {
// Should never happen: reading file failed even though it exists
console.error("Unexpected error loading .gitignore:", error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "_recreateServices",
})
const codebaseIndexConfig = this._configManager!.getContextProxy()?.getGlobalState("codebaseIndexConfig")
respectGitIgnore = codebaseIndexConfig?.respectGitIgnore ?? true
} catch {
// Fall back to default (respect .gitignore) if config proxy is not available
}

// Create .gitignore instance (only when respecting .gitignore)
if (respectGitIgnore) {
const ignorePath = path.join(workspacePath, ".gitignore")
try {
const content = await fs.readFile(ignorePath, "utf8")
ignoreInstance.add(content)
ignoreInstance.add(".gitignore")
} catch (error) {
// Should never happen: reading file failed even though it exists
console.error("Unexpected error loading .gitignore:", error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "_recreateServices",
})
}
}

// Create RooIgnoreController instance
Expand All @@ -338,6 +349,7 @@ export class CodeIndexManager {
this._cacheManager!,
ignoreInstance,
rooIgnoreController,
respectGitIgnore,
)

// Validate embedder configuration before proceeding
Expand Down
10 changes: 8 additions & 2 deletions src/services/code-index/processors/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class DirectoryScanner implements IDirectoryScanner {
private readonly cacheManager: CacheManager,
private readonly ignoreInstance: Ignore,
batchSegmentThreshold?: number,
private readonly respectGitIgnore: boolean = true,
) {
// Get the configurable batch size from VSCode settings, fallback to default
// If not provided in constructor, try to get from VSCode settings
Expand Down Expand Up @@ -76,8 +77,13 @@ export class DirectoryScanner implements IDirectoryScanner {
// Capture workspace context at scan start
const scanWorkspace = getWorkspacePathForContext(directoryPath)

// Get all files recursively (handles .gitignore automatically)
const [allPaths, _] = await listFiles(directoryPath, true, MAX_LIST_FILES_LIMIT_CODE_INDEX)
// Get all files recursively (handles .gitignore based on respectGitIgnore setting)
const [allPaths, _] = await listFiles(
directoryPath,
true,
MAX_LIST_FILES_LIMIT_CODE_INDEX,
this.respectGitIgnore,
)

// Filter out directories (marked with trailing '/')
const filePaths = allPaths.filter((p) => !p.endsWith("/"))
Expand Down
14 changes: 12 additions & 2 deletions src/services/code-index/service-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export class CodeIndexServiceFactory {
vectorStore: IVectorStore,
parser: ICodeParser,
ignoreInstance: Ignore,
respectGitIgnore: boolean = true,
): DirectoryScanner {
// Get the configurable batch size from VSCode settings
let batchSize: number
Expand All @@ -192,7 +193,15 @@ export class CodeIndexServiceFactory {
// In test environment, vscode.workspace might not be available
batchSize = BATCH_SEGMENT_THRESHOLD
}
return new DirectoryScanner(embedder, vectorStore, parser, this.cacheManager, ignoreInstance, batchSize)
return new DirectoryScanner(
embedder,
vectorStore,
parser,
this.cacheManager,
ignoreInstance,
batchSize,
respectGitIgnore,
)
}

/**
Expand Down Expand Up @@ -237,6 +246,7 @@ export class CodeIndexServiceFactory {
cacheManager: CacheManager,
ignoreInstance: Ignore,
rooIgnoreController?: RooIgnoreController,
respectGitIgnore: boolean = true,
): {
embedder: IEmbedder
vectorStore: IVectorStore
Expand All @@ -251,7 +261,7 @@ export class CodeIndexServiceFactory {
const embedder = this.createEmbedder()
const vectorStore = this.createVectorStore()
const parser = codeParser
const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance)
const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance, respectGitIgnore)
const fileWatcher = this.createFileWatcher(
context,
embedder,
Expand Down
3 changes: 2 additions & 1 deletion src/services/glob/__mocks__/list-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ const mockResolve = (dirPath: string): string => {
* @param dirPath - Directory path to list files from
* @param recursive - Whether to list files recursively
* @param limit - Maximum number of files to return
* @param _respectGitIgnore - Whether to respect .gitignore (ignored in mock)
* @returns Promise resolving to [file paths, limit reached flag]
*/
export const listFiles = vi.fn((dirPath: string, _recursive: boolean, limit: number) => {
export const listFiles = vi.fn((dirPath: string, _recursive: boolean, limit: number, _respectGitIgnore?: boolean) => {
// Early return for limit of 0 - matches the actual implementation
if (limit === 0) {
return Promise.resolve([[], false])
Expand Down
46 changes: 36 additions & 10 deletions src/services/glob/list-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ interface ScanContext {
* @param dirPath - Directory path to list files from
* @param recursive - Whether to recursively list files in subdirectories
* @param limit - Maximum number of files to return
* @param respectGitIgnore - Whether to respect .gitignore when listing files (default: true)
* @returns Tuple of [file paths array, whether the limit was reached]
*/
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
export async function listFiles(
dirPath: string,
recursive: boolean,
limit: number,
respectGitIgnore: boolean = true,
): Promise<[string[], boolean]> {
// Early return for limit of 0 - no need to scan anything
if (limit === 0) {
return [[], false]
Expand All @@ -48,17 +54,17 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb

if (!recursive) {
// For non-recursive, use the existing approach
const files = await listFilesWithRipgrep(rgPath, dirPath, false, limit)
const ignoreInstance = await createIgnoreInstance(dirPath)
const files = await listFilesWithRipgrep(rgPath, dirPath, false, limit, respectGitIgnore)
const ignoreInstance = await createIgnoreInstance(dirPath, respectGitIgnore)
// Calculate remaining limit for directories
const remainingLimit = Math.max(0, limit - files.length)
const directories = await listFilteredDirectories(dirPath, false, ignoreInstance, remainingLimit)
return formatAndCombineResults(files, directories, limit)
}

// For recursive mode, use the original approach but ensure first-level directories are included
const files = await listFilesWithRipgrep(rgPath, dirPath, true, limit)
const ignoreInstance = await createIgnoreInstance(dirPath)
const files = await listFilesWithRipgrep(rgPath, dirPath, true, limit, respectGitIgnore)
const ignoreInstance = await createIgnoreInstance(dirPath, respectGitIgnore)
// Calculate remaining limit for directories
const remainingLimit = Math.max(0, limit - files.length)
const directories = await listFilteredDirectories(dirPath, true, ignoreInstance, remainingLimit)
Expand Down Expand Up @@ -202,8 +208,9 @@ async function listFilesWithRipgrep(
dirPath: string,
recursive: boolean,
limit: number,
respectGitIgnore: boolean = true,
): Promise<string[]> {
const rgArgs = buildRipgrepArgs(dirPath, recursive)
const rgArgs = buildRipgrepArgs(dirPath, recursive, respectGitIgnore)

const relativePaths = await execRipgrep(rgPath, rgArgs, limit)

Expand All @@ -216,10 +223,15 @@ async function listFilesWithRipgrep(
/**
* Build appropriate ripgrep arguments based on whether we're doing a recursive search
*/
function buildRipgrepArgs(dirPath: string, recursive: boolean): string[] {
function buildRipgrepArgs(dirPath: string, recursive: boolean, respectGitIgnore: boolean = true): string[] {
// Base arguments to list files
const args = ["--files", "--hidden", "--follow"]

// When not respecting .gitignore, tell ripgrep to skip VCS ignore files
if (!respectGitIgnore) {
args.push("--no-ignore-vcs")
}

if (recursive) {
return [...args, ...buildRecursiveArgs(dirPath), dirPath]
} else {
Expand All @@ -234,7 +246,7 @@ function buildRecursiveArgs(dirPath: string): string[] {
const args: string[] = []

// In recursive mode, respect .gitignore by default
// (ripgrep does this automatically)
// (ripgrep does this automatically; --no-ignore-vcs is added at buildRipgrepArgs level when needed)

// Check if we're explicitly targeting a hidden directory
// Normalize the path first to handle edge cases
Expand Down Expand Up @@ -303,7 +315,7 @@ function buildNonRecursiveArgs(): string[] {
args.push("--maxdepth", "1") // ripgrep uses maxdepth, not max-depth

// Respect .gitignore in non-recursive mode too
// (ripgrep respects .gitignore by default)
// (ripgrep respects .gitignore by default; --no-ignore-vcs is added at buildRipgrepArgs level when needed)

// Apply directory exclusions for non-recursive searches
for (const dir of DIRS_TO_IGNORE) {
Expand All @@ -326,9 +338,23 @@ function buildNonRecursiveArgs(): string[] {
/**
* Create an ignore instance that handles .gitignore files properly
* This replaces the custom gitignore parsing with the proper ignore library
*
* @param dirPath - Directory path to create ignore instance for
* @param respectGitIgnore - Whether to load .gitignore patterns (default: true).
* When false, returns an empty ignore instance so no gitignore filtering is applied.
*/
async function createIgnoreInstance(dirPath: string): Promise<ReturnType<typeof ignore>> {
async function createIgnoreInstance(
dirPath: string,
respectGitIgnore: boolean = true,
): Promise<ReturnType<typeof ignore>> {
const ignoreInstance = ignore()

// When not respecting .gitignore, return an empty ignore instance
// so no gitignore-based filtering is applied to directories
if (!respectGitIgnore) {
return ignoreInstance
}

const absolutePath = path.resolve(dirPath)

// Find all .gitignore files from the target directory up to the root
Expand Down
Loading
Loading