import { useAtomValue } from "@effect/atom-react";import type { AtomCommandFailure, AtomCommandResult, AtomCommandSuccess,} from "@t3tools/client-runtime/state/runtime";import { VcsActionUnavailableError, type VcsActionOperation,} from "@t3tools/client-runtime/state/vcs";import type { EnvironmentId, GitActionProgressEvent, GitResolvePullRequestResult, GitStackedAction, SourceControlCloneProtocol, SourceControlRepositoryVisibility, ThreadId,} from "@t3tools/contracts";import * as Cause from "effect/Cause";import * as Option from "effect/Option";import { AsyncResult } from "effect/unstable/reactivity";import { useCallback } from "react"; import { appAtomRegistry } from "../rpc/atomRegistry";import { gitEnvironment } from "./git";import { useEnvironmentQuery } from "./query";import { sourceControlEnvironment } from "./sourceControl";import { useAtomCommand } from "./use-atom-command";import { vcsActionManager, vcsEnvironment } from "./vcs"; export type SourceControlActionKind = | "init" | "pull" | "publishRepository" | "runStackedAction" | "preparePullRequestThread"; export interface SourceControlActionScope { readonly environmentId: EnvironmentId | null; readonly cwd: string | null;} interface SourceControlActionState< TArgs extends ReadonlyArray<unknown>, R extends AtomCommandResult<unknown, unknown>,> { readonly isPending: boolean; readonly error: unknown; readonly run: ( ...args: TArgs ) => Promise< AtomCommandResult<AtomCommandSuccess<R>, AtomCommandFailure<R> | VcsActionUnavailableError> >; readonly resetError: () => void;} const ACTION_OPERATION = { init: "init", pull: "pull", publishRepository: "publish_repository", runStackedAction: "run_change_request", preparePullRequestThread: "prepare_pull_request_thread",} as const satisfies Record<SourceControlActionKind, VcsActionOperation>; function useAction< TArgs extends ReadonlyArray<unknown>, R extends AtomCommandResult<unknown, unknown>,>(input: { readonly kind: SourceControlActionKind; readonly label: string; readonly scope: SourceControlActionScope; readonly action: (...args: TArgs) => Promise<R>; readonly onSuccess?: () => void; readonly managedExternally?: boolean;}): SourceControlActionState<TArgs, R> { const operation = ACTION_OPERATION[input.kind]; const state = useAtomValue(vcsActionManager.stateAtom(input.scope)); const ownsState = state.operation === operation; const resetError = useCallback(() => { vcsActionManager.resetError(appAtomRegistry, input.scope, operation); }, [input.scope, operation]); const run = useCallback( async (...args: TArgs) => { const execute = async (): Promise< AtomCommandResult<AtomCommandSuccess<R>, AtomCommandFailure<R>> > => { const result = await input.action(...args); if (AsyncResult.isSuccess(result)) { input.onSuccess?.(); } return result as AtomCommandResult<AtomCommandSuccess<R>, AtomCommandFailure<R>>; }; return input.managedExternally === true ? execute() : vcsActionManager.track( appAtomRegistry, input.scope, { operation, label: input.label, }, execute, ); }, [input.action, input.label, input.managedExternally, input.onSuccess, input.scope, operation], ); return { error: ownsState ? state.error : null, isPending: ownsState && state.isRunning, resetError, run, };} function resolveScope(scope: SourceControlActionScope) { if (scope.environmentId === null || scope.cwd === null) { return null; } return { environmentId: scope.environmentId, cwd: scope.cwd, };} export function useSourceControlActionRunning( scope: SourceControlActionScope, kinds: ReadonlyArray<SourceControlActionKind>,): boolean { const state = useAtomValue(vcsActionManager.stateAtom(scope)); return ( state.isRunning && state.operation !== null && kinds.some((kind) => ACTION_OPERATION[kind] === state.operation) );} export function useVcsInitAction(scope: SourceControlActionScope) { const init = useAtomCommand(vcsEnvironment.init, { reportFailure: false }); const action = useCallback(async () => { const target = resolveScope(scope); if (target === null) { return AsyncResult.failure<never, VcsActionUnavailableError>( Cause.fail( new VcsActionUnavailableError({ operation: "init", environmentId: scope.environmentId, cwd: scope.cwd, }), ), ); } return init({ environmentId: target.environmentId, input: { cwd: target.cwd }, }); }, [init, scope]); return useAction({ kind: "init", label: "Initializing repository", scope, action });} export function useVcsPullAction(scope: SourceControlActionScope) { const pull = useAtomCommand(vcsEnvironment.pull, { reportFailure: false }); const status = useEnvironmentQuery( scope.environmentId !== null && scope.cwd !== null ? vcsEnvironment.status({ environmentId: scope.environmentId, input: { cwd: scope.cwd }, }) : null, ); const action = useCallback(async () => { const target = resolveScope(scope); if (target === null) { return AsyncResult.failure<never, VcsActionUnavailableError>( Cause.fail( new VcsActionUnavailableError({ operation: "pull", environmentId: scope.environmentId, cwd: scope.cwd, }), ), ); } return pull({ environmentId: target.environmentId, input: { cwd: target.cwd }, }); }, [pull, scope]); return useAction({ kind: "pull", label: "Pulling latest changes", scope, action, onSuccess: status.refresh, });} export function useGitStackedAction(scope: SourceControlActionScope) { const runStackedAction = useAtomCommand(vcsActionManager.runStackedAction(scope), { reportFailure: false, }); const status = useEnvironmentQuery( scope.environmentId !== null && scope.cwd !== null ? vcsEnvironment.status({ environmentId: scope.environmentId, input: { cwd: scope.cwd }, }) : null, ); const action = useCallback( async (input: { actionId: string; action: GitStackedAction; commitMessage?: string; featureBranch?: boolean; filePaths?: string[]; threadId?: ThreadId; onProgress?: (event: GitActionProgressEvent) => void; }) => { if (resolveScope(scope) === null) { return AsyncResult.failure<never, VcsActionUnavailableError>( Cause.fail( new VcsActionUnavailableError({ operation: "run_change_request", environmentId: scope.environmentId, cwd: scope.cwd, }), ), ); } return runStackedAction({ actionId: input.actionId, action: input.action, ...(input.commitMessage ? { commitMessage: input.commitMessage } : {}), ...(input.featureBranch ? { featureBranch: true } : {}), ...(input.filePaths?.length ? { filePaths: input.filePaths } : {}), ...(input.threadId !== undefined ? { threadId: input.threadId } : {}), ...(input.onProgress ? { onProgress: input.onProgress } : {}), }); }, [runStackedAction, scope], ); return useAction({ kind: "runStackedAction", label: "Running source control action", scope, action, onSuccess: status.refresh, managedExternally: true, });} export function useSourceControlPublishRepositoryAction(scope: SourceControlActionScope) { const publishRepository = useAtomCommand(sourceControlEnvironment.publishRepository, { reportFailure: false, }); const status = useEnvironmentQuery( scope.environmentId !== null && scope.cwd !== null ? vcsEnvironment.status({ environmentId: scope.environmentId, input: { cwd: scope.cwd }, }) : null, ); const action = useCallback( async (input: { provider: "github" | "gitlab" | "bitbucket" | "azure-devops"; repository: string; visibility: SourceControlRepositoryVisibility; remoteName: string; protocol: SourceControlCloneProtocol; }) => { const target = resolveScope(scope); if (target === null) { return AsyncResult.failure<never, VcsActionUnavailableError>( Cause.fail( new VcsActionUnavailableError({ operation: "publish_repository", environmentId: scope.environmentId, cwd: scope.cwd, }), ), ); } return publishRepository({ environmentId: target.environmentId, input: { cwd: target.cwd, ...input, }, }); }, [publishRepository, scope], ); return useAction({ kind: "publishRepository", label: "Publishing repository", scope, action, onSuccess: status.refresh, });} export function usePreparePullRequestThreadAction(scope: SourceControlActionScope) { const preparePullRequestThread = useAtomCommand(gitEnvironment.preparePullRequestThread, { reportFailure: false, }); const action = useCallback( async (input: { reference: string; mode: "local" | "worktree"; threadId?: ThreadId }) => { const target = resolveScope(scope); if (target === null) { return AsyncResult.failure<never, VcsActionUnavailableError>( Cause.fail( new VcsActionUnavailableError({ operation: "prepare_pull_request_thread", environmentId: scope.environmentId, cwd: scope.cwd, }), ), ); } return preparePullRequestThread({ environmentId: target.environmentId, input: { cwd: target.cwd, reference: input.reference, mode: input.mode, ...(input.threadId ? { threadId: input.threadId } : {}), }, }); }, [preparePullRequestThread, scope], ); return useAction({ kind: "preparePullRequestThread", label: "Preparing pull request thread", scope, action, });} export interface PullRequestResolutionTarget { readonly environmentId: EnvironmentId | null; readonly cwd: string | null; readonly reference: string | null;} export function readCachedPullRequestResolution( target: PullRequestResolutionTarget,): GitResolvePullRequestResult | null { if (target.environmentId === null || target.cwd === null || target.reference === null) { return null; } return Option.getOrNull( AsyncResult.value( appAtomRegistry.get( gitEnvironment.pullRequestResolution({ environmentId: target.environmentId, input: { cwd: target.cwd, reference: target.reference }, }), ), ), );} export function usePullRequestResolutionState(target: PullRequestResolutionTarget) { const query = useEnvironmentQuery( target.environmentId !== null && target.cwd !== null && target.reference !== null ? gitEnvironment.pullRequestResolution({ environmentId: target.environmentId, input: { cwd: target.cwd, reference: target.reference, }, }) : null, ); const cached = readCachedPullRequestResolution(target); return { data: query.data ?? cached, error: query.error, isPending: query.isPending && cached === null, isFetching: query.isPending, refresh: query.refresh, };}
import { useAtomValue } from "@effect/atom-react";
import type {
AtomCommandFailure,
AtomCommandResult,
AtomCommandSuccess,
} from "@t3tools/client-runtime/state/runtime";
import {
VcsActionUnavailableError,
type VcsActionOperation,
} from "@t3tools/client-runtime/state/vcs";
import type {
EnvironmentId,
GitActionProgressEvent,
GitResolvePullRequestResult,
GitStackedAction,
SourceControlCloneProtocol,
SourceControlRepositoryVisibility,
ThreadId,
} from "@t3tools/contracts";
import * as Cause from "effect/Cause";
import * as Option from "effect/Option";
import { AsyncResult } from "effect/unstable/reactivity";
import { useCallback } from "react";
import { appAtomRegistry } from "../rpc/atomRegistry";
import { gitEnvironment } from "./git";
import { useEnvironmentQuery } from "./query";
import { sourceControlEnvironment } from "./sourceControl";
import { useAtomCommand } from "./use-atom-command";
import { vcsActionManager, vcsEnvironment } from "./vcs";
export type SourceControlActionKind =
| "init"
| "pull"
| "publishRepository"
| "runStackedAction"
| "preparePullRequestThread";
export interface SourceControlActionScope {
readonly environmentId: EnvironmentId | null;
readonly cwd: string | null;
}
interface SourceControlActionState<
TArgs extends ReadonlyArray<unknown>,
R extends AtomCommandResult<unknown, unknown>,
> {
readonly isPending: boolean;
readonly error: unknown;
readonly run: (
...args: TArgs
) => Promise<
AtomCommandResult<AtomCommandSuccess<R>, AtomCommandFailure<R> | VcsActionUnavailableError>
>;
readonly resetError: () => void;
}
const ACTION_OPERATION = {
init: "init",
pull: "pull",
publishRepository: "publish_repository",
runStackedAction: "run_change_request",
preparePullRequestThread: "prepare_pull_request_thread",
} as const satisfies Record<SourceControlActionKind, VcsActionOperation>;
function useAction<
TArgs extends ReadonlyArray<unknown>,
R extends AtomCommandResult<unknown, unknown>,
>(input: {
readonly kind: SourceControlActionKind;
readonly label: string;
readonly scope: SourceControlActionScope;
readonly action: (...args: TArgs) => Promise<R>;
readonly onSuccess?: () => void;
readonly managedExternally?: boolean;
}): SourceControlActionState<TArgs, R> {
const operation = ACTION_OPERATION[input.kind];
const state = useAtomValue(vcsActionManager.stateAtom(input.scope));
const ownsState = state.operation === operation;
const resetError = useCallback(() => {
vcsActionManager.resetError(appAtomRegistry, input.scope, operation);
}, [input.scope, operation]);
const run = useCallback(
async (...args: TArgs) => {
const execute = async (): Promise<
AtomCommandResult<AtomCommandSuccess<R>, AtomCommandFailure<R>>
> => {
const result = await input.action(...args);
if (AsyncResult.isSuccess(result)) {
input.onSuccess?.();
}
return result as AtomCommandResult<AtomCommandSuccess<R>, AtomCommandFailure<R>>;
};
return input.managedExternally === true
? execute()
: vcsActionManager.track(
appAtomRegistry,
input.scope,
{
operation,
label: input.label,
},
execute,
);
},
[input.action, input.label, input.managedExternally, input.onSuccess, input.scope, operation],
);
return {
error: ownsState ? state.error : null,
isPending: ownsState && state.isRunning,
resetError,
run,
};
}
function resolveScope(scope: SourceControlActionScope) {
if (scope.environmentId === null || scope.cwd === null) {
return null;
}
return {
environmentId: scope.environmentId,
cwd: scope.cwd,
};
}
export function useSourceControlActionRunning(
scope: SourceControlActionScope,
kinds: ReadonlyArray<SourceControlActionKind>,
): boolean {
const state = useAtomValue(vcsActionManager.stateAtom(scope));
return (
state.isRunning &&
state.operation !== null &&
kinds.some((kind) => ACTION_OPERATION[kind] === state.operation)
);
}
export function useVcsInitAction(scope: SourceControlActionScope) {
const init = useAtomCommand(vcsEnvironment.init, { reportFailure: false });
const action = useCallback(async () => {
const target = resolveScope(scope);
if (target === null) {
return AsyncResult.failure<never, VcsActionUnavailableError>(
Cause.fail(
new VcsActionUnavailableError({
operation: "init",
environmentId: scope.environmentId,
cwd: scope.cwd,
}),
),
);
}
return init({
environmentId: target.environmentId,
input: { cwd: target.cwd },
});
}, [init, scope]);
return useAction({ kind: "init", label: "Initializing repository", scope, action });
}
export function useVcsPullAction(scope: SourceControlActionScope) {
const pull = useAtomCommand(vcsEnvironment.pull, { reportFailure: false });
const status = useEnvironmentQuery(
scope.environmentId !== null && scope.cwd !== null
? vcsEnvironment.status({
environmentId: scope.environmentId,
input: { cwd: scope.cwd },
})
: null,
);
const action = useCallback(async () => {
const target = resolveScope(scope);
if (target === null) {
return AsyncResult.failure<never, VcsActionUnavailableError>(
Cause.fail(
new VcsActionUnavailableError({
operation: "pull",
environmentId: scope.environmentId,
cwd: scope.cwd,
}),
),
);
}
return pull({
environmentId: target.environmentId,
input: { cwd: target.cwd },
});
}, [pull, scope]);
return useAction({
kind: "pull",
label: "Pulling latest changes",
scope,
action,
onSuccess: status.refresh,
});
}
export function useGitStackedAction(scope: SourceControlActionScope) {
const runStackedAction = useAtomCommand(vcsActionManager.runStackedAction(scope), {
reportFailure: false,
});
const status = useEnvironmentQuery(
scope.environmentId !== null && scope.cwd !== null
? vcsEnvironment.status({
environmentId: scope.environmentId,
input: { cwd: scope.cwd },
})
: null,
);
const action = useCallback(
async (input: {
actionId: string;
action: GitStackedAction;
commitMessage?: string;
featureBranch?: boolean;
filePaths?: string[];
threadId?: ThreadId;
onProgress?: (event: GitActionProgressEvent) => void;
}) => {
if (resolveScope(scope) === null) {
return AsyncResult.failure<never, VcsActionUnavailableError>(
Cause.fail(
new VcsActionUnavailableError({
operation: "run_change_request",
environmentId: scope.environmentId,
cwd: scope.cwd,
}),
),
);
}
return runStackedAction({
actionId: input.actionId,
action: input.action,
...(input.commitMessage ? { commitMessage: input.commitMessage } : {}),
...(input.featureBranch ? { featureBranch: true } : {}),
...(input.filePaths?.length ? { filePaths: input.filePaths } : {}),
...(input.threadId !== undefined ? { threadId: input.threadId } : {}),
...(input.onProgress ? { onProgress: input.onProgress } : {}),
});
},
[runStackedAction, scope],
);
return useAction({
kind: "runStackedAction",
label: "Running source control action",
scope,
action,
onSuccess: status.refresh,
managedExternally: true,
});
}
export function useSourceControlPublishRepositoryAction(scope: SourceControlActionScope) {
const publishRepository = useAtomCommand(sourceControlEnvironment.publishRepository, {
reportFailure: false,
});
const status = useEnvironmentQuery(
scope.environmentId !== null && scope.cwd !== null
? vcsEnvironment.status({
environmentId: scope.environmentId,
input: { cwd: scope.cwd },
})
: null,
);
const action = useCallback(
async (input: {
provider: "github" | "gitlab" | "bitbucket" | "azure-devops";
repository: string;
visibility: SourceControlRepositoryVisibility;
remoteName: string;
protocol: SourceControlCloneProtocol;
}) => {
const target = resolveScope(scope);
if (target === null) {
return AsyncResult.failure<never, VcsActionUnavailableError>(
Cause.fail(
new VcsActionUnavailableError({
operation: "publish_repository",
environmentId: scope.environmentId,
cwd: scope.cwd,
}),
),
);
}
return publishRepository({
environmentId: target.environmentId,
input: {
cwd: target.cwd,
...input,
},
});
},
[publishRepository, scope],
);
return useAction({
kind: "publishRepository",
label: "Publishing repository",
scope,
action,
onSuccess: status.refresh,
});
}
export function usePreparePullRequestThreadAction(scope: SourceControlActionScope) {
const preparePullRequestThread = useAtomCommand(gitEnvironment.preparePullRequestThread, {
reportFailure: false,
});
const action = useCallback(
async (input: { reference: string; mode: "local" | "worktree"; threadId?: ThreadId }) => {
const target = resolveScope(scope);
if (target === null) {
return AsyncResult.failure<never, VcsActionUnavailableError>(
Cause.fail(
new VcsActionUnavailableError({
operation: "prepare_pull_request_thread",
environmentId: scope.environmentId,
cwd: scope.cwd,
}),
),
);
}
return preparePullRequestThread({
environmentId: target.environmentId,
input: {
cwd: target.cwd,
reference: input.reference,
mode: input.mode,
...(input.threadId ? { threadId: input.threadId } : {}),
},
});
},
[preparePullRequestThread, scope],
);
return useAction({
kind: "preparePullRequestThread",
label: "Preparing pull request thread",
scope,
action,
});
}
export interface PullRequestResolutionTarget {
readonly environmentId: EnvironmentId | null;
readonly cwd: string | null;
readonly reference: string | null;
}
export function readCachedPullRequestResolution(
target: PullRequestResolutionTarget,
): GitResolvePullRequestResult | null {
if (target.environmentId === null || target.cwd === null || target.reference === null) {
return null;
}
return Option.getOrNull(
AsyncResult.value(
appAtomRegistry.get(
gitEnvironment.pullRequestResolution({
environmentId: target.environmentId,
input: { cwd: target.cwd, reference: target.reference },
}),
),
),
);
}
export function usePullRequestResolutionState(target: PullRequestResolutionTarget) {
const query = useEnvironmentQuery(
target.environmentId !== null && target.cwd !== null && target.reference !== null
? gitEnvironment.pullRequestResolution({
environmentId: target.environmentId,
input: {
cwd: target.cwd,
reference: target.reference,
},
})
: null,
);
const cached = readCachedPullRequestResolution(target);
return {
data: query.data ?? cached,
error: query.error,
isPending: query.isPending && cached === null,
isFetching: query.isPending,
refresh: query.refresh,
};
}