@gitkraken/core-gitlens 0.5.108 → 0.5.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/dist/git/models/issue.d.ts +70 -0
  3. package/dist/git/models/issue.d.ts.map +1 -1
  4. package/dist/git/models/issue.js +17 -0
  5. package/dist/git/models/issue.js.map +1 -1
  6. package/dist/git/models/pullRequest.d.ts +81 -0
  7. package/dist/git/models/pullRequest.d.ts.map +1 -1
  8. package/dist/git/models/pullRequest.js +15 -0
  9. package/dist/git/models/pullRequest.js.map +1 -1
  10. package/dist/git/utils/issue.utils.d.ts +41 -1
  11. package/dist/git/utils/issue.utils.d.ts.map +1 -1
  12. package/dist/git/utils/issue.utils.js +80 -0
  13. package/dist/git/utils/issue.utils.js.map +1 -1
  14. package/dist/git/utils/pullRequest.utils.d.ts +11 -1
  15. package/dist/git/utils/pullRequest.utils.d.ts.map +1 -1
  16. package/dist/git/utils/pullRequest.utils.js +40 -0
  17. package/dist/git/utils/pullRequest.utils.js.map +1 -1
  18. package/dist/plus/git-github/api/github.d.ts +70 -10
  19. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  20. package/dist/plus/git-github/api/github.js +284 -31
  21. package/dist/plus/git-github/api/github.js.map +1 -1
  22. package/dist/plus/git-github/api/issueSearchQuery.d.ts +49 -5
  23. package/dist/plus/git-github/api/issueSearchQuery.d.ts.map +1 -1
  24. package/dist/plus/git-github/api/issueSearchQuery.js +64 -6
  25. package/dist/plus/git-github/api/issueSearchQuery.js.map +1 -1
  26. package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts +27 -1
  27. package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts.map +1 -1
  28. package/dist/plus/git-github/api/pullRequestSearchQuery.js +73 -6
  29. package/dist/plus/git-github/api/pullRequestSearchQuery.js.map +1 -1
  30. package/dist/plus/git-github/models.d.ts +17 -5
  31. package/dist/plus/git-github/models.d.ts.map +1 -1
  32. package/dist/plus/git-github/models.js +14 -1
  33. package/dist/plus/git-github/models.js.map +1 -1
  34. package/dist/plus/integrations/collectionMetadata.d.ts +18 -0
  35. package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
  36. package/dist/plus/integrations/collectionMetadata.js +24 -0
  37. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  38. package/dist/plus/integrations/integrationService.d.ts +31 -10
  39. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  40. package/dist/plus/integrations/integrationService.js +9 -1
  41. package/dist/plus/integrations/integrationService.js.map +1 -1
  42. package/dist/plus/integrations/manager.d.ts +85 -31
  43. package/dist/plus/integrations/manager.d.ts.map +1 -1
  44. package/dist/plus/integrations/models/gitHostIntegration.d.ts +18 -31
  45. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  46. package/dist/plus/integrations/models/gitHostIntegration.js +29 -1
  47. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  48. package/dist/plus/integrations/models/integration.d.ts +1 -1
  49. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  50. package/dist/plus/integrations/models/integration.js.map +1 -1
  51. package/dist/plus/integrations/models/issueReads.d.ts +63 -1
  52. package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
  53. package/dist/plus/integrations/models/issuesIntegration.d.ts +7 -21
  54. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
  55. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  56. package/dist/plus/integrations/providerFilters.d.ts +2 -2
  57. package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
  58. package/dist/plus/integrations/providers/accounts.d.ts +6 -0
  59. package/dist/plus/integrations/providers/accounts.d.ts.map +1 -0
  60. package/dist/plus/integrations/providers/accounts.js +30 -0
  61. package/dist/plus/integrations/providers/accounts.js.map +1 -0
  62. package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
  63. package/dist/plus/integrations/providers/azureDevOps.js +1 -0
  64. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  65. package/dist/plus/integrations/providers/github.d.ts +9 -0
  66. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  67. package/dist/plus/integrations/providers/github.js +115 -135
  68. package/dist/plus/integrations/providers/github.js.map +1 -1
  69. package/dist/plus/integrations/providers/gitlab.d.ts +9 -0
  70. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
  71. package/dist/plus/integrations/providers/gitlab.js +24 -4
  72. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  73. package/dist/plus/integrations/providers/issueSorts.d.ts +36 -0
  74. package/dist/plus/integrations/providers/issueSorts.d.ts.map +1 -0
  75. package/dist/plus/integrations/providers/issueSorts.js +76 -0
  76. package/dist/plus/integrations/providers/issueSorts.js.map +1 -0
  77. package/dist/plus/integrations/providers/jira.d.ts +3 -9
  78. package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
  79. package/dist/plus/integrations/providers/jira.js +5 -1
  80. package/dist/plus/integrations/providers/jira.js.map +1 -1
  81. package/dist/plus/integrations/providers/linear.d.ts +24 -9
  82. package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
  83. package/dist/plus/integrations/providers/linear.js +63 -5
  84. package/dist/plus/integrations/providers/linear.js.map +1 -1
  85. package/dist/plus/integrations/providers/models.d.ts +76 -38
  86. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  87. package/dist/plus/integrations/providers/models.js +91 -105
  88. package/dist/plus/integrations/providers/models.js.map +1 -1
  89. package/dist/plus/integrations/providers/providersApi.d.ts +18 -3
  90. package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
  91. package/dist/plus/integrations/providers/providersApi.js +24 -7
  92. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  93. package/dist/plus/integrations/providers/pullRequestReviews.d.ts +50 -0
  94. package/dist/plus/integrations/providers/pullRequestReviews.d.ts.map +1 -0
  95. package/dist/plus/integrations/providers/pullRequestReviews.js +99 -0
  96. package/dist/plus/integrations/providers/pullRequestReviews.js.map +1 -0
  97. package/dist/plus/integrations/providers/trello.d.ts +3 -9
  98. package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
  99. package/dist/plus/integrations/providers/trello.js +1 -0
  100. package/dist/plus/integrations/providers/trello.js.map +1 -1
  101. package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
  102. package/dist/plus/integrations/providers/utils/providerPaging.js +5 -1
  103. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  104. package/dist/plus/integrations/reads/broaden.d.ts +6 -0
  105. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
  106. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  107. package/dist/plus/integrations/reads/counts.d.ts +63 -1
  108. package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
  109. package/dist/plus/integrations/reads/counts.js +146 -2
  110. package/dist/plus/integrations/reads/counts.js.map +1 -1
  111. package/dist/plus/integrations/reads/drains.d.ts +7 -1
  112. package/dist/plus/integrations/reads/drains.d.ts.map +1 -1
  113. package/dist/plus/integrations/reads/drains.js +13 -2
  114. package/dist/plus/integrations/reads/drains.js.map +1 -1
  115. package/dist/plus/integrations/reads/filters.d.ts +59 -9
  116. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  117. package/dist/plus/integrations/reads/filters.js +39 -0
  118. package/dist/plus/integrations/reads/filters.js.map +1 -1
  119. package/dist/plus/integrations/reads/issueTracker.d.ts +18 -1
  120. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  121. package/dist/plus/integrations/reads/issueTracker.js +40 -3
  122. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  123. package/dist/plus/integrations/reads/issues.d.ts +27 -3
  124. package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
  125. package/dist/plus/integrations/reads/issues.js +223 -123
  126. package/dist/plus/integrations/reads/issues.js.map +1 -1
  127. package/dist/plus/integrations/reads/ordering.d.ts +101 -0
  128. package/dist/plus/integrations/reads/ordering.d.ts.map +1 -0
  129. package/dist/plus/integrations/reads/ordering.js +48 -0
  130. package/dist/plus/integrations/reads/ordering.js.map +1 -0
  131. package/dist/plus/integrations/reads/searchIssues.d.ts +7 -2
  132. package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -1
  133. package/dist/plus/integrations/reads/searchIssues.js +9 -3
  134. package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
  135. package/dist/plus/integrations/reads/searchPullRequests.d.ts +4 -3
  136. package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -1
  137. package/dist/plus/integrations/reads/searchPullRequests.js +6 -4
  138. package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
  139. package/dist/plus/integrations/reads/sweeps.js +1 -1
  140. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  141. package/dist/plus/integrations/reads/warnings.d.ts +35 -3
  142. package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
  143. package/dist/plus/integrations/reads/warnings.js +58 -4
  144. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  145. package/dist/plus/integrations/results.d.ts +15 -0
  146. package/dist/plus/integrations/results.d.ts.map +1 -1
  147. package/dist/plus/integrations/results.js.map +1 -1
  148. package/docs/integrations.md +76 -10
  149. package/docs/kepler-read-api-parity.md +9 -0
  150. package/package.json +2 -2
  151. package/src/git/models/issue.ts +73 -0
  152. package/src/git/models/pullRequest.ts +84 -0
  153. package/src/git/utils/issue.utils.ts +91 -1
  154. package/src/git/utils/pullRequest.utils.ts +50 -0
  155. package/src/plus/git-github/api/github.ts +356 -36
  156. package/src/plus/git-github/api/issueSearchQuery.ts +67 -7
  157. package/src/plus/git-github/api/pullRequestSearchQuery.ts +81 -7
  158. package/src/plus/git-github/models.ts +29 -5
  159. package/src/plus/integrations/collectionMetadata.ts +24 -0
  160. package/src/plus/integrations/integrationService.ts +40 -12
  161. package/src/plus/integrations/manager.ts +89 -35
  162. package/src/plus/integrations/models/gitHostIntegration.ts +47 -31
  163. package/src/plus/integrations/models/integration.ts +1 -0
  164. package/src/plus/integrations/models/issueReads.ts +65 -1
  165. package/src/plus/integrations/models/issuesIntegration.ts +7 -6
  166. package/src/plus/integrations/providerFilters.ts +8 -1
  167. package/src/plus/integrations/providers/accounts.ts +34 -0
  168. package/src/plus/integrations/providers/azureDevOps.ts +1 -0
  169. package/src/plus/integrations/providers/github.ts +134 -144
  170. package/src/plus/integrations/providers/gitlab.ts +29 -10
  171. package/src/plus/integrations/providers/issueSorts.ts +89 -0
  172. package/src/plus/integrations/providers/jira.ts +9 -3
  173. package/src/plus/integrations/providers/linear.ts +77 -8
  174. package/src/plus/integrations/providers/models.ts +185 -128
  175. package/src/plus/integrations/providers/providersApi.ts +51 -13
  176. package/src/plus/integrations/providers/pullRequestReviews.ts +122 -0
  177. package/src/plus/integrations/providers/trello.ts +4 -3
  178. package/src/plus/integrations/providers/utils/providerPaging.ts +6 -1
  179. package/src/plus/integrations/reads/broaden.ts +6 -0
  180. package/src/plus/integrations/reads/counts.ts +281 -3
  181. package/src/plus/integrations/reads/drains.ts +12 -2
  182. package/src/plus/integrations/reads/filters.ts +92 -8
  183. package/src/plus/integrations/reads/issueTracker.ts +78 -4
  184. package/src/plus/integrations/reads/issues.ts +386 -194
  185. package/src/plus/integrations/reads/ordering.ts +124 -0
  186. package/src/plus/integrations/reads/searchIssues.ts +15 -3
  187. package/src/plus/integrations/reads/searchPullRequests.ts +12 -4
  188. package/src/plus/integrations/reads/sweeps.ts +1 -1
  189. package/src/plus/integrations/reads/warnings.ts +84 -2
  190. package/src/plus/integrations/results.ts +15 -0
