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, 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 ( operation: VcsActionOperation, label: string, execute: (input: { readonly thread: EnvironmentThreadShell; readonly project: EnvironmentProject; readonly cwd: string; }) => Promise>, options?: { readonly managedExternally?: boolean }, ): Promise => { 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> => { 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> => { 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 => { 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, }; }