"use client";
import { memo, useCallback } from "react";
import {
IconGitCommit,
IconGitPullRequest,
IconGitMerge,
IconGitCherryPick,
IconLoader2,
IconChevronDown,
IconCloudDownload,
IconCloudUpload,
IconAlertTriangle,
} from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
} from "@kandev/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useSessionGit } from "@/hooks/domains/session/use-session-git";
import { useGitWithFeedback } from "@/hooks/use-git-with-feedback";
import { useVcsDialogs } from "@/components/vcs/vcs-dialogs";
import type { ReactNode } from "react";
function determinePrimaryAction(
uncommittedFileCount: number,
aheadCount: number,
behindCount: number,
): "commit" | "pr" | "rebase" {
if (uncommittedFileCount > 0) return "commit";
if (aheadCount > 0) return "pr";
if (behindCount > 0) return "rebase";
return "commit";
}
type PrimaryButtonConfig = {
icon: ReactNode;
label: string;
badge: number | null;
tooltip: string;
onClick: () => void;
};
function buildCommitConfig(
uncommittedFileCount: number,
openCommitDialog: () => void,
): PrimaryButtonConfig {
return {
icon: ,
label: "Commit",
badge: uncommittedFileCount > 0 ? uncommittedFileCount : null,
tooltip:
uncommittedFileCount > 0
? `Commit ${uncommittedFileCount} changed file${uncommittedFileCount !== 1 ? "s" : ""}`
: "No changes to commit",
onClick: openCommitDialog,
};
}
function buildPrConfig(aheadCount: number, openPRDialog: () => void): PrimaryButtonConfig {
return {
icon: ,
label: "Create PR",
badge: aheadCount > 0 ? aheadCount : null,
tooltip: `Create PR (${aheadCount} commit${aheadCount !== 1 ? "s" : ""} ahead)`,
onClick: openPRDialog,
};
}
function buildRebaseConfig(
behindCount: number,
baseBranch: string | undefined,
handleRebase: () => void,
): PrimaryButtonConfig {
return {
icon: ,
label: "Rebase",
badge: behindCount > 0 ? behindCount : null,
tooltip: `Rebase onto ${baseBranch || "origin/main"} (${behindCount} behind)`,
onClick: handleRebase,
};
}
type PrimaryConfigArgs = {
primaryAction: "commit" | "pr" | "rebase";
uncommittedFileCount: number;
aheadCount: number;
behindCount: number;
baseBranch: string | undefined;
openCommitDialog: () => void;
openPRDialog: () => void;
handleRebase: () => void;
};
function buildPrimaryButtonConfig({
primaryAction,
uncommittedFileCount,
aheadCount,
behindCount,
baseBranch,
openCommitDialog,
openPRDialog,
handleRebase,
}: PrimaryConfigArgs): PrimaryButtonConfig {
if (primaryAction === "pr") return buildPrConfig(aheadCount, openPRDialog);
if (primaryAction === "rebase") return buildRebaseConfig(behindCount, baseBranch, handleRebase);
return buildCommitConfig(uncommittedFileCount, openCommitDialog);
}
type VcsDropdownItemsProps = {
disabled: boolean;
baseBranch?: string;
hasMatchingUpstream: boolean | "" | null | undefined;
behindCount: number;
aheadCount: number;
onPR: () => void;
onPull: () => void;
onPush: (force: boolean) => void;
onRebase: () => void;
onMerge: () => void;
};
function VcsDropdownItems({
disabled,
baseBranch,
hasMatchingUpstream,
behindCount,
aheadCount,
onPR,
onPull,
onPush,
onRebase,
onMerge,
}: VcsDropdownItemsProps) {
return (
Create PR
Pull
{hasMatchingUpstream && behindCount > 0 && (
↓{behindCount}
)}
Push
{hasMatchingUpstream && aheadCount > 0 && (
↑{aheadCount}
)}
onPush(false)}
disabled={disabled}
>
Push
onPush(true)}
disabled={disabled}
>
Force Push
Rebase
onto {baseBranch || "origin/main"}
Merge
from {baseBranch || "origin/main"}
);
}
type VcsSplitButtonProps = {
sessionId: string | null;
baseBranch?: string;
};
const VcsSplitButton = memo(function VcsSplitButton({
sessionId,
baseBranch,
}: VcsSplitButtonProps) {
const gitWithFeedback = useGitWithFeedback();
const git = useSessionGit(sessionId);
const { openCommitDialog, openPRDialog } = useVcsDialogs();
const currentBranch = git.branch;
const remoteBranch = git.remoteBranch;
const hasMatchingUpstream =
remoteBranch && currentBranch && remoteBranch === `origin/${currentBranch}`;
const uncommittedFileCount = git.allFiles.length;
const aheadCount = git.ahead;
const behindCount = git.behind;
const isDisabled = git.isLoading || !sessionId;
const isGitLoading = git.isLoading;
const handlePull = useCallback(() => {
gitWithFeedback(() => git.pull(), "Pull");
}, [gitWithFeedback, git]);
const handlePush = useCallback(
(force = false) => {
gitWithFeedback(() => git.push({ force }), force ? "Force Push" : "Push");
},
[gitWithFeedback, git],
);
const handleRebase = useCallback(() => {
const targetBranch = baseBranch?.replace(/^origin\//, "") || "main";
gitWithFeedback(() => git.rebase(targetBranch), "Rebase");
}, [gitWithFeedback, git, baseBranch]);
const handleMerge = useCallback(() => {
const targetBranch = baseBranch?.replace(/^origin\//, "") || "main";
gitWithFeedback(() => git.merge(targetBranch), "Merge");
}, [gitWithFeedback, git, baseBranch]);
const primaryAction = determinePrimaryAction(uncommittedFileCount, aheadCount, behindCount);
const primaryButtonConfig = buildPrimaryButtonConfig({
primaryAction,
uncommittedFileCount,
aheadCount,
behindCount,
baseBranch,
openCommitDialog,
openPRDialog,
handleRebase,
});
return (
{primaryButtonConfig.tooltip}
);
});
export { VcsSplitButton };