384 lines · 13.3 KB · TypeScript
RawDownload
import { useCallback, useEffect, useMemo } from "react"; import { EnvironmentProject, EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";import type { AtomCommandResult } from "@t3tools/client-runtime/state/runtime";import {  type GitActionRequestInput,  type VcsActionOperation,  type VcsRef,} from "@t3tools/client-runtime/state/vcs";import type { GitRunStackedActionResult } from "@t3tools/contracts";import {  dedupeRemoteBranchesWithLocalMatches,  sanitizeFeatureBranchName,} from "@t3tools/shared/git";import * as Cause from "effect/Cause";import { AsyncResult } from "effect/unstable/reactivity"; import { useBranches } from "../state/queries";import { threadEnvironment } from "../state/threads";import { vcsActionManager, vcsEnvironment } from "../state/vcs";import { uuidv4 } from "../lib/uuid";import { appAtomRegistry } from "./atom-registry";import { setPendingConnectionError } from "./use-remote-environment-registry";import { useAtomCommand } from "./use-atom-command";import { showGitActionResult } from "./use-vcs-action-state";import { useThreadSelection } from "./use-thread-selection";import { useSelectedThreadWorktree } from "./use-selected-thread-worktree"; export function useSelectedThreadGitActions() {  const updateThreadMetadata = useAtomCommand(threadEnvironment.updateMetadata, {    reportFailure: false,  });  const refreshStatus = useAtomCommand(vcsEnvironment.refreshStatus, { reportFailure: false });  const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false });  const createRef = useAtomCommand(vcsEnvironment.createRef, { reportFailure: false });  const createWorktree = useAtomCommand(vcsEnvironment.createWorktree, { reportFailure: false });  const pull = useAtomCommand(vcsEnvironment.pull, { reportFailure: false });  const { selectedThread, selectedThreadProject } = useThreadSelection();  const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();  const runStackedAction = useAtomCommand(    vcsActionManager.runStackedAction({      environmentId: selectedThread?.environmentId ?? null,      cwd: selectedThreadCwd,    }),    { reportFailure: false },  );   const selectedThreadGitRootCwd = selectedThreadProject?.workspaceRoot ?? null;  const branchTarget = useMemo(    () => ({      environmentId: selectedThread?.environmentId ?? null,      cwd: selectedThreadGitRootCwd,      query: null,    }),    [selectedThread?.environmentId, selectedThreadGitRootCwd],  );  const branchState = useBranches(branchTarget);  const updateThreadGitContext = useCallback(    async (      thread: NonNullable<typeof selectedThread>,      nextState: {        readonly branch?: string | null;        readonly worktreePath?: string | null;      },    ) => {      return updateThreadMetadata({        environmentId: thread.environmentId,        input: {          threadId: thread.id,          ...(nextState.branch !== undefined ? { branch: nextState.branch } : {}),          ...(nextState.worktreePath !== undefined ? { worktreePath: nextState.worktreePath } : {}),        },      });    },    [updateThreadMetadata],  );   const refreshSelectedThreadGitStatus = useCallback(    async (options?: { readonly quiet?: boolean; readonly cwd?: string | null }) => {      if (!selectedThread || !selectedThreadProject) {        return null;      }       const cwd = options?.cwd ?? selectedThreadCwd;      if (!cwd) {        return null;      }       const target = { environmentId: selectedThread.environmentId, cwd };      const execute = () =>        refreshStatus({          environmentId: selectedThread.environmentId,          input: { cwd },        });      const result = options?.quiet        ? await execute()        : await vcsActionManager.track(            appAtomRegistry,            target,            {              operation: "refresh_status",              label: "Refreshing source control status",            },            execute,          );      if (AsyncResult.isFailure(result)) {        const error = Cause.squash(result.cause);        const message = error instanceof Error ? error.message : "Failed to refresh git status.";        setPendingConnectionError(message);        return null;      }      setPendingConnectionError(null);      return result.value;    },    [refreshStatus, selectedThread, selectedThreadCwd, selectedThreadProject],  );   useEffect(() => {    if (!selectedThread || !selectedThreadProject) {      return;    }    void refreshSelectedThreadGitStatus({ quiet: true });  }, [refreshSelectedThreadGitStatus, selectedThread, selectedThreadProject]);   const runSelectedThreadGitMutation = useCallback(    async <T, E>(      operation: VcsActionOperation,      label: string,      execute: (input: {        readonly thread: EnvironmentThreadShell;        readonly project: EnvironmentProject;        readonly cwd: string;      }) => Promise<AtomCommandResult<T, E>>,      options?: { readonly managedExternally?: boolean },    ): Promise<T | null> => {      if (!selectedThread || !selectedThreadProject || !selectedThreadCwd) {        return null;      }       const target = {        environmentId: selectedThread.environmentId,        cwd: selectedThreadCwd,      };      setPendingConnectionError(null);      const run = () =>        execute({          thread: selectedThread,          project: selectedThreadProject,          cwd: selectedThreadCwd,        });      const result =        options?.managedExternally === true          ? await run()          : await vcsActionManager.track(appAtomRegistry, target, { operation, label }, run);      if (AsyncResult.isFailure(result)) {        const error = Cause.squash(result.cause);        const message = error instanceof Error ? error.message : "Git action failed.";        setPendingConnectionError(message);        showGitActionResult({ type: "error", title: "Git action failed", description: message });        return null;      }      return result.value;    },    [selectedThread, selectedThreadCwd, selectedThreadProject],  );   const refreshSelectedThreadBranches = useCallback(async (): Promise<ReadonlyArray<VcsRef>> => {    branchState.refresh();    return dedupeRemoteBranchesWithLocalMatches(branchState.data?.refs ?? []).filter(      (branch) => !branch.isRemote,    );  }, [branchState]);   const syncSelectedThreadBranchState = useCallback(    async (input: {      readonly thread: EnvironmentThreadShell;      readonly cwd: string;      readonly nextThreadState?: {        readonly branch?: string | null;        readonly worktreePath?: string | null;      };    }): Promise<AtomCommandResult<void, unknown>> => {      if (input.nextThreadState) {        const updateResult = await updateThreadGitContext(input.thread, input.nextThreadState);        if (AsyncResult.isFailure(updateResult)) {          return AsyncResult.failure(updateResult.cause);        }      }      branchState.refresh();      await refreshSelectedThreadGitStatus({ quiet: true, cwd: input.cwd });      return AsyncResult.success(undefined);    },    [branchState, refreshSelectedThreadGitStatus, updateThreadGitContext],  );   const onCheckoutSelectedThreadBranch = useCallback(    async (branch: string) => {      await runSelectedThreadGitMutation(        "switch_ref",        "Switching branch",        async ({ thread, cwd }) => {          const result = await switchRef({            environmentId: thread.environmentId,            input: { cwd, refName: branch },          });          if (AsyncResult.isFailure(result)) {            return result;          }          const syncResult = await syncSelectedThreadBranchState({            thread,            cwd,            nextThreadState: {              branch: result.value.refName ?? thread.branch,              worktreePath: selectedThreadWorktreePath,            },          });          return AsyncResult.isFailure(syncResult) ? AsyncResult.failure(syncResult.cause) : result;        },      );    },    [      runSelectedThreadGitMutation,      selectedThreadWorktreePath,      syncSelectedThreadBranchState,      switchRef,    ],  );   const onCreateSelectedThreadBranch = useCallback(    async (branch: string) => {      await runSelectedThreadGitMutation(        "create_ref",        "Creating branch",        async ({ thread, cwd }) => {          const result = await createRef({            environmentId: thread.environmentId,            input: { cwd, refName: branch, switchRef: true },          });          if (AsyncResult.isFailure(result)) {            return result;          }          const syncResult = await syncSelectedThreadBranchState({            thread,            cwd,            nextThreadState: {              branch: result.value.refName ?? thread.branch,              worktreePath: selectedThreadWorktreePath,            },          });          return AsyncResult.isFailure(syncResult) ? AsyncResult.failure(syncResult.cause) : result;        },      );    },    [      runSelectedThreadGitMutation,      selectedThreadWorktreePath,      syncSelectedThreadBranchState,      createRef,    ],  );   const onCreateSelectedThreadWorktree = useCallback(    async (nextWorktree: { readonly baseBranch: string; readonly newBranch: string }) => {      await runSelectedThreadGitMutation(        "create_worktree",        "Creating worktree",        async ({ thread, project }) => {          const result = await createWorktree({            environmentId: thread.environmentId,            input: {              cwd: project.workspaceRoot,              refName: nextWorktree.baseBranch,              newRefName: sanitizeFeatureBranchName(nextWorktree.newBranch),              path: null,            },          });          if (AsyncResult.isFailure(result)) {            return result;          }          const syncResult = await syncSelectedThreadBranchState({            thread,            cwd: result.value.worktree.path,            nextThreadState: {              branch: result.value.worktree.refName,              worktreePath: result.value.worktree.path,            },          });          return AsyncResult.isFailure(syncResult) ? AsyncResult.failure(syncResult.cause) : result;        },      );    },    [createWorktree, runSelectedThreadGitMutation, syncSelectedThreadBranchState],  );   const onPullSelectedThreadBranch = useCallback(async () => {    await runSelectedThreadGitMutation(      "pull",      "Pulling latest changes",      async ({ thread, cwd }) => {        const result = await pull({          environmentId: thread.environmentId,          input: { cwd },        });        if (AsyncResult.isFailure(result)) {          return result;        }        await refreshSelectedThreadGitStatus({ quiet: true, cwd });        showGitActionResult({          type: "success",          title:            result.value.status === "skipped_up_to_date"              ? "Already up to date"              : `Pulled latest on ${result.value.refName}`,        });        return result;      },    );  }, [pull, refreshSelectedThreadGitStatus, runSelectedThreadGitMutation]);   const onRunSelectedThreadGitAction = useCallback(    async (input: GitActionRequestInput): Promise<GitRunStackedActionResult | null> => {      const actionId = uuidv4();      return await runSelectedThreadGitMutation(        "run_change_request",        "Running source control action",        async ({ thread, cwd }) => {          const result = await runStackedAction({            actionId,            action: input.action,            ...(input.commitMessage ? { commitMessage: input.commitMessage } : {}),            ...(input.featureBranch ? { featureBranch: input.featureBranch } : {}),            ...(input.filePaths?.length ? { filePaths: [...input.filePaths] } : {}),          });          if (AsyncResult.isFailure(result)) {            return result;          }           showGitActionResult({            type: "success",            title: result.value.toast.title,            description: result.value.toast.description,            prUrl:              result.value.toast.cta.kind === "open_pr" ? result.value.toast.cta.url : undefined,          });           if (result.value.branch.status === "created" && result.value.branch.name) {            const syncResult = await syncSelectedThreadBranchState({              thread,              cwd,              nextThreadState: {                branch: result.value.branch.name,                worktreePath: selectedThreadWorktreePath,              },            });            if (AsyncResult.isFailure(syncResult)) {              return AsyncResult.failure(syncResult.cause);            }          } else {            await refreshSelectedThreadGitStatus({ quiet: true, cwd });          }          return result;        },        { managedExternally: true },      );    },    [      runStackedAction,      refreshSelectedThreadGitStatus,      runSelectedThreadGitMutation,      selectedThreadWorktreePath,      syncSelectedThreadBranchState,    ],  );   return {    refreshSelectedThreadGitStatus,    refreshSelectedThreadBranches,    onCheckoutSelectedThreadBranch,    onCreateSelectedThreadBranch,    onCreateSelectedThreadWorktree,    onPullSelectedThreadBranch,    onRunSelectedThreadGitAction,  };}