This commit is contained in:
Cody Seibert
2025-12-16 02:24:49 -05:00
parent bd1c4e0690
commit 8c5759d74e
4 changed files with 93 additions and 0 deletions

View File

@@ -23,6 +23,7 @@ import {
ExternalLink, ExternalLink,
ChevronDown, ChevronDown,
Download, Download,
Upload,
GitBranchPlus, GitBranchPlus,
Check, Check,
Search, Search,
@@ -65,9 +66,12 @@ export function WorktreeSelector({
}: WorktreeSelectorProps) { }: WorktreeSelectorProps) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isPulling, setIsPulling] = useState(false); const [isPulling, setIsPulling] = useState(false);
const [isPushing, setIsPushing] = useState(false);
const [isSwitching, setIsSwitching] = useState(false); const [isSwitching, setIsSwitching] = useState(false);
const [worktrees, setWorktrees] = useState<WorktreeInfo[]>([]); const [worktrees, setWorktrees] = useState<WorktreeInfo[]>([]);
const [branches, setBranches] = useState<BranchInfo[]>([]); const [branches, setBranches] = useState<BranchInfo[]>([]);
const [aheadCount, setAheadCount] = useState(0);
const [behindCount, setBehindCount] = useState(0);
const [isLoadingBranches, setIsLoadingBranches] = useState(false); const [isLoadingBranches, setIsLoadingBranches] = useState(false);
const [branchFilter, setBranchFilter] = useState(""); const [branchFilter, setBranchFilter] = useState("");
const currentWorktree = useAppStore((s) => s.getCurrentWorktree(projectPath)); const currentWorktree = useAppStore((s) => s.getCurrentWorktree(projectPath));
@@ -106,6 +110,8 @@ export function WorktreeSelector({
const result = await api.worktree.listBranches(worktreePath); const result = await api.worktree.listBranches(worktreePath);
if (result.success && result.result) { if (result.success && result.result) {
setBranches(result.result.branches); setBranches(result.result.branches);
setAheadCount(result.result.aheadCount || 0);
setBehindCount(result.result.behindCount || 0);
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch branches:", 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 = const selectedWorktree =
worktrees.find((w) => worktrees.find((w) =>
currentWorktree ? w.path === currentWorktree : w.isMain currentWorktree ? w.path === currentWorktree : w.isMain
@@ -273,6 +305,38 @@ export function WorktreeSelector({
})()} })()}
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground font-normal">
Sync
</DropdownMenuLabel>
{/* Pull option */}
<DropdownMenuItem
onClick={() => handlePull(worktree)}
disabled={isPulling}
className="text-xs"
>
<Download className={cn("w-3.5 h-3.5 mr-2", isPulling && "animate-pulse")} />
{isPulling ? "Pulling..." : "Pull"}
{behindCount > 0 && (
<span className="ml-auto text-[10px] bg-muted px-1.5 py-0.5 rounded">
{behindCount} behind
</span>
)}
</DropdownMenuItem>
{/* Push option */}
<DropdownMenuItem
onClick={() => handlePush(worktree)}
disabled={isPushing || aheadCount === 0}
className="text-xs"
>
<Upload className={cn("w-3.5 h-3.5 mr-2", isPushing && "animate-pulse")} />
{isPushing ? "Pushing..." : "Push"}
{aheadCount > 0 && (
<span className="ml-auto text-[10px] bg-primary/20 text-primary px-1.5 py-0.5 rounded">
{aheadCount} ahead
</span>
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
{worktree.hasChanges && ( {worktree.hasChanges && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => onCommit(worktree)} onClick={() => onCommit(worktree)}

View File

@@ -1187,6 +1187,8 @@ function createMockWorktreeAPI(): WorktreeAPI {
{ name: "develop", isCurrent: false, isRemote: false }, { name: "develop", isCurrent: false, isRemote: false },
{ name: "feature/example", isCurrent: false, isRemote: false }, { name: "feature/example", isCurrent: false, isRemote: false },
], ],
aheadCount: 2,
behindCount: 0,
}, },
}; };
}, },

View File

@@ -765,6 +765,8 @@ export interface WorktreeAPI {
isCurrent: boolean; isCurrent: boolean;
isRemote: boolean; isRemote: boolean;
}>; }>;
aheadCount: number;
behindCount: number;
}; };
error?: string; error?: string;
}>; }>;

View File

@@ -53,11 +53,36 @@ export function createListBranchesHandler() {
isRemote: false, 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({ res.json({
success: true, success: true,
result: { result: {
currentBranch, currentBranch,
branches, branches,
aheadCount,
behindCount,
}, },
}); });
} catch (error) { } catch (error) {