@gitkraken/core-gitlens 0.5.104 → 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 +15 -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/authentication/configuredIntegrationService.d.ts +1 -0
- package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts.map +1 -1
- package/dist/plus/integrations/authentication/configuredIntegrationService.js +4 -1
- package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
- 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 +34 -23
- 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/authentication/configuredIntegrationService.ts +6 -1
- package/src/plus/integrations/index.ts +2 -0
- package/src/plus/integrations/integrationService.ts +108 -70
- 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
|
@@ -149,9 +149,59 @@ export interface PullRequestMember {
|
|
|
149
149
|
url?: string;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/** Provider-neutral relationship facets shared by PR listings and filtered PR search. */
|
|
153
|
+
export enum PullRequestFilter {
|
|
154
|
+
Author = 'author',
|
|
155
|
+
Assignee = 'assignee',
|
|
156
|
+
ReviewRequested = 'review-requested',
|
|
157
|
+
Mention = 'mention',
|
|
158
|
+
}
|
|
159
|
+
|
|
152
160
|
/** Selects which pull request states a read should include. `all` covers open + closed + merged. */
|
|
153
161
|
export type PullRequestStateFilter = 'open' | 'closed' | 'merged' | 'all';
|
|
154
162
|
|
|
163
|
+
/**
|
|
164
|
+
* What the filtered pull-request search narrows on.
|
|
165
|
+
*
|
|
166
|
+
* Structured rather than a provider query string so callers can send free text without giving it a qualifier
|
|
167
|
+
* channel, and can check {@link PullRequestSearchCapabilities} before issuing a read. Every requested criterion
|
|
168
|
+
* is validated all-or-nothing before the provider runs; unsupported criteria never fall through to a wider
|
|
169
|
+
* result set whose paging would no longer describe the returned items.
|
|
170
|
+
*
|
|
171
|
+
* Relationships are OR-ed, as are states. Omitting `relationships` removes the current-user constraint, which is
|
|
172
|
+
* only safe when the manager call supplies a repository or organization scope. Omitting `states` reads open PRs.
|
|
173
|
+
*/
|
|
174
|
+
export interface PullRequestSearchCriteria {
|
|
175
|
+
/**
|
|
176
|
+
* Free text matched by the provider's own relevance rules. Tokens that look like provider qualifiers are
|
|
177
|
+
* removed before the query is built, so text cannot change the search's scope or state.
|
|
178
|
+
*/
|
|
179
|
+
text?: string;
|
|
180
|
+
/** Current-user relationship facets to union. Empty/omitted means no relationship constraint. */
|
|
181
|
+
relationships?: PullRequestFilter[];
|
|
182
|
+
/** Pull request states to union. Empty/omitted reads open PRs; `all` subsumes every other member. */
|
|
183
|
+
states?: PullRequestStateFilter[];
|
|
184
|
+
/** Includes PRs from archived repositories. They are excluded by default. */
|
|
185
|
+
includeArchived?: boolean;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Which {@link PullRequestSearchCriteria} fields and manager-level scopes a provider can express server-side.
|
|
190
|
+
* An empty `relationships` means the provider has no filtered PR search at all.
|
|
191
|
+
*/
|
|
192
|
+
export interface PullRequestSearchCapabilities {
|
|
193
|
+
/** Current-user relationship facets the provider can union. Empty means the search itself is unsupported. */
|
|
194
|
+
relationships: PullRequestFilter[];
|
|
195
|
+
/** Individual state values the provider can union in one logical search. */
|
|
196
|
+
states: PullRequestStateFilter[];
|
|
197
|
+
text: boolean;
|
|
198
|
+
includeArchived: boolean;
|
|
199
|
+
/** Whether the manager's repository-descriptor scope is supported. */
|
|
200
|
+
repositoryScope: boolean;
|
|
201
|
+
/** Whether the manager's organization scope is supported. */
|
|
202
|
+
organizationScope: boolean;
|
|
203
|
+
}
|
|
204
|
+
|
|
155
205
|
export interface PullRequestRef {
|
|
156
206
|
owner: string;
|
|
157
207
|
repo: string;
|
|
@@ -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
|
|
@@ -4022,6 +4052,229 @@ export class GitHubApi {
|
|
|
4022
4052
|
}
|
|
4023
4053
|
}
|
|
4024
4054
|
|
|
4055
|
+
/**
|
|
4056
|
+
* Searches pull requests over a repository/org or current-user relationship scope. One GraphQL document carries
|
|
4057
|
+
* every active relationship × state facet, so one HTTP request serves one page even when the logical search is
|
|
4058
|
+
* a union. The cursor preserves each facet's continuation plus the positional page.
|
|
4059
|
+
*
|
|
4060
|
+
* Ordering is always most-recently-updated-first, and user text is sanitized before it reaches the provider
|
|
4061
|
+
* query. See {@link toGitHubPullRequestSearchFacets}.
|
|
4062
|
+
*/
|
|
4063
|
+
@trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
4064
|
+
async searchPullRequestsPage(
|
|
4065
|
+
provider: Provider,
|
|
4066
|
+
token: GitHubTokenInfo,
|
|
4067
|
+
options?: {
|
|
4068
|
+
repos?: string[];
|
|
4069
|
+
org?: string;
|
|
4070
|
+
criteria?: PullRequestSearchCriteria;
|
|
4071
|
+
baseUrl?: string;
|
|
4072
|
+
avatarSize?: number;
|
|
4073
|
+
cursor?: string;
|
|
4074
|
+
pageSize?: number;
|
|
4075
|
+
},
|
|
4076
|
+
cancellation?: AbortSignal,
|
|
4077
|
+
): Promise<PullRequestSearchResult | undefined> {
|
|
4078
|
+
const scope = getScopedLogger();
|
|
4079
|
+
|
|
4080
|
+
const pageSize = Math.min(
|
|
4081
|
+
maxPullRequestSearchPageSize,
|
|
4082
|
+
Math.max(1, Math.trunc(options?.pageSize ?? defaultPullRequestSearchPageSize)),
|
|
4083
|
+
);
|
|
4084
|
+
const facets = toGitHubPullRequestSearchFacets(options?.criteria);
|
|
4085
|
+
const facetAliases = facets.map(f => f.alias).sort();
|
|
4086
|
+
const scopeQualifiers = toGitHubIssueSearchScopeQualifiers(options?.org, options?.repos);
|
|
4087
|
+
const facetSearches = new Map(
|
|
4088
|
+
facets.map(f => [f.alias, [...scopeQualifiers, ...f.qualifiers].join(' ')] as const),
|
|
4089
|
+
);
|
|
4090
|
+
// Bind a cursor to every qualifier without publishing the user text/scope inside the opaque cursor. FNV-1a
|
|
4091
|
+
// is a compact drift key, not a security primitive: a mismatch only degrades safely to page 1.
|
|
4092
|
+
let cursorKeyHash = 0x811c9dc5;
|
|
4093
|
+
for (const value of [...facetSearches.entries()].sort(([a], [b]) => a.localeCompare(b)).flat()) {
|
|
4094
|
+
for (let i = 0; i < value.length; i++) {
|
|
4095
|
+
cursorKeyHash ^= value.charCodeAt(i);
|
|
4096
|
+
cursorKeyHash = Math.imul(cursorKeyHash, 0x01000193);
|
|
4097
|
+
}
|
|
4098
|
+
cursorKeyHash ^= 0;
|
|
4099
|
+
cursorKeyHash = Math.imul(cursorKeyHash, 0x01000193);
|
|
4100
|
+
}
|
|
4101
|
+
const cursorKey = (cursorKeyHash >>> 0).toString(36);
|
|
4102
|
+
|
|
4103
|
+
type SearchCategory = {
|
|
4104
|
+
issueCount: number;
|
|
4105
|
+
pageInfo?: { endCursor?: string | null; hasNextPage: boolean };
|
|
4106
|
+
nodes: (GitHubPullRequest | null)[] | null;
|
|
4107
|
+
};
|
|
4108
|
+
interface SearchCursor {
|
|
4109
|
+
key: string;
|
|
4110
|
+
page: number;
|
|
4111
|
+
facets: Record<string, string | null>;
|
|
4112
|
+
truncated?: boolean;
|
|
4113
|
+
totalCount?: number;
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
let cursor: SearchCursor | undefined;
|
|
4117
|
+
if (options?.cursor != null) {
|
|
4118
|
+
try {
|
|
4119
|
+
const parsed = JSON.parse(options.cursor) as Partial<SearchCursor>;
|
|
4120
|
+
const parsedFacets = parsed.facets;
|
|
4121
|
+
if (parsedFacets != null && typeof parsedFacets === 'object' && !Array.isArray(parsedFacets)) {
|
|
4122
|
+
const parsedAliases = Object.keys(parsedFacets).sort();
|
|
4123
|
+
const sameFacets =
|
|
4124
|
+
parsedAliases.length === facetAliases.length &&
|
|
4125
|
+
parsedAliases.every((alias, index) => alias === facetAliases[index]);
|
|
4126
|
+
const usableSlots = Object.values(parsedFacets).every(
|
|
4127
|
+
slot => slot === null || (typeof slot === 'string' && slot.length > 0),
|
|
4128
|
+
);
|
|
4129
|
+
if (parsed.key === cursorKey && sameFacets && usableSlots) {
|
|
4130
|
+
cursor = {
|
|
4131
|
+
key: cursorKey,
|
|
4132
|
+
page:
|
|
4133
|
+
typeof parsed.page === 'number' && Number.isFinite(parsed.page)
|
|
4134
|
+
? Math.max(1, Math.trunc(parsed.page))
|
|
4135
|
+
: 1,
|
|
4136
|
+
facets: parsedFacets,
|
|
4137
|
+
truncated: parsed.truncated === true,
|
|
4138
|
+
totalCount:
|
|
4139
|
+
typeof parsed.totalCount === 'number' && Number.isFinite(parsed.totalCount)
|
|
4140
|
+
? Math.max(0, Math.trunc(parsed.totalCount))
|
|
4141
|
+
: undefined,
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
} catch {}
|
|
4146
|
+
}
|
|
4147
|
+
const page = cursor?.page ?? 1;
|
|
4148
|
+
const isActive = (alias: string): boolean => cursor?.facets[alias] !== null;
|
|
4149
|
+
const activeFacets = facets.filter(f => isActive(f.alias));
|
|
4150
|
+
if (activeFacets.length === 0) {
|
|
4151
|
+
return {
|
|
4152
|
+
values: [],
|
|
4153
|
+
hasMore: false,
|
|
4154
|
+
page: page,
|
|
4155
|
+
truncated: cursor?.truncated === true,
|
|
4156
|
+
totalCount: cursor?.totalCount,
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
const includeVar = (alias: string): string => `include${alias.charAt(0).toUpperCase()}${alias.slice(1)}`;
|
|
4161
|
+
const params = facets.flatMap(f => [
|
|
4162
|
+
`$${f.alias}Search: String!`,
|
|
4163
|
+
`$${f.alias}Cursor: String`,
|
|
4164
|
+
`$${includeVar(f.alias)}: Boolean!`,
|
|
4165
|
+
]);
|
|
4166
|
+
const fields = facets.map(
|
|
4167
|
+
f => `${f.alias}: search(first: ${pageSize}, after: $${f.alias}Cursor, query: $${f.alias}Search, type: ISSUE)
|
|
4168
|
+
@include(if: $${includeVar(f.alias)}) {
|
|
4169
|
+
issueCount
|
|
4170
|
+
pageInfo {
|
|
4171
|
+
endCursor
|
|
4172
|
+
hasNextPage
|
|
4173
|
+
}
|
|
4174
|
+
nodes {
|
|
4175
|
+
... on PullRequest {
|
|
4176
|
+
${gqlPullRequestFragment}
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
}`,
|
|
4180
|
+
);
|
|
4181
|
+
const query = `query searchPullRequestsPage(
|
|
4182
|
+
${params.join('\n\t\t\t')}
|
|
4183
|
+
$avatarSize: Int
|
|
4184
|
+
) {
|
|
4185
|
+
${fields.join('\n\t\t\t')}
|
|
4186
|
+
}`;
|
|
4187
|
+
|
|
4188
|
+
const variables: Record<string, unknown> = {
|
|
4189
|
+
baseUrl: options?.baseUrl,
|
|
4190
|
+
avatarSize: options?.avatarSize,
|
|
4191
|
+
};
|
|
4192
|
+
for (const facet of facets) {
|
|
4193
|
+
variables[`${facet.alias}Search`] = facetSearches.get(facet.alias);
|
|
4194
|
+
variables[`${facet.alias}Cursor`] = cursor?.facets[facet.alias] ?? undefined;
|
|
4195
|
+
variables[includeVar(facet.alias)] = isActive(facet.alias);
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
try {
|
|
4199
|
+
const rsp = await this.graphql<Record<string, SearchCategory | undefined>>(
|
|
4200
|
+
provider,
|
|
4201
|
+
token,
|
|
4202
|
+
query,
|
|
4203
|
+
variables,
|
|
4204
|
+
scope,
|
|
4205
|
+
cancellation,
|
|
4206
|
+
);
|
|
4207
|
+
if (rsp == null) return { values: [], hasMore: false, page: page, truncated: false };
|
|
4208
|
+
|
|
4209
|
+
const pullRequests: PullRequestShape[] = [];
|
|
4210
|
+
for (const facet of activeFacets) {
|
|
4211
|
+
for (const node of rsp[facet.alias]?.nodes ?? []) {
|
|
4212
|
+
if (node?.id == null) continue;
|
|
4213
|
+
|
|
4214
|
+
try {
|
|
4215
|
+
pullRequests.push(fromGitHubPullRequest(node, provider));
|
|
4216
|
+
} catch (ex) {
|
|
4217
|
+
scope?.warn(`skipped unmappable pull request; id=${node.id}, url=${node.url}, ex=${ex}`);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
pullRequests.sort((a, b) => b.updatedDate.getTime() - a.updatedDate.getTime());
|
|
4222
|
+
const values = [
|
|
4223
|
+
...uniqueBy(
|
|
4224
|
+
pullRequests,
|
|
4225
|
+
pr => pr.url,
|
|
4226
|
+
(original, _current) => original,
|
|
4227
|
+
),
|
|
4228
|
+
];
|
|
4229
|
+
|
|
4230
|
+
const nextFacets: Record<string, string | null> = {};
|
|
4231
|
+
let hasMore = false;
|
|
4232
|
+
let continuationMissing = false;
|
|
4233
|
+
let totalCount = cursor?.totalCount ?? 0;
|
|
4234
|
+
let providerLimitReached = false;
|
|
4235
|
+
for (const facet of facets) {
|
|
4236
|
+
if (!isActive(facet.alias)) {
|
|
4237
|
+
nextFacets[facet.alias] = null;
|
|
4238
|
+
continue;
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
const category = rsp[facet.alias];
|
|
4242
|
+
const endCursor =
|
|
4243
|
+
category?.pageInfo?.hasNextPage === true && category.pageInfo.endCursor
|
|
4244
|
+
? category.pageInfo.endCursor
|
|
4245
|
+
: null;
|
|
4246
|
+
nextFacets[facet.alias] = endCursor;
|
|
4247
|
+
if (endCursor != null) {
|
|
4248
|
+
hasMore = true;
|
|
4249
|
+
}
|
|
4250
|
+
if (category?.pageInfo?.hasNextPage === true && category.pageInfo.endCursor == null) {
|
|
4251
|
+
continuationMissing = true;
|
|
4252
|
+
}
|
|
4253
|
+
totalCount = Math.max(totalCount, category?.issueCount ?? 0);
|
|
4254
|
+
providerLimitReached ||= (category?.issueCount ?? 0) > githubSearchResultLimit;
|
|
4255
|
+
}
|
|
4256
|
+
const truncated = cursor?.truncated === true || providerLimitReached || continuationMissing;
|
|
4257
|
+
const next: SearchCursor = {
|
|
4258
|
+
key: cursorKey,
|
|
4259
|
+
page: page + 1,
|
|
4260
|
+
facets: nextFacets,
|
|
4261
|
+
truncated: truncated || undefined,
|
|
4262
|
+
totalCount: totalCount,
|
|
4263
|
+
};
|
|
4264
|
+
|
|
4265
|
+
return {
|
|
4266
|
+
values: values,
|
|
4267
|
+
cursor: hasMore ? JSON.stringify(next) : undefined,
|
|
4268
|
+
hasMore: hasMore,
|
|
4269
|
+
page: page,
|
|
4270
|
+
truncated: truncated,
|
|
4271
|
+
totalCount: totalCount,
|
|
4272
|
+
};
|
|
4273
|
+
} catch (ex) {
|
|
4274
|
+
throw this.handleException(ex, provider, scope);
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4025
4278
|
@trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
4026
4279
|
async searchPullRequests(
|
|
4027
4280
|
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
|
+
}
|
|
@@ -40,6 +40,7 @@ export interface ConfiguredIntegrationsChangeEvent {
|
|
|
40
40
|
|
|
41
41
|
export class ConfiguredIntegrationService implements Disposable {
|
|
42
42
|
private readonly _onDidChange = new Emitter<ConfiguredIntegrationsChangeEvent>();
|
|
43
|
+
private storeConfiguredQueue: Promise<void> = Promise.resolve();
|
|
43
44
|
get onDidChange(): Event<ConfiguredIntegrationsChangeEvent> {
|
|
44
45
|
return this._onDidChange.event;
|
|
45
46
|
}
|
|
@@ -129,7 +130,11 @@ export class ConfiguredIntegrationService implements Disposable {
|
|
|
129
130
|
}));
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
const pending = this.storeConfiguredQueue.then(() =>
|
|
134
|
+
this.ctx.storage.store('integrations:configured', configured),
|
|
135
|
+
);
|
|
136
|
+
this.storeConfiguredQueue = pending.catch(() => {});
|
|
137
|
+
await pending;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
private async addOrUpdateConfigured(descriptor: ConfiguredIntegrationDescriptor): Promise<void> {
|
|
@@ -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).
|