feat(git): add multi-remote push with remote selection and fork-aware PR creation (#365)
* fix: Usage drop down should be scrollable * fix: When there are multiple remotes, provide the user with an option of which branch to push to * feat(gitview): add remote-push selection and auto checkout on create * feat: enable selecting remote when creating PR * fix: show PR icon in create button when not creating * fix(pullrequest): drop remote picker and use explicit remote * feat: add remote selection for PR status and creation * fix: improve PR creation with selected remote and fork head * chore(ui): simplify PR remote link UI * fix(web): handle cross-repo PRs and branch validation * feat: add remote selection for commit and push * feat(git): delegate PR head source resolution to server * chore(web): remove noisy PR creation logs
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
RiAddLine,
|
||||
RiCloseLine,
|
||||
RiLoader4Line,
|
||||
RiArrowLeftLine,
|
||||
} from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import type { GitRemote } from '@/lib/api/types';
|
||||
|
||||
interface BranchInfo {
|
||||
ahead?: number;
|
||||
@@ -34,7 +36,8 @@ interface BranchSelectorProps {
|
||||
remoteBranches: string[];
|
||||
branchInfo: Record<string, BranchInfo> | undefined;
|
||||
onCheckout: (branch: string) => void;
|
||||
onCreate: (name: string) => Promise<void>;
|
||||
onCreate: (name: string, remote?: GitRemote) => Promise<void>;
|
||||
remotes?: GitRemote[];
|
||||
disabled?: boolean;
|
||||
tooltipDelayMs?: number;
|
||||
}
|
||||
@@ -59,16 +62,20 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
branchInfo,
|
||||
onCheckout,
|
||||
onCreate,
|
||||
remotes = [],
|
||||
disabled = false,
|
||||
tooltipDelayMs = 1000,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [search, setSearch] = React.useState('');
|
||||
const [showCreate, setShowCreate] = React.useState(false);
|
||||
const [showRemoteSelect, setShowRemoteSelect] = React.useState(false);
|
||||
const [newBranchName, setNewBranchName] = React.useState('');
|
||||
const [isCreating, setIsCreating] = React.useState(false);
|
||||
const createInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const hasMultipleRemotes = remotes.length > 1;
|
||||
|
||||
const sanitizedNewBranch = React.useMemo(
|
||||
() => sanitizeBranchNameInput(newBranchName),
|
||||
[newBranchName]
|
||||
@@ -103,9 +110,17 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!sanitizedNewBranch || isCreating) return;
|
||||
|
||||
// If multiple remotes, show remote selection first
|
||||
if (hasMultipleRemotes) {
|
||||
setShowRemoteSelect(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single or no remote - proceed directly
|
||||
setIsCreating(true);
|
||||
try {
|
||||
await onCreate(sanitizedNewBranch);
|
||||
await onCreate(sanitizedNewBranch, remotes[0]);
|
||||
setNewBranchName('');
|
||||
setShowCreate(false);
|
||||
setIsOpen(false);
|
||||
@@ -114,15 +129,35 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRemote = async (remote: GitRemote) => {
|
||||
if (!sanitizedNewBranch || isCreating) return;
|
||||
setIsCreating(true);
|
||||
try {
|
||||
await onCreate(sanitizedNewBranch, remote);
|
||||
setNewBranchName('');
|
||||
setShowCreate(false);
|
||||
setShowRemoteSelect(false);
|
||||
setIsOpen(false);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackFromRemoteSelect = () => {
|
||||
setShowRemoteSelect(false);
|
||||
};
|
||||
|
||||
const handleCancelCreate = () => {
|
||||
setNewBranchName('');
|
||||
setShowCreate(false);
|
||||
setShowRemoteSelect(false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSearch('');
|
||||
setShowCreate(false);
|
||||
setShowRemoteSelect(false);
|
||||
setNewBranchName('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
@@ -165,7 +200,45 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
|
||||
<CommandEmpty>No branches found.</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{!showCreate ? (
|
||||
{showRemoteSelect ? (
|
||||
// Remote selection step
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBackFromRemoteSelect}
|
||||
disabled={isCreating}
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50"
|
||||
>
|
||||
<RiArrowLeftLine className="size-4" />
|
||||
</button>
|
||||
<span className="typography-meta text-muted-foreground">
|
||||
Push <span className="text-foreground font-medium">{sanitizedNewBranch}</span> to:
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{remotes.map((remote) => (
|
||||
<button
|
||||
key={remote.name}
|
||||
type="button"
|
||||
onClick={() => handleSelectRemote(remote)}
|
||||
disabled={isCreating}
|
||||
className="flex flex-col items-start gap-0.5 px-2 py-1.5 rounded-md text-left hover:bg-accent disabled:opacity-50"
|
||||
>
|
||||
<span className="typography-ui-label text-foreground">
|
||||
{isCreating ? (
|
||||
<RiLoader4Line className="inline size-3 mr-1.5 animate-spin" />
|
||||
) : null}
|
||||
{remote.name}
|
||||
</span>
|
||||
<span className="typography-micro text-muted-foreground truncate max-w-full">
|
||||
{remote.pushUrl}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : !showCreate ? (
|
||||
<CommandItem onSelect={handleShowCreate}>
|
||||
<RiAddLine className="size-4" />
|
||||
<span>Create new branch...</span>
|
||||
|
||||
Reference in New Issue
Block a user