@@ -0,0 +1,122 @@
1
+ import type { GitPullRequest } from '@gitkraken/provider-apis';
2
+ import { GitPullRequestReviewState } from '@gitkraken/provider-apis';
3
+ import type { PullRequestReviewer } from '../../../git/models/pullRequest.js';
4
+ import { PullRequestReviewDecision, PullRequestReviewState } from '../../../git/models/pullRequest.js';
5
+ import { fromProviderAccount, toProviderAccount } from './accounts.js';
6
+
7
+ /**
8
+ * The GitLens-local extension of provider-apis' review shape, in two parts: a `DISMISSED` state the upstream
9
+ * `GitPullRequestReviewState` has no member for, and the head-commit oid a review was submitted against
10
+ * (optional, so a raw SDK review stays assignable — only the GitHub path populates it).
11
+ *
12
+ * Both exist for the same case: on GitHub the `dismiss stale reviews` rule flips an approval to dismissed on
13
+ * the next push, which is exactly the "the PR moved past my review" situation the oid detects. Dropping such a
14
+ * review instead would hand a consumer a PR out of the `reviewed-by:@me` set with no review row at all,
15
+ * indistinguishable from never having reviewed it.
16
+ */
17
+ export const providerPullRequestReviewStateDismissed = 'DISMISSED' as const;
18
+ export type ProviderPullRequestReview = Omit<NonNullable<GitPullRequest['reviews']>[number], 'state'> & {
19
+ state: GitPullRequestReviewState | typeof providerPullRequestReviewStateDismissed;
20
+ commitOid?: string;
21
+ };
22
+ /**
23
+ * The review list as it travels on {@link ProviderPullRequest}: `null` when the read carried no review data at
24
+ * all, as opposed to an empty array for a pull request nobody has reviewed.
25
+ */
26
+ export type ProviderPullRequestReviews = ProviderPullRequestReview[] | null;
27
+
28
+ export const toProviderPullRequestReviewState = {
29
+ [PullRequestReviewState.Approved]: GitPullRequestReviewState.Approved,
30
+ [PullRequestReviewState.ChangesRequested]: GitPullRequestReviewState.ChangesRequested,
31
+ [PullRequestReviewState.Commented]: GitPullRequestReviewState.Commented,
32
+ [PullRequestReviewState.ReviewRequested]: GitPullRequestReviewState.ReviewRequested,
33
+ [PullRequestReviewState.Dismissed]: providerPullRequestReviewStateDismissed,
34
+ // A review the author started but never submitted. Visible only to that author and carrying no verdict, so
35
+ // it stays unmapped and is dropped by the projection.
36
+ [PullRequestReviewState.Pending]: null,
37
+ };
38
+
39
+ export const fromProviderPullRequestReviewState = {
40
+ [GitPullRequestReviewState.Approved]: PullRequestReviewState.Approved,
41
+ [GitPullRequestReviewState.ChangesRequested]: PullRequestReviewState.ChangesRequested,
42
+ [GitPullRequestReviewState.Commented]: PullRequestReviewState.Commented,
43
+ [GitPullRequestReviewState.ReviewRequested]: PullRequestReviewState.ReviewRequested,
44
+ [providerPullRequestReviewStateDismissed]: PullRequestReviewState.Dismissed,
45
+ };
46
+
47
+ export function toProviderReviews(reviewers: PullRequestReviewer[]): ProviderPullRequestReviews {
48
+ // Only `Pending` maps to null (see `toProviderPullRequestReviewState`), so this drops exactly the reviews
49
+ // that carry no verdict rather than defaulting them to `ReviewRequested` — which would report an unsubmitted
50
+ // draft as a pending request from that reviewer.
51
+ return reviewers
52
+ .filter(r => r.state !== PullRequestReviewState.Pending)
53
+ .map(reviewer => ({
54
+ reviewer: toProviderAccount(reviewer.reviewer),
55
+ state: toProviderPullRequestReviewState[reviewer.state] ?? GitPullRequestReviewState.ReviewRequested,
56
+ commitOid: reviewer.commitOid,
57
+ }));
58
+ }
59
+
60
+ export function toReviewRequests(reviews: ProviderPullRequestReviews): PullRequestReviewer[] | undefined {
61
+ return reviews == null
62
+ ? undefined
63
+ : reviews
64
+ ?.filter(r => r.state === GitPullRequestReviewState.ReviewRequested)
65
+ .map(r => ({
66
+ isCodeOwner: false, // TODO: Find this value, and implement in the shared lib if needed
67
+ reviewer: fromProviderAccount(r.reviewer),
68
+ state: PullRequestReviewState.ReviewRequested,
69
+ }));
70
+ }
71
+
72
+ export function toCompletedReviews(reviews: ProviderPullRequestReviews): PullRequestReviewer[] | undefined {
73
+ return reviews == null
74
+ ? undefined
75
+ : reviews
76
+ ?.filter(
77
+ r =>
78
+ r.state !== GitPullRequestReviewState.ReviewRequested &&
79
+ // provider-apis' own GitHub normalizer maps only its four known states, so a review it has no
80
+ // member for (a real `DISMISSED` one, which its `latestReviews(first: 100)` selection does
81
+ // return) arrives with `state: undefined` — a value its type says cannot happen. Publishing it
82
+ // would put an unswitchable state on `PullRequestShape.latestReviews`, and mapping back would
83
+ // hit `toProviderReviews`' fallback and report a dismissed review as an outstanding request.
84
+ // Our own GitHub path never lands here: it carries `providerPullRequestReviewStateDismissed`.
85
+ fromProviderPullRequestReviewState[r.state] != null,
86
+ )
87
+ .map(r => ({
88
+ isCodeOwner: false, // TODO: Find this value, and implement in the shared lib if needed
89
+ reviewer: fromProviderAccount(r.reviewer),
90
+ state: fromProviderPullRequestReviewState[r.state],
91
+ commitOid: r.commitOid,
92
+ }));
93
+ }
94
+
95
+ export function toProviderReviewDecision(
96
+ reviewDecision?: PullRequestReviewDecision,
97
+ reviewers?: PullRequestReviewer[],
98
+ ): GitPullRequestReviewState | null {
99
+ switch (reviewDecision) {
100
+ case PullRequestReviewDecision.Approved:
101
+ return GitPullRequestReviewState.Approved;
102
+ case PullRequestReviewDecision.ChangesRequested:
103
+ return GitPullRequestReviewState.ChangesRequested;
104
+ case PullRequestReviewDecision.ReviewRequired:
105
+ return GitPullRequestReviewState.ReviewRequested;
106
+ default: {
107
+ if (reviewers?.some(r => r.state === PullRequestReviewState.ReviewRequested)) {
108
+ return GitPullRequestReviewState.ReviewRequested;
109
+ } else if (reviewers?.some(r => r.state === PullRequestReviewState.Commented)) {
110
+ return GitPullRequestReviewState.Commented;
111
+ }
112
+ return null;
113
+ }
114
+ }
115
+ }
116
+
117
+ export const fromPullRequestReviewDecision = {
118
+ [GitPullRequestReviewState.Approved]: PullRequestReviewDecision.Approved,
119
+ [GitPullRequestReviewState.ChangesRequested]: PullRequestReviewDecision.ChangesRequested,
120
+ [GitPullRequestReviewState.Commented]: undefined,
121
+ [GitPullRequestReviewState.ReviewRequested]: PullRequestReviewDecision.ReviewRequired,
122
+ };
@@ -9,8 +9,8 @@ import type { ProviderAuthenticationSession } from '../authentication/models.js'
9
9
  import { toTokenWithInfo } from '../authentication/models.js';
