831 lines · 28.6 KB · TypeScript
RawDownload
import {  type GitRunStackedActionResult,  type GitStackedAction,  type GitStatusResult,  type NativeApi,} from "@t3tools/contracts";import { queryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";import { useCallback, useEffect, useMemo, useRef, useState } from "react";import {  CheckIcon,  CircleIcon,  CloudUploadIcon,  GitCommitIcon,  Loader2Icon,  MinusIcon,  XIcon,} from "lucide-react";import { GitHubIcon } from "./Icons"; interface GitActionsControlProps {  api: NativeApi | undefined;  gitCwd: string | null;} type GitActionIconName = "commit" | "push" | "pr"; interface GitActionMenuItem {  id: "commit" | "push" | "pr";  label: string;  disabled: boolean;  icon: GitActionIconName;  kind: "open_modal" | "run_action" | "open_pr";  action?: GitStackedAction;} interface GitModalActionOption {  action: GitStackedAction;  label: string;  disabled: boolean;  icon: GitActionIconName;  detail?: string;} type GitProgressStepStatus = "pending" | "running" | "completed" | "skipped" | "failed"; interface GitProgressStep {  id: "generate" | "commit" | "push" | "pr";  label: string;  status: GitProgressStepStatus;  detail?: string;} function GitActionIcon(props: { icon: GitActionIconName; disabled: boolean }) {  const toneClass = props.disabled ? "text-muted-foreground/45" : "text-foreground/85";   if (props.icon === "commit") {    return <GitCommitIcon className={`h-5 w-5 shrink-0 ${toneClass}`} />;  }   if (props.icon === "push") {    return <CloudUploadIcon className={`h-5 w-5 shrink-0 ${toneClass}`} />;  }   return <GitHubIcon className={`h-5 w-5 shrink-0 ${toneClass}`} />;} function gitActionModalTitle(): string {  return "Commit your changes";} function initialGitProgressSteps(  action: GitStackedAction,  commitMessage: string,): GitProgressStep[] {  const hasCustomMessage = commitMessage.trim().length > 0;  const steps: GitProgressStep[] = [];   if (!hasCustomMessage) {    steps.push({      id: "generate",      label: "Generate commit message",      status: "pending",    });  }   steps.push({    id: "commit",    label: "Commit changes",    status: "pending",  });   if (action !== "commit") {    steps.push({      id: "push",      label: "Push branch",      status: "pending",    });  }   if (action === "commit_push_pr") {    steps.push({      id: "pr",      label: "Create or open PR",      status: "pending",    });  }   return steps;} function updateProgressStep(  steps: GitProgressStep[],  id: GitProgressStep["id"],  status: GitProgressStepStatus,  detail?: string,): GitProgressStep[] {  return steps.map((step) => {    if (step.id !== id) return step;    return {      ...step,      status,      ...(detail ? { detail } : {}),    };  });} function runActionLabel(action: GitStackedAction): string {  if (action === "commit") return "Commit changes";  if (action === "commit_push") return "Commit and push";  return "Commit and create PR";} function buildGitActionMenuItems(  gitStatus: GitStatusResult | null,  isDisabled: boolean,): GitActionMenuItem[] {  if (!gitStatus) return [];   const hasBranch = gitStatus.branch !== null;  const hasOpenPr = gitStatus.openPr !== null;  const hasChanges = gitStatus.hasWorkingTreeChanges;  const hasAhead = gitStatus.aheadCount > 0;  const canCommit = !isDisabled && hasChanges;  const canPush = !isDisabled && hasBranch && !hasChanges && hasAhead;  const canOpenPr = !isDisabled && hasOpenPr;  const canCreatePr =    !isDisabled &&    hasBranch &&    !hasOpenPr &&    !hasChanges &&    gitStatus.hasUpstream &&    gitStatus.behindCount === 0;   return [    {      id: "commit",      label: "Commit",      disabled: !canCommit,      icon: "commit",      kind: "open_modal",    },    {      id: "push",      label: "Push",      disabled: !canPush,      icon: "push",      kind: "run_action",      action: "commit_push",    },    hasOpenPr      ? {          id: "pr",          label: "View PR",          disabled: !canOpenPr,          icon: "pr",          kind: "open_pr",        }      : {          id: "pr",          label: "Create PR",          disabled: !canCreatePr,          icon: "pr",          kind: "run_action",          action: "commit_push_pr",        },  ];} function buildGitModalActionOptions(  gitStatus: GitStatusResult | null,  isDisabled: boolean,): GitModalActionOption[] {  const hasBranch = gitStatus?.branch !== null;  const hasOpenPr = gitStatus?.openPr !== null;   return [    {      action: "commit",      label: "Commit",      disabled: isDisabled,      icon: "commit",    },    {      action: "commit_push",      label: "Commit and push",      disabled: isDisabled || !hasBranch,      icon: "push",      ...(!hasBranch ? { detail: "Requires an attached branch." } : {}),    },    {      action: "commit_push_pr",      label: "Commit and create PR",      disabled: isDisabled || !hasBranch || hasOpenPr,      icon: "pr",      ...(!hasBranch        ? { detail: "Requires an attached branch." }        : hasOpenPr          ? { detail: "A PR is already open for this branch." }          : {}),    },  ];} function getGitStatusQueryKey(gitCwd: string | null): readonly ["git", "status", string | null] {  return ["git", "status", gitCwd];} function gitStatusQueryOptions(api: NativeApi | undefined, gitCwd: string | null) {  return queryOptions({    queryKey: getGitStatusQueryKey(gitCwd),    queryFn: async () => {      if (!api || !gitCwd) {        throw new Error("Git status is unavailable.");      }      return api.git.status({ cwd: gitCwd });    },    enabled: !!api && !!gitCwd,  });} export default function GitActionsControl({ api, gitCwd }: GitActionsControlProps) {  const queryClient = useQueryClient();  const [isGitMenuOpen, setIsGitMenuOpen] = useState(false);  const [isGitModalActionRunning, setIsGitModalActionRunning] = useState(false);  const [gitActionError, setGitActionError] = useState<string | null>(null);  const [isGitModalOpen, setIsGitModalOpen] = useState(false);  const [gitModalSelectedAction, setGitModalSelectedAction] = useState<GitStackedAction>("commit");  const [gitModalCommitMessage, setGitModalCommitMessage] = useState("");  const [gitModalProgress, setGitModalProgress] = useState<GitProgressStep[]>([]);  const [gitModalError, setGitModalError] = useState<string | null>(null);  const [gitModalResult, setGitModalResult] = useState<GitRunStackedActionResult | null>(null);  const gitMenuRef = useRef<HTMLDivElement>(null);   const { data: gitStatus = null, error: gitStatusError } = useQuery(    gitStatusQueryOptions(api, gitCwd),  );   const runImmediateGitActionMutation = useMutation({    mutationFn: async (action: GitStackedAction) => {      if (!api || !gitCwd) {        throw new Error("Git action is unavailable.");      }      return api.git.runStackedAction({ cwd: gitCwd, action });    },    onMutate: () => {      setIsGitMenuOpen(false);      setGitActionError(null);      setGitModalError(null);    },    onSuccess: async () => {      await queryClient.invalidateQueries({ queryKey: ["git"] });    },    onError: (error) => {      setGitActionError(error instanceof Error ? error.message : "Git action failed.");    },  });   const isGitActionRunning = isGitModalActionRunning || runImmediateGitActionMutation.isPending;   const gitBaseDisabled = !api || !gitCwd || !gitStatus || isGitActionRunning;  const gitActionMenuItems = useMemo(    () => buildGitActionMenuItems(gitStatus, gitBaseDisabled),    [gitBaseDisabled, gitStatus],  );  const gitModalActionOptions = useMemo(    () => buildGitModalActionOptions(gitStatus, !gitStatus || isGitActionRunning),    [gitStatus, isGitActionRunning],  );  const selectedGitModalActionOption = useMemo(    () => gitModalActionOptions.find((option) => option.action === gitModalSelectedAction) ?? null,    [gitModalActionOptions, gitModalSelectedAction],  );  const visibleGitActionError = gitActionError ?? gitStatusError?.message;  const gitModalOpenPrUrl = gitModalResult?.pr.url ?? gitStatus?.openPr?.url ?? null;  const gitModalHasProgress = gitModalProgress.length > 0;  const gitModalSelectionMode = !gitModalHasProgress && gitModalResult === null;  const gitModalSteps = gitModalProgress;   const refreshGitStatus = useCallback(async () => {    if (!api || !gitCwd) return;    await queryClient.invalidateQueries({ queryKey: ["git"] });    setGitActionError(null);  }, [api, gitCwd, queryClient]);   const openGitActionModal = useCallback(() => {    setIsGitMenuOpen(false);    setIsGitModalOpen(true);    setGitModalSelectedAction("commit");    setGitModalCommitMessage("");    setGitModalProgress([]);    setGitModalError(null);    setGitModalResult(null);    setGitActionError(null);  }, []);   const closeGitActionModal = useCallback(() => {    if (isGitActionRunning) return;    setIsGitModalOpen(false);    setGitModalSelectedAction("commit");    setGitModalCommitMessage("");    setGitModalProgress([]);    setGitModalError(null);    setGitModalResult(null);  }, [isGitActionRunning]);   const runGitActionImmediately = useCallback(    (action: GitStackedAction) => {      runImmediateGitActionMutation.mutate(action);    },    [runImmediateGitActionMutation],  );   const openExistingPr = useCallback(() => {    setIsGitMenuOpen(false);    setGitActionError(null);    if (!api) {      setGitActionError("Link opening is unavailable.");      return;    }     const prUrl = gitStatus?.openPr?.url ?? null;    if (!prUrl) {      setGitActionError("No open PR found for the current branch.");      return;    }     void api.shell.openExternal(prUrl).catch((error) => {      setGitActionError(error instanceof Error ? error.message : "Unable to open PR link.");    });  }, [api, gitStatus?.openPr?.url]);   const openPrFromModal = useCallback(() => {    if (!api) {      setGitModalError("Link opening is unavailable.");      return;    }     const prUrl = gitModalOpenPrUrl;    if (!prUrl) return;     void api.shell.openExternal(prUrl).catch((error) => {      setGitModalError(error instanceof Error ? error.message : "Unable to open PR link.");    });  }, [api, gitModalOpenPrUrl]);   const runGitAction = useCallback(async () => {    if (!api || !gitCwd || !isGitModalOpen) return;    if (!selectedGitModalActionOption || selectedGitModalActionOption.disabled) return;    const actionCwd = gitCwd;    const action = selectedGitModalActionOption.action;    const commitMessage = gitModalCommitMessage.trim();    const includeGeneratedCommitMessage = commitMessage.length === 0;     setIsGitModalActionRunning(true);    setGitModalError(null);    setGitActionError(null);    setGitModalResult(null);    setGitModalProgress(initialGitProgressSteps(action, commitMessage));     let commit: GitRunStackedActionResult["commit"] = {      status: "skipped_no_changes",    };    let push: GitRunStackedActionResult["push"] = {      status: "skipped_not_requested",    };    let pr: GitRunStackedActionResult["pr"] = {      status: "skipped_not_requested",    };     const updateStep = (      id: GitProgressStep["id"],      status: GitProgressStepStatus,      detail?: string,    ) => {      setGitModalProgress((steps) => updateProgressStep(steps, id, status, detail));    };     try {      if (includeGeneratedCommitMessage) {        updateStep("generate", "running");      } else {        updateStep("commit", "running");      }       const commitRun = await api.git.runStackedAction({        cwd: actionCwd,        action: "commit",        ...(commitMessage.length > 0 ? { commitMessage } : {}),      });      commit = commitRun.commit;       if (includeGeneratedCommitMessage) {        if (commitRun.commit.status === "created") {          if (commitRun.commit.subject) {            setGitModalCommitMessage(commitRun.commit.subject);          }          updateStep("generate", "completed");        } else {          updateStep("generate", "skipped", "No local changes to commit.");        }      }       if (commitRun.commit.status === "created") {        updateStep("commit", "completed", commitRun.commit.subject ?? "Committed local changes.");      } else {        updateStep("commit", "skipped", "No local changes to commit.");      }       if (action !== "commit") {        updateStep("push", "running");        const pushRun = await api.git.runStackedAction({          cwd: actionCwd,          action: "commit_push",        });        push = pushRun.push;        if (pushRun.push.status === "pushed") {          updateStep(            "push",            "completed",            pushRun.push.upstreamBranch              ? `Pushed to ${pushRun.push.upstreamBranch}.`              : "Pushed latest commits.",          );        } else {          updateStep("push", "skipped", "Branch already up to date.");        }      }       if (action === "commit_push_pr") {        updateStep("pr", "running");        const prRun = await api.git.runStackedAction({          cwd: actionCwd,          action: "commit_push_pr",        });        pr = prRun.pr;        if (prRun.pr.status === "opened_existing") {          updateStep(            "pr",            "completed",            prRun.pr.number ? `Opened existing PR #${prRun.pr.number}.` : "Opened existing PR.",          );        } else if (prRun.pr.status === "created") {          updateStep(            "pr",            "completed",            prRun.pr.number ? `Created PR #${prRun.pr.number}.` : "Created PR.",          );        } else {          updateStep("pr", "skipped", "PR step was not requested.");        }      }       setGitModalResult({ action, commit, push, pr });    } catch (error) {      const message = error instanceof Error ? error.message : "Git action failed.";      setGitModalError(message);      setGitActionError(message);      setGitModalProgress((steps) => {        const active = steps.find((step) => step.status === "running");        if (!active) return steps;        return updateProgressStep(steps, active.id, "failed", message);      });    } finally {      setIsGitModalActionRunning(false);      await refreshGitStatus().catch(() => undefined);    }  }, [    api,    gitCwd,    gitModalCommitMessage,    isGitModalOpen,    refreshGitStatus,    selectedGitModalActionOption,  ]);   useEffect(() => {    setGitActionError(null);    setGitModalError(null);    setIsGitModalOpen(false);    setGitModalSelectedAction("commit");

Lines 1–500 of 831.