(repoDir: string, prefix: string, out: GitChanges, overrides?: Record<string, Language>, includeIgnored: Ignore | null = null, exclude: Ignore | null = null)
| 1103 | } |
| 1104 | |
| 1105 | function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, overrides?: Record<string, Language>, includeIgnored: Ignore | null = null, exclude: Ignore | null = null): void { |
| 1106 | const output = execFileSync( |
| 1107 | 'git', |
| 1108 | ['status', '--porcelain', '--no-renames'], |
| 1109 | { cwd: repoDir, encoding: 'utf-8', timeout: 10000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true } |
| 1110 | ); |
| 1111 | |
| 1112 | // This repo's own ignore rules — built-in defaults (#407) plus its .gitignore. |
| 1113 | // Change detection must exclude the SAME files the full index does, but git |
| 1114 | // status hides neither: it ignores nothing for *tracked* paths, and the |
| 1115 | // built-in defaults aren't gitignore at all. Without this filter a committed |
| 1116 | // vendor/ dir, or a tracked file under a .gitignored dir, surfaces here as a |
| 1117 | // change — so `codegraph status` (which reads getChangedFiles) reports a |
| 1118 | // pending edit the full index never tracks and `sync` never clears. Matching |
| 1119 | // repo-relative `rel` at each recursion level mirrors getGitVisibleFiles' |
| 1120 | // ScopeIgnore: every embedded repo is judged by ITS OWN rules, never the |
| 1121 | // parent's. (#766) |
| 1122 | const ig = buildDefaultIgnore(repoDir); |
| 1123 | |
| 1124 | const untrackedDirs: string[] = []; |
| 1125 | for (const line of output.split('\n')) { |
| 1126 | if (line.length < 4) continue; // Minimum: "XY file" |
| 1127 | |
| 1128 | const statusCode = line.substring(0, 2); |
| 1129 | const rel = normalizePath(line.substring(3)); |
| 1130 | |
| 1131 | // Untracked directory entries (trailing slash) may hide an embedded repo — |
| 1132 | // collect for the recursion below instead of treating as a file. |
| 1133 | if (statusCode === '??' && rel.endsWith('/')) { |
| 1134 | untrackedDirs.push(rel); |
| 1135 | continue; |
| 1136 | } |
| 1137 | |
| 1138 | const filePath = normalizePath(prefix + rel); |
| 1139 | if (!isSourceFile(filePath, overrides)) continue; |
| 1140 | |
| 1141 | if (statusCode.includes('D')) { |
| 1142 | // Deletions stay unfiltered: getChangedFiles acts on one only when the |
| 1143 | // path is already tracked in the DB, where removal is always correct — and |
| 1144 | // that lets a newly-excluded dir's stale rows clean themselves up. (#766) |
| 1145 | out.deleted.push(filePath); |
| 1146 | continue; |
| 1147 | } |
| 1148 | |
| 1149 | // Added (`??`) / modified files inside an excluded dir must not enter the |
| 1150 | // index — match against the repo-relative path, same as the full scan. (#766) |
| 1151 | if (ig.ignores(rel)) continue; |
| 1152 | // User `codegraph.json` `exclude` (#999) is project-root-relative, so it's |
| 1153 | // matched against the full path — sync must not re-add a tracked file the |
| 1154 | // full index now keeps out. Deletions above stay unfiltered so a file that |
| 1155 | // WAS indexed before an exclude was added still cleans itself out. |
| 1156 | if (exclude && exclude.ignores(filePath)) continue; |
| 1157 | |
| 1158 | if (statusCode === '??') { |
| 1159 | out.added.push(filePath); |
| 1160 | } else { |
| 1161 | // M, MM, AM, A (staged), etc. — treat as modified |
| 1162 | out.modified.push(filePath); |
no test coverage detected