#514 (v1.0.0) began walking into gitignored directories to discover and index the git repos nested inside them. That broke users who rely on .gitignore to exclude a directory: a gitignored folder of cloned reference repos blew graphs up (one report went 10k to 500k edges, #976) and stalled indexing on multi-gigabyte trees of clones (#970). Respect .gitignore by default again. Discovering embedded repos inside a gitignored directory is now opt-in via codegraph.json: { "includeIgnored": ["packages/", "services/"] } The single choke point findIgnoredEmbeddedRepos now returns nothing unless a gitignored dir matches the project's includeIgnored patterns, and the matcher is threaded from the scan root through the full-index, incremental-sync, and watcher-scope paths. Downstream ScopeIgnore and the watcher are unchanged: they key off the discovered embedded roots, so gating discovery fixes the indexer, sync, and watcher together. Untracked embedded repos (#193) stay indexed by default. This restores the super-repo-of-clones behavior (#622, #699) for the people who want it, while making the default match what every other tool (and CodeGraph's own git ls-files foundation) does: .gitignore excludes. project-config.ts now parses codegraph.json once (loadParsedConfig) and exposes loadIncludeIgnoredPatterns alongside the existing extension map. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.6 KiB
TypeScript
90 lines
3.6 KiB
TypeScript
/**
|
|
* `codegraph.json` `includeIgnored` loader (#970, #976 / #622, #699).
|
|
*
|
|
* Parsing, validation, and mtime-caching of the opt-in patterns that re-include
|
|
* gitignored directories for embedded-repo discovery. The behavioral end of this
|
|
* feature (scanDirectory / discoverEmbeddedRepoRoots / sync honoring the patterns)
|
|
* lives in `multi-repo-workspace.test.ts`; these are the loader unit tests,
|
|
* mirroring the `extensions` loader coverage in `extension-mapping.test.ts`.
|
|
*
|
|
* Invariant under test: every failure mode degrades to the zero-config default
|
|
* (empty patterns → `.gitignore` fully respected), never a throw.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import { loadIncludeIgnoredPatterns, loadExtensionOverrides, clearProjectConfigCache } from '../src/project-config';
|
|
|
|
describe('includeIgnored loader (codegraph.json)', () => {
|
|
let dir: string;
|
|
beforeEach(() => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-includeignored-'));
|
|
clearProjectConfigCache();
|
|
});
|
|
afterEach(() => {
|
|
clearProjectConfigCache();
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
const writeConfig = (obj: unknown) =>
|
|
fs.writeFileSync(
|
|
path.join(dir, 'codegraph.json'),
|
|
typeof obj === 'string' ? obj : JSON.stringify(obj)
|
|
);
|
|
|
|
it('returns an empty list when there is no codegraph.json (the default)', () => {
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual([]);
|
|
});
|
|
|
|
it('loads a well-formed pattern array', () => {
|
|
writeConfig({ includeIgnored: ['packages/', 'services/'] });
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual(['packages/', 'services/']);
|
|
});
|
|
|
|
it('trims whitespace and drops blank / non-string entries', () => {
|
|
writeConfig({ includeIgnored: [' packages/ ', '', ' ', 42, null, 'services/'] });
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual(['packages/', 'services/']);
|
|
});
|
|
|
|
it('ignores a non-array includeIgnored value without throwing', () => {
|
|
writeConfig({ includeIgnored: 'packages/' });
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual([]);
|
|
});
|
|
|
|
it('ignores malformed JSON without throwing', () => {
|
|
writeConfig('{ not: valid json ');
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual([]);
|
|
});
|
|
|
|
it('returns [] when the field is absent but other config is present', () => {
|
|
writeConfig({ extensions: { '.foo': 'typescript' } });
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual([]);
|
|
});
|
|
|
|
it('coexists with extensions in one file (shared single parse)', () => {
|
|
writeConfig({ extensions: { '.foo': 'typescript' }, includeIgnored: ['vendor/'] });
|
|
expect(loadExtensionOverrides(dir)).toEqual({ '.foo': 'typescript' });
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual(['vendor/']);
|
|
});
|
|
|
|
it('picks up a changed config (mtime-invalidated cache)', () => {
|
|
writeConfig({ includeIgnored: ['packages/'] });
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual(['packages/']);
|
|
|
|
writeConfig({ includeIgnored: ['services/'] });
|
|
// Force a distinct mtime in case the filesystem clock is coarse.
|
|
const future = new Date(Date.now() + 2000);
|
|
fs.utimesSync(path.join(dir, 'codegraph.json'), future, future);
|
|
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual(['services/']);
|
|
});
|
|
|
|
it('drops the patterns again when the config file is removed', () => {
|
|
writeConfig({ includeIgnored: ['packages/'] });
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual(['packages/']);
|
|
|
|
fs.rmSync(path.join(dir, 'codegraph.json'));
|
|
expect(loadIncludeIgnoredPatterns(dir)).toEqual([]);
|
|
});
|
|
});
|