Fix .gitignore support and Windows path separator bug in scanner

Normalize paths to forward slashes in matchesGlob() and scanDirectory()
so glob exclude patterns work on Windows. Add getGitIgnoredDirectories()
using git ls-files to skip .gitignore'd directories during indexing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-02-10 04:08:10 -06:00
co-authored by Claude Opus 4.6
parent 4825661e02
commit 62a6cf38fe
4 changed files with 158 additions and 3 deletions
+106 -1
View File
@@ -9,8 +9,10 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { extractFromSource } from '../src/extraction';
import { extractFromSource, scanDirectory, shouldIncludeFile } from '../src/extraction';
import { detectLanguage, isLanguageSupported, getSupportedLanguages } from '../src/extraction/grammars';
import { normalizePath } from '../src/utils';
import { DEFAULT_CONFIG } from '../src/types';
// Create a temporary directory for each test
function createTempDir(): string {
@@ -1880,3 +1882,106 @@ export function multiply(a: number, b: number): number {
cg.close();
});
});
describe('Path Normalization', () => {
it('should convert backslashes to forward slashes', () => {
expect(normalizePath('gui\\node_modules\\foo')).toBe('gui/node_modules/foo');
expect(normalizePath('src\\components\\Button.tsx')).toBe('src/components/Button.tsx');
});
it('should leave forward-slash paths unchanged', () => {
expect(normalizePath('src/components/Button.tsx')).toBe('src/components/Button.tsx');
});
it('should handle empty string', () => {
expect(normalizePath('')).toBe('');
});
});
describe('Directory Exclusion', () => {
let tempDir: string;
beforeEach(() => {
tempDir = createTempDir();
});
afterEach(() => {
cleanupTempDir(tempDir);
});
it('should exclude node_modules directories', () => {
// Create structure: src/index.ts + node_modules/pkg/index.js
const srcDir = path.join(tempDir, 'src');
const nmDir = path.join(tempDir, 'node_modules', 'pkg');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(nmDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
const files = scanDirectory(tempDir, config);
expect(files).toContain('src/index.ts');
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
});
it('should exclude nested node_modules directories', () => {
// Create structure: packages/app/node_modules/pkg/index.js
const srcDir = path.join(tempDir, 'packages', 'app', 'src');
const nmDir = path.join(tempDir, 'packages', 'app', 'node_modules', 'pkg');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(nmDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
const files = scanDirectory(tempDir, config);
expect(files).toContain('packages/app/src/index.ts');
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
});
it('should exclude .git directories', () => {
const srcDir = path.join(tempDir, 'src');
const gitDir = path.join(tempDir, '.git', 'objects');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(gitDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
fs.writeFileSync(path.join(gitDir, 'pack.ts'), 'export const y = 2;');
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
const files = scanDirectory(tempDir, config);
expect(files).toContain('src/index.ts');
expect(files.every((f) => !f.includes('.git'))).toBe(true);
});
it('should return forward-slash paths on all platforms', () => {
const srcDir = path.join(tempDir, 'src', 'components');
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'Button.tsx'), 'export function Button() {}');
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
const files = scanDirectory(tempDir, config);
expect(files.length).toBe(1);
expect(files[0]).toBe('src/components/Button.tsx');
expect(files[0]).not.toContain('\\');
});
it('should respect .codegraphignore marker', () => {
const srcDir = path.join(tempDir, 'src');
const vendorDir = path.join(tempDir, 'vendor');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(vendorDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
fs.writeFileSync(path.join(vendorDir, 'lib.ts'), 'export const y = 2;');
fs.writeFileSync(path.join(vendorDir, '.codegraphignore'), '');
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
const files = scanDirectory(tempDir, config);
expect(files).toContain('src/index.ts');
expect(files.every((f) => !f.includes('vendor'))).toBe(true);
});
});