fix(browse): skip .gitignore append when git already ignores .gstack/ (#2385)

Fixes #2385.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Greg Jackson 2026-08-01 18:57:41 +01:00
parent a3259400a3
commit 670af8bfbc
2 changed files with 49 additions and 0 deletions

View File

@ -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. * Create the .gstack/ state directory if it doesn't exist.
* Throws with a clear message on permission errors. * 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 // 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'); const gitignorePath = path.join(config.projectDir, '.gitignore');
try { try {
const content = fs.readFileSync(gitignorePath, 'utf-8'); const content = fs.readFileSync(gitignorePath, 'utf-8');

View File

@ -124,6 +124,41 @@ describe('config', () => {
expect(fs.existsSync(path.join(tmpDir, '.gitignore'))).toBe(false); expect(fs.existsSync(path.join(tmpDir, '.gitignore'))).toBe(false);
fs.rmSync(tmpDir, { recursive: true, force: true }); 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', () => { describe('getRemoteSlug', () => {