@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/dist/git/models/pullRequest.d.ts +47 -0
  3. package/dist/git/models/pullRequest.d.ts.map +1 -1
  4. package/dist/git/models/pullRequest.js +8 -0
  5. package/dist/git/models/pullRequest.js.map +1 -1
  6. package/dist/plus/git-github/api/github.d.ts +33 -1
  7. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  8. package/dist/plus/git-github/api/github.js +176 -0
  9. package/dist/plus/git-github/api/github.js.map +1 -1
  10. package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts +16 -0
  11. package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts.map +1 -0
  12. package/dist/plus/git-github/api/pullRequestSearchQuery.js +64 -0
  13. package/dist/plus/git-github/api/pullRequestSearchQuery.js.map +1 -0
  14. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts +1 -0
  15. package/dist/plus/integrations/authentication/configuredIntegrationService.d.ts.map +1 -1
  16. package/dist/plus/integrations/authentication/configuredIntegrationService.js +4 -1
  17. package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
  18. package/dist/plus/integrations/index.d.ts +1 -0
  19. package/dist/plus/integrations/index.d.ts.map +1 -1
  20. package/dist/plus/integrations/integrationService.d.ts +16 -2
  21. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  22. package/dist/plus/integrations/integrationService.js +34 -23
  23. package/dist/plus/integrations/integrationService.js.map +1 -1
  24. package/dist/plus/integrations/manager.d.ts +46 -4
  25. package/dist/plus/integrations/manager.d.ts.map +1 -1
  26. package/dist/plus/integrations/models/gitHostIntegration.d.ts +22 -2
  27. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  28. package/dist/plus/integrations/models/gitHostIntegration.js +21 -0
  29. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  30. package/dist/plus/integrations/models/integration.d.ts +2 -1
  31. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  32. package/dist/plus/integrations/models/integration.js.map +1 -1
  33. package/dist/plus/integrations/models/pullRequestReads.d.ts +18 -0
  34. package/dist/plus/integrations/models/pullRequestReads.d.ts.map +1 -0
  35. package/dist/plus/integrations/models/pullRequestReads.js +2 -0
  36. package/dist/plus/integrations/models/pullRequestReads.js.map +1 -0
  37. package/dist/plus/integrations/providerFilters.d.ts +7 -6
  38. package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
  39. package/dist/plus/integrations/providerFilters.js +1 -7
  40. package/dist/plus/integrations/providerFilters.js.map +1 -1
  41. package/dist/plus/integrations/providers/github.d.ts +14 -2
  42. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  43. package/dist/plus/integrations/providers/github.js +15 -0
  44. package/dist/plus/integrations/providers/github.js.map +1 -1
  45. package/dist/plus/integrations/providers/models.d.ts +12 -2
  46. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  47. package/dist/plus/integrations/providers/models.js +18 -0
  48. package/dist/plus/integrations/providers/models.js.map +1 -1
  49. package/dist/plus/integrations/reads/context.d.ts +1 -1
  50. package/dist/plus/integrations/reads/filters.d.ts +34 -1
  51. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  52. package/dist/plus/integrations/reads/filters.js +73 -0
  53. package/dist/plus/integrations/reads/filters.js.map +1 -1
  54. package/dist/plus/integrations/reads/searchPullRequests.d.ts +26 -0
  55. package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -0
  56. package/dist/plus/integrations/reads/searchPullRequests.js +108 -0
  57. package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -0
  58. package/dist/plus/integrations/reads/warnings.d.ts +6 -2
  59. package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
  60. package/dist/plus/integrations/reads/warnings.js +40 -0
  61. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  62. package/docs/integrations.md +53 -4
  63. package/package.json +1 -1
  64. package/src/git/models/pullRequest.ts +50 -0
  65. package/src/plus/git-github/api/github.ts +254 -1
  66. package/src/plus/git-github/api/pullRequestSearchQuery.ts +80 -0
  67. package/src/plus/integrations/authentication/configuredIntegrationService.ts +6 -1
  68. package/src/plus/integrations/index.ts +2 -0
  69. package/src/plus/integrations/integrationService.ts +108 -70
  70. package/src/plus/integrations/manager.ts +55 -4
  71. package/src/plus/integrations/models/gitHostIntegration.ts +56 -2
  72. package/src/plus/integrations/models/integration.ts +2 -0
  73. package/src/plus/integrations/models/pullRequestReads.ts +18 -0
  74. package/src/plus/integrations/providerFilters.ts +8 -6
  75. package/src/plus/integrations/providers/github.ts +37 -1
  76. package/src/plus/integrations/providers/models.ts +37 -2
  77. package/src/plus/integrations/reads/context.ts +1 -1
  78. package/src/plus/integrations/reads/filters.ts +105 -0
  79. package/src/plus/integrations/reads/searchPullRequests.ts +210 -0
  80. 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 { PullRequest, PullRequestState, PullRequestStateFilter } from '../../../git/models/pullRequest.js';
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
- await this.ctx.storage.store('integrations:configured', configured);
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).