diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index 3edd40475..2177216f1 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -6,8 +6,8 @@ * manages create, enter, detect, and teardown for auto-mode worktrees. */ -import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync, mkdirSync, realpathSync, utimesSync, unlinkSync } from "node:fs"; -import { isAbsolute, join, resolve } from "node:path"; +import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync, mkdirSync, realpathSync, unlinkSync } from "node:fs"; +import { isAbsolute, join } from "node:path"; import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js"; import { copyWorktreeDb, reconcileWorktreeDb, isDbAvailable } from "./gsd-db.js"; import { execSync, execFileSync } from "node:child_process"; @@ -16,7 +16,7 @@ import { removeWorktree, worktreePath, } from "./worktree-manager.js"; -import { detectWorktreeName } from "./worktree.js"; +import { detectWorktreeName, resolveGitHeadPath, nudgeGitBranchCache } from "./worktree.js"; import { MergeConflictError, readIntegrationBranch, @@ -43,41 +43,6 @@ import { /** Original project root before chdir into auto-worktree. */ let originalBase: string | null = null; -// ─── Git Helpers (local, mirrors worktree-command.ts pattern) ────────────── - -function resolveGitHeadPath(dir: string): string | null { - const gitPath = join(dir, ".git"); - if (!existsSync(gitPath)) return null; - try { - const content = readFileSync(gitPath, "utf8").trim(); - if (content.startsWith("gitdir: ")) { - const gitDir = resolve(dir, content.slice(8)); - const headPath = join(gitDir, "HEAD"); - return existsSync(headPath) ? headPath : null; - } - const headPath = join(dir, ".git", "HEAD"); - return existsSync(headPath) ? headPath : null; - } catch { - return null; - } -} - -/** - * Nudge pi's FooterDataProvider to re-read the git branch after chdir. - * Touches HEAD in both old and new cwd to fire the fs watcher. - */ -function nudgeGitBranchCache(previousCwd: string): void { - const now = new Date(); - for (const dir of [previousCwd, process.cwd()]) { - try { - const headPath = resolveGitHeadPath(dir); - if (headPath) utimesSync(headPath, now, now); - } catch { - // Best-effort - } - } -} - // ─── Worktree Post-Create Hook (#597) ──────────────────────────────────────── /** diff --git a/src/resources/extensions/gsd/worktree-command.ts b/src/resources/extensions/gsd/worktree-command.ts index a498b630f..d2fd35fb1 100644 --- a/src/resources/extensions/gsd/worktree-command.ts +++ b/src/resources/extensions/gsd/worktree-command.ts @@ -12,7 +12,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; import { loadPrompt } from "./prompt-loader.js"; -import { autoCommitCurrentBranch, getMainBranch } from "./worktree.js"; +import { autoCommitCurrentBranch, getMainBranch, resolveGitHeadPath, nudgeGitBranchCache } from "./worktree.js"; import { runWorktreePostCreateHook } from "./auto-worktree.js"; import { showConfirm } from "../shared/confirm-ui.js"; import { gsdRoot, milestonesDir } from "./paths.js"; @@ -31,9 +31,9 @@ import { } from "./worktree-manager.js"; import { inferCommitType } from "./git-service.js"; import type { FileLineStat } from "./worktree-manager.js"; -import { existsSync, realpathSync, readFileSync, readdirSync, rmSync, unlinkSync, utimesSync } from "node:fs"; +import { existsSync, realpathSync, readdirSync, rmSync, unlinkSync } from "node:fs"; import { nativeMergeAbort } from "./native-git-bridge.js"; -import { join, resolve, sep } from "node:path"; +import { join, sep } from "node:path"; /** * Tracks the original project root so we can switch back. @@ -46,52 +46,6 @@ export function getWorktreeOriginalCwd(): string | null { return originalCwd; } -/** - * Resolve the git HEAD file path for a given directory. - * Handles both normal repos (.git is a directory) and worktrees (.git is a file). - */ -function resolveGitHeadPath(dir: string): string | null { - const gitPath = join(dir, ".git"); - if (!existsSync(gitPath)) return null; - - try { - const content = readFileSync(gitPath, "utf8").trim(); - if (content.startsWith("gitdir: ")) { - // Worktree — .git is a file pointing to the real gitdir - const gitDir = resolve(dir, content.slice(8)); - const headPath = join(gitDir, "HEAD"); - return existsSync(headPath) ? headPath : null; - } - // Normal repo — .git is a directory - const headPath = join(dir, ".git", "HEAD"); - return existsSync(headPath) ? headPath : null; - } catch { - return null; - } -} - -/** - * Nudge pi's FooterDataProvider to re-read the git branch. - * - * The footer caches the branch and watches a single .git dir for changes. - * After process.chdir() into a worktree (or back), the watcher is stale — - * it's still watching the old git dir. We touch HEAD in both the old and - * new git dirs to ensure the watcher fires regardless of which one it's - * monitoring. This clears cachedBranch; the next getGitBranch() call uses - * the new process.cwd() and picks up the correct branch. - */ -function nudgeGitBranchCache(previousCwd: string): void { - const now = new Date(); - for (const dir of [previousCwd, process.cwd()]) { - try { - const headPath = resolveGitHeadPath(dir); - if (headPath) utimesSync(headPath, now, now); - } catch { - // Best-effort — branch display may be stale - } - } -} - /** Get the name of the active worktree, or null if not in one. */ export function getActiveWorktreeName(): string | null { if (!originalCwd) return null; diff --git a/src/resources/extensions/gsd/worktree.ts b/src/resources/extensions/gsd/worktree.ts index 621867e2e..7669aa9db 100644 --- a/src/resources/extensions/gsd/worktree.ts +++ b/src/resources/extensions/gsd/worktree.ts @@ -12,7 +12,8 @@ * SLICE_BRANCH_RE) remain for backwards compatibility with legacy branches. */ -import { sep } from "node:path"; +import { existsSync, readFileSync, utimesSync } from "node:fs"; +import { join, resolve, sep } from "node:path"; import { GitServiceImpl, writeIntegrationBranch, type TaskCommitContext } from "./git-service.js"; import { loadEffectiveGSDPreferences } from "./preferences.js"; @@ -177,4 +178,43 @@ export function autoCommitCurrentBranch( return getService(basePath).autoCommit(unitType, unitId, [], taskContext); } +// ─── Git HEAD Resolution ──────────────────────────────────────────────────── +/** + * Resolve the git HEAD file path for a given directory. + * Handles both normal repos (.git is a directory) and worktrees (.git is a file + * containing a `gitdir:` pointer to the real gitdir). + */ +export function resolveGitHeadPath(dir: string): string | null { + const gitPath = join(dir, ".git"); + if (!existsSync(gitPath)) return null; + + try { + const content = readFileSync(gitPath, "utf8").trim(); + if (content.startsWith("gitdir: ")) { + const gitDir = resolve(dir, content.slice(8)); + const headPath = join(gitDir, "HEAD"); + return existsSync(headPath) ? headPath : null; + } + const headPath = join(dir, ".git", "HEAD"); + return existsSync(headPath) ? headPath : null; + } catch { + return null; + } +} + +/** + * Nudge pi's FooterDataProvider to re-read the git branch after chdir. + * Touches HEAD in both old and new cwd to fire the fs watcher. + */ +export function nudgeGitBranchCache(previousCwd: string): void { + const now = new Date(); + for (const dir of [previousCwd, process.cwd()]) { + try { + const headPath = resolveGitHeadPath(dir); + if (headPath) utimesSync(headPath, now, now); + } catch { + // Best-effort + } + } +}