mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
Merge pull request #489 from AutoMaker-Org/feature/v0.11.0rc-1768410827235-36uf
feat: enhance pr dialog base branch selection
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* POST /list-branches endpoint - List all local branches
|
* POST /list-branches endpoint - List all local branches and optionally remote branches
|
||||||
*
|
*
|
||||||
* Note: Git repository validation (isGitRepo, hasCommits) is handled by
|
* Note: Git repository validation (isGitRepo, hasCommits) is handled by
|
||||||
* the requireValidWorktree middleware in index.ts
|
* the requireValidWorktree middleware in index.ts
|
||||||
@@ -21,8 +21,9 @@ interface BranchInfo {
|
|||||||
export function createListBranchesHandler() {
|
export function createListBranchesHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { worktreePath } = req.body as {
|
const { worktreePath, includeRemote = false } = req.body as {
|
||||||
worktreePath: string;
|
worktreePath: string;
|
||||||
|
includeRemote?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!worktreePath) {
|
if (!worktreePath) {
|
||||||
@@ -60,6 +61,55 @@ export function createListBranchesHandler() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch remote branches if requested
|
||||||
|
if (includeRemote) {
|
||||||
|
try {
|
||||||
|
// Fetch latest remote refs (silently, don't fail if offline)
|
||||||
|
try {
|
||||||
|
await execAsync('git fetch --all --quiet', {
|
||||||
|
cwd: worktreePath,
|
||||||
|
timeout: 10000, // 10 second timeout
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignore fetch errors - we'll use cached remote refs
|
||||||
|
}
|
||||||
|
|
||||||
|
// List remote branches
|
||||||
|
const { stdout: remoteBranchesOutput } = await execAsync(
|
||||||
|
'git branch -r --format="%(refname:short)"',
|
||||||
|
{ cwd: worktreePath }
|
||||||
|
);
|
||||||
|
|
||||||
|
const localBranchNames = new Set(branches.map((b) => b.name));
|
||||||
|
|
||||||
|
remoteBranchesOutput
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter((b) => b.trim())
|
||||||
|
.forEach((name) => {
|
||||||
|
// Remove any surrounding quotes
|
||||||
|
const cleanName = name.trim().replace(/^['"]|['"]$/g, '');
|
||||||
|
// Skip HEAD pointers like "origin/HEAD"
|
||||||
|
if (cleanName.includes('/HEAD')) return;
|
||||||
|
|
||||||
|
// Only add remote branches if a branch with the exact same name isn't already
|
||||||
|
// in the list. This avoids duplicates if a local branch is named like a remote one.
|
||||||
|
// Note: We intentionally include remote branches even when a local branch with the
|
||||||
|
// same base name exists (e.g., show "origin/main" even if local "main" exists),
|
||||||
|
// since users need to select remote branches as PR base targets.
|
||||||
|
if (!localBranchNames.has(cleanName)) {
|
||||||
|
branches.push({
|
||||||
|
name: cleanName, // Keep full name like "origin/main"
|
||||||
|
isCurrent: false,
|
||||||
|
isRemote: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignore errors fetching remote branches - return local branches only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get ahead/behind count for current branch
|
// Get ahead/behind count for current branch
|
||||||
let aheadCount = 0;
|
let aheadCount = 0;
|
||||||
let behindCount = 0;
|
let behindCount = 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -12,6 +12,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { BranchAutocomplete } from '@/components/ui/branch-autocomplete';
|
||||||
import { GitPullRequest, Loader2, ExternalLink } from 'lucide-react';
|
import { GitPullRequest, Loader2, ExternalLink } from 'lucide-react';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -52,40 +53,62 @@ export function CreatePRDialog({
|
|||||||
const [prUrl, setPrUrl] = useState<string | null>(null);
|
const [prUrl, setPrUrl] = useState<string | null>(null);
|
||||||
const [browserUrl, setBrowserUrl] = useState<string | null>(null);
|
const [browserUrl, setBrowserUrl] = useState<string | null>(null);
|
||||||
const [showBrowserFallback, setShowBrowserFallback] = useState(false);
|
const [showBrowserFallback, setShowBrowserFallback] = useState(false);
|
||||||
|
// Branch fetching state
|
||||||
|
const [branches, setBranches] = useState<string[]>([]);
|
||||||
|
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
|
||||||
// Track whether an operation completed that warrants a refresh
|
// Track whether an operation completed that warrants a refresh
|
||||||
const operationCompletedRef = useRef(false);
|
const operationCompletedRef = useRef(false);
|
||||||
|
|
||||||
|
// Common state reset function to avoid duplication
|
||||||
|
const resetState = useCallback(() => {
|
||||||
|
setTitle('');
|
||||||
|
setBody('');
|
||||||
|
setCommitMessage('');
|
||||||
|
setBaseBranch(defaultBaseBranch);
|
||||||
|
setIsDraft(false);
|
||||||
|
setError(null);
|
||||||
|
setPrUrl(null);
|
||||||
|
setBrowserUrl(null);
|
||||||
|
setShowBrowserFallback(false);
|
||||||
|
operationCompletedRef.current = false;
|
||||||
|
setBranches([]);
|
||||||
|
}, [defaultBaseBranch]);
|
||||||
|
|
||||||
|
// Fetch branches for autocomplete
|
||||||
|
const fetchBranches = useCallback(async () => {
|
||||||
|
if (!worktree?.path) return;
|
||||||
|
|
||||||
|
setIsLoadingBranches(true);
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api?.worktree?.listBranches) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fetch both local and remote branches for PR base branch selection
|
||||||
|
const result = await api.worktree.listBranches(worktree.path, true);
|
||||||
|
if (result.success && result.result) {
|
||||||
|
// Extract branch names, filtering out the current worktree branch
|
||||||
|
const branchNames = result.result.branches
|
||||||
|
.map((b) => b.name)
|
||||||
|
.filter((name) => name !== worktree.branch);
|
||||||
|
setBranches(branchNames);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silently fail - branches will default to main only
|
||||||
|
} finally {
|
||||||
|
setIsLoadingBranches(false);
|
||||||
|
}
|
||||||
|
}, [worktree?.path, worktree?.branch]);
|
||||||
|
|
||||||
// Reset state when dialog opens or worktree changes
|
// Reset state when dialog opens or worktree changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Reset all state on both open and close
|
||||||
|
resetState();
|
||||||
if (open) {
|
if (open) {
|
||||||
// Reset form fields
|
// Fetch fresh branches when dialog opens
|
||||||
setTitle('');
|
fetchBranches();
|
||||||
setBody('');
|
|
||||||
setCommitMessage('');
|
|
||||||
setBaseBranch(defaultBaseBranch);
|
|
||||||
setIsDraft(false);
|
|
||||||
setError(null);
|
|
||||||
// Also reset result states when opening for a new worktree
|
|
||||||
// This prevents showing stale PR URLs from previous worktrees
|
|
||||||
setPrUrl(null);
|
|
||||||
setBrowserUrl(null);
|
|
||||||
setShowBrowserFallback(false);
|
|
||||||
// Reset operation tracking
|
|
||||||
operationCompletedRef.current = false;
|
|
||||||
} else {
|
|
||||||
// Reset everything when dialog closes
|
|
||||||
setTitle('');
|
|
||||||
setBody('');
|
|
||||||
setCommitMessage('');
|
|
||||||
setBaseBranch(defaultBaseBranch);
|
|
||||||
setIsDraft(false);
|
|
||||||
setError(null);
|
|
||||||
setPrUrl(null);
|
|
||||||
setBrowserUrl(null);
|
|
||||||
setShowBrowserFallback(false);
|
|
||||||
operationCompletedRef.current = false;
|
|
||||||
}
|
}
|
||||||
}, [open, worktree?.path, defaultBaseBranch]);
|
}, [open, worktree?.path, resetState, fetchBranches]);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (!worktree) return;
|
if (!worktree) return;
|
||||||
@@ -346,15 +369,16 @@ export function CreatePRDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="base-branch">Base Branch</Label>
|
<Label htmlFor="base-branch">Base Branch</Label>
|
||||||
<Input
|
<BranchAutocomplete
|
||||||
id="base-branch"
|
|
||||||
placeholder="main"
|
|
||||||
value={baseBranch}
|
value={baseBranch}
|
||||||
onChange={(e) => setBaseBranch(e.target.value)}
|
onChange={setBaseBranch}
|
||||||
className="font-mono text-sm"
|
branches={branches}
|
||||||
|
placeholder="Select base branch..."
|
||||||
|
disabled={isLoadingBranches}
|
||||||
|
data-testid="base-branch-autocomplete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
|
|||||||
@@ -1746,8 +1746,8 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
pull: (worktreePath: string) => this.post('/api/worktree/pull', { worktreePath }),
|
pull: (worktreePath: string) => this.post('/api/worktree/pull', { worktreePath }),
|
||||||
checkoutBranch: (worktreePath: string, branchName: string) =>
|
checkoutBranch: (worktreePath: string, branchName: string) =>
|
||||||
this.post('/api/worktree/checkout-branch', { worktreePath, branchName }),
|
this.post('/api/worktree/checkout-branch', { worktreePath, branchName }),
|
||||||
listBranches: (worktreePath: string) =>
|
listBranches: (worktreePath: string, includeRemote?: boolean) =>
|
||||||
this.post('/api/worktree/list-branches', { worktreePath }),
|
this.post('/api/worktree/list-branches', { worktreePath, includeRemote }),
|
||||||
switchBranch: (worktreePath: string, branchName: string) =>
|
switchBranch: (worktreePath: string, branchName: string) =>
|
||||||
this.post('/api/worktree/switch-branch', { worktreePath, branchName }),
|
this.post('/api/worktree/switch-branch', { worktreePath, branchName }),
|
||||||
openInEditor: (worktreePath: string, editorCommand?: string) =>
|
openInEditor: (worktreePath: string, editorCommand?: string) =>
|
||||||
|
|||||||
7
apps/ui/src/types/electron.d.ts
vendored
7
apps/ui/src/types/electron.d.ts
vendored
@@ -858,8 +858,11 @@ export interface WorktreeAPI {
|
|||||||
code?: 'NOT_GIT_REPO' | 'NO_COMMITS';
|
code?: 'NOT_GIT_REPO' | 'NO_COMMITS';
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
// List all local branches
|
// List branches (local and optionally remote)
|
||||||
listBranches: (worktreePath: string) => Promise<{
|
listBranches: (
|
||||||
|
worktreePath: string,
|
||||||
|
includeRemote?: boolean
|
||||||
|
) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
result?: {
|
result?: {
|
||||||
currentBranch: string;
|
currentBranch: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user