| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Schema from "effect/Schema"; |
| 3 | import type { |
| 4 | PullRequestStackMembership, |
| 5 | PullRequestAction, |
| 6 | PullRequestStackHead, |
| 7 | PullRequestActor, |
| 8 | PullRequestBaseComparison, |
| 9 | PullRequestCapabilities, |
| 10 | PullRequestChecksState, |
| 11 | PullRequestCheck, |
| 12 | PullRequestChecks, |
| 13 | PullRequestComment, |
| 14 | PullRequestFileViewed, |
| 15 | PullRequestCommit, |
| 16 | PullRequestInvolvement, |
| 17 | PullRequestLabel, |
| 18 | PullRequestListFilters, |
| 19 | PullRequestListState, |
| 20 | PullRequestMergeCapabilities, |
| 21 | PullRequestMergeMethod, |
| 22 | PullRequestMergeability, |
| 23 | PullRequestOmittedFileStat, |
| 24 | PullRequestReaction, |
| 25 | PullRequestReactionContent, |
| 26 | PullRequestReviewCommentDraft, |
| 27 | PullRequestReviewDecision, |
| 28 | PullRequestReviewThread, |
| 29 | PullRequestThreadCommentsResult, |
| 30 | PullRequestReviewVerdict, |
| 31 | PullRequestReviewerCandidateList, |
| 32 | PullRequestReviewerKind, |
| 33 | PullRequestLabelCandidateList, |
| 34 | PullRequestState, |
| 35 | PullRequestPreview, |
| 36 | PullRequestUpdateMethod, |
| 37 | PullRequestViewerPermissions, |
| 38 | SourceControlProviderKind, |
| 39 | } from "@t3tools/contracts"; |
| 40 | import { SourceControlProviderKind as SourceControlProviderKindSchema } from "@t3tools/contracts"; |
| 41 | |
| 42 | /** |
| 43 | * The one failure shape every provider reports, so the service can decide what a failure means |
| 44 | * without knowing which CLI or API produced it. |
| 45 | * |
| 46 | * `reason` is the part the service acts on: a missing or unauthenticated tool disables the |
| 47 | * provider for the whole workspace, a rate limit pauses its host, and anything else is specific |
| 48 | * to the request. |
| 49 | */ |
| 50 | export class PullRequestProviderError extends Schema.TaggedError<PullRequestProviderError>()( |
| 51 | "PullRequestProviderError", |
| 52 | { |
| 53 | provider: SourceControlProviderKindSchema, |
| 54 | operation: Schema.String, |
| 55 | reason: Schema.Literals([ |
| 56 | "missing-tool", |
| 57 | "unauthenticated", |
| 58 | "rate-limited", |
| 59 | "not-found", |
| 60 | "failed", |
| 61 | ]), |
| 62 | detail: Schema.String, |
| 63 | retryAt: Schema.optional(Schema.Number), |
| 64 | cause: Schema.optional(Schema.Defect()), |
| 65 | }, |
| 66 | ) { |
| 67 | override get message(): string { |
| 68 | return `${this.provider} failed in ${this.operation}: ${this.detail}`; |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * `status` moves with the state, mergeability, head, or checks, which the detail read reports; |
| 74 | * `remarks` moves with comments and reviews, which only the activity read reports. |
| 75 | */ |
| 76 | export interface ProviderChangeRequestWatchFingerprint { |
| 77 | readonly status: string; |
| 78 | readonly remarks: string; |
| 79 | } |
| 80 | |
| 81 | export interface PullRequestProviderFailure { |
| 82 | readonly reason: PullRequestProviderError["reason"]; |
| 83 | readonly retryAt?: number | undefined; |
| 84 | } |
| 85 | |
| 86 | /** A change request as the provider sees it, before the service attaches project context. */ |
| 87 | export interface ProviderChangeRequest { |
| 88 | readonly stack?: PullRequestStackMembership; |
| 89 | readonly number: number; |
| 90 | readonly title: string; |
| 91 | readonly url: string; |
| 92 | readonly author: PullRequestActor | null; |
| 93 | readonly headBranch: string; |
| 94 | readonly headRepositoryNameWithOwner?: string | null; |
| 95 | readonly baseBranch: string; |
| 96 | readonly state: PullRequestState; |
| 97 | readonly isDraft: boolean; |
| 98 | readonly mergeability: PullRequestMergeability; |
| 99 | readonly additions: number; |
| 100 | readonly deletions: number; |
| 101 | readonly createdAt: string; |
| 102 | readonly closedAt?: string | null; |
| 103 | readonly mergedAt?: string | null; |
| 104 | readonly updatedAt: string; |
| 105 | /** Accounts with a review requested. Team-level requests are excluded by each provider. */ |
| 106 | readonly reviewRequestLogins: ReadonlyArray<string>; |
| 107 | readonly labels: ReadonlyArray<PullRequestLabel>; |
| 108 | /** Absent from a host that does not summarise its reviews, which is every host but GitHub. */ |
| 109 | readonly reviewDecision?: PullRequestReviewDecision | null | undefined; |
| 110 | /** Absent from a host that reports no check rollup on its listings. */ |
| 111 | readonly checksState?: PullRequestChecksState | null | undefined; |
| 112 | } |
| 113 | |
| 114 | /** The fields needed to keep a linked thread's pull request status live. */ |
| 115 | export interface ProviderChangeRequestSummary { |
| 116 | readonly number: number; |
| 117 | readonly title: string; |
| 118 | readonly url: string; |
| 119 | readonly headBranch: string; |
| 120 | readonly baseBranch: string; |
| 121 | readonly state: PullRequestState; |
| 122 | /** Present when the host says an open pull request is still a draft. */ |
| 123 | readonly isDraft?: boolean; |
| 124 | readonly closedAt?: string | null; |
| 125 | readonly mergedAt?: string | null; |
| 126 | readonly updatedAt: string; |
| 127 | /** Overview fields, present where the host's single read returns them at no extra cost. */ |
| 128 | readonly author?: PullRequestActor | null | undefined; |
| 129 | readonly additions?: number | undefined; |
| 130 | readonly deletions?: number | undefined; |
| 131 | readonly changedFiles?: number | undefined; |
| 132 | readonly reviewDecision?: PullRequestReviewDecision | null | undefined; |
| 133 | readonly checksState?: PullRequestChecksState | null | undefined; |
| 134 | readonly mergeability?: PullRequestMergeability | undefined; |
| 135 | /** |
| 136 | * The host-native stack the pull request sits in, from the same read. Null when the host says |
| 137 | * it is in none; absent when the read did not ask. |
| 138 | */ |
| 139 | readonly stack?: PullRequestStackMembership | null | undefined; |
| 140 | } |
| 141 | |
| 142 | /** One layer of a host-native stack, bottom to top order is the array's. */ |
| 143 | export interface ProviderChangeRequestStackLayer { |
| 144 | readonly title?: string; |
| 145 | readonly isDraft?: boolean; |
| 146 | readonly headSha?: string; |
| 147 | readonly number: number; |
| 148 | readonly headBranch: string; |
| 149 | readonly state: PullRequestState; |
| 150 | } |
| 151 | |
| 152 | /** |
| 153 | * A host-native stack: an ordered set of change requests the host itself merges and retargets as |
| 154 | * a unit. Only GitHub offers one today; the neutral shape lets the sync reactor and the UI stay |
| 155 | * ignorant of which host said so. |
| 156 | */ |
| 157 | export interface ProviderChangeRequestStack { |
| 158 | readonly id: string; |
| 159 | readonly number: number; |
| 160 | readonly url: string; |
| 161 | readonly base: string; |
| 162 | readonly layers: ReadonlyArray<ProviderChangeRequestStackLayer>; |
| 163 | } |
| 164 | |
| 165 | export interface ProviderChangeRequestPage { |
| 166 | readonly items: ReadonlyArray<ProviderChangeRequest>; |
| 167 | /** True when the host has more rows than the page size asked for. */ |
| 168 | readonly truncated: boolean; |
| 169 | /** |
| 170 | * Optional count-based cursor advance. Most hosts advance by the rows delivered after local |
| 171 | * de-duplication; an offset-paged host may need to count malformed raw rows it consumed too. |
| 172 | */ |
| 173 | readonly cursorAdvance?: number; |
| 174 | /** |
| 175 | * This page can be carried on from, so the service may hand the caller a cursor for it. False |
| 176 | * where the host answered in an order a cursor means nothing in, which leaves a larger `limit` |
| 177 | * as the only way to the rest — what every listing did before there were cursors. |
| 178 | */ |
| 179 | readonly continues: boolean; |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * Where a repository's next slice starts, as the provider that has to ask for it needs it. Built |
| 184 | * by the service out of the slice it just handed over, so the boundary that decides whether a row |
| 185 | * arrives twice or not at all is decided in one place rather than in four. |
| 186 | */ |
| 187 | export interface ProviderListCursor { |
| 188 | /** |
| 189 | * The instant of the oldest row already handed over, checked against a timestamp's shape before |
| 190 | * it gets here because it goes into a host's own filter. Asked for inclusively: several rows |
| 191 | * share one instant often enough — a bot that touches eight change requests writes one timestamp |
| 192 | * on all eight — and asking for strictly older would lose whichever of them the slice ended |
| 193 | * before. The service drops the ones it has already sent. |
| 194 | */ |
| 195 | readonly updatedBefore: string; |
| 196 | /** |
| 197 | * How many provider rows this repository has consumed so far, for a host that carries on by |
| 198 | * counting rather than by date. Usually this is the number handed over; malformed raw rows may |
| 199 | * count too when the provider reports a `cursorAdvance`. |
| 200 | */ |
| 201 | readonly delivered: number; |
| 202 | } |
| 203 | |
| 204 | /** One repository's row inside an answer that spans several of them. */ |
| 205 | export interface ProviderBatchedChangeRequest extends ProviderChangeRequest { |
| 206 | /** Provider-native identity, exactly as it was asked for, so the caller can file the row. */ |
| 207 | readonly repository: string; |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * One slice of a host read across several repositories at once, newest update first across all |
| 212 | * of them. There is no per-repository page here because the host was asked one question: the |
| 213 | * caller splits the rows by `repository` and works out where each of them carries on from the |
| 214 | * oldest row in the slice, which every repository the slice covers is now read up to. |
| 215 | */ |
| 216 | export interface ProviderBatchedChangeRequestPage { |
| 217 | readonly items: ReadonlyArray<ProviderBatchedChangeRequest>; |
| 218 | /** True when the host has more rows than the slice asked for, for any of the repositories. */ |
| 219 | readonly truncated: boolean; |
| 220 | } |
| 221 | |
| 222 | /** The line counts for one change request, which a listing may leave for a second read. */ |
| 223 | export interface ProviderChangeRequestStat { |
| 224 | readonly repository: string; |
| 225 | readonly number: number; |
| 226 | readonly additions: number; |
| 227 | readonly deletions: number; |
| 228 | } |
| 229 | |
| 230 | export interface ProviderChangeRequestDetail extends ProviderChangeRequest { |
| 231 | /** The head commit, where the host's detail read reports it. */ |
| 232 | readonly headSha?: string | null; |
| 233 | readonly body: string; |
| 234 | readonly changedFiles: number; |
| 235 | readonly mergedAt: string | null; |
| 236 | readonly closedAt: string | null; |
| 237 | readonly reviewers: ReadonlyArray<PullRequestActor>; |
| 238 | readonly checks: ReadonlyArray<PullRequestCheck>; |
| 239 | readonly mergeCapabilities: PullRequestMergeCapabilities; |
| 240 | readonly viewerPermissions: PullRequestViewerPermissions; |
| 241 | /** Absent from a host that cannot compare the branch with its base, which is most of them. */ |
| 242 | readonly baseComparison?: PullRequestBaseComparison; |
| 243 | readonly behindBy?: number; |
| 244 | /** Absent from a host that does not report whether it is armed to merge this on its own. */ |
| 245 | readonly autoMergeEnabled?: boolean; |
| 246 | /** The strategy stored with an armed auto-merge, where the host reports it. */ |
| 247 | readonly autoMergeMethod?: PullRequestMergeMethod; |
| 248 | /** Workflow runs on this head commit that still need a maintainer's approval. */ |
| 249 | readonly workflowApprovalsRequired?: number; |
| 250 | } |
| 251 | |
| 252 | /** The conversation-shaped half of a detail, loaded after the core can already render. */ |
| 253 | export interface ProviderChangeRequestActivity { |
| 254 | /** An optional richer actor, e.g. after GitHub's GraphQL read supplies an avatar. */ |
| 255 | readonly author?: PullRequestActor | null; |
| 256 | /** Optional because most hosts already report their reviewer list in the core detail. */ |
| 257 | readonly reviewers?: ReadonlyArray<PullRequestActor>; |
| 258 | readonly comments: ReadonlyArray<PullRequestComment>; |
| 259 | /** |
| 260 | * The host's own count of the conversation, which a bounded read can fall short of. A host |
| 261 | * that reports no count of its own answers with what it handed over, which is the same number |
| 262 | * once the read went to the end. |
| 263 | */ |
| 264 | readonly commentCount: number; |
| 265 | readonly commentsTruncated: boolean; |
| 266 | readonly reviewThreadsTruncated?: boolean; |
| 267 | readonly reviewThreads: ReadonlyArray<PullRequestReviewThread>; |
| 268 | readonly commits: ReadonlyArray<PullRequestCommit>; |
| 269 | /** The change request's own reactions, from a host that has them. */ |
| 270 | readonly reactions?: ReadonlyArray<PullRequestReaction>; |
| 271 | } |
| 272 | |
| 273 | export interface ProviderDiffSlice { |
| 274 | readonly patch: string; |
| 275 | /** Something in this slice could not be shown, as opposed to there being more slices. */ |
| 276 | readonly truncated: boolean; |
| 277 | readonly nextCursor: string | null; |
| 278 | /** The host's own counts for the files whose hunks it withheld from this slice. */ |
| 279 | readonly omittedFileStats?: ReadonlyArray<PullRequestOmittedFileStat>; |
| 280 | } |
| 281 | |
| 282 | export interface ProviderDiffFileContents { |
| 283 | readonly oldContents: string; |
| 284 | readonly newContents: string; |
| 285 | } |
| 286 | |
| 287 | export interface ProviderFilesViewed { |
| 288 | readonly files: ReadonlyArray<PullRequestFileViewed>; |
| 289 | /** The host has more files than were read, so the ones missing here are not "unviewed". */ |
| 290 | readonly truncated: boolean; |
| 291 | } |
| 292 | |
| 293 | /** |
| 294 | * What version each of the asked-for files is at, on the change request's head. Opaque strings, |
| 295 | * compared only against one another, where the empty string is the answer for a file the change |
| 296 | * request deletes rather than a gap. A path is absent only where the read could not say, so a |
| 297 | * provider converts its host's own absences on the way here. |
| 298 | * |
| 299 | * The whole path a version travels, and the three senses of null along it, are in |
| 300 | * `docs/internals/pull-request-file-revisions.md`. |
| 301 | */ |
| 302 | export interface ProviderFileRevisions { |
| 303 | readonly revisions: ReadonlyMap<string, string>; |
| 304 | /** |
| 305 | * Whether these are every file the change request carries rather than only the paths asked |
| 306 | * about. A host with no per-file version reads the whole change to answer for one file, and |
| 307 | * saying so is what keeps the next tick, naming a path nothing asked about before, from making |
| 308 | * it read the whole change again. False for a read cut short, which cannot speak past the cut. |
| 309 | */ |
| 310 | readonly complete?: boolean; |
| 311 | } |
| 312 | |
| 313 | export interface ProviderRepositoryRef { |
| 314 | readonly cwd: string; |
| 315 | /** Provider-native repository identity, e.g. `owner/repo` or `group/subgroup/project`. */ |
| 316 | readonly repository: string; |
| 317 | /** |
| 318 | * The host it lives on, which `repository` deliberately leaves out — the same `owner/repo` |
| 319 | * exists on github.com and on a GitHub Enterprise install, and only the caller knows which |
| 320 | * one a project's remote points at. |
| 321 | */ |
| 322 | readonly host: string; |
| 323 | } |
| 324 | |
| 325 | /** |
| 326 | * One host's change requests. Implementations own their own tool and JSON shapes and hand back |
| 327 | * the neutral types above; anything a host cannot do is declared in `capabilities` rather than |
| 328 | * failing at call time. |
| 329 | */ |
| 330 | export interface PullRequestProviderApi { |
| 331 | readonly withVerifiedCredential?: <A, E, R>( |
| 332 | input: { readonly cwd: string; readonly host: string }, |
| 333 | use: (identity: { |
| 334 | readonly accountId: string; |
| 335 | readonly viewer: string; |
| 336 | readonly credentialFingerprint: string; |
| 337 | }) => Effect.Effect<A, E, R>, |
| 338 | ) => Effect.Effect<A, E | PullRequestProviderError, R>; |
| 339 | readonly getRoutingIdentity?: (input: { |
| 340 | readonly cwd: string; |
| 341 | readonly host: string; |
| 342 | }) => Effect.Effect< |
| 343 | { readonly accountId: string; readonly viewer: string }, |
| 344 | PullRequestProviderError |
| 345 | >; |
| 346 | readonly kind: SourceControlProviderKind; |
| 347 | readonly capabilities: PullRequestCapabilities; |
| 348 | /** |
| 349 | * Rewrites the message a merge will use, for a host that lets the merge carry custom text. |
| 350 | * Absent means the host always writes its own message, so nothing is read to decide on one. |
| 351 | * Today's only rewrite strips agent credits (`mergeMessage.removeAgentCredits`) when the |
| 352 | * project asks for it. |
| 353 | */ |
| 354 | readonly mergeMessageRewrite?: (message: string) => string; |
| 355 | /** |
| 356 | * The key a checkout's repository is known by on this host, where `owner/name` is not enough. |
| 357 | * Absent means `owner/name` on the host identifies it, and any checkout on the host can serve a |
| 358 | * change request in another repository there. |
| 359 | * |
| 360 | * A host that resolves it (Azure DevOps: `dev.azure.com/org/project/_git/repo`) derives its |
| 361 | * organization from the checkout, so only a checkout whose key matches the reference serves |
| 362 | * it. The key also names the repository in list cursors and on routing. |
| 363 | */ |
| 364 | readonly repositoryKey?: (input: { |
| 365 | /** The checkout's canonical key, as its remote names it. */ |
| 366 | readonly canonicalKey: string; |
| 367 | }) => string; |
| 368 | |
| 369 | /** The signed-in account, which is what involvement filtering compares against. */ |
| 370 | readonly getViewer: (input: { |
| 371 | readonly cwd: string; |
| 372 | readonly host?: string; |
| 373 | }) => Effect.Effect<string, PullRequestProviderError>; |
| 374 | |
| 375 | readonly listChangeRequests: ( |
| 376 | input: ProviderRepositoryRef & { |
| 377 | readonly state: PullRequestListState; |
| 378 | readonly involvement: PullRequestInvolvement; |
| 379 | readonly viewer: string; |
| 380 | readonly limit: number; |
| 381 | /** |
| 382 | * Free text to narrow the listing by, as the host understands it. A host with no text |
| 383 | * filter of its own ignores it and answers with the page it would have answered with |
| 384 | * anyway — the caller narrows what it gets, so an unfiltered page is a wider answer |
| 385 | * rather than a wrong one. |
| 386 | */ |
| 387 | readonly query?: string | undefined; |
| 388 | /** |
| 389 | * Where to carry on from, rather than reading this repository from its newest row. Absent |
| 390 | * asks for the first slice, which is every listing that has not been continued. |
| 391 | */ |
| 392 | readonly cursor?: ProviderListCursor | undefined; |
| 393 | /** |
| 394 | * Further narrowings, which a host applies as far as it can and ignores the rest of — |
| 395 | * an unnarrowed page is a wider answer rather than a wrong one, and the caller narrows |
| 396 | * what it gets for the fields a row carries. |
| 397 | */ |
| 398 | readonly filters?: PullRequestListFilters | undefined; |
| 399 | }, |
| 400 | ) => Effect.Effect<ProviderChangeRequestPage, PullRequestProviderError>; |
| 401 | |
| 402 | /** |
| 403 | * The same listing for a whole host in one request, for a host that has a search across |
| 404 | * repositories. Optional: three of the four hosts here have no such API, and breaking the port |
| 405 | * for them to spare GitHub a fan-out would be paying for the fix with everyone else's clarity. |
| 406 | * The caller falls back to `listChangeRequests` per repository where this is absent, and where |
| 407 | * it fails. |
| 408 | * |
| 409 | * `limit` is the whole slice rather than a size per repository, because that is the shape of |
| 410 | * the answer: the newest `limit` rows across every repository named, which is exactly the rows |
| 411 | * a page ordered by update shows. |
| 412 | * |
| 413 | * `cursor` is one boundary for all of them, so a caller with repositories standing at different |
| 414 | * boundaries asks in groups rather than in one call. |
| 415 | */ |
| 416 | readonly listChangeRequestsAcross?: (input: { |
| 417 | /** Any checkout on the host, which is what the tool is run in. */ |
| 418 | readonly cwd: string; |
| 419 | readonly host: string; |
| 420 | readonly repositories: ReadonlyArray<string>; |
| 421 | readonly state: PullRequestListState; |
| 422 | readonly involvement: PullRequestInvolvement; |
| 423 | readonly viewer: string; |
| 424 | readonly limit: number; |
| 425 | readonly query?: string | undefined; |
| 426 | readonly cursor?: ProviderListCursor | undefined; |
| 427 | readonly filters?: PullRequestListFilters | undefined; |
| 428 | }) => Effect.Effect<ProviderBatchedChangeRequestPage, PullRequestProviderError>; |
| 429 | |
| 430 | /** |
| 431 | * The line counts for rows a listing has already handed over. Only implemented by a provider |
| 432 | * whose listing leaves them out — for everyone else the numbers arrived with the row, and the |
| 433 | * caller has nothing to ask for. |
| 434 | */ |
| 435 | readonly listChangeRequestStats?: (input: { |
| 436 | readonly cwd: string; |
| 437 | readonly host: string; |
| 438 | readonly changeRequests: ReadonlyArray<{ |
| 439 | readonly repository: string; |
| 440 | readonly number: number; |
| 441 | }>; |
| 442 | }) => Effect.Effect<ReadonlyArray<ProviderChangeRequestStat>, PullRequestProviderError>; |
| 443 | |
| 444 | readonly getChangeRequestChecks?: ( |
| 445 | input: ProviderRepositoryRef & { readonly number: number }, |
| 446 | ) => Effect.Effect<PullRequestChecks, PullRequestProviderError>; |
| 447 | |
| 448 | readonly getChangeRequest: ( |
| 449 | input: ProviderRepositoryRef & { readonly number: number }, |
| 450 | ) => Effect.Effect<ProviderChangeRequestDetail, PullRequestProviderError>; |
| 451 | |
| 452 | /** Hosts without a narrow read use their existing detail response for hover cards. */ |
| 453 | readonly getChangeRequestPreview?: ( |
| 454 | input: ProviderRepositoryRef & { readonly number: number }, |
| 455 | ) => Effect.Effect< |
| 456 | Omit<PullRequestPreview, "projectId" | "repository">, |
| 457 | PullRequestProviderError |
| 458 | >; |
| 459 | |
| 460 | /** |
| 461 | * The cheap live fields used by linked threads. Optional because a provider without a narrow |
| 462 | * endpoint can fall back to its full detail read at the service boundary. |
| 463 | */ |
| 464 | readonly getChangeRequestSummary?: ( |
| 465 | input: ProviderRepositoryRef & { readonly number: number }, |
| 466 | ) => Effect.Effect<ProviderChangeRequestSummary, PullRequestProviderError>; |
| 467 | |
| 468 | /** |
| 469 | * A cheap fingerprint of what a pull request watch reports, so it reads the change request in |
| 470 | * full only when this moves. Null when the host gave no answer for it. Optional: a host |
| 471 | * without one has its watched change requests read in full on every pass. |
| 472 | */ |
| 473 | readonly getChangeRequestWatchFingerprint?: ( |
| 474 | input: ProviderRepositoryRef & { readonly number: number }, |
| 475 | ) => Effect.Effect<ProviderChangeRequestWatchFingerprint | null, PullRequestProviderError>; |
| 476 | |
| 477 | /** |
| 478 | * The host-native stack a change request belongs to, or null when it is not stacked. Optional |
| 479 | * because most hosts have no such object; the service derives chains from base branches there. |
| 480 | */ |
| 481 | readonly getChangeRequestStack?: ( |
| 482 | input: ProviderRepositoryRef & { readonly includeDetails?: boolean; readonly number: number }, |
| 483 | ) => Effect.Effect<ProviderChangeRequestStack | null, PullRequestProviderError>; |
| 484 | |
| 485 | /** Comments, line threads, and commits, kept off the critical path for the core detail. */ |
| 486 | readonly getChangeRequestActivity: ( |
| 487 | input: ProviderRepositoryRef & { readonly number: number }, |
| 488 | ) => Effect.Effect<ProviderChangeRequestActivity, PullRequestProviderError>; |
| 489 | |
| 490 | /** One explicit page after a reader asks to continue an unfinished review thread. */ |
| 491 | readonly getReviewThreadComments?: ( |
| 492 | input: ProviderRepositoryRef & { |
| 493 | readonly number: number; |
| 494 | readonly threadId: string; |
| 495 | readonly cursor: string; |
| 496 | }, |
| 497 | ) => Effect.Effect<PullRequestThreadCommentsResult, PullRequestProviderError>; |
| 498 | |
| 499 | /** |
| 500 | * The same answer `getChangeRequest` carries, on its own. Asked before anything is written, so |
| 501 | * a request that reached the server without going past the page is refused by what the host |
| 502 | * says rather than by what the client claimed — and asked freshly, because access granted or |
| 503 | * taken away since the page loaded is exactly the case this guards. |
| 504 | * |
| 505 | * Implementations read the cheapest thing that answers it, which for a host with nothing to say |
| 506 | * is no request at all. |
| 507 | */ |
| 508 | readonly getViewerPermissions: ( |
| 509 | input: ProviderRepositoryRef & { |
| 510 | readonly number: number; |
| 511 | /** Skip branch comparison when checking permission for an unrelated operation. */ |
| 512 | readonly includeUpdateBranch?: boolean; |
| 513 | }, |
| 514 | ) => Effect.Effect<PullRequestViewerPermissions, PullRequestProviderError>; |
| 515 | |
| 516 | /** |
| 517 | * One slice of the patch. Only called when `capabilities.diff` is true. A provider that can |
| 518 | * serve the whole diff at once answers with `nextCursor: null` and is done; one that pages |
| 519 | * hands back whatever it needs to find the next slice. |
| 520 | */ |
| 521 | readonly getDiff: ( |
| 522 | input: ProviderRepositoryRef & { |
| 523 | readonly number: number; |
| 524 | readonly cursor?: string | undefined; |
| 525 | /** One commit's own changes, rather than everything the change request carries. */ |
| 526 | readonly commit?: string | undefined; |
| 527 | }, |
| 528 | ) => Effect.Effect<ProviderDiffSlice, PullRequestProviderError>; |
| 529 | |
| 530 | /** |
| 531 | * Full files at the exact revisions the host used for its patch. Optional where the provider |
| 532 | * exposes no diff at all; the service refuses expansion there just as it refuses the patch. |
| 533 | */ |
| 534 | readonly getDiffFileContents?: ( |
| 535 | input: ProviderRepositoryRef & { |
| 536 | readonly number: number; |
| 537 | readonly commit?: string | undefined; |
| 538 | readonly changeType: "change" | "rename-pure" | "rename-changed" | "new" | "deleted"; |
| 539 | readonly oldPath: string; |
| 540 | readonly newPath: string; |
| 541 | }, |
| 542 | ) => Effect.Effect<ProviderDiffFileContents, PullRequestProviderError>; |
| 543 | |
| 544 | /** |
| 545 | * Which files the reader has already cleared. Only called when `capabilities.viewedFiles` is |
| 546 | * `"host"`, and read apart from the patch: this moves with every press rather than every push. |
| 547 | */ |
| 548 | readonly getFilesViewed?: ( |
| 549 | input: ProviderRepositoryRef & { readonly number: number }, |
| 550 | ) => Effect.Effect<ProviderFilesViewed, PullRequestProviderError>; |
| 551 | |
| 552 | /** |
| 553 | * Clears files, or puts them back. Only called when `capabilities.viewedFiles` is `"host"`. |
| 554 | * A host with no bulk form is still owed one round trip for the batch rather than one per file, |
| 555 | * since the point of gathering presses is that the host is asked once. |
| 556 | */ |
| 557 | readonly setFilesViewed?: ( |
| 558 | input: ProviderRepositoryRef & { |
| 559 | readonly number: number; |
| 560 | readonly files: ReadonlyArray<{ readonly path: string; readonly viewed: boolean }>; |
| 561 | }, |
| 562 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 563 | |
| 564 | /** |
| 565 | * What version the head has of each of these files. Required of a host whose |
| 566 | * `capabilities.viewedFiles` is `"environment"`, and unused by one that keeps the marks itself: |
| 567 | * the marks live here, but only the host can say whether what a reader cleared last week is |
| 568 | * still what is in front of them. Asked for the marked paths alone, so the cost follows how |
| 569 | * much of the change request has been read rather than how large it is. |
| 570 | */ |
| 571 | readonly getFileRevisions?: ( |
| 572 | input: ProviderRepositoryRef & { |
| 573 | readonly number: number; |
| 574 | readonly paths: ReadonlyArray<string>; |
| 575 | }, |
| 576 | ) => Effect.Effect<ProviderFileRevisions, PullRequestProviderError>; |
| 577 | |
| 578 | readonly runAction: ( |
| 579 | input: ProviderRepositoryRef & { |
| 580 | readonly number: number; |
| 581 | readonly action: PullRequestAction; |
| 582 | readonly stackNumber?: number; |
| 583 | readonly expectedStackHeads?: ReadonlyArray<PullRequestStackHead>; |
| 584 | /** Apply `mergeMessageRewrite` to the merge message; never sent to a host without one. */ |
| 585 | readonly removeAgentCreditsOnMerge?: boolean; |
| 586 | /** Meaningful for `merge` and `enable-auto-merge`; absent takes the host's own default. */ |
| 587 | readonly mergeMethod?: PullRequestMergeMethod; |
| 588 | /** Only meaningful for `update-branch`; absent takes the host's own default. */ |
| 589 | readonly updateMethod?: PullRequestUpdateMethod; |
| 590 | }, |
| 591 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 592 | |
| 593 | /** |
| 594 | * Rewrites the change request's own words. Only called when `capabilities.edit.changeRequest` |
| 595 | * is true, and never with both fields absent — the caller refuses that before it gets here, |
| 596 | * because a host asked to change nothing answers differently on each of them. |
| 597 | */ |
| 598 | readonly updateChangeRequest?: ( |
| 599 | input: ProviderRepositoryRef & { |
| 600 | readonly number: number; |
| 601 | readonly title?: string | undefined; |
| 602 | readonly body?: string | undefined; |
| 603 | }, |
| 604 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 605 | |
| 606 | readonly comment: ( |
| 607 | input: ProviderRepositoryRef & { readonly number: number; readonly body: string }, |
| 608 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 609 | |
| 610 | /** |
| 611 | * Rewrites a remark somebody already posted. Only called when `capabilities.edit.comment` is |
| 612 | * true, with an id exactly as the conversation carried it. |
| 613 | * |
| 614 | * Whether this remark is the reader's to rewrite is the host's own answer: no read here can |
| 615 | * settle it, since access can be taken away between the conversation being read and the |
| 616 | * rewrite being sent, and a host refuses a stranger's remark with a sentence saying so. |
| 617 | */ |
| 618 | readonly updateComment?: ( |
| 619 | input: ProviderRepositoryRef & { |
| 620 | readonly number: number; |
| 621 | readonly commentId: string; |
| 622 | readonly kind: "issue-comment" | "review-comment"; |
| 623 | readonly body: string; |
| 624 | }, |
| 625 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 626 | |
| 627 | /** |
| 628 | * Sends a whole review at once. Only called for a verdict the host declared in |
| 629 | * `capabilities.review.verdicts`, and with line comments only where it declared |
| 630 | * `inlineComment`. |
| 631 | */ |
| 632 | readonly submitReview: ( |
| 633 | input: ProviderRepositoryRef & { |
| 634 | readonly number: number; |
| 635 | readonly verdict: PullRequestReviewVerdict; |
| 636 | readonly body: string; |
| 637 | readonly comments: ReadonlyArray<PullRequestReviewCommentDraft>; |
| 638 | }, |
| 639 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 640 | |
| 641 | /** |
| 642 | * The people this viewer may ask for a review, with whoever has already been asked marked as |
| 643 | * such. Only called when `capabilities.reviewers.listCandidates` is true. |
| 644 | * |
| 645 | * The author is left out by each provider rather than by the caller, because only the provider |
| 646 | * knows how the host spells the same person in a candidate list and on a pull request. |
| 647 | */ |
| 648 | readonly listReviewerCandidates: ( |
| 649 | input: ProviderRepositoryRef & { readonly number: number }, |
| 650 | ) => Effect.Effect<PullRequestReviewerCandidateList, PullRequestProviderError>; |
| 651 | |
| 652 | /** |
| 653 | * Asks for a review, or takes the request back. Only called when |
| 654 | * `capabilities.reviewers.request` is true. |
| 655 | * |
| 656 | * One call for both directions, because that is what every host does with them: GitHub posts and |
| 657 | * deletes the same collection, and GitLab and Bitbucket write the whole reviewer set either way. |
| 658 | * Asking again somebody who has already reviewed is a request like any other — which is how a |
| 659 | * re-request is made. |
| 660 | */ |
| 661 | readonly setReviewerRequest: ( |
| 662 | input: ProviderRepositoryRef & { |
| 663 | readonly number: number; |
| 664 | readonly reviewers: ReadonlyArray<{ |
| 665 | readonly id: string; |
| 666 | readonly kind: PullRequestReviewerKind; |
| 667 | }>; |
| 668 | readonly requested: boolean; |
| 669 | }, |
| 670 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 671 | |
| 672 | /** |
| 673 | * The repository's labels, with the ones already on the change request marked. Present with |
| 674 | * `setLabels` only where `capabilities.labels` is true; the service refuses both without it. |
| 675 | */ |
| 676 | readonly listLabelCandidates?: ( |
| 677 | input: ProviderRepositoryRef & { readonly number: number }, |
| 678 | ) => Effect.Effect<PullRequestLabelCandidateList, PullRequestProviderError>; |
| 679 | |
| 680 | /** Puts labels on the change request, or takes them off. One call for both directions. */ |
| 681 | readonly setLabels?: ( |
| 682 | input: ProviderRepositoryRef & { |
| 683 | readonly number: number; |
| 684 | readonly labels: ReadonlyArray<string>; |
| 685 | readonly applied: boolean; |
| 686 | }, |
| 687 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 688 | |
| 689 | /** Only called when `capabilities.review.reply` is true. */ |
| 690 | readonly replyToThread: ( |
| 691 | input: ProviderRepositoryRef & { |
| 692 | readonly number: number; |
| 693 | readonly threadId: string; |
| 694 | readonly body: string; |
| 695 | }, |
| 696 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 697 | |
| 698 | /** |
| 699 | * Adds a reaction, or takes it back. Only called when `capabilities.reactions` is true. |
| 700 | * |
| 701 | * `subjectId` is a remark's id as the conversation carried it; absent means the change request |
| 702 | * itself, whose reactions sit on its description. Whatever a host needs to address either of |
| 703 | * them is worked out here, because the id a conversation travels with is the one the reader has. |
| 704 | */ |
| 705 | readonly setReaction: ( |
| 706 | input: ProviderRepositoryRef & { |
| 707 | readonly number: number; |
| 708 | readonly subjectId?: string | undefined; |
| 709 | readonly content: PullRequestReactionContent; |
| 710 | readonly reacted: boolean; |
| 711 | }, |
| 712 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 713 | |
| 714 | /** Only called when `capabilities.review.resolve` is true. */ |
| 715 | readonly setThreadResolution: ( |
| 716 | input: ProviderRepositoryRef & { |
| 717 | readonly number: number; |
| 718 | readonly threadId: string; |
| 719 | readonly resolved: boolean; |
| 720 | }, |
| 721 | ) => Effect.Effect<void, PullRequestProviderError>; |
| 722 | } |