mirror of https://github.com/garrytan/gstack.git
fix(browse): skip the .gitignore append when git already ignores .gstack/
ensureStateDir appended ".gstack/" to a tracked .gitignore even when git already ignored the directory via global excludes, .git/info/exclude, or a parent .gitignore — dirtying the working tree on every daemon start. Run `git check-ignore -q -- .gstack/` first and return early when git says it's covered; git-missing/not-a-repo/timeout all fall through to the existing text-check append (the safe default). Closes #2385. Contributed by @gregario (PR #2430). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b50aa963ec
commit
0350b2d758
|
|
@ -78,6 +78,20 @@ 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',
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the .gstack/ state directory if it doesn't exist.
|
||||
* Throws with a clear message on permission errors.
|
||||
|
|
@ -96,6 +110,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');
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue