@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.
- package/CHANGELOG.md +49 -1
- package/dist/git/models/issue.d.ts +70 -0
- package/dist/git/models/issue.d.ts.map +1 -1
- package/dist/git/models/issue.js +17 -0
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/models/pullRequest.d.ts +81 -0
- package/dist/git/models/pullRequest.d.ts.map +1 -1
- package/dist/git/models/pullRequest.js +15 -0
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/git/utils/issue.utils.d.ts +41 -1
- package/dist/git/utils/issue.utils.d.ts.map +1 -1
- package/dist/git/utils/issue.utils.js +80 -0
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/pullRequest.utils.d.ts +11 -1
- package/dist/git/utils/pullRequest.utils.d.ts.map +1 -1
- package/dist/git/utils/pullRequest.utils.js +40 -0
- package/dist/git/utils/pullRequest.utils.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +70 -10
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +284 -31
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/api/issueSearchQuery.d.ts +49 -5
- package/dist/plus/git-github/api/issueSearchQuery.d.ts.map +1 -1
- package/dist/plus/git-github/api/issueSearchQuery.js +64 -6
- package/dist/plus/git-github/api/issueSearchQuery.js.map +1 -1
- package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts +27 -1
- package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts.map +1 -1
- package/dist/plus/git-github/api/pullRequestSearchQuery.js +73 -6
- package/dist/plus/git-github/api/pullRequestSearchQuery.js.map +1 -1
- package/dist/plus/git-github/models.d.ts +17 -5
- package/dist/plus/git-github/models.d.ts.map +1 -1
- package/dist/plus/git-github/models.js +14 -1
- package/dist/plus/git-github/models.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +18 -0
- package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
- package/dist/plus/integrations/collectionMetadata.js +24 -0
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +31 -10
- package/dist/plus/integrations/integrationService.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.js +9 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +85 -31
- package/dist/plus/integrations/manager.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +18 -31
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +29 -1
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +1 -1
- package/dist/plus/integrations/models/integration.d.ts.map +1 -1
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +63 -1
- package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.d.ts +7 -21
- package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providerFilters.d.ts +2 -2
- package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
- package/dist/plus/integrations/providers/accounts.d.ts +6 -0
- package/dist/plus/integrations/providers/accounts.d.ts.map +1 -0
- package/dist/plus/integrations/providers/accounts.js +30 -0
- package/dist/plus/integrations/providers/accounts.js.map +1 -0
- package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.js +1 -0
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +9 -0
- package/dist/plus/integrations/providers/github.d.ts.map +1 -1
- package/dist/plus/integrations/providers/github.js +115 -135
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +9 -0
- package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
- package/dist/plus/integrations/providers/gitlab.js +24 -4
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +36 -0
- package/dist/plus/integrations/providers/issueSorts.d.ts.map +1 -0
- package/dist/plus/integrations/providers/issueSorts.js +76 -0
- package/dist/plus/integrations/providers/issueSorts.js.map +1 -0
- package/dist/plus/integrations/providers/jira.d.ts +3 -9
- package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
- package/dist/plus/integrations/providers/jira.js +5 -1
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +24 -9
- package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
- package/dist/plus/integrations/providers/linear.js +63 -5
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +76 -38
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.js +91 -105
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +18 -3
- package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
- package/dist/plus/integrations/providers/providersApi.js +24 -7
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/pullRequestReviews.d.ts +50 -0
- package/dist/plus/integrations/providers/pullRequestReviews.d.ts.map +1 -0
- package/dist/plus/integrations/providers/pullRequestReviews.js +99 -0
- package/dist/plus/integrations/providers/pullRequestReviews.js.map +1 -0
- package/dist/plus/integrations/providers/trello.d.ts +3 -9
- package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
- package/dist/plus/integrations/providers/trello.js +1 -0
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js +5 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.d.ts +6 -0
- package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/counts.d.ts +63 -1
- package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
- package/dist/plus/integrations/reads/counts.js +146 -2
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +7 -1
- package/dist/plus/integrations/reads/drains.d.ts.map +1 -1
- package/dist/plus/integrations/reads/drains.js +13 -2
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +59 -9
- package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
- package/dist/plus/integrations/reads/filters.js +39 -0
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +18 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +40 -3
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts +27 -3
- package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issues.js +223 -123
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/ordering.d.ts +101 -0
- package/dist/plus/integrations/reads/ordering.d.ts.map +1 -0
- package/dist/plus/integrations/reads/ordering.js +48 -0
- package/dist/plus/integrations/reads/ordering.js.map +1 -0
- package/dist/plus/integrations/reads/searchIssues.d.ts +7 -2
- package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -1
- package/dist/plus/integrations/reads/searchIssues.js +9 -3
- package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.d.ts +4 -3
- package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.js +6 -4
- package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/sweeps.js +1 -1
- package/dist/plus/integrations/reads/sweeps.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +35 -3
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +58 -4
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +15 -0
- package/dist/plus/integrations/results.d.ts.map +1 -1
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +76 -10
- package/docs/kepler-read-api-parity.md +9 -0
- package/package.json +2 -2
- package/src/git/models/issue.ts +73 -0
- package/src/git/models/pullRequest.ts +84 -0
- package/src/git/utils/issue.utils.ts +91 -1
- package/src/git/utils/pullRequest.utils.ts +50 -0
- package/src/plus/git-github/api/github.ts +356 -36
- package/src/plus/git-github/api/issueSearchQuery.ts +67 -7
- package/src/plus/git-github/api/pullRequestSearchQuery.ts +81 -7
- package/src/plus/git-github/models.ts +29 -5
- package/src/plus/integrations/collectionMetadata.ts +24 -0
- package/src/plus/integrations/integrationService.ts +40 -12
- package/src/plus/integrations/manager.ts +89 -35
- package/src/plus/integrations/models/gitHostIntegration.ts +47 -31
- package/src/plus/integrations/models/integration.ts +1 -0
- package/src/plus/integrations/models/issueReads.ts +65 -1
- package/src/plus/integrations/models/issuesIntegration.ts +7 -6
- package/src/plus/integrations/providerFilters.ts +8 -1
- package/src/plus/integrations/providers/accounts.ts +34 -0
- package/src/plus/integrations/providers/azureDevOps.ts +1 -0
- package/src/plus/integrations/providers/github.ts +134 -144
- package/src/plus/integrations/providers/gitlab.ts +29 -10
- package/src/plus/integrations/providers/issueSorts.ts +89 -0
- package/src/plus/integrations/providers/jira.ts +9 -3
- package/src/plus/integrations/providers/linear.ts +77 -8
- package/src/plus/integrations/providers/models.ts +185 -128
- package/src/plus/integrations/providers/providersApi.ts +51 -13
- package/src/plus/integrations/providers/pullRequestReviews.ts +122 -0
- package/src/plus/integrations/providers/trello.ts +4 -3
- package/src/plus/integrations/providers/utils/providerPaging.ts +6 -1
- package/src/plus/integrations/reads/broaden.ts +6 -0
- package/src/plus/integrations/reads/counts.ts +281 -3
- package/src/plus/integrations/reads/drains.ts +12 -2
- package/src/plus/integrations/reads/filters.ts +92 -8
- package/src/plus/integrations/reads/issueTracker.ts +78 -4
- package/src/plus/integrations/reads/issues.ts +386 -194
- package/src/plus/integrations/reads/ordering.ts +124 -0
- package/src/plus/integrations/reads/searchIssues.ts +15 -3
- package/src/plus/integrations/reads/searchPullRequests.ts +12 -4
- package/src/plus/integrations/reads/sweeps.ts +1 -1
- package/src/plus/integrations/reads/warnings.ts +84 -2
- package/src/plus/integrations/results.ts +15 -0
|
@@ -8,8 +8,11 @@ import { graphql, GraphqlResponseError } from '@octokit/graphql';
|
|
|
8
8
|
import { request } from '@octokit/request';
|
|
9
9
|
import { RequestError } from '@octokit/request-error';
|
|
10
10
|
import { AuthenticationError, AuthenticationErrorReason, RequestClientError, RequestNotFoundError, RequestRateLimitError, } from '../../../git/errors.js';
|
|
11
|
-
import {
|
|
11
|
+
import { defaultIssueSort } from '../../../git/models/issue.js';
|
|
12
|
+
import { defaultPullRequestSort, PullRequestMergeMethod } from '../../../git/models/pullRequest.js';
|
|
12
13
|
import { getGitHubNoReplyAddressParts } from '../../../git/remotes/github.js';
|
|
14
|
+
import { effectiveIssueSort, getIssueComparator } from '../../../git/utils/issue.utils.js';
|
|
15
|
+
import { getPullRequestComparator } from '../../../git/utils/pullRequest.utils.js';
|
|
13
16
|
import { createRevisionRange, getRevisionRangeParts, isRevisionRange, isSha, } from '../../../git/utils/revision.utils.js';
|
|
14
17
|
import { chunk } from '../../../utils/array.js';
|
|
15
18
|
import { base64 } from '../../../utils/base64.js';
|
|
@@ -24,9 +27,18 @@ import { parseUri } from '../../../utils/uri.js';
|
|
|
24
27
|
import { fromString, satisfies } from '../../../utils/version.js';
|
|
25
28
|
import { fromGitHubIssue, fromGitHubIssueOrPullRequestState, fromGitHubPullRequest, fromGitHubPullRequestLite, } from '../models.js';
|
|
26
29
|
import { githubSearchResultLimit } from './config.js';
|
|
27
|
-
import { gitHubIssueSearchRelationships, toGitHubIssueSearchQualifiers, toGitHubIssueSearchScopeQualifiers, } from './issueSearchQuery.js';
|
|
30
|
+
import { gitHubIssueSearchRelationships, toGitHubIssueSearchQualifiers, toGitHubIssueSearchScopeQualifiers, toGitHubIssueSortQualifier, } from './issueSearchQuery.js';
|
|
28
31
|
import { toGitHubPullRequestSearchFacets } from './pullRequestSearchQuery.js';
|
|
29
32
|
const emptyPagedResult = Object.freeze({ values: [] });
|
|
33
|
+
/**
|
|
34
|
+
* What an issue-search cursor records when the caller asked for no ordering at all.
|
|
35
|
+
*
|
|
36
|
+
* A sentinel rather than an omitted field, because omitted already means something else and more important: a
|
|
37
|
+
* cursor persisted before ordering existed. Distinguishing the two is what lets an old cursor keep resuming while
|
|
38
|
+
* a genuine change from unordered to ordered is still refused. Not an `IssueSorting`, so it can never collide
|
|
39
|
+
* with one.
|
|
40
|
+
*/
|
|
41
|
+
const unsortedCursorSort = 'unsorted';
|
|
30
42
|
const emptyBlameResult = Object.freeze({ ranges: [] });
|
|
31
43
|
// Transient gateway/network failures (e.g. an upstream `502 Bad Gateway`) are worth a few quick
|
|
32
44
|
// retries before surfacing to the caller. octokit provides no built-in retry for the standalone
|
|
@@ -40,10 +52,17 @@ const requestRetryMaxDelay = 2000; // ms
|
|
|
40
52
|
const accountResolveBatchSize = 25;
|
|
41
53
|
// Pull-request search selects the full PR fragment (reviews, requests, refs, commits, etc.),
|
|
42
54
|
// which makes GitHub reject broad 100-node searches with `Resource limits for this query
|
|
43
|
-
// exceeded` on large repositories. Thirty keeps the default within that GraphQL cost budget
|
|
44
|
-
//
|
|
55
|
+
// exceeded` on large repositories. Thirty keeps the default within that GraphQL cost budget;
|
|
56
|
+
// the 100-node maximum the connection accepts is for the lite shape, whose per-node cost is a
|
|
57
|
+
// fraction of it. The budget is per DOCUMENT, and one document holds every relationship × state
|
|
58
|
+
// facet, so the worst case scales with the facet count (five relationships × four states = 20).
|
|
45
59
|
const defaultPullRequestSearchPageSize = 30;
|
|
46
60
|
const maxPullRequestSearchPageSize = 100;
|
|
61
|
+
// Pages `searchMyPullRequests` drains for a caller that wants a whole list rather than a page.
|
|
62
|
+
// Four reduced pages cover more than the single 100-node page this read served before it paged,
|
|
63
|
+
// so the smaller page costs coverage nowhere; it bounds an unbounded drain on a read whose
|
|
64
|
+
// consumers (Launchpad, the Graph pull-request panel) block on it.
|
|
65
|
+
const maxMyPullRequestSearchPages = 4;
|
|
47
66
|
function isRetryableTransientError(ex) {
|
|
48
67
|
// An aborted request is rethrown as the original `AbortError` (not a `RequestError`), so it is
|
|
49
68
|
// excluded here. octokit maps a fetch/network failure to a `RequestError` with status 500 and
|
|
@@ -142,6 +161,24 @@ repository {
|
|
|
142
161
|
viewerPermission
|
|
143
162
|
}
|
|
144
163
|
`;
|
|
164
|
+
/**
|
|
165
|
+
* One review's fields, shared by `latestReviews` and `viewerLatestReview`. The two selections must stay
|
|
166
|
+
* identical: `fromGitHubPullRequest` merges the viewer's review into the capped `latestReviews` window and
|
|
167
|
+
* dedups by `id`, so a field present in only one of them would produce rows that differ by where they came
|
|
168
|
+
* from rather than by what they are.
|
|
169
|
+
*/
|
|
170
|
+
const gqlPullRequestReviewFragment = `
|
|
171
|
+
id
|
|
172
|
+
author {
|
|
173
|
+
login
|
|
174
|
+
avatarUrl(size: $avatarSize)
|
|
175
|
+
url
|
|
176
|
+
}
|
|
177
|
+
state
|
|
178
|
+
commit {
|
|
179
|
+
oid
|
|
180
|
+
}
|
|
181
|
+
`;
|
|
145
182
|
const gqlPullRequestFragment = `
|
|
146
183
|
${gqlPullRequestLiteFragment}
|
|
147
184
|
additions
|
|
@@ -161,14 +198,12 @@ mergedBy {
|
|
|
161
198
|
reviewDecision
|
|
162
199
|
latestReviews(first: 25) {
|
|
163
200
|
nodes {
|
|
164
|
-
|
|
165
|
-
login
|
|
166
|
-
avatarUrl(size: $avatarSize)
|
|
167
|
-
url
|
|
168
|
-
}
|
|
169
|
-
state
|
|
201
|
+
${gqlPullRequestReviewFragment}
|
|
170
202
|
}
|
|
171
203
|
}
|
|
204
|
+
viewerLatestReview {
|
|
205
|
+
${gqlPullRequestReviewFragment}
|
|
206
|
+
}
|
|
172
207
|
reviewRequests(first: 25) {
|
|
173
208
|
nodes {
|
|
174
209
|
asCodeOwner
|
|
@@ -2150,14 +2185,21 @@ export class GitHubApi {
|
|
|
2150
2185
|
* One page of the current user's pull requests, filtered by state and optionally by an explicit
|
|
2151
2186
|
* relationship qualifier. Backs the PR sweeps, which drain it page by page.
|
|
2152
2187
|
*
|
|
2153
|
-
* Ordering is part of the contract, not an option: always `sort:updated` (most recently updated first),
|
|
2154
|
-
*
|
|
2188
|
+
* Ordering is part of the contract, not an option: always `sort:updated` (most recently updated first), which is
|
|
2189
|
+
* {@link searchPullRequestsPage}'s DEFAULT rather than its only order — that read takes a `criteria.sort`, this
|
|
2190
|
+
* one does not, because a sweep's recency window is what its page budget is defined against. A caller that stops
|
|
2191
|
+
* before `hasMore` clears — every sweep with a
|
|
2155
2192
|
* page budget — therefore retains a well-defined recency window instead of an arbitrary slice of GitHub's
|
|
2156
2193
|
* relevance ranking.
|
|
2157
2194
|
*/
|
|
2158
2195
|
async searchMyPullRequestsPage(provider, token, options, cancellation) {
|
|
2159
2196
|
const scope = getScopedLogger();
|
|
2160
|
-
|
|
2197
|
+
// The page follows the projection rather than being a separate decision: the full fragment is what GitHub
|
|
2198
|
+
// rejects at 100 nodes (see `defaultPullRequestSearchPageSize`), so every read that selects it pages at the
|
|
2199
|
+
// reduced size, and only the lite shape asks for the maximum. A caller that wants a whole list pages for it
|
|
2200
|
+
// — see `searchMyPullRequests`.
|
|
2201
|
+
const configuredLimit = this.config.getLaunchpadQueryLimit?.() ?? maxPullRequestSearchPageSize;
|
|
2202
|
+
const limit = Math.min(options?.summary === true ? maxPullRequestSearchPageSize : defaultPullRequestSearchPageSize, configuredLimit);
|
|
2161
2203
|
try {
|
|
2162
2204
|
const query = `query searchMyPullRequests(
|
|
2163
2205
|
$search: String!
|
|
@@ -2236,9 +2278,47 @@ export class GitHubApi {
|
|
|
2236
2278
|
throw this.handleException(ex, provider, scope, options?.silent);
|
|
2237
2279
|
}
|
|
2238
2280
|
}
|
|
2281
|
+
/**
|
|
2282
|
+
* The whole list rather than a page: this read has no cursor to hand back (its `IntegrationResult<T[]>` return
|
|
2283
|
+
* has no paging channel at all), so it drains up to {@link maxMyPullRequestSearchPages} pages itself.
|
|
2284
|
+
*
|
|
2285
|
+
* Draining instead of taking one page is what lets the full projection page at the reduced size everywhere
|
|
2286
|
+
* (see the `limit` in {@link searchMyPullRequestsPage}): a single page would otherwise have to ask for 100
|
|
2287
|
+
* nodes of the selection GitHub rejects at that size, purely because this caller cannot resume. Ordering is
|
|
2288
|
+
* `sort:updated`, so the pages compose into a defined window rather than a shifting relevance ranking, and the
|
|
2289
|
+
* drain stops as soon as GitHub reports no next page.
|
|
2290
|
+
*/
|
|
2239
2291
|
async searchMyPullRequests(provider, token, options, cancellation) {
|
|
2240
|
-
|
|
2292
|
+
const values = [];
|
|
2293
|
+
// The searches behind one page can overlap (a PR can be authored by and review-requested from the same
|
|
2294
|
+
// user), and a later page can repeat a row an earlier one already served if the PR was updated mid-drain.
|
|
2295
|
+
const seen = new Set();
|
|
2296
|
+
let cursor;
|
|
2297
|
+
for (let page = 0; page < maxMyPullRequestSearchPages; page++) {
|
|
2298
|
+
const result = await this.searchMyPullRequestsPage(provider, token, { ...options, cursor: cursor }, cancellation);
|
|
2299
|
+
for (const pr of result.values) {
|
|
2300
|
+
if (seen.has(pr.url))
|
|
2301
|
+
continue;
|
|
2302
|
+
seen.add(pr.url);
|
|
2303
|
+
values.push(pr);
|
|
2304
|
+
}
|
|
2305
|
+
// A missing or repeated cursor with `hasMore` would loop on the same page forever.
|
|
2306
|
+
if (!result.hasMore || result.cursor == null || result.cursor === cursor)
|
|
2307
|
+
break;
|
|
2308
|
+
cursor = result.cursor;
|
|
2309
|
+
}
|
|
2310
|
+
return values;
|
|
2241
2311
|
}
|
|
2312
|
+
/**
|
|
2313
|
+
* The current user's issues: authored ∪ assigned ∪ mentioned, each its own aliased search behind one composite
|
|
2314
|
+
* cursor. Bound to `@me` by construction, unlike {@link searchIssuesPage}.
|
|
2315
|
+
*
|
|
2316
|
+
* Ordering is OPT-IN here, and that asymmetry with {@link searchIssuesPage} is deliberate: this read has never
|
|
2317
|
+
* requested a sort, so GitHub has always answered it in relevance order. Emitting a default would change which
|
|
2318
|
+
* issues its already-shipped consumers see, so an omitted `sort` still emits no `sort:` qualifier at all and
|
|
2319
|
+
* keeps today's result. Pass one to get a defined order — which is also what makes a page budget meaningful,
|
|
2320
|
+
* since relevance ranking can shift under an unchanged upstream.
|
|
2321
|
+
*/
|
|
2242
2322
|
async searchMyIssues(provider, token, options, cancellation) {
|
|
2243
2323
|
let search = options?.search?.trim() ?? '';
|
|
2244
2324
|
if (options?.user) {
|
|
@@ -2248,7 +2328,10 @@ export class GitHubApi {
|
|
|
2248
2328
|
const repo = ' repo:';
|
|
2249
2329
|
search += `${repo}${options.repos.join(repo)}`;
|
|
2250
2330
|
}
|
|
2251
|
-
|
|
2331
|
+
// A requested sort goes through the same table `searchIssuesPage` uses, so the two GitHub issue reads can't
|
|
2332
|
+
// diverge the first time a key is added. Omitted appends nothing — see this method's contract above.
|
|
2333
|
+
const sortQualifier = toGitHubIssueSortQualifier(options?.sort);
|
|
2334
|
+
const baseFilters = ['type:issue is:open archived:false', sortQualifier].filter(Boolean).join(' ');
|
|
2252
2335
|
// `includeAllAssignees` broadens the assigned category from "assigned to me" to "assigned to anyone"
|
|
2253
2336
|
// (`assignee:*` is GitHub's has-any-assignee qualifier). Authored/mentioned stay bound to `@me` — they're
|
|
2254
2337
|
// user-relative by definition, so an all-assignees read still only surfaces the current user's authored
|
|
@@ -2283,25 +2366,45 @@ export class GitHubApi {
|
|
|
2283
2366
|
if (requested.authored) {
|
|
2284
2367
|
searches.push({ alias: 'authored', query: `${search} ${baseFilters} author:@me`.trim() });
|
|
2285
2368
|
}
|
|
2286
|
-
|
|
2369
|
+
// Field by field, like `searchIssuesPage`: `options` also carries `repos`/`includeAllAssignees`/`categories`,
|
|
2370
|
+
// already folded into `searches[].query` above and undeclared by the callee.
|
|
2371
|
+
return this.searchIssuesByAlias(provider, token, searches, {
|
|
2372
|
+
baseUrl: options?.baseUrl,
|
|
2373
|
+
avatarSize: options?.avatarSize,
|
|
2374
|
+
includeBody: options?.includeBody,
|
|
2375
|
+
cursor: options?.cursor,
|
|
2376
|
+
sort: options?.sort,
|
|
2377
|
+
// This read emitted no `sort:` qualifier at all before ordering existed, so a cursor with no
|
|
2378
|
+
// recorded key came out of a relevance-ordered walk.
|
|
2379
|
+
legacySort: unsortedCursorSort,
|
|
2380
|
+
}, cancellation);
|
|
2287
2381
|
}
|
|
2288
2382
|
/**
|
|
2289
2383
|
* The filtered issue search: issues matching structured criteria over a repository/org scope, with no forced
|
|
2290
2384
|
* relationship to the current user. The issue counterpart of {@link searchMyPullRequestsPage}, and distinct
|
|
2291
2385
|
* from {@link searchMyIssues}, which is permanently bound to `@me`.
|
|
2292
2386
|
*
|
|
2293
|
-
* Ordering is
|
|
2294
|
-
*
|
|
2295
|
-
* order
|
|
2387
|
+
* Ordering is `criteria.sort`, defaulting to most-recently-updated-first — the order this read served before
|
|
2388
|
+
* ordering was an option, so an omitted `sort` emits the identical query. What is NOT optional is that SOME
|
|
2389
|
+
* order is always requested: without one GitHub answers in relevance order, and at the result ceiling that
|
|
2390
|
+
* makes which rows are reachable a function of GitHub's ranking rather than of the request. A key GitHub can't
|
|
2391
|
+
* express (`closed`, `priority`, …) is refused by the facade before the request, not silently downgraded.
|
|
2392
|
+
*
|
|
2393
|
+
* With more than one relationship the page is a UNION of several searches, each ordered by the provider; the
|
|
2394
|
+
* merged page is re-sorted here so the whole page honors the requested key. Across pages the order is still
|
|
2395
|
+
* per-alias — see {@link searchIssuesByAlias}.
|
|
2296
2396
|
*
|
|
2297
2397
|
* Each requested relationship becomes its own aliased search, unioned and deduped by url; with none, a single
|
|
2298
2398
|
* search runs over the scope alone. `criteria.text` and the other free-form values are sanitized so user input
|
|
2299
2399
|
* cannot inject a qualifier and re-scope the search — see {@link toGitHubIssueSearchQualifiers}.
|
|
2300
2400
|
*/
|
|
2301
2401
|
async searchIssuesPage(provider, token, options, cancellation) {
|
|
2402
|
+
// Resolved once: the emitted qualifier, the merged page's comparator and the cursor's fingerprint must all
|
|
2403
|
+
// be the same key, which is what `effectiveIssueSort` exists to guarantee.
|
|
2404
|
+
const sort = effectiveIssueSort(options?.criteria?.sort);
|
|
2302
2405
|
const base = [
|
|
2303
2406
|
...toGitHubIssueSearchScopeQualifiers(options?.org, options?.repos),
|
|
2304
|
-
...toGitHubIssueSearchQualifiers(options?.criteria),
|
|
2407
|
+
...toGitHubIssueSearchQualifiers(options?.criteria, sort),
|
|
2305
2408
|
].join(' ');
|
|
2306
2409
|
// One aliased search per relationship, OR-ed by union. They can't be one query: GitHub AND-s qualifiers,
|
|
2307
2410
|
// so `author:@me assignee:@me` would return the intersection — issues the user both opened and is assigned
|
|
@@ -2313,7 +2416,21 @@ export class GitHubApi {
|
|
|
2313
2416
|
query: `${base} ${gitHubIssueSearchRelationships[r].qualifier}`.trim(),
|
|
2314
2417
|
}))
|
|
2315
2418
|
: [{ alias: 'matched', query: base }];
|
|
2316
|
-
|
|
2419
|
+
// Forwarded field by field rather than spread: `options` also carries `repos`/`org`/`criteria`, which are
|
|
2420
|
+
// already baked into `searches[].query` above and which the callee declares nothing about. `sort` is the
|
|
2421
|
+
// EFFECTIVE key, since the merged page and the cursor's fingerprint must both use the one the query used.
|
|
2422
|
+
return this.searchIssuesByAlias(provider, token, searches, {
|
|
2423
|
+
baseUrl: options?.baseUrl,
|
|
2424
|
+
avatarSize: options?.avatarSize,
|
|
2425
|
+
includeBody: options?.includeBody,
|
|
2426
|
+
cursor: options?.cursor,
|
|
2427
|
+
pageSize: options?.pageSize,
|
|
2428
|
+
sort: sort,
|
|
2429
|
+
// This read has always emitted `sort:updated`, which is `defaultIssueSort` — so a cursor with no
|
|
2430
|
+
// recorded key came out of a walk under exactly that key, and only a caller asking for a
|
|
2431
|
+
// different one has to restart.
|
|
2432
|
+
legacySort: defaultIssueSort,
|
|
2433
|
+
}, cancellation);
|
|
2317
2434
|
}
|
|
2318
2435
|
/**
|
|
2319
2436
|
* Counts issues for several scopes in ONE request, transferring no issues at all — each scope is an aliased
|
|
@@ -2332,9 +2449,12 @@ export class GitHubApi {
|
|
|
2332
2449
|
if (scopes.length === 0)
|
|
2333
2450
|
return [];
|
|
2334
2451
|
const queries = scopes.map(s => {
|
|
2452
|
+
// The same resolved key the search would use, so the count previews the query it previews. Ordering
|
|
2453
|
+
// cannot change a total, but emitting a DIFFERENT qualifier string than the search does would break the
|
|
2454
|
+
// parity this probe is for.
|
|
2335
2455
|
const qualifiers = [
|
|
2336
2456
|
...toGitHubIssueSearchScopeQualifiers(s.org, s.repos),
|
|
2337
|
-
...toGitHubIssueSearchQualifiers(s.criteria),
|
|
2457
|
+
...toGitHubIssueSearchQualifiers(s.criteria, effectiveIssueSort(s.criteria?.sort)),
|
|
2338
2458
|
];
|
|
2339
2459
|
// A relationship set is OR-ed across searches, which a single count can't express — the facade splits
|
|
2340
2460
|
// such a scope into one count per relationship before calling, so at most one is present here.
|
|
@@ -2370,6 +2490,64 @@ export class GitHubApi {
|
|
|
2370
2490
|
throw this.handleException(ex, provider, scope);
|
|
2371
2491
|
}
|
|
2372
2492
|
}
|
|
2493
|
+
/**
|
|
2494
|
+
* The PR twin of {@link countIssues}: counts pull requests for several scopes in ONE request via aliased
|
|
2495
|
+
* `search` fields selecting only `issueCount` with `first: 0`. Same positional/undefined contract as the issue
|
|
2496
|
+
* count.
|
|
2497
|
+
*
|
|
2498
|
+
* The one difference is states. {@link toGitHubPullRequestSearchFacets} fans a scope's states out into one
|
|
2499
|
+
* `search` each, so this reports the count the SAME way {@link searchPullRequestsPage} reports its total — the
|
|
2500
|
+
* LARGEST facet's `issueCount`, not their sum: the result ceiling applies per search, so the max is what
|
|
2501
|
+
* `exceedsProviderLimit` compares against, and summing would claim a total the read never surfaces. Relationships
|
|
2502
|
+
* are OR-ed and can't be a single count; the facade refuses a multi-relationship scope, so at most one is here.
|
|
2503
|
+
*/
|
|
2504
|
+
async countPullRequests(provider, token, scopes, options, cancellation) {
|
|
2505
|
+
const scope = getScopedLogger();
|
|
2506
|
+
if (scopes.length === 0)
|
|
2507
|
+
return [];
|
|
2508
|
+
// Each scope expands to one query string per state facet (all sharing its single relationship, which the
|
|
2509
|
+
// facade guarantees). A scope's count is the MAX across its facets — mirroring searchPullRequestsPage's
|
|
2510
|
+
// totalCount — so the aliases stay grouped by scope and are reduced after the response.
|
|
2511
|
+
const scopeQueries = scopes.map(s => {
|
|
2512
|
+
const scopeQualifiers = toGitHubIssueSearchScopeQualifiers(s.org, s.repos);
|
|
2513
|
+
return toGitHubPullRequestSearchFacets(s.criteria).map(f => [...scopeQualifiers, ...f.qualifiers].join(' '));
|
|
2514
|
+
});
|
|
2515
|
+
// Aliases are positional and generated (`s${scope}f${facet}`): a caller's key is arbitrary text and would
|
|
2516
|
+
// break the document, so results are reduced back to one count per scope by index.
|
|
2517
|
+
const aliased = scopeQueries.flatMap((queries, si) => queries.map((query, fi) => ({ alias: `s${si}f${fi}`, query: query })));
|
|
2518
|
+
const params = aliased.map(a => `$${a.alias}: String!`).join('\n\t\t\t\t');
|
|
2519
|
+
// `first: 0` is what makes this cheap — `issueCount` alone, no nodes over the wire.
|
|
2520
|
+
const fields = aliased
|
|
2521
|
+
.map(a => `${a.alias}: search(query: $${a.alias}, type: ISSUE, first: 0) { issueCount }`)
|
|
2522
|
+
.join('\n\t\t\t\t');
|
|
2523
|
+
const query = `query countPullRequests(
|
|
2524
|
+
${params}
|
|
2525
|
+
) {
|
|
2526
|
+
${fields}
|
|
2527
|
+
}`;
|
|
2528
|
+
const variables = { baseUrl: options?.baseUrl };
|
|
2529
|
+
for (const a of aliased) {
|
|
2530
|
+
variables[a.alias] = a.query;
|
|
2531
|
+
}
|
|
2532
|
+
try {
|
|
2533
|
+
const rsp = await this.graphql(provider, token, query, variables, scope, cancellation);
|
|
2534
|
+
if (rsp == null)
|
|
2535
|
+
return scopes.map(() => undefined);
|
|
2536
|
+
return scopeQueries.map((queries, si) => {
|
|
2537
|
+
let max;
|
|
2538
|
+
for (let fi = 0; fi < queries.length; fi++) {
|
|
2539
|
+
const count = rsp[`s${si}f${fi}`]?.issueCount;
|
|
2540
|
+
if (count != null) {
|
|
2541
|
+
max = max == null ? count : Math.max(max, count);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
return max;
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
catch (ex) {
|
|
2548
|
+
throw this.handleException(ex, provider, scope);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2373
2551
|
/**
|
|
2374
2552
|
* The aliased-search engine behind every GitHub issue search: one GraphQL request carrying N independently
|
|
2375
2553
|
* cursored `search` fields, `@include`-gated so an exhausted or unrequested one costs nothing.
|
|
@@ -2384,8 +2562,17 @@ export class GitHubApi {
|
|
|
2384
2562
|
* {@link searchMyIssues} is one configuration of it (its three `@me` categories), and its alias names are
|
|
2385
2563
|
* that read's published cursor keys.
|
|
2386
2564
|
*
|
|
2387
|
-
* `searches` must have unique aliases, each a valid GraphQL name that is
|
|
2388
|
-
* the composite cursor keys aliases at its top level, alongside those
|
|
2565
|
+
* `searches` must have unique aliases, each a valid GraphQL name that is none of `page`, `truncated` or
|
|
2566
|
+
* `sort` — the composite cursor keys aliases at its top level, alongside those three reserved fields.
|
|
2567
|
+
*
|
|
2568
|
+
* `sort` is the order the caller asked for, which this does two things with. Each alias comes back ordered by
|
|
2569
|
+
* it (the qualifier is already in `searches[].query`), but the UNION of several aliases is not, so the merged
|
|
2570
|
+
* page is re-sorted here; and the key is recorded in the cursor, so a continuation that changed it THROWS
|
|
2571
|
+
* rather than serving a sequence with gaps and repeats. Omitted means the caller asked for no order at all
|
|
2572
|
+
* ({@link searchMyIssues}'s default), which re-sorts nothing and pins nothing.
|
|
2573
|
+
*
|
|
2574
|
+
* `legacySort` is the order the calling read produced BEFORE this field existed, and is what a cursor with no
|
|
2575
|
+
* recorded key is compared against — such a cursor is not of unknown order, it is of that read's old one.
|
|
2389
2576
|
*/
|
|
2390
2577
|
async searchIssuesByAlias(provider, token, searches, options, cancellation) {
|
|
2391
2578
|
const scope = getScopedLogger();
|
|
@@ -2394,10 +2581,19 @@ export class GitHubApi {
|
|
|
2394
2581
|
// by a cursor string that reads back as page 1, restarting the walk with no error and no truncation flag.
|
|
2395
2582
|
// Cheap to check, and it fails at the one call that introduced the collision rather than in a consumer's
|
|
2396
2583
|
// persisted cursor.
|
|
2397
|
-
const reserved = searches.filter(s => s.alias === 'page' || s.alias === 'truncated');
|
|
2584
|
+
const reserved = searches.filter(s => s.alias === 'page' || s.alias === 'truncated' || s.alias === 'sort');
|
|
2398
2585
|
if (reserved.length > 0) {
|
|
2399
2586
|
throw new Error(`Issue search alias(es) ${reserved.map(s => `'${s.alias}'`).join(', ')} collide with the composite cursor's reserved keys`);
|
|
2400
2587
|
}
|
|
2588
|
+
// A key GitHub cannot express emits no `sort:` qualifier, so each alias would come back in RELEVANCE order
|
|
2589
|
+
// while the union below is sorted by that key and the cursor sealed under it: an arbitrary subset,
|
|
2590
|
+
// presented as ordered, resumable only into more of the same. Refused here rather than downgraded, which
|
|
2591
|
+
// is the rule the whole feature is built on. Unreachable through the facade — no `supportedIssueSorts`
|
|
2592
|
+
// table declares a key without a qualifier — so this guards the direct callers of the two public reads,
|
|
2593
|
+
// where `title` is expressible enough to have a comparator and not enough to be a GitHub search qualifier.
|
|
2594
|
+
if (options.sort != null && toGitHubIssueSortQualifier(options.sort) == null) {
|
|
2595
|
+
throw new Error(`GitHub cannot order an issue search by '${options.sort}'`);
|
|
2596
|
+
}
|
|
2401
2597
|
let cursor;
|
|
2402
2598
|
if (options?.cursor != null) {
|
|
2403
2599
|
try {
|
|
@@ -2405,6 +2601,28 @@ export class GitHubApi {
|
|
|
2405
2601
|
}
|
|
2406
2602
|
catch { }
|
|
2407
2603
|
}
|
|
2604
|
+
// The order this request is being made under, as the cursor records it.
|
|
2605
|
+
const requestedSort = options.sort ?? unsortedCursorSort;
|
|
2606
|
+
// A cursor produced under a DIFFERENT order can't be resumed: every alias would continue from a position in
|
|
2607
|
+
// a differently-ordered result set, so the continuation re-emits rows already seen and skips rows never
|
|
2608
|
+
// seen. REFUSED rather than silently restarted from page 1, because a restart cannot be reported honestly
|
|
2609
|
+
// from here: this read is cursor-only, so `resolveCurrentPage` has no page of its own to trust and echoes
|
|
2610
|
+
// the `page` the caller supplied alongside the cursor — page 1's rows would be published as page N, which
|
|
2611
|
+
// is the very confusion the fingerprint exists to prevent. Refusing surfaces a warning + `fetchFailed`, and
|
|
2612
|
+
// the remedy ("drop the cursor") is the caller's to apply.
|
|
2613
|
+
//
|
|
2614
|
+
// A cursor with NO recorded sort predates this field — which is not the same as being of unknown order.
|
|
2615
|
+
// Each read produced exactly one order before ordering was an option (`sort:updated` for the filtered
|
|
2616
|
+
// search, relevance for `searchMyIssues`), so an absent key reads as THAT one, `legacySort`. Compared
|
|
2617
|
+
// rather than waved through: the facade now resolves an omitted key to `defaultIssueSort`, so the
|
|
2618
|
+
// account-wide read's query gained a `sort:updated` qualifier it did not have, and resuming a
|
|
2619
|
+
// relevance-ordered cursor inside it advances each alias through a re-ordered result set — the gaps and
|
|
2620
|
+
// repeats this check exists to prevent, arriving through the very case meant to keep working. A cursor
|
|
2621
|
+
// whose implied key matches the request still resumes, and is sealed with the current one.
|
|
2622
|
+
const cursorSort = cursor?.sort ?? options.legacySort;
|
|
2623
|
+
if (cursor != null && cursorSort !== requestedSort) {
|
|
2624
|
+
throw new Error(`Issue search cursor was produced under sort '${cursorSort}' but '${requestedSort}' was requested; restart the read without a cursor`);
|
|
2625
|
+
}
|
|
2408
2626
|
const page = Math.max(1, Math.trunc(cursor?.page ?? 1));
|
|
2409
2627
|
// A slot is a continuation string, `null` (exhausted), or absent. Anything else came from a malformed or
|
|
2410
2628
|
// foreign cursor, and is read as absent rather than threaded back into the request as a continuation.
|
|
@@ -2481,11 +2699,32 @@ export class GitHubApi {
|
|
|
2481
2699
|
}
|
|
2482
2700
|
// Dedupe by `url`, not `IssueShape.id`: for some providers `id` is a per-repository number, so an
|
|
2483
2701
|
// id-keyed map would collapse distinct issues across repositories.
|
|
2484
|
-
const
|
|
2702
|
+
const deduped = [
|
|
2703
|
+
...uniqueBy(issues, r => r.url, (original, _current) => original),
|
|
2704
|
+
];
|
|
2705
|
+
// Each alias arrived ordered by the server; their concatenation is not, so the merged page is ordered
|
|
2706
|
+
// here. AFTER the dedupe, not before, and that ordering is load-bearing: the alias order is also the
|
|
2707
|
+
// dedupe's precedence (an issue both assigned to and authored by the user surfaces as the assigned one,
|
|
2708
|
+
// per `searchMyIssues`), and sorting first would hand `uniqueBy` a different first occurrence and
|
|
2709
|
+
// silently change which copy wins. The pull-request path sorts BEFORE its dedupe because its facets
|
|
2710
|
+
// carry no such precedence — the difference is deliberate, not an inconsistency to tidy up.
|
|
2711
|
+
//
|
|
2712
|
+
// A comparator is always available for a key GitHub declares (`created`/`updated`/`comments`/
|
|
2713
|
+
// `reactions` are all on `IssueShape`), so `undefined` here means the capability table has outrun this
|
|
2714
|
+
// read; leave the provider's per-alias order rather than inventing one.
|
|
2715
|
+
//
|
|
2716
|
+
// Counted over `active`, not `searches`: continuations exhaust aliases one at a time, so a later page of
|
|
2717
|
+
// a three-category walk can come from ONE surviving search — already ordered by the server. Re-sorting
|
|
2718
|
+
// it could only reproduce that order, while hiding a provider that ignored the qualifier.
|
|
2719
|
+
const comparator = options?.sort != null ? getIssueComparator(options.sort) : undefined;
|
|
2720
|
+
if (comparator != null && active.length > 1) {
|
|
2721
|
+
deduped.sort(comparator);
|
|
2722
|
+
}
|
|
2485
2723
|
// Every alias gets a slot, so an inactive one keeps its `null` and stays out of the next request. A
|
|
2486
2724
|
// missing slot would be read as "never requested", which for a `searches` set that still lists it
|
|
2487
2725
|
// would restart it from its first page.
|
|
2488
|
-
|
|
2726
|
+
// The order is pinned on the way out too, so the next round can refuse a changed key (see above).
|
|
2727
|
+
const next = { page: page + 1, sort: requestedSort };
|
|
2489
2728
|
let hasMore = false;
|
|
2490
2729
|
let continuationMissing = false;
|
|
2491
2730
|
let maxIssueCount = 0;
|
|
@@ -2507,7 +2746,7 @@ export class GitHubApi {
|
|
|
2507
2746
|
const truncated = cursor?.truncated === true || maxIssueCount > githubSearchResultLimit || continuationMissing;
|
|
2508
2747
|
next.truncated = truncated || undefined;
|
|
2509
2748
|
return {
|
|
2510
|
-
values:
|
|
2749
|
+
values: deduped,
|
|
2511
2750
|
cursor: hasMore ? JSON.stringify(next) : undefined,
|
|
2512
2751
|
hasMore: hasMore,
|
|
2513
2752
|
page: page,
|
|
@@ -2524,8 +2763,11 @@ export class GitHubApi {
|
|
|
2524
2763
|
* every active relationship × state facet, so one HTTP request serves one page even when the logical search is
|
|
2525
2764
|
* a union. The cursor preserves each facet's continuation plus the positional page.
|
|
2526
2765
|
*
|
|
2527
|
-
* Ordering is
|
|
2528
|
-
*
|
|
2766
|
+
* Ordering is `criteria.sort`, defaulting to most-recently-updated-first — the order this read served before it
|
|
2767
|
+
* was expressible. It is requested of the provider AND re-applied to the merged page, because the page is a
|
|
2768
|
+
* union of facets and no per-facet server order describes it. The sort is part of the cursor fingerprint, so
|
|
2769
|
+
* changing it invalidates a threaded cursor exactly as changing the text or the scope does. User text is
|
|
2770
|
+
* sanitized before it reaches the provider query. See {@link toGitHubPullRequestSearchFacets}.
|
|
2529
2771
|
*/
|
|
2530
2772
|
async searchPullRequestsPage(provider, token, options, cancellation) {
|
|
2531
2773
|
const scope = getScopedLogger();
|
|
@@ -2636,7 +2878,15 @@ export class GitHubApi {
|
|
|
2636
2878
|
}
|
|
2637
2879
|
}
|
|
2638
2880
|
}
|
|
2639
|
-
|
|
2881
|
+
// The PR path re-sorts the merged page rather than trusting the per-facet server order — GitHub's
|
|
2882
|
+
// server-side PR sort has been unreliable (the per-branch path re-sorts for the same reason), and the
|
|
2883
|
+
// union of several facets is unordered regardless. The key was validated against
|
|
2884
|
+
// `githubPullRequestSearchCapabilities.sorts` upstream, so its comparator is always defined for a PR
|
|
2885
|
+
// shape; the guard only guards the unreachable case rather than inventing an order for it.
|
|
2886
|
+
const comparator = getPullRequestComparator(options?.criteria?.sort ?? defaultPullRequestSort);
|
|
2887
|
+
if (comparator != null) {
|
|
2888
|
+
pullRequests.sort(comparator);
|
|
2889
|
+
}
|
|
2640
2890
|
const values = [
|
|
2641
2891
|
...uniqueBy(pullRequests, pr => pr.url, (original, _current) => original),
|
|
2642
2892
|
];
|
|
@@ -3083,6 +3333,9 @@ __decorate([
|
|
|
3083
3333
|
__decorate([
|
|
3084
3334
|
trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
3085
3335
|
], GitHubApi.prototype, "countIssues", null);
|
|
3336
|
+
__decorate([
|
|
3337
|
+
trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
3338
|
+
], GitHubApi.prototype, "countPullRequests", null);
|
|
3086
3339
|
__decorate([
|
|
3087
3340
|
trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
3088
3341
|
], GitHubApi.prototype, "searchPullRequestsPage", null);
|