Merge pull request #477 from AutoMaker-Org/fix/dynamic-branch-references

fix: use dynamic branch references instead of hardcoded origin/main
This commit is contained in:
Shirone
2026-01-13 23:59:49 +00:00
committed by GitHub
4 changed files with 101 additions and 43 deletions

View File

@@ -13,7 +13,7 @@ import { promisify } from 'util';
import path from 'path';
import * as secureFs from '../../../lib/secure-fs.js';
import { isGitRepo } from '@automaker/git-utils';
import { getErrorMessage, logError, normalizePath } from '../common.js';
import { getErrorMessage, logError, normalizePath, execEnv, isGhCliAvailable } from '../common.js';
import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
import { createLogger } from '@automaker/utils';
@@ -121,6 +121,52 @@ async function scanWorktreesDirectory(
return discovered;
}
/**
* Fetch open PRs from GitHub and create a map of branch name to PR info.
* This allows detecting PRs that were created outside the app.
*/
async function fetchGitHubPRs(projectPath: string): Promise<Map<string, WorktreePRInfo>> {
const prMap = new Map<string, WorktreePRInfo>();
try {
// Check if gh CLI is available
const ghAvailable = await isGhCliAvailable();
if (!ghAvailable) {
return prMap;
}
// Fetch open PRs from GitHub
const { stdout } = await execAsync(
'gh pr list --state open --json number,title,url,state,headRefName,createdAt --limit 1000',
{ cwd: projectPath, env: execEnv, timeout: 15000 }
);
const prs = JSON.parse(stdout || '[]') as Array<{
number: number;
title: string;
url: string;
state: string;
headRefName: string;
createdAt: string;
}>;
for (const pr of prs) {
prMap.set(pr.headRefName, {
number: pr.number,
url: pr.url,
title: pr.title,
state: pr.state,
createdAt: pr.createdAt,
});
}
} catch (error) {
// Silently fail - PR detection is optional
logger.warn(`Failed to fetch GitHub PRs: ${getErrorMessage(error)}`);
}
return prMap;
}
export function createListHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
@@ -241,11 +287,23 @@ export function createListHandler() {
}
}
// Add PR info from metadata for each worktree
// Add PR info from metadata or GitHub for each worktree
// Only fetch GitHub PRs if includeDetails is requested (performance optimization)
const githubPRs = includeDetails
? await fetchGitHubPRs(projectPath)
: new Map<string, WorktreePRInfo>();
for (const worktree of worktrees) {
const metadata = allMetadata.get(worktree.branch);
if (metadata?.pr) {
// Use stored metadata (more complete info)
worktree.pr = metadata.pr;
} else if (includeDetails) {
// Fall back to GitHub PR detection only when includeDetails is requested
const githubPR = githubPRs.get(worktree.branch);
if (githubPR) {
worktree.pr = githubPR;
}
}
}

View File

