(repoDir: string, prefix: string, out: GitChanges, overrides?: Record<string, Language>, includeIgnored: Ignore | null = null, exclude: Ignore | null = null)
| 844 | } |
| 845 | |
| 846 | function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, overrides?: Record<string, Language>, includeIgnored: Ignore | null = null, exclude: Ignore | null = null): void { |
| 847 | const output = execFileSync( |
| 848 | 'git', |
| 849 | ['status', '--porcelain', '--no-renames'], |
| 850 | { cwd: repoDir, encoding: 'utf-8', timeout: 10000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true } |
| 851 | ); |
| 852 | |
| 853 | // This repo's own ignore rules — built-in defaults (#407) plus its .gitignore. |
| 854 | // Change detection must exclude the SAME files the full index does, but git |
| 855 | // status hides neither: it ignores nothing for *tracked* paths, and the |
| 856 | // built-in defaults aren't gitignore at all. Without this filter a committed |
| 857 | // vendor/ dir, or a tracked file under a .gitignored dir, surfaces here as a |
| 858 | // change — so `codegraph status` (which reads getChangedFiles) reports a |
| 859 | // pending edit the full index never tracks and `sync` never clears. Matching |
| 860 | // repo-relative `rel` at each recursion level mirrors getGitVisibleFiles' |
| 861 | // ScopeIgnore: every embedded repo is judged by ITS OWN rules, never the |
| 862 | // parent's. (#766) |
| 863 | const ig = buildDefaultIgnore(repoDir); |
| 864 | |
| 865 | const untrackedDirs: string[] = []; |
| 866 | for (const line of output.split('\n')) { |
| 867 | if (line.length < 4) continue; // Minimum: "XY file" |
| 868 | |
| 869 | const statusCode = line.substring(0, 2); |
| 870 | const rel = normalizePath(line.substring(3)); |
| 871 | |
| 872 | // Untracked directory entries (trailing slash) may hide an embedded repo — |
| 873 | // collect for the recursion below instead of treating as a file. |
| 874 | if (statusCode === '??' && rel.endsWith('/')) { |
| 875 | untrackedDirs.push(rel); |
| 876 | continue; |
| 877 | } |
| 878 | |
| 879 | const filePath = normalizePath(prefix + rel); |
| 880 | if (!isSourceFile(filePath, overrides)) continue; |
| 881 | |
| 882 | if (statusCode.includes('D')) { |
| 883 | // Deletions stay unfiltered: getChangedFiles acts on one only when the |
| 884 | // path is already tracked in the DB, where removal is always correct — and |
| 885 | // that lets a newly-excluded dir's stale rows clean themselves up. (#766) |
| 886 | out.deleted.push(filePath); |
| 887 | continue; |
| 888 | } |
| 889 | |
| 890 | // Added (`??`) / modified files inside an excluded dir must not enter the |
| 891 | // index — match against the repo-relative path, same as the full scan. (#766) |
| 892 | if (ig.ignores(rel)) continue; |
| 893 | // User `codegraph.json` `exclude` (#999) is project-root-relative, so it's |
| 894 | // matched against the full path — sync must not re-add a tracked file the |
| 895 | // full index now keeps out. Deletions above stay unfiltered so a file that |
| 896 | // WAS indexed before an exclude was added still cleans itself out. |
| 897 | if (exclude && exclude.ignores(filePath)) continue; |
| 898 | |
| 899 | if (statusCode === '??') { |
| 900 | out.added.push(filePath); |
| 901 | } else { |
| 902 | // M, MM, AM, A (staged), etc. — treat as modified |
| 903 | out.modified.push(filePath); |
no test coverage detected