diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 2d95c5219f75f81698943d94c54691c618363b8c..cf04296ea5ec52df4e6255977196abb879c9d97c 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -14,9 +14,10 @@ import type { ThreadId, } from "@t3tools/contracts"; -import { GitCommandError, GitHubCliError, TextGenerationError } from "@t3tools/contracts"; +import { GitCommandError, TextGenerationError } from "@t3tools/contracts"; import { type GitManagerShape } from "./GitManager.ts"; import { + GitHubCliError, type GitHubCliShape, type GitHubPullRequestSummary, GitHubCli, diff --git a/apps/server/src/git/githubPullRequests.ts b/apps/server/src/git/githubPullRequests.ts deleted file mode 100644 index f0804dda8c69c76ad8aae37b94839d7ec2da1118..0000000000000000000000000000000000000000 --- a/apps/server/src/git/githubPullRequests.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Cause, DateTime, Exit, Option, Result, Schema } from "effect"; -import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts"; -import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson"; - -export interface NormalizedGitHubPullRequestRecord { - readonly number: number; - readonly title: string; - readonly url: string; - readonly baseRefName: string; - readonly headRefName: string; - readonly state: "open" | "closed" | "merged"; - readonly updatedAt: Option.Option; - readonly isCrossRepository?: boolean; - readonly headRepositoryNameWithOwner?: string | null; - readonly headRepositoryOwnerLogin?: string | null; -} - -const GitHubPullRequestSchema = Schema.Struct({ - number: PositiveInt, - title: TrimmedNonEmptyString, - url: TrimmedNonEmptyString, - baseRefName: TrimmedNonEmptyString, - headRefName: TrimmedNonEmptyString, - state: Schema.optional(Schema.NullOr(Schema.String)), - mergedAt: Schema.optional(Schema.NullOr(Schema.String)), - updatedAt: Schema.optional(Schema.OptionFromNullOr(Schema.DateTimeUtcFromString)), - isCrossRepository: Schema.optional(Schema.Boolean), - headRepository: Schema.optional( - Schema.NullOr( - Schema.Struct({ - nameWithOwner: Schema.String, - }), - ), - ), - headRepositoryOwner: Schema.optional( - Schema.NullOr( - Schema.Struct({ - login: Schema.String, - }), - ), - ), -}); - -function trimOptionalString(value: string | null | undefined): string | null { - const trimmed = value?.trim() ?? ""; - return trimmed.length > 0 ? trimmed : null; -} - -function normalizeGitHubPullRequestState(input: { - state?: string | null | undefined; - mergedAt?: string | null | undefined; -}): "open" | "closed" | "merged" { - const normalizedState = input.state?.trim().toUpperCase(); - if ( - (typeof input.mergedAt === "string" && input.mergedAt.trim().length > 0) || - normalizedState === "MERGED" - ) { - return "merged"; - } - if (normalizedState === "CLOSED") { - return "closed"; - } - return "open"; -} - -function normalizeGitHubPullRequestRecord( - raw: Schema.Schema.Type, -): NormalizedGitHubPullRequestRecord { - const headRepositoryNameWithOwner = trimOptionalString(raw.headRepository?.nameWithOwner); - const headRepositoryOwnerLogin = - trimOptionalString(raw.headRepositoryOwner?.login) ?? - (typeof headRepositoryNameWithOwner === "string" && headRepositoryNameWithOwner.includes("/") - ? (headRepositoryNameWithOwner.split("/")[0] ?? null) - : null); - - return { - number: raw.number, - title: raw.title, - url: raw.url, - baseRefName: raw.baseRefName, - headRefName: raw.headRefName, - state: normalizeGitHubPullRequestState(raw), - updatedAt: raw.updatedAt ?? Option.none(), - ...(typeof raw.isCrossRepository === "boolean" - ? { isCrossRepository: raw.isCrossRepository } - : {}), - ...(headRepositoryNameWithOwner ? { headRepositoryNameWithOwner } : {}), - ...(headRepositoryOwnerLogin ? { headRepositoryOwnerLogin } : {}), - }; -} - -const decodeGitHubPullRequestList = decodeJsonResult(Schema.Array(Schema.Unknown)); -const decodeGitHubPullRequest = decodeJsonResult(GitHubPullRequestSchema); -const decodeGitHubPullRequestEntry = Schema.decodeUnknownExit(GitHubPullRequestSchema); - -export const formatGitHubJsonDecodeError = formatSchemaError; - -export function decodeGitHubPullRequestListJson( - raw: string, -): Result.Result< - ReadonlyArray, - Cause.Cause -> { - const result = decodeGitHubPullRequestList(raw); - if (Result.isSuccess(result)) { - const pullRequests: NormalizedGitHubPullRequestRecord[] = []; - for (const entry of result.success) { - const decodedEntry = decodeGitHubPullRequestEntry(entry); - if (Exit.isFailure(decodedEntry)) { - continue; - } - pullRequests.push(normalizeGitHubPullRequestRecord(decodedEntry.value)); - } - return Result.succeed(pullRequests); - } - return Result.fail(result.failure); -} - -export function decodeGitHubPullRequestJson( - raw: string, -): Result.Result> { - const result = decodeGitHubPullRequest(raw); - if (Result.isSuccess(result)) { - return Result.succeed(normalizeGitHubPullRequestRecord(result.success)); - } - return Result.fail(result.failure); -} diff --git a/apps/server/src/sourceControl/GitHubCli.ts b/apps/server/src/sourceControl/GitHubCli.ts index bb90aeba0145cb90cc850c8c15260b195219ba54..9dcf0ffec750744b1ac44664071467dee4aeec81 100644 --- a/apps/server/src/sourceControl/GitHubCli.ts +++ b/apps/server/src/sourceControl/GitHubCli.ts @@ -1,16 +1,26 @@ import { Context, Effect, Layer, Result, Schema, SchemaIssue } from "effect"; -import { GitHubCliError, TrimmedNonEmptyString, type VcsError } from "@t3tools/contracts"; +import { TrimmedNonEmptyString, type VcsError } from "@t3tools/contracts"; import { VcsProcess, type VcsProcessOutput } from "../vcs/VcsProcess.ts"; import { decodeGitHubPullRequestJson, decodeGitHubPullRequestListJson, formatGitHubJsonDecodeError, -} from "../git/githubPullRequests.ts"; +} from "./gitHubPullRequests.ts"; const DEFAULT_TIMEOUT_MS = 30_000; +export class GitHubCliError extends Schema.TaggedErrorClass()("GitHubCliError", { + operation: Schema.String, + detail: Schema.String, + cause: Schema.optional(Schema.Defect), +}) { + override get message(): string { + return `GitHub CLI failed in ${this.operation}: ${this.detail}`; + } +} + export interface GitHubPullRequestSummary { readonly number: number; readonly title: string; diff --git a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts b/apps/server/src/sourceControl/GitHubSourceControlProvider.ts index 3bdccbf97d16a7f0df6260c3cfca6ae09944be94..916db45fd4f34271994bf28547fb89c3f293bb9e 100644 --- a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts +++ b/apps/server/src/sourceControl/GitHubSourceControlProvider.ts @@ -3,11 +3,10 @@ import { SourceControlProviderError, type ChangeRequest, type ChangeRequestState, - type GitHubCliError, } from "@t3tools/contracts"; -import { GitHubCli, type GitHubPullRequestSummary } from "./GitHubCli.ts"; -import { decodeGitHubPullRequestListJson } from "../git/githubPullRequests.ts"; +import { GitHubCli, type GitHubCliError, type GitHubPullRequestSummary } from "./GitHubCli.ts"; +import { decodeGitHubPullRequestListJson } from "./gitHubPullRequests.ts"; import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts"; function providerError(operation: string, cause: GitHubCliError): SourceControlProviderError { diff --git a/apps/server/src/sourceControl/gitHubPullRequests.ts b/apps/server/src/sourceControl/gitHubPullRequests.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0804dda8c69c76ad8aae37b94839d7ec2da1118 --- /dev/null +++ b/apps/server/src/sourceControl/gitHubPullRequests.ts @@ -0,0 +1,127 @@ +import { Cause, DateTime, Exit, Option, Result, Schema } from "effect"; +import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts"; +import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson"; + +export interface NormalizedGitHubPullRequestRecord { + readonly number: number; + readonly title: string; + readonly url: string; + readonly baseRefName: string; + readonly headRefName: string; + readonly state: "open" | "closed" | "merged"; + readonly updatedAt: Option.Option; + readonly isCrossRepository?: boolean; + readonly headRepositoryNameWithOwner?: string | null; + readonly headRepositoryOwnerLogin?: string | null; +} + +const GitHubPullRequestSchema = Schema.Struct({ + number: PositiveInt, + title: TrimmedNonEmptyString, + url: TrimmedNonEmptyString, + baseRefName: TrimmedNonEmptyString, + headRefName: TrimmedNonEmptyString, + state: Schema.optional(Schema.NullOr(Schema.String)), + mergedAt: Schema.optional(Schema.NullOr(Schema.String)), + updatedAt: Schema.optional(Schema.OptionFromNullOr(Schema.DateTimeUtcFromString)), + isCrossRepository: Schema.optional(Schema.Boolean), + headRepository: Schema.optional( + Schema.NullOr( + Schema.Struct({ + nameWithOwner: Schema.String, + }), + ), + ), + headRepositoryOwner: Schema.optional( + Schema.NullOr( + Schema.Struct({ + login: Schema.String, + }), + ), + ), +}); + +function trimOptionalString(value: string | null | undefined): string | null { + const trimmed = value?.trim() ?? ""; + return trimmed.length > 0 ? trimmed : null; +} + +function normalizeGitHubPullRequestState(input: { + state?: string | null | undefined; + mergedAt?: string | null | undefined; +}): "open" | "closed" | "merged" { + const normalizedState = input.state?.trim().toUpperCase(); + if ( + (typeof input.mergedAt === "string" && input.mergedAt.trim().length > 0) || + normalizedState === "MERGED" + ) { + return "merged"; + } + if (normalizedState === "CLOSED") { + return "closed"; + } + return "open"; +} + +function normalizeGitHubPullRequestRecord( + raw: Schema.Schema.Type, +): NormalizedGitHubPullRequestRecord { + const headRepositoryNameWithOwner = trimOptionalString(raw.headRepository?.nameWithOwner); + const headRepositoryOwnerLogin = + trimOptionalString(raw.headRepositoryOwner?.login) ?? + (typeof headRepositoryNameWithOwner === "string" && headRepositoryNameWithOwner.includes("/") + ? (headRepositoryNameWithOwner.split("/")[0] ?? null) + : null); + + return { + number: raw.number, + title: raw.title, + url: raw.url, + baseRefName: raw.baseRefName, + headRefName: raw.headRefName, + state: normalizeGitHubPullRequestState(raw), + updatedAt: raw.updatedAt ?? Option.none(), + ...(typeof raw.isCrossRepository === "boolean" + ? { isCrossRepository: raw.isCrossRepository } + : {}), + ...(headRepositoryNameWithOwner ? { headRepositoryNameWithOwner } : {}), + ...(headRepositoryOwnerLogin ? { headRepositoryOwnerLogin } : {}), + }; +} + +const decodeGitHubPullRequestList = decodeJsonResult(Schema.Array(Schema.Unknown)); +const decodeGitHubPullRequest = decodeJsonResult(GitHubPullRequestSchema); +const decodeGitHubPullRequestEntry = Schema.decodeUnknownExit(GitHubPullRequestSchema); + +export const formatGitHubJsonDecodeError = formatSchemaError; + +export function decodeGitHubPullRequestListJson( + raw: string, +): Result.Result< + ReadonlyArray, + Cause.Cause +> { + const result = decodeGitHubPullRequestList(raw); + if (Result.isSuccess(result)) { + const pullRequests: NormalizedGitHubPullRequestRecord[] = []; + for (const entry of result.success) { + const decodedEntry = decodeGitHubPullRequestEntry(entry); + if (Exit.isFailure(decodedEntry)) { + continue; + } + pullRequests.push(normalizeGitHubPullRequestRecord(decodedEntry.value)); + } + return Result.succeed(pullRequests); + } + return Result.fail(result.failure); +} + +export function decodeGitHubPullRequestJson( + raw: string, +): Result.Result> { + const result = decodeGitHubPullRequest(raw); + if (Result.isSuccess(result)) { + return Result.succeed(normalizeGitHubPullRequestRecord(result.success)); + } + return Result.fail(result.failure); +} diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index dd12e749da44156c100ef64d4352e8c82e835098..cf3bfe93efa7bbcacd5a327eeea3f9c97cf69908 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -328,16 +328,6 @@ export class GitCommandError extends Schema.TaggedErrorClass()( } } -export class GitHubCliError extends Schema.TaggedErrorClass()("GitHubCliError", { - operation: Schema.String, - detail: Schema.String, - cause: Schema.optional(Schema.Defect), -}) { - override get message(): string { - return `GitHub CLI failed in ${this.operation}: ${this.detail}`; - } -} - export class TextGenerationError extends Schema.TaggedErrorClass()( "TextGenerationError", { @@ -364,7 +354,6 @@ export class GitManagerError extends Schema.TaggedErrorClass()( export const GitManagerServiceError = Schema.Union([ GitManagerError, GitCommandError, - GitHubCliError, SourceControlProviderError, TextGenerationError, ]);