@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
@@ -1,3 +1,4 @@
1
+ import { gitHubPullRequestRelationshipQualifiers } from '../../git-github/api/pullRequestSearchQuery.js';
1
2
  import type { Account, UnidentifiedAuthor } from '../../../git/models/author.js';
2
3
  import type { DefaultBranch } from '../../../git/models/defaultBranch.js';
3
4
  import type { Issue, IssueSearchCriteria, IssueShape } from '../../../git/models/issue.js';
@@ -50,17 +51,6 @@ import {
50
51
  } from './models.js';
51
52
  import type { ProvidersApi } from './providersApi.js';
52
53
 
53
- /**
54
- * Page size for GitHub's account-wide search reads: the maximum GitHub's search connection accepts, and what
55
- * `GitHubApi.searchMyPullRequestsPage` already caps itself to.
56
- *
57
- * Requested explicitly rather than left to the SDK's own fallback. That fallback is also 100 as of
58
- * `@gitkraken/provider-apis` 0.54.0 (it was 15 before, which cost a drained sweep ~7x the round trips it needs),
59
- * so this is now a pin rather than an override — it keeps the size this read is tuned for from silently tracking
60
- * a shared SDK default that other reads may want smaller.
61
- */
62
- const githubSearchMaxPageSize = 100;
63
-
64
54
  type GitHubPullRequestFacetCursor = Record<string, string>;
65
55
 
66
56
  function toPullRequestFacetCursor(value: unknown): GitHubPullRequestFacetCursor {
@@ -91,13 +81,6 @@ function parsePullRequestFacetCursor(cursor: string | undefined): GitHubPullRequ
91
81
  }
92
82
  }
93
83
 
94
- const pullRequestRelationshipQualifier: Record<PullRequestFilter, string> = {
95
- [PullRequestFilter.Author]: 'author:@me',
96
- [PullRequestFilter.Assignee]: 'assignee:@me',
97
- [PullRequestFilter.ReviewRequested]: 'review-requested:@me',
98
- [PullRequestFilter.Mention]: 'mentions:@me',
99
- };
100
-
101
84
  const metadata = providersMetadata[GitCloudHostIntegrationId.GitHub];
