import { Context, Effect, Layer } from "effect"; import { GitManagerError, GitCommandError, type VcsSwitchRefInput, type VcsSwitchRefResult, type VcsCreateRefInput, type VcsCreateRefResult, type VcsCreateWorktreeInput, type VcsCreateWorktreeResult, type VcsListRefsInput, type VcsListRefsResult, type GitManagerServiceError, type GitPreparePullRequestThreadInput, type GitPreparePullRequestThreadResult, type GitPullRequestRefInput, type VcsPullResult, type VcsRemoveWorktreeInput, type GitResolvePullRequestResult, type GitRunStackedActionInput, type GitRunStackedActionResult, type VcsStatusInput, type VcsStatusLocalResult, type VcsStatusRemoteResult, type VcsStatusResult, } from "@t3tools/contracts"; import { GitManager, type GitRunStackedActionOptions } from "./GitManager.ts"; import { GitVcsDriver } from "../vcs/GitVcsDriver.ts"; import { VcsDriverRegistry } from "../vcs/VcsDriverRegistry.ts"; export interface GitWorkflowServiceShape { readonly status: ( input: VcsStatusInput, ) => Effect.Effect; readonly localStatus: ( input: VcsStatusInput, ) => Effect.Effect; readonly remoteStatus: ( input: VcsStatusInput, ) => Effect.Effect; readonly invalidateLocalStatus: (cwd: string) => Effect.Effect; readonly invalidateRemoteStatus: (cwd: string) => Effect.Effect; readonly invalidateStatus: (cwd: string) => Effect.Effect; readonly pullCurrentBranch: (cwd: string) => Effect.Effect; readonly runStackedAction: ( input: GitRunStackedActionInput, options?: GitRunStackedActionOptions, ) => Effect.Effect; readonly resolvePullRequest: ( input: GitPullRequestRefInput, ) => Effect.Effect; readonly preparePullRequestThread: ( input: GitPreparePullRequestThreadInput, ) => Effect.Effect; readonly listRefs: (input: VcsListRefsInput) => Effect.Effect; readonly createWorktree: ( input: VcsCreateWorktreeInput, ) => Effect.Effect; readonly removeWorktree: (input: VcsRemoveWorktreeInput) => Effect.Effect; readonly createRef: ( input: VcsCreateRefInput, ) => Effect.Effect; readonly switchRef: ( input: VcsSwitchRefInput, ) => Effect.Effect; readonly renameBranch: (input: { readonly cwd: string; readonly oldBranch: string; readonly newBranch: string; }) => Effect.Effect<{ readonly branch: string }, GitManagerServiceError>; } export class GitWorkflowService extends Context.Service< GitWorkflowService, GitWorkflowServiceShape >()("t3/git/GitWorkflowService") {} const unsupportedGitWorkflow = (operation: string, cwd: string, detail: string) => new GitManagerError({ operation, detail: `${detail} (${cwd})`, }); const unsupportedGitCommand = (operation: string, cwd: string, detail: string) => new GitCommandError({ operation, command: "vcs-route", cwd, detail, }); function nonRepositoryLocalStatus(): VcsStatusLocalResult { return { isRepo: false, hasPrimaryRemote: false, isDefaultRef: false, refName: null, hasWorkingTreeChanges: false, workingTree: { files: [], insertions: 0, deletions: 0, }, }; } function nonRepositoryStatus(): VcsStatusResult { return { ...nonRepositoryLocalStatus(), hasUpstream: false, aheadCount: 0, behindCount: 0, aheadOfDefaultCount: 0, pr: null, }; } function nonRepositoryListRefs(): VcsListRefsResult { return { refs: [], isRepo: false, hasPrimaryRemote: false, nextCursor: null, totalCount: 0, }; } export const make = Effect.fn("makeGitWorkflowService")(function* () { const registry = yield* VcsDriverRegistry; const git = yield* GitVcsDriver; const gitManager = yield* GitManager; const ensureGit = Effect.fn("GitWorkflowService.ensureGit")(function* ( operation: string, cwd: string, ) { const handle = yield* registry .resolve({ cwd }) .pipe( Effect.mapError((error) => unsupportedGitWorkflow( operation, cwd, error instanceof Error ? error.message : String(error), ), ), ); if (handle.kind !== "git") { return yield* unsupportedGitWorkflow( operation, cwd, `The ${operation} workflow currently supports Git repositories only; detected ${handle.kind}.`, ); } }); const ensureGitCommand = Effect.fn("GitWorkflowService.ensureGitCommand")(function* ( operation: string, cwd: string, ) { const handle = yield* registry .resolve({ cwd }) .pipe( Effect.mapError((error) => unsupportedGitCommand( operation, cwd, error instanceof Error ? error.message : String(error), ), ), ); if (handle.kind !== "git") { return yield* unsupportedGitCommand( operation, cwd, `The ${operation} command currently supports Git repositories only; detected ${handle.kind}.`, ); } }); const detectGitRepositoryForStatus = Effect.fn("GitWorkflowService.detectGitRepositoryForStatus")( function* (operation: string, cwd: string) { const handle = yield* registry .detect({ cwd }) .pipe( Effect.mapError((error) => unsupportedGitWorkflow( operation, cwd, error instanceof Error ? error.message : String(error), ), ), ); if (!handle) { return false; } if (handle.kind !== "git") { return yield* unsupportedGitWorkflow( operation, cwd, `The ${operation} workflow currently supports Git repositories only; detected ${handle.kind}.`, ); } return true; }, ); const detectGitRepositoryForCommand = Effect.fn( "GitWorkflowService.detectGitRepositoryForCommand", )(function* (operation: string, cwd: string) { const handle = yield* registry .detect({ cwd }) .pipe( Effect.mapError((error) => unsupportedGitCommand( operation, cwd, error instanceof Error ? error.message : String(error), ), ), ); if (!handle) { return false; } if (handle.kind !== "git") { return yield* unsupportedGitCommand( operation, cwd, `The ${operation} command currently supports Git repositories only; detected ${handle.kind}.`, ); } return true; }); const routeGitManager = ( operation: string, run: (input: Input) => Effect.Effect, ) => (input: Input) => ensureGit(operation, input.cwd).pipe(Effect.andThen(run(input))); return GitWorkflowService.of({ status: (input) => detectGitRepositoryForStatus("GitWorkflowService.status", input.cwd).pipe( Effect.flatMap((isGitRepository) => isGitRepository ? gitManager.status(input) : Effect.succeed(nonRepositoryStatus()), ), ), localStatus: (input) => detectGitRepositoryForStatus("GitWorkflowService.localStatus", input.cwd).pipe( Effect.flatMap((isGitRepository) => isGitRepository ? gitManager.localStatus(input) : Effect.succeed(nonRepositoryLocalStatus()), ), ), remoteStatus: (input) => detectGitRepositoryForStatus("GitWorkflowService.remoteStatus", input.cwd).pipe( Effect.flatMap((isGitRepository) => isGitRepository ? gitManager.remoteStatus(input) : Effect.succeed(null), ), ), invalidateLocalStatus: gitManager.invalidateLocalStatus, invalidateRemoteStatus: gitManager.invalidateRemoteStatus, invalidateStatus: gitManager.invalidateStatus, pullCurrentBranch: (cwd) => ensureGitCommand("GitWorkflowService.pullCurrentBranch", cwd).pipe( Effect.andThen(git.pullCurrentBranch(cwd)), ), runStackedAction: (input, options) => ensureGit("GitWorkflowService.runStackedAction", input.cwd).pipe( Effect.andThen(gitManager.runStackedAction(input, options)), ), resolvePullRequest: routeGitManager( "GitWorkflowService.resolvePullRequest", gitManager.resolvePullRequest, ), preparePullRequestThread: routeGitManager( "GitWorkflowService.preparePullRequestThread", gitManager.preparePullRequestThread, ), listRefs: (input) => detectGitRepositoryForCommand("GitWorkflowService.listRefs", input.cwd).pipe( Effect.flatMap((isGitRepository) => isGitRepository ? git.listRefs(input) : Effect.succeed(nonRepositoryListRefs()), ), ), createWorktree: (input) => ensureGitCommand("GitWorkflowService.createWorktree", input.cwd).pipe( Effect.andThen(git.createWorktree(input)), ), removeWorktree: (input) => ensureGitCommand("GitWorkflowService.removeWorktree", input.cwd).pipe( Effect.andThen(git.removeWorktree(input)), ), createRef: (input) => ensureGitCommand("GitWorkflowService.createRef", input.cwd).pipe( Effect.andThen(git.createRef(input)), ), switchRef: (input) => ensureGitCommand("GitWorkflowService.switchRef", input.cwd).pipe( Effect.andThen(Effect.scoped(git.switchRef(input))), ), renameBranch: (input) => ensureGit("GitWorkflowService.renameBranch", input.cwd).pipe( Effect.andThen(git.renameBranch(input)), ), }); }); export const layer = Layer.effect(GitWorkflowService, make());