diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index bb7f91ffc39..c06915c51b9 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -2757,6 +2757,65 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { }), ); + it.effect("preserves both branch materialization failures when the fallback also fails", () => + Effect.gen(function* () { + const repoDir = yield* makeTempDir("t3code-git-manager-"); + yield* initRepo(repoDir); + const originDir = yield* createBareRemote(); + yield* runGit(repoDir, ["remote", "add", "origin", originDir]); + yield* runGit(repoDir, ["push", "-u", "origin", "main"]); + + const missingForkDir = NodePath.join(repoDir, "missing-fork.git"); + const { manager } = yield* makeManager({ + ghScenario: { + pullRequest: { + number: 93, + title: "Missing fork branch", + url: "https://github.com/pingdotgg/codething-mvp/pull/93", + baseRefName: "main", + headRefName: "feature/missing-fork-branch", + state: "open", + isCrossRepository: true, + headRepositoryNameWithOwner: "octocat/codething-mvp", + headRepositoryOwnerLogin: "octocat", + }, + repositoryCloneUrls: { + "octocat/codething-mvp": { + url: missingForkDir, + sshUrl: missingForkDir, + }, + }, + }, + }); + + const error = yield* preparePullRequestThread(manager, { + cwd: repoDir, + reference: "93", + mode: "worktree", + }).pipe(Effect.flip); + + if (error._tag !== "GitPullRequestMaterializationError") { + return yield* Effect.die(error); + } + expect(error).toMatchObject({ + cwd: repoDir, + pullRequestNumber: 93, + headRepository: "octocat/codething-mvp", + headBranch: "feature/missing-fork-branch", + localBranch: "t3code/pr-93/feature/missing-fork-branch", + }); + if (!(error.cause instanceof AggregateError)) { + return yield* Effect.die(error.cause); + } + expect(error.cause.errors).toHaveLength(2); + expect(error.cause.errors).toEqual([ + expect.objectContaining({ _tag: "GitCommandError" }), + expect.objectContaining({ _tag: "GitCommandError" }), + ]); + expect(error.cause.cause).toBe(error.cause.errors[0]); + }), + ); + it.effect("launches setup only when creating a new PR worktree", () => Effect.gen(function* () { const repoDir = yield* makeTempDir("t3code-git-manager-"); diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 46da2e6c1f9..f1fb03e7e45 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -41,7 +41,7 @@ import { type ChangeRequestTerminology, } from "@t3tools/shared/sourceControl"; -import { GitManagerError } from "@t3tools/contracts"; +import { GitManagerError, GitPullRequestMaterializationError } from "@t3tools/contracts"; import * as TextGeneration from "../textGeneration/TextGeneration.ts"; import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts"; import { extractBranchNameFromRemoteRef } from "./remoteRefs.ts"; @@ -631,9 +631,12 @@ export const make = Effect.gen(function* () { ) => configurePullRequestHeadUpstreamBase(cwd, pullRequest, localBranch).pipe( Effect.catch((error) => - Effect.logWarning( - `GitManager.configurePullRequestHeadUpstream: failed to configure upstream for ${localBranch} -> ${pullRequest.headBranch} in ${cwd}: ${error.message}`, - ).pipe(Effect.asVoid), + Effect.logWarning("GitManager.configurePullRequestHeadUpstream failed", { + cwd, + localBranch, + headBranch: pullRequest.headBranch, + cause: error, + }).pipe(Effect.asVoid), ), ); @@ -691,12 +694,30 @@ export const make = Effect.gen(function* () { localBranch = pullRequest.headBranch, ) => materializePullRequestHeadBranchBase(cwd, pullRequest, localBranch).pipe( - Effect.catch(() => - gitCore.fetchPullRequestBranch({ - cwd, - prNumber: pullRequest.number, - branch: localBranch, - }), + Effect.catch((primaryCause) => + gitCore + .fetchPullRequestBranch({ + cwd, + prNumber: pullRequest.number, + branch: localBranch, + }) + .pipe( + Effect.mapError( + (fallbackCause) => + new GitPullRequestMaterializationError({ + cwd, + pullRequestNumber: pullRequest.number, + headRepository: resolveHeadRepositoryNameWithOwner(pullRequest), + headBranch: pullRequest.headBranch, + localBranch, + cause: new AggregateError( + [primaryCause, fallbackCause], + `Repository-head and pull-request-ref fetches both failed for pull request #${pullRequest.number}.`, + { cause: primaryCause }, + ), + }), + ), + ), ), ); const fileSystem = yield* FileSystem.FileSystem; @@ -1452,9 +1473,11 @@ export const make = Effect.gen(function* () { }) .pipe( Effect.catch((error) => - Effect.logWarning( - `GitManager.preparePullRequestThread: failed to launch worktree setup script for thread ${input.threadId} in ${worktreePath}: ${error.message}`, - ).pipe(Effect.asVoid), + Effect.logWarning("GitManager.preparePullRequestThread setup script failed", { + threadId: input.threadId, + worktreePath, + cause: error, + }).pipe(Effect.asVoid), ), ); }; diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 0f1f09729be..aa5cdf8432b 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -361,8 +361,25 @@ export class GitManagerError extends Schema.TaggedErrorClass()( } } +export class GitPullRequestMaterializationError extends Schema.TaggedErrorClass()( + "GitPullRequestMaterializationError", + { + cwd: TrimmedNonEmptyStringSchema, + pullRequestNumber: PositiveInt, + headRepository: Schema.NullOr(TrimmedNonEmptyStringSchema), + headBranch: TrimmedNonEmptyStringSchema, + localBranch: TrimmedNonEmptyStringSchema, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to materialize pull request #${this.pullRequestNumber} branch ${this.headBranch} as ${this.localBranch}.`; + } +} + export const GitManagerServiceError = Schema.Union([ GitManagerError, + GitPullRequestMaterializationError, GitCommandError, SourceControlProviderError, TextGenerationError,