@gitkraken/core-gitlens 0.5.108 → 0.5.109
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 +17 -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/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/plus/git-github/api/github.d.ts +33 -6
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +137 -15
- 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/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 +15 -9
- package/dist/plus/integrations/integrationService.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +40 -26
- package/dist/plus/integrations/manager.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +6 -31
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +11 -1
- package/dist/plus/integrations/models/gitHostIntegration.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 +1 -1
- package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/plus/integrations/providers/github.js +1 -0
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
- package/dist/plus/integrations/providers/gitlab.js +1 -0
- 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 +63 -7
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.js +55 -0
- 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/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 +8 -1
- package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
- package/dist/plus/integrations/reads/counts.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 +17 -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/warnings.d.ts +34 -2
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +47 -2
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +10 -0
- package/dist/plus/integrations/results.d.ts.map +1 -1
- package/dist/plus/integrations/results.js.map +1 -1
- package/package.json +2 -2
- package/src/git/models/issue.ts +73 -0
- package/src/git/utils/issue.utils.ts +91 -1
- package/src/plus/git-github/api/github.ts +180 -20
- package/src/plus/git-github/api/issueSearchQuery.ts +67 -7
- package/src/plus/integrations/collectionMetadata.ts +24 -0
- package/src/plus/integrations/integrationService.ts +15 -10
- package/src/plus/integrations/manager.ts +40 -31
- package/src/plus/integrations/models/gitHostIntegration.ts +18 -31
- package/src/plus/integrations/models/issueReads.ts +65 -1
- package/src/plus/integrations/models/issuesIntegration.ts +7 -6
- package/src/plus/integrations/providerFilters.ts +2 -0
- package/src/plus/integrations/providers/azureDevOps.ts +1 -0
- package/src/plus/integrations/providers/github.ts +1 -0
- package/src/plus/integrations/providers/gitlab.ts +1 -0
- 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 +122 -5
- package/src/plus/integrations/providers/providersApi.ts +51 -13
- 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 +8 -1
- package/src/plus/integrations/reads/filters.ts +70 -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/warnings.ts +72 -1
- package/src/plus/integrations/results.ts +10 -0
|
@@ -8,8 +8,10 @@ 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 { defaultIssueSort } from '../../../git/models/issue.js';
|
|
11
12
|
import { 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';
|
|
13
15
|
import { createRevisionRange, getRevisionRangeParts, isRevisionRange, isSha, } from '../../../git/utils/revision.utils.js';
|
|
14
16
|
import { chunk } from '../../../utils/array.js';
|
|
15
17
|
import { base64 } from '../../../utils/base64.js';
|
|
@@ -24,9 +26,18 @@ import { parseUri } from '../../../utils/uri.js';
|
|
|
24
26
|
import { fromString, satisfies } from '../../../utils/version.js';
|
|
25
27
|
import { fromGitHubIssue, fromGitHubIssueOrPullRequestState, fromGitHubPullRequest, fromGitHubPullRequestLite, } from '../models.js';
|
|
26
28
|
import { githubSearchResultLimit } from './config.js';
|
|
27
|
-
import { gitHubIssueSearchRelationships, toGitHubIssueSearchQualifiers, toGitHubIssueSearchScopeQualifiers, } from './issueSearchQuery.js';
|
|
29
|
+
import { gitHubIssueSearchRelationships, toGitHubIssueSearchQualifiers, toGitHubIssueSearchScopeQualifiers, toGitHubIssueSortQualifier, } from './issueSearchQuery.js';
|
|
28
30
|
import { toGitHubPullRequestSearchFacets } from './pullRequestSearchQuery.js';
|
|
29
31
|
const emptyPagedResult = Object.freeze({ values: [] });
|
|
32
|
+
/**
|
|
33
|
+
* What an issue-search cursor records when the caller asked for no ordering at all.
|
|
34
|
+
*
|
|
35
|
+
* A sentinel rather than an omitted field, because omitted already means something else and more important: a
|
|
36
|
+
* cursor persisted before ordering existed. Distinguishing the two is what lets an old cursor keep resuming while
|
|
37
|
+
* a genuine change from unordered to ordered is still refused. Not an `IssueSorting`, so it can never collide
|
|
38
|
+
* with one.
|
|
39
|
+
*/
|
|
40
|
+
const unsortedCursorSort = 'unsorted';
|
|
30
41
|
const emptyBlameResult = Object.freeze({ ranges: [] });
|
|
31
42
|
// Transient gateway/network failures (e.g. an upstream `502 Bad Gateway`) are worth a few quick
|
|
32
43
|
// retries before surfacing to the caller. octokit provides no built-in retry for the standalone
|
|
@@ -2239,6 +2250,16 @@ export class GitHubApi {
|
|
|
2239
2250
|
async searchMyPullRequests(provider, token, options, cancellation) {
|
|
2240
2251
|
return (await this.searchMyPullRequestsPage(provider, token, options, cancellation)).values;
|
|
2241
2252
|
}
|
|
2253
|
+
/**
|
|
2254
|
+
* The current user's issues: authored ∪ assigned ∪ mentioned, each its own aliased search behind one composite
|
|
2255
|
+
* cursor. Bound to `@me` by construction, unlike {@link searchIssuesPage}.
|
|
2256
|
+
*
|
|
2257
|
+
* Ordering is OPT-IN here, and that asymmetry with {@link searchIssuesPage} is deliberate: this read has never
|
|
2258
|
+
* requested a sort, so GitHub has always answered it in relevance order. Emitting a default would change which
|
|
2259
|
+
* issues its already-shipped consumers see, so an omitted `sort` still emits no `sort:` qualifier at all and
|
|
2260
|
+
* keeps today's result. Pass one to get a defined order — which is also what makes a page budget meaningful,
|
|
2261
|
+
* since relevance ranking can shift under an unchanged upstream.
|
|
2262
|
+
*/
|
|
2242
2263
|
async searchMyIssues(provider, token, options, cancellation) {
|
|
2243
2264
|
let search = options?.search?.trim() ?? '';
|
|
2244
2265
|
if (options?.user) {
|
|
@@ -2248,7 +2269,10 @@ export class GitHubApi {
|
|
|
2248
2269
|
const repo = ' repo:';
|
|
2249
2270
|
search += `${repo}${options.repos.join(repo)}`;
|
|
2250
2271
|
}
|
|
2251
|
-
|
|
2272
|
+
// A requested sort goes through the same table `searchIssuesPage` uses, so the two GitHub issue reads can't
|
|
2273
|
+
// diverge the first time a key is added. Omitted appends nothing — see this method's contract above.
|
|
2274
|
+
const sortQualifier = toGitHubIssueSortQualifier(options?.sort);
|
|
2275
|
+
const baseFilters = ['type:issue is:open archived:false', sortQualifier].filter(Boolean).join(' ');
|
|
2252
2276
|
// `includeAllAssignees` broadens the assigned category from "assigned to me" to "assigned to anyone"
|
|
2253
2277
|
// (`assignee:*` is GitHub's has-any-assignee qualifier). Authored/mentioned stay bound to `@me` — they're
|
|
2254
2278
|
// user-relative by definition, so an all-assignees read still only surfaces the current user's authored
|
|
@@ -2283,25 +2307,45 @@ export class GitHubApi {
|
|
|
2283
2307
|
if (requested.authored) {
|
|
2284
2308
|
searches.push({ alias: 'authored', query: `${search} ${baseFilters} author:@me`.trim() });
|
|
2285
2309
|
}
|
|
2286
|
-
|
|
2310
|
+
// Field by field, like `searchIssuesPage`: `options` also carries `repos`/`includeAllAssignees`/`categories`,
|
|
2311
|
+
// already folded into `searches[].query` above and undeclared by the callee.
|
|
2312
|
+
return this.searchIssuesByAlias(provider, token, searches, {
|
|
2313
|
+
baseUrl: options?.baseUrl,
|
|
2314
|
+
avatarSize: options?.avatarSize,
|
|
2315
|
+
includeBody: options?.includeBody,
|
|
2316
|
+
cursor: options?.cursor,
|
|
2317
|
+
sort: options?.sort,
|
|
2318
|
+
// This read emitted no `sort:` qualifier at all before ordering existed, so a cursor with no
|
|
2319
|
+
// recorded key came out of a relevance-ordered walk.
|
|
2320
|
+
legacySort: unsortedCursorSort,
|
|
2321
|
+
}, cancellation);
|
|
2287
2322
|
}
|
|
2288
2323
|
/**
|
|
2289
2324
|
* The filtered issue search: issues matching structured criteria over a repository/org scope, with no forced
|
|
2290
2325
|
* relationship to the current user. The issue counterpart of {@link searchMyPullRequestsPage}, and distinct
|
|
2291
2326
|
* from {@link searchMyIssues}, which is permanently bound to `@me`.
|
|
2292
2327
|
*
|
|
2293
|
-
* Ordering is
|
|
2294
|
-
*
|
|
2295
|
-
* order
|
|
2328
|
+
* Ordering is `criteria.sort`, defaulting to most-recently-updated-first — the order this read served before
|
|
2329
|
+
* ordering was an option, so an omitted `sort` emits the identical query. What is NOT optional is that SOME
|
|
2330
|
+
* order is always requested: without one GitHub answers in relevance order, and at the result ceiling that
|
|
2331
|
+
* makes which rows are reachable a function of GitHub's ranking rather than of the request. A key GitHub can't
|
|
2332
|
+
* express (`closed`, `priority`, …) is refused by the facade before the request, not silently downgraded.
|
|
2333
|
+
*
|
|
2334
|
+
* With more than one relationship the page is a UNION of several searches, each ordered by the provider; the
|
|
2335
|
+
* merged page is re-sorted here so the whole page honors the requested key. Across pages the order is still
|
|
2336
|
+
* per-alias — see {@link searchIssuesByAlias}.
|
|
2296
2337
|
*
|
|
2297
2338
|
* Each requested relationship becomes its own aliased search, unioned and deduped by url; with none, a single
|
|
2298
2339
|
* search runs over the scope alone. `criteria.text` and the other free-form values are sanitized so user input
|
|
2299
2340
|
* cannot inject a qualifier and re-scope the search — see {@link toGitHubIssueSearchQualifiers}.
|
|
2300
2341
|
*/
|
|
2301
2342
|
async searchIssuesPage(provider, token, options, cancellation) {
|
|
2343
|
+
// Resolved once: the emitted qualifier, the merged page's comparator and the cursor's fingerprint must all
|
|
2344
|
+
// be the same key, which is what `effectiveIssueSort` exists to guarantee.
|
|
2345
|
+
const sort = effectiveIssueSort(options?.criteria?.sort);
|
|
2302
2346
|
const base = [
|
|
2303
2347
|
...toGitHubIssueSearchScopeQualifiers(options?.org, options?.repos),
|
|
2304
|
-
...toGitHubIssueSearchQualifiers(options?.criteria),
|
|
2348
|
+
...toGitHubIssueSearchQualifiers(options?.criteria, sort),
|
|
2305
2349
|
].join(' ');
|
|
2306
2350
|
// One aliased search per relationship, OR-ed by union. They can't be one query: GitHub AND-s qualifiers,
|
|
2307
2351
|
// so `author:@me assignee:@me` would return the intersection — issues the user both opened and is assigned
|
|
@@ -2313,7 +2357,21 @@ export class GitHubApi {
|
|
|
2313
2357
|
query: `${base} ${gitHubIssueSearchRelationships[r].qualifier}`.trim(),
|
|
2314
2358
|
}))
|
|
2315
2359
|
: [{ alias: 'matched', query: base }];
|
|
2316
|
-
|
|
2360
|
+
// Forwarded field by field rather than spread: `options` also carries `repos`/`org`/`criteria`, which are
|
|
2361
|
+
// already baked into `searches[].query` above and which the callee declares nothing about. `sort` is the
|
|
2362
|
+
// EFFECTIVE key, since the merged page and the cursor's fingerprint must both use the one the query used.
|
|
2363
|
+
return this.searchIssuesByAlias(provider, token, searches, {
|
|
2364
|
+
baseUrl: options?.baseUrl,
|
|
2365
|
+
avatarSize: options?.avatarSize,
|
|
2366
|
+
includeBody: options?.includeBody,
|
|
2367
|
+
cursor: options?.cursor,
|
|
2368
|
+
pageSize: options?.pageSize,
|
|
2369
|
+
sort: sort,
|
|
2370
|
+
// This read has always emitted `sort:updated`, which is `defaultIssueSort` — so a cursor with no
|
|
2371
|
+
// recorded key came out of a walk under exactly that key, and only a caller asking for a
|
|
2372
|
+
// different one has to restart.
|
|
2373
|
+
legacySort: defaultIssueSort,
|
|
2374
|
+
}, cancellation);
|
|
2317
2375
|
}
|
|
2318
2376
|
/**
|
|
2319
2377
|
* Counts issues for several scopes in ONE request, transferring no issues at all — each scope is an aliased
|
|
@@ -2332,9 +2390,12 @@ export class GitHubApi {
|
|
|
2332
2390
|
if (scopes.length === 0)
|
|
2333
2391
|
return [];
|
|
2334
2392
|
const queries = scopes.map(s => {
|
|
2393
|
+
// The same resolved key the search would use, so the count previews the query it previews. Ordering
|
|
2394
|
+
// cannot change a total, but emitting a DIFFERENT qualifier string than the search does would break the
|
|
2395
|
+
// parity this probe is for.
|
|
2335
2396
|
const qualifiers = [
|
|
2336
2397
|
...toGitHubIssueSearchScopeQualifiers(s.org, s.repos),
|
|
2337
|
-
...toGitHubIssueSearchQualifiers(s.criteria),
|
|
2398
|
+
...toGitHubIssueSearchQualifiers(s.criteria, effectiveIssueSort(s.criteria?.sort)),
|
|
2338
2399
|
];
|
|
2339
2400
|
// A relationship set is OR-ed across searches, which a single count can't express — the facade splits
|
|
2340
2401
|
// such a scope into one count per relationship before calling, so at most one is present here.
|
|
@@ -2384,8 +2445,17 @@ export class GitHubApi {
|
|
|
2384
2445
|
* {@link searchMyIssues} is one configuration of it (its three `@me` categories), and its alias names are
|
|
2385
2446
|
* that read's published cursor keys.
|
|
2386
2447
|
*
|
|
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
|
|
2448
|
+
* `searches` must have unique aliases, each a valid GraphQL name that is none of `page`, `truncated` or
|
|
2449
|
+
* `sort` — the composite cursor keys aliases at its top level, alongside those three reserved fields.
|
|
2450
|
+
*
|
|
2451
|
+
* `sort` is the order the caller asked for, which this does two things with. Each alias comes back ordered by
|
|
2452
|
+
* it (the qualifier is already in `searches[].query`), but the UNION of several aliases is not, so the merged
|
|
2453
|
+
* page is re-sorted here; and the key is recorded in the cursor, so a continuation that changed it THROWS
|
|
2454
|
+
* rather than serving a sequence with gaps and repeats. Omitted means the caller asked for no order at all
|
|
2455
|
+
* ({@link searchMyIssues}'s default), which re-sorts nothing and pins nothing.
|
|
2456
|
+
*
|
|
2457
|
+
* `legacySort` is the order the calling read produced BEFORE this field existed, and is what a cursor with no
|
|
2458
|
+
* recorded key is compared against — such a cursor is not of unknown order, it is of that read's old one.
|
|
2389
2459
|
*/
|
|
2390
2460
|
async searchIssuesByAlias(provider, token, searches, options, cancellation) {
|
|
2391
2461
|
const scope = getScopedLogger();
|
|
@@ -2394,10 +2464,19 @@ export class GitHubApi {
|
|
|
2394
2464
|
// by a cursor string that reads back as page 1, restarting the walk with no error and no truncation flag.
|
|
2395
2465
|
// Cheap to check, and it fails at the one call that introduced the collision rather than in a consumer's
|
|
2396
2466
|
// persisted cursor.
|
|
2397
|
-
const reserved = searches.filter(s => s.alias === 'page' || s.alias === 'truncated');
|
|
2467
|
+
const reserved = searches.filter(s => s.alias === 'page' || s.alias === 'truncated' || s.alias === 'sort');
|
|
2398
2468
|
if (reserved.length > 0) {
|
|
2399
2469
|
throw new Error(`Issue search alias(es) ${reserved.map(s => `'${s.alias}'`).join(', ')} collide with the composite cursor's reserved keys`);
|
|
2400
2470
|
}
|
|
2471
|
+
// A key GitHub cannot express emits no `sort:` qualifier, so each alias would come back in RELEVANCE order
|
|
2472
|
+
// while the union below is sorted by that key and the cursor sealed under it: an arbitrary subset,
|
|
2473
|
+
// presented as ordered, resumable only into more of the same. Refused here rather than downgraded, which
|
|
2474
|
+
// is the rule the whole feature is built on. Unreachable through the facade — no `supportedIssueSorts`
|
|
2475
|
+
// table declares a key without a qualifier — so this guards the direct callers of the two public reads,
|
|
2476
|
+
// where `title` is expressible enough to have a comparator and not enough to be a GitHub search qualifier.
|
|
2477
|
+
if (options.sort != null && toGitHubIssueSortQualifier(options.sort) == null) {
|
|
2478
|
+
throw new Error(`GitHub cannot order an issue search by '${options.sort}'`);
|
|
2479
|
+
}
|
|
2401
2480
|
let cursor;
|
|
2402
2481
|
if (options?.cursor != null) {
|
|
2403
2482
|
try {
|
|
@@ -2405,6 +2484,28 @@ export class GitHubApi {
|
|
|
2405
2484
|
}
|
|
2406
2485
|
catch { }
|
|
2407
2486
|
}
|
|
2487
|
+
// The order this request is being made under, as the cursor records it.
|
|
2488
|
+
const requestedSort = options.sort ?? unsortedCursorSort;
|
|
2489
|
+
// A cursor produced under a DIFFERENT order can't be resumed: every alias would continue from a position in
|
|
2490
|
+
// a differently-ordered result set, so the continuation re-emits rows already seen and skips rows never
|
|
2491
|
+
// seen. REFUSED rather than silently restarted from page 1, because a restart cannot be reported honestly
|
|
2492
|
+
// from here: this read is cursor-only, so `resolveCurrentPage` has no page of its own to trust and echoes
|
|
2493
|
+
// the `page` the caller supplied alongside the cursor — page 1's rows would be published as page N, which
|
|
2494
|
+
// is the very confusion the fingerprint exists to prevent. Refusing surfaces a warning + `fetchFailed`, and
|
|
2495
|
+
// the remedy ("drop the cursor") is the caller's to apply.
|
|
2496
|
+
//
|
|
2497
|
+
// A cursor with NO recorded sort predates this field — which is not the same as being of unknown order.
|
|
2498
|
+
// Each read produced exactly one order before ordering was an option (`sort:updated` for the filtered
|
|
2499
|
+
// search, relevance for `searchMyIssues`), so an absent key reads as THAT one, `legacySort`. Compared
|
|
2500
|
+
// rather than waved through: the facade now resolves an omitted key to `defaultIssueSort`, so the
|
|
2501
|
+
// account-wide read's query gained a `sort:updated` qualifier it did not have, and resuming a
|
|
2502
|
+
// relevance-ordered cursor inside it advances each alias through a re-ordered result set — the gaps and
|
|
2503
|
+
// repeats this check exists to prevent, arriving through the very case meant to keep working. A cursor
|
|
2504
|
+
// whose implied key matches the request still resumes, and is sealed with the current one.
|
|
2505
|
+
const cursorSort = cursor?.sort ?? options.legacySort;
|
|
2506
|
+
if (cursor != null && cursorSort !== requestedSort) {
|
|
2507
|
+
throw new Error(`Issue search cursor was produced under sort '${cursorSort}' but '${requestedSort}' was requested; restart the read without a cursor`);
|
|
2508
|
+
}
|
|
2408
2509
|
const page = Math.max(1, Math.trunc(cursor?.page ?? 1));
|
|
2409
2510
|
// A slot is a continuation string, `null` (exhausted), or absent. Anything else came from a malformed or
|
|
2410
2511
|
// foreign cursor, and is read as absent rather than threaded back into the request as a continuation.
|
|
@@ -2481,11 +2582,32 @@ export class GitHubApi {
|
|
|
2481
2582
|
}
|
|
2482
2583
|
// Dedupe by `url`, not `IssueShape.id`: for some providers `id` is a per-repository number, so an
|
|
2483
2584
|
// id-keyed map would collapse distinct issues across repositories.
|
|
2484
|
-
const
|
|
2585
|
+
const deduped = [
|
|
2586
|
+
...uniqueBy(issues, r => r.url, (original, _current) => original),
|
|
2587
|
+
];
|
|
2588
|
+
// Each alias arrived ordered by the server; their concatenation is not, so the merged page is ordered
|
|
2589
|
+
// here. AFTER the dedupe, not before, and that ordering is load-bearing: the alias order is also the
|
|
2590
|
+
// dedupe's precedence (an issue both assigned to and authored by the user surfaces as the assigned one,
|
|
2591
|
+
// per `searchMyIssues`), and sorting first would hand `uniqueBy` a different first occurrence and
|
|
2592
|
+
// silently change which copy wins. The pull-request path sorts BEFORE its dedupe because its facets
|
|
2593
|
+
// carry no such precedence — the difference is deliberate, not an inconsistency to tidy up.
|
|
2594
|
+
//
|
|
2595
|
+
// A comparator is always available for a key GitHub declares (`created`/`updated`/`comments`/
|
|
2596
|
+
// `reactions` are all on `IssueShape`), so `undefined` here means the capability table has outrun this
|
|
2597
|
+
// read; leave the provider's per-alias order rather than inventing one.
|
|
2598
|
+
//
|
|
2599
|
+
// Counted over `active`, not `searches`: continuations exhaust aliases one at a time, so a later page of
|
|
2600
|
+
// a three-category walk can come from ONE surviving search — already ordered by the server. Re-sorting
|
|
2601
|
+
// it could only reproduce that order, while hiding a provider that ignored the qualifier.
|
|
2602
|
+
const comparator = options?.sort != null ? getIssueComparator(options.sort) : undefined;
|
|
2603
|
+
if (comparator != null && active.length > 1) {
|
|
2604
|
+
deduped.sort(comparator);
|
|
2605
|
+
}
|
|
2485
2606
|
// Every alias gets a slot, so an inactive one keeps its `null` and stays out of the next request. A
|
|
2486
2607
|
// missing slot would be read as "never requested", which for a `searches` set that still lists it
|
|
2487
2608
|
// would restart it from its first page.
|
|
2488
|
-
|
|
2609
|
+
// The order is pinned on the way out too, so the next round can refuse a changed key (see above).
|
|
2610
|
+
const next = { page: page + 1, sort: requestedSort };
|
|
2489
2611
|
let hasMore = false;
|
|
2490
2612
|
let continuationMissing = false;
|
|
2491
2613
|
let maxIssueCount = 0;
|
|
@@ -2507,7 +2629,7 @@ export class GitHubApi {
|
|
|
2507
2629
|
const truncated = cursor?.truncated === true || maxIssueCount > githubSearchResultLimit || continuationMissing;
|
|
2508
2630
|
next.truncated = truncated || undefined;
|
|
2509
2631
|
return {
|
|
2510
|
-
values:
|
|
2632
|
+
values: deduped,
|
|
2511
2633
|
cursor: hasMore ? JSON.stringify(next) : undefined,
|
|
2512
2634
|
hasMore: hasMore,
|
|
2513
2635
|
page: page,
|