Commit efea8a9ce1e6

Vincent Demeester <vincent@sbr.pm>
2026-02-16 13:25:13
fix: auto-detect --head branch for pr-create in worktree contexts
- Prevent "No commits between main and main" when cwd is on main but work was done in a worktree on a feature branch - Add worktree detection via git worktree list (Strategy 4) - Extract resolveGitCwd helper from execGh for reuse Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent 25ac81f
Changed files (2)
dots
pi
agent
extensions
github
dots/pi/agent/extensions/github/actions/pr.ts
@@ -25,6 +25,7 @@ import {
 	getReviewDecisionText,
 	formatRelativeDate,
 	execGh,
+	resolveGitCwd,
 	findPRTemplate,
 } from "../utils";
 
@@ -301,6 +302,41 @@ export async function handlePRCreate(
 
 	const args = ["pr", "create", "--title", params.title];
 
+	// Auto-detect --head when not explicitly provided.
+	// This prevents "No commits between main and main" errors when the tool's
+	// cwd (ctx.cwd) is the main repo but work was done in a worktree on a
+	// different branch. We resolve the effective git cwd (worktree-aware) and
+	// read the branch from there. If it differs from the default branch we set
+	// --head so gh targets the correct branch even if the process cwd falls
+	// back to the main repo checkout.
+	if (!params.head) {
+		try {
+			const gitCwd = await resolveGitCwd(pi, ctx);
+			const branchResult = await pi.exec(
+				"git", ["rev-parse", "--abbrev-ref", "HEAD"],
+				{ cwd: gitCwd, timeout: 5000 },
+			);
+			if (branchResult.code === 0) {
+				const branch = branchResult.stdout.trim();
+				const defaultBranches = ["main", "master"];
+				if (branch && !defaultBranches.includes(branch)) {
+					// Detect fork owner so --head is <owner>:<branch>
+					const ownerResult = await pi.exec(
+						"gh", ["repo", "view", "--json", "owner", "-q", ".owner.login"],
+						{ cwd: gitCwd, timeout: 10000 },
+					);
+					if (ownerResult.code === 0 && ownerResult.stdout.trim()) {
+						args.push("--head", `${ownerResult.stdout.trim()}:${branch}`);
+					} else {
+						args.push("--head", branch);
+					}
+				}
+			}
+		} catch {
+			// Ignore detection errors — gh will use defaults
+		}
+	}
+
 	// Add template if found and body not explicitly provided
 	if (template && !params.body) {
 		args.push("--template", template);
dots/pi/agent/extensions/github/utils.ts
@@ -118,6 +118,34 @@ async function detectWorktreeContext(
 	} catch {
 		// Ignore errors in bash history scanning
 	}
+
+	// Strategy 4: Check `git worktree list` from the main repo for any linked
+	// worktrees under the conventional ~/.local/share/worktrees/ directory.
+	// Only use this if there is exactly one linked worktree (ambiguous otherwise).
+	try {
+		const wtResult = await pi.exec(
+			"git", ["worktree", "list", "--porcelain"],
+			{ cwd: ctx.cwd, timeout: 5000 },
+		);
+		if (wtResult.code === 0) {
+			const lines = wtResult.stdout.split("\n");
+			const worktrees: string[] = [];
+			for (const line of lines) {
+				if (line.startsWith("worktree ")) {
+					const wtPath = line.slice("worktree ".length);
+					if (wtPath.includes("/.local/share/worktrees/")) {
+						worktrees.push(wtPath);
+					}
+				}
+			}
+			if (worktrees.length === 1) {
+				cachedWorktreeRoot = worktrees[0];
+				return cachedWorktreeRoot;
+			}
+		}
+	} catch {
+		// Ignore errors
+	}
 	
 	return null;
 }
@@ -130,6 +158,20 @@ export function resetGitRoot() {
 	cachedWorktreeRoot = null;
 }
 
+/**
+ * Resolve the effective git working directory.
+ * Prefers worktree context, falls back to git root, then ctx.cwd.
+ */
+export async function resolveGitCwd(
+	pi: ExtensionAPI,
+	ctx: ExtensionContext,
+): Promise<string> {
+	const worktree = await detectWorktreeContext(pi, ctx);
+	if (worktree) return worktree;
+	const root = await findGitRoot(pi, ctx.cwd);
+	return root ?? ctx.cwd;
+}
+
 /**
  * Execute gh command with correct working directory.
  * Automatically uses git repository root if available.
@@ -141,15 +183,7 @@ export async function execGh(
 	args: string[],
 	options?: { signal?: AbortSignal; timeout?: number }
 ) {
-	// First check if we're working in a worktree context
-	const worktree = await detectWorktreeContext(pi, ctx);
-	if (worktree) {
-		return await pi.exec("gh", args, { ...options, cwd: worktree });
-	}
-	
-	// Fall back to regular git root detection
-	const root = await findGitRoot(pi, ctx.cwd);
-	const cwd = root ?? ctx.cwd;
+	const cwd = await resolveGitCwd(pi, ctx);
 	return await pi.exec("gh", args, { ...options, cwd });
 }