10
10
  import { IssuesCloudHostIntegrationId } from '../constants.js';
11
11
  import { IntegrationReadUnavailableError } from '../errors.js';
12
+ import type { IssuesForProjectOptions } from '../models/issueReads.js';
12
13
  import { IssuesIntegration } from '../models/issuesIntegration.js';
13
- import type { IssueFilter } from './models.js';
14
14
  import { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';
15
15
 
16
16
  const metadata = providersMetadata[IssuesCloudHostIntegrationId.Trello];
@@ -95,7 +95,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
95
95
  protected override async getProviderIssuesForProject(
96
96
  session: ProviderAuthenticationSession,
97
97
  project: ResourceDescriptor,
98
- options?: { user?: string; filters?: IssueFilter[] },
98
+ options?: IssuesForProjectOptions,
99
99
  ): Promise<IssueShape[] | undefined> {
100
100
  return (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;
101
101
  }
@@ -103,7 +103,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
103
103
  protected override async getProviderIssuesForProjectWithTruncation(
104
104
  session: ProviderAuthenticationSession,
105
105
  project: ResourceDescriptor,
106
- options?: { user?: string; filters?: IssueFilter[] },
106
+ options?: IssuesForProjectOptions,
107
107
  ): Promise<{ values: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata } | undefined> {
108
108
  // A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.
109
109
  if (!isIssueResourceDescriptor(project)) return undefined;
@@ -129,6 +129,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
129
129
  const result = await api.getTrelloIssuesForBoard(tokenWithInfo, appKey, project.id, {
130
130
  assigneeLogins: options?.user != null ? [options.user] : undefined,
131
131
  trelloBoardListsById: trelloBoardListsById,
132
+ sort: options?.sort,
132
133
  });
133
134
 
134
135
  const values = result.values.flatMap(issue => {
@@ -6,7 +6,7 @@ import type {
6
6
  } from '@gitkraken/provider-apis';
7
7
  import { isCancellationError } from '../../../../utils/cancellation.js';
8
8
  import { uniqueBy } from '../../../../utils/iterable.js';
9
- import { toCollectionScopeFailure } from '../../collectionMetadata.js';
9
+ import { throwIfCallerContractError, toCollectionScopeFailure } from '../../collectionMetadata.js';
10
10
  import { collectionScopeKey } from '../../results.js';
11
11
  import type { ProviderApiPagedResult, ProviderHierarchyResult } from '../models.js';
12
12
 
@@ -144,6 +144,11 @@ export async function collectProviderPagedResult<T>(
144
144
  } catch (ex) {
145
145
  if (isCancellationError(ex)) throw ex;
146
146
 
147
+ // A caller-contract error is not a fact about this scope, so it is never recorded as one — see
148
+ // `throwIfCallerContractError`. Checked alongside cancellation because both are errors that a
149
+ // per-scope failure would misdescribe.
150
+ throwIfCallerContractError(ex);
151
+
147
152
  // When the caller supplied a scope, preserve the items already fetched from that scope and record the
148
153
  // failure in collection metadata rather than re-throwing and discarding the prefix. Callers without a
149
154
  // scope keep the legacy throw behavior.
@@ -33,6 +33,12 @@ import { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, noConnectionWarnin
33
33
  * `includeAllAssignees: true`, which resolves to no assignee constraint at all, so unassigned issues ARE
34
34
  * included. The equivalent is an OMITTED `relationships`, not `['any-assignee']`: `assignee:*` means "has some
35
35
  * assignee" and would silently exclude every unassigned issue, which is the opposite of broadening.
36
+ *
37
+ * Takes no `sort`, unlike `listIssuesPage` and `searchIssuesPage`. One logical page here spans several orgs, each
38
+ * at its own provider position in a cursor bundle, so honoring an order across them would need a k-way merge with
39
+ * a buffer per org rather than a sort of what arrived — the page is a slice of several independent walks, not a
40
+ * union of one round's results. `searchIssuesPage({ repos, criteria: { sort } })` answers the ordered version of
41
+ * this question for a caller that knows its repositories, and is already the recommended migration above.
36
42
  */
37
43
 
38
44
  export interface BroadenIssuesOptions {
@@ -1,4 +1,5 @@
1
1
  import type { IssueSearchCriteria } from '../../../git/models/issue.js';
2
+ import type { PullRequestSearchCriteria } from '../../../git/models/pullRequest.js';
2
3
  import { chunk } from '../../../utils/array.js';
3
4
  import { mapBounded } from '../../../utils/promise.js';
4
5
  import type { IntegrationIds } from '../constants.js';
@@ -14,12 +15,18 @@ import {
14
15
  } from '../utils/integration.utils.js';
15
16
  import type { ProviderReadContext } from './context.js';
16
17
  import { runCaptured } from './drains.js';
17
- import { resolveIssueSearchCriteria, resolveIssueSearchScope } from './filters.js';
18
+ import {
19
+ resolveIssueSearchCriteria,
20
+ resolveIssueSearchScope,
21
+ resolvePullRequestSearchCriteria,
22
+ resolvePullRequestSearchScope,
23
+ } from './filters.js';
18
24
  import {
19
25
  gitHostOnlySurfaceWarning,
20
26
  issuesUnsupportedWarning,
21
27
  otherWarning,
22
28
  unsupportedIssueSearchCriteriaWarning,
29
+ unsupportedPullRequestSearchCriteriaWarning,
23
30
  } from './warnings.js';
24
31
 
25
32
  /**
@@ -32,6 +39,15 @@ import {
32
39
  */
33
40
  const issueCountChunkSize = 25;
34
41
 
42
+ /**
43
+ * How many pull-request scopes go into one upstream request.
44
+ *
45
+ * The same latency/complexity reasoning as {@link issueCountChunkSize}, kept slightly smaller because a pull-request
46
+ * scope can fan out into one aliased count PER requested state (open/closed/merged), so a chunk of scopes carries a
47
+ * small multiple of that many `search` aliases.
48
+ */
49
+ const pullRequestCountChunkSize = 15;
50
+
35
51
  /**
36
52
  * One scope to count.
37
53
  *
@@ -44,7 +60,14 @@ export interface IssueCountScope {
44
60
  /** Repositories to count over. Combines with `org`, exactly as in `searchIssuesPage`. */
45
61
  repos?: ProviderReposInput;
46
62
  org?: string;
47
- /** Same criteria model as `searchIssuesPage`, validated against the same capability table. */
63
+ /**
64
+ * Same criteria model as `searchIssuesPage`, validated against the same capability table.
65
+ *
66
+ * `criteria.sort` does not affect the count — a total is invariant under ordering — but it is still VALIDATED,
67
+ * and an unsupported key refuses this scope exactly as it would refuse the read. That is the point of the
68
+ * count: it previews the constraints the read will apply, so one that accepted what the read refuses would
69
+ * promise a fetch that can't happen, which is worse than no preview at all.
70
+ */
48
71
  criteria?: IssueSearchCriteria;
49
72
  }
50
73
 
@@ -66,6 +89,42 @@ export interface IssueCountResult {
66
89
  providerLimit?: number;
67
90
  }
68
91
 
92
+ /**
93
+ * One scope to count pull requests over — the PR twin of {@link IssueCountScope}.
94
+ *
95
+ * `key` is caller-owned and echoed back verbatim, never reaching the provider query (the aliases are generated), so
96
+ * it can be any string the caller finds meaningful.
97
+ */
98
+ export interface PullRequestCountScope {
99
+ key: string;
100
+ /** Repositories to count over. Combines with `org`, exactly as in `searchPullRequestsPage`. */
101
+ repos?: ProviderReposInput;
102
+ org?: string;
103
+ /**
104
+ * Same criteria model as `searchPullRequestsPage`, validated against the same capability table — so a count
105
+ * refuses exactly what the read would, and never previews a fetch that can't happen.
106
+ */
107
+ criteria?: PullRequestSearchCriteria;
108
+ }
109
+
110
+ /** The count for one {@link PullRequestCountScope}, echoed back under the caller's own `key`. */
111
+ export interface PullRequestCountResult {
112
+ key: string;
113
+ /**
114
+ * Total matches the provider reports. `undefined` when the provider didn't report one for this scope — NEVER
115
+ * zero, which is a real answer. For a multi-state scope this is the LARGEST of its per-state counts, the same
116
+ * total `searchPullRequestsPage` surfaces, not their sum.
117
+ */
118
+ count?: number;
119
+ /**
120
+ * True when `count` exceeds the provider's own per-search result ceiling, so a full read CANNOT return
121
+ * everything no matter how it is paged. This is the signal to warn before starting an expensive fetch.
122
+ */
123
+ exceedsProviderLimit: boolean;
124
+ /** The ceiling itself, when the provider declares one. */
125
+ providerLimit?: number;
126
+ }
127
+
69
128
  /**
70
129
  * Counts issues for many scopes without fetching any — the probe behind a "this will fetch ~N issues" preview.
71
130
  *
@@ -221,7 +280,7 @@ export async function countIssues(
221
280
  }
222
281
 
223
282
  /** The first key that appears twice, or `undefined` when every key is unique. */
224
- function findDuplicateKey(scopes: readonly IssueCountScope[]): string | undefined {
283
+ function findDuplicateKey(scopes: readonly { key: string }[]): string | undefined {
225
284
  const seen = new Set<string>();
226
285
  for (const scope of scopes) {
227
286
  if (seen.has(scope.key)) return scope.key;
@@ -283,3 +342,222 @@ function rejectScope(
283
342
 
284
343
  return undefined;
285
344
  }
345
+
346
+ /**
347
+ * Counts pull requests for many scopes without fetching any — the PR twin of {@link countIssues}, and the probe
348
+ * behind a "this will fetch ~N pull requests" preview.
349
+ *
350
+ * Identical batching, per-scope isolation, and `key`-echo contract as {@link countIssues}; see it for the cost
351
+ * model and the `count: undefined` ≠ zero rule. The one difference is inherent to pull requests: a scope's criteria
352
+ * can name several STATES, which the provider counts as independent searches — the reported `count` is the LARGEST
353
+ * of them (mirroring `searchPullRequestsPage`'s total), so an unpaged read still can't exceed a single search's
354
+ * ceiling undetected.
355
+ */
356
+ export async function countPullRequests(
357
+ ctx: ProviderReadContext,
358
+ options: {
359
+ providerId: IntegrationIds;
360
+ scopes: readonly PullRequestCountScope[];
361
+ connectionId?: string;
362
+ /**
363
+ * Explicit self-managed host domain. Used only when the requested connection has no configured domain;
364
+ * it must come from the trusted authentication configuration, not repository or remote data.
365
+ */
366
+ domain?: string;
367
+ },
368
+ ): Promise<ProviderResult<PullRequestCountResult>> {
369
+ const refused = (warning: ProviderWarning): ProviderResult<PullRequestCountResult> => ({
370
+ items: [],
371
+ warnings: [warning],
372
+ fetchFailed: true,
373
+ });
374
+
375
+ if (isIssuesHostIntegrationId(options.providerId)) {
376
+ return refused(
377
+ gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Pull request counts'),
378
+ );
379
+ }
380
+
381
+ // Nothing was asked for, so nothing is missing: an empty success, not a refusal.
382
+ if (options.scopes.length === 0) return { items: [], warnings: [] };
383
+
384
+ const duplicateKey = findDuplicateKey(options.scopes);
385
+ if (duplicateKey != null) {
386
+ // Refuses the whole call rather than deduping: `key` exists so the caller can match results without
387
+ // positional bookkeeping, and two results under one key make that ambiguous for EVERY scope, not just the
388
+ // repeated one.
389
+ return refused(
390
+ otherWarning(
391
+ options.providerId,
392
+ undefined,
393
+ options.connectionId,
394
+ `Duplicate pull request count scope key '${duplicateKey}'; keys identify results, so each must be unique.`,
395
+ ),
396
+ );
397
+ }
398
+
399
+ const integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);
400
+ if (integration == null) {
401
+ // A supplied connection or domain that no longer resolves is a broken target, not an empty account.
402
+ const early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);
403
+ return { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };
404
+ }
405
+ if (!isGitHostIntegration(integration)) {
406
+ return refused(
407
+ gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Pull request counts'),
408
+ );
409
+ }
410
+
411
+ const domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);
412
+ const warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);
413
+
414
+ // A provider with no filtered pull-request search has no count either: refuse ONCE for the provider rather than
415
+ // letting every scope repeat the same rejection. `undefined` criteria probes only the search's existence.
416
+ const searchSupport = resolvePullRequestSearchCriteria(options.providerId, undefined);
417
+ if (searchSupport.rejection != null) {
418
+ return refused(
419
+ unsupportedPullRequestSearchCriteriaWarning(
420
+ options.providerId,
421
+ domain,
422
+ options.connectionId,
423
+ searchSupport.rejection,
424
+ ),
425
+ );
426
+ }
427
+
428
+ const providerLimit = providersMetadata[options.providerId]?.pullRequestSearchResultLimit;
429
+ const warnings: ProviderWarning[] = [];
430
+ let fetchFailed = false;
431
+
432
+ // Validate every scope first, so a refusal costs no request at all and the countable ones are still batched
433
+ // together.
434
+ const countable: PullRequestCountScope[] = [];
435
+ for (const scope of options.scopes) {
436
+ const warning = rejectPullRequestScope(options.providerId, domain, options.connectionId, scope);
437
+ if (warning != null) {
438
+ // `push`, not `appendDedupedWarning`: every rejection message embeds the scope's own key, and duplicate
439
+ // keys were already refused above, so no two of these can ever collapse.
440
+ warnings.push(warning);
441
+ fetchFailed = true;
442
+ continue;
443
+ }
444
+
445
+ countable.push(scope);
446
+ }
447
+
448
+ // Chunks are independent requests over their own slice of scopes, `runCaptured` never throws, so they run
449
+ // concurrently, bounded like every other fan-out on the facade. `mapBounded` returns in input order, so `items`
450
+ // and `warnings` stay in scope order.
451
+ const batches = await mapBounded(chunk(countable, pullRequestCountChunkSize), providerFanOutConcurrency, batch =>
452
+ runCaptured(
453
+ options.providerId,
454
+ domain,
455
+ options.connectionId,
456
+ () =>
457
+ integration.countPullRequestsResult(
458
+ batch.map(s => ({
459
+ repos: s.repos as ProviderRepoInput[] | undefined,
460
+ org: s.org,
461
+ criteria: s.criteria,
462
+ })),
463
+ undefined,
464
+ options.connectionId,
465
+ ),
466
+ { warnOnMissingSession: warnOnMissingSession },
467
+ ).then(result => ({ batch: batch, ...result })),
468
+ );
469
+
470
+ const items: PullRequestCountResult[] = [];
471
+ for (const { batch, value, warning } of batches) {
472
+ if (warning != null) {
473
+ appendDedupedWarning(warnings, warning);
474
+ }
475
+ if (value == null) {
476
+ // This batch contributes nothing, but the batches around it still do. Drop only these scopes.
477
+ fetchFailed = true;
478
+ continue;
479
+ }
480
+
481
+ for (let i = 0; i < batch.length; i++) {
482
+ const count = value[i];
483
+ items.push({
484
+ key: batch[i].key,
485
+ count: count,
486
+ // Only a reported count can exceed a declared ceiling; unknown-vs-limit is not a comparison.
487
+ exceedsProviderLimit: count != null && providerLimit != null && count > providerLimit,
488
+ providerLimit: providerLimit,
489
+ });
490
+ }
491
+ }
492
+
493
+ // A provider that reported nothing for any scope and raised no warning: say so explicitly rather than letting it
494
+ // read as "every scope matched nothing".
495
+ if (items.length === 0 && warnings.length === 0) {
496
+ return refused(
497
+ unsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {
498
+ reason: 'unsupported-search',
499
+ }),
500
+ );
501
+ }
502
+
503
+ return { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };
504
+ }
505
+
506
+ /**
507
+ * Why one pull-request scope can't be counted, as the warning to report — or `undefined` when it can. The PR twin
508
+ * of {@link rejectScope}, validating against the same {@link resolvePullRequestSearchScope} /
509
+ * {@link resolvePullRequestSearchCriteria} the read uses, so a count always previews the constraints the read
510
+ * would apply.
511
+ */
512
+ function rejectPullRequestScope(
513
+ providerId: IntegrationIds,
514
+ domain: string | undefined,
515
+ connectionId: string | undefined,
516
+ scope: PullRequestCountScope,
517
+ ): ProviderWarning | undefined {
518
+ const scoping = resolvePullRequestSearchScope(providerId, scope.repos, scope.org, scope.criteria);
519
+ switch (scoping.rejection) {
520
+ case 'repo-ids':
521
+ return otherWarning(
522
+ providerId,
523
+ domain,
524
+ connectionId,
525
+ `Pull request count scope '${scope.key}' cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.`,
526
+ );
527
+ case 'unscoped':
528
+ return otherWarning(
529
+ providerId,
530
+ domain,
531
+ connectionId,
532
+ `Pull request count scope '${scope.key}' is unscoped; pass \`repos\`, \`org\`, or at least one current-user relationship.`,
533
+ );
534
+ case 'unsupported-repository-scope':
535
+ case 'unsupported-organization-scope':
536
+ return unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, {
537
+ reason: 'unsupported-criteria',
538
+ criteria: [
539
+ scoping.rejection === 'unsupported-repository-scope' ? 'repositoryScope' : 'organizationScope',
540
+ ],
541
+ });
542
+ }
543
+
544
+ const resolved = resolvePullRequestSearchCriteria(providerId, scope.criteria);
545
+ if (resolved.rejection != null) {
546
+ return unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);
547
+ }
548
+
549
+ // Count-only, with no counterpart in the read: a relationship set is an OR across several searches, which one
550
+ // count can't express — summing would double-count overlaps and max would under-report. Ask the caller to count
551
+ // each relationship as its own scope, where the keys make the OR explicit. (States are disjoint, so a scope may
552
+ // still name several — the provider counts them as the max, not a refusal.)
553
+ if ((scope.criteria?.relationships?.length ?? 0) > 1) {
554
+ return otherWarning(
555
+ providerId,
556
+ domain,
557
+ connectionId,
558
+ `Pull request count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`,
559
+ );
560
+ }
561
+
562
+ return undefined;
563
+ }
@@ -70,7 +70,12 @@ export async function drainPullRequests(
70
70
  repos: ProviderReposInput,
71
71
  state: PullRequestStateFilter[] | undefined,
72
72
  filters: PullRequestFilter[] | undefined,
73
- includeReviewRequested: boolean,
73
+ /**
74
+ * Breadth opt-ins that only the account-wide read can express. Grouped rather than passed positionally
75
+ * because they are same-typed neighbors a transposition would silently swap, and picked from the sweep
76
+ * options rather than restated so a third opt-in is declared once.
77
+ */
78
+ accountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined,
74
79
  connectionId: string | undefined,
75
80
  maxPages: number,
76
81
  attributeUnavailableProvider: boolean,
@@ -100,6 +105,10 @@ export async function drainPullRequests(
100
105
  // With no repos this is an account-wide "my PRs" sweep. The repo-scoped core rejects an empty `repos`
101
106
  // input, so read the provider-native account-wide core instead.
102
107
  const accountWide = repos.length === 0;
108
+ // The breadth opt-ins are only expressible on the account-wide core, so gate them here — the single place
109
+ // that already knows which core it is about to call.
110
+ const includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);
111
+ const includeReviews = accountWide && (accountWideOptions?.includeReviews ?? false);
103
112
  /** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */
104
113
  const seenCursors = new Set<string>();
105
114
 
@@ -115,7 +124,8 @@ export async function drainPullRequests(
115
124
  cursor: pageCursor,
116
125
  includeReviewRequested: includeReviewRequested,
117
126
  filters: filters,
118
- summary: true,
127
+ // Lite by default; the full projection (reviews) only when a caller opts in.
128
+ summary: !includeReviews,
119
129
  },
120
130
  connectionId,
121
131
  )