Merge pull request #3683 from Tibsfox/fix/project-root-cwd-crash

fix(gsd): handle deleted cwd crash and validate main_branch pref
This commit is contained in:
Jeremy McSpadden 2026-04-07 07:05:57 -05:00 committed by GitHub
commit 44872ca501
3 changed files with 66 additions and 2 deletions

View file

@ -1453,8 +1453,13 @@ export function mergeMilestoneToMain(
originalBasePath_,
milestoneId,
);
// Validate prefs.main_branch exists before using it — a stale preference
// (e.g. "master" when repo uses "main") causes merge failure (#3589).
const validatedPrefBranch = prefs.main_branch && nativeBranchExists(originalBasePath_, prefs.main_branch)
? prefs.main_branch
: undefined;
const mainBranch =
integrationBranch ?? prefs.main_branch ?? nativeDetectMainBranch(originalBasePath_);
integrationBranch ?? validatedPrefBranch ?? nativeDetectMainBranch(originalBasePath_);
// Remove transient project-root state files before any branch or merge
// operation. Untracked milestone metadata can otherwise block squash merges.

View file

@ -13,7 +13,13 @@ export interface GsdDispatchContext {
}
export function projectRoot(): string {
const cwd = process.cwd();
let cwd: string;
try {
cwd = process.cwd();
} catch {
// cwd directory was deleted (e.g. worktree teardown) — fall back to HOME (#3598)
cwd = process.env.HOME ?? "/";
}
const root = resolveProjectRoot(cwd);
if (root !== cwd) {
assertSafeDirectory(cwd);

View file

@ -0,0 +1,53 @@
/**
* Regression test for #3598 projectRoot ENOENT crash on deleted cwd
*
* When the working directory is deleted (e.g. worktree teardown), process.cwd()
* throws ENOENT. The fix wraps process.cwd() in a try/catch and falls back to
* process.env.HOME.
*
* Also verifies #3589 nativeBranchExists validation for prefs.main_branch
* in auto-worktree.ts to prevent merge failures with stale preferences.
*
* Structural verification test reads source to confirm the guards exist.
*/
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const contextSource = readFileSync(join(__dirname, '..', 'commands', 'context.ts'), 'utf-8');
const worktreeSource = readFileSync(join(__dirname, '..', 'auto-worktree.ts'), 'utf-8');
describe('projectRoot cwd crash guard (#3598)', () => {
test('projectRoot wraps process.cwd() in try/catch', () => {
assert.match(contextSource, /try\s*\{[\s\S]*?process\.cwd\(\)/,
'process.cwd() should be inside a try block');
});
test('catch block falls back to process.env.HOME', () => {
assert.match(contextSource, /catch[\s\S]*?process\.env\.HOME/,
'catch block should fall back to process.env.HOME');
});
test('projectRoot function is exported', () => {
assert.match(contextSource, /export function projectRoot\(\)/,
'projectRoot should be an exported function');
});
});
describe('main_branch nativeBranchExists validation (#3589)', () => {
test('prefs.main_branch is validated with nativeBranchExists', () => {
assert.match(worktreeSource, /nativeBranchExists\(.*prefs\.main_branch\)/,
'nativeBranchExists should validate prefs.main_branch');
});
test('validatedPrefBranch falls back to undefined when branch missing', () => {
assert.match(worktreeSource, /validatedPrefBranch[\s\S]*?:\s*undefined/,
'validatedPrefBranch should fall back to undefined');
});
});