mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-25 00:33:08 +00:00
refactor: Improve all git operations, add stash support, add improved pull request flow, add worktree file copy options, address code review comments, add cherry pick options
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { getErrorMessage } from '@/lib/utils';
|
||||
import { Download, Upload, RefreshCw, AlertTriangle } from 'lucide-react';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import type { WorktreeInfo } from '../worktree-panel/types';
|
||||
|
||||
interface RemoteInfo {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const logger = createLogger('SelectRemoteDialog');
|
||||
|
||||
export type SelectRemoteOperation = 'pull' | 'push';
|
||||
|
||||
interface SelectRemoteDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
worktree: WorktreeInfo | null;
|
||||
operation: SelectRemoteOperation;
|
||||
onConfirm: (worktree: WorktreeInfo, remote: string) => void;
|
||||
}
|
||||
|
||||
export function SelectRemoteDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
worktree,
|
||||
operation,
|
||||
onConfirm,
|
||||
}: SelectRemoteDialogProps) {
|
||||
const [remotes, setRemotes] = useState<RemoteInfo[]>([]);
|
||||
const [selectedRemote, setSelectedRemote] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchRemotes = useCallback(async () => {
|
||||
if (!worktree) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const result = await api.worktree.listRemotes(worktree.path);
|
||||
|
||||
if (result.success && result.result) {
|
||||
const remoteInfos = result.result.remotes.map((r: { name: string; url: string }) => ({
|
||||
name: r.name,
|
||||
url: r.url,
|
||||
}));
|
||||
setRemotes(remoteInfos);
|
||||
} else {
|
||||
setError(result.error || 'Failed to fetch remotes');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to fetch remotes:', err);
|
||||
setError(getErrorMessage(err));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [worktree]);
|
||||
|
||||
// Fetch remotes when dialog opens
|
||||
useEffect(() => {
|
||||
if (open && worktree) {
|
||||
fetchRemotes();
|
||||
}
|
||||
}, [open, worktree, fetchRemotes]);
|
||||
|
||||
// Reset state when dialog closes
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSelectedRemote('');
|
||||
setError(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Auto-select default remote when remotes are loaded
|
||||
useEffect(() => {
|
||||
if (remotes.length > 0 && !selectedRemote) {
|
||||
// Default to 'origin' if available, otherwise first remote
|
||||
const defaultRemote = remotes.find((r) => r.name === 'origin') || remotes[0];
|
||||
setSelectedRemote(defaultRemote.name);
|
||||
}
|
||||
}, [remotes, selectedRemote]);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if (!worktree) return;
|
||||
|
||||
setIsRefreshing(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const result = await api.worktree.listRemotes(worktree.path);
|
||||
|
||||
if (result.success && result.result) {
|
||||
const remoteInfos = result.result.remotes.map((r: { name: string; url: string }) => ({
|
||||
name: r.name,
|
||||
url: r.url,
|
||||
}));
|
||||
setRemotes(remoteInfos);
|
||||
} else {
|
||||
setError(result.error || 'Failed to refresh remotes');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh remotes:', err);
|
||||
setError(getErrorMessage(err));
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!worktree || !selectedRemote) return;
|
||||
onConfirm(worktree, selectedRemote);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const isPull = operation === 'pull';
|
||||
const Icon = isPull ? Download : Upload;
|
||||
const title = isPull ? 'Pull from Remote' : 'Push to Remote';
|
||||
const actionLabel = isPull
|
||||
? `Pull from ${selectedRemote || 'Remote'}`
|
||||
: `Push to ${selectedRemote || 'Remote'}`;
|
||||
const description = isPull ? (
|
||||
<>
|
||||
Select a remote to pull changes into{' '}
|
||||
<span className="font-mono text-foreground">{worktree?.branch || 'current branch'}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Select a remote to push{' '}
|
||||
<span className="font-mono text-foreground">{worktree?.branch || 'current branch'}</span> to
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[450px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Icon className="w-5 h-5 text-primary" />
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center gap-4 py-6">
|
||||
<div className="flex items-center gap-2 text-destructive">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
<span className="text-sm">{error}</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={fetchRemotes}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="remote-select">Select Remote</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<Spinner size="xs" className="mr-1" />
|
||||
) : (
|
||||
<RefreshCw className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
<Select value={selectedRemote} onValueChange={setSelectedRemote}>
|
||||
<SelectTrigger id="remote-select">
|
||||
<SelectValue placeholder="Select a remote" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{remotes.map((remote) => (
|
||||
<SelectItem
|
||||
key={remote.name}
|
||||
value={remote.name}
|
||||
description={
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[300px]">
|
||||
{remote.url}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span className="font-medium">{remote.name}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{selectedRemote && (
|
||||
<div className="mt-2 p-3 rounded-md bg-muted/50 border border-border">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isPull ? (
|
||||
<>
|
||||
This will pull changes from{' '}
|
||||
<span className="font-mono text-foreground">
|
||||
{selectedRemote}/{worktree?.branch}
|
||||
</span>{' '}
|
||||
into your local branch.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
This will push your local changes to{' '}
|
||||
<span className="font-mono text-foreground">
|
||||
{selectedRemote}/{worktree?.branch}
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} disabled={!selectedRemote || isLoading}>
|
||||
<Icon className="w-4 h-4 mr-2" />
|
||||
{actionLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user