mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
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:
@@ -13,7 +13,7 @@ import { promisify } from 'util';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as secureFs from '../../../lib/secure-fs.js';
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { isGitRepo } from '@automaker/git-utils';
|
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 { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
|
|
||||||
@@ -121,6 +121,52 @@ async function scanWorktreesDirectory(
|
|||||||
return discovered;
|
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() {
|
export function createListHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
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) {
|
for (const worktree of worktrees) {
|
||||||
const metadata = allMetadata.get(worktree.branch);
|
const metadata = allMetadata.get(worktree.branch);
|
||||||
if (metadata?.pr) {
|
if (metadata?.pr) {
|
||||||
|
// Use stored metadata (more complete info)
|
||||||
worktree.pr = metadata.pr;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -328,20 +328,6 @@ export function BoardView() {
|
|||||||
fetchBranches();
|
fetchBranches();
|
||||||
}, [currentProject, worktreeRefreshKey]);
|
}, [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
|
// Custom collision detection that prioritizes columns over cards
|
||||||
const collisionDetectionStrategy = useCallback((args: any) => {
|
const collisionDetectionStrategy = useCallback((args: any) => {
|
||||||
// First, check if pointer is within a column
|
// First, check if pointer is within a column
|
||||||
@@ -426,6 +412,22 @@ export function BoardView() {
|
|||||||
const selectedWorktreeBranch =
|
const selectedWorktreeBranch =
|
||||||
currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main';
|
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
|
// Helper function to add and select a worktree
|
||||||
const addAndSelectWorktree = useCallback(
|
const addAndSelectWorktree = useCallback(
|
||||||
(worktreeResult: { path: string; branch: string }) => {
|
(worktreeResult: { path: string; branch: string }) => {
|
||||||
@@ -724,10 +726,11 @@ export function BoardView() {
|
|||||||
[handleAddFeature, handleStartImplementation, defaultSkipTests]
|
[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(
|
const handleResolveConflicts = useCallback(
|
||||||
async (worktree: WorktreeInfo) => {
|
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
|
// Create the feature
|
||||||
const featureData = {
|
const featureData = {
|
||||||
@@ -1710,6 +1713,7 @@ export function BoardView() {
|
|||||||
onOpenChange={setShowCreatePRDialog}
|
onOpenChange={setShowCreatePRDialog}
|
||||||
worktree={selectedWorktreeForAction}
|
worktree={selectedWorktreeForAction}
|
||||||
projectPath={currentProject?.path || null}
|
projectPath={currentProject?.path || null}
|
||||||
|
defaultBaseBranch={selectedWorktreeBranch}
|
||||||
onCreated={(prUrl) => {
|
onCreated={(prUrl) => {
|
||||||
// If a PR was created and we have the worktree branch, update all features on that branch with the PR URL
|
// 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) {
|
if (prUrl && selectedWorktreeForAction?.branch) {
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ interface CreatePRDialogProps {
|
|||||||
worktree: WorktreeInfo | null;
|
worktree: WorktreeInfo | null;
|
||||||
projectPath: string | null;
|
projectPath: string | null;
|
||||||
onCreated: (prUrl?: string) => void;
|
onCreated: (prUrl?: string) => void;
|
||||||
|
/** Default base branch for the PR (defaults to 'main' if not provided) */
|
||||||
|
defaultBaseBranch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CreatePRDialog({
|
export function CreatePRDialog({
|
||||||
@@ -38,10 +40,11 @@ export function CreatePRDialog({
|
|||||||
worktree,
|
worktree,
|
||||||
projectPath,
|
projectPath,
|
||||||
onCreated,
|
onCreated,
|
||||||
|
defaultBaseBranch = 'main',
|
||||||
}: CreatePRDialogProps) {
|
}: CreatePRDialogProps) {
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [body, setBody] = useState('');
|
const [body, setBody] = useState('');
|
||||||
const [baseBranch, setBaseBranch] = useState('main');
|
const [baseBranch, setBaseBranch] = useState(defaultBaseBranch);
|
||||||
const [commitMessage, setCommitMessage] = useState('');
|
const [commitMessage, setCommitMessage] = useState('');
|
||||||
const [isDraft, setIsDraft] = useState(false);
|
const [isDraft, setIsDraft] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -59,7 +62,7 @@ export function CreatePRDialog({
|
|||||||
setTitle('');
|
setTitle('');
|
||||||
setBody('');
|
setBody('');
|
||||||
setCommitMessage('');
|
setCommitMessage('');
|
||||||
setBaseBranch('main');
|
setBaseBranch(defaultBaseBranch);
|
||||||
setIsDraft(false);
|
setIsDraft(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
// Also reset result states when opening for a new worktree
|
// Also reset result states when opening for a new worktree
|
||||||
@@ -74,7 +77,7 @@ export function CreatePRDialog({
|
|||||||
setTitle('');
|
setTitle('');
|
||||||
setBody('');
|
setBody('');
|
||||||
setCommitMessage('');
|
setCommitMessage('');
|
||||||
setBaseBranch('main');
|
setBaseBranch(defaultBaseBranch);
|
||||||
setIsDraft(false);
|
setIsDraft(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
setPrUrl(null);
|
setPrUrl(null);
|
||||||
@@ -82,7 +85,7 @@ export function CreatePRDialog({
|
|||||||
setShowBrowserFallback(false);
|
setShowBrowserFallback(false);
|
||||||
operationCompletedRef.current = false;
|
operationCompletedRef.current = false;
|
||||||
}
|
}
|
||||||
}, [open, worktree?.path]);
|
}, [open, worktree?.path, defaultBaseBranch]);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (!worktree) return;
|
if (!worktree) return;
|
||||||
|
|||||||
@@ -217,27 +217,20 @@ export function WorktreeActionsDropdown({
|
|||||||
)}
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
{!worktree.isMain && (
|
<TooltipWrapper showTooltip={!!gitOpsDisabledReason} tooltipContent={gitOpsDisabledReason}>
|
||||||
<TooltipWrapper
|
<DropdownMenuItem
|
||||||
showTooltip={!!gitOpsDisabledReason}
|
onClick={() => canPerformGitOps && onResolveConflicts(worktree)}
|
||||||
tooltipContent={gitOpsDisabledReason}
|
disabled={!canPerformGitOps}
|
||||||
|
className={cn(
|
||||||
|
'text-xs text-purple-500 focus:text-purple-600',
|
||||||
|
!canPerformGitOps && 'opacity-50 cursor-not-allowed'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<DropdownMenuItem
|
<GitMerge className="w-3.5 h-3.5 mr-2" />
|
||||||
onClick={() => canPerformGitOps && onResolveConflicts(worktree)}
|
Pull & Resolve Conflicts
|
||||||
disabled={!canPerformGitOps}
|
{!canPerformGitOps && <AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />}
|
||||||
className={cn(
|
</DropdownMenuItem>
|
||||||
'text-xs text-purple-500 focus:text-purple-600',
|
</TooltipWrapper>
|
||||||
!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>
|
|
||||||
)}
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{/* Open in editor - split button: click main area for default, chevron for other options */}
|
{/* Open in editor - split button: click main area for default, chevron for other options */}
|
||||||
{effectiveDefaultEditor && (
|
{effectiveDefaultEditor && (
|
||||||
@@ -332,7 +325,7 @@ export function WorktreeActionsDropdown({
|
|||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
)}
|
)}
|
||||||
{/* Show PR info and Address Comments button if PR exists */}
|
{/* Show PR info and Address Comments button if PR exists */}
|
||||||
{!worktree.isMain && hasPR && worktree.pr && (
|
{hasPR && worktree.pr && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user