@gitkraken/core-gitlens 0.5.109 → 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 +33 -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/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 +37 -4
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +147 -16
- package/dist/plus/git-github/api/github.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/integrationService.d.ts +16 -1
- 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 +45 -5
- package/dist/plus/integrations/manager.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +12 -0
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +18 -0
- 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/providerFilters.d.ts +1 -1
- 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/github.d.ts +9 -0
- package/dist/plus/integrations/providers/github.d.ts.map +1 -1
- package/dist/plus/integrations/providers/github.js +114 -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 +23 -4
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +14 -32
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.js +36 -105
- package/dist/plus/integrations/providers/models.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/reads/counts.d.ts +55 -0
- 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.map +1 -1
- package/dist/plus/integrations/reads/filters.js +22 -0
- package/dist/plus/integrations/reads/filters.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 +2 -2
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +11 -2
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +6 -1
- 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 +1 -1
- package/src/git/models/pullRequest.ts +84 -0
- package/src/git/utils/pullRequest.utils.ts +50 -0
- package/src/plus/git-github/api/github.ts +176 -16
- package/src/plus/git-github/api/pullRequestSearchQuery.ts +81 -7
- package/src/plus/git-github/models.ts +29 -5
- package/src/plus/integrations/integrationService.ts +25 -2
- package/src/plus/integrations/manager.ts +50 -5
- package/src/plus/integrations/models/gitHostIntegration.ts +29 -0
- package/src/plus/integrations/models/integration.ts +1 -0
- package/src/plus/integrations/providerFilters.ts +6 -1
- package/src/plus/integrations/providers/accounts.ts +34 -0
- package/src/plus/integrations/providers/github.ts +133 -144
- package/src/plus/integrations/providers/gitlab.ts +28 -10
- package/src/plus/integrations/providers/models.ts +64 -124
- package/src/plus/integrations/providers/pullRequestReviews.ts +122 -0
- package/src/plus/integrations/reads/counts.ts +273 -2
- package/src/plus/integrations/reads/drains.ts +12 -2
- package/src/plus/integrations/reads/filters.ts +22 -0
- 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 +12 -1
- package/src/plus/integrations/results.ts +6 -1
|
@@ -4,9 +4,58 @@ import type {
|
|
|
4
4
|
PullRequestRefs,
|
|
5
5
|
PullRequestRepositoryIdentityDescriptor,
|
|
6
6
|
PullRequestShape,
|
|
7
|
+
PullRequestSortField,
|
|
8
|
+
PullRequestSorting,
|
|
7
9
|
} from '../models/pullRequest.js';
|
|
8
10
|
import { shortenRevision } from './revision.utils.js';
|
|
9
11
|
|
|
12
|
+
/**
|
|
13
|
+
* How each sort field is read off a {@link PullRequestShape}, for the filtered PR search, which MERGES its
|
|
14
|
+
* relationship × state facets in the facade and so has to order the union itself instead of trusting the
|
|
15
|
+
* per-facet server order. Only the fields the shape carries appear — the same rule as `getIssueComparator`, and
|
|
16
|
+
* the reason {@link PullRequestSortField} is just these two.
|
|
17
|
+
*/
|
|
18
|
+
const pullRequestSortValues: Partial<
|
|
19
|
+
Record<PullRequestSortField, (pr: PullRequestShape) => number | Date | string | undefined>
|
|
20
|
+
> = {
|
|
21
|
+
created: pr => pr.createdDate,
|
|
22
|
+
updated: pr => pr.updatedDate,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A comparator for one sort key over normalized pull requests, or `undefined` when the field isn't derivable
|
|
27
|
+
* from a {@link PullRequestShape} — the signal that the key is only honorable on a SINGLE-origin read where the
|
|
28
|
+
* provider already ordered the page. Missing values sort LAST in both directions (partitioned before any
|
|
29
|
+
* arithmetic so two missing values compare equal, exactly as `getIssueComparator` does).
|
|
30
|
+
*
|
|
31
|
+
* Note it orders whatever it is handed. Sorting a page already capped at the result ceiling does not make it the
|
|
32
|
+
* top N.
|
|
33
|
+
*/
|
|
34
|
+
export function getPullRequestComparator(
|
|
35
|
+
sort: PullRequestSorting,
|
|
36
|
+
): ((a: PullRequestShape, b: PullRequestShape) => number) | undefined {
|
|
37
|
+
const [field, direction] = sort.split(':') as [PullRequestSortField, 'asc' | 'desc'];
|
|
38
|
+
const getValue = pullRequestSortValues[field];
|
|
39
|
+
if (getValue == null) return undefined;
|
|
40
|
+
|
|
41
|
+
return (a, b) => {
|
|
42
|
+
const left = toComparable(getValue(a));
|
|
43
|
+
const right = toComparable(getValue(b));
|
|
44
|
+
if (left == null || right == null) {
|
|
45
|
+
if (left == null && right == null) return 0;
|
|
46
|
+
|
|
47
|
+
return left == null ? 1 : -1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ordered = left < right ? -1 : left > right ? 1 : 0;
|
|
51
|
+
return direction === 'asc' ? ordered : -ordered;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toComparable(value: number | Date | string | undefined): number | string | undefined {
|
|
56
|
+
return value instanceof Date ? value.getTime() : value;
|
|
57
|
+
}
|
|
58
|
+
|
|
10
59
|
export interface PullRequestUrlIdentity<TProvider extends string = string> {
|
|
11
60
|
provider?: TProvider;
|
|
12
61
|
|
|
@@ -143,6 +192,7 @@ export function serializePullRequest(value: PullRequest): PullRequestShape {
|
|
|
143
192
|
thumbsUpCount: value.thumbsUpCount,
|
|
144
193
|
reviewDecision: value.reviewDecision,
|
|
145
194
|
reviewRequests: value.reviewRequests,
|
|
195
|
+
latestReviews: value.latestReviews,
|
|
146
196
|
assignees: value.assignees,
|
|
147
197
|
authoredByMe: value.authoredByMe,
|
|
148
198
|
project: value.project
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
PullRequestState,
|
|
22
22
|
PullRequestStateFilter,
|
|
23
23
|
} from '../../../git/models/pullRequest.js';
|
|
24
|
-
import { PullRequestMergeMethod } from '../../../git/models/pullRequest.js';
|
|
24
|
+
import { defaultPullRequestSort, PullRequestMergeMethod } from '../../../git/models/pullRequest.js';
|
|
25
25
|
import type { Provider } from '../../../git/models/remoteProvider.js';
|
|
26
26
|
import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata.js';
|
|
27
27
|
import type { GitRevisionRange } from '../../../git/models/revision.js';
|
|
@@ -29,6 +29,7 @@ import type { GitUser } from '../../../git/models/user.js';
|
|
|
29
29
|
import type { RepositoryVisibility } from '../../../git/providers/types.js';
|
|
30
30
|
import { getGitHubNoReplyAddressParts } from '../../../git/remotes/github.js';
|
|
31
31
|
import { effectiveIssueSort, getIssueComparator } from '../../../git/utils/issue.utils.js';
|
|
32
|
+
import { getPullRequestComparator } from '../../../git/utils/pullRequest.utils.js';
|
|
32
33
|
import {
|
|
33
34
|
createRevisionRange,
|
|
34
35
|
getRevisionRangeParts,
|
|
@@ -110,10 +111,17 @@ const accountResolveBatchSize = 25;
|
|
|
110
111
|
|
|
111
112
|
// Pull-request search selects the full PR fragment (reviews, requests, refs, commits, etc.),
|
|
112
113
|
// which makes GitHub reject broad 100-node searches with `Resource limits for this query
|
|
113
|
-
// exceeded` on large repositories. Thirty keeps the default within that GraphQL cost budget
|
|
114
|
-
//
|
|
114
|
+
// exceeded` on large repositories. Thirty keeps the default within that GraphQL cost budget;
|
|
115
|
+
// the 100-node maximum the connection accepts is for the lite shape, whose per-node cost is a
|
|
116
|
+
// fraction of it. The budget is per DOCUMENT, and one document holds every relationship × state
|
|
117
|
+
// facet, so the worst case scales with the facet count (five relationships × four states = 20).
|
|
115
118
|
const defaultPullRequestSearchPageSize = 30;
|
|
116
119
|
const maxPullRequestSearchPageSize = 100;
|
|
120
|
+
// Pages `searchMyPullRequests` drains for a caller that wants a whole list rather than a page.
|
|
121
|
+
// Four reduced pages cover more than the single 100-node page this read served before it paged,
|
|
122
|
+
// so the smaller page costs coverage nowhere; it bounds an unbounded drain on a read whose
|
|
123
|
+
// consumers (Launchpad, the Graph pull-request panel) block on it.
|
|
124
|
+
const maxMyPullRequestSearchPages = 4;
|
|
117
125
|
|
|
118
126
|
function isRetryableTransientError(ex: unknown): ex is RequestError {
|
|
119
127
|
// An aborted request is rethrown as the original `AbortError` (not a `RequestError`), so it is
|
|
@@ -259,6 +267,24 @@ repository {
|
|
|
259
267
|
viewerPermission
|
|
260
268
|
}
|
|
261
269
|
`;
|
|
270
|
+
/**
|
|
271
|
+
* One review's fields, shared by `latestReviews` and `viewerLatestReview`. The two selections must stay
|
|
272
|
+
* identical: `fromGitHubPullRequest` merges the viewer's review into the capped `latestReviews` window and
|
|
273
|
+
* dedups by `id`, so a field present in only one of them would produce rows that differ by where they came
|
|
274
|
+
* from rather than by what they are.
|
|
275
|
+
*/
|
|
276
|
+
const gqlPullRequestReviewFragment = `
|
|
277
|
+
id
|
|
278
|
+
author {
|
|
279
|
+
login
|
|
280
|
+
avatarUrl(size: $avatarSize)
|
|
281
|
+
url
|
|
282
|
+
}
|
|
283
|
+
state
|
|
284
|
+
commit {
|
|
285
|
+
oid
|
|
286
|
+
}
|
|
287
|
+
`;
|
|
262
288
|
const gqlPullRequestFragment = `
|
|
263
289
|
${gqlPullRequestLiteFragment}
|
|
264
290
|
additions
|
|
@@ -278,14 +304,12 @@ mergedBy {
|
|
|
278
304
|
reviewDecision
|
|
279
305
|
latestReviews(first: 25) {
|
|
280
306
|
nodes {
|
|
281
|
-
|
|
282
|
-
login
|
|
283
|
-
avatarUrl(size: $avatarSize)
|
|
284
|
-
url
|
|
285
|
-
}
|
|
286
|
-
state
|
|
307
|
+
${gqlPullRequestReviewFragment}
|
|
287
308
|
}
|
|
288
309
|
}
|
|
310
|
+
viewerLatestReview {
|
|
311
|
+
${gqlPullRequestReviewFragment}
|
|
312
|
+
}
|
|
289
313
|
reviewRequests(first: 25) {
|
|
290
314
|
nodes {
|
|
291
315
|
asCodeOwner
|
|
@@ -3526,8 +3550,10 @@ export class GitHubApi {
|
|
|
3526
3550
|
* One page of the current user's pull requests, filtered by state and optionally by an explicit
|
|
3527
3551
|
* relationship qualifier. Backs the PR sweeps, which drain it page by page.
|
|
3528
3552
|
*
|
|
3529
|
-
* Ordering is part of the contract, not an option: always `sort:updated` (most recently updated first),
|
|
3530
|
-
*
|
|
3553
|
+
* Ordering is part of the contract, not an option: always `sort:updated` (most recently updated first), which is
|
|
3554
|
+
* {@link searchPullRequestsPage}'s DEFAULT rather than its only order — that read takes a `criteria.sort`, this
|
|
3555
|
+
* one does not, because a sweep's recency window is what its page budget is defined against. A caller that stops
|
|
3556
|
+
* before `hasMore` clears — every sweep with a
|
|
3531
3557
|
* page budget — therefore retains a well-defined recency window instead of an arbitrary slice of GitHub's
|
|
3532
3558
|
* relevance ranking.
|
|
3533
3559
|
*/
|
|
@@ -3558,7 +3584,15 @@ export class GitHubApi {
|
|
|
3558
3584
|
cancellation?: AbortSignal,
|
|
3559
3585
|
): Promise<{ values: PullRequest[]; cursor?: string; hasMore: boolean; truncated: boolean }> {
|
|
3560
3586
|
const scope = getScopedLogger();
|
|
3561
|
-
|
|
3587
|
+
// The page follows the projection rather than being a separate decision: the full fragment is what GitHub
|
|
3588
|
+
// rejects at 100 nodes (see `defaultPullRequestSearchPageSize`), so every read that selects it pages at the
|
|
3589
|
+
// reduced size, and only the lite shape asks for the maximum. A caller that wants a whole list pages for it
|
|
3590
|
+
// — see `searchMyPullRequests`.
|
|
3591
|
+
const configuredLimit = this.config.getLaunchpadQueryLimit?.() ?? maxPullRequestSearchPageSize;
|
|
3592
|
+
const limit = Math.min(
|
|
3593
|
+
options?.summary === true ? maxPullRequestSearchPageSize : defaultPullRequestSearchPageSize,
|
|
3594
|
+
configuredLimit,
|
|
3595
|
+
);
|
|
3562
3596
|
|
|
3563
3597
|
try {
|
|
3564
3598
|
interface SearchResult {
|
|
@@ -3666,6 +3700,16 @@ export class GitHubApi {
|
|
|
3666
3700
|
}
|
|
3667
3701
|
}
|
|
3668
3702
|
|
|
3703
|
+
/**
|
|
3704
|
+
* The whole list rather than a page: this read has no cursor to hand back (its `IntegrationResult<T[]>` return
|
|
3705
|
+
* has no paging channel at all), so it drains up to {@link maxMyPullRequestSearchPages} pages itself.
|
|
3706
|
+
*
|
|
3707
|
+
* Draining instead of taking one page is what lets the full projection page at the reduced size everywhere
|
|
3708
|
+
* (see the `limit` in {@link searchMyPullRequestsPage}): a single page would otherwise have to ask for 100
|
|
3709
|
+
* nodes of the selection GitHub rejects at that size, purely because this caller cannot resume. Ordering is
|
|
3710
|
+
* `sort:updated`, so the pages compose into a defined window rather than a shifting relevance ranking, and the
|
|
3711
|
+
* drain stops as soon as GitHub reports no next page.
|
|
3712
|
+
*/
|
|
3669
3713
|
@trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
3670
3714
|
async searchMyPullRequests(
|
|
3671
3715
|
provider: Provider,
|
|
@@ -3681,7 +3725,33 @@ export class GitHubApi {
|
|
|
3681
3725
|
},
|
|
3682
3726
|
cancellation?: AbortSignal,
|
|
3683
3727
|
): Promise<PullRequest[]> {
|
|
3684
|
-
|
|
3728
|
+
const values: PullRequest[] = [];
|
|
3729
|
+
// The searches behind one page can overlap (a PR can be authored by and review-requested from the same
|
|
3730
|
+
// user), and a later page can repeat a row an earlier one already served if the PR was updated mid-drain.
|
|
3731
|
+
const seen = new Set<string>();
|
|
3732
|
+
let cursor: string | undefined;
|
|
3733
|
+
|
|
3734
|
+
for (let page = 0; page < maxMyPullRequestSearchPages; page++) {
|
|
3735
|
+
const result = await this.searchMyPullRequestsPage(
|
|
3736
|
+
provider,
|
|
3737
|
+
token,
|
|
3738
|
+
{ ...options, cursor: cursor },
|
|
3739
|
+
cancellation,
|
|
3740
|
+
);
|
|
3741
|
+
for (const pr of result.values) {
|
|
3742
|
+
if (seen.has(pr.url)) continue;
|
|
3743
|
+
|
|
3744
|
+
seen.add(pr.url);
|
|
3745
|
+
values.push(pr);
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
// A missing or repeated cursor with `hasMore` would loop on the same page forever.
|
|
3749
|
+
if (!result.hasMore || result.cursor == null || result.cursor === cursor) break;
|
|
3750
|
+
|
|
3751
|
+
cursor = result.cursor;
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
return values;
|
|
3685
3755
|
}
|
|
3686
3756
|
|
|
3687
3757
|
/**
|
|
@@ -3948,6 +4018,85 @@ export class GitHubApi {
|
|
|
3948
4018
|
}
|
|
3949
4019
|
}
|
|
3950
4020
|
|
|
4021
|
+
/**
|
|
4022
|
+
* The PR twin of {@link countIssues}: counts pull requests for several scopes in ONE request via aliased
|
|
4023
|
+
* `search` fields selecting only `issueCount` with `first: 0`. Same positional/undefined contract as the issue
|
|
4024
|
+
* count.
|
|
4025
|
+
*
|
|
4026
|
+
* The one difference is states. {@link toGitHubPullRequestSearchFacets} fans a scope's states out into one
|
|
4027
|
+
* `search` each, so this reports the count the SAME way {@link searchPullRequestsPage} reports its total — the
|
|
4028
|
+
* LARGEST facet's `issueCount`, not their sum: the result ceiling applies per search, so the max is what
|
|
4029
|
+
* `exceedsProviderLimit` compares against, and summing would claim a total the read never surfaces. Relationships
|
|
4030
|
+
* are OR-ed and can't be a single count; the facade refuses a multi-relationship scope, so at most one is here.
|
|
4031
|
+
*/
|
|
4032
|
+
@trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
4033
|
+
async countPullRequests(
|
|
4034
|
+
provider: Provider,
|
|
4035
|
+
token: GitHubTokenInfo,
|
|
4036
|
+
scopes: readonly { repos?: string[]; org?: string; criteria?: PullRequestSearchCriteria }[],
|
|
4037
|
+
options?: { baseUrl?: string },
|
|
4038
|
+
cancellation?: AbortSignal,
|
|
4039
|
+
): Promise<(number | undefined)[]> {
|
|
4040
|
+
const scope = getScopedLogger();
|
|
4041
|
+
if (scopes.length === 0) return [];
|
|
4042
|
+
|
|
4043
|
+
// Each scope expands to one query string per state facet (all sharing its single relationship, which the
|
|
4044
|
+
// facade guarantees). A scope's count is the MAX across its facets — mirroring searchPullRequestsPage's
|
|
4045
|
+
// totalCount — so the aliases stay grouped by scope and are reduced after the response.
|
|
4046
|
+
const scopeQueries = scopes.map(s => {
|
|
4047
|
+
const scopeQualifiers = toGitHubIssueSearchScopeQualifiers(s.org, s.repos);
|
|
4048
|
+
return toGitHubPullRequestSearchFacets(s.criteria).map(f =>
|
|
4049
|
+
[...scopeQualifiers, ...f.qualifiers].join(' '),
|
|
4050
|
+
);
|
|
4051
|
+
});
|
|
4052
|
+
|
|
4053
|
+
// Aliases are positional and generated (`s${scope}f${facet}`): a caller's key is arbitrary text and would
|
|
4054
|
+
// break the document, so results are reduced back to one count per scope by index.
|
|
4055
|
+
const aliased = scopeQueries.flatMap((queries, si) =>
|
|
4056
|
+
queries.map((query, fi) => ({ alias: `s${si}f${fi}`, query: query })),
|
|
4057
|
+
);
|
|
4058
|
+
const params = aliased.map(a => `$${a.alias}: String!`).join('\n\t\t\t\t');
|
|
4059
|
+
// `first: 0` is what makes this cheap — `issueCount` alone, no nodes over the wire.
|
|
4060
|
+
const fields = aliased
|
|
4061
|
+
.map(a => `${a.alias}: search(query: $${a.alias}, type: ISSUE, first: 0) { issueCount }`)
|
|
4062
|
+
.join('\n\t\t\t\t');
|
|
4063
|
+
const query = `query countPullRequests(
|
|
4064
|
+
${params}
|
|
4065
|
+
) {
|
|
4066
|
+
${fields}
|
|
4067
|
+
}`;
|
|
4068
|
+
|
|
4069
|
+
const variables: Record<string, unknown> = { baseUrl: options?.baseUrl };
|
|
4070
|
+
for (const a of aliased) {
|
|
4071
|
+
variables[a.alias] = a.query;
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
try {
|
|
4075
|
+
const rsp = await this.graphql<Record<string, { issueCount?: number } | undefined>>(
|
|
4076
|
+
provider,
|
|
4077
|
+
token,
|
|
4078
|
+
query,
|
|
4079
|
+
variables,
|
|
4080
|
+
scope,
|
|
4081
|
+
cancellation,
|
|
4082
|
+
);
|
|
4083
|
+
if (rsp == null) return scopes.map(() => undefined);
|
|
4084
|
+
|
|
4085
|
+
return scopeQueries.map((queries, si) => {
|
|
4086
|
+
let max: number | undefined;
|
|
4087
|
+
for (let fi = 0; fi < queries.length; fi++) {
|
|
4088
|
+
const count = rsp[`s${si}f${fi}`]?.issueCount;
|
|
4089
|
+
if (count != null) {
|
|
4090
|
+
max = max == null ? count : Math.max(max, count);
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
return max;
|
|
4094
|
+
});
|
|
4095
|
+
} catch (ex) {
|
|
4096
|
+
throw this.handleException(ex, provider, scope);
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
|
|
3951
4100
|
/**
|
|
3952
4101
|
* The aliased-search engine behind every GitHub issue search: one GraphQL request carrying N independently
|
|
3953
4102
|
* cursored `search` fields, `@include`-gated so an exhausted or unrequested one costs nothing.
|
|
@@ -4234,8 +4383,11 @@ export class GitHubApi {
|
|
|
4234
4383
|
* every active relationship × state facet, so one HTTP request serves one page even when the logical search is
|
|
4235
4384
|
* a union. The cursor preserves each facet's continuation plus the positional page.
|
|
4236
4385
|
*
|
|
4237
|
-
* Ordering is
|
|
4238
|
-
*
|
|
4386
|
+
* Ordering is `criteria.sort`, defaulting to most-recently-updated-first — the order this read served before it
|
|
4387
|
+
* was expressible. It is requested of the provider AND re-applied to the merged page, because the page is a
|
|
4388
|
+
* union of facets and no per-facet server order describes it. The sort is part of the cursor fingerprint, so
|
|
4389
|
+
* changing it invalidates a threaded cursor exactly as changing the text or the scope does. User text is
|
|
4390
|
+
* sanitized before it reaches the provider query. See {@link toGitHubPullRequestSearchFacets}.
|
|
4239
4391
|
*/
|
|
4240
4392
|
@trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
4241
4393
|
async searchPullRequestsPage(
|
|
@@ -4395,7 +4547,15 @@ export class GitHubApi {
|
|
|
4395
4547
|
}
|
|
4396
4548
|
}
|
|
4397
4549
|
}
|
|
4398
|
-
|
|
4550
|
+
// The PR path re-sorts the merged page rather than trusting the per-facet server order — GitHub's
|
|
4551
|
+
// server-side PR sort has been unreliable (the per-branch path re-sorts for the same reason), and the
|
|
4552
|
+
// union of several facets is unordered regardless. The key was validated against
|
|
4553
|
+
// `githubPullRequestSearchCapabilities.sorts` upstream, so its comparator is always defined for a PR
|
|
4554
|
+
// shape; the guard only guards the unreachable case rather than inventing an order for it.
|
|
4555
|
+
const comparator = getPullRequestComparator(options?.criteria?.sort ?? defaultPullRequestSort);
|
|
4556
|
+
if (comparator != null) {
|
|
4557
|
+
pullRequests.sort(comparator);
|
|
4558
|
+
}
|
|
4399
4559
|
const values = [
|
|
4400
4560
|
...uniqueBy(
|
|
4401
4561
|
pullRequests,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
PullRequestSearchCriteria,
|
|
3
|
+
PullRequestSorting,
|
|
4
|
+
PullRequestStateFilter,
|
|
5
|
+
} from '../../../git/models/pullRequest.js';
|
|
6
|
+
import { defaultPullRequestSort, PullRequestFilter } from '../../../git/models/pullRequest.js';
|
|
7
|
+
import { sanitizeGitHubQualifierValue, sanitizeGitHubSearchText } from './issueSearchQuery.js';
|
|
4
8
|
|
|
5
9
|
export type GitHubPullRequestSearchFacet = {
|
|
6
10
|
/** Stable GraphQL alias and composite-cursor key. */
|
|
@@ -8,10 +12,20 @@ export type GitHubPullRequestSearchFacet = {
|
|
|
8
12
|
qualifiers: string[];
|
|
9
13
|
};
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The GitHub search qualifier each relationship translates to. Exhaustive over `PullRequestFilter` so a new
|
|
17
|
+
* member can't be silently unmapped.
|
|
18
|
+
*
|
|
19
|
+
* Exported because the account-wide read builds its own facets from the same vocabulary (see
|
|
20
|
+
* `GitHubIntegrationBase.getProviderMyPullRequestsForUser`). Kept as ONE declaration rather than a copy per
|
|
21
|
+
* caller: the exhaustive `Record` makes a missing key a compile error, but nothing would catch two copies
|
|
22
|
+
* disagreeing on a qualifier STRING, and a wrong qualifier is a read that quietly answers a different question.
|
|
23
|
+
*/
|
|
24
|
+
export const gitHubPullRequestRelationshipQualifiers: Record<PullRequestFilter, string> = {
|
|
12
25
|
[PullRequestFilter.Author]: 'author:@me',
|
|
13
26
|
[PullRequestFilter.Assignee]: 'assignee:@me',
|
|
14
27
|
[PullRequestFilter.ReviewRequested]: 'review-requested:@me',
|
|
28
|
+
[PullRequestFilter.Reviewed]: 'reviewed-by:@me',
|
|
15
29
|
[PullRequestFilter.Mention]: 'mentions:@me',
|
|
16
30
|
};
|
|
17
31
|
|
|
@@ -19,6 +33,7 @@ const relationshipAlias: Record<PullRequestFilter, string> = {
|
|
|
19
33
|
[PullRequestFilter.Author]: 'author',
|
|
20
34
|
[PullRequestFilter.Assignee]: 'assignee',
|
|
21
35
|
[PullRequestFilter.ReviewRequested]: 'reviewRequested',
|
|
36
|
+
[PullRequestFilter.Reviewed]: 'reviewed',
|
|
22
37
|
[PullRequestFilter.Mention]: 'mention',
|
|
23
38
|
};
|
|
24
39
|
|
|
@@ -29,6 +44,30 @@ const stateAlias: Record<PullRequestStateFilter, string> = {
|
|
|
29
44
|
all: 'All',
|
|
30
45
|
};
|
|
31
46
|
|
|
47
|
+
/**
|
|
48
|
+
* How each orderable key becomes a GitHub `sort:` qualifier.
|
|
49
|
+
*
|
|
50
|
+
* A literal `Record` over the union rather than a derived transform, for the same reason the relationship tables
|
|
51
|
+
* are: adding a sort field without deciding its qualifier fails the build instead of quietly emitting a search
|
|
52
|
+
* with no ordering constraint — the one failure mode the ordering contract exists to prevent, and the one a
|
|
53
|
+
* caller cannot detect from the result.
|
|
54
|
+
*
|
|
55
|
+
* `updated:desc` maps to the bare `sort:updated`, not `sort:updated-desc`. The two are the same query to GitHub,
|
|
56
|
+
* but the bare form is what this read has always emitted, so keeping it makes the default byte-identical to
|
|
57
|
+
* today's query — matching `toGitHubIssueSortQualifier`. Do not "normalize" it.
|
|
58
|
+
*/
|
|
59
|
+
export const gitHubPullRequestSortQualifiers: Partial<Record<PullRequestSorting, string>> = {
|
|
60
|
+
'created:asc': 'sort:created-asc',
|
|
61
|
+
'created:desc': 'sort:created-desc',
|
|
62
|
+
'updated:asc': 'sort:updated-asc',
|
|
63
|
+
'updated:desc': 'sort:updated',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** The `sort:` qualifier for a key, or `undefined` when there is no key or GitHub can't express it. */
|
|
67
|
+
export function toGitHubPullRequestSortQualifier(sort: PullRequestSorting | undefined): string | undefined {
|
|
68
|
+
return sort != null ? gitHubPullRequestSortQualifiers[sort] : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
32
71
|
function stateQualifiers(state: PullRequestStateFilter): string[] {
|
|
33
72
|
switch (state) {
|
|
34
73
|
case 'closed':
|
|
@@ -62,18 +101,53 @@ export function toGitHubPullRequestSearchFacets(
|
|
|
62
101
|
: ['open'];
|
|
63
102
|
const states: PullRequestStateFilter[] = requestedStates.includes('all') ? ['all'] : requestedStates;
|
|
64
103
|
const text = criteria?.text != null ? sanitizeGitHubSearchText(criteria.text) : '';
|
|
104
|
+
// Ordering is part of the contract, not an option: without an explicit `sort:` GitHub answers in relevance
|
|
105
|
+
// order, so which rows land inside the result ceiling would shift with its ranking even when nothing changed
|
|
106
|
+
// upstream. A key GitHub can't express is REFUSED here, not approximated with a fallback order — a silent
|
|
107
|
+
// `sort:updated` substitute would emit one order in the query while the merged-page comparator applied another
|
|
108
|
+
// (or none for an unknown field), which is exactly the "the N most recent are what you get" promise the ceiling
|
|
109
|
+
// contract cannot keep under a guessed order. Unreachable through the facade — every key is validated against
|
|
110
|
+
// `githubPullRequestSearchCapabilities.sorts` first — so this guards the direct callers of this exported builder
|
|
111
|
+
// and of `GitHubApi.searchPullRequestsPage`.
|
|
112
|
+
//
|
|
113
|
+
// Deliberately STRICTER than the issue path, which drops an inexpressible qualifier and searches on
|
|
114
|
+
// (`toGitHubIssueSearchQualifiers` pushes `toGitHubIssueSortQualifier`'s result only when it is non-null). The
|
|
115
|
+
// two reads differ in what that costs: an issue search is served by whichever facets the provider ordered, while
|
|
116
|
+
// this one hands its merged page to `getPullRequestComparator`, so a dropped qualifier here means the query and
|
|
117
|
+
// the comparator disagree about the order the ceiling was applied under. Refusing is the only answer that
|
|
118
|
+
// cannot lie about which rows were reachable.
|
|
119
|
+
const sort = criteria?.sort ?? defaultPullRequestSort;
|
|
120
|
+
const sortQualifier = toGitHubPullRequestSortQualifier(sort);
|
|
121
|
+
if (sortQualifier == null) {
|
|
122
|
+
throw new Error(`GitHub cannot order a pull request search by '${sort}'`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Facet-independent, unlike the relationship/state qualifiers below — build once and share.
|
|
126
|
+
const dateQualifiers: string[] = [];
|
|
127
|
+
const pushDate = (value: string | undefined, toQualifier: (sanitized: string) => string): void => {
|
|
128
|
+
if (value == null) return;
|
|
129
|
+
|
|
130
|
+
const sanitized = sanitizeGitHubQualifierValue(value);
|
|
131
|
+
if (sanitized.length > 0) {
|
|
132
|
+
dateQualifiers.push(toQualifier(sanitized));
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
pushDate(criteria?.updatedAfter, v => `updated:>=${v}`);
|
|
136
|
+
pushDate(criteria?.createdAfter, v => `created:>=${v}`);
|
|
65
137
|
|
|
66
138
|
return relationships.flatMap(relationship =>
|
|
67
139
|
states.map(state => ({
|
|
68
140
|
alias: `${relationship != null ? relationshipAlias[relationship] : 'scope'}${stateAlias[state]}`,
|
|
69
141
|
qualifiers: [
|
|
70
142
|
'is:pr',
|
|
71
|
-
...(relationship != null ? [
|
|
143
|
+
...(relationship != null ? [gitHubPullRequestRelationshipQualifiers[relationship]] : []),
|
|
72
144
|
...stateQualifiers(state),
|
|
145
|
+
// `!= null`, not truthy: `draft:false` narrows to ready-for-review PRs just as `draft:true` narrows to drafts.
|
|
146
|
+
...(criteria?.draft != null ? [`draft:${criteria.draft}`] : []),
|
|
73
147
|
...(criteria?.includeArchived === true ? [] : ['archived:false']),
|
|
74
148
|
...(text.length > 0 ? [text] : []),
|
|
75
|
-
|
|
76
|
-
|
|
149
|
+
...dateQualifiers,
|
|
150
|
+
sortQualifier,
|
|
77
151
|
],
|
|
78
152
|
})),
|
|
79
153
|
);
|
|
@@ -194,6 +194,13 @@ export type GitHubPullRequestMergeableState = 'MERGEABLE' | 'CONFLICTING' | 'UNK
|
|
|
194
194
|
export type GitHubPullRequestStatusCheckRollupState = 'SUCCESS' | 'FAILURE' | 'PENDING' | 'EXPECTED' | 'ERROR';
|
|
195
195
|
export type GitHubPullRequestReviewState = 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'DISMISSED' | 'PENDING';
|
|
196
196
|
|
|
197
|
+
type GitHubPullRequestReview = {
|
|
198
|
+
id: string;
|
|
199
|
+
author: GitHubMember | null;
|
|
200
|
+
state: GitHubPullRequestReviewState;
|
|
201
|
+
commit?: { oid: string } | null;
|
|
202
|
+
};
|
|
203
|
+
|
|
197
204
|
export interface GitHubPullRequest extends GitHubPullRequestLite {
|
|
198
205
|
additions: number;
|
|
199
206
|
assignees: {
|
|
@@ -204,11 +211,14 @@ export interface GitHubPullRequest extends GitHubPullRequestLite {
|
|
|
204
211
|
mergeable: GitHubPullRequestMergeableState;
|
|
205
212
|
reviewDecision: GitHubPullRequestReviewDecision;
|
|
206
213
|
latestReviews: {
|
|
207
|
-
nodes:
|
|
208
|
-
author: GitHubMember | null;
|
|
209
|
-
state: GitHubPullRequestReviewState;
|
|
210
|
-
}[];
|
|
214
|
+
nodes: GitHubPullRequestReview[];
|
|
211
215
|
};
|
|
216
|
+
/**
|
|
217
|
+
* The current user's own latest review. `latestReviews` is capped, so on a heavily-reviewed pull request
|
|
218
|
+
* the viewer's own review can fall outside that window — which is exactly the row a "needs my review"
|
|
219
|
+
* surface is asking about.
|
|
220
|
+
*/
|
|
221
|
+
viewerLatestReview: GitHubPullRequestReview | null;
|
|
212
222
|
reviewRequests: {
|
|
213
223
|
nodes: {
|
|
214
224
|
asCodeOwner: boolean;
|
|
@@ -401,6 +411,19 @@ export function fromGitHubPullRequestStatusCheckRollupState(
|
|
|
401
411
|
}
|
|
402
412
|
|
|
403
413
|
export function fromGitHubPullRequest(pr: GitHubPullRequest, provider: Provider): PullRequest {
|
|
414
|
+
// `latestReviews` is capped, so keep the viewer's own review even when it falls outside that window, deduped
|
|
415
|
+
// by review id since the two selections overlap. Unsubmitted drafts are dropped from the union rather than
|
|
416
|
+
// from the viewer's side alone: `PENDING` is in GitHub's review-state enum on both selections, and the field
|
|
417
|
+
// this feeds is documented as reviews already SUBMITTED — a draft there tells a "needs my review" consumer
|
|
418
|
+
// the review is done, and a reviewer with a submitted review plus a draft would get two conflicting rows.
|
|
419
|
+
const viewerLatestReview = pr.viewerLatestReview;
|
|
420
|
+
const latestReviews = [
|
|
421
|
+
...pr.latestReviews.nodes,
|
|
422
|
+
...(viewerLatestReview != null && !pr.latestReviews.nodes.some(r => r.id === viewerLatestReview.id)
|
|
423
|
+
? [viewerLatestReview]
|
|
424
|
+
: []),
|
|
425
|
+
].filter(review => review.state !== 'PENDING');
|
|
426
|
+
|
|
404
427
|
return new PullRequest(
|
|
405
428
|
provider,
|
|
406
429
|
fromGitHubMemberOrGhost(pr.author),
|
|
@@ -467,9 +490,10 @@ export function fromGitHubPullRequest(pr: GitHubPullRequest, provider: Provider)
|
|
|
467
490
|
: undefined,
|
|
468
491
|
)
|
|
469
492
|
.filter(<T>(r?: T): r is T => Boolean(r)),
|
|
470
|
-
|
|
493
|
+
latestReviews.map(r => ({
|
|
471
494
|
reviewer: fromGitHubMemberOrGhost(r.author),
|
|
472
495
|
state: fromGitHubPullRequestReviewState(r.state),
|
|
496
|
+
commitOid: r.commit?.oid,
|
|
473
497
|
})),
|
|
474
498
|
pr.assignees.nodes.map(r => ({
|
|
475
499
|
id: r.login,
|
|
@@ -77,8 +77,13 @@ import { providersMetadata } from './providers/models.js';
|
|
|
77
77
|
import type { ProvidersApi } from './providers/providersApi.js';
|
|
78
78
|
import { broadenIssues } from './reads/broaden.js';
|
|
79
79
|
import type { RepositoryResolutionContext } from './reads/context.js';
|
|
80
|
-
import type {
|
|
81
|
-
|
|
80
|
+
import type {
|
|
81
|
+
IssueCountResult,
|
|
82
|
+
IssueCountScope,
|
|
83
|
+
PullRequestCountResult,
|
|
84
|
+
PullRequestCountScope,
|
|
85
|
+
} from './reads/counts.js';
|
|
86
|
+
import { countIssues, countPullRequests } from './reads/counts.js';
|
|
82
87
|
import type { SupportedFilters } from './reads/filters.js';
|
|
83
88
|
import { getSupportedFilters } from './reads/filters.js';
|
|
84
89
|
import { listOrgs, listProjects, listRepos } from './reads/hierarchy.js';
|
|
@@ -1147,6 +1152,24 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
|
|
|
1147
1152
|
return countIssues(this, options);
|
|
1148
1153
|
}
|
|
1149
1154
|
|
|
1155
|
+
/**
|
|
1156
|
+
* How many pull requests match each scope, without fetching any — the PR twin of {@link countIssues}. See
|
|
1157
|
+
* {@link IntegrationManager.countPullRequests} for the cost model, the per-scope isolation rules, and why
|
|
1158
|
+
* `count: undefined` must not be rendered as zero.
|
|
1159
|
+
*/
|
|
1160
|
+
async countPullRequests(options: {
|
|
1161
|
+
providerId: IntegrationIds;
|
|
1162
|
+
scopes: readonly PullRequestCountScope[];
|
|
1163
|
+
connectionId?: string;
|
|
1164
|
+
/**
|
|
1165
|
+
* Explicit self-managed host domain. Used only when the requested connection has no configured domain;
|
|
1166
|
+
* it must come from the trusted authentication configuration, not repository or remote data.
|
|
1167
|
+
*/
|
|
1168
|
+
domain?: string;
|
|
1169
|
+
}): Promise<ProviderResult<PullRequestCountResult>> {
|
|
1170
|
+
return countPullRequests(this, options);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1150
1173
|
/**
|
|
1151
1174
|
* Reads the user's issues from an issue-tracker provider (Jira/Linear/Trello), whose issues live under
|
|
1152
1175
|
* resource → project (not repos), so they can't go through {@link listIssuesPage} (git-host, repo-scoped).
|
|
@@ -9,7 +9,12 @@ import type { ConfiguredIntegrationsChangeEvent } from './authentication/configu
|
|
|
9
9
|
import type { ConfiguredIntegrationDescriptor } from './authentication/models.js';
|
|
10
10
|
import type { IntegrationIds } from './constants.js';
|
|
11
11
|
import type { IssueFilter, IssueSorting, PullRequestFilter } from './providerFilters.js';
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
IssueCountResult,
|
|
14
|
+
IssueCountScope,
|
|
15
|
+
PullRequestCountResult,
|
|
16
|
+
PullRequestCountScope,
|
|
17
|
+
} from './reads/counts.js';
|
|
13
18
|
import type { SupportedFilters } from './reads/filters.js';
|
|
14
19
|
import type {
|
|
15
20
|
ConnectionStateChangeEvent,
|
|
@@ -156,6 +161,22 @@ type PullRequestSweepCommonOptions = {
|
|
|
156
161
|
* This extends the provider-native result; it is not a narrowing filter.
|
|
157
162
|
*/
|
|
158
163
|
includeReviewRequested?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Account-wide only: fetch the full PR projection (review decision, review requests, latest reviews)
|
|
166
|
+
* instead of the default summary/lite one. Opt-in because it enlarges every account-wide page; callers
|
|
167
|
+
* that classify by review state (e.g. a "needs my review" surface) set it, others keep the lean read.
|
|
168
|
+
*
|
|
169
|
+
* GitHub/GHE only today — no other provider's account-wide read has a projection switch, so they return
|
|
170
|
+
* their native shape regardless. A cross-provider sweep therefore gets reviews from GitHub targets and
|
|
171
|
+
* whatever the others natively carry; it is a breadth request, not a guarantee, so it does not refuse
|
|
172
|
+
* (unlike `filters`, where dropping a member would change the requested set).
|
|
173
|
+
*
|
|
174
|
+
* Projection only: it cannot change which pull requests come back. Every GitHub account-wide read goes through
|
|
175
|
+
* the same search whether or not it is set, so the Launchpad ignored-repository and organization qualifiers
|
|
176
|
+
* (and the per-facet result ceiling) apply the same either way. What it costs is the reduced page size, and a
|
|
177
|
+
* document whose weight scales with the number of relationship × state facets in flight.
|
|
178
|
+
*/
|
|
179
|
+
includeReviews?: boolean;
|
|
159
180
|
forceSync?: boolean;
|
|
160
181
|
maxPages?: number;
|
|
161
182
|
/**
|
|
@@ -355,10 +376,17 @@ export interface IntegrationManager {
|
|
|
355
376
|
* GitHub's mismatched `involves:@me`. Omit relationships only with a repository/organization scope to search
|
|
356
377
|
* every PR there.
|
|
357
378
|
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
379
|
+
* **Ordering is `criteria.sort`**, most-recently-updated-first when omitted. It is validated like every other
|
|
380
|
+
* criterion — a key not in `getSupportedFilters().pullRequestSearch.sorts` refuses the whole read rather than
|
|
381
|
+
* falling back, because at a bounded result window another order reaches another subset. The page is a union of
|
|
382
|
+
* the provider's relationship × state facets, so it is re-ordered as a whole rather than served as concatenated
|
|
383
|
+
* per-facet runs. Changing the sort invalidates a threaded cursor (it is part of the cursor's fingerprint,
|
|
384
|
+
* alongside the text and the scope); drop the cursor when you change the order.
|
|
385
|
+
*
|
|
386
|
+
* If the provider's result ceiling is reached, the request still succeeds and carries a warning omission with
|
|
387
|
+
* `totalCount`, `limit`, `sort`, and `recovery: 'none'`. `totalCount` is the largest provider-reported
|
|
388
|
+
* pre-ceiling facet count, not the reachable or returned row count; this mirrors the per-search ceiling's own
|
|
389
|
+
* unit.
|
|
362
390
|
*
|
|
363
391
|
* Check `getSupportedFilters().pullRequestSearch` before calling. A provider that reports no relationships
|
|
364
392
|
* refuses the read rather than returning a page that never honored the criteria or scope.
|
|
@@ -518,6 +546,23 @@ export interface IntegrationManager {
|
|
|
518
546
|
/** Self-managed host domain fallback; see {@link ProviderSweepTarget.domain}. */
|
|
519
547
|
domain?: string;
|
|
520
548
|
}): Promise<ProviderResult<IssueCountResult>>;
|
|
549
|
+
/**
|
|
550
|
+
* How many pull requests match each scope, fetching none of them — the PR twin of {@link countIssues}, behind a
|
|
551
|
+
* "this will fetch ~N pull requests" preview and a live count next to an unapplied filter. Same cost model,
|
|
552
|
+
* per-scope isolation, `key`-echo, and `count: undefined` ≠ zero rule as the issue count; GitHub/GHE only.
|
|
553
|
+
*
|
|
554
|
+
* The one PR-specific difference: a scope's `states` are counted as independent searches, so the reported count
|
|
555
|
+
* is the LARGEST of them (the same total {@link searchPullRequestsPage} surfaces), not their sum. Several states
|
|
556
|
+
* in one scope are therefore fine; only several relationships are refused — one relationship per scope, see
|
|
557
|
+
* {@link PullRequestCountScope}.
|
|
558
|
+
*/
|
|
559
|
+
countPullRequests(options: {
|
|
560
|
+
providerId: IntegrationIds;
|
|
561
|
+
scopes: readonly PullRequestCountScope[];
|
|
562
|
+
connectionId?: string;
|
|
563
|
+
/** Self-managed host domain fallback; see {@link ProviderSweepTarget.domain}. */
|
|
564
|
+
domain?: string;
|
|
565
|
+
}): Promise<ProviderResult<PullRequestCountResult>>;
|
|
521
566
|
/** Issue trackers are cloud-only, so this read takes no `domain`. */
|
|
522
567
|
listIssueTrackerIssuesPage(options: {
|
|
523
568
|
providerId: IntegrationIds;
|