fix(watcher): align ignore scope with git --exclude-standard (#1728) (#1754)

buildDefaultIgnore now reads .git/info/exclude and core.excludesFile; buildScopeIgnore also seeds directories git ls-files reports as ignored-untracked so nested .gitignore effects prune the live watcher. Defect B (full-project sync per event) was already fixed via scoped pendingFiles sync.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 01:19:05 -05:00
committed by GitHub
co-authored by Colby McHenry
parent 9b8bb4aba0
commit 9a32487491
5 changed files with 272 additions and 24 deletions
+36
View File
@@ -608,6 +608,42 @@ describe('FileWatcher', () => {
watcher.stop();
});
it('info/exclude patterns drop worktree paths from pending (#1728)', async () => {
const { execFileSync } = await import('child_process');
// Re-init the testDir as a real git repo so buildScopeIgnore can read
// .git/info/exclude (createTempDir fixtures are usually plain dirs).
execFileSync('git', ['init', '-q'], { cwd: testDir, stdio: 'pipe' });
fs.mkdirSync(path.join(testDir, '.claude', 'worktrees', 'w1', 'src'), { recursive: true });
fs.writeFileSync(
path.join(testDir, '.claude', 'worktrees', 'w1', 'src', 'x.ts'),
'export const x = 1;\n',
);
fs.writeFileSync(
path.join(testDir, '.git', 'info', 'exclude'),
'**/.claude/worktrees/\n',
);
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
const watcher = newWatcher(syncFn, { debounceMs: 100 });
watcher.start();
await watcher.waitUntilReady();
__emitWatchEventForTests(testDir, '.claude/worktrees/w1/src/x.ts');
expect(watcher.getPendingFiles().map((p) => p.path)).not.toContain(
'.claude/worktrees/w1/src/x.ts',
);
await new Promise((r) => setTimeout(r, 300));
expect(syncFn).not.toHaveBeenCalled();
// In-scope edits still schedule a scoped sync.
fs.writeFileSync(path.join(testDir, 'src', 'ok.ts'), 'export const ok = 1;\n');
__emitWatchEventForTests(testDir, 'src/ok.ts');
await waitFor(() => syncFn.mock.calls.length > 0);
expect(syncFn.mock.calls[0]![0]).toEqual(['src/ok.ts']);
watcher.stop();
});
it('a nested .gitignore inside the scope forces a full sync', async () => {
const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
const watcher = newWatcher(syncFn, { debounceMs: 100 });