@gitkraken/core-gitlens 0.5.105 → 0.5.106
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 +8 -1
- package/dist/git/models/pullRequest.d.ts +47 -0
- package/dist/git/models/pullRequest.d.ts.map +1 -1
- package/dist/git/models/pullRequest.js +8 -0
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +33 -1
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +176 -0
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts +16 -0
- package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts.map +1 -0
- package/dist/plus/git-github/api/pullRequestSearchQuery.js +64 -0
- package/dist/plus/git-github/api/pullRequestSearchQuery.js.map +1 -0
- package/dist/plus/integrations/index.d.ts +1 -0
- package/dist/plus/integrations/index.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +16 -2
- package/dist/plus/integrations/integrationService.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.js +5 -0
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +46 -4
- package/dist/plus/integrations/manager.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +22 -2
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +21 -0
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +2 -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/pullRequestReads.d.ts +18 -0
- package/dist/plus/integrations/models/pullRequestReads.d.ts.map +1 -0
- package/dist/plus/integrations/models/pullRequestReads.js +2 -0
- package/dist/plus/integrations/models/pullRequestReads.js.map +1 -0
- package/dist/plus/integrations/providerFilters.d.ts +7 -6
- package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
- package/dist/plus/integrations/providerFilters.js +1 -7
- package/dist/plus/integrations/providerFilters.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +14 -2
- package/dist/plus/integrations/providers/github.d.ts.map +1 -1
- package/dist/plus/integrations/providers/github.js +15 -0
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +12 -2
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.js +18 -0
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/reads/context.d.ts +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +34 -1
- package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
- package/dist/plus/integrations/reads/filters.js +73 -0
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.d.ts +26 -0
- package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -0
- package/dist/plus/integrations/reads/searchPullRequests.js +108 -0
- package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -0
- package/dist/plus/integrations/reads/warnings.d.ts +6 -2
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +40 -0
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/docs/integrations.md +53 -4
- package/package.json +1 -1
- package/src/git/models/pullRequest.ts +50 -0
- package/src/plus/git-github/api/github.ts +254 -1
- package/src/plus/git-github/api/pullRequestSearchQuery.ts +80 -0
- package/src/plus/integrations/index.ts +2 -0
- package/src/plus/integrations/integrationService.ts +25 -1
- package/src/plus/integrations/manager.ts +55 -4
- package/src/plus/integrations/models/gitHostIntegration.ts +56 -2
- package/src/plus/integrations/models/integration.ts +2 -0
- package/src/plus/integrations/models/pullRequestReads.ts +18 -0
- package/src/plus/integrations/providerFilters.ts +8 -6
- package/src/plus/integrations/providers/github.ts +37 -1
- package/src/plus/integrations/providers/models.ts +37 -2
- package/src/plus/integrations/reads/context.ts +1 -1
- package/src/plus/integrations/reads/filters.ts +105 -0
- package/src/plus/integrations/reads/searchPullRequests.ts +210 -0
- package/src/plus/integrations/reads/warnings.ts +73 -3
|
@@ -5,6 +5,7 @@ import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest.
|
|
|
5
5
|
import type {
|
|
6
6
|
PullRequest,
|
|
7
7
|
PullRequestMergeMethod,
|
|
8
|
+
PullRequestSearchCriteria,
|
|
8
9
|
PullRequestState,
|
|
9
10
|
PullRequestStateFilter,
|
|
10
11
|
} from '../../../git/models/pullRequest.js';
|
|
@@ -25,7 +26,11 @@ import { IntegrationReadUnavailableError } from '../errors.js';
|
|
|
25
26
|
import type { IntegrationConnectionChangeEvent } from '../integrationService.js';
|
|
26
27
|
import type { SearchMyPullRequestsOptions } from '../models/gitHostIntegration.js';
|
|
27
28
|
import { GitHostIntegration } from '../models/gitHostIntegration.js';
|
|
28
|
-
import type {
|
|
29
|
+
import type {
|
|
30
|
+
ProviderIssueSearchPage,
|
|
31
|
+
ProviderPullRequestSearchPage,
|
|
32
|
+
SearchMyIssuesOptions,
|
|
33
|
+
} from '../models/integration.js';
|
|
29
34
|
import type { GitHubIntegrationIds } from './github/github.utils.js';
|
|
30
35
|
import { getGitHubPullRequestIdentityFromMaybeUrl } from './github/github.utils.js';
|
|
31
36
|
import type {
|
|
@@ -625,6 +630,37 @@ abstract class GitHubIntegrationBase<ID extends GitHubIntegrationIds> extends Gi
|
|
|
625
630
|
);
|
|
626
631
|
}
|
|
627
632
|
|
|
633
|
+
/**
|
|
634
|
+
* Filtered pull-request search over a repository/organization or explicit current-user relationship scope. The
|
|
635
|
+
* API client owns criteria sanitizing, facet unioning, ordering, and the composite cursor that carries GitHub's
|
|
636
|
+
* result-ceiling signal across pages.
|
|
637
|
+
*/
|
|
638
|
+
protected override async searchProviderPullRequestsPage(
|
|
639
|
+
session: ProviderAuthenticationSession,
|
|
640
|
+
options: {
|
|
641
|
+
repos?: ProviderRepoInput[];
|
|
642
|
+
org?: string;
|
|
643
|
+
criteria?: PullRequestSearchCriteria;
|
|
644
|
+
cursor?: string;
|
|
645
|
+
pageSize?: number;
|
|
646
|
+
},
|
|
647
|
+
cancellation?: AbortSignal,
|
|
648
|
+
): Promise<ProviderPullRequestSearchPage | undefined> {
|
|
649
|
+
return (await this.authenticationService.apis.github)?.searchPullRequestsPage(
|
|
650
|
+
this,
|
|
651
|
+
toTokenWithInfo(this.id, session),
|
|
652
|
+
{
|
|
653
|
+
repos: options.repos?.map(r => `${r.namespace}/${r.name}`),
|
|
654
|
+
org: options.org,
|
|
655
|
+
criteria: options.criteria,
|
|
656
|
+
baseUrl: this.apiBaseUrl,
|
|
657
|
+
cursor: options.cursor,
|
|
658
|
+
pageSize: options.pageSize,
|
|
659
|
+
},
|
|
660
|
+
cancellation,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
628
664
|
/**
|
|
629
665
|
* The filtered issue search: one GraphQL request per page, no forced relationship to the current user, and no
|
|
630
666
|
* route through the SDK's repo-scoped read (whose over-limit recovery walk can spend up to 128 requests).
|
|
@@ -75,11 +75,17 @@ import {
|
|
|
75
75
|
IssuesCloudHostIntegrationId,
|
|
76
76
|
} from '../constants.js';
|
|
77
77
|
import type { Integration, IntegrationType } from '../models/integration.js';
|
|
78
|
-
import type { IssueSearchCapabilities } from '../providerFilters.js';
|
|
78
|
+
import type { IssueSearchCapabilities, PullRequestSearchCapabilities } from '../providerFilters.js';
|
|
79
79
|
import { IssueFilter, PullRequestFilter } from '../providerFilters.js';
|
|
80
80
|
|
|
81
81
|
export { IssueFilter, PullRequestFilter } from '../providerFilters.js';
|
|
82
|
-
export type {
|
|
82
|
+
export type {
|
|
83
|
+
IssueSearchCapabilities,
|
|
84
|
+
IssueSearchCriteria,
|
|
85
|
+
IssueSearchRelationship,
|
|
86
|
+
PullRequestSearchCapabilities,
|
|
87
|
+
PullRequestSearchCriteria,
|
|
88
|
+
} from '../providerFilters.js';
|
|
83
89
|
import type { ProviderRepositoryShape } from '../results.js';
|
|
84
90
|
|
|
85
91
|
export type { ProviderOrganization, ProviderRepositoryShape } from '../results.js';
|
|
@@ -696,6 +702,16 @@ export interface ProviderMetadata {
|
|
|
696
702
|
* This is independent from `supportedPullRequestFilters`, which describes repo-scoped reads.
|
|
697
703
|
*/
|
|
698
704
|
supportedAccountWidePullRequestFilters?: PullRequestFilter[];
|
|
705
|
+
/**
|
|
706
|
+
* What the filtered pull-request search can express server-side. Absent means the provider has
|
|
707
|
+
* no such search and the facade refuses it rather than returning an unfiltered page.
|
|
708
|
+
*/
|
|
709
|
+
supportedPullRequestSearch?: PullRequestSearchCapabilities;
|
|
710
|
+
/**
|
|
711
|
+
* Maximum results the provider will serve for one pull-request search facet. Matches beyond it are unreachable
|
|
712
|
+
* and are reported as a succeeded omission carrying this limit plus the provider's pre-ceiling facet count.
|
|
713
|
+
*/
|
|
714
|
+
pullRequestSearchResultLimit?: number;
|
|
699
715
|
/** Filters the REPO-scoped issue read can express. */
|
|
700
716
|
supportedIssueFilters?: IssueFilter[];
|
|
701
717
|
/**
|
|
@@ -760,6 +776,21 @@ const githubIssueSearchCapabilities: IssueSearchCapabilities = {
|
|
|
760
776
|
states: true,
|
|
761
777
|
};
|
|
762
778
|
|
|
779
|
+
/** GitHub and GHE use the same filtered pull-request search syntax and result ceiling. */
|
|
780
|
+
const githubPullRequestSearchCapabilities: PullRequestSearchCapabilities = {
|
|
781
|
+
relationships: [
|
|
782
|
+
PullRequestFilter.Author,
|
|
783
|
+
PullRequestFilter.Assignee,
|
|
784
|
+
PullRequestFilter.ReviewRequested,
|
|
785
|
+
PullRequestFilter.Mention,
|
|
786
|
+
],
|
|
787
|
+
states: ['open', 'closed', 'merged', 'all'],
|
|
788
|
+
text: true,
|
|
789
|
+
includeArchived: true,
|
|
790
|
+
repositoryScope: true,
|
|
791
|
+
organizationScope: true,
|
|
792
|
+
};
|
|
793
|
+
|
|
763
794
|
export const providersMetadata: ProvidersMetadata = {
|
|
764
795
|
[GitCloudHostIntegrationId.GitHub]: {
|
|
765
796
|
domain: 'github.com',
|
|
@@ -782,6 +813,8 @@ export const providersMetadata: ProvidersMetadata = {
|
|
|
782
813
|
PullRequestFilter.ReviewRequested,
|
|
783
814
|
PullRequestFilter.Mention,
|
|
784
815
|
],
|
|
816
|
+
supportedPullRequestSearch: githubPullRequestSearchCapabilities,
|
|
817
|
+
pullRequestSearchResultLimit: githubSearchResultLimit,
|
|
785
818
|
// Use 'username' property on account for issue filters
|
|
786
819
|
supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
|
|
787
820
|
// The account-wide read is three independent searches (`author:@me`, `assignee:@me`, `mentions:@me`) behind
|
|
@@ -812,6 +845,8 @@ export const providersMetadata: ProvidersMetadata = {
|
|
|
812
845
|
PullRequestFilter.ReviewRequested,
|
|
813
846
|
PullRequestFilter.Mention,
|
|
814
847
|
],
|
|
848
|
+
supportedPullRequestSearch: githubPullRequestSearchCapabilities,
|
|
849
|
+
pullRequestSearchResultLimit: githubSearchResultLimit,
|
|
815
850
|
// Use 'username' property on account for issue filters
|
|
816
851
|
supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
|
|
817
852
|
// The account-wide read is three independent searches (`author:@me`, `assignee:@me`, `mentions:@me`) behind
|
|
@@ -7,7 +7,7 @@ import type { ProviderWarning } from '../results.js';
|
|
|
7
7
|
/**
|
|
8
8
|
* Everything the facade's provider reads need from the service that owns connection state — and nothing else.
|
|
9
9
|
*
|
|
10
|
-
* The reads (`listOrgs`, `listRepos`, the paged PR/issue reads, the sweeps, `broadenIssues`,
|
|
10
|
+
* The reads (`listOrgs`, `listRepos`, the paged PR/issue reads and searches, the sweeps, `broadenIssues`,
|
|
11
11
|
* `resolveRepository`) are otherwise stateless: given a resolved integration they only transform provider
|
|
12
12
|
* results into the neutral shapes consumers get. What they can't do themselves is decide WHICH integration
|
|
13
13
|
* instance and which host a request addresses, because that depends on the configured connections, the
|
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
IssueSearchCriteria,
|
|
6
6
|
IssueSearchRelationship,
|
|
7
7
|
PullRequestFilter,
|
|
8
|
+
PullRequestSearchCapabilities,
|
|
9
|
+
PullRequestSearchCriteria,
|
|
8
10
|
} from '../providerFilters.js';
|
|
9
11
|
import type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';
|
|
10
12
|
import { providersMetadata } from '../providers/models.js';
|
|
@@ -65,6 +67,88 @@ export function resolveAccountWidePullRequestFilters(
|
|
|
65
67
|
return { filters: [...new Set(filters)], unsupported: false };
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
/** Why {@link resolvePullRequestSearchCriteria} refused a criteria set. */
|
|
71
|
+
export type PullRequestSearchCriteriaRejection =
|
|
72
|
+
/** The provider has no filtered pull-request search. */
|
|
73
|
+
| { reason: 'unsupported-search' }
|
|
74
|
+
/** The provider has one, but cannot express every requested criterion server-side. */
|
|
75
|
+
| { reason: 'unsupported-criteria'; criteria: string[] };
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validates filtered pull-request search criteria all-or-nothing. Dropping an unsupported criterion would
|
|
79
|
+
* widen the provider query while leaving its page and cursor describing the wider set, so the facade refuses the
|
|
80
|
+
* request before any upstream call instead.
|
|
81
|
+
*/
|
|
82
|
+
export function resolvePullRequestSearchCriteria(
|
|
83
|
+
id: IntegrationIds,
|
|
84
|
+
criteria: PullRequestSearchCriteria | undefined,
|
|
85
|
+
): { rejection?: PullRequestSearchCriteriaRejection } {
|
|
86
|
+
const supported = providersMetadata[id]?.supportedPullRequestSearch;
|
|
87
|
+
if (supported == null || supported.relationships.length === 0) {
|
|
88
|
+
return { rejection: { reason: 'unsupported-search' } };
|
|
89
|
+
}
|
|
90
|
+
if (criteria == null) return {};
|
|
91
|
+
|
|
92
|
+
const unsupported: string[] = [];
|
|
93
|
+
for (const relationship of criteria.relationships ?? []) {
|
|
94
|
+
if (!supported.relationships.includes(relationship)) {
|
|
95
|
+
unsupported.push(`relationships:${relationship}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const state of criteria.states ?? []) {
|
|
99
|
+
if (!supported.states.includes(state)) {
|
|
100
|
+
unsupported.push(`states:${state}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {
|
|
104
|
+
unsupported.push('text');
|
|
105
|
+
}
|
|
106
|
+
if (criteria.includeArchived === true && !supported.includeArchived) {
|
|
107
|
+
unsupported.push('includeArchived');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return unsupported.length > 0 ? { rejection: { reason: 'unsupported-criteria', criteria: unsupported } } : {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Why a filtered pull-request search's repository/organization boundary was refused. */
|
|
114
|
+
export type PullRequestSearchScopeRejection =
|
|
115
|
+
| 'unscoped'
|
|
116
|
+
| 'repo-ids'
|
|
117
|
+
| 'unsupported-repository-scope'
|
|
118
|
+
| 'unsupported-organization-scope';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validates the search boundary independently from its criteria. A current-user relationship is itself a safe
|
|
122
|
+
* account-wide boundary; without one, a repository or organization scope is mandatory.
|
|
123
|
+
*/
|
|
124
|
+
export function resolvePullRequestSearchScope(
|
|
125
|
+
id: IntegrationIds,
|
|
126
|
+
repos: ProviderReposInput | undefined,
|
|
127
|
+
org: string | undefined,
|
|
128
|
+
criteria: PullRequestSearchCriteria | undefined,
|
|
129
|
+
): { rejection?: PullRequestSearchScopeRejection; repos?: ProviderRepoInput[] } {
|
|
130
|
+
const supported = providersMetadata[id]?.supportedPullRequestSearch;
|
|
131
|
+
let resolvedRepos: ProviderRepoInput[] | undefined;
|
|
132
|
+
|
|
133
|
+
if (repos?.length) {
|
|
134
|
+
if (repos.some(r => typeof r === 'string' || typeof r === 'number')) return { rejection: 'repo-ids' };
|
|
135
|
+
if (supported?.repositoryScope !== true) return { rejection: 'unsupported-repository-scope' };
|
|
136
|
+
|
|
137
|
+
resolvedRepos = repos as ProviderRepoInput[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const hasOrganizationScope = org != null && org.length > 0;
|
|
141
|
+
if (hasOrganizationScope && supported?.organizationScope !== true) {
|
|
142
|
+
return { rejection: 'unsupported-organization-scope' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (resolvedRepos != null || hasOrganizationScope || (criteria?.relationships?.length ?? 0) > 0) {
|
|
146
|
+
return { repos: resolvedRepos };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { rejection: 'unscoped' };
|
|
150
|
+
}
|
|
151
|
+
|
|
68
152
|
/**
|
|
69
153
|
* Validates a caller-provided issue filter set against what the provider's ACCOUNT-WIDE issue read can express
|
|
70
154
|
* server-side (`ProviderMetadata.supportedAccountWideIssueFilters`), which is a different — usually narrower —
|
|
@@ -182,6 +266,16 @@ const unsupportedIssueSearchCapabilities: IssueSearchCapabilities = {
|
|
|
182
266
|
states: false,
|
|
183
267
|
};
|
|
184
268
|
|
|
269
|
+
/** What a provider with no filtered pull-request search reports. */
|
|
270
|
+
const unsupportedPullRequestSearchCapabilities: PullRequestSearchCapabilities = {
|
|
271
|
+
relationships: [],
|
|
272
|
+
states: [],
|
|
273
|
+
text: false,
|
|
274
|
+
includeArchived: false,
|
|
275
|
+
repositoryScope: false,
|
|
276
|
+
organizationScope: false,
|
|
277
|
+
};
|
|
278
|
+
|
|
185
279
|
/** Why a filtered issue search's scope was refused, or `undefined` when it is usable. */
|
|
186
280
|
export type IssueSearchScopeRejection =
|
|
187
281
|
/** No repositories, no org, and no user-relative relationship: a search of the whole host. */
|
|
@@ -248,6 +342,9 @@ export function resolveIssueSearchScope(
|
|
|
248
342
|
* rather than a list, because its criteria aren't a single kind. An empty `relationships` means the provider has
|
|
249
343
|
* no filtered issue search — hide the surface, not just its chips.
|
|
250
344
|
*
|
|
345
|
+
* `pullRequestSearch` describes the separate filtered PR search. Its table declares each criteria vocabulary
|
|
346
|
+
* plus repository/organization scope support; an empty relationship list means the search itself is absent.
|
|
347
|
+
*
|
|
251
348
|
* Note this is a CAPABILITY table — "what the provider can express" — not a recommendation. A consumer
|
|
252
349
|
* matching another tool's behavior may deliberately pass fewer filters than are listed here (or none, where an
|
|
253
350
|
* already-scoped read would only be narrowed by them). Intersecting against this table is what keeps a
|
|
@@ -256,15 +353,23 @@ export function resolveIssueSearchScope(
|
|
|
256
353
|
export function getSupportedFilters(providerId: IntegrationIds): {
|
|
257
354
|
pullRequests: PullRequestFilter[];
|
|
258
355
|
pullRequestsAccountWide: PullRequestFilter[];
|
|
356
|
+
pullRequestSearch: PullRequestSearchCapabilities;
|
|
259
357
|
issues: IssueFilter[];
|
|
260
358
|
issuesAccountWide: IssueFilter[];
|
|
261
359
|
issueSearch: IssueSearchCapabilities;
|
|
262
360
|
} {
|
|
263
361
|
const metadata = providersMetadata[providerId];
|
|
264
362
|
const issueSearch = metadata?.supportedIssueSearch;
|
|
363
|
+
const pullRequestSearch = metadata?.supportedPullRequestSearch;
|
|
265
364
|
return {
|
|
266
365
|
pullRequests: [...(metadata?.supportedPullRequestFilters ?? [])],
|
|
267
366
|
pullRequestsAccountWide: [...(metadata?.supportedAccountWidePullRequestFilters ?? [])],
|
|
367
|
+
pullRequestSearch: {
|
|
368
|
+
...unsupportedPullRequestSearchCapabilities,
|
|
369
|
+
...pullRequestSearch,
|
|
370
|
+
relationships: [...(pullRequestSearch?.relationships ?? [])],
|
|
371
|
+
states: [...(pullRequestSearch?.states ?? [])],
|
|
372
|
+
},
|
|
268
373
|
issues: [...(metadata?.supportedIssueFilters ?? [])],
|
|
269
374
|
issuesAccountWide: [...(metadata?.supportedAccountWideIssueFilters ?? [])],
|
|
270
375
|
// Always an object, never `undefined`: a provider WITHOUT a filtered issue search reports one whose
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { PullRequestSearchCriteria, PullRequestShape } from '../../../git/models/pullRequest.js';
|
|
2
|
+
import type { IntegrationIds } from '../constants.js';
|
|
3
|
+
import type { ProviderReposInput } from '../providers/models.js';
|
|
4
|
+
import type { ProviderPagedResult, ProviderWarning } from '../results.js';
|
|
5
|
+
import {
|
|
6
|
+
isGitHostIntegration,
|
|
7
|
+
isIssuesHostIntegrationId,
|
|
8
|
+
warnOnMissingSessionForDomain,
|
|
9
|
+
} from '../utils/integration.utils.js';
|
|
10
|
+
import type { ProviderReadContext } from './context.js';
|
|
11
|
+
import { runCaptured } from './drains.js';
|
|
12
|
+
import { resolvePullRequestSearchCriteria, resolvePullRequestSearchScope } from './filters.js';
|
|
13
|
+
import {
|
|
14
|
+
drainFlatPagesToRequestedPage,
|
|
15
|
+
refusedPage,
|
|
16
|
+
resolveContinuation,
|
|
17
|
+
resolveCurrentPage,
|
|
18
|
+
usableCursor,
|
|
19
|
+
} from './paging.js';
|
|
20
|
+
import {
|
|
21
|
+
gitHostOnlySurfaceWarning,
|
|
22
|
+
otherWarning,
|
|
23
|
+
pullRequestSearchCapResultWarning,
|
|
24
|
+
truncationWarning,
|
|
25
|
+
unsupportedPullRequestSearchCriteriaWarning,
|
|
26
|
+
} from './warnings.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Pull requests matching structured criteria over a repository/organization or current-user relationship scope.
|
|
30
|
+
*
|
|
31
|
+
* The read is deliberately separate from `listPullRequestsPage`: the list has provider-specific "my PRs"
|
|
32
|
+
* semantics and no text channel, while this method promises a sanitized query, most-recently-updated-first
|
|
33
|
+
* ordering, one upstream request per cursor-threaded page, and a quantified omission at the provider's result
|
|
34
|
+
* ceiling.
|
|
35
|
+
*/
|
|
36
|
+
export async function searchPullRequestsPage(
|
|
37
|
+
ctx: ProviderReadContext,
|
|
38
|
+
options: {
|
|
39
|
+
providerId: IntegrationIds;
|
|
40
|
+
repos?: ProviderReposInput;
|
|
41
|
+
org?: string;
|
|
42
|
+
criteria?: PullRequestSearchCriteria;
|
|
43
|
+
page?: number;
|
|
44
|
+
cursor?: string;
|
|
45
|
+
itemsPerPage?: number;
|
|
46
|
+
forceSync?: boolean;
|
|
47
|
+
connectionId?: string;
|
|
48
|
+
domain?: string;
|
|
49
|
+
},
|
|
50
|
+
): Promise<ProviderPagedResult<PullRequestShape>> {
|
|
51
|
+
const page = Math.max(1, Math.trunc(options.page ?? 1));
|
|
52
|
+
const refused = (warning: ProviderWarning) => refusedPage<PullRequestShape>(1, [warning], true);
|
|
53
|
+
|
|
54
|
+
if (isIssuesHostIntegrationId(options.providerId)) {
|
|
55
|
+
return refused(
|
|
56
|
+
gitHostOnlySurfaceWarning(
|
|
57
|
+
options.providerId,
|
|
58
|
+
undefined,
|
|
59
|
+
options.connectionId,
|
|
60
|
+
'Filtered pull request search',
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);
|
|
66
|
+
if (integration == null) {
|
|
67
|
+
const early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);
|
|
68
|
+
return refusedPage(1, early.warnings, early.fetchFailed);
|
|
69
|
+
}
|
|
70
|
+
if (!isGitHostIntegration(integration)) {
|
|
71
|
+
return refused(
|
|
72
|
+
gitHostOnlySurfaceWarning(
|
|
73
|
+
options.providerId,
|
|
74
|
+
undefined,
|
|
75
|
+
options.connectionId,
|
|
76
|
+
'Filtered pull request search',
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);
|
|
82
|
+
|
|
83
|
+
const domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);
|
|
84
|
+
const warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);
|
|
85
|
+
|
|
86
|
+
const resolved = resolvePullRequestSearchCriteria(options.providerId, options.criteria);
|
|
87
|
+
if (resolved.rejection != null) {
|
|
88
|
+
return refused(
|
|
89
|
+
unsupportedPullRequestSearchCriteriaWarning(
|
|
90
|
+
options.providerId,
|
|
91
|
+
domain,
|
|
92
|
+
options.connectionId,
|
|
93
|
+
resolved.rejection,
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const scope = resolvePullRequestSearchScope(options.providerId, options.repos, options.org, options.criteria);
|
|
99
|
+
switch (scope.rejection) {
|
|
100
|
+
case 'unscoped':
|
|
101
|
+
return refused(
|
|
102
|
+
otherWarning(
|
|
103
|
+
options.providerId,
|
|
104
|
+
domain,
|
|
105
|
+
options.connectionId,
|
|
106
|
+
'A filtered pull request search must be scoped: pass `repos`, `org`, or at least one current-user relationship.',
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
case 'repo-ids':
|
|
110
|
+
return refused(
|
|
111
|
+
otherWarning(
|
|
112
|
+
options.providerId,
|
|
113
|
+
domain,
|
|
114
|
+
options.connectionId,
|
|
115
|
+
'A filtered pull request search cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.',
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
case 'unsupported-repository-scope':
|
|
119
|
+
case 'unsupported-organization-scope':
|
|
120
|
+
return refused(
|
|
121
|
+
unsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {
|
|
122
|
+
reason: 'unsupported-criteria',
|
|
123
|
+
criteria: [
|
|
124
|
+
scope.rejection === 'unsupported-repository-scope' ? 'repositoryScope' : 'organizationScope',
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const readPage = (cursor: string | undefined) =>
|
|
131
|
+
runCaptured(
|
|
132
|
+
options.providerId,
|
|
133
|
+
domain,
|
|
134
|
+
options.connectionId,
|
|
135
|
+
() =>
|
|
136
|
+
integration.searchPullRequestsPageResult(
|
|
137
|
+
{
|
|
138
|
+
repos: scope.repos,
|
|
139
|
+
org: options.org,
|
|
140
|
+
criteria: options.criteria,
|
|
141
|
+
cursor: cursor,
|
|
142
|
+
pageSize: options.itemsPerPage,
|
|
143
|
+
},
|
|
144
|
+
undefined,
|
|
145
|
+
options.connectionId,
|
|
146
|
+
),
|
|
147
|
+
{ warnOnMissingSession: warnOnMissingSession },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const first = await readPage(options.cursor);
|
|
151
|
+
const warnings = first.warning != null ? [first.warning] : [];
|
|
152
|
+
let totalCount = first.value?.totalCount;
|
|
153
|
+
const drained = await drainFlatPagesToRequestedPage(first, {
|
|
154
|
+
requestedPage: page,
|
|
155
|
+
suppliedCursor: options.cursor,
|
|
156
|
+
warnings: warnings,
|
|
157
|
+
readPage: readPage,
|
|
158
|
+
fold: p => {
|
|
159
|
+
if (p.totalCount != null) {
|
|
160
|
+
totalCount = Math.max(totalCount ?? 0, p.totalCount);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const { value, currentPage, requestedPageMissing } = drained;
|
|
165
|
+
|
|
166
|
+
if (value == null && warnings.length === 0) {
|
|
167
|
+
return refused(
|
|
168
|
+
unsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {
|
|
169
|
+
reason: 'unsupported-search',
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const items = requestedPageMissing ? [] : (value?.values ?? []);
|
|
175
|
+
const continuation = resolveContinuation(
|
|
176
|
+
{
|
|
177
|
+
hasMore: requestedPageMissing ? false : (value?.hasMore ?? false),
|
|
178
|
+
cursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),
|
|
179
|
+
truncated: drained.truncated,
|
|
180
|
+
},
|
|
181
|
+
undefined,
|
|
182
|
+
);
|
|
183
|
+
const truncated = continuation.truncated;
|
|
184
|
+
if (truncated && warnings.length === 0) {
|
|
185
|
+
warnings.push(
|
|
186
|
+
pullRequestSearchCapResultWarning(options.providerId, domain, options.connectionId, totalCount) ??
|
|
187
|
+
truncationWarning(options.providerId, domain, options.connectionId, 'Pull request search', 'exhausted'),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
items: items,
|
|
193
|
+
warnings: warnings,
|
|
194
|
+
page: {
|
|
195
|
+
currentPage: requestedPageMissing
|
|
196
|
+
? page
|
|
197
|
+
: resolveCurrentPage({
|
|
198
|
+
providerPage: currentPage,
|
|
199
|
+
requestedPage: page,
|
|
200
|
+
suppliedCursor: options.cursor,
|
|
201
|
+
pageAdvanceable: false,
|
|
202
|
+
}),
|
|
203
|
+
itemsPerPage: items.length,
|
|
204
|
+
truncated: truncated || undefined,
|
|
205
|
+
},
|
|
206
|
+
hasMore: continuation.hasMore,
|
|
207
|
+
cursor: continuation.cursor,
|
|
208
|
+
fetchFailed: drained.fetchFailed || undefined,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { IntegrationIds } from '../constants.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
IssueFilter,
|
|
4
|
+
IssueSearchCapabilities,
|
|
5
|
+
PullRequestFilter,
|
|
6
|
+
PullRequestSearchCapabilities,
|
|
7
|
+
} from '../providerFilters.js';
|
|
3
8
|
import { providersMetadata } from '../providers/models.js';
|
|
4
9
|
import type { ProviderWarning } from '../results.js';
|
|
5
|
-
import type { IssueSearchCriteriaRejection } from './filters.js';
|
|
10
|
+
import type { IssueSearchCriteriaRejection, PullRequestSearchCriteriaRejection } from './filters.js';
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* The warnings the provider facade raises on its OWN terms, as opposed to the ones derived from a caught
|
|
@@ -154,7 +159,8 @@ type TruncatedReadKind =
|
|
|
154
159
|
| 'Organization'
|
|
155
160
|
| 'Project'
|
|
156
161
|
| 'Account-wide issue search'
|
|
157
|
-
| 'Issue search'
|
|
162
|
+
| 'Issue search'
|
|
163
|
+
| 'Pull request search';
|
|
158
164
|
|
|
159
165
|
/** {@link incompleteReadWarning} for a paged or drained read, phrased per surface. */
|
|
160
166
|
export function truncationWarning(
|
|
@@ -246,6 +252,44 @@ export function unsupportedFiltersWarning(
|
|
|
246
252
|
);
|
|
247
253
|
}
|
|
248
254
|
|
|
255
|
+
/** Warning for a filtered pull-request search the provider cannot run as requested. */
|
|
256
|
+
export function unsupportedPullRequestSearchCriteriaWarning(
|
|
257
|
+
id: IntegrationIds,
|
|
258
|
+
domain: string | undefined,
|
|
259
|
+
connectionId: string | undefined,
|
|
260
|
+
rejection: PullRequestSearchCriteriaRejection,
|
|
261
|
+
): ProviderWarning {
|
|
262
|
+
let message: string;
|
|
263
|
+
switch (rejection.reason) {
|
|
264
|
+
case 'unsupported-search':
|
|
265
|
+
message = `Filtered pull request search is not supported by '${id}'; use \`listPullRequestsPage\` for its own pull request reads.`;
|
|
266
|
+
break;
|
|
267
|
+
case 'unsupported-criteria': {
|
|
268
|
+
const supported = providersMetadata[id]?.supportedPullRequestSearch;
|
|
269
|
+
message = unsupportedNarrowingMessage(
|
|
270
|
+
id,
|
|
271
|
+
'pull request search criteria',
|
|
272
|
+
rejection.criteria.join(', '),
|
|
273
|
+
supported != null ? describePullRequestSearchCapabilities(supported) : '',
|
|
274
|
+
);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return otherWarning(id, domain, connectionId, message);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function describePullRequestSearchCapabilities(capabilities: PullRequestSearchCapabilities): string {
|
|
283
|
+
return [
|
|
284
|
+
...(capabilities.relationships.length ? [`relationships:${capabilities.relationships.join('|')}`] : []),
|
|
285
|
+
...(capabilities.states.length ? [`states:${capabilities.states.join('|')}`] : []),
|
|
286
|
+
...(capabilities.text ? ['text'] : []),
|
|
287
|
+
...(capabilities.includeArchived ? ['includeArchived'] : []),
|
|
288
|
+
...(capabilities.repositoryScope ? ['repository scope'] : []),
|
|
289
|
+
...(capabilities.organizationScope ? ['organization scope'] : []),
|
|
290
|
+
].join(', ');
|
|
291
|
+
}
|
|
292
|
+
|
|
249
293
|
/**
|
|
250
294
|
* Warning for a filtered issue search whose criteria the provider can't serve — either it has no such search at
|
|
251
295
|
* all, or it can't express what was asked for, or the set contradicts itself.
|
|
@@ -343,6 +387,32 @@ export function issueSearchCapResultWarning(
|
|
|
343
387
|
};
|
|
344
388
|
}
|
|
345
389
|
|
|
390
|
+
/** Quantified omission for a pull-request search that exceeded the provider's reachable result window. */
|
|
391
|
+
export function pullRequestSearchCapResultWarning(
|
|
392
|
+
id: IntegrationIds,
|
|
393
|
+
domain: string | undefined,
|
|
394
|
+
connectionId: string | undefined,
|
|
395
|
+
totalCount: number | undefined,
|
|
396
|
+
): ProviderWarning | undefined {
|
|
397
|
+
const limit = providersMetadata[id]?.pullRequestSearchResultLimit;
|
|
398
|
+
if (limit == null || totalCount == null || totalCount <= limit) return undefined;
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
...otherWarning(
|
|
402
|
+
id,
|
|
403
|
+
domain,
|
|
404
|
+
connectionId,
|
|
405
|
+
`Pull request search matched ${totalCount} results, but '${id}' serves at most ${limit}; narrow the search to read the rest.`,
|
|
406
|
+
),
|
|
407
|
+
omission: {
|
|
408
|
+
kind: 'provider-limit',
|
|
409
|
+
recovery: 'none',
|
|
410
|
+
limit: limit,
|
|
411
|
+
totalCount: totalCount,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
346
416
|
/** Warning for a git host that doesn't expose issues on this surface (e.g. Bitbucket, deprecated in favor of Jira). */
|
|
347
417
|
export function issuesUnsupportedWarning(
|
|
348
418
|
id: IntegrationIds,
|