102
85
  const authProvider: IntegrationAuthenticationProviderDescriptor = Object.freeze({
103
86
  id: metadata.id,
@@ -441,140 +424,124 @@ abstract class GitHubIntegrationBase<ID extends GitHubIntegrationIds> extends Gi
441
424
  },
442
425
  ): Promise<ProviderApiPagedResult<ProviderPullRequest> | undefined> {
443
426
  const explicitFilters = options?.filters?.length ? [...new Set(options.filters)] : undefined;
444
- // Explicit relationships need independent search facets so the returned union is exact. State-only reads
445
- // retain the provider-native `involves:@me` behavior for backward compatibility.
446
- if ((options?.state?.length ?? 0) > 0 || explicitFilters != null || options?.includeReviewRequested === true) {
447
- const github = await this.authenticationService.apis.github;
448
- if (github == null) return undefined;
449
-
450
- const requestedStates: PullRequestStateFilter[] =
451
- options?.state != null && options.state.length > 0 ? [...new Set(options.state)] : ['open'];
452
- const facets =
453
- explicitFilters?.length != null
454
- ? requestedStates.flatMap(state =>
455
- explicitFilters.map(filter => ({
456
- key: `${state}:${filter}`,
457
- state: state,
458
- search: pullRequestRelationshipQualifier[filter],
459
- })),
460
- )
461
- : requestedStates.flatMap(state => [
462
- { key: state, state: state, search: undefined },
463
- ...(options?.includeReviewRequested === true
464
- ? [
465
- {
466
- key: `${state}:${PullRequestFilter.ReviewRequested}`,
467
- state: state,
468
- search: pullRequestRelationshipQualifier[PullRequestFilter.ReviewRequested],
469
- },
470
- ]
471
- : []),
472
- ]);
473
- const cursors = parsePullRequestFacetCursor(options?.cursor);
474
- const hasResumableFacetCursor = Object.keys(cursors).length !== 0;
475
- const facetsWithCursor = facets.filter(facet => cursors[facet.key] != null);
476
- // The first call has no cursor, so query every requested state. A continuation only happens after a
477
- // prior page reported `more:true`, whose bundle carries a cursor for each facet still in flight;
478
- // facets absent from the bundle are exhausted, so re-querying them from scratch would refetch the
479
- // same PRs (duplicated by the dedup-free sweep) and waste an API call per page. Query only the
480
- // states that still have a cursor, but degrade a malformed/empty cursor bundle, or one that doesn't
481
- // apply to the current requested states, to the first page rather than returning an empty page.
482
- const facetsToQuery =
483
- options?.cursor != null && hasResumableFacetCursor && facetsWithCursor.length !== 0
484
- ? facetsWithCursor
485
- : facets;
486
- const results = await Promise.allSettled(
487
- facetsToQuery.map(async facet => ({
488
- key: facet.key,
489
- result: await github.searchMyPullRequestsPage(this, toTokenWithInfo(this.id, session), {
490
- baseUrl: this.apiBaseUrl,
491
- state: facet.state,
492
- cursor: cursors[facet.key],
493
- summary: options?.summary,
494
- ...(facet.search != null
495
- ? { search: facet.search, includeDefaultInvolvement: false }
496
- : undefined),
497
- }),
498
- })),
499
- );
500
- if (results.every(result => result.status === 'rejected')) {
501
- const first = results[0];
502
- if (first?.status === 'rejected') throw first.reason;
503
- }
427
+ // Every account-wide read goes through our own facet search there is deliberately no SDK
428
+ // `getPullRequestsForUser` fallback. It could express neither a state set, nor exact relationships (its
429
+ // `involves:` is a different question), nor the full projection, so which reads it could serve depended on
430
+ // the options, and the two routes disagreed on more than they had to: only this one applies the Launchpad
431
+ // ignored/included repository and organization qualifiers, so the same read returned different pull
432
+ // requests depending on options that were supposed to affect only projection or relationship. With no
433
+ // state and no filters this emits a single facet with no relationship qualifier, which is GitHub's own
434
+ // `involves:@me` and `is:open` default the same question the fallback asked, now on one route.
435
+ const github = await this.authenticationService.apis.github;
436
+ if (github == null) return undefined;
437
+
438
+ const requestedStates: PullRequestStateFilter[] =
439
+ options?.state != null && options.state.length > 0 ? [...new Set(options.state)] : ['open'];
440
+ const facets =
441
+ explicitFilters?.length != null
442
+ ? requestedStates.flatMap(state =>
443
+ explicitFilters.map(filter => ({
444
+ key: `${state}:${filter}`,
445
+ state: state,
446
+ search: gitHubPullRequestRelationshipQualifiers[filter],
447
+ })),
448
+ )
449
+ : requestedStates.flatMap(state => [
450
+ { key: state, state: state, search: undefined },
451
+ ...(options?.includeReviewRequested === true
452
+ ? [
453
+ {
454
+ key: `${state}:${PullRequestFilter.ReviewRequested}`,
455
+ state: state,
456
+ search: gitHubPullRequestRelationshipQualifiers[
457
+ PullRequestFilter.ReviewRequested
458
+ ],
459
+ },
460
+ ]
461
+ : []),
462
+ ]);
463
+ const cursors = parsePullRequestFacetCursor(options?.cursor);
464
+ const hasResumableFacetCursor = Object.keys(cursors).length !== 0;
465
+ const facetsWithCursor = facets.filter(facet => cursors[facet.key] != null);
466
+ // The first call has no cursor, so query every requested state. A continuation only happens after a
467
+ // prior page reported `more:true`, whose bundle carries a cursor for each facet still in flight;
468
+ // facets absent from the bundle are exhausted, so re-querying them from scratch would refetch the
469
+ // same PRs (duplicated by the dedup-free sweep) and waste an API call per page. Query only the
470
+ // states that still have a cursor, but degrade a malformed/empty cursor bundle, or one that doesn't
471
+ // apply to the current requested states, to the first page rather than returning an empty page.
472
+ const facetsToQuery =
473
+ options?.cursor != null && hasResumableFacetCursor && facetsWithCursor.length !== 0
474
+ ? facetsWithCursor
475
+ : facets;
476
+ const results = await Promise.allSettled(
477
+ facetsToQuery.map(async facet => ({
478
+ key: facet.key,
479
+ result: await github.searchMyPullRequestsPage(this, toTokenWithInfo(this.id, session), {
480
+ baseUrl: this.apiBaseUrl,
481
+ state: facet.state,
482
+ cursor: cursors[facet.key],
483
+ summary: options?.summary,
484
+ ...(facet.search != null ? { search: facet.search, includeDefaultInvolvement: false } : undefined),
485
+ }),
486
+ })),
487
+ );
488
+ if (results.every(result => result.status === 'rejected')) {
489
+ const first = results[0];
490
+ if (first?.status === 'rejected') throw first.reason;
491
+ }
504
492
 
505
- const values = new Map<string, ProviderPullRequest>();
506
- const nextCursors: GitHubPullRequestFacetCursor = {};
507
- const failures = [];
508
- let hasMore = false;
509
- let truncated = false;
510
- let structuralIncompleteness = false;
511
- let unkeyedPullRequest = 0;
512
- for (const outcome of results) {
513
- if (outcome.status === 'rejected') {
514
- failures.push(toCollectionScopeFailure({ providerId: this.id }, outcome.reason));
515
- truncated = true;
516
- continue;
517
- }
493
+ const values = new Map<string, ProviderPullRequest>();
494
+ const nextCursors: GitHubPullRequestFacetCursor = {};
495
+ const failures = [];
496
+ let hasMore = false;
497
+ let truncated = false;
498
+ let structuralIncompleteness = false;
499
+ let unkeyedPullRequest = 0;
500
+ for (const outcome of results) {
501
+ if (outcome.status === 'rejected') {
502
+ failures.push(toCollectionScopeFailure({ providerId: this.id }, outcome.reason));
503
+ truncated = true;
504
+ continue;
505
+ }
518
506
 
519
- const { key, result } = outcome.value;
520
- for (const pr of result.values) {
521
- const mapped = toProviderPullRequest(pr);
522
- const identity = getProviderPullRequestIdentity(mapped) ?? `unkeyed:${unkeyedPullRequest++}`;
523
- if (!values.has(identity)) {
524
- values.set(identity, mapped);
525
- }
507
+ const { key, result } = outcome.value;
508
+ for (const pr of result.values) {
509
+ const mapped = toProviderPullRequest(pr);
510
+ const identity = getProviderPullRequestIdentity(mapped) ?? `unkeyed:${unkeyedPullRequest++}`;
511
+ if (!values.has(identity)) {
512
+ values.set(identity, mapped);
526
513
  }
527
- if (result.hasMore) {
528
- if (result.cursor == null || result.cursor === '{}' || result.cursor === cursors[key]) {
529
- truncated = true;
530
- structuralIncompleteness = true;
531
- } else {
532
- hasMore = true;
533
- nextCursors[key] = result.cursor;
534
- }
535
- }
536
- if (result.truncated) {
514
+ }
515
+ if (result.hasMore) {
516
+ if (result.cursor == null || result.cursor === '{}' || result.cursor === cursors[key]) {
537
517
  truncated = true;
518
+ structuralIncompleteness = true;
519
+ } else {
520
+ hasMore = true;
521
+ nextCursors[key] = result.cursor;
538
522
  }
539
523
  }
540
-
541
- return {
542
- values: [...values.values()],
543
- paging: {
544
- more: hasMore,
545
- cursor: hasMore ? JSON.stringify({ type: 'cursor', cursors: nextCursors }) : '{}',
546
- truncated: truncated || undefined,
547
- },
548
- ...(failures.length || structuralIncompleteness
549
- ? {
550
- metadata: {
551
- completeness: 'partial' as const,
552
- ...(failures.length ? { failures: failures } : {}),
553
- },
554
- }
555
- : undefined),
556
- };
524
+ if (result.truncated) {
525
+ truncated = true;
526
+ }
557
527
  }
558
528
 
559
- // The current user's login scopes the account-wide `involves:` query (see getPullRequestsForUser →
560
- // getPullRequestsAssociatedWithUser). Resolve it from THIS session (multi-account safe).
561
- const username = (await this.getProviderCurrentAccount(session))?.username;
562
- if (username == null) return undefined;
563
-
564
- const api = await this.getProvidersApi();
565
- // Only reachable with NO requested state (the branch above owns every state-filtered read), so there is
566
- // deliberately no `states` to forward: the SDK's `involves:` search applies its own `is:open` default
567
- // qualifier, which is exactly the unfiltered "my open PRs" this path is for. Nor could a state be
568
- // honored here — `getPullRequestsAssociatedWithUser` doesn't accept one — which is why the state-filtered
569
- // read goes through `searchMyPullRequestsPage` instead of post-filtering a page that never contained the
570
- // requested states.
571
- const result = await api.getPullRequestsForUser(toTokenWithInfo(this.id, session), username, {
572
- baseUrl: this.apiBaseUrl,
573
- cursor: options?.cursor,
574
- // The sweep drains this read page by page, so it asks for the largest page the search allows.
575
- pageSize: githubSearchMaxPageSize,
576
- });
577
- return result ?? undefined;
529
+ return {
530
+ values: [...values.values()],
531
+ paging: {
532
+ more: hasMore,
533
+ cursor: hasMore ? JSON.stringify({ type: 'cursor', cursors: nextCursors }) : '{}',
534
+ truncated: truncated || undefined,
535
+ },
536
+ ...(failures.length || structuralIncompleteness
537
+ ? {
538
+ metadata: {
539
+ completeness: 'partial' as const,
540
+ ...(failures.length ? { failures: failures } : {}),
541
+ },
542
+ }
543
+ : undefined),
544
+ };
578
545
  }
579
546
 
580
547
  protected override async searchProviderMyIssues(
@@ -618,6 +585,7 @@ abstract class GitHubIntegrationBase<ID extends GitHubIntegrationIds> extends Gi
618
585
  includeBody: true,
619
586
  includeAllAssignees: options?.includeAllAssignees,
620
587
  cursor: options?.cursor,
588
+ sort: options?.sort,
621
589
  categories: options?.filters?.length
622
590
  ? {
623
591
  authored: options.filters.includes(IssueFilter.Author),
@@ -718,6 +686,28 @@ abstract class GitHubIntegrationBase<ID extends GitHubIntegrationIds> extends Gi
718
686
  );
719
687
  }
720
688
 
689
+ /**
690
+ * Counts several pull-request scopes in ONE request. Like {@link countProviderIssues}, GitHub's `search`
691
+ * reports `issueCount` on a zero-node selection, so a count preview costs no pull-request transfer.
692
+ */
693
+ protected override async countProviderPullRequests(
694
+ session: ProviderAuthenticationSession,
695
+ scopes: readonly { repos?: ProviderRepoInput[]; org?: string; criteria?: PullRequestSearchCriteria }[],
696
+ cancellation?: AbortSignal,
697
+ ): Promise<(number | undefined)[] | undefined> {
698
+ return (await this.authenticationService.apis.github)?.countPullRequests(
699
+ this,
700
+ toTokenWithInfo(this.id, session),
701
+ scopes.map(s => ({
702
+ repos: s.repos?.map(r => `${r.namespace}/${r.name}`),
703
+ org: s.org,
704
+ criteria: s.criteria,
705
+ })),
706
+ { baseUrl: this.apiBaseUrl },
707
+ cancellation,
708
+ );
709
+ }
710
+
721
711
  protected override async searchProviderPullRequests(
722
712
  session: ProviderAuthenticationSession,
723
713
  searchQuery: string,
@@ -23,6 +23,7 @@ import { toTokenWithInfo } from '../authentication/models.js';
23
23
  import { toCollectionScopeFailure } from '../collectionMetadata.js';
24
24
  import { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';
25
25
  import type { IntegrationServiceContext } from '../context.js';
26
+ import { IntegrationReadUnavailableError } from '../errors.js';
26
27
  import type { IntegrationConnectionChangeEvent } from '../integrationService.js';
27
28
  import type { SearchMyPullRequestsOptions } from '../models/gitHostIntegration.js';
28
29
  import { GitHostIntegration } from '../models/gitHostIntegration.js';
@@ -64,13 +65,19 @@ const cloudEnterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor =
64
65
  type GitLabPullRequestAssociation = 'assigned' | 'authored' | 'reviewRequested';
65
66
  type GitLabPullRequestFacetCursor = Partial<Record<GitLabPullRequestAssociation, string>>;
66
67
 
67
- const gitLabAssociationForFilter: Record<
68
- Exclude<PullRequestFilter, PullRequestFilter.Mention>,
69
- GitLabPullRequestAssociation
70
- > = {
68
+ /**
69
+ * The account-wide relationships GitLab can express, and the `scope` each maps to. `undefined` means GitLab has
70
+ * no equivalent axis (it has neither `Mention` nor `reviewed-by`), and this map is the single declaration of
71
+ * that. Exhaustive over `PullRequestFilter` on purpose: a new member must be decided for GitLab here rather
72
+ * than compiling through as silently unsupported. Exported for the advertised-vs-expressible capability test,
73
+ * which is what keeps it agreeing with `supportedAccountWidePullRequestFilters`; no runtime consumer imports it.
74
+ */
75
+ export const gitLabAssociationForFilter: Record<PullRequestFilter, GitLabPullRequestAssociation | undefined> = {
71
76
  [PullRequestFilter.Assignee]: 'assigned',
72
77
  [PullRequestFilter.Author]: 'authored',
73
78
  [PullRequestFilter.ReviewRequested]: 'reviewRequested',
79
+ [PullRequestFilter.Reviewed]: undefined,
80
+ [PullRequestFilter.Mention]: undefined,
74
81
  };
75
82
 
76
83
  function parseGitLabPullRequestFacetCursor(cursor: string | undefined): GitLabPullRequestFacetCursor {
@@ -473,15 +480,26 @@ abstract class GitLabIntegrationBase<ID extends GitLabIntegrationIds> extends Gi
473
480
 
474
481
  const api = await this.getProvidersApi();
475
482
  const requested = options?.filters?.length ? new Set(options.filters) : undefined;
483
+ // The filter contract is all-or-nothing, so refuse as soon as ONE requested member is inexpressible rather
484
+ // than only when every one is: dropping the rest would answer a narrower question than was asked
485
+ // (`[Author, Mention]` becoming `authored`). Throw rather than return `undefined`, which the facade reads
486
+ // as "no session could be resolved" and a drain turns into a not-connected warning, misattributing a
487
+ // refusal; `getMyPullRequestsForUserResult` recovers the throw into `{ error }`. Defense in depth:
488
+ // `resolveAccountWidePullRequestFilters` already refuses such a set, so only a direct caller of the public
489
+ // method reaches this.
490
+ const unsupported = requested == null ? [] : [...requested].filter(f => gitLabAssociationForFilter[f] == null);
491
+ if (unsupported.length > 0) {
492
+ throw new IntegrationReadUnavailableError(
493
+ this.name,
494
+ `account-wide pull request filters not expressible by GitLab: ${unsupported.join(', ')}`,
495
+ );
496
+ }
497
+
476
498
  const associations: GitLabPullRequestAssociation[] =
477
499
  requested == null
478
500
  ? ['authored', 'assigned', 'reviewRequested']
479
- : [...requested]
480
- .filter(
481
- (filter): filter is Exclude<PullRequestFilter, PullRequestFilter.Mention> =>
482
- filter !== PullRequestFilter.Mention,
483
- )
484
- .map(filter => gitLabAssociationForFilter[filter]);
501
+ : Array.from(requested, f => gitLabAssociationForFilter[f]).filter(a => a != null);
502
+
485
503
  const cursors = parseGitLabPullRequestFacetCursor(options?.cursor);
486
504
  const resumable = associations.filter(association => cursors[association] != null);
487
505
  const associationsToQuery = options?.cursor != null && resumable.length > 0 ? resumable : associations;
@@ -668,6 +686,7 @@ abstract class GitLabIntegrationBase<ID extends GitLabIntegrationIds> extends Gi
668
686
  isPAT: this.isEnterprise,
669
687
  baseUrl: baseUrl,
670
688
  cursor: cursor,
689
+ sort: options?.sort,
671
690
  });
672
691
  if (cancellation?.aborted) throw new CancellationError();
673
692
  return page;
@@ -0,0 +1,89 @@
1
+ import { SUPPORTED_ISSUE_SORTS } from '@gitkraken/provider-apis';
2
+ import { getIssueComparator } from '../../../git/utils/issue.utils.js';
3
+ import type { IssueSorting } from '../providerFilters.js';
4
+
5
+ /**
6
+ * The per-provider ordering vocabulary of the issue reads: which neutral `field:direction` keys each provider
7
+ * surface can express SERVER-SIDE.
8
+ *
9
+ * DERIVED from `@gitkraken/provider-apis`, not transcribed from it. The SDK is where a neutral key becomes a
10
+ * provider query, so it is the only place that can say what a provider accepts; a copy kept here would be a
11
+ * second hand-maintained table on a second release cadence, and the symptom of drift is not a failed build but a
12
+ * read this facade promised and the SDK then rejects at runtime — which is exactly what happened while these were
13
+ * transcribed, on the one key (`reactions`) whose meaning is narrower than its name.
14
+ *
15
+ * Why the asymmetries between surfaces exist (GitHub cannot order by close date, GitLab's two reads are different
16
+ * APIs with different vocabularies, Azure has no `dueDate` outside the Agile process, Trello has only its
17
+ * `sort:edited` modifier, Jira has votes rather than reactions) is documented on the SDK's own maps. This module
18
+ * adds exactly one rule of its own — {@link mergeableSorts} — and everything else is a rename from the SDK's
19
+ * surface names to the two fields `ProviderMetadata` publishes.
20
+ *
21
+ * A caveat for anything building ONE ordering control across providers: a neutral key does not always mean the
22
+ * same thing. `priority:desc` is most-important-first on Jira and Linear, and least-important-first on Azure
23
+ * DevOps, whose WIQL orders the raw numeric column where 1 is the highest priority — so no single direction
24
+ * means "highest priority first" everywhere, and a unified control has to pick the direction per provider.
25
+ * `title` likewise orders by each backing database's collation, never JavaScript's.
26
+ */
27
+
28
+ /** One SDK read surface, widened to the union this package uses (structurally the same `field:direction` type). */
29
+ const surface = (name: keyof typeof SUPPORTED_ISSUE_SORTS): IssueSorting[] => [...SUPPORTED_ISSUE_SORTS[name]];
30
+
31
+ /**
32
+ * The subset a MERGED page can honor: the keys a normalized issue carries.
33
+ *
34
+ * Applied to the account-wide surfaces because every account-wide read is a union of several provider queries
35
+ * merged in this facade — GitHub's three `@me` searches, GitLab's one REST call per relationship, Azure's
36
+ * (project × relationship) drains — and the caller has no scope count to reduce, so it ALWAYS merges. GitLab's
37
+ * REST endpoint really does order by `priority` and `dueDate`, and Azure's WIQL by `resolved` and `priority`;
38
+ * none of them survive a merge, so the surface cannot honor them and must not advertise them.
39
+ *
40
+ * Expressed as the rule rather than as a hand-removed list, so a key the SDK adds later is classified by the same
41
+ * predicate that will decide at read time whether the merge can order by it, instead of by whoever last edited a
42
+ * literal here. The repo-scoped surfaces are NOT filtered: those merge only when the caller passes several
43
+ * scopes, which is a property of the call rather than of the provider (see `mergesProviderQueries`).
44
+ */
45
+ const mergeableSorts = (sorts: readonly IssueSorting[]): IssueSorting[] =>
46
+ sorts.filter(sort => getIssueComparator(sort) != null);
47
+
48
+ /** GitHub and GHE: one `search` channel serves all three issue reads, so all three order the same way. */
49
+ export const githubIssueSorts: IssueSorting[] = surface('github');
50
+
51
+ /**
52
+ * The same `search` channel, narrowed to what the three `@me` searches can order once this facade has merged them.
53
+ *
54
+ * Identical to {@link githubIssueSorts} today — every qualifier the SDK's GitHub surface carries is a field an
55
+ * `IssueShape` models — so this exists for the drift, not for a difference: `readAccountWideIssuesPage`
56
+ * deliberately runs no `unmergeable` check, on the stated grounds that an account-wide table only lists keys a
57
+ * merge can honor. Left as the raw surface, one qualifier added upstream (a `closed` or `interactions` sort) would
58
+ * make that assumption false for GitHub alone and publish three concatenated alias runs under it, unwarned.
59
+ */
60
+ export const githubAccountWideIssueSorts: IssueSorting[] = mergeableSorts(surface('github'));
61
+
62
+ /** GitLab's repository-scoped read, which goes through GraphQL `IssueSort`. */
63
+ export const gitlabIssueSorts: IssueSorting[] = surface('gitlabRepository');
64
+
65
+ /** GitLab's account-wide read: the REST `order_by`/`sort` pair, narrowed to what its merge can order. */
66
+ export const gitlabAccountWideIssueSorts: IssueSorting[] = mergeableSorts(surface('gitlabAccountWide'));
67
+
68
+ /** Azure DevOps, as WIQL `ORDER BY` columns. */
69
+ export const azureIssueSorts: IssueSorting[] = surface('azureDevOps');
70
+
71
+ /** The same WIQL, narrowed to what the per-project fan-out can order once it has merged. */
72
+ export const azureAccountWideIssueSorts: IssueSorting[] = mergeableSorts(surface('azureDevOps'));
73
+
74
+ /** Jira Cloud and Server, as JQL `ORDER BY` fields — one JQL builder, so one surface. */
75
+ export const jiraIssueSorts: IssueSorting[] = surface('jira');
76
+
77
+ /**
78
+ * Linear's one root `issues` query, which both of its reads share and which takes the same `sort` argument in
79
+ * either direction — so this is one surface, not a repo-scoped and an account-wide pair.
80
+ *
81
+ * NOT filtered by {@link mergeableSorts} even though the account-wide read is "my issues": that read is a
82
+ * single server-ordered query (one `or` filter over the four relationships), so nothing is merged in this
83
+ * facade and there is no comparator to satisfy. Filtering it would drop `priority` and `dueDate` — keys the
84
+ * server really does order by — for a merge that does not happen.
85
+ */
86
+ export const linearIssueSorts: IssueSorting[] = surface('linear');
87
+
88
+ /** Trello's search modifiers: `sort:edited` / `sort:-edited`, and nothing else usable for issues. */
89
+ export const trelloIssueSorts: IssueSorting[] = surface('trello');
@@ -8,8 +8,9 @@ import { Logger } from '../../../utils/logger.js';
8
8
  import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';
9
9
  import type { ProviderAuthenticationSession } from '../authentication/models.js';
10
10
  import { toTokenWithInfo } from '../authentication/models.js';
11
- import { toCollectionScopeFailure } from '../collectionMetadata.js';
11
+ import { throwIfCallerContractError, toCollectionScopeFailure } from '../collectionMetadata.js';
12
12
  import { IssuesCloudHostIntegrationId } from '../constants.js';
13
+ import type { IssuesForProjectOptions } from '../models/issueReads.js';
13
14
  import { IssuesIntegration } from '../models/issuesIntegration.js';
14
15
  import type { ProviderApiCollectionResult, ProviderIssue } from './models.js';
15
16
  import { IssueFilter, providersMetadata, toAccount, toIssueShape } from './models.js';
@@ -234,7 +235,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
234
235
  protected override async getProviderIssuesForProject(
235
236
  session: ProviderAuthenticationSession,
236
237
  project: JiraProjectDescriptor,
237
- options?: { user?: string; filters?: IssueFilter[] },
238
+ options?: IssuesForProjectOptions,
238
239
  ): Promise<IssueShape[] | undefined> {
239
240
  return (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;
240
241
  }
@@ -242,7 +243,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
242
243
  protected override async getProviderIssuesForProjectWithTruncation(
243
244
  session: ProviderAuthenticationSession,
244
245
  project: JiraProjectDescriptor,
245
- options?: { user?: string; filters?: IssueFilter[] },
246
+ options?: IssuesForProjectOptions,
246
247
  ): Promise<{ values: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata } | undefined> {
247
248
  const tokenWithInfo = toTokenWithInfo(this.id, session);
248
249
 
@@ -273,6 +274,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
273
274
  result = await api.getIssuesForProjectPaged(tokenWithInfo, project.name, project.resourceId, {
274
275
  ...scope,
275
276
  cursor: cursor,
277
+ sort: options?.sort,
276
278
  });
277
279
  } catch (ex) {
278
280
  // A page failure after the first page leaves the already-drained prefix intact; record the
@@ -390,6 +392,10 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
390
392
  // incomplete: keep the sibling results but record a structured failure so the facade can warn on
391
393
  // the specific filter (auth/rate-limit) instead of just a generic truncation flag.
392
394
  if (outcome.status !== 'fulfilled') {
395
+ // Identical for every filter branch, so degrading it would report one failure per branch for a
396
+ // single invalid call — see `throwIfCallerContractError`.
397
+ throwIfCallerContractError(outcome.reason);
398
+
393
399
  truncated = true;
394
400
  const failure = toCollectionScopeFailure(
395
401
  { providerId: this.id, resourceId: project.resourceId, projectId: project.name },