@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
@@ -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 { PullRequest, PullRequestShape, PullRequestStateFilter } from '../../git/models/pullRequest.js';
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;
@@ -1559,9 +1583,11 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
1559
1583
 
1560
1584
  // Persist every account when the backend advertises per-connection identity (multi-account). This
1561
1585
  // is a strict no-op for backends that return a single, id-less connection per provider.
1562
- for (const [integrationId, connections] of connectionsById) {
1563
- await this.reconcileCloudConnections(integrationId, connections, forceConnect);
1564
- }
1586
+ await Promise.all(
1587
+ Array.from(connectionsById, ([integrationId, connections]) =>
1588
+ this.reconcileCloudConnections(integrationId, connections, forceConnect),
1589
+ ),
1590
+ );
1565
1591
 
1566
1592
  this.ctx.hooks?.connection?.onConnectedChanged?.({
1567
1593
  integrationIds: [...connectedIntegrations.values()],
@@ -1612,79 +1638,91 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
1612
1638
  const existingById = new Map(
1613
1639
  this.configuredIntegrationService.getConfigured(id, { cloud: true }).map(c => [c.id, c]),
1614
1640
  );
1615
- for (const connection of identified) {
1616
- // The wire `domain` is usually a full URL, though cloud providers can return a bare host.
1617
- // Self-managed integrations are keyed/constructed by host.
1618
- const host = hostFromDomain(connection.domain);
1619
-
1620
- // Self-managed connections are keyed by host, so an unparseable/empty domain would store the
1621
- // session and descriptor under an empty host producing ambiguous keys (`connected:<id>:`) that
1622
- // break later resolution and local-disconnect checks. Skip such a connection rather than corrupt
1623
- // state; cloud providers key off their canonical domain and are unaffected.
1624
- if (isGitSelfManagedHostIntegrationId(id) && !host) {
1625
- scope?.warn(`Skipping connection '${connection.id}' for ${id}: unresolved host from domain`);
1626
- continue;
1627
- }
1641
+ const preparedConnections = await Promise.all(
1642
+ identified.map(async connection => {
1643
+ // The wire `domain` is usually a full URL, though cloud providers can return a bare host.
1644
+ // Self-managed integrations are keyed/constructed by host.
1645
+ const host = hostFromDomain(connection.domain);
1646
+
1647
+ // Self-managed connections are keyed by host, so an unparseable/empty domain would store the
1648
+ // session and descriptor under an empty host producing ambiguous keys (`connected:<id>:`) that
1649
+ // break later resolution and local-disconnect checks. Skip such a connection rather than corrupt
1650
+ // state; cloud providers key off their canonical domain and are unaffected.
1651
+ if (isGitSelfManagedHostIntegrationId(id) && !host) {
1652
+ scope?.warn(`Skipping connection '${connection.id}' for ${id}: unresolved host from domain`);
1653
+ return undefined;
1654
+ }
1628
1655
 
1629
- // Don't resurrect a connection the user disconnected locally: a host "disconnect" only clears
1630
- // local state (the backend still lists the token), so without this the next non-forced sync would
1631
- // re-store the secret/config. A forced reconnect clears this flag (in the sync loop above) before
1632
- // reconcile runs, so it proceeds normally.
1633
- if (this.isLocallyDisconnected(id, host)) continue;
1634
-
1635
- syncEligibleIds.add(connection.id);
1636
-
1637
- // On a routine (non-forced) check-in, skip the token fetch + secret write for a connection we
1638
- // already have stored and that hasn't expired: nothing to refresh, so avoid the extra GK API
1639
- // traffic and secret churn. Still treat it as synced (so it doesn't trip the prune guard) and
1640
- // record its primary below. Forced syncs, new connections, and expired tokens fall through and
1641
- // fetch as before.
1642
- const cached = existingById.get(connection.id);
1643
- if (!forceConnect && cached != null && !isDescriptorExpired(cached)) {
1644
- syncedIds.add(connection.id);
1645
- if (connection.primary) {
1646
- const domain = isGitSelfManagedHostIntegrationId(id) ? host : undefined;
1647
- if (!syncedPrimaryIdsByDomain.has(domain)) {
1648
- syncedPrimaryIdsByDomain.set(domain, connection.id);
1649
- }
1656
+ // Don't resurrect a connection the user disconnected locally: a host "disconnect" only clears
1657
+ // local state (the backend still lists the token), so without this the next non-forced sync would
1658
+ // re-store the secret/config. A forced reconnect clears this flag (in the sync loop above) before
1659
+ // reconcile runs, so it proceeds normally.
1660
+ if (this.isLocallyDisconnected(id, host)) return undefined;
1661
+
1662
+ syncEligibleIds.add(connection.id);
1663
+
1664
+ // On a routine (non-forced) check-in, skip the token fetch + secret write for a connection we
1665
+ // already have stored and that hasn't expired: nothing to refresh, so avoid the extra GK API
1666
+ // traffic and secret churn. Still treat it as synced (so it doesn't trip the prune guard) and
1667
+ // record its primary below. Forced syncs, new connections, and expired tokens fall through and
1668
+ // fetch as before.
1669
+ const cached = existingById.get(connection.id);
1670
+ if (!forceConnect && cached != null && !isDescriptorExpired(cached)) {
1671
+ return { kind: 'cached' as const, connection: connection, host: host };
1650
1672
  }
1651
- continue;
1652
- }
1653
1673
 
1654
- try {
1655
- const session = await cloudIntegrations.getConnectionSession(id, undefined, connection.id);
1656
- if (session == null) continue;
1657
-
1658
- let providerSession = toProviderSession(id, connection, session, host);
1659
-
1660
- // Resolve a human-readable account handle with the same precedence as the gk CLI:
1661
- // (1) the value the backend put on the connection, (2) a previously-resolved name cached in
1662
- // our configured store (keyed by connection id), (3) a live provider-API lookup. This keeps
1663
- // provider round-trips to the first sight of a connection; degrade to undefined on failure.
1664
- const existing = existingById.get(connection.id);
1665
- const accountName =
1666
- normalizeAccountName(connection.accountName) ??
1667
- normalizeAccountName(existing?.accountName) ??
1668
- (await this.resolveAccountName(id, host, providerSession));
1669
- if (accountName != null) {
1670
- providerSession = {
1671
- ...providerSession,
1672
- account: { ...providerSession.account, label: accountName },
1674
+ try {
1675
+ const session = await cloudIntegrations.getConnectionSession(id, undefined, connection.id);
1676
+ if (session == null) return undefined;
1677
+
1678
+ let providerSession = toProviderSession(id, connection, session, host);
1679
+
1680
+ // Resolve a human-readable account handle with the same precedence as the gk CLI:
1681
+ // (1) the value the backend put on the connection, (2) a previously-resolved name cached in
1682
+ // our configured store (keyed by connection id), (3) a live provider-API lookup. This keeps
1683
+ // provider round-trips to the first sight of a connection; degrade to undefined on failure.
1684
+ const existing = existingById.get(connection.id);
1685
+ const accountName =
1686
+ normalizeAccountName(connection.accountName) ??
1687
+ normalizeAccountName(existing?.accountName) ??
1688
+ (await this.resolveAccountName(id, host, providerSession));
1689
+ if (accountName != null) {
1690
+ providerSession = {
1691
+ ...providerSession,
1692
+ account: { ...providerSession.account, label: accountName },
1693
+ };
1694
+ }
1695
+
1696
+ return {
1697
+ kind: 'fetched' as const,
1698
+ connection: connection,
1699
+ host: host,
1700
+ providerSession: providerSession,
1673
1701
  };
1702
+ } catch (ex) {
1703
+ scope?.warn(
1704
+ `Failed to sync connection '${connection.id}' for ${id}: ${ex instanceof Error ? ex.message : String(ex)}`,
1705
+ );
1706
+ return undefined;
1674
1707
  }
1708
+ }),
1709
+ );
1675
1710
 
1676
- await this.configuredIntegrationService.storeSession(id, providerSession);
1677
- syncedIds.add(connection.id);
1678
- if (connection.primary) {
1679
- const domain = isGitSelfManagedHostIntegrationId(id) ? host : undefined;
1680
- if (!syncedPrimaryIdsByDomain.has(domain)) {
1681
- syncedPrimaryIdsByDomain.set(domain, connection.id);
1682
- }
1711
+ // The remote work above is independent, but session persistence remains ordered because storage
1712
+ // implementations may update a shared configured-connections collection with read-modify-write.
1713
+ for (const prepared of preparedConnections) {
1714
+ if (prepared == null) continue;
1715
+
1716
+ if (prepared.kind === 'fetched') {
1717
+ await this.configuredIntegrationService.storeSession(id, prepared.providerSession);
1718
+ }
1719
+
1720
+ syncedIds.add(prepared.connection.id);
1721
+ if (prepared.connection.primary) {
1722
+ const domain = isGitSelfManagedHostIntegrationId(id) ? prepared.host : undefined;
1723
+ if (!syncedPrimaryIdsByDomain.has(domain)) {
1724
+ syncedPrimaryIdsByDomain.set(domain, prepared.connection.id);
1683
1725
  }
1684
- } catch (ex) {
1685
- scope?.warn(
1686
- `Failed to sync connection '${connection.id}' for ${id}: ${ex instanceof Error ? ex.message : String(ex)}`,
1687
- );
1688
1726
  }
1689
1727
  }
1690
1728
 
@@ -1,10 +1,19 @@
1
1
  import type { IssueSearchCriteria, IssueShape } from '../../git/models/issue.js';
2
- import type { PullRequestShape, PullRequestStateFilter } from '../../git/models/pullRequest.js';
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 { IssueFilter, IssueSearchCapabilities, PullRequestFilter } from './providerFilters.js';
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`, `listIssuesPage`, `listIssueTrackerIssuesPage`,
146
- * `broadenIssues`) takes both `page` and `cursor`, and the two are NOT interchangeable:
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 { PullRequest, PullRequestMergeMethod, PullRequestStateFilter } from '../../../git/models/pullRequest.js';
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 { IntegrationResult, IntegrationType, ProviderIssueSearchPage } from './integration.js';
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,
@@ -42,6 +42,7 @@ export type IntegrationType = 'git' | 'issues';
42
42
  // The issue-read contracts live in their own module (pure data, and their relationship to each other is the
43
43
  // point of reading them together); re-exported here so the providers that implement these reads keep one import.
44
44
  export type { AccountWideIssuesResult, ProviderIssueSearchPage, SearchMyIssuesOptions } from './issueReads.js';
45
+ export type { ProviderPullRequestSearchPage } from './pullRequestReads.js';
45
46
 
46
47
  export type IntegrationKey<T extends IntegrationIds = IntegrationIds> = T extends
47
48
  | GitCloudHostIntegrationId
@@ -82,6 +83,7 @@ type SyncReqUsecase = Exclude<
82
83
  | 'countIssues'
83
84
  | 'mergePullRequest'
84
85
  | 'searchIssuesPage'
86
+ | 'searchPullRequestsPage'
85
87
  | 'searchMyIssues'
86
88
  | 'searchMyPullRequests'
87
89
  | 'searchPullRequests',
@@ -0,0 +1,18 @@
1
+ import type { PullRequestShape } from '../../../git/models/pullRequest.js';
2
+
3
+ /**
4
+ * One provider page of the filtered pull-request search, before the facade turns it into a neutral
5
+ * paged result with warnings.
6
+ *
7
+ * `totalCount` is the provider-reported pre-ceiling facet count, not the number of rows that remain reachable
8
+ * after a provider limit. It stays optional because absence means "not reported", never zero. Together with
9
+ * `truncated` it lets the facade distinguish GitHub's quantified ceiling from another unusable continuation.
10
+ */
11
+ export type ProviderPullRequestSearchPage = {
12
+ values: PullRequestShape[];
13
+ truncated: boolean;
14
+ cursor?: string;
15
+ hasMore: boolean;
16
+ page: number;
17
+ totalCount?: number;
18
+ };
@@ -1,9 +1,4 @@
1
- export enum PullRequestFilter {
2
- Author = 'author',
3
- Assignee = 'assignee',
4
- ReviewRequested = 'review-requested',
5
- Mention = 'mention',
6
- }
1
+ export { PullRequestFilter } from '../../git/models/pullRequest.js';
7
2
 
8
3
  export enum IssueFilter {
9
4
  Author = 'author',
@@ -25,3 +20,10 @@ export type {
25
20
  IssueSearchCriteria,
26
21
  IssueSearchRelationship,
27
22
  } from '../../git/models/issue.js';
23
+
24
+ /**
25
+ * The filtered pull-request search criteria and its capability table. These live in `@gitkraken/core-gitlens/git` for the
26
+ * same dependency-direction reason as the issue-search model above: provider API clients translate them below
27
+ * this package and cannot import this facade.
28
+ */
29
+ export type { PullRequestSearchCapabilities, PullRequestSearchCriteria } from '../../git/models/pullRequest.js';
@@ -5,6 +5,7 @@ import type { IssueOrPullRequest } from '../../../git/models/issueOrPullRequest.
5
5
  import type {
6
6
  PullRequest,
7
7
  PullRequestMergeMethod,
8
+ PullRequestSearchCriteria,
8
9
  PullRequestState,
9
10
  PullRequestStateFilter,
10
11
  } from '../../../git/models/pullRequest.js';
@@ -25,7 +26,11 @@ import { IntegrationReadUnavailableError } from '../errors.js';
25
26
  import type { IntegrationConnectionChangeEvent } from '../integrationService.js';
26
27
  import type { SearchMyPullRequestsOptions } from '../models/gitHostIntegration.js';
27
28
  import { GitHostIntegration } from '../models/gitHostIntegration.js';
28
- import type { ProviderIssueSearchPage, SearchMyIssuesOptions } from '../models/integration.js';
29
+ import type {
30
+ ProviderIssueSearchPage,
31
+ ProviderPullRequestSearchPage,
32
+ SearchMyIssuesOptions,
33
+ } from '../models/integration.js';
29
34
  import type { GitHubIntegrationIds } from './github/github.utils.js';
30
35
  import { getGitHubPullRequestIdentityFromMaybeUrl } from './github/github.utils.js';
31
36
  import type {
@@ -625,6 +630,37 @@ abstract class GitHubIntegrationBase<ID extends GitHubIntegrationIds> extends Gi
625
630
  );
626
631
  }
627
632
 
633
+ /**
634
+ * Filtered pull-request search over a repository/organization or explicit current-user relationship scope. The
635
+ * API client owns criteria sanitizing, facet unioning, ordering, and the composite cursor that carries GitHub's
636
+ * result-ceiling signal across pages.
637
+ */
638
+ protected override async searchProviderPullRequestsPage(
639
+ session: ProviderAuthenticationSession,
640
+ options: {
641
+ repos?: ProviderRepoInput[];
642
+ org?: string;
643
+ criteria?: PullRequestSearchCriteria;
644
+ cursor?: string;
645
+ pageSize?: number;
646
+ },
647
+ cancellation?: AbortSignal,
648
+ ): Promise<ProviderPullRequestSearchPage | undefined> {
649
+ return (await this.authenticationService.apis.github)?.searchPullRequestsPage(
650
+ this,
651
+ toTokenWithInfo(this.id, session),
652
+ {
653
+ repos: options.repos?.map(r => `${r.namespace}/${r.name}`),
654
+ org: options.org,
655
+ criteria: options.criteria,
656
+ baseUrl: this.apiBaseUrl,
657
+ cursor: options.cursor,
658
+ pageSize: options.pageSize,
659
+ },
660
+ cancellation,
661
+ );
662
+ }
663
+
628
664
  /**
629
665
  * The filtered issue search: one GraphQL request per page, no forced relationship to the current user, and no
630
666
  * route through the SDK's repo-scoped read (whose over-limit recovery walk can spend up to 128 requests).
@@ -75,11 +75,17 @@ import {
75
75
  IssuesCloudHostIntegrationId,
76
76
  } from '../constants.js';
77
77
  import type { Integration, IntegrationType } from '../models/integration.js';
78
- import type { IssueSearchCapabilities } from '../providerFilters.js';
78
+ import type { IssueSearchCapabilities, PullRequestSearchCapabilities } from '../providerFilters.js';
79
79
  import { IssueFilter, PullRequestFilter } from '../providerFilters.js';
80
80
 
81
81
  export { IssueFilter, PullRequestFilter } from '../providerFilters.js';
82
- export type { IssueSearchCapabilities, IssueSearchCriteria, IssueSearchRelationship } from '../providerFilters.js';
82
+ export type {
83
+ IssueSearchCapabilities,
84
+ IssueSearchCriteria,
85
+ IssueSearchRelationship,
86
+ PullRequestSearchCapabilities,
87
+ PullRequestSearchCriteria,
88
+ } from '../providerFilters.js';
83
89
  import type { ProviderRepositoryShape } from '../results.js';
84
90
 
85
91
  export type { ProviderOrganization, ProviderRepositoryShape } from '../results.js';
@@ -696,6 +702,16 @@ export interface ProviderMetadata {
696
702
  * This is independent from `supportedPullRequestFilters`, which describes repo-scoped reads.
697
703
  */
698
704
  supportedAccountWidePullRequestFilters?: PullRequestFilter[];
705
+ /**
706
+ * What the filtered pull-request search can express server-side. Absent means the provider has
707
+ * no such search and the facade refuses it rather than returning an unfiltered page.
708
+ */
709
+ supportedPullRequestSearch?: PullRequestSearchCapabilities;
710
+ /**
711
+ * Maximum results the provider will serve for one pull-request search facet. Matches beyond it are unreachable
712
+ * and are reported as a succeeded omission carrying this limit plus the provider's pre-ceiling facet count.
713
+ */
714
+ pullRequestSearchResultLimit?: number;
699
715
  /** Filters the REPO-scoped issue read can express. */
700
716
  supportedIssueFilters?: IssueFilter[];
701
717
  /**
@@ -760,6 +776,21 @@ const githubIssueSearchCapabilities: IssueSearchCapabilities = {
760
776
  states: true,
761
777
  };
762
778
 
779
+ /** GitHub and GHE use the same filtered pull-request search syntax and result ceiling. */
780
+ const githubPullRequestSearchCapabilities: PullRequestSearchCapabilities = {
781
+ relationships: [
782
+ PullRequestFilter.Author,
783
+ PullRequestFilter.Assignee,
784
+ PullRequestFilter.ReviewRequested,
785
+ PullRequestFilter.Mention,
786
+ ],
787
+ states: ['open', 'closed', 'merged', 'all'],
788
+ text: true,
789
+ includeArchived: true,
790
+ repositoryScope: true,
791
+ organizationScope: true,
792
+ };
793
+
763
794
  export const providersMetadata: ProvidersMetadata = {
764
795
  [GitCloudHostIntegrationId.GitHub]: {
765
796
  domain: 'github.com',
@@ -782,6 +813,8 @@ export const providersMetadata: ProvidersMetadata = {
782
813
  PullRequestFilter.ReviewRequested,
783
814
  PullRequestFilter.Mention,
784
815
  ],
816
+ supportedPullRequestSearch: githubPullRequestSearchCapabilities,
817
+ pullRequestSearchResultLimit: githubSearchResultLimit,
785
818
  // Use 'username' property on account for issue filters
786
819
  supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
787
820
  // The account-wide read is three independent searches (`author:@me`, `assignee:@me`, `mentions:@me`) behind
@@ -812,6 +845,8 @@ export const providersMetadata: ProvidersMetadata = {
812
845
  PullRequestFilter.ReviewRequested,
813
846
  PullRequestFilter.Mention,
814
847
  ],
848
+ supportedPullRequestSearch: githubPullRequestSearchCapabilities,
849
+ pullRequestSearchResultLimit: githubSearchResultLimit,
815
850
  // Use 'username' property on account for issue filters
816
851
  supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
817
852
  // The account-wide read is three independent searches (`author:@me`, `assignee:@me`, `mentions:@me`) behind
@@ -7,7 +7,7 @@ import type { ProviderWarning } from '../results.js';
7
7
  /**
8
8
  * Everything the facade's provider reads need from the service that owns connection state — and nothing else.
9
9
  *
10
- * The reads (`listOrgs`, `listRepos`, the paged PR/issue reads, the sweeps, `broadenIssues`,
10
+ * The reads (`listOrgs`, `listRepos`, the paged PR/issue reads and searches, the sweeps, `broadenIssues`,
11
11
  * `resolveRepository`) are otherwise stateless: given a resolved integration they only transform provider
12
12
  * results into the neutral shapes consumers get. What they can't do themselves is decide WHICH integration
13
13
  * instance and which host a request addresses, because that depends on the configured connections, the