Skip to content

Commit

Permalink
fix: switch to node-ignore package to better handle excluded files
Browse files Browse the repository at this point in the history
  • Loading branch information
rexdotsh committed Jan 27, 2025
1 parent fab4086 commit 62cbfd9
Show file tree
Hide file tree
Showing 4 changed files with 3,182 additions and 357 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@
"test": "vscode-test"
},
"dependencies": {
"axios": "^1.6.2"
"axios": "^1.6.2",
"ignore": "^7.0.3"
},
"devDependencies": {
"@types/mocha": "^10.0.10",
Expand All @@ -244,12 +245,14 @@
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"@vscode/vsce": "^3.2.1",
"@webpack-cli/generators": "^3.0.7",
"esbuild": "^0.24.0",
"eslint": "^9.16.0",
"npm-run-all": "^4.1.5",
"ts-loader": "^9.5.1",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.2",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1"
}
}
89 changes: 15 additions & 74 deletions src/gitignoreManager.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,40 @@
import ignore from "ignore";
import * as vscode from "vscode";

// TODO: make this spec compliant, something like https://github.com/mherrmann/gitignore_parser
// BUGFIX: doesn't match patterns that start with /, like `/.next/*`

export class GitignoreManager {
private patterns: { pattern: string; isDirectory: boolean }[] = [];
private ig: ReturnType<typeof ignore> | null = null;
private outputChannel: vscode.OutputChannel;

constructor(outputChannel: vscode.OutputChannel) {
this.outputChannel = outputChannel;
}

// Static utility methods that can be used by other classes
public static convertGlobToRegex(pattern: string): string {
return pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars
.replace(/\*\*/g, ".*") // ** matches anything including /
.replace(/\*/g, "[^/]*") // * matches anything except /
.replace(/\?/g, "[^/]"); // ? matches any single non-/ char
}

public static normalizePath(path: string): string {
return path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
}

public static isMatch(pattern: string, filePath: string, isDirectory = false): boolean {
pattern = this.normalizePath(pattern);
filePath = this.normalizePath(filePath);

// For directory patterns without a slash, match anywhere in the path
if (isDirectory && !pattern.includes("/")) {
const dirName = pattern.replace(/\/$/, "");
return new RegExp(`(^|/)${this.convertGlobToRegex(dirName)}(/|$)`).test(filePath);
}

// For file patterns without a slash, match the basename
if (!isDirectory && !pattern.includes("/")) {
const basename = filePath.split("/").pop() || "";
return new RegExp(`^${this.convertGlobToRegex(pattern)}$`).test(basename);
}

// For patterns with slashes, match the full path
const regexPattern = this.convertGlobToRegex(pattern);
if (isDirectory || pattern.endsWith("/**")) {
return new RegExp(`^${regexPattern}(/.*)?$`).test(filePath);
public shouldIgnore(filePath: string): boolean {
if (!this.ig) {
return false;
}
return new RegExp(`^${regexPattern}$`).test(filePath);
return this.ig.ignores(filePath);
}

public async loadGitignore(workspaceFolder: vscode.Uri): Promise<void> {
try {
const gitignorePath = vscode.Uri.joinPath(workspaceFolder, ".gitignore");
const content = await vscode.workspace.fs.readFile(gitignorePath);
const gitignoreContent = Buffer.from(content).toString("utf8");

this.patterns = gitignoreContent
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
.map((pattern) => {
pattern = GitignoreManager.normalizePath(pattern);
const isDirectory = !pattern.includes(".") || pattern.endsWith("/");
return { pattern, isDirectory };
});

const formattedPatterns = this.patterns.map((p) => {
if (p.isDirectory && !p.pattern.includes("/")) {
return `**/${p.pattern}/**`;
}
return p.pattern;
});

this.outputChannel.appendLine(`Loaded ${this.patterns.length} patterns from .gitignore`);
this.outputChannel.appendLine(`Patterns: ${formattedPatterns.join(", ")}`);
this.loadFromContent(gitignoreContent);
} catch (error) {
this.outputChannel.appendLine("No .gitignore file found or failed to read it");
this.patterns = [];
this.ig = null;
}
}

public shouldIgnore(filePath: string): boolean {
let shouldIgnore = false;

// process patterns in order, with negations overriding previous matches
for (const { pattern, isDirectory } of this.patterns) {
if (pattern.startsWith("!")) {
// if a negated pattern matches, the file should NOT be ignored
if (GitignoreManager.isMatch(pattern.slice(1), filePath, isDirectory)) {
shouldIgnore = false;
}
} else if (GitignoreManager.isMatch(pattern, filePath, isDirectory)) {
shouldIgnore = true;
}
public loadFromContent(content: string): void {
try {
this.ig = ignore().add(content);
this.outputChannel.appendLine("Successfully loaded gitignore patterns");
} catch (error) {
this.outputChannel.appendLine("Failed to parse gitignore content");
this.ig = null;
}

return shouldIgnore;
}
}
43 changes: 30 additions & 13 deletions src/syncManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ignore from "ignore";
import * as vscode from "vscode";
import { ClaudeClient, Organization, Project } from "./claude/client";
import { ConfigManager } from "./config";
Expand Down Expand Up @@ -392,10 +393,7 @@ export class SyncManager {
const relativePath = vscode.workspace.asRelativePath(file);

// skip excluded files
if (
this.config.excludePatterns.some((pattern) => GitignoreManager.isMatch(pattern, relativePath, true)) ||
this.gitignoreManager.shouldIgnore(relativePath)
) {
if (this.shouldExclude(relativePath)) {
continue;
}

Expand All @@ -407,7 +405,23 @@ export class SyncManager {
}

if (this.isBinaryContent(content)) {
binaryFiles.add(relativePath);
// check if any parent folder is already excluded
const pathParts = relativePath.split("/");
let isParentExcluded = false;
let currentPath = "";

for (const part of pathParts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
if (this.shouldExclude(currentPath)) {
isParentExcluded = true;
break;
}
}

if (!isParentExcluded) {
// if no parent folder is excluded, add the file to binary files
binaryFiles.add(relativePath);
}
continue;
}

Expand All @@ -433,16 +447,19 @@ export class SyncManager {
return result;
}

public shouldExcludeFile(filePath: string): boolean {
const isExcludedByConfig = this.config.excludePatterns.some((pattern) => {
return GitignoreManager.isMatch(pattern, filePath, true);
});

if (isExcludedByConfig) {
private shouldExclude(relativePath: string): boolean {
// first check against .gitignore patterns
if (this.gitignoreManager.shouldIgnore(relativePath)) {
return true;
}

// check gitignore patterns
return this.gitignoreManager.shouldIgnore(filePath);
// then check against exclude patterns from config
if (this.config.excludePatterns.length === 0) {
return false;
}

// create a single ignore instance for all exclude patterns
const ig = ignore().add(this.config.excludePatterns);
return ig.ignores(relativePath);
}
}
Loading

0 comments on commit 62cbfd9

Please sign in to comment.