mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 05:55:17 -07:00
fix: gitignore handling (#8177)
This commit is contained in:
@@ -5,12 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { GitIgnoreFilter } from '../utils/gitIgnoreParser.js';
|
import type { GitIgnoreFilter } from '../utils/gitIgnoreParser.js';
|
||||||
|
import type { GeminiIgnoreFilter } from '../utils/geminiIgnoreParser.js';
|
||||||
import { GitIgnoreParser } from '../utils/gitIgnoreParser.js';
|
import { GitIgnoreParser } from '../utils/gitIgnoreParser.js';
|
||||||
|
import { GeminiIgnoreParser } from '../utils/geminiIgnoreParser.js';
|
||||||
import { isGitRepository } from '../utils/gitUtils.js';
|
import { isGitRepository } from '../utils/gitUtils.js';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
|
||||||
const GEMINI_IGNORE_FILE_NAME = '.geminiignore';
|
|
||||||
|
|
||||||
export interface FilterFilesOptions {
|
export interface FilterFilesOptions {
|
||||||
respectGitIgnore?: boolean;
|
respectGitIgnore?: boolean;
|
||||||
respectGeminiIgnore?: boolean;
|
respectGeminiIgnore?: boolean;
|
||||||
@@ -24,27 +24,15 @@ export interface FilterReport {
|
|||||||
|
|
||||||
export class FileDiscoveryService {
|
export class FileDiscoveryService {
|
||||||
private gitIgnoreFilter: GitIgnoreFilter | null = null;
|
private gitIgnoreFilter: GitIgnoreFilter | null = null;
|
||||||
private geminiIgnoreFilter: GitIgnoreFilter | null = null;
|
private geminiIgnoreFilter: GeminiIgnoreFilter | null = null;
|
||||||
private projectRoot: string;
|
private projectRoot: string;
|
||||||
|
|
||||||
constructor(projectRoot: string) {
|
constructor(projectRoot: string) {
|
||||||
this.projectRoot = path.resolve(projectRoot);
|
this.projectRoot = path.resolve(projectRoot);
|
||||||
if (isGitRepository(this.projectRoot)) {
|
if (isGitRepository(this.projectRoot)) {
|
||||||
const parser = new GitIgnoreParser(this.projectRoot);
|
this.gitIgnoreFilter = new GitIgnoreParser(this.projectRoot);
|
||||||
try {
|
|
||||||
parser.loadGitRepoPatterns();
|
|
||||||
} catch (_error) {
|
|
||||||
// ignore file not found
|
|
||||||
}
|
|
||||||
this.gitIgnoreFilter = parser;
|
|
||||||
}
|
}
|
||||||
const gParser = new GitIgnoreParser(this.projectRoot);
|
this.geminiIgnoreFilter = new GeminiIgnoreParser(this.projectRoot);
|
||||||
try {
|
|
||||||
gParser.loadPatterns(GEMINI_IGNORE_FILE_NAME);
|
|
||||||
} catch (_error) {
|
|
||||||
// ignore file not found
|
|
||||||
}
|
|
||||||
this.geminiIgnoreFilter = gParser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { GeminiIgnoreParser } from './geminiIgnoreParser.js';
|
||||||
|
import * as fs from 'node:fs/promises';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
|
||||||
|
describe('GeminiIgnoreParser', () => {
|
||||||
|
let projectRoot: string;
|
||||||
|
|
||||||
|
async function createTestFile(filePath: string, content = '') {
|
||||||
|
const fullPath = path.join(projectRoot, filePath);
|
||||||
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
||||||
|
await fs.writeFile(fullPath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
projectRoot = await fs.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), 'geminiignore-test-'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await fs.rm(projectRoot, { recursive: true, force: true });
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when .geminiignore exists', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createTestFile(
|
||||||
|
'.geminiignore',
|
||||||
|
'ignored.txt\n# A comment\n/ignored_dir/\n',
|
||||||
|
);
|
||||||
|
await createTestFile('ignored.txt', 'ignored');
|
||||||
|
await createTestFile('not_ignored.txt', 'not ignored');
|
||||||
|
await createTestFile(
|
||||||
|
path.join('ignored_dir', 'file.txt'),
|
||||||
|
'in ignored dir',
|
||||||
|
);
|
||||||
|
await createTestFile(
|
||||||
|
path.join('subdir', 'not_ignored.txt'),
|
||||||
|
'not ignored',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore files specified in .geminiignore', () => {
|
||||||
|
const parser = new GeminiIgnoreParser(projectRoot);
|
||||||
|
expect(parser.getPatterns()).toEqual(['ignored.txt', '/ignored_dir/']);
|
||||||
|
expect(parser.isIgnored('ignored.txt')).toBe(true);
|
||||||
|
expect(parser.isIgnored('not_ignored.txt')).toBe(false);
|
||||||
|
expect(parser.isIgnored(path.join('ignored_dir', 'file.txt'))).toBe(true);
|
||||||
|
expect(parser.isIgnored(path.join('subdir', 'not_ignored.txt'))).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when .geminiignore does not exist', () => {
|
||||||
|
it('should not load any patterns and not ignore any files', () => {
|
||||||
|
const parser = new GeminiIgnoreParser(projectRoot);
|
||||||
|
expect(parser.getPatterns()).toEqual([]);
|
||||||
|
expect(parser.isIgnored('any_file.txt')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import ignore from 'ignore';
|
||||||
|
|
||||||
|
export interface GeminiIgnoreFilter {
|
||||||
|
isIgnored(filePath: string): boolean;
|
||||||
|
getPatterns(): string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GeminiIgnoreParser implements GeminiIgnoreFilter {
|
||||||
|
private projectRoot: string;
|
||||||
|
private patterns: string[] = [];
|
||||||
|
private ig = ignore();
|
||||||
|
|
||||||
|
constructor(projectRoot: string) {
|
||||||
|
this.projectRoot = path.resolve(projectRoot);
|
||||||
|
this.loadPatterns();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadPatterns(): void {
|
||||||
|
const patternsFilePath = path.join(this.projectRoot, '.geminiignore');
|
||||||
|
let content: string;
|
||||||
|
try {
|
||||||
|
content = fs.readFileSync(patternsFilePath, 'utf-8');
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore file not found
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.patterns = (content ?? '')
|
||||||
|
.split('\n')
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter((p) => p !== '' && !p.startsWith('#'));
|
||||||
|
|
||||||
|
this.ig.add(this.patterns);
|
||||||
|
}
|
||||||
|
|
||||||
|
isIgnored(filePath: string): boolean {
|
||||||
|
if (this.patterns.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePath || typeof filePath !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filePath.startsWith('\\') ||
|
||||||
|
filePath === '/' ||
|
||||||
|
filePath.includes('\0')
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = path.resolve(this.projectRoot, filePath);
|
||||||
|
const relativePath = path.relative(this.projectRoot, resolved);
|
||||||
|
|
||||||
|
if (relativePath === '' || relativePath.startsWith('..')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even in windows, Ignore expects forward slashes.
|
||||||
|
const normalizedPath = relativePath.replace(/\\/g, '/');
|
||||||
|
|
||||||
|
if (normalizedPath.startsWith('/') || normalizedPath === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ig.ignores(normalizedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPatterns(): string[] {
|
||||||
|
return this.patterns;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,14 +33,16 @@ describe('GitIgnoreParser', () => {
|
|||||||
await fs.rm(projectRoot, { recursive: true, force: true });
|
await fs.rm(projectRoot, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('initialization', () => {
|
describe('Basic ignore behaviors', () => {
|
||||||
it('should initialize without errors when no .gitignore exists', async () => {
|
beforeEach(async () => {
|
||||||
await setupGitRepo();
|
await setupGitRepo();
|
||||||
expect(() => parser.loadGitRepoPatterns()).not.toThrow();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load .gitignore patterns when file exists', async () => {
|
it('should not ignore files when no .gitignore exists', async () => {
|
||||||
await setupGitRepo();
|
expect(parser.isIgnored('file.txt')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore files based on a root .gitignore', async () => {
|
||||||
const gitignoreContent = `
|
const gitignoreContent = `
|
||||||
# Comment
|
# Comment
|
||||||
node_modules/
|
node_modules/
|
||||||
@@ -50,52 +52,28 @@ node_modules/
|
|||||||
`;
|
`;
|
||||||
await createTestFile('.gitignore', gitignoreContent);
|
await createTestFile('.gitignore', gitignoreContent);
|
||||||
|
|
||||||
parser.loadGitRepoPatterns();
|
|
||||||
|
|
||||||
expect(parser.getPatterns()).toEqual([
|
|
||||||
'.git',
|
|
||||||
'node_modules/',
|
|
||||||
'*.log',
|
|
||||||
'/dist',
|
|
||||||
'.env',
|
|
||||||
]);
|
|
||||||
expect(parser.isIgnored(path.join('node_modules', 'some-lib'))).toBe(
|
expect(parser.isIgnored(path.join('node_modules', 'some-lib'))).toBe(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
expect(parser.isIgnored(path.join('src', 'app.log'))).toBe(true);
|
expect(parser.isIgnored(path.join('src', 'app.log'))).toBe(true);
|
||||||
expect(parser.isIgnored(path.join('dist', 'index.js'))).toBe(true);
|
expect(parser.isIgnored(path.join('dist', 'index.js'))).toBe(true);
|
||||||
expect(parser.isIgnored('.env')).toBe(true);
|
expect(parser.isIgnored('.env')).toBe(true);
|
||||||
|
expect(parser.isIgnored('src/index.js')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle git exclude file', async () => {
|
it('should handle git exclude file', async () => {
|
||||||
await setupGitRepo();
|
|
||||||
await createTestFile(
|
await createTestFile(
|
||||||
path.join('.git', 'info', 'exclude'),
|
path.join('.git', 'info', 'exclude'),
|
||||||
'temp/\n*.tmp',
|
'temp/\n*.tmp',
|
||||||
);
|
);
|
||||||
|
|
||||||
parser.loadGitRepoPatterns();
|
|
||||||
expect(parser.getPatterns()).toEqual(['.git', 'temp/', '*.tmp']);
|
|
||||||
expect(parser.isIgnored(path.join('temp', 'file.txt'))).toBe(true);
|
expect(parser.isIgnored(path.join('temp', 'file.txt'))).toBe(true);
|
||||||
expect(parser.isIgnored(path.join('src', 'file.tmp'))).toBe(true);
|
expect(parser.isIgnored(path.join('src', 'file.tmp'))).toBe(true);
|
||||||
});
|
expect(parser.isIgnored('src/file.js')).toBe(false);
|
||||||
|
|
||||||
it('should handle custom patterns file name', async () => {
|
|
||||||
// No .git directory for this test
|
|
||||||
await createTestFile('.geminiignore', 'temp/\n*.tmp');
|
|
||||||
|
|
||||||
parser.loadPatterns('.geminiignore');
|
|
||||||
expect(parser.getPatterns()).toEqual(['temp/', '*.tmp']);
|
|
||||||
expect(parser.isIgnored(path.join('temp', 'file.txt'))).toBe(true);
|
|
||||||
expect(parser.isIgnored(path.join('src', 'file.tmp'))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should initialize without errors when no .geminiignore exists', () => {
|
|
||||||
expect(() => parser.loadPatterns('.geminiignore')).not.toThrow();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isIgnored', () => {
|
describe('isIgnored path handling', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await setupGitRepo();
|
await setupGitRepo();
|
||||||
const gitignoreContent = `
|
const gitignoreContent = `
|
||||||
@@ -107,7 +85,6 @@ src/*.tmp
|
|||||||
!src/important.tmp
|
!src/important.tmp
|
||||||
`;
|
`;
|
||||||
await createTestFile('.gitignore', gitignoreContent);
|
await createTestFile('.gitignore', gitignoreContent);
|
||||||
parser.loadGitRepoPatterns();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should always ignore .git directory', () => {
|
it('should always ignore .git directory', () => {
|
||||||
@@ -205,8 +182,6 @@ src/*.tmp
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle nested .gitignore files correctly', async () => {
|
it('should handle nested .gitignore files correctly', async () => {
|
||||||
parser.loadGitRepoPatterns();
|
|
||||||
|
|
||||||
// From root .gitignore
|
// From root .gitignore
|
||||||
expect(parser.isIgnored('root-ignored.txt')).toBe(true);
|
expect(parser.isIgnored('root-ignored.txt')).toBe(true);
|
||||||
expect(parser.isIgnored('a/root-ignored.txt')).toBe(true);
|
expect(parser.isIgnored('a/root-ignored.txt')).toBe(true);
|
||||||
@@ -230,34 +205,27 @@ src/*.tmp
|
|||||||
expect(parser.isIgnored('a/d/f/g')).toBe(true);
|
expect(parser.isIgnored('a/d/f/g')).toBe(true);
|
||||||
expect(parser.isIgnored('a/f/g')).toBe(false);
|
expect(parser.isIgnored('a/f/g')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly transform patterns from nested gitignore files', () => {
|
|
||||||
parser.loadGitRepoPatterns();
|
|
||||||
const patterns = parser.getPatterns();
|
|
||||||
|
|
||||||
// From root .gitignore
|
|
||||||
expect(patterns).toContain('root-ignored.txt');
|
|
||||||
|
|
||||||
// From a/.gitignore
|
|
||||||
expect(patterns).toContain('/a/b'); // /b becomes /a/b
|
|
||||||
expect(patterns).toContain('/a/**/c'); // c becomes /a/**/c
|
|
||||||
|
|
||||||
// From a/d/.gitignore
|
|
||||||
expect(patterns).toContain('/a/d/**/e.txt'); // e.txt becomes /a/d/**/e.txt
|
|
||||||
expect(patterns).toContain('/a/d/f/g'); // f/g becomes /a/d/f/g
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('precedence rules', () => {
|
describe('precedence rules', () => {
|
||||||
it('should prioritize root .gitignore over .git/info/exclude', async () => {
|
beforeEach(async () => {
|
||||||
await setupGitRepo();
|
await setupGitRepo();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize nested .gitignore over root .gitignore', async () => {
|
||||||
|
await createTestFile('.gitignore', '*.log');
|
||||||
|
await createTestFile('a/b/.gitignore', '!special.log');
|
||||||
|
|
||||||
|
expect(parser.isIgnored('a/b/any.log')).toBe(true);
|
||||||
|
expect(parser.isIgnored('a/b/special.log')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize .gitignore over .git/info/exclude', async () => {
|
||||||
// Exclude all .log files
|
// Exclude all .log files
|
||||||
await createTestFile(path.join('.git', 'info', 'exclude'), '*.log');
|
await createTestFile(path.join('.git', 'info', 'exclude'), '*.log');
|
||||||
// But make an exception in the root .gitignore
|
// But make an exception in the root .gitignore
|
||||||
await createTestFile('.gitignore', '!important.log');
|
await createTestFile('.gitignore', '!important.log');
|
||||||
|
|
||||||
parser.loadGitRepoPatterns();
|
|
||||||
|
|
||||||
expect(parser.isIgnored('some.log')).toBe(true);
|
expect(parser.isIgnored('some.log')).toBe(true);
|
||||||
expect(parser.isIgnored('important.log')).toBe(false);
|
expect(parser.isIgnored('important.log')).toBe(false);
|
||||||
expect(parser.isIgnored(path.join('subdir', 'some.log'))).toBe(true);
|
expect(parser.isIgnored(path.join('subdir', 'some.log'))).toBe(true);
|
||||||
@@ -266,15 +234,4 @@ src/*.tmp
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getIgnoredPatterns', () => {
|
|
||||||
it('should return the raw patterns added', async () => {
|
|
||||||
await setupGitRepo();
|
|
||||||
const gitignoreContent = '*.log\n!important.log';
|
|
||||||
await createTestFile('.gitignore', gitignoreContent);
|
|
||||||
|
|
||||||
parser.loadGitRepoPatterns();
|
|
||||||
expect(parser.getPatterns()).toEqual(['.git', '*.log', '!important.log']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,93 +6,38 @@
|
|||||||
|
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import ignore, { type Ignore } from 'ignore';
|
import ignore from 'ignore';
|
||||||
import { isGitRepository } from './gitUtils.js';
|
|
||||||
|
|
||||||
export interface GitIgnoreFilter {
|
export interface GitIgnoreFilter {
|
||||||
isIgnored(filePath: string): boolean;
|
isIgnored(filePath: string): boolean;
|
||||||
getPatterns(): string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GitIgnoreParser implements GitIgnoreFilter {
|
export class GitIgnoreParser implements GitIgnoreFilter {
|
||||||
private projectRoot: string;
|
private projectRoot: string;
|
||||||
private ig: Ignore = ignore();
|
private cache: Map<string, string[]> = new Map();
|
||||||
private patterns: string[] = [];
|
private globalPatterns: string[] | undefined;
|
||||||
private readonly maxScannedDirs = 200;
|
|
||||||
|
|
||||||
constructor(projectRoot: string) {
|
constructor(projectRoot: string) {
|
||||||
this.projectRoot = path.resolve(projectRoot);
|
this.projectRoot = path.resolve(projectRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadGitRepoPatterns(): void {
|
private loadPatternsForFile(patternsFilePath: string): string[] {
|
||||||
if (!isGitRepository(this.projectRoot)) return;
|
|
||||||
|
|
||||||
// Always ignore .git directory regardless of .gitignore content
|
|
||||||
this.addPatterns(['.git']);
|
|
||||||
|
|
||||||
this.loadPatterns(path.join('.git', 'info', 'exclude'));
|
|
||||||
this.findAndLoadGitignoreFiles(this.projectRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
private findAndLoadGitignoreFiles(startDir: string): void {
|
|
||||||
const queue: string[] = [startDir];
|
|
||||||
let scannedDirs = 0;
|
|
||||||
let queueHead = 0;
|
|
||||||
|
|
||||||
while (queueHead < queue.length && scannedDirs < this.maxScannedDirs) {
|
|
||||||
const dir = queue[queueHead];
|
|
||||||
queueHead++;
|
|
||||||
scannedDirs++;
|
|
||||||
|
|
||||||
const relativeDir = path.relative(this.projectRoot, dir);
|
|
||||||
|
|
||||||
// For sub-directories, check if they are ignored before proceeding.
|
|
||||||
// The root directory (relativeDir === '') should not be checked.
|
|
||||||
if (relativeDir && this.isIgnored(relativeDir)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load patterns from .gitignore in the current directory
|
|
||||||
const gitignorePath = path.join(dir, '.gitignore');
|
|
||||||
if (fs.existsSync(gitignorePath)) {
|
|
||||||
this.loadPatterns(path.relative(this.projectRoot, gitignorePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurse into subdirectories
|
|
||||||
try {
|
|
||||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.name === '.git') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
queue.push(path.join(dir, entry.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_error) {
|
|
||||||
// ignore readdir errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPatterns(patternsFileName: string): void {
|
|
||||||
const patternsFilePath = path.join(this.projectRoot, patternsFileName);
|
|
||||||
let content: string;
|
let content: string;
|
||||||
try {
|
try {
|
||||||
content = fs.readFileSync(patternsFilePath, 'utf-8');
|
content = fs.readFileSync(patternsFilePath, 'utf-8');
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// ignore file not found
|
return [];
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// .git/info/exclude file patterns are relative to project root and not file directory
|
const isExcludeFile = patternsFilePath.endsWith(
|
||||||
const isExcludeFile =
|
path.join('.git', 'info', 'exclude'),
|
||||||
patternsFileName.replace(/\\/g, '/') === '.git/info/exclude';
|
);
|
||||||
|
|
||||||
const relativeBaseDir = isExcludeFile
|
const relativeBaseDir = isExcludeFile
|
||||||
? '.'
|
? '.'
|
||||||
: path.dirname(patternsFileName);
|
: path.dirname(path.relative(this.projectRoot, patternsFilePath));
|
||||||
|
|
||||||
const patterns = (content ?? '')
|
return content
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((p) => p.trim())
|
.map((p) => p.trim())
|
||||||
.filter((p) => p !== '' && !p.startsWith('#'))
|
.filter((p) => p !== '' && !p.startsWith('#'))
|
||||||
@@ -150,12 +95,6 @@ export class GitIgnoreParser implements GitIgnoreFilter {
|
|||||||
return newPattern;
|
return newPattern;
|
||||||
})
|
})
|
||||||
.filter((p) => p !== '');
|
.filter((p) => p !== '');
|
||||||
this.addPatterns(patterns);
|
|
||||||
}
|
|
||||||
|
|
||||||
private addPatterns(patterns: string[]) {
|
|
||||||
this.ig.add(patterns);
|
|
||||||
this.patterns.push(...patterns);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isIgnored(filePath: string): boolean {
|
isIgnored(filePath: string): boolean {
|
||||||
@@ -163,11 +102,8 @@ export class GitIgnoreParser implements GitIgnoreFilter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const absoluteFilePath = path.resolve(this.projectRoot, filePath);
|
||||||
filePath.startsWith('\\') ||
|
if (!absoluteFilePath.startsWith(this.projectRoot)) {
|
||||||
filePath === '/' ||
|
|
||||||
filePath.includes('\0')
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,13 +122,68 @@ export class GitIgnoreParser implements GitIgnoreFilter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.ig.ignores(normalizedPath);
|
const ig = ignore();
|
||||||
|
|
||||||
|
// Always ignore .git directory
|
||||||
|
ig.add('.git');
|
||||||
|
|
||||||
|
// Load global patterns from .git/info/exclude on first call
|
||||||
|
if (this.globalPatterns === undefined) {
|
||||||
|
const excludeFile = path.join(
|
||||||
|
this.projectRoot,
|
||||||
|
'.git',
|
||||||
|
'info',
|
||||||
|
'exclude',
|
||||||
|
);
|
||||||
|
this.globalPatterns = fs.existsSync(excludeFile)
|
||||||
|
? this.loadPatternsForFile(excludeFile)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
ig.add(this.globalPatterns);
|
||||||
|
|
||||||
|
const pathParts = relativePath.split(path.sep);
|
||||||
|
|
||||||
|
const dirsToVisit = [this.projectRoot];
|
||||||
|
let currentAbsDir = this.projectRoot;
|
||||||
|
// Collect all directories in the path
|
||||||
|
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||||
|
currentAbsDir = path.join(currentAbsDir, pathParts[i]);
|
||||||
|
dirsToVisit.push(currentAbsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dir of dirsToVisit) {
|
||||||
|
const relativeDir = path.relative(this.projectRoot, dir);
|
||||||
|
if (relativeDir) {
|
||||||
|
const normalizedRelativeDir = relativeDir.replace(/\\/g, '/');
|
||||||
|
if (ig.ignores(normalizedRelativeDir)) {
|
||||||
|
// This directory is ignored by an ancestor's .gitignore.
|
||||||
|
// According to git behavior, we don't need to process this
|
||||||
|
// directory's .gitignore, as nothing inside it can be
|
||||||
|
// un-ignored.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cache.has(dir)) {
|
||||||
|
const patterns = this.cache.get(dir);
|
||||||
|
if (patterns) {
|
||||||
|
ig.add(patterns);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const gitignorePath = path.join(dir, '.gitignore');
|
||||||
|
if (fs.existsSync(gitignorePath)) {
|
||||||
|
const patterns = this.loadPatternsForFile(gitignorePath);
|
||||||
|
this.cache.set(dir, patterns);
|
||||||
|
ig.add(patterns);
|
||||||
|
} else {
|
||||||
|
this.cache.set(dir, []); // Cache miss
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ig.ignores(normalizedPath);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPatterns(): string[] {
|
|
||||||
return this.patterns;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user