@gitkraken/core-gitlens 0.5.105 → 0.5.107
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 +20 -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 +42 -1
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +194 -1
- 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 +21 -1
- 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 +7 -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 +272 -2
- package/src/plus/git-github/api/pullRequestSearchQuery.ts +80 -0
- package/src/plus/integrations/index.ts +2 -0
- package/src/plus/integrations/integrationService.ts +48 -5
- package/src/plus/integrations/manager.ts +55 -4
- package/src/plus/integrations/models/gitHostIntegration.ts +56 -2
- package/src/plus/integrations/models/integration.ts +9 -1
- 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
|
@@ -13,7 +13,13 @@ import type { Account, UnidentifiedAuthor } from '../../../git/models/author.js'
|
|
|
13
13
|
import type { DefaultBranch } from '../../../git/models/defaultBranch.js';
|
|
14
14
|
import type { Issue, IssueSearchCriteria, IssueShape } from '../../../git/models/issue.js';
|
|
15
15
|
import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest.js';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
PullRequest,
|
|
18
|
+
PullRequestSearchCriteria,
|
|
19
|
+
PullRequestShape,
|
|
20
|
+
PullRequestState,
|
|
21
|
+
PullRequestStateFilter,
|
|
22
|
+
} from '../../../git/models/pullRequest.js';
|
|
17
23
|
import { PullRequestMergeMethod } from '../../../git/models/pullRequest.js';
|
|
18
24
|
import type { Provider } from '../../../git/models/remoteProvider.js';
|
|
19
25
|
import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata.js';
|
|
@@ -72,6 +78,7 @@ import {
|
|
|
72
78
|
toGitHubIssueSearchQualifiers,
|
|
73
79
|
toGitHubIssueSearchScopeQualifiers,
|
|
74
80
|
} from './issueSearchQuery.js';
|
|
81
|
+
import { toGitHubPullRequestSearchFacets } from './pullRequestSearchQuery.js';
|
|
75
82
|
import type { GitHubTokenInfo } from './token.js';
|
|
76
83
|
|
|
77
84
|
const emptyPagedResult: PagedResult<any> = Object.freeze({ values: [] });
|
|
@@ -89,6 +96,13 @@ const requestRetryMaxDelay = 2000; // ms
|
|
|
89
96
|
/** How many email->login user searches to alias into a single GraphQL request (keeps query cost within limits). */
|
|
90
97
|
const accountResolveBatchSize = 25;
|
|
91
98
|
|
|
99
|
+
// Pull-request search selects the full PR fragment (reviews, requests, refs, commits, etc.),
|
|
100
|
+
// which makes GitHub reject broad 100-node searches with `Resource limits for this query
|
|
101
|
+
// exceeded` on large repositories. Thirty keeps the default within that GraphQL cost budget
|
|
102
|
+
// while callers that know their scope is cheap can still opt into the supported 100-node max.
|
|
103
|
+
const defaultPullRequestSearchPageSize = 30;
|
|
104
|
+
const maxPullRequestSearchPageSize = 100;
|
|
105
|
+
|
|
92
106
|
function isRetryableTransientError(ex: unknown): ex is RequestError {
|
|
93
107
|
// An aborted request is rethrown as the original `AbortError` (not a `RequestError`), so it is
|
|
94
108
|
// excluded here. octokit maps a fetch/network failure to a `RequestError` with status 500 and
|
|
@@ -164,6 +178,22 @@ export interface AliasedIssueSearchResult {
|
|
|
164
178
|
totalCount?: number;
|
|
165
179
|
}
|
|
166
180
|
|
|
181
|
+
/** One page of the filtered pull-request search. */
|
|
182
|
+
export interface PullRequestSearchResult {
|
|
183
|
+
values: PullRequestShape[];
|
|
184
|
+
/** Opaque cursor carrying each active facet continuation plus the positional page. */
|
|
185
|
+
cursor?: string;
|
|
186
|
+
hasMore: boolean;
|
|
187
|
+
page: number;
|
|
188
|
+
/** True at GitHub's search ceiling or when GitHub advertises a page without a usable cursor. */
|
|
189
|
+
truncated: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* The largest pre-ceiling `issueCount` any relationship × state facet reported, which is what
|
|
192
|
+
* {@link githubSearchResultLimit} applies to. Never the number of rows reachable after that ceiling.
|
|
193
|
+
*/
|
|
194
|
+
totalCount?: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
167
197
|
// Matches a GraphQL document whose operation is definitely a read query — the `query` keyword as
|
|
168
198
|
// the first significant token, after any leading whitespace or `#` comment lines. We retry only
|
|
169
199
|
// when this matches; a mutation (even one prefixed by a comment), a subscription, or anything we
|
|
@@ -3480,6 +3510,15 @@ export class GitHubApi {
|
|
|
3480
3510
|
return `https://avatars.githubusercontent.com/u/e?email=${encodeURIComponent(email)}&s=${avatarSize}`;
|
|
3481
3511
|
}
|
|
3482
3512
|
|
|
3513
|
+
/**
|
|
3514
|
+
* One page of the current user's pull requests, filtered by state and optionally by an explicit
|
|
3515
|
+
* relationship qualifier. Backs the PR sweeps, which drain it page by page.
|
|
3516
|
+
*
|
|
3517
|
+
* Ordering is part of the contract, not an option: always `sort:updated` (most recently updated first),
|
|
3518
|
+
* matching {@link searchPullRequestsPage}. A caller that stops before `hasMore` clears — every sweep with a
|
|
3519
|
+
* page budget — therefore retains a well-defined recency window instead of an arbitrary slice of GitHub's
|
|
3520
|
+
* relevance ranking.
|
|
3521
|
+
*/
|
|
3483
3522
|
@trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
3484
3523
|
async searchMyPullRequestsPage(
|
|
3485
3524
|
provider: Provider,
|
|
@@ -3578,12 +3617,20 @@ export class GitHubApi {
|
|
|
3578
3617
|
options?.includeDefaultInvolvement === false
|
|
3579
3618
|
? 'is:pr archived:false'
|
|
3580
3619
|
: 'is:pr involves:@me archived:false';
|
|
3620
|
+
// Ordering is part of the contract, not an option — same as `searchPullRequestsPage`. Without it
|
|
3621
|
+
// GitHub answers in `best-match` (relevance) order, so any result set the caller stops short of
|
|
3622
|
+
// draining is an arbitrary sample rather than "the N most recent": which rows land inside a page
|
|
3623
|
+
// budget can then shift with GitHub's ranking even when nothing changed upstream. Consumers that
|
|
3624
|
+
// cap the walk depend on this to make their window deterministic and time-bounded.
|
|
3581
3625
|
const rsp = await this.graphql<SearchResult>(
|
|
3582
3626
|
provider,
|
|
3583
3627
|
token,
|
|
3584
3628
|
query,
|
|
3585
3629
|
{
|
|
3586
|
-
search: [stateQualifier, relationshipQualifier, search
|
|
3630
|
+
search: [stateQualifier, relationshipQualifier, search, 'sort:updated']
|
|
3631
|
+
.filter(Boolean)
|
|
3632
|
+
.join(' ')
|
|
3633
|
+
.trim(),
|
|
3587
3634
|
cursor: options?.cursor,
|
|
3588
3635
|
baseUrl: options?.baseUrl,
|
|
3589
3636
|
avatarSize: options?.avatarSize,
|
|
@@ -4022,6 +4069,229 @@ export class GitHubApi {
|
|
|
4022
4069
|
}
|
|
4023
4070
|
}
|
|
4024
4071
|
|
|
4072
|
+
/**
|
|
4073
|
+
* Searches pull requests over a repository/org or current-user relationship scope. One GraphQL document carries
|
|
4074
|
+
* every active relationship × state facet, so one HTTP request serves one page even when the logical search is
|
|
4075
|
+
* a union. The cursor preserves each facet's continuation plus the positional page.
|
|
4076
|
+
*
|
|
4077
|
+
* Ordering is always most-recently-updated-first, and user text is sanitized before it reaches the provider
|
|
4078
|
+
* query. See {@link toGitHubPullRequestSearchFacets}.
|
|
4079
|
+
*/
|
|
4080
|
+
@trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
4081
|
+
async searchPullRequestsPage(
|
|
4082
|
+
provider: Provider,
|
|
4083
|
+
token: GitHubTokenInfo,
|
|
4084
|
+
options?: {
|
|
4085
|
+
repos?: string[];
|
|
4086
|
+
org?: string;
|
|
4087
|
+
criteria?: PullRequestSearchCriteria;
|
|
4088
|
+
baseUrl?: string;
|
|
4089
|
+
avatarSize?: number;
|
|
4090
|
+
cursor?: string;
|
|
4091
|
+
pageSize?: number;
|
|
4092
|
+
},
|
|
4093
|
+
cancellation?: AbortSignal,
|
|
4094
|
+
): Promise<PullRequestSearchResult | undefined> {
|
|
4095
|
+
const scope = getScopedLogger();
|
|
4096
|
+
|
|
4097
|
+
const pageSize = Math.min(
|
|
4098
|
+
maxPullRequestSearchPageSize,
|
|
4099
|
+
Math.max(1, Math.trunc(options?.pageSize ?? defaultPullRequestSearchPageSize)),
|
|
4100
|
+
);
|
|
4101
|
+
const facets = toGitHubPullRequestSearchFacets(options?.criteria);
|
|
4102
|
+
const facetAliases = facets.map(f => f.alias).sort();
|
|
4103
|
+
const scopeQualifiers = toGitHubIssueSearchScopeQualifiers(options?.org, options?.repos);
|
|
4104
|
+
const facetSearches = new Map(
|
|
4105
|
+
facets.map(f => [f.alias, [...scopeQualifiers, ...f.qualifiers].join(' ')] as const),
|
|
4106
|
+
);
|
|
4107
|
+
// Bind a cursor to every qualifier without publishing the user text/scope inside the opaque cursor. FNV-1a
|
|
4108
|
+
// is a compact drift key, not a security primitive: a mismatch only degrades safely to page 1.
|
|
4109
|
+
let cursorKeyHash = 0x811c9dc5;
|
|
4110
|
+
for (const value of [...facetSearches.entries()].sort(([a], [b]) => a.localeCompare(b)).flat()) {
|
|
4111
|
+
for (let i = 0; i < value.length; i++) {
|
|
4112
|
+
cursorKeyHash ^= value.charCodeAt(i);
|
|
4113
|
+
cursorKeyHash = Math.imul(cursorKeyHash, 0x01000193);
|
|
4114
|
+
}
|
|
4115
|
+
cursorKeyHash ^= 0;
|
|
4116
|
+
cursorKeyHash = Math.imul(cursorKeyHash, 0x01000193);
|
|
4117
|
+
}
|
|
4118
|
+
const cursorKey = (cursorKeyHash >>> 0).toString(36);
|
|
4119
|
+
|
|
4120
|
+
type SearchCategory = {
|
|
4121
|
+
issueCount: number;
|
|
4122
|
+
pageInfo?: { endCursor?: string | null; hasNextPage: boolean };
|
|
4123
|
+
nodes: (GitHubPullRequest | null)[] | null;
|
|
4124
|
+
};
|
|
4125
|
+
interface SearchCursor {
|
|
4126
|
+
key: string;
|
|
4127
|
+
page: number;
|
|
4128
|
+
facets: Record<string, string | null>;
|
|
4129
|
+
truncated?: boolean;
|
|
4130
|
+
totalCount?: number;
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
let cursor: SearchCursor | undefined;
|
|
4134
|
+
if (options?.cursor != null) {
|
|
4135
|
+
try {
|
|
4136
|
+
const parsed = JSON.parse(options.cursor) as Partial<SearchCursor>;
|
|
4137
|
+
const parsedFacets = parsed.facets;
|
|
4138
|
+
if (parsedFacets != null && typeof parsedFacets === 'object' && !Array.isArray(parsedFacets)) {
|
|
4139
|
+
const parsedAliases = Object.keys(parsedFacets).sort();
|
|
4140
|
+
const sameFacets =
|
|
4141
|
+
parsedAliases.length === facetAliases.length &&
|
|
4142
|
+
parsedAliases.every((alias, index) => alias === facetAliases[index]);
|
|
4143
|
+
const usableSlots = Object.values(parsedFacets).every(
|
|
4144
|
+
slot => slot === null || (typeof slot === 'string' && slot.length > 0),
|
|
4145
|
+
);
|
|
4146
|
+
if (parsed.key === cursorKey && sameFacets && usableSlots) {
|
|
4147
|
+
cursor = {
|
|
4148
|
+
key: cursorKey,
|
|
4149
|
+
page:
|
|
4150
|
+
typeof parsed.page === 'number' && Number.isFinite(parsed.page)
|
|
4151
|
+
? Math.max(1, Math.trunc(parsed.page))
|
|
4152
|
+
: 1,
|
|
4153
|
+
facets: parsedFacets,
|
|
4154
|
+
truncated: parsed.truncated === true,
|
|
4155
|
+
totalCount:
|
|
4156
|
+
typeof parsed.totalCount === 'number' && Number.isFinite(parsed.totalCount)
|
|
4157
|
+
? Math.max(0, Math.trunc(parsed.totalCount))
|
|
4158
|
+
: undefined,
|
|
4159
|
+
};
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
} catch {}
|
|
4163
|
+
}
|
|
4164
|
+
const page = cursor?.page ?? 1;
|
|
4165
|
+
const isActive = (alias: string): boolean => cursor?.facets[alias] !== null;
|
|
4166
|
+
const activeFacets = facets.filter(f => isActive(f.alias));
|
|
4167
|
+
if (activeFacets.length === 0) {
|
|
4168
|
+
return {
|
|
4169
|
+
values: [],
|
|
4170
|
+
hasMore: false,
|
|
4171
|
+
page: page,
|
|
4172
|
+
truncated: cursor?.truncated === true,
|
|
4173
|
+
totalCount: cursor?.totalCount,
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
const includeVar = (alias: string): string => `include${alias.charAt(0).toUpperCase()}${alias.slice(1)}`;
|
|
4178
|
+
const params = facets.flatMap(f => [
|
|
4179
|
+
`$${f.alias}Search: String!`,
|
|
4180
|
+
`$${f.alias}Cursor: String`,
|
|
4181
|
+
`$${includeVar(f.alias)}: Boolean!`,
|
|
4182
|
+
]);
|
|
4183
|
+
const fields = facets.map(
|
|
4184
|
+
f => `${f.alias}: search(first: ${pageSize}, after: $${f.alias}Cursor, query: $${f.alias}Search, type: ISSUE)
|
|
4185
|
+
@include(if: $${includeVar(f.alias)}) {
|
|
4186
|
+
issueCount
|
|
4187
|
+
pageInfo {
|
|
4188
|
+
endCursor
|
|
4189
|
+
hasNextPage
|
|
4190
|
+
}
|
|
4191
|
+
nodes {
|
|
4192
|
+
... on PullRequest {
|
|
4193
|
+
${gqlPullRequestFragment}
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
}`,
|
|
4197
|
+
);
|
|
4198
|
+
const query = `query searchPullRequestsPage(
|
|
4199
|
+
${params.join('\n\t\t\t')}
|
|
4200
|
+
$avatarSize: Int
|
|
4201
|
+
) {
|
|
4202
|
+
${fields.join('\n\t\t\t')}
|
|
4203
|
+
}`;
|
|
4204
|
+
|
|
4205
|
+
const variables: Record<string, unknown> = {
|
|
4206
|
+
baseUrl: options?.baseUrl,
|
|
4207
|
+
avatarSize: options?.avatarSize,
|
|
4208
|
+
};
|
|
4209
|
+
for (const facet of facets) {
|
|
4210
|
+
variables[`${facet.alias}Search`] = facetSearches.get(facet.alias);
|
|
4211
|
+
variables[`${facet.alias}Cursor`] = cursor?.facets[facet.alias] ?? undefined;
|
|
4212
|
+
variables[includeVar(facet.alias)] = isActive(facet.alias);
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
try {
|
|
4216
|
+
const rsp = await this.graphql<Record<string, SearchCategory | undefined>>(
|
|
4217
|
+
provider,
|
|
4218
|
+
token,
|
|
4219
|
+
query,
|
|
4220
|
+
variables,
|
|
4221
|
+
scope,
|
|
4222
|
+
cancellation,
|
|
4223
|
+
);
|
|
4224
|
+
if (rsp == null) return { values: [], hasMore: false, page: page, truncated: false };
|
|
4225
|
+
|
|
4226
|
+
const pullRequests: PullRequestShape[] = [];
|
|
4227
|
+
for (const facet of activeFacets) {
|
|
4228
|
+
for (const node of rsp[facet.alias]?.nodes ?? []) {
|
|
4229
|
+
if (node?.id == null) continue;
|
|
4230
|
+
|
|
4231
|
+
try {
|
|
4232
|
+
pullRequests.push(fromGitHubPullRequest(node, provider));
|
|
4233
|
+
} catch (ex) {
|
|
4234
|
+
scope?.warn(`skipped unmappable pull request; id=${node.id}, url=${node.url}, ex=${ex}`);
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
pullRequests.sort((a, b) => b.updatedDate.getTime() - a.updatedDate.getTime());
|
|
4239
|
+
const values = [
|
|
4240
|
+
...uniqueBy(
|
|
4241
|
+
pullRequests,
|
|
4242
|
+
pr => pr.url,
|
|
4243
|
+
(original, _current) => original,
|
|
4244
|
+
),
|
|
4245
|
+
];
|
|
4246
|
+
|
|
4247
|
+
const nextFacets: Record<string, string | null> = {};
|
|
4248
|
+
let hasMore = false;
|
|
4249
|
+
let continuationMissing = false;
|
|
4250
|
+
let totalCount = cursor?.totalCount ?? 0;
|
|
4251
|
+
let providerLimitReached = false;
|
|
4252
|
+
for (const facet of facets) {
|
|
4253
|
+
if (!isActive(facet.alias)) {
|
|
4254
|
+
nextFacets[facet.alias] = null;
|
|
4255
|
+
continue;
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
const category = rsp[facet.alias];
|
|
4259
|
+
const endCursor =
|
|
4260
|
+
category?.pageInfo?.hasNextPage === true && category.pageInfo.endCursor
|
|
4261
|
+
? category.pageInfo.endCursor
|
|
4262
|
+
: null;
|
|
4263
|
+
nextFacets[facet.alias] = endCursor;
|
|
4264
|
+
if (endCursor != null) {
|
|
4265
|
+
hasMore = true;
|
|
4266
|
+
}
|
|
4267
|
+
if (category?.pageInfo?.hasNextPage === true && category.pageInfo.endCursor == null) {
|
|
4268
|
+
continuationMissing = true;
|
|
4269
|
+
}
|
|
4270
|
+
totalCount = Math.max(totalCount, category?.issueCount ?? 0);
|
|
4271
|
+
providerLimitReached ||= (category?.issueCount ?? 0) > githubSearchResultLimit;
|
|
4272
|
+
}
|
|
4273
|
+
const truncated = cursor?.truncated === true || providerLimitReached || continuationMissing;
|
|
4274
|
+
const next: SearchCursor = {
|
|
4275
|
+
key: cursorKey,
|
|
4276
|
+
page: page + 1,
|
|
4277
|
+
facets: nextFacets,
|
|
4278
|
+
truncated: truncated || undefined,
|
|
4279
|
+
totalCount: totalCount,
|
|
4280
|
+
};
|
|
4281
|
+
|
|
4282
|
+
return {
|
|
4283
|
+
values: values,
|
|
4284
|
+
cursor: hasMore ? JSON.stringify(next) : undefined,
|
|
4285
|
+
hasMore: hasMore,
|
|
4286
|
+
page: page,
|
|
4287
|
+
truncated: truncated,
|
|
4288
|
+
totalCount: totalCount,
|
|
4289
|
+
};
|
|
4290
|
+
} catch (ex) {
|
|
4291
|
+
throw this.handleException(ex, provider, scope);
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4025
4295
|
@trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
4026
4296
|
async searchPullRequests(
|
|
4027
4297
|
provider: Provider,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { PullRequestSearchCriteria, PullRequestStateFilter } from '../../../git/models/pullRequest.js';
|
|
2
|
+
import { PullRequestFilter } from '../../../git/models/pullRequest.js';
|
|
3
|
+
import { sanitizeGitHubSearchText } from './issueSearchQuery.js';
|
|
4
|
+
|
|
5
|
+
export type GitHubPullRequestSearchFacet = {
|
|
6
|
+
/** Stable GraphQL alias and composite-cursor key. */
|
|
7
|
+
alias: string;
|
|
8
|
+
qualifiers: string[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const relationshipQualifier: Record<PullRequestFilter, string> = {
|
|
12
|
+
[PullRequestFilter.Author]: 'author:@me',
|
|
13
|
+
[PullRequestFilter.Assignee]: 'assignee:@me',
|
|
14
|
+
[PullRequestFilter.ReviewRequested]: 'review-requested:@me',
|
|
15
|
+
[PullRequestFilter.Mention]: 'mentions:@me',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const relationshipAlias: Record<PullRequestFilter, string> = {
|
|
19
|
+
[PullRequestFilter.Author]: 'author',
|
|
20
|
+
[PullRequestFilter.Assignee]: 'assignee',
|
|
21
|
+
[PullRequestFilter.ReviewRequested]: 'reviewRequested',
|
|
22
|
+
[PullRequestFilter.Mention]: 'mention',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const stateAlias: Record<PullRequestStateFilter, string> = {
|
|
26
|
+
open: 'Open',
|
|
27
|
+
closed: 'Closed',
|
|
28
|
+
merged: 'Merged',
|
|
29
|
+
all: 'All',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function stateQualifiers(state: PullRequestStateFilter): string[] {
|
|
33
|
+
switch (state) {
|
|
34
|
+
case 'closed':
|
|
35
|
+
// GitHub's `is:closed` includes merged PRs; `is:unmerged` removes that subset.
|
|
36
|
+
return ['is:closed', 'is:unmerged'];
|
|
37
|
+
case 'merged':
|
|
38
|
+
return ['is:merged'];
|
|
39
|
+
case 'all':
|
|
40
|
+
return [];
|
|
41
|
+
case 'open':
|
|
42
|
+
return ['is:open'];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Translates provider-neutral PR criteria into the independent GitHub searches whose union answers the request.
|
|
48
|
+
*
|
|
49
|
+
* GitHub's API search syntax ANDs repeated relationship/state qualifiers and does not implement the boolean
|
|
50
|
+
* groups accepted by the newer web UI. Each requested relationship × state pair therefore becomes one GraphQL
|
|
51
|
+
* alias; the API client sends every active alias in a single document, preserving one HTTP request per page.
|
|
52
|
+
* An omitted relationship is the explicitly unfiltered repository/organization scope, never `involves:@me`.
|
|
53
|
+
*/
|
|
54
|
+
export function toGitHubPullRequestSearchFacets(
|
|
55
|
+
criteria: PullRequestSearchCriteria | undefined,
|
|
56
|
+
): GitHubPullRequestSearchFacet[] {
|
|
57
|
+
const relationships: (PullRequestFilter | undefined)[] = criteria?.relationships?.length
|
|
58
|
+
? [...new Set(criteria.relationships)]
|
|
59
|
+
: [undefined];
|
|
60
|
+
const requestedStates: PullRequestStateFilter[] = criteria?.states?.length
|
|
61
|
+
? [...new Set(criteria.states)]
|
|
62
|
+
: ['open'];
|
|
63
|
+
const states: PullRequestStateFilter[] = requestedStates.includes('all') ? ['all'] : requestedStates;
|
|
64
|
+
const text = criteria?.text != null ? sanitizeGitHubSearchText(criteria.text) : '';
|
|
65
|
+
|
|
66
|
+
return relationships.flatMap(relationship =>
|
|
67
|
+
states.map(state => ({
|
|
68
|
+
alias: `${relationship != null ? relationshipAlias[relationship] : 'scope'}${stateAlias[state]}`,
|
|
69
|
+
qualifiers: [
|
|
70
|
+
'is:pr',
|
|
71
|
+
...(relationship != null ? [relationshipQualifier[relationship]] : []),
|
|
72
|
+
...stateQualifiers(state),
|
|
73
|
+
...(criteria?.includeArchived === true ? [] : ['archived:false']),
|
|
74
|
+
...(text.length > 0 ? [text] : []),
|
|
75
|
+
// Contract, not an option: a capped result is the N most recently updated only under this order.
|
|
76
|
+
'sort:updated',
|
|
77
|
+
],
|
|
78
|
+
})),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -218,6 +218,8 @@ export { IssueFilter, PullRequestFilter } from './providerFilters.js';
|
|
|
218
218
|
// The filtered issue search's criteria model and the capability table that says which of it a provider honors.
|
|
219
219
|
// String unions and plain interfaces, so a consumer builds criteria without importing an internal subpath.
|
|
220
220
|
export type { IssueSearchCapabilities, IssueSearchCriteria, IssueSearchRelationship } from './providerFilters.js';
|
|
221
|
+
// Account-wide pull-request search criteria and the per-provider capability table.
|
|
222
|
+
export type { PullRequestSearchCapabilities, PullRequestSearchCriteria } from './providerFilters.js';
|
|
221
223
|
// The count-only issue probe's input and result shapes.
|
|
222
224
|
export type { IssueCountResult, IssueCountScope } from './reads/counts.js';
|
|
223
225
|
// Cross-provider PR/issue state filters (string unions in the git models).
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Account } from '../../git/models/author.js';
|
|
2
2
|
import type { IssueSearchCriteria, IssueShape } from '../../git/models/issue.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
PullRequest,
|
|
5
|
+
PullRequestSearchCriteria,
|
|
6
|
+
PullRequestShape,
|
|
7
|
+
PullRequestStateFilter,
|
|
8
|
+
} from '../../git/models/pullRequest.js';
|
|
4
9
|
import type { GitRemote } from '../../git/models/remote.js';
|
|
5
10
|
import type { RemoteProviderId } from '../../git/models/remoteProvider.js';
|
|
6
11
|
import type { ResourceDescriptor } from '../../git/models/resourceDescriptor.js';
|
|
@@ -67,6 +72,7 @@ import type {
|
|
|
67
72
|
ProviderReposInput,
|
|
68
73
|
ProviderRepositoryShape,
|
|
69
74
|
PullRequestFilter,
|
|
75
|
+
PullRequestSearchCapabilities,
|
|
70
76
|
} from './providers/models.js';
|
|
71
77
|
import { providersMetadata } from './providers/models.js';
|
|
72
78
|
import type { ProvidersApi } from './providers/providersApi.js';
|
|
@@ -81,6 +87,7 @@ import { listIssueTrackerIssuesPage } from './reads/issueTracker.js';
|
|
|
81
87
|
import { listPullRequestsPage } from './reads/pullRequests.js';
|
|
82
88
|
import { resolveRepository } from './reads/resolveRepository.js';
|
|
83
89
|
import { searchIssuesPage } from './reads/searchIssues.js';
|
|
90
|
+
import { searchPullRequestsPage } from './reads/searchPullRequests.js';
|
|
84
91
|
import { sweepClosedPullRequests, sweepPullRequests } from './reads/sweeps.js';
|
|
85
92
|
import { noConnectionWarning } from './reads/warnings.js';
|
|
86
93
|
import type {
|
|
@@ -533,6 +540,7 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
|
|
|
533
540
|
getSupportedFilters(providerId: IntegrationIds): {
|
|
534
541
|
pullRequests: PullRequestFilter[];
|
|
535
542
|
pullRequestsAccountWide: PullRequestFilter[];
|
|
543
|
+
pullRequestSearch: PullRequestSearchCapabilities;
|
|
536
544
|
issues: IssueFilter[];
|
|
537
545
|
issuesAccountWide: IssueFilter[];
|
|
538
546
|
issueSearch: IssueSearchCapabilities;
|
|
@@ -1051,6 +1059,22 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
|
|
|
1051
1059
|
return listPullRequestsPage(this, options);
|
|
1052
1060
|
}
|
|
1053
1061
|
|
|
1062
|
+
/** See {@link IntegrationManager.searchPullRequestsPage}. */
|
|
1063
|
+
async searchPullRequestsPage(options: {
|
|
1064
|
+
providerId: IntegrationIds;
|
|
1065
|
+
repos?: ProviderReposInput;
|
|
1066
|
+
org?: string;
|
|
1067
|
+
criteria?: PullRequestSearchCriteria;
|
|
1068
|
+
page?: number;
|
|
1069
|
+
cursor?: string;
|
|
1070
|
+
itemsPerPage?: number;
|
|
1071
|
+
forceSync?: boolean;
|
|
1072
|
+
connectionId?: string;
|
|
1073
|
+
domain?: string;
|
|
1074
|
+
}): Promise<ProviderPagedResult<PullRequestShape>> {
|
|
1075
|
+
return searchPullRequestsPage(this, options);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1054
1078
|
async listIssuesPage(options: {
|
|
1055
1079
|
providerId: IntegrationIds;
|
|
1056
1080
|
repos?: ProviderReposInput;
|
|
@@ -1550,12 +1574,31 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
|
|
|
1550
1574
|
}
|
|
1551
1575
|
}
|
|
1552
1576
|
|
|
1577
|
+
// Concurrent, not serial: each `syncCloudConnection` is a cloud round trip (a forced sync deletes the
|
|
1578
|
+
// stored session and refetches it), so a serial loop pays the SUM of every provider's latency on a
|
|
1579
|
+
// path that gates EVERY provider read. `reconcileCloudConnections` below is already `Promise.all`.
|
|
1580
|
+
//
|
|
1581
|
+
// Safe because every shared mutable path is already serialized, which is why this can be a plain
|
|
1582
|
+
// fan-out rather than needing per-provider grouping:
|
|
1583
|
+
// - `ensureProvider` is `@gate()`d on `providerId`, so the two integrations a multi-host
|
|
1584
|
+
// self-managed id yields (one per domain, see `getSupportedCloudIntegrations`) share one
|
|
1585
|
+
// in-flight construction instead of racing to `providers.set`.
|
|
1586
|
+
// - `ensureSession` is `@gate()`d per integration instance.
|
|
1587
|
+
// - `addOrUpdateConfigured`/`removeConfigured` mutate `configured` in a synchronous critical
|
|
1588
|
+
// section (no `await` between read and `set`), and `storeConfigured` has its own write queue.
|
|
1589
|
+
// - Secrets and the `connected:` workspace flags are keyed by integration id + domain.
|
|
1590
|
+
const integrations: Integration[] = [];
|
|
1553
1591
|
for await (const integration of this.getSupportedCloudIntegrations(domainsById)) {
|
|
1554
|
-
|
|
1555
|
-
this.getCloudConnectionState(integration, connectedIntegrations, domainsById),
|
|
1556
|
-
forceConnect,
|
|
1557
|
-
);
|
|
1592
|
+
integrations.push(integration);
|
|
1558
1593
|
}
|
|
1594
|
+
await Promise.all(
|
|
1595
|
+
integrations.map(integration =>
|
|
1596
|
+
integration.syncCloudConnection(
|
|
1597
|
+
this.getCloudConnectionState(integration, connectedIntegrations, domainsById),
|
|
1598
|
+
forceConnect,
|
|
1599
|
+
),
|
|
1600
|
+
),
|
|
1601
|
+
);
|
|
1559
1602
|
|
|
1560
1603
|
// Persist every account when the backend advertises per-connection identity (multi-account). This
|
|
1561
1604
|
// is a strict no-op for backends that return a single, id-less connection per provider.
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { IssueSearchCriteria, IssueShape } from '../../git/models/issue.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
PullRequestSearchCriteria,
|
|
4
|
+
PullRequestShape,
|
|
5
|
+
PullRequestStateFilter,
|
|
6
|
+
} from '../../git/models/pullRequest.js';
|
|
3
7
|
import type { Event } from '../../utils/event.js';
|
|
4
8
|
import type { ConfiguredIntegrationsChangeEvent } from './authentication/configuredIntegrationService.js';
|
|
5
9
|
import type { ConfiguredIntegrationDescriptor } from './authentication/models.js';
|
|
6
10
|
import type { IntegrationIds } from './constants.js';
|
|
7
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
IssueFilter,
|
|
13
|
+
IssueSearchCapabilities,
|
|
14
|
+
PullRequestFilter,
|
|
15
|
+
PullRequestSearchCapabilities,
|
|
16
|
+
} from './providerFilters.js';
|
|
8
17
|
import type { IssueCountResult, IssueCountScope } from './reads/counts.js';
|
|
9
18
|
import type {
|
|
10
19
|
ConnectionStateChangeEvent,
|
|
@@ -142,8 +151,9 @@ export type ListProjectsOptions =
|
|
|
142
151
|
*
|
|
143
152
|
* ## Paging contract
|
|
144
153
|
*
|
|
145
|
-
* Every paged read (`listRepos`, `listPullRequestsPage`, `
|
|
146
|
-
* `broadenIssues`) takes both `page` and `cursor`, and the two are NOT
|
|
154
|
+
* Every paged read (`listRepos`, `listPullRequestsPage`, `searchPullRequestsPage`, `listIssuesPage`,
|
|
155
|
+
* `listIssueTrackerIssuesPage`, `broadenIssues`) takes both `page` and `cursor`, and the two are NOT
|
|
156
|
+
* interchangeable:
|
|
147
157
|
*
|
|
148
158
|
* - **Supplying `cursor` guarantees a single upstream round trip per provider scope.** A threaded continuation
|
|
149
159
|
* is handed to the provider as-is; the facade never walks the pages before it. This is a guarantee, not an
|
|
@@ -199,6 +209,11 @@ export interface IntegrationManager {
|
|
|
199
209
|
pullRequests: PullRequestFilter[];
|
|
200
210
|
/** Optional for structural compatibility; missing means no account-wide narrowing filters are supported. */
|
|
201
211
|
pullRequestsAccountWide?: PullRequestFilter[];
|
|
212
|
+
/**
|
|
213
|
+
* Criteria and scopes {@link searchPullRequestsPage} can express. Always present; an empty relationship list
|
|
214
|
+
* means the provider exposes no filtered pull-request search.
|
|
215
|
+
*/
|
|
216
|
+
pullRequestSearch: PullRequestSearchCapabilities;
|
|
202
217
|
issues: IssueFilter[];
|
|
203
218
|
issuesAccountWide: IssueFilter[];
|
|
204
219
|
/**
|
|
@@ -274,6 +289,42 @@ export interface IntegrationManager {
|
|
|
274
289
|
*/
|
|
275
290
|
domain?: string;
|
|
276
291
|
}): Promise<ProviderPagedResult<PullRequestShape>>;
|
|
292
|
+
/**
|
|
293
|
+
* Pull requests matching structured criteria over a repository/organization or current-user relationship
|
|
294
|
+
* scope.
|
|
295
|
+
*
|
|
296
|
+
* This is a separate read from {@link listPullRequestsPage}: free text reaches the provider instead of filtering
|
|
297
|
+
* whichever rows happened to be loaded, and every cursor-threaded page costs exactly one upstream request.
|
|
298
|
+
* `criteria.relationships` and `criteria.states` are OR sets, so `[closed, merged]` expresses the complete
|
|
299
|
+
* terminal set and `[Author, Assignee, ReviewRequested]` matches the visible PR list without falling back to
|
|
300
|
+
* GitHub's mismatched `involves:@me`. Omit relationships only with a repository/organization scope to search
|
|
301
|
+
* every PR there.
|
|
302
|
+
*
|
|
303
|
+
* Results are always ordered most-recently-updated-first. If the provider's result ceiling is reached, the
|
|
304
|
+
* request still succeeds and carries a warning omission with `totalCount`, `limit`, and `recovery: 'none'`.
|
|
305
|
+
* `totalCount` is the largest provider-reported pre-ceiling facet count, not the reachable or returned row
|
|
306
|
+
* count; this mirrors the per-search ceiling's own unit.
|
|
307
|
+
*
|
|
308
|
+
* Check `getSupportedFilters().pullRequestSearch` before calling. A provider that reports no relationships
|
|
309
|
+
* refuses the read rather than returning a page that never honored the criteria or scope.
|
|
310
|
+
*/
|
|
311
|
+
searchPullRequestsPage(options: {
|
|
312
|
+
providerId: IntegrationIds;
|
|
313
|
+
/** Repository descriptors that bound the search; ids cannot name provider search qualifiers. */
|
|
314
|
+
repos?: ProviderRepositoriesInput;
|
|
315
|
+
/** Organization/account that bounds the search. */
|
|
316
|
+
org?: string;
|
|
317
|
+
criteria?: PullRequestSearchCriteria;
|
|
318
|
+
/** Cursor-only: without a cursor, reaching page N costs O(N) upstream requests. */
|
|
319
|
+
page?: number;
|
|
320
|
+
cursor?: string;
|
|
321
|
+
/** Page size per relationship × state facet; the deduped union can contain more rows. */
|
|
322
|
+
itemsPerPage?: number;
|
|
323
|
+
forceSync?: boolean;
|
|
324
|
+
connectionId?: string;
|
|
325
|
+
/** Self-managed host domain fallback; see {@link ProviderSweepTarget.domain}. */
|
|
326
|
+
domain?: string;
|
|
327
|
+
}): Promise<ProviderPagedResult<PullRequestShape>>;
|
|
277
328
|
listIssuesPage(options: {
|
|
278
329
|
providerId: IntegrationIds;
|
|
279
330
|
repos?: ProviderRepositoriesInput;
|
|
@@ -3,7 +3,12 @@ import type { Account, UnidentifiedAuthor } from '../../../git/models/author.js'
|
|
|
3
3
|
import type { DefaultBranch } from '../../../git/models/defaultBranch.js';
|
|
4
4
|
import type { IssueSearchCriteria, IssueShape, IssueStateFilter } from '../../../git/models/issue.js';
|
|
5
5
|
import type { IssueOrPullRequestState as PullRequestState } from '../../../git/models/issueOrPullRequest.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
PullRequest,
|
|
8
|
+
PullRequestMergeMethod,
|
|
9
|
+
PullRequestSearchCriteria,
|
|
10
|
+
PullRequestStateFilter,
|
|
11
|
+
} from '../../../git/models/pullRequest.js';
|
|
7
12
|
import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata.js';
|
|
8
13
|
import type { ResourceDescriptor } from '../../../git/models/resourceDescriptor.js';
|
|
9
14
|
import type { PullRequestUrlIdentity } from '../../../git/utils/pullRequest.utils.js';
|
|
@@ -44,7 +49,12 @@ import {
|
|
|
44
49
|
} from '../providers/models.js';
|
|
45
50
|
import type { ProvidersApi } from '../providers/providersApi.js';
|
|
46
51
|
import { mergeCollectionMetadata } from '../providers/utils/providerPaging.js';
|
|
47
|
-
import type {
|
|
52
|
+
import type {
|
|
53
|
+
IntegrationResult,
|
|
54
|
+
IntegrationType,
|
|
55
|
+
ProviderIssueSearchPage,
|
|
56
|
+
ProviderPullRequestSearchPage,
|
|
57
|
+
} from './integration.js';
|
|
48
58
|
import { IntegrationBase } from './integration.js';
|
|
49
59
|
|
|
50
60
|
function isAzureDevOpsProvider(
|
|
@@ -1543,6 +1553,50 @@ export abstract class GitHostIntegration<
|
|
|
1543
1553
|
options?: SearchMyPullRequestsOptions,
|
|
1544
1554
|
): Promise<IntegrationResult<PullRequest[] | undefined>>;
|
|
1545
1555
|
|
|
1556
|
+
/**
|
|
1557
|
+
* Result-returning wrapper for the filtered pull-request search. Errors become the soft
|
|
1558
|
+
* `{ error }` branch so the public facade can preserve an empty account and a failed request as distinct
|
|
1559
|
+
* outcomes.
|
|
1560
|
+
*/
|
|
1561
|
+
async searchPullRequestsPageResult(
|
|
1562
|
+
options: {
|
|
1563
|
+
repos?: ProviderRepoInput[];
|
|
1564
|
+
org?: string;
|
|
1565
|
+
criteria?: PullRequestSearchCriteria;
|
|
1566
|
+
cursor?: string;
|
|
1567
|
+
pageSize?: number;
|
|
1568
|
+
},
|
|
1569
|
+
cancellation?: AbortSignal,
|
|
1570
|
+
connectionId?: string,
|
|
1571
|
+
): Promise<IntegrationResult<ProviderPullRequestSearchPage | undefined>> {
|
|
1572
|
+
const scope = getScopedLogger();
|
|
1573
|
+
const session = await this.resolveReadSession(connectionId, scope);
|
|
1574
|
+
if (session == null) return undefined;
|
|
1575
|
+
|
|
1576
|
+
const start = performance.now();
|
|
1577
|
+
try {
|
|
1578
|
+
const result = await this.searchProviderPullRequestsPage?.(session, options, cancellation);
|
|
1579
|
+
this.resetRequestExceptionCount('searchPullRequestsPage');
|
|
1580
|
+
return { value: result, duration: performance.now() - start };
|
|
1581
|
+
} catch (ex) {
|
|
1582
|
+
this.handleProviderException('searchPullRequestsPage', ex, { scope: scope, connectionId: connectionId });
|
|
1583
|
+
return { error: toError(ex), duration: performance.now() - start };
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/** Must agree with `ProviderMetadata.supportedPullRequestSearch`. */
|
|
1588
|
+
protected searchProviderPullRequestsPage?(
|
|
1589
|
+
session: ProviderAuthenticationSession,
|
|
1590
|
+
options: {
|
|
1591
|
+
repos?: ProviderRepoInput[];
|
|
1592
|
+
org?: string;
|
|
1593
|
+
criteria?: PullRequestSearchCriteria;
|
|
1594
|
+
cursor?: string;
|
|
1595
|
+
pageSize?: number;
|
|
1596
|
+
},
|
|
1597
|
+
cancellation?: AbortSignal,
|
|
1598
|
+
): Promise<ProviderPullRequestSearchPage | undefined>;
|
|
1599
|
+
|
|
1546
1600
|
async searchPullRequests(
|
|
1547
1601
|
searchQuery: string,
|
|
1548
1602
|
repo?: T,
|