1import * as Schema from "effect/Schema";
2import * as Effect from "effect/Effect";
3import * as HttpServerRespondable from "effect/http/HttpServerRespondable";
4import * as HttpServerResponse from "effect/http/HttpServerResponse";
5
6import {
7 ForwardCompatibleArray,
8 IsoDateTime,
9 NonNegativeInt,
10 PositiveInt,
11 ProjectId,
12 ThreadId,
13 TrimmedNonEmptyString,
14} from "./baseSchemas.ts";
15import { SourceControlProviderKind } from "./sourceControl.ts";
16
17export const PullRequestInvolvement = Schema.Literals(["all", "reviewing", "authored"]);
18export type PullRequestInvolvement = typeof PullRequestInvolvement.Type;
19
20export const PullRequestState = Schema.Literals(["open", "closed", "merged"]);
21export type PullRequestState = typeof PullRequestState.Type;
22
23export const PullRequestListState = Schema.Literals(["all", "open", "closed", "merged"]);
24export type PullRequestListState = typeof PullRequestListState.Type;
25
26export const PullRequestReviewDecision = Schema.Literals([
27 "approved",
28 "changes-requested",
29 "review-required",
30]);
31export type PullRequestReviewDecision = typeof PullRequestReviewDecision.Type;
32
33const PullRequestQualifierValue = TrimmedNonEmptyString.check(Schema.isMaxLength(200));
34const PullRequestQualifierValues = Schema.Array(PullRequestQualifierValue).check(
35 Schema.isMaxLength(10),
36);
37
38export const PullRequestListFilters = Schema.Struct({
39 draft: Schema.optional(Schema.Literals(["only", "hide"])),
40 review: Schema.optional(
41 Schema.Literals(["approved", "changes-requested", "review-required", "none"]),
42 ),
43 checks: Schema.optional(Schema.Literals(["passing", "failing"])),
44 /** GitHub label qualifiers: every group must match; any name within a group suffices. */
45 labels: Schema.optional(Schema.Array(PullRequestQualifierValues).check(Schema.isMaxLength(10))),
46 excludedLabels: Schema.optional(PullRequestQualifierValues),
47 author: Schema.optional(PullRequestQualifierValue),
48});
49export type PullRequestListFilters = typeof PullRequestListFilters.Type;
50
51export const PullRequestChecksState = Schema.Literals(["passing", "failing", "pending"]);
52export type PullRequestChecksState = typeof PullRequestChecksState.Type;
53
54export const PullRequestMergeability = Schema.Literals(["mergeable", "conflicting", "unknown"]);
55export type PullRequestMergeability = typeof PullRequestMergeability.Type;
56
57export const PullRequestMergeMethod = Schema.Literals(["merge", "squash", "rebase"]);
58export type PullRequestMergeMethod = typeof PullRequestMergeMethod.Type;
59
60export const PullRequestAction = Schema.Literals([
61 "merge",
62 "ready",
63 "draft",
64 "close",
65 "reopen",
66 "delete-branch",
67 "update-branch",
68 "enable-auto-merge",
69 "disable-auto-merge",
70 "revert",
71 "approve-workflows",
72]);
73export type PullRequestAction = typeof PullRequestAction.Type;
74
75export const PullRequestUpdateMethod = Schema.Literals(["merge", "rebase"]);
76export type PullRequestUpdateMethod = typeof PullRequestUpdateMethod.Type;
77
78export const PullRequestBaseComparison = Schema.Literals(["up-to-date", "behind", "unknown"]);
79export type PullRequestBaseComparison = typeof PullRequestBaseComparison.Type;
80
81export const PullRequestActor = Schema.Struct({
82 isBot: Schema.optional(Schema.Boolean),
83 login: TrimmedNonEmptyString,
84 name: Schema.NullOr(Schema.String),
85 avatarUrl: Schema.NullOr(Schema.String),
86});
87export type PullRequestActor = typeof PullRequestActor.Type;
88
89export const PullRequestLabel = Schema.Struct({
90 name: TrimmedNonEmptyString,
91 color: Schema.NullOr(Schema.String),
92 description: Schema.optional(Schema.NullOr(Schema.String)),
93});
94export type PullRequestLabel = typeof PullRequestLabel.Type;
95
96export const PullRequestCheckStatus = Schema.Literals([
97 "pending",
98 "action-required",
99 "success",
100 "failure",
101 "skipped",
102 "neutral",
103 "cancelled",
104]);
105export type PullRequestCheckStatus = typeof PullRequestCheckStatus.Type;
106
107export const PullRequestCheck = Schema.Struct({
108 name: TrimmedNonEmptyString,
109 status: PullRequestCheckStatus,
110 description: Schema.NullOr(Schema.String),
111 url: Schema.NullOr(Schema.String),
112 /**
113 * When the run started and finished, where the host reports them, so a finished check can say
114 * how long it took. Absent on a commit status, which is a verdict rather than a run.
115 */
116 startedAt: Schema.optional(IsoDateTime),
117 completedAt: Schema.optional(IsoDateTime),
118 required: Schema.optional(Schema.Boolean),
119 /**
120 * Not a check: it stands for ones the host could not list, such as workflow runs awaiting
121 * approval when that read fails, so nothing waits on it or counts it as passing.
122 */
123 placeholder: Schema.optional(Schema.Boolean),
124});
125export type PullRequestCheck = typeof PullRequestCheck.Type;
126
127export const PullRequestReactionContent = Schema.Literals([
128 "thumbs-up",
129 "thumbs-down",
130 "laugh",
131 "hooray",
132 "confused",
133 "heart",
134 "rocket",
135 "eyes",
136]);
137export type PullRequestReactionContent = typeof PullRequestReactionContent.Type;
138
139export const PullRequestReaction = Schema.Struct({
140 content: PullRequestReactionContent,
141 count: PositiveInt,
142 /** Excludes the viewer's login when viewerHasReacted; count still includes them. */
143 actors: Schema.Array(TrimmedNonEmptyString),
144 viewerHasReacted: Schema.Boolean,
145});
146export type PullRequestReaction = typeof PullRequestReaction.Type;
147
148export const PullRequestCommentKind = Schema.Literals([
149 "issue-comment",
150 "review-comment",
151 "review",
152]);
153export type PullRequestCommentKind = typeof PullRequestCommentKind.Type;
154
155export const PullRequestComment = Schema.Struct({
156 id: TrimmedNonEmptyString,
157 kind: PullRequestCommentKind,
158 author: Schema.NullOr(PullRequestActor),
159 body: Schema.String,
160 createdAt: IsoDateTime,
161 editedAt: Schema.optional(Schema.NullOr(IsoDateTime)),
162 url: Schema.NullOr(Schema.String),
163 path: Schema.NullOr(Schema.String),
164 reviewState: Schema.NullOr(Schema.String),
165 reactions: Schema.optional(Schema.Array(PullRequestReaction)),
166 canEdit: Schema.optional(Schema.Boolean),
167});
168export type PullRequestComment = typeof PullRequestComment.Type;
169
170export const PullRequestDiffSide = Schema.Literals(["left", "right"]);
171export type PullRequestDiffSide = typeof PullRequestDiffSide.Type;
172
173export const PullRequestReviewVerdict = Schema.Literals(["comment", "approve", "request-changes"]);
174export type PullRequestReviewVerdict = typeof PullRequestReviewVerdict.Type;
175
176export const PullRequestThreadComment = Schema.Struct({
177 id: TrimmedNonEmptyString,
178 author: Schema.NullOr(PullRequestActor),
179 body: Schema.String,
180 createdAt: IsoDateTime,
181 editedAt: Schema.optional(Schema.NullOr(IsoDateTime)),
182 url: Schema.NullOr(Schema.String),
183 /** The review that posted the comment, so a link to a review with no text finds its comments. */
184 reviewUrl: Schema.optional(Schema.NullOr(Schema.String)),
185 reactions: Schema.optional(Schema.Array(PullRequestReaction)),
186 canEdit: Schema.optional(Schema.Boolean),
187});
188export type PullRequestThreadComment = typeof PullRequestThreadComment.Type;
189
190/** One numbered line of the code a review thread was written against. */
191export const PullRequestReviewThreadSnippetLine = Schema.Struct({
192 kind: Schema.Literals(["added", "deleted", "context"]),
193 /** The line's number on the side it belongs to: the old side for a deletion, else the new. */
194 line: Schema.NullOr(PositiveInt),
195 text: Schema.String,
196});
197export type PullRequestReviewThreadSnippetLine = typeof PullRequestReviewThreadSnippetLine.Type;
198
199export const PullRequestReviewThread = Schema.Struct({
200 id: TrimmedNonEmptyString,
201 path: TrimmedNonEmptyString,
202 line: Schema.NullOr(PositiveInt),
203 side: PullRequestDiffSide,
204 isResolved: Schema.Boolean,
205 isOutdated: Schema.Boolean,
206 comments: Schema.Array(PullRequestThreadComment),
207 commentCount: Schema.optional(NonNegativeInt),
208 nextCommentsCursor: Schema.optional(TrimmedNonEmptyString),
209 /** Last diff lines ending at the comment; absent when the host omits the hunk. */
210 snippet: Schema.optional(Schema.Array(PullRequestReviewThreadSnippetLine)),
211 /** Can only narrow repository-wide resolve permission. Absent when unreported. */
212 canResolve: Schema.optional(Schema.Boolean),
213});
214export type PullRequestReviewThread = typeof PullRequestReviewThread.Type;
215
216/** A thread on the change request as a whole, such as a GitLab overview thread. */
217export const PullRequestOverviewThread = Schema.Struct({
218 ...PullRequestReviewThread.fields,
219 path: Schema.Null,
220 line: Schema.Null,
221});
222export type PullRequestOverviewThread = typeof PullRequestOverviewThread.Type;
223
224export type PullRequestConversationThread = PullRequestReviewThread | PullRequestOverviewThread;
225
226export const PullRequestReviewerKind = Schema.Literals(["user", "team"]);
227export type PullRequestReviewerKind = typeof PullRequestReviewerKind.Type;
228
229export const PullRequestReviewerCandidate = Schema.Struct({
230 ...PullRequestActor.fields,
231 id: TrimmedNonEmptyString,
232 kind: PullRequestReviewerKind,
233 isRequested: Schema.Boolean,
234 /**
235 * Why the host suggests this person, such as "wrote some of these lines". Present only on a
236 * suggestion, which is how the menu tells its suggested people from everyone else with access.
237 */
238 suggestion: Schema.optional(TrimmedNonEmptyString),
239 /** When the outstanding request was made, where the host keeps that event. */
240 requestedAt: Schema.optional(IsoDateTime),
241});
242export type PullRequestReviewerCandidate = typeof PullRequestReviewerCandidate.Type;
243
244export const PullRequestReviewerCandidateList = Schema.Struct({
245 candidates: Schema.Array(PullRequestReviewerCandidate),
246 truncated: Schema.Boolean,
247});
248export type PullRequestReviewerCandidateList = typeof PullRequestReviewerCandidateList.Type;
249
250export const PullRequestLabelCandidate = Schema.Struct({
251 ...PullRequestLabel.fields,
252 description: Schema.NullOr(Schema.String),
253 isApplied: Schema.Boolean,
254});
255export type PullRequestLabelCandidate = typeof PullRequestLabelCandidate.Type;
256
257export const PullRequestLabelCandidateList = Schema.Struct({
258 candidates: Schema.Array(PullRequestLabelCandidate),
259 truncated: Schema.Boolean,
260});
261export type PullRequestLabelCandidateList = typeof PullRequestLabelCandidateList.Type;
262
263export const PullRequestCommit = Schema.Struct({
264 oid: TrimmedNonEmptyString,
265 messageHeadline: Schema.String,
266 committedDate: IsoDateTime,
267 additions: Schema.optional(NonNegativeInt),
268 deletions: Schema.optional(NonNegativeInt),
269 authors: Schema.optional(Schema.Array(PullRequestActor)),
270 /** How this commit's own checks came out, where the host rolls them up per commit. */
271 checks: Schema.optional(
272 Schema.Struct({
273 state: PullRequestChecksState,
274 passed: NonNegativeInt,
275 total: PositiveInt,
276 }),
277 ),
278});
279export type PullRequestCommit = typeof PullRequestCommit.Type;
280
281export const PullRequestReviewCapabilities = Schema.Struct({
282 inlineComment: Schema.Boolean,
283 /**
284 * An inline comment can cover a range of lines rather than its last one. Absent, as from a
285 * server that predates it, means one line.
286 */
287 rangeComment: Schema.optional(Schema.Boolean),
288 reply: Schema.Boolean,
289 resolve: Schema.Boolean,
290 verdicts: ForwardCompatibleArray(PullRequestReviewVerdict),
291});
292export type PullRequestReviewCapabilities = typeof PullRequestReviewCapabilities.Type;
293
294export const PullRequestEditCapabilities = Schema.Struct({
295 changeRequest: Schema.Boolean,
296 comment: Schema.Boolean,
297 maintainerEdits: Schema.optional(Schema.Boolean),
298});
299export type PullRequestEditCapabilities = typeof PullRequestEditCapabilities.Type;
300
301export const PullRequestReviewerCapabilities = Schema.Struct({
302 request: Schema.Boolean,
303 listCandidates: Schema.Boolean,
304 /**
305 * Reviewer candidates take a `query` the host matches past the first page. Absent, as from a
306 * server that predates it, means the client narrows the loaded page itself.
307 */
308 searchCandidates: Schema.optional(Schema.Boolean),
309});
310export type PullRequestReviewerCapabilities = typeof PullRequestReviewerCapabilities.Type;
311
312export const PullRequestViewedFilesStore = Schema.Literals(["host", "environment"]);
313export type PullRequestViewedFilesStore = typeof PullRequestViewedFilesStore.Type;
314
315/** `#` references issues (and GitHub pull requests); `!` references GitLab merge requests. */
316export const PullRequestReferenceSigil = Schema.Literals(["#", "!"]);
317export type PullRequestReferenceSigil = typeof PullRequestReferenceSigil.Type;
318
319export const PullRequestCapabilities = Schema.Struct({
320 diff: Schema.Boolean,
321 comment: Schema.Boolean,
322 actions: ForwardCompatibleArray(PullRequestAction),
323 mergeMethods: ForwardCompatibleArray(PullRequestMergeMethod),
324 deleteBranch: Schema.optional(Schema.Boolean),
325 /**
326 * Absent where it cannot at all, which is every host that has not said otherwise — so a
327 * provider that says nothing offers nothing.
328 */
329 updateMethods: Schema.optional(ForwardCompatibleArray(PullRequestUpdateMethod)),
330 search: Schema.Boolean,
331 reactions: Schema.optional(Schema.Boolean),
332 viewedFiles: Schema.optional(PullRequestViewedFilesStore),
333 review: PullRequestReviewCapabilities,
334 reviewers: PullRequestReviewerCapabilities,
335 edit: Schema.optional(PullRequestEditCapabilities),
336 stacks: Schema.optional(Schema.Boolean),
337 stackActions: Schema.optional(Schema.Boolean),
338 labels: Schema.optional(Schema.Boolean),
339 /** The host searches the repository's users, so composers can complete `@login`. */
340 mentions: Schema.optional(Schema.Boolean),
341 /** The sigils whose issue and change request references composers can complete. */
342 referenceSigils: Schema.optional(ForwardCompatibleArray(PullRequestReferenceSigil)),
343 /**
344 * Every head gets at least one check, so a head that has gone a while without any means none
345 * reported rather than that the repository runs none. Absent means an empty list says nothing.
346 */
347 checksAlwaysReport: Schema.optional(Schema.Boolean),
348});
349export type PullRequestCapabilities = typeof PullRequestCapabilities.Type;
350
351/** Unreported host permissions are granted. */
352export const PullRequestViewerPermissions = Schema.Struct({
353 stackRebase: Schema.optional(Schema.Boolean),
354 actions: ForwardCompatibleArray(PullRequestAction),
355 comment: Schema.Boolean,
356 resolve: Schema.Boolean,
357 verdicts: ForwardCompatibleArray(PullRequestReviewVerdict),
358 requestReviewers: Schema.Boolean,
359 updateMethods: Schema.optional(ForwardCompatibleArray(PullRequestUpdateMethod)),
360 labels: Schema.optional(Schema.Boolean),
361 editChangeRequest: Schema.optional(Schema.Boolean),
362 maintainerEdits: Schema.optional(Schema.Boolean),
363});
364export type PullRequestViewerPermissions = typeof PullRequestViewerPermissions.Type;
365
366export const PullRequestMergeCapabilities = Schema.Struct({
367 merge: Schema.Boolean,
368 squash: Schema.Boolean,
369 rebase: Schema.Boolean,
370});
371export type PullRequestMergeCapabilities = typeof PullRequestMergeCapabilities.Type;
372
373export const PullRequestStackMembership = Schema.Struct({
374 number: PositiveInt,
375 position: PositiveInt,
376 size: PositiveInt,
377 base: TrimmedNonEmptyString,
378});
379export type PullRequestStackMembership = typeof PullRequestStackMembership.Type;
380
381export const PullRequestListEntry = Schema.Struct({
382 stack: Schema.optional(PullRequestStackMembership),
383 provider: SourceControlProviderKind,
384 host: TrimmedNonEmptyString,
385 projectId: ProjectId,
386 projectTitle: TrimmedNonEmptyString,
387 repository: TrimmedNonEmptyString,
388 number: PositiveInt,
389 title: TrimmedNonEmptyString,
390 url: TrimmedNonEmptyString,
391 author: Schema.NullOr(PullRequestActor),
392 headBranch: TrimmedNonEmptyString,
393 baseBranch: TrimmedNonEmptyString,
394 state: PullRequestState,
395 isDraft: Schema.Boolean,
396 mergeability: PullRequestMergeability,
397 /** Zero until fetched through `PullRequestListStatsInput`; GitHub listings omit counts. */
398 additions: NonNegativeInt,
399 deletions: NonNegativeInt,
400 createdAt: IsoDateTime,
401 updatedAt: IsoDateTime,
402 observedAt: Schema.optional(Schema.Finite),
403 viewerReviewRequested: Schema.Boolean,
404 labels: Schema.Array(PullRequestLabel),
405 reviewDecision: Schema.optional(PullRequestReviewDecision),
406 checksState: Schema.optional(PullRequestChecksState),
407});
408export type PullRequestListEntry = typeof PullRequestListEntry.Type;
409
410export const PullRequestListCursors = Schema.Record(
411 TrimmedNonEmptyString,
412 TrimmedNonEmptyString.check(Schema.isMaxLength(4096)),
413);
414export type PullRequestListCursors = typeof PullRequestListCursors.Type;
415
416export const PULL_REQUEST_LIST_PROJECT_LIMIT = 100;
417
418export const PullRequestListInput = Schema.Struct({
419 state: PullRequestListState,
420 involvement: Schema.optional(PullRequestInvolvement),
421 filters: Schema.optional(PullRequestListFilters),
422 projectId: Schema.optional(ProjectId),
423 projectIds: Schema.optional(
424 Schema.Array(ProjectId).check(Schema.isMaxLength(PULL_REQUEST_LIST_PROJECT_LIMIT)),
425 ),
426 host: Schema.optional(TrimmedNonEmptyString),
427 limit: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 500 }))),
428 cursors: Schema.optional(PullRequestListCursors),
429 query: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(200))),
430});
431export type PullRequestListInput = typeof PullRequestListInput.Type;
432
433export const PullRequestProviderSummary = Schema.Struct({
434 host: TrimmedNonEmptyString,
435 kind: SourceControlProviderKind,
436 searchesOnHost: Schema.Boolean,
437 actions: Schema.optional(ForwardCompatibleArray(PullRequestAction)),
438 projectCount: PositiveInt,
439 /** SSH checkouts can resolve to an HTTP authority different from their stored identity. */
440 projectIds: Schema.optional(Schema.Array(ProjectId)),
441 configured: Schema.Boolean,
442 detail: Schema.NullOr(TrimmedNonEmptyString),
443});
444export type PullRequestProviderSummary = typeof PullRequestProviderSummary.Type;
445
446export const PullRequestListProjectError = Schema.Struct({
447 projectId: ProjectId,
448 projectTitle: TrimmedNonEmptyString,
449 message: TrimmedNonEmptyString,
450});
451export type PullRequestListProjectError = typeof PullRequestListProjectError.Type;
452
453export const PullRequestListResult = Schema.Struct({
454 /** Unreadable hosts are absent; JSON codecs reject optional record values. */
455 viewers: Schema.Record(TrimmedNonEmptyString, TrimmedNonEmptyString),
456 providers: Schema.Array(PullRequestProviderSummary),
457 /** Re-sort after appending: one repository's older slice may outrank another's existing rows. */
458 entries: Schema.Array(PullRequestListEntry),
459 errors: Schema.Array(PullRequestListProjectError),
460 truncated: Schema.Boolean,
461 nextCursors: PullRequestListCursors,
462});
463export type PullRequestListResult = typeof PullRequestListResult.Type;
464
465export const PullRequestRef = Schema.Struct({
466 projectId: ProjectId,
467 host: Schema.optional(TrimmedNonEmptyString),
468 expectedAccountId: Schema.optional(TrimmedNonEmptyString),
469 allowStale: Schema.optional(Schema.Boolean),
470 repository: TrimmedNonEmptyString,
471 number: PositiveInt,
472});
473export type PullRequestRef = typeof PullRequestRef.Type;
474
475/** `query` asks the host for matches past the first page of candidates. */
476export const PullRequestReviewerCandidatesInput = Schema.Struct({
477 ...PullRequestRef.fields,
478 query: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(100))),
479});
480export type PullRequestReviewerCandidatesInput = typeof PullRequestReviewerCandidatesInput.Type;
481
482export const PullRequestRoutingIdentityInput = Schema.Struct({
483 host: TrimmedNonEmptyString,
484});
485export type PullRequestRoutingIdentityInput = typeof PullRequestRoutingIdentityInput.Type;
486
487export const PullRequestRoutingIdentityResult = Schema.Struct({
488 accountId: TrimmedNonEmptyString,
489 host: TrimmedNonEmptyString,
490 provider: SourceControlProviderKind,
491 viewer: TrimmedNonEmptyString,
492});
493export type PullRequestRoutingIdentityResult = typeof PullRequestRoutingIdentityResult.Type;
494
495export const PullRequestRoutingResult = Schema.Struct({
496 accountId: TrimmedNonEmptyString,
497 host: TrimmedNonEmptyString,
498 provider: SourceControlProviderKind,
499 viewer: TrimmedNonEmptyString,
500 projectTitle: TrimmedNonEmptyString,
501 workspaceRoot: TrimmedNonEmptyString,
502});
503export type PullRequestRoutingResult = typeof PullRequestRoutingResult.Type;
504
505export const PullRequestLinkedThreadsResult = Schema.Struct({
506 threads: Schema.Array(
507 Schema.Struct({
508 id: ThreadId,
509 projectId: ProjectId,
510 title: Schema.String,
511 archivedAt: Schema.NullOr(IsoDateTime),
512 settled: Schema.optionalKey(Schema.Boolean),
513 }),
514 ),
515});
516export type PullRequestLinkedThreadsResult = typeof PullRequestLinkedThreadsResult.Type;
517
518export const PullRequestPreview = Schema.Struct({
519 projectId: ProjectId,
520 repository: TrimmedNonEmptyString,
521 number: PositiveInt,
522 title: TrimmedNonEmptyString,
523 url: TrimmedNonEmptyString,
524 author: Schema.NullOr(PullRequestActor),
525 state: PullRequestState,
526 isDraft: Schema.Boolean,
527 createdAt: IsoDateTime,
528 updatedAt: Schema.optional(IsoDateTime),
529 observedAt: Schema.optional(Schema.Finite),
530});
531export type PullRequestPreview = typeof PullRequestPreview.Type;
532
533export const PullRequestSummary = Schema.Struct({
534 provider: SourceControlProviderKind,
535 projectId: ProjectId,
536 repository: TrimmedNonEmptyString,
537 number: PositiveInt,
538 title: TrimmedNonEmptyString,
539 url: TrimmedNonEmptyString,
540 state: PullRequestState,
541 isDraft: Schema.optional(Schema.Boolean),
542 headBranch: TrimmedNonEmptyString,
543 baseBranch: TrimmedNonEmptyString,
544 closedAt: Schema.optional(Schema.NullOr(Schema.String)),
545 mergedAt: Schema.optional(Schema.NullOr(Schema.String)),
546 updatedAt: IsoDateTime,
547 observedAt: Schema.optional(Schema.Finite),
548 author: Schema.optional(Schema.NullOr(PullRequestActor)),
549 additions: Schema.optional(NonNegativeInt),
550 deletions: Schema.optional(NonNegativeInt),
551 changedFiles: Schema.optional(NonNegativeInt),
552 reviewDecision: Schema.optional(Schema.NullOr(PullRequestReviewDecision)),
553 checksState: Schema.optional(Schema.NullOr(PullRequestChecksState)),
554 mergeability: Schema.optional(PullRequestMergeability),
555 /** Null when the host says the pull request is in no stack; absent when the read did not ask. */
556 stack: Schema.optional(Schema.NullOr(PullRequestStackMembership)),
557 headSha: Schema.optional(TrimmedNonEmptyString),
558 /** Absent means the read did not report auto-merge state, not that it is off. */
559 autoMergeEnabled: Schema.optional(Schema.Boolean),
560 autoMergeMethod: Schema.optional(PullRequestMergeMethod),
561});
562export type PullRequestSummary = typeof PullRequestSummary.Type;
563
564export const PullRequestStack = Schema.Struct({
565 id: TrimmedNonEmptyString,
566 number: PositiveInt,
567 url: TrimmedNonEmptyString,
568 base: TrimmedNonEmptyString,
569 layers: Schema.Array(
570 Schema.Struct({
571 number: PositiveInt,
572 title: Schema.optional(Schema.String),
573 isDraft: Schema.optional(Schema.Boolean),
574 headSha: Schema.optional(TrimmedNonEmptyString),
575 headBranch: TrimmedNonEmptyString,
576 state: PullRequestState,
577 }),
578 ),
579});
580export type PullRequestStack = typeof PullRequestStack.Type;
581
582export const PullRequestDiffStat = Schema.Struct({
583 projectId: ProjectId,
584 repository: TrimmedNonEmptyString,
585 number: PositiveInt,
586 additions: NonNegativeInt,
587 deletions: NonNegativeInt,
588});
589export type PullRequestDiffStat = typeof PullRequestDiffStat.Type;
590
591export const PullRequestListStatsInput = Schema.Struct({
592 refs: Schema.Array(PullRequestRef).check(Schema.isMaxLength(500)),
593});
594export type PullRequestListStatsInput = typeof PullRequestListStatsInput.Type;
595
596export const PullRequestListStatsResult = Schema.Struct({
597 stats: Schema.Array(PullRequestDiffStat),
598});
599export type PullRequestListStatsResult = typeof PullRequestListStatsResult.Type;
600
601export const PullRequestFilterOptionKind = Schema.Literals(["labels", "authors", "references"]);
602export type PullRequestFilterOptionKind = typeof PullRequestFilterOptionKind.Type;
603
604/** Scoped like `PullRequestListInput`, so the options come from the repositories it lists. */
605export const PullRequestFilterOptionsInput = Schema.Struct({
606 kind: PullRequestFilterOptionKind,
607 projectId: Schema.optional(ProjectId),
608 projectIds: Schema.optional(Schema.Array(ProjectId).check(Schema.isMaxLength(100))),
609 host: Schema.optional(TrimmedNonEmptyString),
610 query: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(100))),
611 /** For `references`, the sigil typed, so a host can search only what it links. */
612 sigil: Schema.optional(PullRequestReferenceSigil),
613});
614export type PullRequestFilterOptionsInput = typeof PullRequestFilterOptionsInput.Type;
615
616/** An issue or change request a composer can reference, with the text the host links. */
617export const PullRequestReferenceCandidate = Schema.Struct({
618 reference: TrimmedNonEmptyString,
619 kind: Schema.Literals(["issue", "change-request"]),
620 number: PositiveInt,
621 title: Schema.String,
622 state: Schema.Literals(["open", "closed", "merged"]),
623});
624export type PullRequestReferenceCandidate = typeof PullRequestReferenceCandidate.Type;
625
626export const PullRequestFilterOptions = Schema.Struct({
627 labels: Schema.Array(PullRequestLabel),
628 authors: Schema.Array(PullRequestActor),
629 // Servers from before reference completion send no key.
630 references: Schema.Array(PullRequestReferenceCandidate).pipe(
631 Schema.withDecodingDefault(Effect.succeed([])),
632 ),
633 truncated: Schema.Boolean,
634});
635export type PullRequestFilterOptions = typeof PullRequestFilterOptions.Type;
636
637/**
638 * A separate request rather than a flag on the reads, so an explicit "refresh" one person
639 * presses is the only thing that spends host requests — every ordinary read shares.
640 */
641export const PullRequestInvalidateInput = Schema.Struct({
642 reference: Schema.optional(PullRequestRef),
643 filesViewedOnly: Schema.optional(Schema.Boolean),
644});
645export type PullRequestInvalidateInput = typeof PullRequestInvalidateInput.Type;
646
647export const PullRequestReportStateInput = Schema.Struct({
648 reference: PullRequestRef,
649 state: PullRequestState,
650 isDraft: Schema.optional(Schema.Boolean),
651});
652export type PullRequestReportStateInput = typeof PullRequestReportStateInput.Type;
653
654export const PullRequestDetail = Schema.Struct({
655 provider: SourceControlProviderKind,
656 capabilities: PullRequestCapabilities,
657 viewerPermissions: PullRequestViewerPermissions,
658 projectId: ProjectId,
659 projectTitle: TrimmedNonEmptyString,
660 workspaceRoot: TrimmedNonEmptyString,
661 repository: TrimmedNonEmptyString,
662 number: PositiveInt,
663 title: TrimmedNonEmptyString,
664 body: Schema.String,
665 url: TrimmedNonEmptyString,
666 author: Schema.NullOr(PullRequestActor),
667 state: PullRequestState,
668 isDraft: Schema.Boolean,
669 mergeability: PullRequestMergeability,
670 additions: NonNegativeInt,
671 deletions: NonNegativeInt,
672 changedFiles: NonNegativeInt,
673 headBranch: TrimmedNonEmptyString,
674 headRepositoryNameWithOwner: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
675 headSha: Schema.optional(TrimmedNonEmptyString),
676 baseBranch: TrimmedNonEmptyString,
677 createdAt: IsoDateTime,
678 updatedAt: IsoDateTime,
679 observedAt: Schema.optional(Schema.Finite),
680 mergedAt: Schema.NullOr(IsoDateTime),
681 closedAt: Schema.NullOr(IsoDateTime),
682 reviewers: Schema.Array(PullRequestActor),
683 labels: Schema.Array(PullRequestLabel),
684 checks: Schema.Array(PullRequestCheck),
685 mergeCapabilities: PullRequestMergeCapabilities,
686 viewer: Schema.optional(TrimmedNonEmptyString),
687 baseComparison: Schema.optional(PullRequestBaseComparison),
688 behindBy: Schema.optional(NonNegativeInt),
689 /** Absent means the host does not report auto-merge state, not that it is off. */
690 autoMergeEnabled: Schema.optional(Schema.Boolean),
691 autoMergeMethod: Schema.optional(PullRequestMergeMethod),
692 workflowApprovalsRequired: Schema.optional(NonNegativeInt),
693 maintainerCanModify: Schema.optional(Schema.Boolean),
694});
695export type PullRequestDetail = typeof PullRequestDetail.Type;
696
697export const PullRequestChecks = Schema.Struct({
698 observedAt: Schema.optional(Schema.Finite),
699 state: PullRequestState,
700 checks: Schema.Array(PullRequestCheck),
701});
702export type PullRequestChecks = typeof PullRequestChecks.Type;
703
704export const PullRequestReviewRequestEvent = Schema.Struct({
705 /** Who asked, where the host still knows. */
706 actor: Schema.NullOr(PullRequestActor),
707 /** Who was asked. A team wears its slug as the login. */
708 reviewer: PullRequestActor,
709 createdAt: IsoDateTime,
710});
711export type PullRequestReviewRequestEvent = typeof PullRequestReviewRequestEvent.Type;
712
713export const PullRequestActivity = Schema.Struct({
714 author: Schema.optional(Schema.NullOr(PullRequestActor)),
715 reviewers: Schema.optional(Schema.Array(PullRequestActor)),
716 comments: Schema.Array(PullRequestComment),
717 commentCount: NonNegativeInt,
718 commentsTruncated: Schema.Boolean,
719 reviewThreadsTruncated: Schema.optional(Schema.Boolean),
720 reviewThreads: Schema.Array(PullRequestReviewThread),
721 // Apart from reviewThreads so clients that require a thread path still decode the activity.
722 overviewThreads: Schema.optional(Schema.Array(PullRequestOverviewThread)),
723 commits: Schema.Array(PullRequestCommit),
724 /**
725 * Each time somebody was asked for a review, oldest first, where the host keeps those events.
726 * A request made after that reviewer already reviewed is a re-request.
727 */
728 reviewRequests: Schema.optional(Schema.Array(PullRequestReviewRequestEvent)),
729 reactions: Schema.optional(Schema.Array(PullRequestReaction)),
730});
731export type PullRequestActivity = typeof PullRequestActivity.Type;
732
733export const pullRequestConversationThreads = (
734 activity: Pick<PullRequestActivity, "reviewThreads" | "overviewThreads">,
735): ReadonlyArray<PullRequestConversationThread> =>
736 activity.overviewThreads === undefined || activity.overviewThreads.length === 0
737 ? activity.reviewThreads
738 : [...activity.reviewThreads, ...activity.overviewThreads];
739
740export const PullRequestDetailView = Schema.Struct({
741 ...PullRequestDetail.fields,
742 ...PullRequestActivity.fields,
743 author: Schema.NullOr(PullRequestActor),
744 reviewers: Schema.Array(PullRequestActor),
745});
746export type PullRequestDetailView = typeof PullRequestDetailView.Type;
747
748/** GitHub refuses diffs past 300 files, so callers must page large changes. */
749export const PullRequestDiffInput = Schema.Struct({
750 ...PullRequestRef.fields,
751 cursor: Schema.optional(TrimmedNonEmptyString),
752 commit: Schema.optional(TrimmedNonEmptyString),
753});
754export type PullRequestDiffInput = typeof PullRequestDiffInput.Type;
755
756export const PullRequestOmittedFileStat = Schema.Struct({
757 path: TrimmedNonEmptyString,
758 additions: Schema.Number,
759 deletions: Schema.Number,
760});
761export type PullRequestOmittedFileStat = typeof PullRequestOmittedFileStat.Type;
762
763export const PullRequestDiffResult = Schema.Struct({
764 patch: Schema.String,
765 /** Content omitted within this slice; nextCursor separately indicates more slices. */
766 truncated: Schema.Boolean,
767 nextCursor: Schema.NullOr(TrimmedNonEmptyString),
768 omittedFileStats: Schema.optional(Schema.Array(PullRequestOmittedFileStat)),
769});
770export type PullRequestDiffResult = typeof PullRequestDiffResult.Type;
771
772export const PullRequestDiffFileContentsInput = Schema.Struct({
773 ...PullRequestRef.fields,
774 commit: Schema.optional(TrimmedNonEmptyString),
775 changeType: Schema.Literals(["change", "rename-pure", "rename-changed", "new", "deleted"]),
776 oldPath: TrimmedNonEmptyString,
777 newPath: TrimmedNonEmptyString,
778});
779export type PullRequestDiffFileContentsInput = typeof PullRequestDiffFileContentsInput.Type;
780
781export const PullRequestDiffFileContentsResult = Schema.Struct({
782 oldContents: Schema.String,
783 newContents: Schema.String,
784});
785export type PullRequestDiffFileContentsResult = typeof PullRequestDiffFileContentsResult.Type;
786
787const MAX_FILE_PATH_LENGTH = 4096;
788// Preserve filename whitespace so viewed marks match host paths.
789const FilePath = Schema.String.check(Schema.isNonEmpty(), Schema.isMaxLength(MAX_FILE_PATH_LENGTH));
790
791export const PullRequestFileViewedState = Schema.Literals(["unviewed", "viewed", "dismissed"]);
792export type PullRequestFileViewedState = typeof PullRequestFileViewedState.Type;
793
794export const PullRequestFileViewed = Schema.Struct({
795 path: FilePath,
796 state: PullRequestFileViewedState,
797});
798export type PullRequestFileViewed = typeof PullRequestFileViewed.Type;
799
800export const PullRequestFilesViewedResult = Schema.Struct({
801 /** Only the files the host reported a state for. A file missing from this list is unviewed. */
802 files: Schema.Array(PullRequestFileViewed),
803 truncated: Schema.Boolean,
804});
805export type PullRequestFilesViewedResult = typeof PullRequestFilesViewedResult.Type;
806
807const PullRequestLineCount = Schema.Struct({
808 additions: NonNegativeInt,
809 deletions: NonNegativeInt,
810});
811
812/**
813 * A change's line counts split by what the changed files are, judged from their paths. Null when
814 * the host gives no per-file counts, or has more files than one read covers: a split of part of
815 * the change would not add up to the totals shown beside it.
816 */
817export const PullRequestChangeBreakdown = Schema.NullOr(
818 Schema.Struct({
819 code: PullRequestLineCount,
820 tests: PullRequestLineCount,
821 mocks: PullRequestLineCount,
822 generated: PullRequestLineCount,
823 // Servers from before the docs split send no key; their docs lines sit in `code`.
824 docs: PullRequestLineCount.pipe(
825 Schema.withDecodingDefault(Effect.succeed({ additions: 0, deletions: 0 })),
826 ),
827 }),
828);
829export type PullRequestChangeBreakdown = typeof PullRequestChangeBreakdown.Type;
830
831const MAX_FILES_VIEWED_PRESSES = 500;
832
833export const PullRequestSetFilesViewedInput = Schema.Struct({
834 ...PullRequestRef.fields,
835 files: Schema.Array(
836 Schema.Struct({
837 path: FilePath,
838 viewed: Schema.Boolean,
839 /** The file's version in the diff the reader was shown; without it the head's is recorded. */
840 revision: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(256))),
841 }),
842 ).check(Schema.isMaxLength(MAX_FILES_VIEWED_PRESSES)),
843});
844export type PullRequestSetFilesViewedInput = typeof PullRequestSetFilesViewedInput.Type;
845
846export const PullRequestStackHead = Schema.Struct({
847 number: PositiveInt,
848 headSha: TrimmedNonEmptyString,
849});
850export type PullRequestStackHead = typeof PullRequestStackHead.Type;
851
852export const PullRequestActionInput = Schema.Struct({
853 /** Native stack scope; only send to environments advertising pullRequestStackActions. */
854 stackNumber: Schema.optional(PositiveInt),
855 expectedStackHeads: Schema.optional(Schema.Array(PullRequestStackHead)),
856 ...PullRequestRef.fields,
857 action: PullRequestAction,
858 mergeMethod: Schema.optional(PullRequestMergeMethod),
859 updateMethod: Schema.optional(PullRequestUpdateMethod),
860 /** Opt-in, because older clients decode the action's success as void and reject a summary. */
861 replyWithSummary: Schema.optional(Schema.Literal(true)),
862});
863export type PullRequestActionInput = typeof PullRequestActionInput.Type;
864
865// Preserve whitespace because it affects Markdown formatting.
866const CommentBody = Schema.String.check(Schema.isNonEmpty()).check(Schema.isMaxLength(65_536));
867
868export const PullRequestCommentInput = Schema.Struct({
869 ...PullRequestRef.fields,
870 body: CommentBody,
871});
872export type PullRequestCommentInput = typeof PullRequestCommentInput.Type;
873
874export const PullRequestUpdateInput = Schema.Struct({
875 ...PullRequestRef.fields,
876 title: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(1024))),
877 body: Schema.optional(Schema.String.check(Schema.isMaxLength(65_536))),
878 maintainerCanModify: Schema.optional(Schema.Boolean),
879});
880export type PullRequestUpdateInput = typeof PullRequestUpdateInput.Type;
881
882/** Comment kind selects the endpoint; GitHub uses separate mutations for conversation and line comments. */
883export const PullRequestCommentUpdateInput = Schema.Struct({
884 ...PullRequestRef.fields,
885 commentId: TrimmedNonEmptyString,
886 kind: Schema.Literals(["issue-comment", "review-comment"]),
887 body: CommentBody,
888});
889export type PullRequestCommentUpdateInput = typeof PullRequestCommentUpdateInput.Type;
890
891export const PullRequestReviewPosition = Schema.Union([
892 Schema.Struct({
893 kind: Schema.Literal("added"),
894 newLine: PositiveInt,
895 }),
896 Schema.Struct({
897 kind: Schema.Literal("deleted"),
898 oldLine: PositiveInt,
899 }),
900 Schema.Struct({
901 kind: Schema.Literal("context"),
902 oldLine: PositiveInt,
903 newLine: PositiveInt,
904 side: PullRequestDiffSide,
905 }),
906]);
907export type PullRequestReviewPosition = typeof PullRequestReviewPosition.Type;
908
909export const PullRequestReviewCommentDraft = Schema.Struct({
910 path: TrimmedNonEmptyString,
911 /** GitLab needs both names for renamed-file comments; hosts using one path ignore this. */
912 oldPath: Schema.optional(TrimmedNonEmptyString),
913 position: PullRequestReviewPosition,
914 /**
915 * The first line of a comment on a range, which ends at `position`. Sent only to a host whose
916 * capabilities include `review.rangeComment`.
917 */
918 startPosition: Schema.optional(PullRequestReviewPosition),
919 body: CommentBody,
920});
921export type PullRequestReviewCommentDraft = typeof PullRequestReviewCommentDraft.Type;
922
923export const PullRequestSubmitReviewInput = Schema.Struct({
924 ...PullRequestRef.fields,
925 verdict: PullRequestReviewVerdict,
926 body: Schema.String.check(Schema.isMaxLength(65_536)),
927 comments: Schema.Array(PullRequestReviewCommentDraft),
928});
929export type PullRequestSubmitReviewInput = typeof PullRequestSubmitReviewInput.Type;
930
931export const PullRequestThreadCommentsInput = Schema.Struct({
932 ...PullRequestRef.fields,
933 threadId: TrimmedNonEmptyString,
934 cursor: TrimmedNonEmptyString,
935});
936export type PullRequestThreadCommentsInput = typeof PullRequestThreadCommentsInput.Type;
937
938export const PullRequestThreadCommentsResult = Schema.Struct({
939 comments: Schema.Array(PullRequestThreadComment),
940 nextCursor: Schema.NullOr(TrimmedNonEmptyString),
941 // Comments the host hid that this page left out; the thread's `commentCount` still counts them.
942 hiddenCount: Schema.optional(NonNegativeInt),
943});
944export type PullRequestThreadCommentsResult = typeof PullRequestThreadCommentsResult.Type;
945
946export const PullRequestThreadReplyInput = Schema.Struct({
947 ...PullRequestRef.fields,
948 threadId: TrimmedNonEmptyString,
949 body: CommentBody,
950});
951export type PullRequestThreadReplyInput = typeof PullRequestThreadReplyInput.Type;
952
953export const PullRequestThreadResolutionInput = Schema.Struct({
954 ...PullRequestRef.fields,
955 threadId: TrimmedNonEmptyString,
956 resolved: Schema.Boolean,
957});
958export type PullRequestThreadResolutionInput = typeof PullRequestThreadResolutionInput.Type;
959
960export const PullRequestReactionInput = Schema.Struct({
961 ...PullRequestRef.fields,
962 subjectId: Schema.optional(TrimmedNonEmptyString),
963 content: PullRequestReactionContent,
964 reacted: Schema.Boolean,
965});
966export type PullRequestReactionInput = typeof PullRequestReactionInput.Type;
967
968export const PullRequestReviewerRequestInput = Schema.Struct({
969 ...PullRequestRef.fields,
970 reviewers: Schema.Array(
971 Schema.Struct({ id: TrimmedNonEmptyString, kind: PullRequestReviewerKind }),
972 ).check(Schema.isMinLength(1), Schema.isMaxLength(25)),
973 requested: Schema.Boolean,
974});
975export type PullRequestReviewerRequestInput = typeof PullRequestReviewerRequestInput.Type;
976
977export const PullRequestLabelChangeInput = Schema.Struct({
978 ...PullRequestRef.fields,
979 labels: Schema.Array(TrimmedNonEmptyString).check(Schema.isMinLength(1), Schema.isMaxLength(25)),
980 applied: Schema.Boolean,
981});
982export type PullRequestLabelChangeInput = typeof PullRequestLabelChangeInput.Type;
983
984/**
985 * The names keep their `cli-` prefix for wire compatibility; for a host read with a credential
986 * rather than a tool they mean "no credential" and "a refused credential".
987 */
988export const PullRequestUnavailableReason = Schema.Literals([
989 "cli-missing",
990 "cli-unauthenticated",
991 "provider-unsupported",
992]);
993export type PullRequestUnavailableReason = typeof PullRequestUnavailableReason.Type;
994
995/**
996 * `author:me` names whoever is signed in to the host being read, GitHub's `@me` spelled either
997 * way. A host's own search would answer it, but the page and the server both re-check an author
998 * against the row's login, so the name is resolved before either comparison rather than being
999 * matched as the literal login "me".
1000 */
1001export function resolvePullRequestAuthorFilter(
1002 author: string,
1003 viewer: string | null | undefined,
1004): string {
1005 const trimmed = author.trim();
1006 if (!/^@?me$/i.test(trimmed)) return trimmed;
1007 return viewer === null || viewer === undefined || viewer.trim().length === 0 ? trimmed : viewer;
1008}
1009
1010export class PullRequestUnavailableError extends Schema.TaggedError<PullRequestUnavailableError>()(
1011 "PullRequestUnavailableError",
1012 {
1013 reason: PullRequestUnavailableReason,
1014 provider: Schema.optional(SourceControlProviderKind),
1015 listResult: Schema.optional(PullRequestListResult),
1016 /**
1017 * What the host needs on the server, in its own words. Absent from older servers, which
1018 * shipped no such field, and for a host that names nothing.
1019 */
1020 requirement: Schema.optional(TrimmedNonEmptyString),
1021 cause: Schema.optional(Schema.Defect()),
1022 },
1023 { httpApiStatus: 503 },
1024) {
1025 [HttpServerRespondable.symbol]() {
1026 return HttpServerResponse.schemaJson(PullRequestUnavailableError)(this, { status: 503 });
1027 }
1028
1029 override get message(): string {
1030 switch (this.reason) {
1031 case "cli-missing":
1032 return this.requirement ?? "The tool this host is read through is not installed or set up.";
1033 case "cli-unauthenticated":
1034 return this.requirement ?? "This host has no working credentials.";
1035 case "provider-unsupported":
1036 return "Change requests cannot be browsed for this project's host yet.";
1037 }
1038 }
1039}
1040
1041export class PullRequestOperationError extends Schema.TaggedError<PullRequestOperationError>()(
1042 "PullRequestOperationError",
1043 {
1044 operation: Schema.String,
1045 detail: TrimmedNonEmptyString,
1046 reason: Schema.optional(Schema.Literal("not-found")),
1047 listResult: Schema.optional(PullRequestListResult),
1048 cause: Schema.optional(Schema.Defect()),
1049 },
1050 { httpApiStatus: 502 },
1051) {
1052 [HttpServerRespondable.symbol]() {
1053 return HttpServerResponse.schemaJson(PullRequestOperationError)(this, { status: 502 });
1054 }
1055
1056 override get message(): string {
1057 return `Pull request operation ${this.operation} failed: ${this.detail}`;
1058 }
1059}