@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,9 +1,10 @@
1
- import type { IssueShape } from '../../../git/models/issue.js';
1
+ import type { IssueShape, IssueSorting } from '../../../git/models/issue.js';
2
2
  import { mergeAssessmentInto } from '../collectionMetadata.js';
3
3
  import type { IntegrationIds } from '../constants.js';
4
4
  import { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';
5
+ import type { GitHostIntegration } from '../models/gitHostIntegration.js';
5
6
  import type { IssueFilter, ProviderReposInput } from '../providers/models.js';
6
- import { PagingMode, providersMetadata } from '../providers/models.js';
7
+ import { isRepoIdsInput, PagingMode, providersMetadata } from '../providers/models.js';
7
8
  import { mergeCollectionMetadata } from '../providers/utils/providerPaging.js';
8
9
  import type { ProviderPagedResult, ProviderWarning } from '../results.js';
9
10
  import { reconcileOmissionsWithFailure } from '../results.js';
@@ -15,6 +16,7 @@ import {
15
16
  import type { ProviderReadContext } from './context.js';
16
17
  import { runCaptured } from './drains.js';
17
18
  import { resolveAccountWideIssueFilters } from './filters.js';
19
+ import { resolveIssueSort, toIssueOrdering } from './ordering.js';
18
20
  import {
19
21
  drainFlatPagesToRequestedPage,
20
22
  drainToRequestedPage,
@@ -31,39 +33,115 @@ import {
31
33
  issuesUnsupportedWarning,
32
34
  otherWarning,
33
35
  truncationWarning,
36
+ unmergeableIssueSortWarning,
34
37
  unsupportedAccountWideIssueFiltersWarning,
38
+ unsupportedIssueSortWarning,
35
39
  } from './warnings.js';
36
40
 
41
+ /**
42
+ * Whether a repo-scoped page is assembled from SEVERAL provider queries and merged here — which is what decides
43
+ * whether the requested order can be honored at all.
44
+ *
45
+ * `PagingMode.Repos` (GitHub) sends one search however many repositories are named, so its page arrives ordered by
46
+ * the provider. `Repo` (GitLab) issues one query per repository, and `Project` (Azure) one per PROJECT — several
47
+ * repositories of the same project are still one query, which is why that case counts distinct projects rather than
48
+ * repositories.
49
+ *
50
+ * The `Repo` case has a second way to merge that has nothing to do with the count: given repository IDS rather than
51
+ * descriptors, `getMyIssuesForReposResult` skips its per-repository fan-out entirely (that branch is guarded on
52
+ * `!isRepoIdsInput`) and calls the SDK's `getIssuesForRepos`, which for GitLab is the multi-project aggregate that
53
+ * merges in the SDK and refuses `priority`/`dueDate` however few scopes it was given. Treating that as merged is
54
+ * what makes this facade refuse it up front, with the message that names the reason, instead of letting the SDK
55
+ * reject a read the capability table had just promised.
56
+ */
57
+ function mergesProviderQueries(pagingMode: PagingMode | undefined, repos: ProviderReposInput | undefined): boolean {
58
+ const scopes = repos ?? [];
59
+ switch (pagingMode) {
60
+ case PagingMode.Repo:
61
+ // `isRepoIdsInput` is the same predicate `ProvidersApi` selects the SDK aggregate with, so this cannot
62
+ // disagree with the branch it is predicting.
63
+ return isRepoIdsInput(scopes) ? scopes.length > 0 : scopes.length > 1;
64
+ case PagingMode.Project:
65
+ // Only the descriptor form carries a project; the id form is refused by the Azure read before it runs.
66
+ return new Set(scopes.map(r => (typeof r === 'object' ? r.project : undefined))).size > 1;
67
+ default:
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * What {@link listIssuesPage} accepts, shared by name with the two functions it dispatches to.
74
+ *
75
+ * Named rather than left inline: three functions now take this exact shape, and reaching it from the other two as
76
+ * `Parameters<typeof listIssuesPage>[1]` would define the contract in terms of the function that consumes it —
77
+ * inside out, and unreadable at the point where the fields are actually used.
78
+ */
79
+ export interface ListIssuesPageOptions {
80
+ providerId: IntegrationIds;
81
+ repos?: ProviderReposInput;
82
+ /** Narrows the account-wide read to one org/account. Requires a host with a project layer (Azure). */
83
+ org?: string;
84
+ /** Narrows the account-wide read to one project. Requires a host with a project layer (Azure). */
85
+ project?: string;
86
+ /**
87
+ * Narrows to the requested relationship(s). On the account-wide path this replaces the provider's own
88
+ * definition of "my issues" (GitHub/GHE: authored ∪ assigned ∪ mentioned; Azure: assigned ∪ authored;
89
+ * GitLab: assigned-to-me), so `[Assignee]` yields `assignee:@me` everywhere it's expressible. A set the
90
+ * provider can't express server-side is refused whole (warning + `fetchFailed`), never widened — check
91
+ * getSupportedFilters first to avoid that path.
92
+ */
93
+ filters?: IssueFilter[];
94
+ /** Broadens the read to every assignee. Contradicts `filters`; passing both is refused. */
95
+ includeAllAssignees?: boolean;
96
+ /**
97
+ * How to order the page, as `field:direction`. Omitted orders most-recently-updated-first wherever the
98
+ * provider can express that, which is this facade's default rather than the provider's own.
99
+ *
100
+ * Validated against `getSupportedFilters().issueSorts` on the repo-scoped path and `.issueSortsAccountWide`
101
+ * on the account-wide one — two genuinely different vocabularies for GitLab, whose two reads are different
102
+ * APIs. A key the provider can't express refuses the read (warning + `fetchFailed`) rather than serving a
103
+ * differently-ordered list, for the same reason an inexpressible filter does: the reachable window is
104
+ * bounded, so another order is another subset.
105
+ *
106
+ * On the repo-scoped path with SEVERAL scopes the page is a merge of one query per repository/project, so
107
+ * only a key a normalized issue carries can be honored: `priority`, `dueDate` and `resolved` are refused
108
+ * there even where the provider supports them on a single scope. Order is per page, and per scope across
109
+ * pages — the merge orders what a page contains, not the sequence of pages.
110
+ */
111
+ sort?: IssueSorting;
112
+ page?: number;
113
+ cursor?: string;
114
+ itemsPerPage?: number;
115
+ forceSync?: boolean;
116
+ connectionId?: string;
117
+ /**
118
+ * Explicit self-managed host domain. Used only when the requested connection has no configured domain;
119
+ * it must come from the trusted authentication configuration, not repository or remote data.
120
+ */
121
+ domain?: string;
122
+ }
123
+
124
+ /**
125
+ * What both halves of {@link listIssuesPage} need from its shared prologue.
126
+ *
127
+ * A bundle rather than six parameters because the two reads take exactly the same inputs and differ only in what
128
+ * they do with them; threading them positionally would make the two call sites read as two different reads of two
129
+ * different things, which is the opposite of what is true.
130
+ *
131
+ * Note `ctx` is absent: every use of it — resolving the integration, the domain, the forced refresh, the
132
+ * connection warnings — is in the prologue, which is what makes this the right cut.
133
+ */
134
+ interface IssueReadContext {
135
+ options: ListIssuesPageOptions;
136
+ integration: GitHostIntegration;
137
+ domain: string | undefined;
138
+ warnOnMissingSession: boolean;
139
+ page: number;
140
+ }
141
+
37
142
  export async function listIssuesPage(
38
143
  ctx: ProviderReadContext,
39
- options: {
40
- providerId: IntegrationIds;
41
- repos?: ProviderReposInput;
42
- /** Narrows the account-wide read to one org/account. Requires a host with a project layer (Azure). */
43
- org?: string;
44
- /** Narrows the account-wide read to one project. Requires a host with a project layer (Azure). */
45
- project?: string;
46
- /**
47
- * Narrows to the requested relationship(s). On the account-wide path this replaces the provider's own
48
- * definition of "my issues" (GitHub/GHE: authored ∪ assigned ∪ mentioned; Azure: assigned ∪ authored;
49
- * GitLab: assigned-to-me), so `[Assignee]` yields `assignee:@me` everywhere it's expressible. A set the
50
- * provider can't express server-side is refused whole (warning + `fetchFailed`), never widened — check
51
- * getSupportedFilters first to avoid that path.
52
- */
53
- filters?: IssueFilter[];
54
- /** Broadens the read to every assignee. Contradicts `filters`; passing both is refused. */
55
- includeAllAssignees?: boolean;
56
- page?: number;
57
- cursor?: string;
58
- itemsPerPage?: number;
59
- forceSync?: boolean;
60
- connectionId?: string;
61
- /**
62
- * Explicit self-managed host domain. Used only when the requested connection has no configured domain;
63
- * it must come from the trusted authentication configuration, not repository or remote data.
64
- */
65
- domain?: string;
66
- },
144
+ options: ListIssuesPageOptions,
67
145
  ): Promise<ProviderPagedResult<IssueShape>> {
68
146
  const page = Math.max(1, options.page ?? 1);
69
147
  if (isIssuesHostIntegrationId(options.providerId)) {
@@ -100,188 +178,297 @@ export async function listIssuesPage(
100
178
  return refusedPage(page, [issuesUnsupportedWarning(options.providerId, domain, options.connectionId)], true);
101
179
  }
102
180
 
103
- const accountWide = (options.repos?.length ?? 0) === 0;
181
+ // `repos` is what selects between the two: with none there is no scope to query per repository, so the
182
+ // provider's own account-wide search answers instead. They are different provider queries with different
183
+ // capability tables, cursors and completeness signals — see each function.
184
+ const context: IssueReadContext = {
185
+ options: options,
186
+ integration: integration,
187
+ domain: domain,
188
+ warnOnMissingSession: warnOnMissingSession,
189
+ page: page,
190
+ };
191
+ return (options.repos?.length ?? 0) === 0 ? readAccountWideIssuesPage(context) : readRepoScopedIssuesPage(context);
192
+ }
104
193
 
105
- if (accountWide) {
106
- // The account-wide read is cursor-only, so a refusal can't claim the requested position it reports
107
- // page 1, per ProviderPageInfo.currentPage.
108
- const refused = (warning: ProviderWarning) => refusedPage<IssueShape>(1, [warning], true);
194
+ /**
195
+ * The account-wide read: the provider's own definition of "my issues", behind a composite cursor that spans
196
+ * several provider searches.
197
+ *
198
+ * Its own function rather than a branch of the paged read, because almost nothing is shared past the guards: it
199
+ * validates a different filter table and a different sort table, it is cursor-only (a page number cannot address
200
+ * a composite cursor), it accumulates per-scope failures across the pages it walks, and it always merges several
201
+ * queries. The two used to share one 460-line function behind a mode flag, and the comments explaining how far
202
+ * apart they are were the clue that they wanted separating.
203
+ */
204
+ async function readAccountWideIssuesPage({
205
+ options,
206
+ integration,
207
+ domain,
208
+ warnOnMissingSession,
209
+ page,
210
+ }: IssueReadContext): Promise<ProviderPagedResult<IssueShape>> {
211
+ // The account-wide read is cursor-only, so a refusal can't claim the requested position — it reports
212
+ // page 1, per ProviderPageInfo.currentPage.
213
+ const refused = (warning: ProviderWarning) => refusedPage<IssueShape>(1, [warning], true);
109
214
 
110
- // Checked before the provider-specific guards below: a caller passing both has a contradictory request
111
- // whatever the provider, and saying so is more useful than reporting one half of it as unsupported.
112
- // `filters` narrows this read to a relationship (`[Assignee]` ⇒ just assigned-to-me); `includeAllAssignees`
113
- // broadens it to every assignee. Honoring either silently would answer a question the caller didn't ask.
114
- if (options.filters?.length && options.includeAllAssignees === true) {
115
- return refused(
116
- otherWarning(
117
- options.providerId,
118
- domain,
119
- options.connectionId,
120
- '`filters` and `includeAllAssignees` are contradictory for an account-wide issue read; pass only one.',
121
- ),
122
- );
123
- }
215
+ // Checked before the provider-specific guards below: a caller passing both has a contradictory request
216
+ // whatever the provider, and saying so is more useful than reporting one half of it as unsupported.
217
+ // `filters` narrows this read to a relationship (`[Assignee]` ⇒ just assigned-to-me); `includeAllAssignees`
218
+ // broadens it to every assignee. Honoring either silently would answer a question the caller didn't ask.
219
+ if (options.filters?.length && options.includeAllAssignees === true) {
220
+ return refused(
221
+ otherWarning(
222
+ options.providerId,
223
+ domain,
224
+ options.connectionId,
225
+ '`filters` and `includeAllAssignees` are contradictory for an account-wide issue read; pass only one.',
226
+ ),
227
+ );
228
+ }
124
229
 
125
- // GitHub expresses "any assignee" as `assignee:*`, which is meaningless without a scope: unscoped it
126
- // matches millions of issues across all of GitHub instead of the user's own world. ANY scope makes it
127
- // meaningful (one repository, several, or an org) — it is specifically THIS branch, the account-wide read,
128
- // that has none to offer. Scope the read to repositories (or use `searchIssuesPage`, whose criteria model
129
- // takes `any-assignee` alongside a `repos`/`org` scope) rather than reading unscoped.
130
- if (
131
- options.includeAllAssignees === true &&
132
- (options.providerId === GitCloudHostIntegrationId.GitHub ||
133
- options.providerId === GitSelfManagedHostIntegrationId.CloudGitHubEnterprise)
134
- ) {
135
- return refused(
136
- otherWarning(
137
- options.providerId,
138
- domain,
139
- options.connectionId,
140
- '`includeAllAssignees` is not supported for account-wide GitHub issue reads; scope the read to repositories instead.',
141
- ),
142
- );
143
- }
230
+ // GitHub expresses "any assignee" as `assignee:*`, which is meaningless without a scope: unscoped it
231
+ // matches millions of issues across all of GitHub instead of the user's own world. ANY scope makes it
232
+ // meaningful (one repository, several, or an org) — it is specifically THIS branch, the account-wide read,
233
+ // that has none to offer. Scope the read to repositories (or use `searchIssuesPage`, whose criteria model
234
+ // takes `any-assignee` alongside a `repos`/`org` scope) rather than reading unscoped.
235
+ if (
236
+ options.includeAllAssignees === true &&
237
+ (options.providerId === GitCloudHostIntegrationId.GitHub ||
238
+ options.providerId === GitSelfManagedHostIntegrationId.CloudGitHubEnterprise)
239
+ ) {
240
+ return refused(
241
+ otherWarning(
242
+ options.providerId,
243
+ domain,
244
+ options.connectionId,
245
+ '`includeAllAssignees` is not supported for account-wide GitHub issue reads; scope the read to repositories instead.',
246
+ ),
247
+ );
248
+ }
144
249
 
145
- // Only a host with a project layer can narrow server-side. Reject the request rather than serving an
146
- // unscoped list as if it had been narrowed: the caller would otherwise have to filter client-side,
147
- // which desynchronizes the filtered `items` from this read's `hasMore`/`currentPage` and shows
148
- // "no issues" for a page that simply held none of the requested project's.
149
- if ((options.org != null || options.project != null) && !integration.supportsProjectDiscovery) {
150
- return refused(
151
- otherWarning(
152
- options.providerId,
153
- domain,
154
- options.connectionId,
155
- `Project-scoped issue reads are not supported by '${options.providerId}'; scope the read to repositories instead.`,
156
- ),
157
- );
158
- }
250
+ // Only a host with a project layer can narrow server-side. Reject the request rather than serving an
251
+ // unscoped list as if it had been narrowed: the caller would otherwise have to filter client-side,
252
+ // which desynchronizes the filtered `items` from this read's `hasMore`/`currentPage` and shows
253
+ // "no issues" for a page that simply held none of the requested project's.
254
+ if ((options.org != null || options.project != null) && !integration.supportsProjectDiscovery) {
255
+ return refused(
256
+ otherWarning(
257
+ options.providerId,
258
+ domain,
259
+ options.connectionId,
260
+ `Project-scoped issue reads are not supported by '${options.providerId}'; scope the read to repositories instead.`,
261
+ ),
262
+ );
263
+ }
159
264
 
160
- // Narrowing the account-wide read is only honest when the provider can express it server-side: its
161
- // per-relationship queries produced the page and the cursor together, so dropping items afterward would
162
- // leave `items` describing a different result set than `hasMore`/`currentPage`.
163
- const resolvedIssueFilters = resolveAccountWideIssueFilters(options.providerId, options.filters);
164
- if (resolvedIssueFilters.unsupported) {
165
- return refused(
166
- unsupportedAccountWideIssueFiltersWarning(
167
- options.providerId,
168
- domain,
169
- options.connectionId,
170
- options.filters!,
171
- ),
172
- );
173
- }
265
+ // Narrowing the account-wide read is only honest when the provider can express it server-side: its
266
+ // per-relationship queries produced the page and the cursor together, so dropping items afterward would
267
+ // leave `items` describing a different result set than `hasMore`/`currentPage`.
268
+ const resolvedIssueFilters = resolveAccountWideIssueFilters(options.providerId, options.filters);
269
+ if (resolvedIssueFilters.unsupported) {
270
+ return refused(
271
+ unsupportedAccountWideIssueFiltersWarning(
272
+ options.providerId,
273
+ domain,
274
+ options.connectionId,
275
+ options.filters!,
276
+ ),
277
+ );
278
+ }
279
+
280
+ // Every provider's account-wide read is a UNION of several queries — GitHub's three `@me` searches,
281
+ // GitLab's one REST call per relationship, Azure's (project x relationship) drains — so it always merges,
282
+ // and `supportedAccountWideIssueSorts` already lists only keys a merge can honor. No separate
283
+ // mergeability refusal is needed here, unlike on the repo-scoped path below.
284
+ const resolvedSort = resolveIssueSort(
285
+ providersMetadata[options.providerId]?.supportedAccountWideIssueSorts,
286
+ options.sort,
287
+ );
288
+ if (resolvedSort.rejection != null) {
289
+ return refused(
290
+ unsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),
291
+ );
292
+ }
174
293
 
175
- // The repo-scoped core rejects empty repos (GitHub/Bitbucket/Azure); read the account-wide,
176
- // already-user-scoped core instead. GitHub exposes a composite cursor across its authored,
177
- // assigned, and mentioned searches. Walk it internally when the caller supplies only page N.
178
- const readAccountWidePage = (cursor: string | undefined) =>
179
- runCaptured(
294
+ // `merged: true` unconditionally: this read has no scope count for a caller to reduce. It is also why no
295
+ // `unmergeable` check follows `supportedAccountWideIssueSorts` declares only keys a merge can honor.
296
+ const accountWideOrdering = toIssueOrdering(resolvedSort.sort, true);
297
+
298
+ // The repo-scoped core rejects empty repos (GitHub/Bitbucket/Azure); read the account-wide,
299
+ // already-user-scoped core instead. GitHub exposes a composite cursor across its authored,
300
+ // assigned, and mentioned searches. Walk it internally when the caller supplies only page N.
301
+ const readAccountWidePage = (cursor: string | undefined) =>
302
+ runCaptured(
303
+ options.providerId,
304
+ domain,
305
+ options.connectionId,
306
+ () =>
307
+ integration.searchMyIssuesWithTruncationResult(undefined, undefined, options.connectionId, {
308
+ includeAllAssignees: options.includeAllAssignees,
309
+ filters: resolvedIssueFilters.filters,
310
+ cursor: cursor,
311
+ org: options.org,
312
+ project: options.project,
313
+ sort: accountWideOrdering.sort,
314
+ }),
315
+ { warnOnMissingSession: warnOnMissingSession },
316
+ );
317
+ const first = await readAccountWidePage(options.cursor);
318
+ const warnings = first.warning != null ? [first.warning] : [];
319
+ // Merged across the walked pages, so a per-scope failure reported on an earlier page isn't lost by
320
+ // paging past it — the one thing this read accumulates that the filtered search doesn't.
321
+ let allMetadata = first.value?.metadata;
322
+ const drained = await drainFlatPagesToRequestedPage(first, {
323
+ requestedPage: page,
324
+ suppliedCursor: options.cursor,
325
+ warnings: warnings,
326
+ readPage: readAccountWidePage,
327
+ fold: p => {
328
+ allMetadata = mergeCollectionMetadata(allMetadata, p.metadata);
329
+ },
330
+ });
331
+ const { value, currentPage, requestedPageMissing } = drained;
332
+ const currentTruncated = drained.truncated;
333
+ const pageFetchFailed = drained.fetchFailed;
334
+
335
+ // GitHub, GitLab, and Azure implement an account-wide issue search; a provider that doesn't (Bitbucket
336
+ // exposes no issues at all, and `supportsIssues` already short-circuits it above) returns `undefined`
337
+ // with no error. Surface that as an explicit unsupported warning + fetchFailed rather than a silent
338
+ // empty success — the caller must fall back (e.g. broadenIssues over repos).
339
+ if (value == null && warnings.length === 0) {
340
+ return refused(
341
+ otherWarning(
180
342
  options.providerId,
181
343
  domain,
182
344
  options.connectionId,
183
- () =>
184
- integration.searchMyIssuesWithTruncationResult(undefined, undefined, options.connectionId, {
185
- includeAllAssignees: options.includeAllAssignees,
186
- filters: resolvedIssueFilters.filters,
187
- cursor: cursor,
188
- org: options.org,
189
- project: options.project,
345
+ `Account-wide issue search is not supported by '${options.providerId}'; scope the read to repositories instead.`,
346
+ ),
347
+ );
348
+ }
349
+
350
+ const items = requestedPageMissing ? [] : (value?.values ?? []);
351
+ // Each query arrived ordered by the provider; their union is not, and the union is what this read publishes.
352
+ // Idempotent for GitHub, whose aliased-search engine already ordered the same merge — one call here rather
353
+ // than the same logic repeated in each of the three provider implementations.
354
+ const orderedItems = accountWideOrdering.order(items);
355
+ // Fold in structured per-scope failures from the account-wide fan-out (e.g. Azure across projects):
356
+ // scope-aware warnings + `fetchFailed` when a scope failed, without discarding the successful items.
357
+ const assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);
358
+ // An account-wide search that couldn't confirm completeness (a provider cap with no cursor, or a
359
+ // per-scope backstop/failure) is incomplete and can't be paged; report it as truncated (+ a
360
+ // provider-neutral warning, unless a structured failure already explains it) rather than a complete
361
+ // list. Don't hard-code GitHub's "100 per category" cap here — Azure reaches this via a per-project
362
+ // backstop, and other providers may cap differently.
363
+ // The account-wide read is cursor-only (its composite cursor spans several provider searches, so a
364
+ // page number can't address it): `hasMore` without a real cursor is a dead end, so report it as
365
+ // terminal-but-incomplete rather than inviting the caller to page forever.
366
+ const continuation = resolveContinuation(
367
+ {
368
+ hasMore: requestedPageMissing ? false : (value?.hasMore ?? false),
369
+ cursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),
370
+ truncated: currentTruncated,
371
+ },
372
+ undefined,
373
+ );
374
+ const truncated = continuation.truncated || assessment.truncated;
375
+ if (truncated && warnings.length === 0) {
376
+ warnings.push(
377
+ truncationWarning(
378
+ options.providerId,
379
+ domain,
380
+ options.connectionId,
381
+ 'Account-wide issue search',
382
+ // `exhausted`: this composite read exposes no budget the caller can raise, so nothing it
383
+ // could call would return the withheld items.
384
+ assessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',
385
+ ),
386
+ );
387
+ }
388
+ // A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.
389
+ reconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);
390
+ return {
391
+ items: orderedItems,
392
+ warnings: warnings,
393
+ page: {
394
+ // Positional, per ProviderPageInfo.currentPage. `currentPage` already carries what the provider
395
+ // reported or what the internal drain counted; a requested page past the terminal cursor is
396
+ // reported as that empty page N. The account-wide read is cursor-only, so a `page` the caller
397
+ // didn't pair with a cursor is never echoed.
398
+ currentPage: requestedPageMissing
399
+ ? page
400
+ : resolveCurrentPage({
401
+ providerPage: currentPage,
402
+ requestedPage: page,
403
+ suppliedCursor: options.cursor,
404
+ pageAdvanceable: false,
190
405
  }),
191
- { warnOnMissingSession: warnOnMissingSession },
192
- );
193
- const first = await readAccountWidePage(options.cursor);
194
- const warnings = first.warning != null ? [first.warning] : [];
195
- // Merged across the walked pages, so a per-scope failure reported on an earlier page isn't lost by
196
- // paging past it the one thing this read accumulates that the filtered search doesn't.
197
- let allMetadata = first.value?.metadata;
198
- const drained = await drainFlatPagesToRequestedPage(first, {
199
- requestedPage: page,
200
- suppliedCursor: options.cursor,
201
- warnings: warnings,
202
- readPage: readAccountWidePage,
203
- fold: p => {
204
- allMetadata = mergeCollectionMetadata(allMetadata, p.metadata);
205
- },
206
- });
207
- const { value, currentPage, requestedPageMissing } = drained;
208
- const currentTruncated = drained.truncated;
209
- const pageFetchFailed = drained.fetchFailed;
406
+ itemsPerPage: orderedItems.length,
407
+ truncated: truncated || undefined,
408
+ },
409
+ hasMore: continuation.hasMore,
410
+ cursor: continuation.cursor,
411
+ fetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,
412
+ };
413
+ }
210
414
 
211
- // GitHub, GitLab, and Azure implement an account-wide issue search; a provider that doesn't (Bitbucket
212
- // exposes no issues at all, and `supportsIssues` already short-circuits it above) returns `undefined`
213
- // with no error. Surface that as an explicit unsupported warning + fetchFailed rather than a silent
214
- // empty success — the caller must fall back (e.g. broadenIssues over repos).
215
- if (value == null && warnings.length === 0) {
216
- return refused(
217
- otherWarning(
415
+ /**
416
+ * The repository-scoped read: one provider query per repository or project, paged by the provider's own
417
+ * continuation and drained internally when the caller supplied only a page number.
418
+ *
419
+ * See {@link readAccountWideIssuesPage} for why the two are separate functions.
420
+ */
421
+ async function readRepoScopedIssuesPage({
422
+ options,
423
+ integration,
424
+ domain,
425
+ warnOnMissingSession,
426
+ page,
427
+ }: IssueReadContext): Promise<ProviderPagedResult<IssueShape>> {
428
+ const metadata = providersMetadata[options.providerId];
429
+
430
+ // How many separate provider queries this page is assembled from, which decides whether the requested order can
431
+ // be honored at all. `PagingMode.Repos` (GitHub) sends ONE search however many repositories are named, so its
432
+ // page arrives ordered by the provider; `Repo` (GitLab) and `Project` (Azure) issue one query per scope and are
433
+ // concatenated here, so a page spanning several of them is only as orderable as a normalized issue is.
434
+ const resolvedRepoScopedSort = resolveIssueSort(metadata?.supportedIssueSorts, options.sort);
435
+ if (resolvedRepoScopedSort.rejection != null) {
436
+ return refusedPage(
437
+ page,
438
+ [
439
+ unsupportedIssueSortWarning(
218
440
  options.providerId,
219
441
  domain,
220
442
  options.connectionId,
221
- `Account-wide issue search is not supported by '${options.providerId}'; scope the read to repositories instead.`,
443
+ resolvedRepoScopedSort.rejection,
222
444
  ),
223
- );
224
- }
225
-
226
- const items = requestedPageMissing ? [] : (value?.values ?? []);
227
- // Fold in structured per-scope failures from the account-wide fan-out (e.g. Azure across projects):
228
- // scope-aware warnings + `fetchFailed` when a scope failed, without discarding the successful items.
229
- const assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);
230
- // An account-wide search that couldn't confirm completeness (a provider cap with no cursor, or a
231
- // per-scope backstop/failure) is incomplete and can't be paged; report it as truncated (+ a
232
- // provider-neutral warning, unless a structured failure already explains it) rather than a complete
233
- // list. Don't hard-code GitHub's "100 per category" cap here — Azure reaches this via a per-project
234
- // backstop, and other providers may cap differently.
235
- // The account-wide read is cursor-only (its composite cursor spans several provider searches, so a
236
- // page number can't address it): `hasMore` without a real cursor is a dead end, so report it as
237
- // terminal-but-incomplete rather than inviting the caller to page forever.
238
- const continuation = resolveContinuation(
239
- {
240
- hasMore: requestedPageMissing ? false : (value?.hasMore ?? false),
241
- cursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),
242
- truncated: currentTruncated,
243
- },
244
- undefined,
445
+ ],
446
+ true,
245
447
  );
246
- const truncated = continuation.truncated || assessment.truncated;
247
- if (truncated && warnings.length === 0) {
248
- warnings.push(
249
- truncationWarning(
448
+ }
449
+
450
+ const issuesPagingMode = metadata?.issuesPagingMode;
451
+ const ordering = toIssueOrdering(
452
+ resolvedRepoScopedSort.sort,
453
+ mergesProviderQueries(issuesPagingMode, options.repos),
454
+ );
455
+ // Refused rather than served as concatenated per-scope runs, which would look ordered without being so — and
456
+ // only where it actually merges, since the same key against the same provider is perfectly answerable for a
457
+ // single repository or project.
458
+ if (ordering.unmergeable != null) {
459
+ return refusedPage(
460
+ page,
461
+ [
462
+ unmergeableIssueSortWarning(
250
463
  options.providerId,
251
464
  domain,
252
465
  options.connectionId,
253
- 'Account-wide issue search',
254
- // `exhausted`: this composite read exposes no budget the caller can raise, so nothing it
255
- // could call would return the withheld items.
256
- assessment.fetchFailed || pageFetchFailed ? 'interrupted' : 'exhausted',
466
+ ordering.unmergeable,
467
+ issuesPagingMode === PagingMode.Project ? 'projects' : 'repositories',
257
468
  ),
258
- );
259
- }
260
- // A metadata omission from an earlier page asserts the read succeeded; a later page may since have failed.
261
- reconcileOmissionsWithFailure(warnings, assessment.fetchFailed || pageFetchFailed);
262
- return {
263
- items: items,
264
- warnings: warnings,
265
- page: {
266
- // Positional, per ProviderPageInfo.currentPage. `currentPage` already carries what the provider
267
- // reported or what the internal drain counted; a requested page past the terminal cursor is
268
- // reported as that empty page N. The account-wide read is cursor-only, so a `page` the caller
269
- // didn't pair with a cursor is never echoed.
270
- currentPage: requestedPageMissing
271
- ? page
272
- : resolveCurrentPage({
273
- providerPage: currentPage,
274
- requestedPage: page,
275
- suppliedCursor: options.cursor,
276
- pageAdvanceable: false,
277
- }),
278
- itemsPerPage: items.length,
279
- truncated: truncated || undefined,
280
- },
281
- hasMore: continuation.hasMore,
282
- cursor: continuation.cursor,
283
- fetchFailed: assessment.fetchFailed || pageFetchFailed || undefined,
284
- };
469
+ ],
470
+ true,
471
+ );
285
472
  }
286
473
 
287
474
  const cursor = options.cursor ?? pageToCursor(page);
@@ -302,6 +489,7 @@ export async function listIssuesPage(
302
489
  cursor: cursor,
303
490
  page: options.page,
304
491
  pageSize: options.itemsPerPage,
492
+ sort: ordering.sort,
305
493
  },
306
494
  options.connectionId,
307
495
  ),
@@ -317,7 +505,7 @@ export async function listIssuesPage(
317
505
  // Cursor-only repo-scoped hosts (e.g. GitHub) ignore a synthesized page-number cursor, so a page-only
318
506
  // request is advanced through the provider's own continuations (see drainToRequestedPage).
319
507
  if (
320
- providersMetadata[options.providerId]?.issuesPagingMode === PagingMode.Repos &&
508
+ issuesPagingMode === PagingMode.Repos &&
321
509
  options.page != null &&
322
510
  options.page > 1 &&
323
511
  options.cursor == null &&
@@ -342,6 +530,7 @@ export async function listIssuesPage(
342
530
  includeAllAssignees: options.includeAllAssignees,
343
531
  cursor: cursor,
344
532
  pageSize: options.itemsPerPage,
533
+ sort: ordering.sort,
345
534
  },
346
535
  options.connectionId,
347
536
  ),
@@ -355,12 +544,15 @@ export async function listIssuesPage(
355
544
  pageFetchFailed = drained.fetchFailed;
356
545
  }
357
546
 
547
+ // Ordered once the drain (above) has assembled whichever pages this position spans.
548
+ items = ordering.order(items);
549
+
358
550
  // Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending
359
551
  // them to any captured thrown-error warning without discarding the partial result's items.
360
552
  const assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);
361
553
  // Never advertise `hasMore` without a continuation the caller can act on, and only synthesize a page
362
554
  // number for a host that reads it as one (see `isPageNumberAdvanceable`).
363
- const issuesPageAdvanceable = isPageNumberAdvanceable(providersMetadata[options.providerId]?.issuesPagingMode);
555
+ const issuesPageAdvanceable = isPageNumberAdvanceable(issuesPagingMode);
364
556
  const continuation = resolveContinuation(paged, issuesPageAdvanceable ? paged.page.currentPage + 1 : undefined);
365
557
  // A provider read that couldn't confirm completeness (e.g. Bitbucket's single-page repo issue read
366
558
  // that dropped a repo) sets `paging.truncated`; surface it as a terminal `page.truncated` so a partial