fix(index): respect .gitignore for tracked gitlink embedded repos (#1065) (#1066)

The gitlink discovery added in #1031/#1033 indexed a tracked 160000 gitlink even when the parent .gitignore excludes its directory, pulling a gitignored reference/benchmark corpus of git add'ed clones into the index (one report: ~138k files, 4.8 GiB, wedged "Resolving refs" watchdog).

Gate both gitlink-discovery sites on the same rule the untracked-embedded path already uses: skip a gitignored gitlink unless codegraph.json includeIgnored opts it in; index non-ignored gitlinks as before. Validated real-world on macOS, Linux, and Windows.

Closes #1065

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-29 16:54:43 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 738d2dc4ad
commit 4159539fb8
3 changed files with 106 additions and 10 deletions
+50 -1
View File
@@ -9,7 +9,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { extractFromSource, scanDirectory, buildDefaultIgnore } from '../src/extraction';
import { extractFromSource, scanDirectory, buildDefaultIgnore, discoverEmbeddedRepoRoots, buildScopeIgnore } from '../src/extraction';
import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars, isSourceFile } from '../src/extraction/grammars';
import { stripCppTemplateArgs } from '../src/extraction/languages/c-cpp';
import { normalizePath } from '../src/utils';
@@ -5659,6 +5659,55 @@ describe('Nested gitlink repos (#1031, #1033)', () => {
expect(files).toContain('app.ts');
expect(files).not.toContain('libs/lib/lib.ts'); // not on disk → correctly absent
});
// #1065: a gitlink under a path the super-repo's OWN `.gitignore` covers is the
// tracked-gitlink twin of the untracked-ignored embedded repo (#514, #970). The
// gitlink-discovery pass must honor that `.gitignore` the same way — otherwise a
// gitignored reference/benchmark corpus full of `git add`ed clones gets pulled
// into the index (the 138k-file blow-up the reporter hit). Respect it by default;
// re-include only via `codegraph.json` `includeIgnored`.
it('does not index a gitlink under a gitignored directory by default (#1065)', async () => {
const { execFileSync } = await import('child_process');
const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' });
const root = path.join(tempDir, 'root');
await makeRepo(root, 'app');
// An embedded clone under a path the super-repo gitignores (a benchmark corpus).
await makeRepo(path.join(root, 'benchmark', 'repos', 'ref'), 'ref');
git(root, 'add', 'benchmark/repos/ref'); // tracked as a 160000 gitlink
fs.writeFileSync(path.join(root, '.gitignore'), 'benchmark/repos/\n');
git(root, 'add', '.gitignore');
git(root, 'commit', '-q', '-m', 'add gitignored gitlink + ignore rule');
const files = scanDirectory(root);
expect(files).toContain('app.ts');
expect(files).not.toContain('benchmark/repos/ref/ref.ts'); // gitignored → excluded
// The watcher path agrees: the ignored root is never discovered, and the dir is
// pruned (the reporter's exact clue — `ignores('benchmark/repos/')` was false).
expect(discoverEmbeddedRepoRoots(root)).toEqual([]);
expect(buildScopeIgnore(root).ignores('benchmark/repos/')).toBe(true);
expect(buildScopeIgnore(root).ignores('benchmark/repos/ref/ref.ts')).toBe(true);
});
it('re-includes a gitignored gitlink when codegraph.json includeIgnored opts in (#1065)', async () => {
const { execFileSync } = await import('child_process');
const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' });
const root = path.join(tempDir, 'root');
await makeRepo(root, 'app');
await makeRepo(path.join(root, 'benchmark', 'repos', 'ref'), 'ref');
git(root, 'add', 'benchmark/repos/ref');
fs.writeFileSync(path.join(root, '.gitignore'), 'benchmark/repos/\n');
fs.writeFileSync(path.join(root, 'codegraph.json'), JSON.stringify({ includeIgnored: ['benchmark/repos/'] }));
git(root, 'add', '.gitignore', 'codegraph.json');
git(root, 'commit', '-q', '-m', 'opt the gitignored gitlink back in');
const files = scanDirectory(root);
expect(files).toContain('app.ts');
expect(files).toContain('benchmark/repos/ref/ref.ts'); // opted in → indexed
expect(discoverEmbeddedRepoRoots(root)).toContain('benchmark/repos/ref/');
});
});
describe('Nested non-submodule git repos', () => {