mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
Feature: Git sync, set-tracking, and push divergence handling (#796)
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
import {
|
||||
Trash2,
|
||||
MoreHorizontal,
|
||||
GitBranch,
|
||||
GitCommit,
|
||||
GitPullRequest,
|
||||
Download,
|
||||
@@ -138,6 +139,85 @@ interface WorktreeActionsDropdownProps {
|
||||
onRunTerminalScript?: (worktree: WorktreeInfo, command: string) => void;
|
||||
/** Callback to open the script editor UI */
|
||||
onEditScripts?: () => void;
|
||||
/** Whether sync is in progress */
|
||||
isSyncing?: boolean;
|
||||
/** Sync (pull + push) callback */
|
||||
onSync?: (worktree: WorktreeInfo) => void;
|
||||
/** Sync with a specific remote */
|
||||
onSyncWithRemote?: (worktree: WorktreeInfo, remote: string) => void;
|
||||
/** Set tracking branch to a specific remote */
|
||||
onSetTracking?: (worktree: WorktreeInfo, remote: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A remote item that either renders as a split-button with "Set as Tracking Branch"
|
||||
* sub-action, or a plain menu item if onSetTracking is not provided.
|
||||
*/
|
||||
function RemoteActionMenuItem({
|
||||
remote,
|
||||
icon: Icon,
|
||||
trackingRemote,
|
||||
isDisabled,
|
||||
isGitOpsAvailable,
|
||||
onAction,
|
||||
onSetTracking,
|
||||
}: {
|
||||
remote: { name: string; url: string };
|
||||
icon: typeof Download;
|
||||
trackingRemote?: string;
|
||||
isDisabled: boolean;
|
||||
isGitOpsAvailable: boolean;
|
||||
onAction: () => void;
|
||||
onSetTracking?: () => void;
|
||||
}) {
|
||||
if (onSetTracking) {
|
||||
return (
|
||||
<DropdownMenuSub key={remote.name}>
|
||||
<div className="flex items-center">
|
||||
<DropdownMenuItem
|
||||
onClick={onAction}
|
||||
disabled={isDisabled || !isGitOpsAvailable}
|
||||
className="text-xs flex-1 pr-0 rounded-r-none"
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5 mr-2" />
|
||||
{remote.name}
|
||||
{trackingRemote === remote.name && (
|
||||
<span className="ml-auto text-[10px] text-muted-foreground mr-1">tracking</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSubTrigger
|
||||
className="text-xs px-1 rounded-l-none border-l border-border/30 h-8"
|
||||
disabled={!isGitOpsAvailable}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem
|
||||
onClick={onSetTracking}
|
||||
disabled={!isGitOpsAvailable}
|
||||
className="text-xs"
|
||||
>
|
||||
<GitBranch className="w-3.5 h-3.5 mr-2" />
|
||||
Set as Tracking Branch
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={remote.name}
|
||||
onClick={onAction}
|
||||
disabled={isDisabled || !isGitOpsAvailable}
|
||||
className="text-xs"
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5 mr-2" />
|
||||
{remote.name}
|
||||
<span className="ml-auto text-[10px] text-muted-foreground max-w-[100px] truncate">
|
||||
{remote.url}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorktreeActionsDropdown({
|
||||
@@ -198,6 +278,10 @@ export function WorktreeActionsDropdown({
|
||||
terminalScripts,
|
||||
onRunTerminalScript,
|
||||
onEditScripts,
|
||||
isSyncing = false,
|
||||
onSync,
|
||||
onSyncWithRemote,
|
||||
onSetTracking,
|
||||
}: WorktreeActionsDropdownProps) {
|
||||
// Get available editors for the "Open In" submenu
|
||||
const { editors } = useAvailableEditors();
|
||||
@@ -719,18 +803,20 @@ export function WorktreeActionsDropdown({
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{remotes.map((remote) => (
|
||||
<DropdownMenuItem
|
||||
<RemoteActionMenuItem
|
||||
key={remote.name}
|
||||
onClick={() => isGitOpsAvailable && onPullWithRemote(worktree, remote.name)}
|
||||
disabled={isPulling || !isGitOpsAvailable}
|
||||
className="text-xs"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5 mr-2" />
|
||||
{remote.name}
|
||||
<span className="ml-auto text-[10px] text-muted-foreground max-w-[100px] truncate">
|
||||
{remote.url}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
remote={remote}
|
||||
icon={Download}
|
||||
trackingRemote={trackingRemote}
|
||||
isDisabled={isPulling}
|
||||
isGitOpsAvailable={isGitOpsAvailable}
|
||||
onAction={() => isGitOpsAvailable && onPullWithRemote(worktree, remote.name)}
|
||||
onSetTracking={
|
||||
onSetTracking
|
||||
? () => isGitOpsAvailable && onSetTracking(worktree, remote.name)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
@@ -818,18 +904,20 @@ export function WorktreeActionsDropdown({
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{remotes.map((remote) => (
|
||||
<DropdownMenuItem
|
||||
<RemoteActionMenuItem
|
||||
key={remote.name}
|
||||
onClick={() => isGitOpsAvailable && onPushWithRemote(worktree, remote.name)}
|
||||
disabled={isPushing || !isGitOpsAvailable}
|
||||
className="text-xs"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5 mr-2" />
|
||||
{remote.name}
|
||||
<span className="ml-auto text-[10px] text-muted-foreground max-w-[100px] truncate">
|
||||
{remote.url}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
remote={remote}
|
||||
icon={Upload}
|
||||
trackingRemote={trackingRemote}
|
||||
isDisabled={isPushing}
|
||||
isGitOpsAvailable={isGitOpsAvailable}
|
||||
onAction={() => isGitOpsAvailable && onPushWithRemote(worktree, remote.name)}
|
||||
onSetTracking={
|
||||
onSetTracking
|
||||
? () => isGitOpsAvailable && onSetTracking(worktree, remote.name)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
@@ -876,6 +964,72 @@ export function WorktreeActionsDropdown({
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</TooltipWrapper>
|
||||
{onSync && (
|
||||
<TooltipWrapper
|
||||
showTooltip={!!gitOpsDisabledReason}
|
||||
tooltipContent={gitOpsDisabledReason}
|
||||
>
|
||||
{remotes && remotes.length > 1 && onSyncWithRemote ? (
|
||||
<DropdownMenuSub>
|
||||
<div className="flex items-center">
|
||||
<DropdownMenuItem
|
||||
onClick={() => isGitOpsAvailable && onSync(worktree)}
|
||||
disabled={isSyncing || !isGitOpsAvailable}
|
||||
className={cn(
|
||||
'text-xs flex-1 pr-0 rounded-r-none',
|
||||
!isGitOpsAvailable && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<RefreshCw className={cn('w-3.5 h-3.5 mr-2', isSyncing && 'animate-spin')} />
|
||||
{isSyncing ? 'Syncing...' : 'Sync'}
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSubTrigger
|
||||
className={cn(
|
||||
'text-xs px-1 rounded-l-none border-l border-border/30 h-8',
|
||||
(!isGitOpsAvailable || isSyncing) && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
disabled={!isGitOpsAvailable || isSyncing}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground font-normal">
|
||||
Sync with remote
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{remotes.map((remote) => (
|
||||
<DropdownMenuItem
|
||||
key={`sync-${remote.name}`}
|
||||
onClick={() => isGitOpsAvailable && onSyncWithRemote(worktree, remote.name)}
|
||||
disabled={isSyncing || !isGitOpsAvailable}
|
||||
className="text-xs"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5 mr-2" />
|
||||
{remote.name}
|
||||
<span className="ml-auto text-[10px] text-muted-foreground max-w-[100px] truncate">
|
||||
{remote.url}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
onClick={() => isGitOpsAvailable && onSync(worktree)}
|
||||
disabled={isSyncing || !isGitOpsAvailable}
|
||||
className={cn('text-xs', !isGitOpsAvailable && 'opacity-50 cursor-not-allowed')}
|
||||
>
|
||||
<RefreshCw className={cn('w-3.5 h-3.5 mr-2', isSyncing && 'animate-spin')} />
|
||||
{isSyncing ? 'Syncing...' : 'Sync'}
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
<TooltipWrapper showTooltip={!!gitOpsDisabledReason} tooltipContent={gitOpsDisabledReason}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => isGitOpsAvailable && onResolveConflicts(worktree)}
|
||||
|
||||
@@ -138,6 +138,14 @@ export interface WorktreeDropdownProps {
|
||||
onRunTerminalScript?: (worktree: WorktreeInfo, command: string) => void;
|
||||
/** Callback to open the script editor UI */
|
||||
onEditScripts?: () => void;
|
||||
/** Whether sync is in progress */
|
||||
isSyncing?: boolean;
|
||||
/** Sync (pull + push) callback */
|
||||
onSync?: (worktree: WorktreeInfo) => void;
|
||||
/** Sync with a specific remote */
|
||||
onSyncWithRemote?: (worktree: WorktreeInfo, remote: string) => void;
|
||||
/** Set tracking branch to a specific remote */
|
||||
onSetTracking?: (worktree: WorktreeInfo, remote: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,6 +238,10 @@ export function WorktreeDropdown({
|
||||
terminalScripts,
|
||||
onRunTerminalScript,
|
||||
onEditScripts,
|
||||
isSyncing = false,
|
||||
onSync,
|
||||
onSyncWithRemote,
|
||||
onSetTracking,
|
||||
}: WorktreeDropdownProps) {
|
||||
// Find the currently selected worktree to display in the trigger
|
||||
const selectedWorktree = worktrees.find((w) => isWorktreeSelected(w));
|
||||
@@ -549,6 +561,10 @@ export function WorktreeDropdown({
|
||||
terminalScripts={terminalScripts}
|
||||
onRunTerminalScript={onRunTerminalScript}
|
||||
onEditScripts={onEditScripts}
|
||||
isSyncing={isSyncing}
|
||||
onSync={onSync}
|
||||
onSyncWithRemote={onSyncWithRemote}
|
||||
onSetTracking={onSetTracking}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -108,6 +108,14 @@ interface WorktreeTabProps {
|
||||
onRunTerminalScript?: (worktree: WorktreeInfo, command: string) => void;
|
||||
/** Callback to open the script editor UI */
|
||||
onEditScripts?: () => void;
|
||||
/** Whether sync is in progress */
|
||||
isSyncing?: boolean;
|
||||
/** Sync (pull + push) callback */
|
||||
onSync?: (worktree: WorktreeInfo) => void;
|
||||
/** Sync with a specific remote */
|
||||
onSyncWithRemote?: (worktree: WorktreeInfo, remote: string) => void;
|
||||
/** Set tracking branch to a specific remote */
|
||||
onSetTracking?: (worktree: WorktreeInfo, remote: string) => void;
|
||||
}
|
||||
|
||||
export function WorktreeTab({
|
||||
@@ -181,6 +189,10 @@ export function WorktreeTab({
|
||||
terminalScripts,
|
||||
onRunTerminalScript,
|
||||
onEditScripts,
|
||||
isSyncing = false,
|
||||
onSync,
|
||||
onSyncWithRemote,
|
||||
onSetTracking,
|
||||
}: WorktreeTabProps) {
|
||||
// Make the worktree tab a drop target for feature cards
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
@@ -550,6 +562,10 @@ export function WorktreeTab({
|
||||
terminalScripts={terminalScripts}
|
||||
onRunTerminalScript={onRunTerminalScript}
|
||||
onEditScripts={onEditScripts}
|
||||
isSyncing={isSyncing}
|
||||
onSync={onSync}
|
||||
onSyncWithRemote={onSyncWithRemote}
|
||||
onSetTracking={onSetTracking}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
useSwitchBranch,
|
||||
usePullWorktree,
|
||||
usePushWorktree,
|
||||
useSyncWorktree,
|
||||
useSetTracking,
|
||||
useOpenInEditor,
|
||||
} from '@/hooks/mutations';
|
||||
import type { WorktreeInfo } from '../types';
|
||||
@@ -51,6 +53,8 @@ export function useWorktreeActions(options?: UseWorktreeActionsOptions) {
|
||||
});
|
||||
const pullMutation = usePullWorktree();
|
||||
const pushMutation = usePushWorktree();
|
||||
const syncMutation = useSyncWorktree();
|
||||
const setTrackingMutation = useSetTracking();
|
||||
const openInEditorMutation = useOpenInEditor();
|
||||
|
||||
/**
|
||||
@@ -150,6 +154,28 @@ export function useWorktreeActions(options?: UseWorktreeActionsOptions) {
|
||||
[pushMutation]
|
||||
);
|
||||
|
||||
const handleSync = useCallback(
|
||||
async (worktree: WorktreeInfo, remote?: string) => {
|
||||
if (syncMutation.isPending) return;
|
||||
syncMutation.mutate({
|
||||
worktreePath: worktree.path,
|
||||
remote,
|
||||
});
|
||||
},
|
||||
[syncMutation]
|
||||
);
|
||||
|
||||
const handleSetTracking = useCallback(
|
||||
async (worktree: WorktreeInfo, remote: string) => {
|
||||
if (setTrackingMutation.isPending) return;
|
||||
setTrackingMutation.mutate({
|
||||
worktreePath: worktree.path,
|
||||
remote,
|
||||
});
|
||||
},
|
||||
[setTrackingMutation]
|
||||
);
|
||||
|
||||
const handleOpenInIntegratedTerminal = useCallback(
|
||||
(worktree: WorktreeInfo, mode?: 'tab' | 'split') => {
|
||||
// Navigate to the terminal view with the worktree path and branch name
|
||||
@@ -215,12 +241,15 @@ export function useWorktreeActions(options?: UseWorktreeActionsOptions) {
|
||||
return {
|
||||
isPulling: pullMutation.isPending,
|
||||
isPushing: pushMutation.isPending,
|
||||
isSyncing: syncMutation.isPending,
|
||||
isSwitching: switchBranchMutation.isPending,
|
||||
isActivating,
|
||||
setIsActivating,
|
||||
handleSwitchBranch,
|
||||
handlePull,
|
||||
handlePush,
|
||||
handleSync,
|
||||
handleSetTracking,
|
||||
handleOpenInIntegratedTerminal,
|
||||
handleRunTerminalScript,
|
||||
handleOpenInEditor,
|
||||
|
||||
@@ -113,11 +113,14 @@ export function WorktreePanel({
|
||||
const {
|
||||
isPulling,
|
||||
isPushing,
|
||||
isSyncing,
|
||||
isSwitching,
|
||||
isActivating,
|
||||
handleSwitchBranch,
|
||||
handlePull: _handlePull,
|
||||
handlePush,
|
||||
handleSync,
|
||||
handleSetTracking,
|
||||
handleOpenInIntegratedTerminal,
|
||||
handleRunTerminalScript,
|
||||
handleOpenInEditor,
|
||||
@@ -828,6 +831,30 @@ export function WorktreePanel({
|
||||
[handlePush, fetchBranches, fetchWorktrees]
|
||||
);
|
||||
|
||||
// Handle sync (pull + push) with optional remote selection
|
||||
const handleSyncWithRemoteSelection = useCallback(
|
||||
(worktree: WorktreeInfo) => {
|
||||
handleSync(worktree);
|
||||
},
|
||||
[handleSync]
|
||||
);
|
||||
|
||||
// Handle sync with a specific remote selected from the submenu
|
||||
const handleSyncWithSpecificRemote = useCallback(
|
||||
(worktree: WorktreeInfo, remote: string) => {
|
||||
handleSync(worktree, remote);
|
||||
},
|
||||
[handleSync]
|
||||
);
|
||||
|
||||
// Handle set tracking branch for a specific remote
|
||||
const handleSetTrackingForRemote = useCallback(
|
||||
(worktree: WorktreeInfo, remote: string) => {
|
||||
handleSetTracking(worktree, remote);
|
||||
},
|
||||
[handleSetTracking]
|
||||
);
|
||||
|
||||
// Handle confirming the push to remote dialog
|
||||
const handleConfirmPushToRemote = useCallback(
|
||||
async (worktree: WorktreeInfo, remote: string) => {
|
||||
@@ -936,6 +963,10 @@ export function WorktreePanel({
|
||||
onPushNewBranch={handlePushNewBranch}
|
||||
onPullWithRemote={handlePullWithSpecificRemote}
|
||||
onPushWithRemote={handlePushWithSpecificRemote}
|
||||
isSyncing={isSyncing}
|
||||
onSync={handleSyncWithRemoteSelection}
|
||||
onSyncWithRemote={handleSyncWithSpecificRemote}
|
||||
onSetTracking={handleSetTrackingForRemote}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
onOpenInExternalTerminal={handleOpenInExternalTerminal}
|
||||
@@ -1179,6 +1210,10 @@ export function WorktreePanel({
|
||||
onPushNewBranch={handlePushNewBranch}
|
||||
onPullWithRemote={handlePullWithSpecificRemote}
|
||||
onPushWithRemote={handlePushWithSpecificRemote}
|
||||
isSyncing={isSyncing}
|
||||
onSync={handleSyncWithRemoteSelection}
|
||||
onSyncWithRemote={handleSyncWithSpecificRemote}
|
||||
onSetTracking={handleSetTrackingForRemote}
|
||||
remotesCache={remotesCache}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
@@ -1286,6 +1321,10 @@ export function WorktreePanel({
|
||||
onPushNewBranch={handlePushNewBranch}
|
||||
onPullWithRemote={handlePullWithSpecificRemote}
|
||||
onPushWithRemote={handlePushWithSpecificRemote}
|
||||
isSyncing={isSyncing}
|
||||
onSync={handleSyncWithRemoteSelection}
|
||||
onSyncWithRemote={handleSyncWithSpecificRemote}
|
||||
onSetTracking={handleSetTrackingForRemote}
|
||||
remotes={remotesCache[mainWorktree.path]}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
@@ -1373,6 +1412,10 @@ export function WorktreePanel({
|
||||
onPushNewBranch={handlePushNewBranch}
|
||||
onPullWithRemote={handlePullWithSpecificRemote}
|
||||
onPushWithRemote={handlePushWithSpecificRemote}
|
||||
isSyncing={isSyncing}
|
||||
onSync={handleSyncWithRemoteSelection}
|
||||
onSyncWithRemote={handleSyncWithSpecificRemote}
|
||||
onSetTracking={handleSetTrackingForRemote}
|
||||
remotes={remotesCache[worktree.path]}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
|
||||
Reference in New Issue
Block a user