From 670af8bfbcb638952e3a24bcd9ec496d19f5aaba Mon Sep 17 00:00:00 2001 From: Greg Jackson Date: Sat, 1 Aug 2026 18:57:41 +0100 Subject: [PATCH 1/2] fix(browse): skip .gitignore append when git already ignores .gstack/ (#2385) Fixes #2385. Co-Authored-By: Claude Opus 4.6 --- browse/src/config.ts | 14 ++++++++++++++ browse/test/config.test.ts | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/browse/src/config.ts b/browse/src/config.ts index fc4c97b958..61c9fa32f0 100644 --- a/browse/src/config.ts +++ b/browse/src/config.ts @@ -78,6 +78,17 @@ export function resolveConfig( }; } +function isIgnoredByGit(projectDir: string, relPath: string): boolean { + try { + const proc = Bun.spawnSync(['git', 'check-ignore', '-q', '--', relPath], { + cwd: projectDir, stdout: 'pipe', stderr: 'pipe', + }); + return proc.exitCode === 0; + } catch { + return false; + } +} + /** * Create the .gstack/ state directory if it doesn't exist. * Throws with a clear message on permission errors. @@ -96,6 +107,9 @@ export function ensureStateDir(config: BrowseConfig): void { } // Ensure .gstack/ is in the project's .gitignore + // First, check if git already ignores .gstack/ (via global excludes, .git/info/exclude, or parent .gitignore) + if (isIgnoredByGit(config.projectDir, '.gstack/')) return; + const gitignorePath = path.join(config.projectDir, '.gitignore'); try { const content = fs.readFileSync(gitignorePath, 'utf-8'); diff --git a/browse/test/config.test.ts b/browse/test/config.test.ts index 8daa27c3eb..5f8cd55353 100644 --- a/browse/test/config.test.ts +++ b/browse/test/config.test.ts @@ -124,6 +124,41 @@ describe('config', () => { expect(fs.existsSync(path.join(tmpDir, '.gitignore'))).toBe(false); fs.rmSync(tmpDir, { recursive: true, force: true }); }); + + test('leaves .gitignore alone when git already ignores .gstack/ globally', () => { + const { spawnSync } = require('child_process'); + const tmpDir = path.join(os.tmpdir(), `browse-gitignore-global-${Date.now()}`); + fs.mkdirSync(tmpDir, { recursive: true }); + + // Set up a real git repo + spawnSync('git', ['init', '-q'], { cwd: tmpDir }); + spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: tmpDir }); + spawnSync('git', ['config', 'user.name', 'Test'], { cwd: tmpDir }); + + // Write a global excludes file that ignores .gstack/ + const excludesFile = path.join(tmpDir, 'global-gitignore'); + fs.writeFileSync(excludesFile, '.gstack/\n'); + spawnSync('git', ['config', 'core.excludesFile', excludesFile], { cwd: tmpDir }); + + // .gitignore exists but does NOT contain .gstack/ + fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'node_modules/\n'); + spawnSync('git', ['add', '.gitignore'], { cwd: tmpDir }); + spawnSync('git', ['commit', '-qm', 'init'], { cwd: tmpDir }); + + // Verify git knows .gstack/ is ignored + const check = spawnSync('git', ['check-ignore', '-q', '.gstack/'], { cwd: tmpDir }); + expect(check.status).toBe(0); + + const config = resolveConfig({ BROWSE_STATE_FILE: path.join(tmpDir, '.gstack', 'browse.json') }); + ensureStateDir(config); + + // .gitignore must NOT have been modified + const content = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8'); + expect(content).toBe('node_modules/\n'); + expect(fs.existsSync(path.join(tmpDir, '.gstack'))).toBe(true); + + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); }); describe('getRemoteSlug', () => { From d7540ee2d5f013e58d3446c98c4b977c9ca9a7d4 Mon Sep 17 00:00:00 2001 From: Greg Jackson Date: Thu, 6 Aug 2026 14:26:02 +0100 Subject: [PATCH 2/2] fix(browse): add timeout to git check-ignore and clarify false return Addresses review feedback from kraftbj: - Add timeout: 2_000 to Bun.spawnSync to prevent indefinite hangs on network filesystems or slow credential helpers - Add comment explaining why the catch returns false (intentionally collapses "not ignored" and "git errored" since falling through to the text-check is the safe default) Co-Authored-By: Claude Opus 4.6 --- browse/src/config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/browse/src/config.ts b/browse/src/config.ts index 61c9fa32f0..f348239dee 100644 --- a/browse/src/config.ts +++ b/browse/src/config.ts @@ -82,9 +82,12 @@ function isIgnoredByGit(projectDir: string, relPath: string): boolean { try { const proc = Bun.spawnSync(['git', 'check-ignore', '-q', '--', relPath], { cwd: projectDir, stdout: 'pipe', stderr: 'pipe', + timeout: 2_000, }); return proc.exitCode === 0; } catch { + // git not found, timed out, or not a repo (exit 128). Fall through to + // the text-check path — appending is the safe default when unsure. return false; } }