| 1 | import * as Context from "effect/Context"; |
| 2 | import * as DateTime from "effect/DateTime"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Path from "effect/Path"; |
| 7 | |
| 8 | import { |
| 9 | VcsRepositoryDetectionError, |
| 10 | VcsUnsupportedOperationError, |
| 11 | type ReviewDiffFileContentsInput, |
| 12 | type ReviewDiffFileContentsResult, |
| 13 | type ReviewDiffPreviewError, |
| 14 | type ReviewDiffPreviewInput, |
| 15 | type ReviewDiffPreviewResult, |
| 16 | } from "@t3tools/contracts"; |
| 17 | |
| 18 | import * as ServerConfig from "../config.ts"; |
| 19 | import * as ProjectStore from "../orchestration-v2/ProjectStore.ts"; |
| 20 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 21 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; |
| 22 | import * as ServerSettings from "../serverSettings.ts"; |
| 23 | import { isFilesystemRoot, managedWorktreesDirectories } from "../worktreesDirectory.ts"; |
| 24 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 25 | |
| 26 | export class ReviewService extends Context.Service< |
| 27 | ReviewService, |
| 28 | { |
| 29 | readonly getDiffPreview: ( |
| 30 | input: ReviewDiffPreviewInput, |
| 31 | ) => Effect.Effect<ReviewDiffPreviewResult, ReviewDiffPreviewError>; |
| 32 | readonly getDiffFileContents: ( |
| 33 | input: ReviewDiffFileContentsInput, |
| 34 | ) => Effect.Effect<ReviewDiffFileContentsResult, ReviewDiffPreviewError>; |
| 35 | } |
| 36 | >()("t3/review/ReviewService") {} |
| 37 | |
| 38 | /** @public Service construction is part of the canonical Effect module API. */ |
| 39 | export const make = Effect.gen(function* () { |
| 40 | const config = yield* ServerConfig.ServerConfig; |
| 41 | const fileSystem = yield* FileSystem.FileSystem; |
| 42 | const path = yield* Path.Path; |
| 43 | const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; |
| 44 | const git = yield* GitVcsDriver.GitVcsDriver; |
| 45 | const settings = yield* ServerSettings.ServerSettingsService; |
| 46 | const projectStore = yield* ProjectStore.ProjectStoreV2; |
| 47 | |
| 48 | const canonicalizePath = (value: string) => { |
| 49 | const resolvedPath = path.resolve(value); |
| 50 | return fileSystem.realPath(resolvedPath).pipe( |
| 51 | Effect.catchTags({ |
| 52 | PlatformError: (cause) => |
| 53 | cause.reason._tag === "NotFound" |
| 54 | ? Effect.succeed(resolvedPath) |
| 55 | : Effect.fail( |
| 56 | new VcsRepositoryDetectionError({ |
| 57 | operation: "ReviewService.assertWorkspaceBoundCwd.canonicalizePath", |
| 58 | cwd: resolvedPath, |
| 59 | detail: "Failed to resolve a path while validating the review workspace.", |
| 60 | cause, |
| 61 | }), |
| 62 | ), |
| 63 | }), |
| 64 | ); |
| 65 | }; |
| 66 | |
| 67 | const isWithinRoot = (candidate: string, root: string) => { |
| 68 | const relative = path.relative(root, candidate); |
| 69 | return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); |
| 70 | }; |
| 71 | |
| 72 | const assertWorkspaceBoundCwd = Effect.fn("ReviewService.assertWorkspaceBoundCwd")(function* ( |
| 73 | operation: "ReviewService.getDiffPreview" | "ReviewService.getDiffFileContents", |
| 74 | cwd: string, |
| 75 | ) { |
| 76 | const worktreesDirectories = yield* settings.getSettings.pipe( |
| 77 | Effect.orElseSucceed(() => ({ worktreesDirectory: "", previousWorktreesDirectories: [] })), |
| 78 | ); |
| 79 | const home = yield* HostProcess.HomeDirectory; |
| 80 | const [candidate, workspaceRoot, worktreesRoots] = yield* Effect.all([ |
| 81 | canonicalizePath(cwd), |
| 82 | canonicalizePath(config.cwd), |
| 83 | // A managed root that cannot be resolved, or resolves to a filesystem |
| 84 | // root through a symlink, is skipped rather than failing every review. |
| 85 | Effect.forEach( |
| 86 | managedWorktreesDirectories(worktreesDirectories, config.worktreesDir, path, home), |
| 87 | (directory) => canonicalizePath(directory).pipe(Effect.orElseSucceed(() => null)), |
| 88 | ).pipe( |
| 89 | Effect.map((roots) => |
| 90 | roots.filter((root): root is string => root !== null && !isFilesystemRoot(root, path)), |
| 91 | ), |
| 92 | ), |
| 93 | ]); |
| 94 | |
| 95 | if ( |
| 96 | isWithinRoot(candidate, workspaceRoot) || |
| 97 | worktreesRoots.some((root) => isWithinRoot(candidate, root)) |
| 98 | ) { |
| 99 | return; |
| 100 | } |
| 101 | |
| 102 | // Registered projects can live outside the server cwd, which is the home |
| 103 | // directory in packaged desktop builds, e.g. a repository on another |
| 104 | // Windows drive. Unreadable or unresolvable project roots grant nothing. |
| 105 | const projects = yield* projectStore.listShells().pipe(Effect.orElseSucceed(() => [])); |
| 106 | for (const project of projects) { |
| 107 | const root = yield* canonicalizePath(project.workspaceRoot).pipe( |
| 108 | Effect.orElseSucceed(() => null), |
| 109 | ); |
| 110 | if (root !== null && isWithinRoot(candidate, root)) { |
| 111 | return; |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | return yield* new VcsRepositoryDetectionError({ |
| 116 | operation, |
| 117 | cwd, |
| 118 | detail: |
| 119 | operation === "ReviewService.getDiffPreview" |
| 120 | ? "Review diff preview cwd must stay within the configured workspace root." |
| 121 | : "Review diff file contents cwd must stay within the configured workspace root.", |
| 122 | }); |
| 123 | }); |
| 124 | |
| 125 | const getDiffPreview: ReviewService["Service"]["getDiffPreview"] = Effect.fn( |
| 126 | "ReviewService.getDiffPreview", |
| 127 | )(function* (input) { |
| 128 | yield* assertWorkspaceBoundCwd("ReviewService.getDiffPreview", input.cwd); |
| 129 | |
| 130 | const handle = yield* vcsRegistry.detect({ cwd: input.cwd, requestedKind: "auto" }); |
| 131 | if (!handle) { |
| 132 | return { |
| 133 | cwd: input.cwd, |
| 134 | generatedAt: yield* DateTime.now, |
| 135 | sources: [], |
| 136 | }; |
| 137 | } |
| 138 | |
| 139 | const getDriverDiffPreview = handle.driver.getDiffPreview; |
| 140 | if (!getDriverDiffPreview) { |
| 141 | if (handle.kind === "git") { |
| 142 | return yield* git.getReviewDiffPreview(input); |
| 143 | } |
| 144 | return yield* new VcsUnsupportedOperationError({ |
| 145 | operation: "ReviewService.getDiffPreview", |
| 146 | kind: handle.kind, |
| 147 | detail: `The ${handle.kind} VCS driver does not support review diff previews.`, |
| 148 | }); |
| 149 | } |
| 150 | |
| 151 | return yield* getDriverDiffPreview(input); |
| 152 | }); |
| 153 | |
| 154 | const getDiffFileContents: ReviewService["Service"]["getDiffFileContents"] = Effect.fn( |
| 155 | "ReviewService.getDiffFileContents", |
| 156 | )(function* (input) { |
| 157 | yield* assertWorkspaceBoundCwd("ReviewService.getDiffFileContents", input.cwd); |
| 158 | |
| 159 | const handle = yield* vcsRegistry.detect({ cwd: input.cwd, requestedKind: "auto" }); |
| 160 | if (handle?.kind !== "git") { |
| 161 | return yield* new VcsUnsupportedOperationError({ |
| 162 | operation: "ReviewService.getDiffFileContents", |
| 163 | kind: handle?.kind ?? "unknown", |
| 164 | detail: "Unchanged diff expansion currently requires a Git repository.", |
| 165 | }); |
| 166 | } |
| 167 | |
| 168 | return yield* git.getReviewDiffFileContents(input); |
| 169 | }); |
| 170 | |
| 171 | return ReviewService.of({ |
| 172 | getDiffPreview, |
| 173 | getDiffFileContents, |
| 174 | }); |
| 175 | }); |
| 176 | |
| 177 | export const layer = Layer.effect(ReviewService, make); |