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:
co-authored by
Colby McHenry
parent
9b8bb4aba0
commit
9a32487491
@@ -7793,6 +7793,82 @@ describe('Nested non-submodule git repos', () => {
|
||||
expect(ig.ignores('dist/')).toBe(true); // valid rule survives
|
||||
expect(ig.ignores('src/app.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('buildDefaultIgnore honors .git/info/exclude (#1728)', async () => {
|
||||
const { execFileSync } = await import('child_process');
|
||||
const git = (cwd: string, ...args: string[]) =>
|
||||
execFileSync('git', args, { cwd, stdio: 'pipe' });
|
||||
|
||||
const root = path.join(tempDir, 'exclude-root');
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
fs.writeFileSync(path.join(root, 'src.ts'), 'export const x = 1;\n');
|
||||
fs.mkdirSync(path.join(root, '.claude', 'worktrees', 'agent-1'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(root, '.claude', 'worktrees', 'agent-1', 'src.ts'),
|
||||
'export const w = 1;\n',
|
||||
);
|
||||
// Not in .gitignore — only in info/exclude (the reporter's exact shape).
|
||||
fs.writeFileSync(
|
||||
path.join(root, '.git', 'info', 'exclude'),
|
||||
'**/.claude/worktrees/\n',
|
||||
);
|
||||
|
||||
const ig = buildDefaultIgnore(root);
|
||||
expect(ig.ignores('src.ts')).toBe(false);
|
||||
expect(ig.ignores('.claude/worktrees/agent-1/src.ts')).toBe(true);
|
||||
expect(ig.ignores('.claude/worktrees/')).toBe(true);
|
||||
|
||||
// ScopeIgnore (watcher path) agrees, including via git ignored-dir seeding.
|
||||
const scope = buildScopeIgnore(root);
|
||||
expect(scope.ignores('src.ts')).toBe(false);
|
||||
expect(scope.ignores('.claude/worktrees/agent-1/')).toBe(true);
|
||||
expect(scope.ignores('.claude/worktrees/agent-1/src.ts')).toBe(true);
|
||||
});
|
||||
|
||||
it('buildDefaultIgnore honors core.excludesFile (#1728)', async () => {
|
||||
const { execFileSync } = await import('child_process');
|
||||
const git = (cwd: string, ...args: string[]) =>
|
||||
execFileSync('git', args, { cwd, stdio: 'pipe' });
|
||||
|
||||
const root = path.join(tempDir, 'excludesfile-root');
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
const globalExcludes = path.join(tempDir, 'global-excludes');
|
||||
fs.writeFileSync(globalExcludes, 'scratch/\n');
|
||||
git(root, 'config', 'core.excludesFile', globalExcludes);
|
||||
fs.mkdirSync(path.join(root, 'scratch'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'scratch', 'tmp.ts'), 'export const t = 1;\n');
|
||||
fs.writeFileSync(path.join(root, 'app.ts'), 'export const a = 1;\n');
|
||||
|
||||
const ig = buildDefaultIgnore(root);
|
||||
expect(ig.ignores('app.ts')).toBe(false);
|
||||
expect(ig.ignores('scratch/')).toBe(true);
|
||||
expect(ig.ignores('scratch/tmp.ts')).toBe(true);
|
||||
});
|
||||
|
||||
it('buildScopeIgnore prunes dirs ignored only by a nested .gitignore (#1728)', async () => {
|
||||
const { execFileSync } = await import('child_process');
|
||||
const git = (cwd: string, ...args: string[]) =>
|
||||
execFileSync('git', args, { cwd, stdio: 'pipe' });
|
||||
|
||||
const root = path.join(tempDir, 'nested-gi-root');
|
||||
fs.mkdirSync(path.join(root, 'pkg', 'build'), { recursive: true });
|
||||
git(root, 'init', '-q');
|
||||
git(root, 'config', 'user.email', 'test@test.com');
|
||||
git(root, 'config', 'user.name', 'Test');
|
||||
fs.writeFileSync(path.join(root, 'pkg', 'app.ts'), 'export const a = 1;\n');
|
||||
fs.writeFileSync(path.join(root, 'pkg', 'build', 'out.ts'), 'export const o = 1;\n');
|
||||
fs.writeFileSync(path.join(root, 'pkg', '.gitignore'), 'build/\n');
|
||||
// Commit only the non-ignored file so git still reports build/ as ignored-other.
|
||||
git(root, 'add', 'pkg/app.ts', 'pkg/.gitignore');
|
||||
git(root, 'commit', '-q', '-m', 'init');
|
||||
|
||||
const scope = buildScopeIgnore(root);
|
||||
expect(scope.ignores('pkg/app.ts')).toBe(false);
|
||||
expect(scope.ignores('pkg/build/')).toBe(true);
|
||||
expect(scope.ignores('pkg/build/out.ts')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user