fix(gsd): deduplicate resolveGitHeadPath function (#1015)

Move resolveGitHeadPath() and nudgeGitBranchCache() to worktree.ts as
the canonical shared location. Both auto-worktree.ts and
worktree-command.ts now import from worktree.ts instead of defining
their own copies.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-17 18:02:00 -06:00 committed by GitHub
parent f8b16beddb
commit ea0dcef576
3 changed files with 47 additions and 88 deletions

View file

@ -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) ────────────────────────────────────────
/**

View file

@ -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;

View file

@ -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
}
}
}