feat(config): codegraph.json include to force gitignored first-party source into the index (#1063)

Adds an `include` list to the root `codegraph.json` that forces gitignored first-party source (second-VCS / SVN / Perforce dual-tracked repos) into the index — discovered directly off disk on the full index, incremental sync, and file-watching, on both git and non-git projects. Gitignore-style patterns, root-relative; explicit `exclude` still wins and built-in skips (node_modules, dist, .git) are never re-included. Complements `exclude` and `includeIgnored`.

Closes #1163.

Thanks @luoyxy for the contribution.
This commit is contained in:
xyyxr
2026-07-07 10:11:58 -05:00
committed by GitHub
parent f5edf8cf49
commit 2a06d9a71f
6 changed files with 568 additions and 6 deletions
+65 -2
View File
@@ -53,6 +53,20 @@ export interface ProjectConfig {
* and your `.gitignore`.
*/
exclude?: string[];
/**
* Gitignore-style patterns for first-party source to force INTO the index even
* when `.gitignore` would drop it — the general whitelist `includeIgnored`
* never was (that one only revives *embedded git repos* inside ignored dirs).
* The case this exists for: a project under a second VCS (SVN, Perforce, …)
* deliberately `.gitignore`s its own real source so it never lands in Git, yet
* that source must still be indexed. Matched against project-root-relative
* paths, so `"Tools/"`, a recursive `"Tools/**"` glob, or `"Local/typescript"`
* all work.
* Built-in default-ignored dirs (`node_modules`, `dist`, …), `.git`, and
* CodeGraph's own data dir are never resurfaced; an explicit `exclude` still
* wins. Absent/empty (the default) forces nothing in.
*/
include?: string[];
}
/** Parsed, validated view of a project's `codegraph.json`. */
@@ -60,6 +74,7 @@ interface ParsedConfig {
extensions: Record<string, Language>;
includeIgnored: string[];
exclude: string[];
include: string[];
}
interface CacheEntry {
@@ -81,6 +96,7 @@ const EMPTY_CONFIG: ParsedConfig = Object.freeze({
extensions: EMPTY_EXTENSIONS,
includeIgnored: Object.freeze([]) as unknown as string[],
exclude: Object.freeze([]) as unknown as string[],
include: Object.freeze([]) as unknown as string[],
});
/**
@@ -132,10 +148,16 @@ function parseConfig(file: string): ParsedConfig {
const extensions = extractExtensions(parsed, file);
const includeIgnored = extractIncludeIgnored(parsed, file);
const exclude = extractExclude(parsed, file);
if (extensions === EMPTY_EXTENSIONS && includeIgnored.length === 0 && exclude.length === 0) {
const include = extractInclude(parsed, file);
if (
extensions === EMPTY_EXTENSIONS &&
includeIgnored.length === 0 &&
exclude.length === 0 &&
include.length === 0
) {
return EMPTY_CONFIG;
}
return { extensions, includeIgnored, exclude };
return { extensions, includeIgnored, exclude, include };
}
/**
@@ -214,6 +236,34 @@ function extractExclude(parsed: object, file: string): string[] {
return out;
}
/**
* Validate the `include` patterns: an array of non-empty gitignore-style strings
* naming first-party source to force INTO the index despite `.gitignore` — the
* whitelist for SVN/Perforce-only source a project gitignores out of Git (the
* general case `includeIgnored` never covered). A non-array value or a
* non-string/blank entry warns-and-skips; never throws. Patterns are kept
* verbatim (trimmed) so they match exactly as a `.gitignore` line would, against
* project-root-relative paths.
*/
function extractInclude(parsed: object, file: string): string[] {
const raw = (parsed as ProjectConfig).include;
if (raw === undefined) return [];
if (!Array.isArray(raw)) {
logWarn(`Ignoring "include" in ${PROJECT_CONFIG_FILENAME}: must be an array of gitignore-style patterns`, { file });
return [];
}
const out: string[] = [];
for (const entry of raw) {
if (typeof entry !== 'string' || !entry.trim()) {
logWarn(`Ignoring an "include" entry in ${PROJECT_CONFIG_FILENAME}: every pattern must be a non-empty string`, { file });
continue;
}
out.push(entry.trim());
}
return out;
}
/**
* Load the parsed `codegraph.json` for a project, mtime-cached. A missing or
* malformed file yields the zero-config default. One `stat` (and at most one
@@ -275,6 +325,19 @@ export function loadExcludePatterns(rootDir: string): string[] {
return loadParsedConfig(rootDir).exclude;
}
/**
* Load the validated `include` patterns for a project, mtime-cached.
*
* These name first-party source to force INTO the index even when `.gitignore`
* would drop it — the whitelist for SVN/Perforce-only source a project
* gitignores out of Git. An empty result — the zero-config default — forces
* nothing in. Built-in default-ignored dirs, `.git`, and CodeGraph's data dir
* are never resurfaced, and an explicit `exclude` still wins.
*/
export function loadIncludePatterns(rootDir: string): string[] {
return loadParsedConfig(rootDir).include;
}
/** Test/maintenance hook: forget cached config (e.g. after rewriting it in a test). */
export function clearProjectConfigCache(): void {
cache.clear();