905 lines · 30.3 KB · TypeScript
RawDownload
import {  type GitRunStackedActionResult,  type GitStackedAction,  type GitStatusResult,  type NativeApi,} from "@t3tools/contracts";import { queryOptions, useQuery, useQueryClient } from "@tanstack/react-query";import { useCallback, useEffect, useMemo, useRef, useState } from "react";import {  CheckIcon,  CloudUploadIcon,  GitCommitIcon,  Loader2Icon,  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;} interface GitProgressToastAction {  kind: "open_pr" | "create_pr";  label: "Open PR" | "Create PR";} 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 runningStepMessage(stepId: GitProgressStep["id"]): string {  if (stepId === "generate") return "Generating commit message...";  if (stepId === "commit") return "Committing changes...";  if (stepId === "push") return "Pushing branch...";  return "Preparing pull request...";} function gitActionModalTitle(): string {  return "Commit your changes";} function initialGitProgressSteps(action: GitStackedAction, includeGenerateStep: boolean): GitProgressStep[] {  const steps: GitProgressStep[] = [];   if (includeGenerateStep) {    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 canCreatePrFromStatus(gitStatus: GitStatusResult | null): boolean {  if (!gitStatus) return false;  const hasBranch = gitStatus.branch !== null;  const hasOpenPr = gitStatus.openPr !== null;   return (    hasBranch &&    !hasOpenPr &&    !gitStatus.hasWorkingTreeChanges &&    gitStatus.hasUpstream &&    gitStatus.behindCount === 0  );} export function getGitProgressToastAction(params: {  isRunning: boolean;  hasError: boolean;  result: GitRunStackedActionResult | null;  gitStatus: GitStatusResult | null;  openPrUrl: string | null;}): GitProgressToastAction | null {  if (params.isRunning || params.hasError || !params.result) {    return null;  }   if (params.openPrUrl) {    return {      kind: "open_pr",      label: "Open PR",    };  }   if (params.result.action === "commit_push" && canCreatePrFromStatus(params.gitStatus)) {    return {      kind: "create_pr",      label: "Create PR",    };  }   return null;} 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 && canCreatePrFromStatus(gitStatus);   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 [isGitProgressToastVisible, setIsGitProgressToastVisible] = useState(false);  const gitMenuRef = useRef<HTMLDivElement>(null);   const { data: gitStatus = null, error: gitStatusError } = useQuery(    gitStatusQueryOptions(api, gitCwd),  );   const isGitActionRunning = isGitModalActionRunning;   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 gitProgressToastOpenPrUrl = gitModalResult?.pr.url ?? gitStatus?.openPr?.url ?? null;  const showGitProgressToast =    isGitProgressToastVisible &&    (isGitModalActionRunning ||      gitModalProgress.length > 0 ||      gitModalError !== null ||      gitModalResult !== null);  const activeGitProgressStep = useMemo(    () => gitModalProgress.find((step) => step.status === "running") ?? null,    [gitModalProgress],  );  const gitProgressToastStatus = isGitModalActionRunning    ? "running"    : gitModalError      ? "failed"      : "done";  const gitProgressToastMessage = isGitModalActionRunning    ? runningStepMessage(activeGitProgressStep?.id ?? "commit")    : gitModalError      ? "Git action failed."      : "Done.";  const gitProgressToastAction = useMemo(    () =>      getGitProgressToastAction({        isRunning: isGitModalActionRunning,        hasError: gitModalError !== null,        result: gitModalResult,        gitStatus,        openPrUrl: gitProgressToastOpenPrUrl,      }),    [      gitModalError,      gitModalResult,      gitProgressToastOpenPrUrl,      gitStatus,      isGitModalActionRunning,    ],  );   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("");    setIsGitProgressToastVisible(false);    setGitModalProgress([]);    setGitModalError(null);    setGitModalResult(null);    setGitActionError(null);  }, []);   const closeGitActionModal = useCallback(() => {    if (isGitActionRunning) return;    setIsGitModalOpen(false);    setGitModalSelectedAction("commit");    setGitModalCommitMessage("");  }, [isGitActionRunning]);   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 openPrFromToast = useCallback(() => {    if (!api) {      setGitModalError("Link opening is unavailable.");      return;    }     const prUrl = gitProgressToastOpenPrUrl;    if (!prUrl) return;     void api.shell.openExternal(prUrl).catch((error) => {      setGitModalError(error instanceof Error ? error.message : "Unable to open PR link.");    });  }, [api, gitProgressToastOpenPrUrl]);   const dismissGitProgressToast = useCallback(() => {    if (isGitModalActionRunning) return;    setIsGitProgressToastVisible(false);    setGitModalProgress([]);    setGitModalError(null);    setGitModalResult(null);  }, [isGitModalActionRunning]);   const executeGitAction = useCallback(    async (params: {      action: GitStackedAction;      commitMessage: string;      includeGeneratedCommitMessage: boolean;      closeModal: boolean;    }) => {      if (!api || !gitCwd) return;      const actionCwd = gitCwd;      const { action, closeModal, commitMessage, includeGeneratedCommitMessage } = params;       setIsGitMenuOpen(false);      if (closeModal) {        setIsGitModalOpen(false);      }      setIsGitProgressToastVisible(true);      setIsGitModalActionRunning(true);      setGitModalError(null);      setGitActionError(null);      setGitModalResult(null);      setGitModalProgress(initialGitProgressSteps(action, includeGeneratedCommitMessage));       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") {

Lines 1–500 of 905.