@@ -328,20 +328,6 @@ export function BoardView() {
fetchBranches();
}, [currentProject, worktreeRefreshKey]);
// Calculate unarchived card counts per branch
const branchCardCounts = useMemo(() => {
return hookFeatures.reduce(
(counts, feature) => {
if (feature.status !== 'completed') {
const branch = feature.branchName ?? 'main';
counts[branch] = (counts[branch] || 0) + 1;
}
return counts;
},
{} as Record<string, number>
);
}, [hookFeatures]);
// Custom collision detection that prioritizes columns over cards
const collisionDetectionStrategy = useCallback((args: any) => {
// First, check if pointer is within a column
@@ -426,6 +412,22 @@ export function BoardView() {
const selectedWorktreeBranch =
currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main';
// Calculate unarchived card counts per branch
const branchCardCounts = useMemo(() => {
// Use primary worktree branch as default for features without branchName
const primaryBranch = worktrees.find((w) => w.isMain)?.branch || 'main';
return hookFeatures.reduce(
(counts, feature) => {
if (feature.status !== 'completed') {
const branch = feature.branchName ?? primaryBranch;
counts[branch] = (counts[branch] || 0) + 1;
}
return counts;
},
{} as Record<string, number>
);
}, [hookFeatures, worktrees]);
// Helper function to add and select a worktree
const addAndSelectWorktree = useCallback(
(worktreeResult: { path: string; branch: string }) => {
@@ -724,10 +726,11 @@ export function BoardView() {
[handleAddFeature, handleStartImplementation, defaultSkipTests]
);
// Handler for resolving conflicts - creates a feature to pull from origin/main and resolve conflicts
// Handler for resolving conflicts - creates a feature to pull from the remote branch and resolve conflicts
const handleResolveConflicts = useCallback(
async (worktree: WorktreeInfo) => {
const description = `Pull latest from origin/main and resolve conflicts. Merge origin/main into the current branch (${worktree.branch}), resolving any merge conflicts that arise. After resolving conflicts, ensure the code compiles and tests pass.`;
const remoteBranch = `origin/${worktree.branch}`;
const description = `Pull latest from ${remoteBranch} and resolve conflicts. Merge ${remoteBranch} into the current branch (${worktree.branch}), resolving any merge conflicts that arise. After resolving conflicts, ensure the code compiles and tests pass.`;
// Create the feature
const featureData = {
@@ -1710,6 +1713,7 @@ export function BoardView() {
onOpenChange={setShowCreatePRDialog}
worktree={selectedWorktreeForAction}
projectPath={currentProject?.path || null}
defaultBaseBranch={selectedWorktreeBranch}
onCreated={(prUrl) => {
// If a PR was created and we have the worktree branch, update all features on that branch with the PR URL
if (prUrl && selectedWorktreeForAction?.branch) {

View File

@@ -30,6 +30,8 @@ interface CreatePRDialogProps {
worktree: WorktreeInfo | null;
projectPath: string | null;
onCreated: (prUrl?: string) => void;
/** Default base branch for the PR (defaults to 'main' if not provided) */
defaultBaseBranch?: string;
}
export function CreatePRDialog({
@@ -38,10 +40,11 @@ export function CreatePRDialog({
worktree,
projectPath,
onCreated,
defaultBaseBranch = 'main',
}: CreatePRDialogProps) {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [baseBranch, setBaseBranch] = useState('main');
const [baseBranch, setBaseBranch] = useState(defaultBaseBranch);
const [commitMessage, setCommitMessage] = useState('');
const [isDraft, setIsDraft] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@@ -59,7 +62,7 @@ export function CreatePRDialog({
setTitle('');
setBody('');
setCommitMessage('');
setBaseBranch('main');
setBaseBranch(defaultBaseBranch);
setIsDraft(false);
setError(null);
// Also reset result states when opening for a new worktree
@@ -74,7 +77,7 @@ export function CreatePRDialog({
setTitle('');
setBody('');
setCommitMessage('');
setBaseBranch('main');
setBaseBranch(defaultBaseBranch);
setIsDraft(false);
setError(null);
setPrUrl(null);
@@ -82,7 +85,7 @@ export function CreatePRDialog({
setShowBrowserFallback(false);
operationCompletedRef.current = false;
}
}, [open, worktree?.path]);
}, [open, worktree?.path, defaultBaseBranch]);
const handleCreate = async () => {
if (!worktree) return;

View File

@@ -217,27 +217,20 @@ export function WorktreeActionsDropdown({
)}
</DropdownMenuItem>
</TooltipWrapper>
{!worktree.isMain && (
<TooltipWrapper
showTooltip={!!gitOpsDisabledReason}
tooltipContent={gitOpsDisabledReason}
<TooltipWrapper showTooltip={!!gitOpsDisabledReason} tooltipContent={gitOpsDisabledReason}>
<DropdownMenuItem
onClick={() => canPerformGitOps && onResolveConflicts(worktree)}
disabled={!canPerformGitOps}
className={cn(
'text-xs text-purple-500 focus:text-purple-600',
!canPerformGitOps && 'opacity-50 cursor-not-allowed'
)}
>
<DropdownMenuItem
onClick={() => canPerformGitOps && onResolveConflicts(worktree)}
disabled={!canPerformGitOps}
className={cn(
'text-xs text-purple-500 focus:text-purple-600',
!canPerformGitOps && 'opacity-50 cursor-not-allowed'
)}
>
<GitMerge className="w-3.5 h-3.5 mr-2" />
Pull & Resolve Conflicts
{!canPerformGitOps && (
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
)}
</DropdownMenuItem>
</TooltipWrapper>
)}
<GitMerge className="w-3.5 h-3.5 mr-2" />
Pull & Resolve Conflicts
{!canPerformGitOps && <AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />}
</DropdownMenuItem>
</TooltipWrapper>
<DropdownMenuSeparator />
{/* Open in editor - split button: click main area for default, chevron for other options */}
{effectiveDefaultEditor && (
@@ -332,7 +325,7 @@ export function WorktreeActionsDropdown({
</TooltipWrapper>
)}
{/* Show PR info and Address Comments button if PR exists */}
{!worktree.isMain && hasPR && worktree.pr && (
{hasPR && worktree.pr && (
<>
<DropdownMenuItem
onClick={() => {