diff --git a/apps/app/src/components/views/board-view/components/worktree-selector.tsx b/apps/app/src/components/views/board-view/components/worktree-selector.tsx index ce9d34c0..61f17202 100644 --- a/apps/app/src/components/views/board-view/components/worktree-selector.tsx +++ b/apps/app/src/components/views/board-view/components/worktree-selector.tsx @@ -23,6 +23,7 @@ import { ExternalLink, ChevronDown, Download, + Upload, GitBranchPlus, Check, Search, @@ -65,9 +66,12 @@ export function WorktreeSelector({ }: WorktreeSelectorProps) { const [isLoading, setIsLoading] = useState(false); const [isPulling, setIsPulling] = useState(false); + const [isPushing, setIsPushing] = useState(false); const [isSwitching, setIsSwitching] = useState(false); const [worktrees, setWorktrees] = useState([]); const [branches, setBranches] = useState([]); + const [aheadCount, setAheadCount] = useState(0); + const [behindCount, setBehindCount] = useState(0); const [isLoadingBranches, setIsLoadingBranches] = useState(false); const [branchFilter, setBranchFilter] = useState(""); const currentWorktree = useAppStore((s) => s.getCurrentWorktree(projectPath)); @@ -106,6 +110,8 @@ export function WorktreeSelector({ const result = await api.worktree.listBranches(worktreePath); if (result.success && result.result) { setBranches(result.result.branches); + setAheadCount(result.result.aheadCount || 0); + setBehindCount(result.result.behindCount || 0); } } catch (error) { console.error("Failed to fetch branches:", error); @@ -172,6 +178,32 @@ export function WorktreeSelector({ } }; + const handlePush = async (worktree: WorktreeInfo) => { + if (isPushing) return; + setIsPushing(true); + try { + const api = getElectronAPI(); + if (!api?.worktree?.push) { + toast.error("Push API not available"); + return; + } + const result = await api.worktree.push(worktree.path); + if (result.success && result.result) { + toast.success(result.result.message); + // Refresh to update ahead/behind counts + fetchBranches(worktree.path); + fetchWorktrees(); + } else { + toast.error(result.error || "Failed to push changes"); + } + } catch (error) { + console.error("Push failed:", error); + toast.error("Failed to push changes"); + } finally { + setIsPushing(false); + } + }; + const selectedWorktree = worktrees.find((w) => currentWorktree ? w.path === currentWorktree : w.isMain @@ -273,6 +305,38 @@ export function WorktreeSelector({ })()} + + Sync + + {/* Pull option */} + handlePull(worktree)} + disabled={isPulling} + className="text-xs" + > + + {isPulling ? "Pulling..." : "Pull"} + {behindCount > 0 && ( + + {behindCount} behind + + )} + + {/* Push option */} + handlePush(worktree)} + disabled={isPushing || aheadCount === 0} + className="text-xs" + > + + {isPushing ? "Pushing..." : "Push"} + {aheadCount > 0 && ( + + {aheadCount} ahead + + )} + + {worktree.hasChanges && ( onCommit(worktree)} diff --git a/apps/app/src/lib/electron.ts b/apps/app/src/lib/electron.ts index d6ff53c0..d3c27faa 100644 --- a/apps/app/src/lib/electron.ts +++ b/apps/app/src/lib/electron.ts @@ -1187,6 +1187,8 @@ function createMockWorktreeAPI(): WorktreeAPI { { name: "develop", isCurrent: false, isRemote: false }, { name: "feature/example", isCurrent: false, isRemote: false }, ], + aheadCount: 2, + behindCount: 0, }, }; }, diff --git a/apps/app/src/types/electron.d.ts b/apps/app/src/types/electron.d.ts index 11edefab..a6d53af9 100644 --- a/apps/app/src/types/electron.d.ts +++ b/apps/app/src/types/electron.d.ts @@ -765,6 +765,8 @@ export interface WorktreeAPI { isCurrent: boolean; isRemote: boolean; }>; + aheadCount: number; + behindCount: number; }; error?: string; }>; diff --git a/apps/server/src/routes/worktree/routes/list-branches.ts b/apps/server/src/routes/worktree/routes/list-branches.ts index 25f1f82c..fc2822d2 100644 --- a/apps/server/src/routes/worktree/routes/list-branches.ts +++ b/apps/server/src/routes/worktree/routes/list-branches.ts @@ -53,11 +53,36 @@ export function createListBranchesHandler() { isRemote: false, })); + // Get ahead/behind count for current branch + let aheadCount = 0; + let behindCount = 0; + try { + // First check if there's a remote tracking branch + const { stdout: upstreamOutput } = await execAsync( + `git rev-parse --abbrev-ref ${currentBranch}@{upstream}`, + { cwd: worktreePath } + ); + + if (upstreamOutput.trim()) { + const { stdout: aheadBehindOutput } = await execAsync( + `git rev-list --left-right --count ${currentBranch}@{upstream}...HEAD`, + { cwd: worktreePath } + ); + const [behind, ahead] = aheadBehindOutput.trim().split(/\s+/).map(Number); + aheadCount = ahead || 0; + behindCount = behind || 0; + } + } catch { + // No upstream branch set, that's okay + } + res.json({ success: true, result: { currentBranch, branches, + aheadCount, + behindCount, }, }); } catch (error) {