diff --git a/packages/source-control-gitlab/src/server/GitLabCli.ts b/packages/source-control-gitlab/src/server/GitLabCli.ts index 0234b6fcd9f5e..0368395a6fabd 100644 --- a/packages/source-control-gitlab/src/server/GitLabCli.ts +++ b/packages/source-control-gitlab/src/server/GitLabCli.ts @@ -267,6 +267,7 @@ export class GitLabCli extends Context.Service< { readonly execute: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly args: ReadonlyArray; readonly timeoutMs?: number; /** Keeps sensitive payloads out of argv. */ @@ -432,6 +433,8 @@ export const make = Effect.gen(function* () { command: "glab", args: input.args, cwd: input.cwd, + // glab may otherwise prefer a different checkout remote. + ...(input.host === undefined ? {} : { env: { GITLAB_HOST: input.host } }), classifyFailure: classifyGlabFailure, notFoundDetail: "Merge request not found.", timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS, diff --git a/packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts index 43f77e56c5598..f24c4ca314eb1 100644 --- a/packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestCli.ts @@ -224,10 +224,12 @@ export class GitLabPullRequestCli extends Context.Service< { readonly getViewerUsername: (input: { readonly cwd: string; + readonly host?: string | undefined; }) => Effect.Effect; readonly listMergeRequests: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly state: PullRequestListState; readonly involvement: PullRequestInvolvement; @@ -240,6 +242,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly getMergeRequestDetail: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect; @@ -247,6 +250,7 @@ export class GitLabPullRequestCli extends Context.Service< /** Line counts for the given merge requests; one with no answer is left out. */ readonly listMergeRequestStats: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly changeRequests: ReadonlyArray<{ readonly repository: string; readonly number: number; @@ -263,12 +267,14 @@ export class GitLabPullRequestCli extends Context.Service< readonly getMergeRequestFileStats: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect; readonly listNotes: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect< @@ -278,12 +284,14 @@ export class GitLabPullRequestCli extends Context.Service< readonly listCommits: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect, GitLabPullRequestCliError>; readonly getMergeRequestDiff: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly cursor?: string | undefined; @@ -292,6 +300,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly getMergeRequestDiffFileContents: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly commit?: string | undefined; @@ -305,11 +314,13 @@ export class GitLabPullRequestCli extends Context.Service< readonly getProjectMergeCapabilities: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; }) => Effect.Effect; readonly getFileRevisions: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly paths: ReadonlyArray; @@ -317,12 +328,14 @@ export class GitLabPullRequestCli extends Context.Service< readonly listReviewerCandidates: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect; readonly listProjectLabels: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly search?: string | undefined; }) => Effect.Effect< @@ -332,6 +345,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly listProjectUsers: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly search?: string | undefined; }) => Effect.Effect< @@ -341,6 +355,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly setReviewerRequest: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly reviewers: ReadonlyArray<{ readonly id: string }>; @@ -349,6 +364,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly runMergeRequestAction: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly action: PullRequestAction; @@ -357,6 +373,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly updateMergeRequest: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly title?: string | undefined; @@ -365,6 +382,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly commentOnMergeRequest: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly body: string; @@ -372,6 +390,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly updateNote: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly noteId: string; @@ -380,6 +399,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly listDiscussions: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect< @@ -389,6 +409,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly submitReview: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly verdict: PullRequestReviewVerdict; @@ -398,6 +419,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly replyToDiscussion: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly discussionId: string; @@ -406,6 +428,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly setDiscussionResolution: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly discussionId: string; @@ -414,6 +437,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly listReactions: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }) => Effect.Effect< @@ -426,6 +450,7 @@ export class GitLabPullRequestCli extends Context.Service< readonly setReaction: (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly noteId?: string | undefined; @@ -566,6 +591,7 @@ export const make = Effect.gen(function* () { const api = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly path: string; readonly method?: string; readonly stdin?: string; @@ -574,6 +600,7 @@ export const make = Effect.gen(function* () { }) => gitlab.execute({ cwd: input.cwd, + host: input.host, args: [ "api", input.path, @@ -590,6 +617,7 @@ export const make = Effect.gen(function* () { const listPage = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly state: PullRequestListState; readonly involvement: PullRequestInvolvement; @@ -611,6 +639,7 @@ export const make = Effect.gen(function* () { const lastPage = Math.floor((delivered + input.limit) / perPage) + 1; return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests?${query([ ["state", stateParam(input.state)], ...involvementParams(input), @@ -692,6 +721,7 @@ export const make = Effect.gen(function* () { const diffPage = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly page: number; @@ -699,6 +729,7 @@ export const make = Effect.gen(function* () { }): Effect.Effect => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/${ input.commit === undefined ? `merge_requests/${input.number}/diffs` @@ -724,6 +755,7 @@ export const make = Effect.gen(function* () { // GitLab before 15.7 has no /diffs endpoint. /changes returns one complete slice. return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/changes?access_raw_diffs=true`, maxOutputBytes: DIFF_MAX_OUTPUT_BYTES, timeoutMs: DIFF_TIMEOUT_MS, @@ -772,6 +804,7 @@ export const make = Effect.gen(function* () { const notesPage = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly page: number; @@ -782,6 +815,7 @@ export const make = Effect.gen(function* () { > => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/notes?${query( [ ["per_page", String(MAX_PAGE_SIZE)], @@ -815,6 +849,7 @@ export const make = Effect.gen(function* () { const discussionsPage = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly page: number; @@ -825,6 +860,7 @@ export const make = Effect.gen(function* () { > => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/discussions?${query( [ ["per_page", String(MAX_PAGE_SIZE)], @@ -857,11 +893,13 @@ export const make = Effect.gen(function* () { const getDiffRefs = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }): Effect.Effect => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}`, }).pipe( Effect.flatMap((result): Effect.Effect => { @@ -890,12 +928,14 @@ export const make = Effect.gen(function* () { const getCommitDiffRefs = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly commit: string; readonly allowRoot: boolean; }): Effect.Effect => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/repository/commits/${input.commit}`, }).pipe( Effect.flatMap((result): Effect.Effect => { @@ -930,11 +970,13 @@ export const make = Effect.gen(function* () { const mergeRequestDetail = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; }): Effect.Effect => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}?${query([ ["include_diverged_commits_count", "true"], ])}`, @@ -956,11 +998,13 @@ export const make = Effect.gen(function* () { const projectUsers = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly search?: string | undefined; }): Effect.Effect => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/users?${query([ ["per_page", String(MAX_PAGE_SIZE)], ...searchParams(input.search), @@ -994,6 +1038,7 @@ export const make = Effect.gen(function* () { const awardsPage = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly cursor: string | null; @@ -1011,6 +1056,7 @@ export const make = Effect.gen(function* () { > => api({ cwd: input.cwd, + host: input.host, path: "graphql", method: "POST", stdin: JSON.stringify({ @@ -1056,6 +1102,7 @@ export const make = Effect.gen(function* () { const graphqlRead = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly operation: string; readonly query: string; readonly variables: Record; @@ -1063,6 +1110,7 @@ export const make = Effect.gen(function* () { }) => api({ cwd: input.cwd, + host: input.host, path: "graphql", method: "POST", stdin: JSON.stringify({ query: input.query, variables: input.variables }), @@ -1084,11 +1132,13 @@ export const make = Effect.gen(function* () { const mergeRequestStats = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly numbers: ReadonlyArray; }) => graphqlRead({ cwd: input.cwd, + host: input.host, operation: "listMergeRequestStats", query: MERGE_REQUEST_STATS_GRAPHQL_QUERY, variables: { fullPath: input.repository, iids: input.numbers.map(String) }, @@ -1117,7 +1167,7 @@ export const make = Effect.gen(function* () { return Effect.forEach( reads, (read) => - mergeRequestStats({ cwd: input.cwd, ...read }).pipe( + mergeRequestStats({ cwd: input.cwd, host: input.host, ...read }).pipe( Effect.map((stats) => read.numbers.flatMap((number) => { const stat = stats.get(number); @@ -1147,12 +1197,14 @@ export const make = Effect.gen(function* () { const blobsAt = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly ref: string; readonly paths: ReadonlyArray; }): Effect.Effect | null, GitLabPullRequestCliError> => api({ cwd: input.cwd, + host: input.host, path: "graphql", method: "POST", stdin: JSON.stringify({ @@ -1179,6 +1231,7 @@ export const make = Effect.gen(function* () { const fileRevisions = (input: { readonly cwd: string; + readonly host?: string | undefined; readonly repository: string; readonly number: number; readonly paths: ReadonlyArray; @@ -1215,8 +1268,8 @@ export const make = Effect.gen(function* () { }), ); - const viewerUsername = (input: { readonly cwd: string }) => - api({ cwd: input.cwd, path: "user" }).pipe( + const viewerUsername = (input: { readonly cwd: string; readonly host?: string | undefined }) => + api({ cwd: input.cwd, host: input.host, path: "user" }).pipe( Effect.flatMap((result): Effect.Effect => { const decoded = decodeViewerJson(result.stdout.trim()); if (!Result.isSuccess(decoded)) { @@ -1251,6 +1304,7 @@ export const make = Effect.gen(function* () { getMergeRequestFileStats: Effect.fn("GitLabPullRequestCli.getMergeRequestFileStats")((input) => graphqlRead({ cwd: input.cwd, + host: input.host, operation: "getMergeRequestFileStats", query: MERGE_REQUEST_FILE_STATS_GRAPHQL_QUERY, variables: { fullPath: input.repository, iid: String(input.number) }, @@ -1273,6 +1327,7 @@ export const make = Effect.gen(function* () { if (input.reacted) { yield* api({ cwd: input.cwd, + host: input.host, path: `${subject}?${query([["name", gitLabAwardName(input.content)]])}`, method: "POST", }); @@ -1280,8 +1335,8 @@ export const make = Effect.gen(function* () { } // GitLab deletes an award by its id and takes no emoji name there, so the reader's own // award of that name is looked up first. - const viewer = yield* viewerUsername({ cwd: input.cwd }); - const listed = yield* api({ cwd: input.cwd, path: subject }); + const viewer = yield* viewerUsername(input); + const listed = yield* api({ cwd: input.cwd, host: input.host, path: subject }); const own = decodeOwnAwardIdJson(listed.stdout.trim(), { content: input.content, viewer, @@ -1297,6 +1352,7 @@ export const make = Effect.gen(function* () { if (own.success === null) return; yield* api({ cwd: input.cwd, + host: input.host, path: `${subject}/${own.success}`, method: "DELETE", }); @@ -1305,6 +1361,7 @@ export const make = Effect.gen(function* () { listCommits: Effect.fn("GitLabPullRequestCli.listCommits")(function (input) { return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/commits?${query( [ ["per_page", String(COMMIT_PAGE_SIZE)], @@ -1334,6 +1391,7 @@ export const make = Effect.gen(function* () { } const target = { cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, ...(input.commit === undefined ? {} : { commit: input.commit }), @@ -1357,6 +1415,7 @@ export const make = Effect.gen(function* () { ? getDiffRefs(input) : getCommitDiffRefs({ cwd: input.cwd, + host: input.host, repository: input.repository, commit: input.commit, allowRoot: input.changeType === "new", @@ -1365,6 +1424,7 @@ export const make = Effect.gen(function* () { const readFile = (revision: string, filePath: string) => api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/repository/files/${encodeURIComponent( filePath, )}/raw?ref=${encodeURIComponent(revision)}`, @@ -1403,6 +1463,7 @@ export const make = Effect.gen(function* () { function (input) { return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}?license=false`, }).pipe( Effect.flatMap((result) => { @@ -1425,6 +1486,7 @@ export const make = Effect.gen(function* () { listProjectLabels: Effect.fn("GitLabPullRequestCli.listProjectLabels")(function (input) { return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/labels?${query([ ["per_page", String(MAX_PAGE_SIZE)], ...searchParams(input.search), @@ -1493,6 +1555,7 @@ export const make = Effect.gen(function* () { } return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}`, method: "PUT", stdin: JSON.stringify({ reviewer_ids: [...ids] }), @@ -1510,6 +1573,7 @@ export const make = Effect.gen(function* () { if (input.action === "disable-auto-merge") { return yield* api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/cancel_merge_when_pipeline_succeeds`, method: "POST", }).pipe(Effect.asVoid); @@ -1517,6 +1581,7 @@ export const make = Effect.gen(function* () { const executeMr = (subcommand: string, ...flags: ReadonlyArray) => gitlab.execute({ cwd: input.cwd, + host: input.host, args: ["mr", subcommand, String(input.number), "--repo", input.repository, ...flags], }); const [subcommand, ...flags] = actionArgs(input.action, input.mergeMethod); @@ -1535,6 +1600,7 @@ export const make = Effect.gen(function* () { updateMergeRequest: Effect.fn("GitLabPullRequestCli.updateMergeRequest")(function (input) { return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}`, method: "PUT", // Omitted fields remain unchanged; empty values clear them. @@ -1549,6 +1615,7 @@ export const make = Effect.gen(function* () { function (input) { return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/notes`, method: "POST", // JSON avoids glab's coercion of numeric/boolean comment bodies. @@ -1560,6 +1627,7 @@ export const make = Effect.gen(function* () { updateNote: Effect.fn("GitLabPullRequestCli.updateNote")(function (input) { return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/notes/${encodeURIComponent( input.noteId, )}`, @@ -1586,6 +1654,7 @@ export const make = Effect.gen(function* () { (comment) => api({ cwd: input.cwd, + host: input.host, path: `${mergeRequest}/discussions`, method: "POST", stdin: JSON.stringify({ @@ -1610,6 +1679,7 @@ export const make = Effect.gen(function* () { if (input.body.trim().length > 0) { yield* api({ cwd: input.cwd, + host: input.host, path: `${mergeRequest}/notes`, method: "POST", // @effect-diagnostics-next-line preferSchemaOverJson:off @@ -1617,13 +1687,19 @@ export const make = Effect.gen(function* () { }); } if (input.verdict === "approve") { - yield* api({ cwd: input.cwd, path: `${mergeRequest}/approve`, method: "POST" }); + yield* api({ + cwd: input.cwd, + host: input.host, + path: `${mergeRequest}/approve`, + method: "POST", + }); } }), replyToDiscussion: Effect.fn("GitLabPullRequestCli.replyToDiscussion")(function (input) { return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/discussions/${encodeURIComponent( input.discussionId, )}/notes`, @@ -1636,6 +1712,7 @@ export const make = Effect.gen(function* () { function (input) { return api({ cwd: input.cwd, + host: input.host, path: `projects/${projectPath(input.repository)}/merge_requests/${input.number}/discussions/${encodeURIComponent( input.discussionId, )}`, diff --git a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts index a00569a1b6b20..d0b318f11485f 100644 --- a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.test.ts @@ -3,10 +3,123 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as TestClock from "effect/testing/TestClock"; +import * as TestSourceControlHost from "@t3tools/source-control-testing/TestSourceControlHost"; + import * as GitLabCli from "./GitLabCli.ts"; import * as GitLabPullRequestCli from "./GitLabPullRequestCli.ts"; import { gitLabViewerPermissions, make } from "./GitLabPullRequestProvider.ts"; +it.effect.each(["merge", "disable-auto-merge"] as const)( + "keeps reads and %s on the selected host when glab prefers another remote", + (action) => + Effect.gen(function* () { + const selectedHost = "gitlab.example.test:8443"; + const otherHost = "other.gitlab.example.test"; + const readHosts: string[] = []; + const mergedHosts: string[] = []; + const native = yield* GitLabCli.make.pipe( + Effect.provide( + TestSourceControlHost.layer({ + process: { + run: (input) => { + // glab prefers a remote named `gitlab` to `origin`. The checkout's + // selected repository must still own every request and mutation. + const host = input.env?.GITLAB_HOST ?? otherHost; + const path = input.args[1]; + const mutation = + input.args[0] === "mr" || (input.args.includes("POST") && path !== "graphql"); + if (mutation) mergedHosts.push(host); + else readHosts.push(host); + const response = mutation + ? {} + : path === "graphql" + ? { + data: { + project: { + mergeRequests: { + nodes: [ + { iid: "7", diffStatsSummary: { additions: 2, deletions: 1 } }, + ], + }, + mergeRequest: { + diffStatsSummary: { fileCount: 1 }, + diffStats: [{ path: "src/a.ts", additions: 2, deletions: 1 }], + }, + }, + }, + } + : path?.includes("merge_requests/") + ? { + iid: 7, + title: "Merge request 7", + web_url: `https://${host}/acme/web/-/merge_requests/7`, + source_branch: "feature", + target_branch: "main", + created_at: "2026-07-01T00:00:00Z", + updated_at: "2026-07-02T00:00:00Z", + user: { can_merge: true }, + diff_refs: { + head_sha: "a".repeat(40), + base_sha: "b".repeat(40), + start_sha: "b".repeat(40), + }, + } + : path === "user" + ? { username: "reviewer" } + : path?.includes("/labels?") || path?.includes("/users?") + ? [] + : { merge_method: "merge", squash_option: "default_off" }; + return Effect.succeed( + TestSourceControlHost.processOutput(JSON.stringify(response)), + ); + }, + }, + }), + ), + ); + const cli = yield* GitLabPullRequestCli.make.pipe( + Effect.provideService(GitLabCli.GitLabCli, native), + ); + const provider = yield* make.pipe( + Effect.provideService(GitLabPullRequestCli.GitLabPullRequestCli, cli), + ); + const reference = { + cwd: "/repo", + repository: "acme/web", + host: selectedHost, + number: 7, + }; + const detail = yield* provider.getChangeRequest(reference); + expect({ additions: detail.additions, deletions: detail.deletions }).toEqual({ + additions: 2, + deletions: 1, + }); + assert.isDefined(provider.getChangeRequestSummary); + expect((yield* provider.getChangeRequestSummary(reference)).additions).toBe(2); + assert.isDefined(provider.listChangeRequestStats); + expect( + yield* provider.listChangeRequestStats({ ...reference, changeRequests: [reference] }), + ).toEqual([{ repository: reference.repository, number: 7, additions: 2, deletions: 1 }]); + assert.isDefined(provider.getFileStats); + expect(yield* provider.getFileStats(reference)).toEqual({ + files: [{ path: "src/a.ts", additions: 2, deletions: 1 }], + truncated: false, + }); + expect(yield* provider.getViewer(reference)).toBe("reviewer"); + assert.isDefined(provider.listRepositoryLabels); + yield* provider.listRepositoryLabels(reference); + assert.isDefined(provider.searchRepositoryAuthors); + yield* provider.searchRepositoryAuthors(reference); + yield* provider.runAction({ ...reference, action }); + + expect(readHosts).toEqual(Array(action === "merge" ? 11 : 10).fill(selectedHost)); + expect({ url: detail.url, mutatedHosts: mergedHosts }).toEqual({ + url: `https://${selectedHost}/acme/web/-/merge_requests/7`, + mutatedHosts: [selectedHost], + }); + }), +); + describe("gitLabViewerPermissions", () => { it("offers everything to a viewer GitLab says can merge", () => { expect(gitLabViewerPermissions({ viewerCanMerge: true })).toEqual({ @@ -282,15 +395,19 @@ describe("merge request reads", () => { }), ); - it.effect("does not share reads between checkouts", () => - Effect.gen(function* () { - const { provider, calls } = yield* countingProvider; + it.effect.each([{ cwd: "/other" }, { host: "other.gitlab.test" }])( + "does not share reads with %j", + (other) => + Effect.gen(function* () { + const { provider, calls } = yield* countingProvider; - yield* provider.getChangeRequest(reference); - yield* provider.getChangeRequest({ ...reference, cwd: "/other" }); + yield* provider.getChangeRequest(reference); + yield* provider.getChangeRequest({ ...reference, ...other }); + yield* provider.getChangeRequest(reference); + yield* provider.getChangeRequest({ ...reference, ...other }); - expect(calls).toEqual({ detail: 2, mergeCapabilities: 2 }); - }), + expect(calls).toEqual({ detail: 2, mergeCapabilities: 2 }); + }), ); it.effect("reads a merge request again once a minute has passed", () => diff --git a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts index 7184ebaa3ca05..0e799bd4c2953 100644 --- a/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts +++ b/packages/source-control-gitlab/src/server/GitLabPullRequestProvider.ts @@ -102,8 +102,8 @@ export const make = Effect.gen(function* () { // checkout's remote and config, so two checkouts may read the same project as different people. const mergeRequestDetails = yield* Cache.makeWith( (key: string) => { - const [cwd, repository, number] = JSON.parse(key) as [string, string, number]; - return cli.getMergeRequestDetail({ cwd, repository, number }); + const [cwd, repository, number, host] = JSON.parse(key) as [string, string, number, string]; + return cli.getMergeRequestDetail({ cwd, repository, number, host }); }, { capacity: READ_CACHE_CAPACITY, @@ -112,8 +112,8 @@ export const make = Effect.gen(function* () { ); const projectMergeCapabilities = yield* Cache.makeWith( (key: string) => { - const [cwd, repository] = JSON.parse(key) as [string, string]; - return cli.getProjectMergeCapabilities({ cwd, repository }); + const [cwd, repository, host] = JSON.parse(key) as [string, string, string]; + return cli.getProjectMergeCapabilities({ cwd, repository, host }); }, { capacity: READ_CACHE_CAPACITY, @@ -123,11 +123,13 @@ export const make = Effect.gen(function* () { const detailKey = (input: { readonly cwd: string; + readonly host: string; readonly repository: string; readonly number: number; - }) => JSON.stringify([input.cwd, input.repository, input.number]); + }) => JSON.stringify([input.cwd, input.repository, input.number, input.host]); const mergeRequestDetail = (input: { readonly cwd: string; + readonly host: string; readonly repository: string; readonly number: number; }) => Cache.get(mergeRequestDetails, detailKey(input)); @@ -136,7 +138,12 @@ export const make = Effect.gen(function* () { // write is in flight must not outlive it. const writing = < - I extends { readonly cwd: string; readonly repository: string; readonly number: number }, + I extends { + readonly cwd: string; + readonly host: string; + readonly repository: string; + readonly number: number; + }, A, E, >( @@ -177,14 +184,21 @@ export const make = Effect.gen(function* () { const mergeRequestWithStats = ( input: { readonly cwd: string; + readonly host: string; readonly repository: string; readonly number: number; }, detail = cli.getMergeRequestDetail(input), ) => - Effect.all([detail, cli.listMergeRequestStats({ cwd: input.cwd, changeRequests: [input] })], { - concurrency: 2, - }).pipe( + Effect.all( + [ + detail, + cli.listMergeRequestStats({ cwd: input.cwd, host: input.host, changeRequests: [input] }), + ], + { + concurrency: 2, + }, + ).pipe( Effect.map(([mergeRequest, [stat]]) => stat === undefined ? mergeRequest @@ -197,13 +211,13 @@ export const make = Effect.gen(function* () { capabilities: CAPABILITIES, unavailableRequirement: UNAVAILABLE_REQUIREMENT, - getViewer: (input) => - cli.getViewerUsername({ cwd: input.cwd }).pipe(Effect.mapError(fail("getViewer"))), + getViewer: (input) => cli.getViewerUsername(input).pipe(Effect.mapError(fail("getViewer"))), listChangeRequests: (input) => cli .listMergeRequests({ cwd: input.cwd, + host: input.host, repository: input.repository, state: input.state, involvement: input.involvement, @@ -222,9 +236,7 @@ export const make = Effect.gen(function* () { cli.getMergeRequestFileStats(input).pipe(Effect.mapError(fail("getFileStats"))), listChangeRequestStats: (input) => - cli - .listMergeRequestStats({ cwd: input.cwd, changeRequests: input.changeRequests }) - .pipe(Effect.mapError(fail("listChangeRequestStats"))), + cli.listMergeRequestStats(input).pipe(Effect.mapError(fail("listChangeRequestStats"))), getChangeRequestChecks: (input) => mergeRequestDetail(input).pipe( @@ -243,7 +255,10 @@ export const make = Effect.gen(function* () { Effect.all( [ mergeRequestWithStats(input, mergeRequestDetail(input)), - Cache.get(projectMergeCapabilities, JSON.stringify([input.cwd, input.repository])), + Cache.get( + projectMergeCapabilities, + JSON.stringify([input.cwd, input.repository, input.host]), + ), ], { concurrency: 2 }, ).pipe( @@ -339,6 +354,7 @@ export const make = Effect.gen(function* () { cli .listReviewerCandidates({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, }) @@ -346,18 +362,29 @@ export const make = Effect.gen(function* () { listRepositoryLabels: (input) => cli - .listProjectLabels({ cwd: input.cwd, repository: input.repository, search: input.query }) + .listProjectLabels({ + cwd: input.cwd, + host: input.host, + repository: input.repository, + search: input.query, + }) .pipe(Effect.mapError(fail("listRepositoryLabels"))), searchRepositoryAuthors: (input) => cli - .listProjectUsers({ cwd: input.cwd, repository: input.repository, search: input.query }) + .listProjectUsers({ + cwd: input.cwd, + host: input.host, + repository: input.repository, + search: input.query, + }) .pipe(Effect.mapError(fail("searchRepositoryAuthors"))), setReviewerRequest: writing((input) => cli .setReviewerRequest({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, reviewers: input.reviewers, @@ -370,6 +397,7 @@ export const make = Effect.gen(function* () { cli .runMergeRequestAction({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, action: input.action, @@ -382,6 +410,7 @@ export const make = Effect.gen(function* () { cli .updateMergeRequest({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, ...(input.title === undefined ? {} : { title: input.title }), @@ -398,6 +427,7 @@ export const make = Effect.gen(function* () { cli .updateNote({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, noteId: input.commentId, @@ -414,6 +444,7 @@ export const make = Effect.gen(function* () { cli .replyToDiscussion({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, discussionId: input.threadId, @@ -426,6 +457,7 @@ export const make = Effect.gen(function* () { cli .setReaction({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, ...(input.subjectId === undefined ? {} : { noteId: input.subjectId }), @@ -439,6 +471,7 @@ export const make = Effect.gen(function* () { cli .setDiscussionResolution({ cwd: input.cwd, + host: input.host, repository: input.repository, number: input.number, discussionId: input.threadId,