@gitkraken/core-gitlens 0.7.0 → 0.7.2

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 (125) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/dist/git/remotes/azure-devops.js +3 -3
  3. package/dist/git/remotes/azure-devops.js.map +1 -1
  4. package/dist/plus/integrations/authentication/configuredIntegrationService.js +9 -2
  5. package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
  6. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
  7. package/dist/plus/integrations/collectionMetadata.d.ts +48 -3
  8. package/dist/plus/integrations/collectionMetadata.js +138 -8
  9. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  10. package/dist/plus/integrations/errors.d.ts +3 -1
  11. package/dist/plus/integrations/errors.js +7 -3
  12. package/dist/plus/integrations/errors.js.map +1 -1
  13. package/dist/plus/integrations/index.d.ts +3 -3
  14. package/dist/plus/integrations/index.js.map +1 -1
  15. package/dist/plus/integrations/integrationService.d.ts +35 -1
  16. package/dist/plus/integrations/integrationService.js +90 -43
  17. package/dist/plus/integrations/integrationService.js.map +1 -1
  18. package/dist/plus/integrations/lite.js +5 -4
  19. package/dist/plus/integrations/lite.js.map +1 -1
  20. package/dist/plus/integrations/manager.d.ts +68 -23
  21. package/dist/plus/integrations/models/gitHostIntegration.d.ts +31 -15
  22. package/dist/plus/integrations/models/gitHostIntegration.js +71 -22
  23. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  24. package/dist/plus/integrations/models/integration.d.ts +70 -12
  25. package/dist/plus/integrations/models/integration.js +56 -1
  26. package/dist/plus/integrations/models/integration.js.map +1 -1
  27. package/dist/plus/integrations/models/issueReads.d.ts +17 -0
  28. package/dist/plus/integrations/models/issuesIntegration.js +2 -0
  29. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  30. package/dist/plus/integrations/models/pullRequestReads.d.ts +26 -0
  31. package/dist/plus/integrations/providers/azure/azure.d.ts +66 -1
  32. package/dist/plus/integrations/providers/azure/azure.js +192 -12
  33. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  34. package/dist/plus/integrations/providers/azure/search.d.ts +144 -0
  35. package/dist/plus/integrations/providers/azure/search.js +250 -0
  36. package/dist/plus/integrations/providers/azure/search.js.map +1 -0
  37. package/dist/plus/integrations/providers/azureDevOps.d.ts +73 -36
  38. package/dist/plus/integrations/providers/azureDevOps.js +191 -122
  39. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  40. package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +176 -0
  41. package/dist/plus/integrations/providers/azureDevOpsServer.js +560 -0
  42. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -0
  43. package/dist/plus/integrations/providers/bitbucket-server/discovery.d.ts +12 -0
  44. package/dist/plus/integrations/providers/bitbucket-server/discovery.js +128 -0
  45. package/dist/plus/integrations/providers/bitbucket-server/discovery.js.map +1 -0
  46. package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +10 -1
  47. package/dist/plus/integrations/providers/bitbucket-server/models.js +3 -2
  48. package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
  49. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +58 -0
  50. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +485 -0
  51. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -0
  52. package/dist/plus/integrations/providers/bitbucket-server.d.ts +61 -3
  53. package/dist/plus/integrations/providers/bitbucket-server.js +114 -20
  54. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
  55. package/dist/plus/integrations/providers/bitbucket.d.ts +9 -1
  56. package/dist/plus/integrations/providers/bitbucket.js +13 -2
  57. package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
  58. package/dist/plus/integrations/providers/github.d.ts +6 -6
  59. package/dist/plus/integrations/providers/github.js +35 -34
  60. package/dist/plus/integrations/providers/github.js.map +1 -1
  61. package/dist/plus/integrations/providers/gitlab.d.ts +6 -4
  62. package/dist/plus/integrations/providers/gitlab.js +32 -28
  63. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  64. package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
  65. package/dist/plus/integrations/providers/jira-server.d.ts +9 -0
  66. package/dist/plus/integrations/providers/jira-server.js +15 -7
  67. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  68. package/dist/plus/integrations/providers/jira.d.ts +9 -0
  69. package/dist/plus/integrations/providers/jira.js +14 -2
  70. package/dist/plus/integrations/providers/jira.js.map +1 -1
  71. package/dist/plus/integrations/providers/linear.js +2 -2
  72. package/dist/plus/integrations/providers/models.d.ts +10 -1
  73. package/dist/plus/integrations/providers/models.js +86 -0
  74. package/dist/plus/integrations/providers/models.js.map +1 -1
  75. package/dist/plus/integrations/providers/providersApi.d.ts +10 -0
  76. package/dist/plus/integrations/providers/providersApi.js +18 -3
  77. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  78. package/dist/plus/integrations/providers/utils.d.ts +12 -1
  79. package/dist/plus/integrations/providers/utils.js +36 -6
  80. package/dist/plus/integrations/providers/utils.js.map +1 -1
  81. package/dist/plus/integrations/reads/broaden.js +6 -6
  82. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  83. package/dist/plus/integrations/reads/context.d.ts +1 -1
  84. package/dist/plus/integrations/reads/counts.d.ts +18 -4
  85. package/dist/plus/integrations/reads/counts.js +66 -15
  86. package/dist/plus/integrations/reads/counts.js.map +1 -1
  87. package/dist/plus/integrations/reads/cursors.js +1 -1
  88. package/dist/plus/integrations/reads/drains.d.ts +2 -1
  89. package/dist/plus/integrations/reads/drains.js +1 -1
  90. package/dist/plus/integrations/reads/filters.d.ts +1 -1
  91. package/dist/plus/integrations/reads/filters.js +17 -12
  92. package/dist/plus/integrations/reads/filters.js.map +1 -1
  93. package/dist/plus/integrations/reads/hierarchy.js +2 -2
  94. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  95. package/dist/plus/integrations/reads/hierarchy.utils.js +1 -1
  96. package/dist/plus/integrations/reads/issueBatch.js +1 -1
  97. package/dist/plus/integrations/reads/issueTracker.js +2 -2
  98. package/dist/plus/integrations/reads/issues.js +2 -2
  99. package/dist/plus/integrations/reads/paging.js +1 -1
  100. package/dist/plus/integrations/reads/resolveRepository.d.ts +12 -11
  101. package/dist/plus/integrations/reads/resolveRepository.js +110 -19
  102. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
  103. package/dist/plus/integrations/reads/searchIssues.js +4 -2
  104. package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
  105. package/dist/plus/integrations/reads/searchPullRequests.js +9 -3
  106. package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
  107. package/dist/plus/integrations/reads/sweeps.js +1 -1
  108. package/dist/plus/integrations/reads/trackerIssue.d.ts +9 -5
  109. package/dist/plus/integrations/reads/trackerIssue.js +15 -9
  110. package/dist/plus/integrations/reads/trackerIssue.js.map +1 -1
  111. package/dist/plus/integrations/reads/warnings.d.ts +6 -2
  112. package/dist/plus/integrations/reads/warnings.js +15 -19
  113. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  114. package/dist/plus/integrations/results.d.ts +84 -6
  115. package/dist/plus/integrations/results.js +9 -2
  116. package/dist/plus/integrations/results.js.map +1 -1
  117. package/dist/plus/integrations/utils/domain.utils.d.ts +19 -0
  118. package/dist/plus/integrations/utils/domain.utils.js +36 -1
  119. package/dist/plus/integrations/utils/domain.utils.js.map +1 -1
  120. package/dist/plus/integrations/utils/integration.utils.d.ts +1 -0
  121. package/dist/plus/integrations/utils/integration.utils.js +12 -1
  122. package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
  123. package/docs/integrations.md +263 -74
  124. package/docs/kepler-read-api-parity.md +19 -6
  125. package/package.json +1 -1
@@ -1,11 +1,11 @@
1
1
  import { AuthenticationError, RequestNotFoundError } from "../../../git/errors.js";
2
2
  import { parseGitRemoteUrl } from "../../../git/utils/remote.utils.js";
3
3
  import { createRemoteProviderMatcher } from "../../../git/remotes/matcher.js";
4
- import { toProviderWarning } from "../results.js";
5
4
  import "../constants.js";
6
- import { getIntegrationIdForRemote, isGitCloudHostIntegrationId, isGitSelfManagedHostIntegrationId, remoteProviderTypeForConfig, remoteProviderTypeForIntegration } from "../utils/integration.utils.js";
5
+ import { toProviderWarning } from "../results.js";
6
+ import { decodePathSegment, getRemoteHostMatcher, hostFromDomain, isWebRemoteScheme } from "../utils/domain.utils.js";
7
+ import { getIntegrationIdForRemote, getSelfManagedBaseUrl, isGitCloudHostIntegrationId, isGitSelfManagedHostIntegrationId, remoteProviderTypeForConfig, remoteProviderTypeForIntegration } from "../utils/integration.utils.js";
7
8
  import { isAzureCloudDomain, isBitbucketCloudDomain, isGitHubDotCom, isGitLabDotCom } from "../providers/models.js";
8
- import { hostFromDomain } from "../utils/domain.utils.js";
9
9
  import { noConnectionWarning } from "./warnings.js";
10
10
  import { isIssuesIntegration } from "../models/issuesIntegration.js";
11
11
  //#region ../plus/integrations/src/reads/resolveRepository.ts
@@ -21,9 +21,10 @@ import { isIssuesIntegration } from "../models/issuesIntegration.js";
21
21
  async function resolveRepository(ctx, options) {
22
22
  const result = (status) => ({ resolution: { status } });
23
23
  const [scheme, parsedDomain, path] = parseGitRemoteUrl(options.remoteUrl);
24
+ const hostsMatch = getRemoteHostMatcher(scheme);
24
25
  const parsedHost = hostFromDomain(parsedDomain);
25
26
  const explicitHost = hostFromDomain(options.host);
26
- if (parsedHost != null && explicitHost != null && parsedHost !== explicitHost) return result("host-mismatch");
27
+ if (parsedHost != null && explicitHost != null && !hostsMatch(parsedHost, explicitHost)) return result("host-mismatch");
27
28
  const matcherDomain = parsedDomain || options.host || "";
28
29
  if (options.providerId != null && isGitCloudHostIntegrationId(options.providerId)) {
29
30
  const nativeProvider = createRemoteProviderMatcher([])(options.remoteUrl, matcherDomain, path, scheme);
@@ -32,36 +33,55 @@ async function resolveRepository(ctx, options) {
32
33
  const canonicalHost = options.providerId === "github" ? isGitHubDotCom(nativeDomain) : options.providerId === "gitlab" ? isGitLabDotCom(nativeDomain) : options.providerId === "bitbucket" ? isBitbucketCloudDomain(nativeDomain) : isAzureCloudDomain(nativeDomain);
33
34
  if (nativeId !== options.providerId || !canonicalHost) return result("host-mismatch");
34
35
  }
35
- const provider = createRemoteProviderMatcher(buildMatcherConfigs(ctx, options, parsedDomain))(options.remoteUrl, matcherDomain, path, scheme);
36
+ let provider = createRemoteProviderMatcher(buildMatcherConfigs(ctx, options, parsedDomain))(options.remoteUrl, matcherDomain, path, scheme);
36
37
  if (provider == null) return result("invalid-remote-url");
37
38
  let id = options.providerId ?? getIntegrationIdForRemote(provider);
38
39
  if (id == null && provider.id === "azure-devops" && provider.custom) id = "azure-devops-server";
39
40
  if (id == null) return result("unsupported-provider");
40
- const owner = provider.owner;
41
- const name = provider.repoName;
42
- if (owner == null || name == null) return result("invalid-remote-url");
43
- const providerRepo = provider.repoDesc;
44
41
  const urlHost = hostFromDomain(provider.domain);
45
42
  let trustedHost = options.connectionId != null || options.domain != null ? hostFromDomain(ctx.resolveDomainForRead(id, options.connectionId, options.domain)) : void 0;
43
+ let connectionId = options.connectionId;
44
+ let providerRepo;
46
45
  if (isGitSelfManagedHostIntegrationId(id)) {
47
- const configuredHosts = ctx.getConfigured(id).map((c) => hostFromDomain(c.domain));
48
- if (trustedHost == null && urlHost != null) trustedHost = configuredHosts.find((host) => host === urlHost);
46
+ if (path.split("/").some((segment) => /^(?:\.|%2e){1,2}$/i.test(segment) || /%2f|%5c|\\/i.test(segment))) return result("invalid-remote-url");
47
+ const configured = ctx.getConfigured(id);
48
+ const configuredHosts = configured.map((c) => hostFromDomain(c.domain));
49
+ if (trustedHost == null && urlHost != null) {
50
+ const matchingHosts = new Set(configuredHosts.filter((host) => hostsMatch(host, urlHost)));
51
+ if (matchingHosts.size > 1) return result("host-mismatch");
52
+ trustedHost = matchingHosts.values().next().value;
53
+ }
49
54
  if (trustedHost == null && configuredHosts.length !== 0) return result("host-mismatch");
50
- if (trustedHost != null && urlHost != null && trustedHost !== urlHost) return result("host-mismatch");
55
+ if (trustedHost != null && urlHost != null && !hostsMatch(trustedHost, urlHost)) return result("host-mismatch");
56
+ const installation = matchInstallation(ctx, options, id, configured.filter((c) => hostFromDomain(c.domain) === trustedHost), {
57
+ scheme,
58
+ domain: parsedDomain,
59
+ path
60
+ });
61
+ if (typeof installation === "string") return result(installation);
62
+ if (installation != null) {
63
+ provider = installation.provider;
64
+ connectionId = installation.connectionId;
65
+ providerRepo = installation.repoDesc;
66
+ }
51
67
  }
68
+ const owner = provider.owner;
69
+ const name = provider.repoName;
70
+ if (owner == null || name == null) return result("invalid-remote-url");
71
+ providerRepo ??= provider.repoDesc;
52
72
  let integration;
53
73
  try {
54
- integration = isGitSelfManagedHostIntegrationId(id) ? trustedHost != null ? await ctx.getIntegrationForRead(id, options.connectionId, trustedHost) : void 0 : await ctx.getIntegrationForDomain(id, provider.domain);
74
+ integration = isGitSelfManagedHostIntegrationId(id) ? trustedHost != null ? await ctx.getIntegrationForRead(id, connectionId, trustedHost) : void 0 : await ctx.getIntegrationForDomain(id, provider.domain);
55
75
  } catch {
56
76
  integration = void 0;
57
77
  }
58
78
  if (integration == null) return { resolution: {
59
79
  status: "unauthorized",
60
- warning: noConnectionWarning(id, provider.domain, options.connectionId)
80
+ warning: noConnectionWarning(id, provider.domain, connectionId)
61
81
  } };
62
82
  if (isIssuesIntegration(integration) || integration.getRepoInfo == null) return result("unsupported-provider");
63
83
  const project = provider.id === "azure-devops" ? provider.providerDesc?.repoDomain : void 0;
64
- const domain = provider.domain;
84
+ const domain = trustedHost ?? provider.domain;
65
85
  if (provider.id === "azure-devops" && project == null) return result("invalid-remote-url");
66
86
  try {
67
87
  const repo = await integration.getRepoInfo({
@@ -69,11 +89,11 @@ async function resolveRepository(ctx, options) {
69
89
  owner,
70
90
  name,
71
91
  project,
72
- connectionId: options.connectionId
92
+ connectionId
73
93
  });
74
94
  if (repo == null) return { resolution: {
75
95
  status: "unauthorized",
76
- warning: noConnectionWarning(id, domain, options.connectionId)
96
+ warning: noConnectionWarning(id, domain, connectionId)
77
97
  } };
78
98
  const canonicalOwner = repo.namespace || owner;
79
99
  const canonicalName = repo.name || name;
@@ -95,16 +115,87 @@ async function resolveRepository(ctx, options) {
95
115
  if (ex instanceof RequestNotFoundError) resolution = { status: "not-found" };
96
116
  else if (ex instanceof AuthenticationError) resolution = {
97
117
  status: "unauthorized",
98
- warning: toProviderWarning(id, domain, options.connectionId, ex)
118
+ warning: toProviderWarning(id, domain, connectionId, ex)
99
119
  };
100
120
  else resolution = {
101
121
  status: "undetermined",
102
- warning: toProviderWarning(id, domain, options.connectionId, ex)
122
+ warning: toProviderWarning(id, domain, connectionId, ex)
103
123
  };
104
124
  return { resolution };
105
125
  }
106
126
  }
107
127
  /**
128
+ * Narrows a web remote on a self-managed host to the configured installation it lives under: the pinned
129
+ * connection's or, unpinned, the one whose installation path is the longest prefix of the remote's, so two
130
+ * installations behind one host each resolve with their own credentials. The provider is re-matched against the
131
+ * path relative to that installation, because the remote parsers expect the repository path to start at the
132
+ * host root. An SSH remote carries no installation path, so it is left to the host-level checks.
133
+ */
134
+ function matchInstallation(ctx, options, id, connections, remote) {
135
+ if (!isWebRemoteScheme(remote.scheme)) return void 0;
136
+ const installationPath = (connection) => {
137
+ const baseUrl = getSelfManagedBaseUrl(id, connection != null ? connection.baseUrl ?? connection.domain : options.domain);
138
+ return baseUrl != null ? new URL(baseUrl).pathname.replace(/\/+$/, "") : void 0;
139
+ };
140
+ const defaultConnection = connections.find((c) => c.primary) ?? connections[0];
141
+ const cloudConnections = connections.filter((c) => c.cloud);
142
+ const cloudDefault = cloudConnections.find((c) => c.primary) ?? cloudConnections[0];
143
+ const isDefaultReadable = defaultConnection?.cloud !== false || cloudDefault == null || installationPath(cloudDefault) === installationPath(defaultConnection);
144
+ const isReadable = (connection) => options.connectionId != null || (connection === defaultConnection ? isDefaultReadable : connection?.cloud === true && hostFromDomain(connection.id) !== connection.domain);
145
+ const candidates = options.connectionId != null ? connections.filter((c) => c.id === options.connectionId) : [defaultConnection, ...connections.filter((c) => c !== defaultConnection)];
146
+ if (!candidates.length) candidates.push(void 0);
147
+ const remotePath = `/${remote.path}`;
148
+ const remoteSegments = remote.path.split("/");
149
+ const relativeTo = (basePath) => {
150
+ if (id !== "azure-devops-server") return remotePath.startsWith(`${basePath}/`) ? remotePath.slice(basePath.length + 1) : void 0;
151
+ const baseSegments = basePath.split("/").filter(Boolean);
152
+ return remoteSegments.length > baseSegments.length && baseSegments.every((segment, i) => decodePathSegment(segment).toLowerCase() === decodePathSegment(remoteSegments[i]).toLowerCase()) ? remoteSegments.slice(baseSegments.length).join("/") : void 0;
153
+ };
154
+ let match;
155
+ let checked = false;
156
+ for (const connection of candidates) {
157
+ const basePath = installationPath(connection);
158
+ if (basePath == null) continue;
159
+ checked = true;
160
+ if (relativeTo(basePath) == null) continue;
161
+ if (match == null || basePath.length > match.basePath.length || basePath.length === match.basePath.length && !isReadable(match.connection) && isReadable(connection)) match = {
162
+ connection,
163
+ basePath
164
+ };
165
+ }
166
+ if (match == null) return checked ? "host-mismatch" : void 0;
167
+ if (!isReadable(match.connection)) return "host-mismatch";
168
+ const connectionId = options.connectionId ?? (match.connection !== defaultConnection ? match.connection?.id : void 0);
169
+ const matcher = createRemoteProviderMatcher(buildMatcherConfigs(ctx, {
170
+ ...options,
171
+ providerId: id
172
+ }, remote.domain));
173
+ const rematch = (path) => matcher(options.remoteUrl, remote.domain, relativeTo(path) ?? "", remote.scheme);
174
+ let basePath = match.basePath;
175
+ let provider = rematch(basePath);
176
+ if (id !== "azure-devops-server") return provider != null ? {
177
+ provider,
178
+ connectionId,
179
+ repoDesc: provider.repoDesc
180
+ } : "invalid-remote-url";
181
+ if (basePath && provider?.providerDesc?.repoDomain == null) {
182
+ basePath = basePath.slice(0, basePath.lastIndexOf("/"));
183
+ provider = rematch(basePath);
184
+ }
185
+ if (provider == null) return "invalid-remote-url";
186
+ const repoDesc = provider.repoDesc;
187
+ const relative = typeof repoDesc.virtualDirectory === "string" ? repoDesc.virtualDirectory : void 0;
188
+ const virtualDirectory = [...basePath.split("/").filter(Boolean).map(decodePathSegment), ...relative?.split("/") ?? []].join("/") || void 0;
189
+ return {
190
+ provider,
191
+ connectionId,
192
+ repoDesc: {
193
+ ...repoDesc,
194
+ virtualDirectory
195
+ }
196
+ };
197
+ }
198
+ /**
108
199
  * The remote-matcher configs for this resolution: the user's own host remote configs (self-managed/custom
109
200
  * domains), plus a synthetic exact-domain entry for an explicit `providerId` + host so a custom domain still maps
110
201
  * to the right provider for path parsing.
@@ -1 +1 @@
1
- {"version":3,"file":"resolveRepository.js","names":[],"sources":["../../../../../plus/integrations/src/reads/resolveRepository.ts"],"sourcesContent":["import type { RemoteProviderConfig } from '@gitlens/git/remotes/matcher.js';\nimport { createRemoteProviderMatcher } from '@gitlens/git/remotes/matcher.js';\nimport { parseGitRemoteUrl } from '@gitlens/git/utils/remote.utils.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';\nimport { AuthenticationError, RequestNotFoundError } from '../errors.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { isAzureCloudDomain, isBitbucketCloudDomain, isGitHubDotCom, isGitLabDotCom } from '../providers/models.js';\nimport type { RepositoryIdentity, RepositoryResolution, ResolveRepositoryResult } from '../results.js';\nimport { toProviderWarning } from '../results.js';\nimport { hostFromDomain } from '../utils/domain.utils.js';\nimport {\n\tgetIntegrationIdForRemote,\n\tisGitCloudHostIntegrationId,\n\tisGitSelfManagedHostIntegrationId,\n\tremoteProviderTypeForConfig,\n\tremoteProviderTypeForIntegration,\n} from '../utils/integration.utils.js';\nimport type { RepositoryResolutionContext } from './context.js';\nimport { noConnectionWarning } from './warnings.js';\n\n/**\n * Resolution of a remote URL to a provider repository identity — the facade's equivalent of `gk repo resolve`.\n *\n * Unlike every other read here, this one's input is REPOSITORY data, so most of the code below is about not\n * letting that data choose the credentials: an explicit cloud `providerId` must agree with the remote's canonical\n * host, and a self-managed host resolves only against a domain the user already authenticated. The\n * `host-mismatch` returns are those guards, not edge cases — resolving `owner/repo` against the wrong account\n * would hand back a confidently wrong identity and seed the domain-keyed instance cache from remote data.\n */\nexport async function resolveRepository(\n\tctx: RepositoryResolutionContext,\n\toptions: {\n\t\tproviderId?: IntegrationIds;\n\t\tremoteUrl: string;\n\t\thost?: string;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain used to select the integration instance. Used only when the requested\n\t\t * connection has no configured domain; it must come from the trusted authentication configuration, not\n\t\t * repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ResolveRepositoryResult> {\n\tconst result = (status: RepositoryResolution['status']): ResolveRepositoryResult => ({\n\t\tresolution: { status: status },\n\t});\n\n\tconst [scheme, parsedDomain, path] = parseGitRemoteUrl(options.remoteUrl);\n\tconst parsedHost = hostFromDomain(parsedDomain);\n\tconst explicitHost = hostFromDomain(options.host);\n\tif (parsedHost != null && explicitHost != null && parsedHost !== explicitHost) {\n\t\treturn result('host-mismatch');\n\t}\n\n\tconst matcherDomain = parsedDomain || options.host || '';\n\n\t// An explicit cloud provider must agree with the remote's canonical cloud host. Without this guard the\n\t// synthetic matcher below can reinterpret, for example, a gitlab.com URL as GitHub and then resolve a\n\t// homonymous owner/repo through the wrong account. Match once without caller configs/synthetic entries;\n\t// Azure's matcher also normalizes ssh.dev.azure.com and vs-ssh.visualstudio.com to their web host.\n\tif (options.providerId != null && isGitCloudHostIntegrationId(options.providerId)) {\n\t\tconst nativeProvider = createRemoteProviderMatcher([])(options.remoteUrl, matcherDomain, path, scheme);\n\t\tconst nativeId = nativeProvider != null ? getIntegrationIdForRemote(nativeProvider) : undefined;\n\t\tconst nativeDomain = nativeProvider?.domain ?? matcherDomain;\n\t\tconst canonicalHost =\n\t\t\toptions.providerId === GitCloudHostIntegrationId.GitHub\n\t\t\t\t? isGitHubDotCom(nativeDomain)\n\t\t\t\t: options.providerId === GitCloudHostIntegrationId.GitLab\n\t\t\t\t\t? isGitLabDotCom(nativeDomain)\n\t\t\t\t\t: options.providerId === GitCloudHostIntegrationId.Bitbucket\n\t\t\t\t\t\t? isBitbucketCloudDomain(nativeDomain)\n\t\t\t\t\t\t: isAzureCloudDomain(nativeDomain);\n\t\tif (nativeId !== options.providerId || !canonicalHost) return result('host-mismatch');\n\t}\n\n\tconst provider = createRemoteProviderMatcher(buildMatcherConfigs(ctx, options, parsedDomain))(\n\t\toptions.remoteUrl,\n\t\tmatcherDomain,\n\t\tpath,\n\t\tscheme,\n\t);\n\tif (provider == null) return result('invalid-remote-url');\n\n\tlet id = options.providerId ?? getIntegrationIdForRemote(provider);\n\t// Custom Azure DevOps Server domains matched via getRemoteConfigs return undefined from\n\t// getIntegrationIdForRemote because the provider is marked custom; map them to the server id so the\n\t// project-scoped lookup uses that host's configured connection and API base URL.\n\tif (id == null && provider.id === 'azure-devops' && provider.custom) {\n\t\tid = GitSelfManagedHostIntegrationId.AzureDevOpsServer;\n\t}\n\tif (id == null) return result('unsupported-provider');\n\n\tconst owner = provider.owner;\n\tconst name = provider.repoName;\n\tif (owner == null || name == null) return result('invalid-remote-url');\n\n\tconst providerRepo = provider.repoDesc;\n\n\t// On a self-managed host, resolve only against a TRUSTED host: the pinned connection's configured\n\t// domain, the explicit `domain`, or — when neither was supplied — a configured host matching the\n\t// remote's. That last case keeps `remoteUrl` (repository-supplied) out of the trusted path: it selects\n\t// among hosts the user already authenticated and can never introduce a new one. Resolving `owner/repo`\n\t// against some other host's account would, if that host has the same owner/repo, return a confidently\n\t// wrong identity — and would seed the domain-keyed integration cache (never evicted before dispose) with\n\t// an entry derived from repository data.\n\tconst urlHost = hostFromDomain(provider.domain);\n\t// Normalize BOTH sides before comparing: a stored connection domain is usually a full URL\n\t// (`https://git.example.com`) while `urlHost` is already a bare host, so a raw compare would fail on\n\t// scheme/trailing-slash alone and wrongly reject a correctly-configured connection.\n\tlet trustedHost =\n\t\toptions.connectionId != null || options.domain != null\n\t\t\t? hostFromDomain(ctx.resolveDomainForRead(id, options.connectionId, options.domain))\n\t\t\t: undefined;\n\tif (isGitSelfManagedHostIntegrationId(id)) {\n\t\tconst configuredHosts = ctx.getConfigured(id).map(c => hostFromDomain(c.domain));\n\t\tif (trustedHost == null && urlHost != null) {\n\t\t\ttrustedHost = configuredHosts.find(host => host === urlHost);\n\t\t}\n\t\t// A configured host that doesn't match the remote's is a genuine mismatch. With nothing configured\n\t\t// there is no host to mismatch against — it's simply not connected, so fall through to the\n\t\t// `unauthorized` path below (still WITHOUT resolving by the URL domain, so repo data can't seed the\n\t\t// cache).\n\t\tif (trustedHost == null && configuredHosts.length !== 0) return result('host-mismatch');\n\t\tif (trustedHost != null && urlHost != null && trustedHost !== urlHost) return result('host-mismatch');\n\t}\n\n\tlet integration;\n\ttry {\n\t\t// Resolve the instance through the trusted target (as `getIntegrationForRead` does):\n\t\t// `resolveReadSession` looks the session up against the instance's domain-scoped descriptor, so\n\t\t// selecting the instance by the URL domain could miss the session and degrade to `no-connection`.\n\t\tintegration = isGitSelfManagedHostIntegrationId(id)\n\t\t\t? trustedHost != null\n\t\t\t\t? await ctx.getIntegrationForRead(id, options.connectionId, trustedHost)\n\t\t\t\t: undefined\n\t\t\t: await ctx.getIntegrationForDomain(id, provider.domain);\n\t} catch {\n\t\tintegration = undefined;\n\t}\n\tif (integration == null) {\n\t\t// Attach a warning, as the thrown-AuthenticationError branch below does: a consumer driving auth\n\t\t// recovery off `warning.kind` would otherwise get nothing for an unresolvable target.\n\t\treturn {\n\t\t\tresolution: {\n\t\t\t\tstatus: 'unauthorized',\n\t\t\t\twarning: noConnectionWarning(id, provider.domain, options.connectionId),\n\t\t\t},\n\t\t};\n\t}\n\t// Issue trackers have no `getRepo` client; a git host without `getRepoFn` leaves `getRepoInfo`\n\t// undefined. Either way this provider can't resolve repositories.\n\tif (isIssuesIntegration(integration) || integration.getRepoInfo == null) {\n\t\treturn result('unsupported-provider');\n\t}\n\n\t// Azure repos are org + project scoped; the remote provider exposes project as `providerDesc.repoDomain`.\n\tconst project = provider.id === 'azure-devops' ? provider.providerDesc?.repoDomain : undefined;\n\tconst domain = provider.domain;\n\n\t// Azure's lookup is project-scoped and returns `undefined` without one, which the connection-gap branch\n\t// below would misreport as `unauthorized` (driving a pointless reconnect). A remote URL that carries no\n\t// project simply can't address an Azure repo.\n\tif (provider.id === 'azure-devops' && project == null) return result('invalid-remote-url');\n\n\ttry {\n\t\tconst repo = await integration.getRepoInfo({\n\t\t\t...providerRepo,\n\t\t\towner: owner,\n\t\t\tname: name,\n\t\t\tproject: project,\n\t\t\tconnectionId: options.connectionId,\n\t\t});\n\t\tif (repo == null) {\n\t\t\t// `getRepoInfo` returns undefined only when no session could be resolved (not connected, or the\n\t\t\t// requested connection is gone) — a real 404 throws below. So this is a connection gap. Attach a\n\t\t\t// warning so a consumer driving auth recovery off `warning.kind` sees the same signal it gets from\n\t\t\t// the thrown-AuthenticationError branch below; without it, the most common unauthorized case is\n\t\t\t// silent.\n\t\t\treturn {\n\t\t\t\tresolution: {\n\t\t\t\t\tstatus: 'unauthorized',\n\t\t\t\t\twarning: noConnectionWarning(id, domain, options.connectionId),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Prefer the provider's canonical namespace/name (GitHub's REST/GraphQL lookup follows the 301\n\t\t// rename redirect, so a stale old name resolves to the new canonical identity), falling back to the\n\t\t// parsed remote when the response omits them. `renamed` is a case-insensitive compare of input vs\n\t\t// canonical, mirroring gkcli's `EqualFold`, so hosts that merely echo the input casing (e.g.\n\t\t// Bitbucket Server/Azure) don't get spuriously flagged.\n\t\tconst canonicalOwner = repo.namespace || owner;\n\t\tconst canonicalName = repo.name || name;\n\t\tconst renamed =\n\t\t\tcanonicalOwner.toLowerCase() !== owner.toLowerCase() || canonicalName.toLowerCase() !== name.toLowerCase();\n\n\t\tconst identity: RepositoryIdentity = {\n\t\t\tproviderId: id,\n\t\t\tdomain: domain,\n\t\t\towner: canonicalOwner,\n\t\t\tname: canonicalName,\n\t\t\tproject: project,\n\t\t\tremoteUrl: options.remoteUrl,\n\t\t\trenamed: renamed,\n\t\t};\n\t\treturn { resolution: { status: 'resolved', identity: identity } };\n\t} catch (ex) {\n\t\t// Order matters: 404 throws RequestNotFoundError (not `undefined`), so check not-found before auth\n\t\t// and before the generic 5xx/unknown bucket — never classify a 401/403 as not-found.\n\t\tlet resolution: RepositoryResolution;\n\t\tif (ex instanceof RequestNotFoundError) {\n\t\t\tresolution = { status: 'not-found' };\n\t\t} else if (ex instanceof AuthenticationError) {\n\t\t\tresolution = { status: 'unauthorized', warning: toProviderWarning(id, domain, options.connectionId, ex) };\n\t\t} else {\n\t\t\tresolution = { status: 'undetermined', warning: toProviderWarning(id, domain, options.connectionId, ex) };\n\t\t}\n\t\treturn { resolution: resolution };\n\t}\n}\n\n/**\n * The remote-matcher configs for this resolution: the user's own host remote configs (self-managed/custom\n * domains), plus a synthetic exact-domain entry for an explicit `providerId` + host so a custom domain still maps\n * to the right provider for path parsing.\n */\nfunction buildMatcherConfigs(\n\tctx: RepositoryResolutionContext,\n\toptions: { providerId?: IntegrationIds; host?: string },\n\tparsedDomain: string,\n): RemoteProviderConfig[] {\n\tconst configs: RemoteProviderConfig[] = [];\n\tfor (const cfg of ctx.getRemoteConfigs()) {\n\t\tconst type = remoteProviderTypeForConfig(cfg.type);\n\t\tif (type == null) continue;\n\n\t\t// Forward both domain- and regex-based custom remotes (carrying any protocol override), so a\n\t\t// regex-configured host resolves instead of falling through to `unsupported`.\n\t\tif (cfg.domain) {\n\t\t\tconfigs.push({ type: type, domain: cfg.domain, protocol: cfg.protocol });\n\t\t} else if (cfg.regex) {\n\t\t\tconfigs.push({ type: type, regex: cfg.regex, protocol: cfg.protocol });\n\t\t}\n\t}\n\tif (options.providerId == null) return configs;\n\n\tconst type = remoteProviderTypeForIntegration(options.providerId);\n\tconst domain = parsedDomain || options.host;\n\tif (type == null || !domain) return configs;\n\n\t// The synthetic exact-domain entry is unshifted to the front, so it wins the match over the user's own\n\t// config for the same host. Carry that config's protocol override across (matched by domain or regex,\n\t// mirroring `ignoreSSLErrors`) so a self-managed host configured for a custom protocol — e.g. plain\n\t// `http` — isn't silently downgraded to the provider default here.\n\tconst lowerDomain = domain.toLowerCase();\n\tconst protocol = configs.find(c => {\n\t\tif (c.type !== type) return false;\n\t\tif (c.domain != null) return c.domain.toLowerCase() === lowerDomain;\n\n\t\t// Truthy (not just non-null): an empty regex would compile to a match-everything pattern.\n\t\tif (c.regex) {\n\t\t\ttry {\n\t\t\t\treturn new RegExp(c.regex, 'i').test(lowerDomain);\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t})?.protocol;\n\tconfigs.unshift({ type: type, domain: domain, protocol: protocol });\n\treturn configs;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"resolveRepository.js","names":[],"sources":["../../../../../plus/integrations/src/reads/resolveRepository.ts"],"sourcesContent":["import type { RemoteProvider } from '@gitlens/git/models/remoteProvider.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport type { RemoteProviderConfig } from '@gitlens/git/remotes/matcher.js';\nimport { createRemoteProviderMatcher } from '@gitlens/git/remotes/matcher.js';\nimport { parseGitRemoteUrl } from '@gitlens/git/utils/remote.utils.js';\nimport type { ConfiguredIntegrationDescriptor } from '../authentication/models.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';\nimport { AuthenticationError, RequestNotFoundError } from '../errors.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { isAzureCloudDomain, isBitbucketCloudDomain, isGitHubDotCom, isGitLabDotCom } from '../providers/models.js';\nimport type { RepositoryIdentity, RepositoryResolution, ResolveRepositoryResult } from '../results.js';\nimport { toProviderWarning } from '../results.js';\nimport { decodePathSegment, getRemoteHostMatcher, hostFromDomain, isWebRemoteScheme } from '../utils/domain.utils.js';\nimport {\n\tgetIntegrationIdForRemote,\n\tgetSelfManagedBaseUrl,\n\tisGitCloudHostIntegrationId,\n\tisGitSelfManagedHostIntegrationId,\n\tremoteProviderTypeForConfig,\n\tremoteProviderTypeForIntegration,\n} from '../utils/integration.utils.js';\nimport type { RepositoryResolutionContext } from './context.js';\nimport { noConnectionWarning } from './warnings.js';\n\ntype ResolveRepositoryOptions = {\n\tproviderId?: IntegrationIds;\n\tremoteUrl: string;\n\thost?: string;\n\tconnectionId?: string;\n\t/**\n\t * Explicit self-managed host domain used to select the integration instance. Used only when the requested\n\t * connection has no configured domain; it must come from the trusted authentication configuration, not\n\t * repository or remote data.\n\t */\n\tdomain?: string;\n};\n\n/**\n * Resolution of a remote URL to a provider repository identity — the facade's equivalent of `gk repo resolve`.\n *\n * Unlike every other read here, this one's input is REPOSITORY data, so most of the code below is about not\n * letting that data choose the credentials: an explicit cloud `providerId` must agree with the remote's canonical\n * host, and a self-managed host resolves only against a domain the user already authenticated. The\n * `host-mismatch` returns are those guards, not edge cases — resolving `owner/repo` against the wrong account\n * would hand back a confidently wrong identity and seed the domain-keyed instance cache from remote data.\n */\nexport async function resolveRepository(\n\tctx: RepositoryResolutionContext,\n\toptions: ResolveRepositoryOptions,\n): Promise<ResolveRepositoryResult> {\n\tconst result = (status: RepositoryResolution['status']): ResolveRepositoryResult => ({\n\t\tresolution: { status: status },\n\t});\n\n\tconst [scheme, parsedDomain, path] = parseGitRemoteUrl(options.remoteUrl);\n\t// An SSH remote's port belongs to its SSH daemon, not to the web authority a connection is keyed by.\n\tconst hostsMatch = getRemoteHostMatcher(scheme);\n\tconst parsedHost = hostFromDomain(parsedDomain);\n\tconst explicitHost = hostFromDomain(options.host);\n\tif (parsedHost != null && explicitHost != null && !hostsMatch(parsedHost, explicitHost)) {\n\t\treturn result('host-mismatch');\n\t}\n\n\tconst matcherDomain = parsedDomain || options.host || '';\n\n\t// An explicit cloud provider must agree with the remote's canonical cloud host. Without this guard the\n\t// synthetic matcher below can reinterpret, for example, a gitlab.com URL as GitHub and then resolve a\n\t// homonymous owner/repo through the wrong account. Match once without caller configs/synthetic entries;\n\t// Azure's matcher also normalizes ssh.dev.azure.com and vs-ssh.visualstudio.com to their web host.\n\tif (options.providerId != null && isGitCloudHostIntegrationId(options.providerId)) {\n\t\tconst nativeProvider = createRemoteProviderMatcher([])(options.remoteUrl, matcherDomain, path, scheme);\n\t\tconst nativeId = nativeProvider != null ? getIntegrationIdForRemote(nativeProvider) : undefined;\n\t\tconst nativeDomain = nativeProvider?.domain ?? matcherDomain;\n\t\tconst canonicalHost =\n\t\t\toptions.providerId === GitCloudHostIntegrationId.GitHub\n\t\t\t\t? isGitHubDotCom(nativeDomain)\n\t\t\t\t: options.providerId === GitCloudHostIntegrationId.GitLab\n\t\t\t\t\t? isGitLabDotCom(nativeDomain)\n\t\t\t\t\t: options.providerId === GitCloudHostIntegrationId.Bitbucket\n\t\t\t\t\t\t? isBitbucketCloudDomain(nativeDomain)\n\t\t\t\t\t\t: isAzureCloudDomain(nativeDomain);\n\t\tif (nativeId !== options.providerId || !canonicalHost) return result('host-mismatch');\n\t}\n\n\tlet provider = createRemoteProviderMatcher(buildMatcherConfigs(ctx, options, parsedDomain))(\n\t\toptions.remoteUrl,\n\t\tmatcherDomain,\n\t\tpath,\n\t\tscheme,\n\t);\n\tif (provider == null) return result('invalid-remote-url');\n\n\tlet id = options.providerId ?? getIntegrationIdForRemote(provider);\n\t// Custom Azure DevOps Server domains matched via getRemoteConfigs return undefined from\n\t// getIntegrationIdForRemote because the provider is marked custom; map them to the server id so the\n\t// project-scoped lookup uses that host's configured connection and API base URL.\n\tif (id == null && provider.id === 'azure-devops' && provider.custom) {\n\t\tid = GitSelfManagedHostIntegrationId.AzureDevOpsServer;\n\t}\n\tif (id == null) return result('unsupported-provider');\n\n\t// On a self-managed host, resolve only against a TRUSTED host: the pinned connection's configured\n\t// domain, the explicit `domain`, or — when neither was supplied — a configured host matching the\n\t// remote's. That last case keeps `remoteUrl` (repository-supplied) out of the trusted path: it selects\n\t// among hosts the user already authenticated and can never introduce a new one. Resolving `owner/repo`\n\t// against some other host's account would, if that host has the same owner/repo, return a confidently\n\t// wrong identity — and would seed the domain-keyed integration cache (never evicted before dispose) with\n\t// an entry derived from repository data.\n\tconst urlHost = hostFromDomain(provider.domain);\n\t// Normalize BOTH sides before comparing: a stored connection domain is usually a full URL\n\t// (`https://git.example.com`) while `urlHost` is already a bare host, so a raw compare would fail on\n\t// scheme/trailing-slash alone and wrongly reject a correctly-configured connection.\n\tlet trustedHost =\n\t\toptions.connectionId != null || options.domain != null\n\t\t\t? hostFromDomain(ctx.resolveDomainForRead(id, options.connectionId, options.domain))\n\t\t\t: undefined;\n\tlet connectionId = options.connectionId;\n\tlet providerRepo: ResourceDescriptor | undefined;\n\tif (isGitSelfManagedHostIntegrationId(id)) {\n\t\tif (path.split('/').some(segment => /^(?:\\.|%2e){1,2}$/i.test(segment) || /%2f|%5c|\\\\/i.test(segment))) {\n\t\t\treturn result('invalid-remote-url');\n\t\t}\n\n\t\tconst configured = ctx.getConfigured(id);\n\t\tconst configuredHosts = configured.map(c => hostFromDomain(c.domain));\n\t\tif (trustedHost == null && urlHost != null) {\n\t\t\tconst matchingHosts = new Set(configuredHosts.filter(host => hostsMatch(host, urlHost)));\n\t\t\tif (matchingHosts.size > 1) return result('host-mismatch');\n\n\t\t\ttrustedHost = matchingHosts.values().next().value;\n\t\t}\n\t\t// A configured host that doesn't match the remote's is a genuine mismatch. With nothing configured\n\t\t// there is no host to mismatch against — it's simply not connected, so fall through to the\n\t\t// `unauthorized` path below (still WITHOUT resolving by the URL domain, so repo data can't seed the\n\t\t// cache).\n\t\tif (trustedHost == null && configuredHosts.length !== 0) return result('host-mismatch');\n\t\tif (trustedHost != null && urlHost != null && !hostsMatch(trustedHost, urlHost)) {\n\t\t\treturn result('host-mismatch');\n\t\t}\n\n\t\tconst installation = matchInstallation(\n\t\t\tctx,\n\t\t\toptions,\n\t\t\tid,\n\t\t\tconfigured.filter(c => hostFromDomain(c.domain) === trustedHost),\n\t\t\t{ scheme: scheme, domain: parsedDomain, path: path },\n\t\t);\n\t\tif (typeof installation === 'string') return result(installation);\n\n\t\tif (installation != null) {\n\t\t\tprovider = installation.provider;\n\t\t\tconnectionId = installation.connectionId;\n\t\t\tproviderRepo = installation.repoDesc;\n\t\t}\n\t}\n\n\tconst owner = provider.owner;\n\tconst name = provider.repoName;\n\tif (owner == null || name == null) return result('invalid-remote-url');\n\n\tproviderRepo ??= provider.repoDesc;\n\n\tlet integration;\n\ttry {\n\t\t// Resolve the instance through the trusted target (as `getIntegrationForRead` does):\n\t\t// `resolveReadSession` looks the session up against the instance's domain-scoped descriptor, so\n\t\t// selecting the instance by the URL domain could miss the session and degrade to `no-connection`.\n\t\tintegration = isGitSelfManagedHostIntegrationId(id)\n\t\t\t? trustedHost != null\n\t\t\t\t? await ctx.getIntegrationForRead(id, connectionId, trustedHost)\n\t\t\t\t: undefined\n\t\t\t: await ctx.getIntegrationForDomain(id, provider.domain);\n\t} catch {\n\t\tintegration = undefined;\n\t}\n\tif (integration == null) {\n\t\t// Attach a warning, as the thrown-AuthenticationError branch below does: a consumer driving auth\n\t\t// recovery off `warning.kind` would otherwise get nothing for an unresolvable target.\n\t\treturn {\n\t\t\tresolution: {\n\t\t\t\tstatus: 'unauthorized',\n\t\t\t\twarning: noConnectionWarning(id, provider.domain, connectionId),\n\t\t\t},\n\t\t};\n\t}\n\t// Issue trackers have no `getRepo` client; a git host without `getRepoFn` leaves `getRepoInfo`\n\t// undefined. Either way this provider can't resolve repositories.\n\tif (isIssuesIntegration(integration) || integration.getRepoInfo == null) {\n\t\treturn result('unsupported-provider');\n\t}\n\n\t// Azure repos are org + project scoped; the remote provider exposes project as `providerDesc.repoDomain`.\n\tconst project = provider.id === 'azure-devops' ? provider.providerDesc?.repoDomain : undefined;\n\tconst domain = trustedHost ?? provider.domain;\n\n\t// Azure's lookup is project-scoped and returns `undefined` without one, which the connection-gap branch\n\t// below would misreport as `unauthorized` (driving a pointless reconnect). A remote URL that carries no\n\t// project simply can't address an Azure repo.\n\tif (provider.id === 'azure-devops' && project == null) return result('invalid-remote-url');\n\n\ttry {\n\t\tconst repo = await integration.getRepoInfo({\n\t\t\t...providerRepo,\n\t\t\towner: owner,\n\t\t\tname: name,\n\t\t\tproject: project,\n\t\t\tconnectionId: connectionId,\n\t\t});\n\t\tif (repo == null) {\n\t\t\t// `getRepoInfo` returns undefined only when no session could be resolved (not connected, or the\n\t\t\t// requested connection is gone) — a real 404 throws below. So this is a connection gap. Attach a\n\t\t\t// warning so a consumer driving auth recovery off `warning.kind` sees the same signal it gets from\n\t\t\t// the thrown-AuthenticationError branch below; without it, the most common unauthorized case is\n\t\t\t// silent.\n\t\t\treturn {\n\t\t\t\tresolution: {\n\t\t\t\t\tstatus: 'unauthorized',\n\t\t\t\t\twarning: noConnectionWarning(id, domain, connectionId),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Prefer the provider's canonical namespace/name (GitHub's REST/GraphQL lookup follows the 301\n\t\t// rename redirect, so a stale old name resolves to the new canonical identity), falling back to the\n\t\t// parsed remote when the response omits them. `renamed` is a case-insensitive compare of input vs\n\t\t// canonical, mirroring gkcli's `EqualFold`, so hosts that merely echo the input casing (e.g.\n\t\t// Bitbucket Server/Azure) don't get spuriously flagged.\n\t\tconst canonicalOwner = repo.namespace || owner;\n\t\tconst canonicalName = repo.name || name;\n\t\tconst renamed =\n\t\t\tcanonicalOwner.toLowerCase() !== owner.toLowerCase() || canonicalName.toLowerCase() !== name.toLowerCase();\n\n\t\tconst identity: RepositoryIdentity = {\n\t\t\tproviderId: id,\n\t\t\tdomain: domain,\n\t\t\towner: canonicalOwner,\n\t\t\tname: canonicalName,\n\t\t\tproject: project,\n\t\t\tremoteUrl: options.remoteUrl,\n\t\t\trenamed: renamed,\n\t\t};\n\t\treturn { resolution: { status: 'resolved', identity: identity } };\n\t} catch (ex) {\n\t\t// Order matters: 404 throws RequestNotFoundError (not `undefined`), so check not-found before auth\n\t\t// and before the generic 5xx/unknown bucket — never classify a 401/403 as not-found.\n\t\tlet resolution: RepositoryResolution;\n\t\tif (ex instanceof RequestNotFoundError) {\n\t\t\tresolution = { status: 'not-found' };\n\t\t} else if (ex instanceof AuthenticationError) {\n\t\t\tresolution = { status: 'unauthorized', warning: toProviderWarning(id, domain, connectionId, ex) };\n\t\t} else {\n\t\t\tresolution = { status: 'undetermined', warning: toProviderWarning(id, domain, connectionId, ex) };\n\t\t}\n\t\treturn { resolution: resolution };\n\t}\n}\n\n/**\n * Narrows a web remote on a self-managed host to the configured installation it lives under: the pinned\n * connection's or, unpinned, the one whose installation path is the longest prefix of the remote's, so two\n * installations behind one host each resolve with their own credentials. The provider is re-matched against the\n * path relative to that installation, because the remote parsers expect the repository path to start at the\n * host root. An SSH remote carries no installation path, so it is left to the host-level checks.\n */\nfunction matchInstallation(\n\tctx: RepositoryResolutionContext,\n\toptions: ResolveRepositoryOptions,\n\tid: GitSelfManagedHostIntegrationId,\n\tconnections: ConfiguredIntegrationDescriptor[],\n\tremote: { scheme: string; domain: string; path: string },\n):\n\t| { provider: RemoteProvider; connectionId: string | undefined; repoDesc: ResourceDescriptor }\n\t| 'host-mismatch'\n\t| 'invalid-remote-url'\n\t| undefined {\n\tif (!isWebRemoteScheme(remote.scheme)) return undefined;\n\n\tconst installationPath = (connection: ConfiguredIntegrationDescriptor | undefined): string | undefined => {\n\t\tconst baseUrl = getSelfManagedBaseUrl(\n\t\t\tid,\n\t\t\tconnection != null ? (connection.baseUrl ?? connection.domain) : options.domain,\n\t\t);\n\t\treturn baseUrl != null ? new URL(baseUrl).pathname.replace(/\\/+$/, '') : undefined;\n\t};\n\n\t// The connection an unpinned read already targets comes first, so it wins a tie between installations.\n\tconst defaultConnection = connections.find(c => c.primary) ?? connections[0];\n\t// A cloud sync serves the host's cloud-scoped default instead, even over a local primary (see\n\t// `getStoredSession`), so an unpinned read reaches the default's installation only when both agree.\n\tconst cloudConnections = connections.filter(c => c.cloud);\n\tconst cloudDefault = cloudConnections.find(c => c.primary) ?? cloudConnections[0];\n\tconst isDefaultReadable =\n\t\tdefaultConnection?.cloud !== false ||\n\t\tcloudDefault == null ||\n\t\tinstallationPath(cloudDefault) === installationPath(defaultConnection);\n\t// Whether an unpinned read can reach this connection's session: the default one is read without an id, any\n\t// other only by a real cloud token id. Session lookup treats every connection id as one, and a local (PAT)\n\t// descriptor's or domain-derived legacy id is not (see `getConfiguredConnectionId`).\n\tconst isReadable = (connection: ConfiguredIntegrationDescriptor | undefined): boolean =>\n\t\toptions.connectionId != null ||\n\t\t(connection === defaultConnection\n\t\t\t? isDefaultReadable\n\t\t\t: connection?.cloud === true && hostFromDomain(connection.id) !== connection.domain);\n\n\tconst candidates: (ConfiguredIntegrationDescriptor | undefined)[] =\n\t\toptions.connectionId != null\n\t\t\t? connections.filter(c => c.id === options.connectionId)\n\t\t\t: [defaultConnection, ...connections.filter(c => c !== defaultConnection)];\n\t// With no configured candidate, the explicit (trusted) `domain` is the only installation address there is.\n\tif (!candidates.length) {\n\t\tcandidates.push(undefined);\n\t}\n\n\tconst remotePath = `/${remote.path}`;\n\tconst remoteSegments = remote.path.split('/');\n\t// The remote's path below an installation, or `undefined` when it isn't under it. Azure DevOps Server runs on\n\t// IIS, which serves its paths decoded and case-insensitively, so it compares segment by segment.\n\tconst relativeTo = (basePath: string): string | undefined => {\n\t\tif (id !== GitSelfManagedHostIntegrationId.AzureDevOpsServer) {\n\t\t\treturn remotePath.startsWith(`${basePath}/`) ? remotePath.slice(basePath.length + 1) : undefined;\n\t\t}\n\n\t\tconst baseSegments = basePath.split('/').filter(Boolean);\n\t\tconst isUnder =\n\t\t\tremoteSegments.length > baseSegments.length &&\n\t\t\tbaseSegments.every(\n\t\t\t\t(segment, i) =>\n\t\t\t\t\tdecodePathSegment(segment).toLowerCase() === decodePathSegment(remoteSegments[i]).toLowerCase(),\n\t\t\t);\n\t\treturn isUnder ? remoteSegments.slice(baseSegments.length).join('/') : undefined;\n\t};\n\tlet match: { connection: ConfiguredIntegrationDescriptor | undefined; basePath: string } | undefined;\n\tlet checked = false;\n\tfor (const connection of candidates) {\n\t\tconst basePath = installationPath(connection);\n\t\tif (basePath == null) continue;\n\n\t\tchecked = true;\n\t\tif (relativeTo(basePath) == null) continue;\n\n\t\t// The longest installation wins; on a tie, a readable connection replaces an unreadable one.\n\t\tconst isBetter =\n\t\t\tmatch == null ||\n\t\t\tbasePath.length > match.basePath.length ||\n\t\t\t(basePath.length === match.basePath.length && !isReadable(match.connection) && isReadable(connection));\n\t\tif (isBetter) {\n\t\t\tmatch = { connection: connection, basePath: basePath };\n\t\t}\n\t}\n\tif (match == null) return checked ? 'host-mismatch' : undefined;\n\t// Reading through the default connection instead would address its installation with a path relative to\n\t// another one, so an unreachable installation fails closed.\n\tif (!isReadable(match.connection)) return 'host-mismatch';\n\n\tconst connectionId =\n\t\toptions.connectionId ?? (match.connection !== defaultConnection ? match.connection?.id : undefined);\n\n\tconst matcher = createRemoteProviderMatcher(\n\t\tbuildMatcherConfigs(ctx, { ...options, providerId: id }, remote.domain),\n\t);\n\tconst rematch = (path: string) => matcher(options.remoteUrl, remote.domain, relativeTo(path) ?? '', remote.scheme);\n\tlet basePath = match.basePath;\n\tlet provider = rematch(basePath);\n\tif (id !== GitSelfManagedHostIntegrationId.AzureDevOpsServer) {\n\t\treturn provider != null\n\t\t\t? { provider: provider, connectionId: connectionId, repoDesc: provider.repoDesc }\n\t\t\t: 'invalid-remote-url';\n\t}\n\n\t// An Azure DevOps Server address may name the collection as well as the virtual directory; the parser expects\n\t// the collection in the path, so re-base on the address's parent when no project was found.\n\tif (basePath && provider?.providerDesc?.repoDomain == null) {\n\t\tbasePath = basePath.slice(0, basePath.lastIndexOf('/'));\n\t\tprovider = rematch(basePath);\n\t}\n\tif (provider == null) return 'invalid-remote-url';\n\n\t// Requests address the repository's virtual directory from the host root, not from the installation the\n\t// re-match made it relative to; the absolute form also tells a directory apart from a collection of that name.\n\tconst repoDesc = provider.repoDesc;\n\tconst relative = typeof repoDesc.virtualDirectory === 'string' ? repoDesc.virtualDirectory : undefined;\n\tconst virtualDirectory =\n\t\t[...basePath.split('/').filter(Boolean).map(decodePathSegment), ...(relative?.split('/') ?? [])].join('/') ||\n\t\tundefined;\n\treturn {\n\t\tprovider: provider,\n\t\tconnectionId: connectionId,\n\t\trepoDesc: { ...repoDesc, virtualDirectory: virtualDirectory },\n\t};\n}\n\n/**\n * The remote-matcher configs for this resolution: the user's own host remote configs (self-managed/custom\n * domains), plus a synthetic exact-domain entry for an explicit `providerId` + host so a custom domain still maps\n * to the right provider for path parsing.\n */\nfunction buildMatcherConfigs(\n\tctx: RepositoryResolutionContext,\n\toptions: { providerId?: IntegrationIds; host?: string },\n\tparsedDomain: string,\n): RemoteProviderConfig[] {\n\tconst configs: RemoteProviderConfig[] = [];\n\tfor (const cfg of ctx.getRemoteConfigs()) {\n\t\tconst type = remoteProviderTypeForConfig(cfg.type);\n\t\tif (type == null) continue;\n\n\t\t// Forward both domain- and regex-based custom remotes (carrying any protocol override), so a\n\t\t// regex-configured host resolves instead of falling through to `unsupported`.\n\t\tif (cfg.domain) {\n\t\t\tconfigs.push({ type: type, domain: cfg.domain, protocol: cfg.protocol });\n\t\t} else if (cfg.regex) {\n\t\t\tconfigs.push({ type: type, regex: cfg.regex, protocol: cfg.protocol });\n\t\t}\n\t}\n\tif (options.providerId == null) return configs;\n\n\tconst type = remoteProviderTypeForIntegration(options.providerId);\n\tconst domain = parsedDomain || options.host;\n\tif (type == null || !domain) return configs;\n\n\t// The synthetic exact-domain entry is unshifted to the front, so it wins the match over the user's own\n\t// config for the same host. Carry that config's protocol override across (matched by domain or regex,\n\t// mirroring `ignoreSSLErrors`) so a self-managed host configured for a custom protocol — e.g. plain\n\t// `http` — isn't silently downgraded to the provider default here.\n\tconst lowerDomain = domain.toLowerCase();\n\tconst protocol = configs.find(c => {\n\t\tif (c.type !== type) return false;\n\t\tif (c.domain != null) return c.domain.toLowerCase() === lowerDomain;\n\n\t\t// Truthy (not just non-null): an empty regex would compile to a match-everything pattern.\n\t\tif (c.regex) {\n\t\t\ttry {\n\t\t\t\treturn new RegExp(c.regex, 'i').test(lowerDomain);\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t})?.protocol;\n\tconfigs.unshift({ type: type, domain: domain, protocol: protocol });\n\treturn configs;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA+CA,eAAsB,kBACrB,KACA,SACmC;CACnC,MAAM,UAAU,YAAqE,EACpF,YAAY,EAAU,OAAO,EAC9B;CAEA,MAAM,CAAC,QAAQ,cAAc,QAAQ,kBAAkB,QAAQ,SAAS;CAExE,MAAM,aAAa,qBAAqB,MAAM;CAC9C,MAAM,aAAa,eAAe,YAAY;CAC9C,MAAM,eAAe,eAAe,QAAQ,IAAI;CAChD,IAAI,cAAc,QAAQ,gBAAgB,QAAQ,CAAC,WAAW,YAAY,YAAY,GACrF,OAAO,OAAO,eAAe;CAG9B,MAAM,gBAAgB,gBAAgB,QAAQ,QAAQ;CAMtD,IAAI,QAAQ,cAAc,QAAQ,4BAA4B,QAAQ,UAAU,GAAG;EAClF,MAAM,iBAAiB,4BAA4B,CAAC,CAAC,CAAC,CAAC,QAAQ,WAAW,eAAe,MAAM,MAAM;EACrG,MAAM,WAAW,kBAAkB,OAAO,0BAA0B,cAAc,IAAI,KAAA;EACtF,MAAM,eAAe,gBAAgB,UAAU;EAC/C,MAAM,gBACL,QAAQ,eAAA,WACL,eAAe,YAAY,IAC3B,QAAQ,eAAA,WACP,eAAe,YAAY,IAC3B,QAAQ,eAAA,cACP,uBAAuB,YAAY,IACnC,mBAAmB,YAAY;EACrC,IAAI,aAAa,QAAQ,cAAc,CAAC,eAAe,OAAO,OAAO,eAAe;CACrF;CAEA,IAAI,WAAW,4BAA4B,oBAAoB,KAAK,SAAS,YAAY,CAAC,CAAC,CAC1F,QAAQ,WACR,eACA,MACA,MACD;CACA,IAAI,YAAY,MAAM,OAAO,OAAO,oBAAoB;CAExD,IAAI,KAAK,QAAQ,cAAc,0BAA0B,QAAQ;CAIjE,IAAI,MAAM,QAAQ,SAAS,OAAO,kBAAkB,SAAS,QAC5D,KAAA;CAED,IAAI,MAAM,MAAM,OAAO,OAAO,sBAAsB;CASpD,MAAM,UAAU,eAAe,SAAS,MAAM;CAI9C,IAAI,cACH,QAAQ,gBAAgB,QAAQ,QAAQ,UAAU,OAC/C,eAAe,IAAI,qBAAqB,IAAI,QAAQ,cAAc,QAAQ,MAAM,CAAC,IACjF,KAAA;CACJ,IAAI,eAAe,QAAQ;CAC3B,IAAI;CACJ,IAAI,kCAAkC,EAAE,GAAG;EAC1C,IAAI,KAAK,MAAM,GAAG,CAAC,CAAC,MAAK,YAAW,qBAAqB,KAAK,OAAO,KAAK,cAAc,KAAK,OAAO,CAAC,GACpG,OAAO,OAAO,oBAAoB;EAGnC,MAAM,aAAa,IAAI,cAAc,EAAE;EACvC,MAAM,kBAAkB,WAAW,KAAI,MAAK,eAAe,EAAE,MAAM,CAAC;EACpE,IAAI,eAAe,QAAQ,WAAW,MAAM;GAC3C,MAAM,gBAAgB,IAAI,IAAI,gBAAgB,QAAO,SAAQ,WAAW,MAAM,OAAO,CAAC,CAAC;GACvF,IAAI,cAAc,OAAO,GAAG,OAAO,OAAO,eAAe;GAEzD,cAAc,cAAc,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC;EAC7C;EAKA,IAAI,eAAe,QAAQ,gBAAgB,WAAW,GAAG,OAAO,OAAO,eAAe;EACtF,IAAI,eAAe,QAAQ,WAAW,QAAQ,CAAC,WAAW,aAAa,OAAO,GAC7E,OAAO,OAAO,eAAe;EAG9B,MAAM,eAAe,kBACpB,KACA,SACA,IACA,WAAW,QAAO,MAAK,eAAe,EAAE,MAAM,MAAM,WAAW,GAC/D;GAAU;GAAQ,QAAQ;GAAoB;EAAK,CACpD;EACA,IAAI,OAAO,iBAAiB,UAAU,OAAO,OAAO,YAAY;EAEhE,IAAI,gBAAgB,MAAM;GACzB,WAAW,aAAa;GACxB,eAAe,aAAa;GAC5B,eAAe,aAAa;EAC7B;CACD;CAEA,MAAM,QAAQ,SAAS;CACvB,MAAM,OAAO,SAAS;CACtB,IAAI,SAAS,QAAQ,QAAQ,MAAM,OAAO,OAAO,oBAAoB;CAErE,iBAAiB,SAAS;CAE1B,IAAI;CACJ,IAAI;EAIH,cAAc,kCAAkC,EAAE,IAC/C,eAAe,OACd,MAAM,IAAI,sBAAsB,IAAI,cAAc,WAAW,IAC7D,KAAA,IACD,MAAM,IAAI,wBAAwB,IAAI,SAAS,MAAM;CACzD,QAAQ;EACP,cAAc,KAAA;CACf;CACA,IAAI,eAAe,MAGlB,OAAO,EACN,YAAY;EACX,QAAQ;EACR,SAAS,oBAAoB,IAAI,SAAS,QAAQ,YAAY;CAC/D,EACD;CAID,IAAI,oBAAoB,WAAW,KAAK,YAAY,eAAe,MAClE,OAAO,OAAO,sBAAsB;CAIrC,MAAM,UAAU,SAAS,OAAO,iBAAiB,SAAS,cAAc,aAAa,KAAA;CACrF,MAAM,SAAS,eAAe,SAAS;CAKvC,IAAI,SAAS,OAAO,kBAAkB,WAAW,MAAM,OAAO,OAAO,oBAAoB;CAEzF,IAAI;EACH,MAAM,OAAO,MAAM,YAAY,YAAY;GAC1C,GAAG;GACI;GACD;GACG;GACK;EACf,CAAC;EACD,IAAI,QAAQ,MAMX,OAAO,EACN,YAAY;GACX,QAAQ;GACR,SAAS,oBAAoB,IAAI,QAAQ,YAAY;EACtD,EACD;EAQD,MAAM,iBAAiB,KAAK,aAAa;EACzC,MAAM,gBAAgB,KAAK,QAAQ;EACnC,MAAM,UACL,eAAe,YAAY,MAAM,MAAM,YAAY,KAAK,cAAc,YAAY,MAAM,KAAK,YAAY;EAW1G,OAAO,EAAE,YAAY;GAAE,QAAQ;GAAY,UAAU;IARpD,YAAY;IACJ;IACR,OAAO;IACP,MAAM;IACG;IACT,WAAW,QAAQ;IACV;GAEkD;EAAE,EAAE;CACjE,SAAS,IAAI;EAGZ,IAAI;EACJ,IAAI,cAAc,sBACjB,aAAa,EAAE,QAAQ,YAAY;OAC7B,IAAI,cAAc,qBACxB,aAAa;GAAE,QAAQ;GAAgB,SAAS,kBAAkB,IAAI,QAAQ,cAAc,EAAE;EAAE;OAEhG,aAAa;GAAE,QAAQ;GAAgB,SAAS,kBAAkB,IAAI,QAAQ,cAAc,EAAE;EAAE;EAEjG,OAAO,EAAc,WAAW;CACjC;AACD;;;;;;;;AASA,SAAS,kBACR,KACA,SACA,IACA,aACA,QAKY;CACZ,IAAI,CAAC,kBAAkB,OAAO,MAAM,GAAG,OAAO,KAAA;CAE9C,MAAM,oBAAoB,eAAgF;EACzG,MAAM,UAAU,sBACf,IACA,cAAc,OAAQ,WAAW,WAAW,WAAW,SAAU,QAAQ,MAC1E;EACA,OAAO,WAAW,OAAO,IAAI,IAAI,OAAO,CAAC,CAAC,SAAS,QAAQ,QAAQ,EAAE,IAAI,KAAA;CAC1E;CAGA,MAAM,oBAAoB,YAAY,MAAK,MAAK,EAAE,OAAO,KAAK,YAAY;CAG1E,MAAM,mBAAmB,YAAY,QAAO,MAAK,EAAE,KAAK;CACxD,MAAM,eAAe,iBAAiB,MAAK,MAAK,EAAE,OAAO,KAAK,iBAAiB;CAC/E,MAAM,oBACL,mBAAmB,UAAU,SAC7B,gBAAgB,QAChB,iBAAiB,YAAY,MAAM,iBAAiB,iBAAiB;CAItE,MAAM,cAAc,eACnB,QAAQ,gBAAgB,SACvB,eAAe,oBACb,oBACA,YAAY,UAAU,QAAQ,eAAe,WAAW,EAAE,MAAM,WAAW;CAE/E,MAAM,aACL,QAAQ,gBAAgB,OACrB,YAAY,QAAO,MAAK,EAAE,OAAO,QAAQ,YAAY,IACrD,CAAC,mBAAmB,GAAG,YAAY,QAAO,MAAK,MAAM,iBAAiB,CAAC;CAE3E,IAAI,CAAC,WAAW,QACf,WAAW,KAAK,KAAA,CAAS;CAG1B,MAAM,aAAa,IAAI,OAAO;CAC9B,MAAM,iBAAiB,OAAO,KAAK,MAAM,GAAG;CAG5C,MAAM,cAAc,aAAyC;EAC5D,IAAI,OAAA,uBACH,OAAO,WAAW,WAAW,GAAG,SAAS,EAAE,IAAI,WAAW,MAAM,SAAS,SAAS,CAAC,IAAI,KAAA;EAGxF,MAAM,eAAe,SAAS,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;EAOvD,OALC,eAAe,SAAS,aAAa,UACrC,aAAa,OACX,SAAS,MACT,kBAAkB,OAAO,CAAC,CAAC,YAAY,MAAM,kBAAkB,eAAe,EAAE,CAAC,CAAC,YAAY,CAChG,IACgB,eAAe,MAAM,aAAa,MAAM,CAAC,CAAC,KAAK,GAAG,IAAI,KAAA;CACxE;CACA,IAAI;CACJ,IAAI,UAAU;CACd,KAAK,MAAM,cAAc,YAAY;EACpC,MAAM,WAAW,iBAAiB,UAAU;EAC5C,IAAI,YAAY,MAAM;EAEtB,UAAU;EACV,IAAI,WAAW,QAAQ,KAAK,MAAM;EAOlC,IAHC,SAAS,QACT,SAAS,SAAS,MAAM,SAAS,UAChC,SAAS,WAAW,MAAM,SAAS,UAAU,CAAC,WAAW,MAAM,UAAU,KAAK,WAAW,UAAU,GAEpG,QAAQ;GAAc;GAAsB;EAAS;CAEvD;CACA,IAAI,SAAS,MAAM,OAAO,UAAU,kBAAkB,KAAA;CAGtD,IAAI,CAAC,WAAW,MAAM,UAAU,GAAG,OAAO;CAE1C,MAAM,eACL,QAAQ,iBAAiB,MAAM,eAAe,oBAAoB,MAAM,YAAY,KAAK,KAAA;CAE1F,MAAM,UAAU,4BACf,oBAAoB,KAAK;EAAE,GAAG;EAAS,YAAY;CAAG,GAAG,OAAO,MAAM,CACvE;CACA,MAAM,WAAW,SAAiB,QAAQ,QAAQ,WAAW,OAAO,QAAQ,WAAW,IAAI,KAAK,IAAI,OAAO,MAAM;CACjH,IAAI,WAAW,MAAM;CACrB,IAAI,WAAW,QAAQ,QAAQ;CAC/B,IAAI,OAAA,uBACH,OAAO,YAAY,OAChB;EAAY;EAAwB;EAAc,UAAU,SAAS;CAAS,IAC9E;CAKJ,IAAI,YAAY,UAAU,cAAc,cAAc,MAAM;EAC3D,WAAW,SAAS,MAAM,GAAG,SAAS,YAAY,GAAG,CAAC;EACtD,WAAW,QAAQ,QAAQ;CAC5B;CACA,IAAI,YAAY,MAAM,OAAO;CAI7B,MAAM,WAAW,SAAS;CAC1B,MAAM,WAAW,OAAO,SAAS,qBAAqB,WAAW,SAAS,mBAAmB,KAAA;CAC7F,MAAM,mBACL,CAAC,GAAG,SAAS,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,IAAI,iBAAiB,GAAG,GAAI,UAAU,MAAM,GAAG,KAAK,CAAC,CAAE,CAAC,CAAC,KAAK,GAAG,KACzG,KAAA;CACD,OAAO;EACI;EACI;EACd,UAAU;GAAE,GAAG;GAA4B;EAAiB;CAC7D;AACD;;;;;;AAOA,SAAS,oBACR,KACA,SACA,cACyB;CACzB,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,OAAO,IAAI,iBAAiB,GAAG;EACzC,MAAM,OAAO,4BAA4B,IAAI,IAAI;EACjD,IAAI,QAAQ,MAAM;EAIlB,IAAI,IAAI,QACP,QAAQ,KAAK;GAAQ;GAAM,QAAQ,IAAI;GAAQ,UAAU,IAAI;EAAS,CAAC;OACjE,IAAI,IAAI,OACd,QAAQ,KAAK;GAAQ;GAAM,OAAO,IAAI;GAAO,UAAU,IAAI;EAAS,CAAC;CAEvE;CACA,IAAI,QAAQ,cAAc,MAAM,OAAO;CAEvC,MAAM,OAAO,iCAAiC,QAAQ,UAAU;CAChE,MAAM,SAAS,gBAAgB,QAAQ;CACvC,IAAI,QAAQ,QAAQ,CAAC,QAAQ,OAAO;CAMpC,MAAM,cAAc,OAAO,YAAY;CACvC,MAAM,WAAW,QAAQ,MAAK,MAAK;EAClC,IAAI,EAAE,SAAS,MAAM,OAAO;EAC5B,IAAI,EAAE,UAAU,MAAM,OAAO,EAAE,OAAO,YAAY,MAAM;EAGxD,IAAI,EAAE,OACL,IAAI;GACH,OAAO,IAAI,OAAO,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,WAAW;EACjD,QAAQ;GACP,OAAO;EACR;EAGD,OAAO;CACR,CAAC,CAAC,EAAE;CACJ,QAAQ,QAAQ;EAAQ;EAAc;EAAkB;CAAS,CAAC;CAClE,OAAO;AACR"}
@@ -47,7 +47,7 @@ async function searchIssuesPage(ctx, options) {
47
47
  if (!integration.supportsIssues) return refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));
48
48
  const resolved = resolveIssueSearchCriteria(options.providerId, options.criteria);
49
49
  if (resolved.rejection != null) return refused(unsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, resolved.rejection));
50
- const scope = resolveIssueSearchScope(options.repos, options.org, options.criteria);
50
+ const scope = resolveIssueSearchScope(options.providerId, options.repos, options.org, options.criteria);
51
51
  switch (scope.rejection?.reason) {
52
52
  case "unscoped": return refused(otherWarning(options.providerId, domain, options.connectionId, "A filtered issue search must be scoped: pass `repos`, `org`, or a relationship to the current user (`authored`/`assigned`/`mentioned`). `any-assignee` and `unassigned` are not scopes — either one alone matches every such issue on the host."));
53
53
  case "repo-ids": return refused(otherWarning(options.providerId, domain, options.connectionId, "A filtered issue search cannot be scoped by repository id; pass repository descriptors (namespace + name) instead."));
@@ -63,6 +63,7 @@ async function searchIssuesPage(ctx, options) {
63
63
  const first = await readPage(options.cursor);
64
64
  const warnings = first.warning != null ? [first.warning] : [];
65
65
  let totalCount = first.value?.totalCount;
66
+ let limitReached = first.value?.limitReached === true;
66
67
  const drained = await drainFlatPagesToRequestedPage(first, {
67
68
  requestedPage: page,
68
69
  suppliedCursor: options.cursor,
@@ -70,6 +71,7 @@ async function searchIssuesPage(ctx, options) {
70
71
  readPage,
71
72
  fold: (p) => {
72
73
  if (p.totalCount != null) totalCount = Math.max(totalCount ?? 0, p.totalCount);
74
+ limitReached ||= p.limitReached === true;
73
75
  }
74
76
  });
75
77
  const { value, currentPage, requestedPageMissing } = drained;
@@ -82,7 +84,7 @@ async function searchIssuesPage(ctx, options) {
82
84
  truncated: drained.truncated
83
85
  }, void 0);
84
86
  const truncated = continuation.truncated;
85
- if (truncated && warnings.length === 0) warnings.push(issueSearchCapResultWarning(options.providerId, domain, options.connectionId, totalCount, effectiveIssueSort(options.criteria?.sort)) ?? truncationWarning(options.providerId, domain, options.connectionId, "Issue search", "exhausted"));
87
+ if (truncated && warnings.length === 0) warnings.push(issueSearchCapResultWarning(options.providerId, domain, options.connectionId, totalCount, effectiveIssueSort(options.criteria?.sort), limitReached) ?? truncationWarning(options.providerId, domain, options.connectionId, "Issue search", "exhausted"));
86
88
  return {
87
89
  items,
88
90
  warnings,
@@ -1 +1 @@
1
- {"version":3,"file":"searchIssues.js","names":[],"sources":["../../../../../plus/integrations/src/reads/searchIssues.ts"],"sourcesContent":["import type { IssueSearchCriteria, IssueShape } from '@gitlens/git/models/issue.js';\nimport { effectiveIssueSort } from '@gitlens/git/utils/issue.utils.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { ProviderReposInput } from '../providers/models.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport { resolveIssueSearchCriteria, resolveIssueSearchScope } from './filters.js';\nimport {\n\tdrainFlatPagesToRequestedPage,\n\trefusedPage,\n\tresolveContinuation,\n\tresolveCurrentPage,\n\tusableCursor,\n} from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissueSearchCapResultWarning,\n\tissuesUnsupportedWarning,\n\totherWarning,\n\ttruncationWarning,\n\tunsupportedIssueSearchCriteriaWarning,\n\tunusableSearchScopeMessage,\n} from './warnings.js';\n\n/**\n * The FILTERED issue search: issues matching structured criteria over a repository/org scope, with no forced\n * relationship to the current user.\n *\n * A sibling of `listIssuesPage` rather than a mode of it, deliberately. That read is already two divergent\n * branches (a cursor-composite account-wide path and a page-draining repo-scoped one) around a documented\n * contract where `filters` REPLACES the provider's definition of \"my issues\". This search answers a different\n * question — its relationships include two that are not about the user at all — and routes around the SDK's\n * repo-scoped read entirely, whose over-limit recovery walk can spend up to 128 requests and still return an\n * incomplete set. Folding the two together would multiply that branch matrix and put every existing consumer of\n * `listIssuesPage` at risk; keeping them siblings shares the primitives below without sharing the branch space,\n * for the same reason `broaden.ts`, `sweeps.ts` and `issueTracker.ts` are separate files.\n *\n * Contract details worth knowing before calling:\n * - Ordering is `criteria.sort`, most-recently-updated-first by default, and validated like every other criterion\n * against `getSupportedFilters().issueSearch.sorts`. SOME order is always requested — a \"show the N most recent\"\n * policy at the provider's result ceiling is only correct under a guaranteed one, and the ceiling warning names\n * the key it selected the window under. A key the provider can't express refuses the read rather than serving a\n * differently-ordered result; do not change it mid-pagination (a cursor is bound to the order it was made under,\n * and threading it under a different key is refused with a warning + `fetchFailed` — drop the cursor and read\n * from the first page instead).\n * - Paging is cursor-only. `page` alone walks 1..N internally (O(N) requests); a page past the last one is an\n * empty page N, never page N−1 relabeled.\n * - At the provider's result ceiling the read SUCCEEDS and reports an omission carrying the total match count,\n * so a consumer can say how many were withheld. It never falls back to a per-repository recovery walk.\n */\nexport async function searchIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\t/** Repositories to search. Combines with `org`; both become scope constraints on the same query. */\n\t\trepos?: ProviderReposInput;\n\t\t/** Organization/account to search. Combines with `repos`. */\n\t\torg?: string;\n\t\t/**\n\t\t * What to narrow on, validated against `getSupportedFilters().issueSearch` before the read runs. A\n\t\t * criterion the provider can't express server-side refuses the whole read (warning + `fetchFailed`)\n\t\t * rather than serving a list that was never narrowed.\n\t\t */\n\t\tcriteria?: IssueSearchCriteria;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\t/** Per relationship, not per page; see {@link IntegrationManager.searchIssuesPage}. */\n\t\titemsPerPage?: number;\n\t\tforceSync?: boolean;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<IssueShape>> {\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\t// Cursor-only, so a refusal can't claim the requested position — it reports page 1, per\n\t// ProviderPageInfo.currentPage.\n\tconst refused = (warning: ProviderWarning) => refusedPage<IssueShape>(1, [warning], true);\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Filtered issue search'),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(1, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Filtered issue search'),\n\t\t);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\tconst resolved = resolveIssueSearchCriteria(options.providerId, options.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, resolved.rejection),\n\t\t);\n\t}\n\n\t// AFTER the criteria check, which is what reports a provider with no filtered issue search at all: that is\n\t// the more fundamental refusal, and answering \"your scope name is malformed\" to a caller whose provider has\n\t// no such search names the wrong defect. Matches the pull-request twin, which probes existence first.\n\tconst scope = resolveIssueSearchScope(options.repos, options.org, options.criteria);\n\tswitch (scope.rejection?.reason) {\n\t\tcase 'unscoped':\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t'A filtered issue search must be scoped: pass `repos`, `org`, or a relationship to the current user (`authored`/`assigned`/`mentioned`). `any-assignee` and `unassigned` are not scopes — either one alone matches every such issue on the host.',\n\t\t\t\t),\n\t\t\t);\n\t\tcase 'repo-ids':\n\t\t\t// A search query names repositories by PATH, so repository ids can't be expressed as a scope. Refusing\n\t\t\t// is the only honest answer: dropping them would search the whole org (or the whole host) as if the\n\t\t\t// caller had asked for that.\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t'A filtered issue search cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.',\n\t\t\t\t),\n\t\t\t);\n\t\t// A scope the query cannot spell, refused rather than sanitized — see `isUsableSearchScopeName`.\n\t\tcase 'unusable-scope':\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\tunusableSearchScopeMessage('issue search scopes', scope.rejection.scopes),\n\t\t\t\t),\n\t\t\t);\n\t}\n\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.searchIssuesPageResult(\n\t\t\t\t\t{\n\t\t\t\t\t\trepos: scope.repos,\n\t\t\t\t\t\torg: options.org,\n\t\t\t\t\t\tcriteria: options.criteria,\n\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\t},\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\tconst first = await readPage(options.cursor);\n\tconst warnings = first.warning != null ? [first.warning] : [];\n\t// The largest total any page reported. Folded across the walk so a cap detected on page 1 is still reportable\n\t// after paging on to page N — the one thing this read accumulates that its sibling doesn't.\n\tlet totalCount = first.value?.totalCount;\n\tconst drained = await drainFlatPagesToRequestedPage(first, {\n\t\trequestedPage: page,\n\t\tsuppliedCursor: options.cursor,\n\t\twarnings: warnings,\n\t\treadPage: readPage,\n\t\tfold: p => {\n\t\t\tif (p.totalCount != null) {\n\t\t\t\ttotalCount = Math.max(totalCount ?? 0, p.totalCount);\n\t\t\t}\n\t\t},\n\t});\n\tconst { value, currentPage, requestedPageMissing } = drained;\n\tconst pageFetchFailed = drained.fetchFailed;\n\n\t// A provider that doesn't implement the search hook returns `undefined` with no error. Surface that as an\n\t// explicit unsupported warning + fetchFailed rather than a silent empty success, so a caller isn't left\n\t// unable to tell it apart from an account with no matching issues. `getSupportedFilters().issueSearch`\n\t// declares this ahead of the read, so a consumer that checks it never reaches here.\n\tif (value == null && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\tconst items = requestedPageMissing ? [] : (value?.values ?? []);\n\t// Cursor-only: `hasMore` without a real cursor is a dead end, so report it as terminal-but-incomplete rather\n\t// than inviting the caller to page forever.\n\tconst continuation = resolveContinuation(\n\t\t{\n\t\t\thasMore: requestedPageMissing ? false : (value?.hasMore ?? false),\n\t\t\tcursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),\n\t\t\ttruncated: drained.truncated,\n\t\t},\n\t\tundefined,\n\t);\n\tconst truncated = continuation.truncated;\n\t// Only reachable when nothing else already explained the incompleteness — and a failed page always warns\n\t// first, so a read that gets here SUCCEEDED. That is why the cause below is unconditionally `exhausted` and\n\t// `fetchFailed` is never set from here.\n\tif (truncated && warnings.length === 0) {\n\t\t// The result ceiling is the one truncation cause this read can EXPLAIN with a number, so it gets its own\n\t\t// warning carrying the total. Anything else truncated (an unusable continuation, a provider that stopped\n\t\t// advancing) falls back to the generic wording.\n\t\twarnings.push(\n\t\t\tissueSearchCapResultWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\ttotalCount,\n\t\t\t\teffectiveIssueSort(options.criteria?.sort),\n\t\t\t) ??\n\t\t\t\ttruncationWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t'Issue search',\n\t\t\t\t\t// `exhausted`: this read exposes no budget the caller could raise, so nothing it could call\n\t\t\t\t\t// would return the withheld items.\n\t\t\t\t\t'exhausted',\n\t\t\t\t),\n\t\t);\n\t}\n\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\t// Positional, per ProviderPageInfo.currentPage. A requested page past the terminal cursor is reported\n\t\t\t// as that empty page N; otherwise this read is cursor-only, so a `page` the caller didn't pair with a\n\t\t\t// cursor is never echoed.\n\t\t\tcurrentPage: requestedPageMissing\n\t\t\t\t? page\n\t\t\t\t: resolveCurrentPage({\n\t\t\t\t\t\tproviderPage: currentPage,\n\t\t\t\t\t\trequestedPage: page,\n\t\t\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\t\t\tpageAdvanceable: false,\n\t\t\t\t\t}),\n\t\t\titemsPerPage: items.length,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: pageFetchFailed || undefined,\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"searchIssues.js","names":[],"sources":["../../../../../plus/integrations/src/reads/searchIssues.ts"],"sourcesContent":["import type { IssueSearchCriteria, IssueShape } from '@gitlens/git/models/issue.js';\nimport { effectiveIssueSort } from '@gitlens/git/utils/issue.utils.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { ProviderReposInput } from '../providers/models.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport { resolveIssueSearchCriteria, resolveIssueSearchScope } from './filters.js';\nimport {\n\tdrainFlatPagesToRequestedPage,\n\trefusedPage,\n\tresolveContinuation,\n\tresolveCurrentPage,\n\tusableCursor,\n} from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissueSearchCapResultWarning,\n\tissuesUnsupportedWarning,\n\totherWarning,\n\ttruncationWarning,\n\tunsupportedIssueSearchCriteriaWarning,\n\tunusableSearchScopeMessage,\n} from './warnings.js';\n\n/**\n * The FILTERED issue search: issues matching structured criteria over a repository/org scope, with no forced\n * relationship to the current user.\n *\n * A sibling of `listIssuesPage` rather than a mode of it, deliberately. That read is already two divergent\n * branches (a cursor-composite account-wide path and a page-draining repo-scoped one) around a documented\n * contract where `filters` REPLACES the provider's definition of \"my issues\". This search answers a different\n * question — its relationships include two that are not about the user at all — and routes around the SDK's\n * repo-scoped read entirely, whose over-limit recovery walk can spend up to 128 requests and still return an\n * incomplete set. Folding the two together would multiply that branch matrix and put every existing consumer of\n * `listIssuesPage` at risk; keeping them siblings shares the primitives below without sharing the branch space,\n * for the same reason `broaden.ts`, `sweeps.ts` and `issueTracker.ts` are separate files.\n *\n * Contract details worth knowing before calling:\n * - Ordering is `criteria.sort`, most-recently-updated-first by default, and validated like every other criterion\n * against `getSupportedFilters().issueSearch.sorts`. SOME order is always requested — a \"show the N most recent\"\n * policy at the provider's result ceiling is only correct under a guaranteed one, and the ceiling warning names\n * the key it selected the window under. A key the provider can't express refuses the read rather than serving a\n * differently-ordered result; do not change it mid-pagination (a cursor is bound to the order it was made under,\n * and threading it under a different key is refused with a warning + `fetchFailed` — drop the cursor and read\n * from the first page instead).\n * - Paging is cursor-only. `page` alone walks 1..N internally (O(N) requests); a page past the last one is an\n * empty page N, never page N−1 relabeled.\n * - At the provider's result ceiling the read SUCCEEDS and reports an omission carrying the total match count,\n * so a consumer can say how many were withheld. It never falls back to a per-repository recovery walk.\n */\nexport async function searchIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\t/** Repositories to search. Combines with `org`; both become scope constraints on the same query. */\n\t\trepos?: ProviderReposInput;\n\t\t/** Organization/account to search. Combines with `repos`. */\n\t\torg?: string;\n\t\t/**\n\t\t * What to narrow on, validated against `getSupportedFilters().issueSearch` before the read runs. A\n\t\t * criterion the provider can't express server-side refuses the whole read (warning + `fetchFailed`)\n\t\t * rather than serving a list that was never narrowed.\n\t\t */\n\t\tcriteria?: IssueSearchCriteria;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\t/** Per relationship, not per page; see {@link IntegrationManager.searchIssuesPage}. */\n\t\titemsPerPage?: number;\n\t\tforceSync?: boolean;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<IssueShape>> {\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\t// Cursor-only, so a refusal can't claim the requested position — it reports page 1, per\n\t// ProviderPageInfo.currentPage.\n\tconst refused = (warning: ProviderWarning) => refusedPage<IssueShape>(1, [warning], true);\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Filtered issue search'),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(1, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Filtered issue search'),\n\t\t);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\tconst resolved = resolveIssueSearchCriteria(options.providerId, options.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, resolved.rejection),\n\t\t);\n\t}\n\n\t// AFTER the criteria check, which is what reports a provider with no filtered issue search at all: that is\n\t// the more fundamental refusal, and answering \"your scope name is malformed\" to a caller whose provider has\n\t// no such search names the wrong defect. Matches the pull-request twin, which probes existence first.\n\tconst scope = resolveIssueSearchScope(options.providerId, options.repos, options.org, options.criteria);\n\tswitch (scope.rejection?.reason) {\n\t\tcase 'unscoped':\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t'A filtered issue search must be scoped: pass `repos`, `org`, or a relationship to the current user (`authored`/`assigned`/`mentioned`). `any-assignee` and `unassigned` are not scopes — either one alone matches every such issue on the host.',\n\t\t\t\t),\n\t\t\t);\n\t\tcase 'repo-ids':\n\t\t\t// A search query names repositories by PATH, so repository ids can't be expressed as a scope. Refusing\n\t\t\t// is the only honest answer: dropping them would search the whole org (or the whole host) as if the\n\t\t\t// caller had asked for that.\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t'A filtered issue search cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.',\n\t\t\t\t),\n\t\t\t);\n\t\t// A scope the query cannot spell, refused rather than sanitized — see `isUsableSearchScopeName`.\n\t\tcase 'unusable-scope':\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\tunusableSearchScopeMessage('issue search scopes', scope.rejection.scopes),\n\t\t\t\t),\n\t\t\t);\n\t}\n\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.searchIssuesPageResult(\n\t\t\t\t\t{\n\t\t\t\t\t\trepos: scope.repos,\n\t\t\t\t\t\torg: options.org,\n\t\t\t\t\t\tcriteria: options.criteria,\n\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\t},\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\tconst first = await readPage(options.cursor);\n\tconst warnings = first.warning != null ? [first.warning] : [];\n\t// The largest total any page reported. Folded across the walk so a cap detected on page 1 is still reportable\n\t// after paging on to page N — the one thing this read accumulates that its sibling doesn't.\n\tlet totalCount = first.value?.totalCount;\n\tlet limitReached = first.value?.limitReached === true;\n\tconst drained = await drainFlatPagesToRequestedPage(first, {\n\t\trequestedPage: page,\n\t\tsuppliedCursor: options.cursor,\n\t\twarnings: warnings,\n\t\treadPage: readPage,\n\t\tfold: p => {\n\t\t\tif (p.totalCount != null) {\n\t\t\t\ttotalCount = Math.max(totalCount ?? 0, p.totalCount);\n\t\t\t}\n\t\t\tlimitReached ||= p.limitReached === true;\n\t\t},\n\t});\n\tconst { value, currentPage, requestedPageMissing } = drained;\n\tconst pageFetchFailed = drained.fetchFailed;\n\n\t// A provider that doesn't implement the search hook returns `undefined` with no error. Surface that as an\n\t// explicit unsupported warning + fetchFailed rather than a silent empty success, so a caller isn't left\n\t// unable to tell it apart from an account with no matching issues. `getSupportedFilters().issueSearch`\n\t// declares this ahead of the read, so a consumer that checks it never reaches here.\n\tif (value == null && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\tconst items = requestedPageMissing ? [] : (value?.values ?? []);\n\t// Cursor-only: `hasMore` without a real cursor is a dead end, so report it as terminal-but-incomplete rather\n\t// than inviting the caller to page forever.\n\tconst continuation = resolveContinuation(\n\t\t{\n\t\t\thasMore: requestedPageMissing ? false : (value?.hasMore ?? false),\n\t\t\tcursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),\n\t\t\ttruncated: drained.truncated,\n\t\t},\n\t\tundefined,\n\t);\n\tconst truncated = continuation.truncated;\n\t// Only reachable when nothing else already explained the incompleteness — and a failed page always warns\n\t// first, so a read that gets here SUCCEEDED. That is why the cause below is unconditionally `exhausted` and\n\t// `fetchFailed` is never set from here.\n\tif (truncated && warnings.length === 0) {\n\t\t// The result ceiling is the one truncation cause this read can EXPLAIN with a number, so it gets its own\n\t\t// warning carrying the total. Anything else truncated (an unusable continuation, a provider that stopped\n\t\t// advancing) falls back to the generic wording.\n\t\twarnings.push(\n\t\t\tissueSearchCapResultWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\ttotalCount,\n\t\t\t\teffectiveIssueSort(options.criteria?.sort),\n\t\t\t\tlimitReached,\n\t\t\t) ??\n\t\t\t\ttruncationWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t'Issue search',\n\t\t\t\t\t// `exhausted`: this read exposes no budget the caller could raise, so nothing it could call\n\t\t\t\t\t// would return the withheld items.\n\t\t\t\t\t'exhausted',\n\t\t\t\t),\n\t\t);\n\t}\n\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\t// Positional, per ProviderPageInfo.currentPage. A requested page past the terminal cursor is reported\n\t\t\t// as that empty page N; otherwise this read is cursor-only, so a `page` the caller didn't pair with a\n\t\t\t// cursor is never echoed.\n\t\t\tcurrentPage: requestedPageMissing\n\t\t\t\t? page\n\t\t\t\t: resolveCurrentPage({\n\t\t\t\t\t\tproviderPage: currentPage,\n\t\t\t\t\t\trequestedPage: page,\n\t\t\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\t\t\tpageAdvanceable: false,\n\t\t\t\t\t}),\n\t\t\titemsPerPage: items.length,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: pageFetchFailed || undefined,\n\t};\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import "../../../git/models/pullRequest.js";
2
+ import { mergeAssessmentInto } from "../collectionMetadata.js";
2
3
  import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
3
4
  import { drainFlatPagesToRequestedPage, refusedPage, resolveContinuation, resolveCurrentPage, usableCursor } from "./paging.js";
4
5
  import { gitHostOnlySurfaceWarning, otherWarning, pullRequestSearchCapResultWarning, truncationWarning, unsupportedPullRequestSearchCriteriaWarning, unusableSearchScopeMessage } from "./warnings.js";
@@ -51,6 +52,8 @@ async function searchPullRequestsPage(ctx, options) {
51
52
  const first = await readPage(options.cursor);
52
53
  const warnings = first.warning != null ? [first.warning] : [];
53
54
  let totalCount = first.value?.totalCount;
55
+ let lastMetadata = first.value?.metadata;
56
+ let limitReached = first.value?.limitReached === true;
54
57
  const drained = await drainFlatPagesToRequestedPage(first, {
55
58
  requestedPage: page,
56
59
  suppliedCursor: options.cursor,
@@ -58,9 +61,12 @@ async function searchPullRequestsPage(ctx, options) {
58
61
  readPage,
59
62
  fold: (p) => {
60
63
  if (p.totalCount != null) totalCount = Math.max(totalCount ?? 0, p.totalCount);
64
+ lastMetadata = p.metadata;
65
+ limitReached ||= p.limitReached === true;
61
66
  }
62
67
  });
63
68
  const { value, currentPage, requestedPageMissing } = drained;
69
+ const assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, lastMetadata);
64
70
  if (value == null && warnings.length === 0) return refused(unsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, { reason: "unsupported-search" }));
65
71
  const items = requestedPageMissing ? [] : value?.values ?? [];
66
72
  const continuation = resolveContinuation({
@@ -68,8 +74,8 @@ async function searchPullRequestsPage(ctx, options) {
68
74
  cursor: requestedPageMissing ? void 0 : usableCursor(value?.cursor),
69
75
  truncated: drained.truncated
70
76
  }, void 0);
71
- const truncated = continuation.truncated;
72
- if (truncated && warnings.length === 0) warnings.push(pullRequestSearchCapResultWarning(options.providerId, domain, options.connectionId, totalCount, options.criteria?.sort ?? "updated:desc") ?? truncationWarning(options.providerId, domain, options.connectionId, "Pull request search", "exhausted"));
77
+ const truncated = continuation.truncated || assessment.truncated;
78
+ if (truncated && warnings.length === 0) warnings.push(pullRequestSearchCapResultWarning(options.providerId, domain, options.connectionId, totalCount, options.criteria?.sort ?? "updated:desc", limitReached) ?? truncationWarning(options.providerId, domain, options.connectionId, "Pull request search", "exhausted"));
73
79
  return {
74
80
  items,
75
81
  warnings,
@@ -85,7 +91,7 @@ async function searchPullRequestsPage(ctx, options) {
85
91
  },
86
92
  hasMore: continuation.hasMore,
87
93
  cursor: continuation.cursor,
88
- fetchFailed: drained.fetchFailed || void 0
94
+ fetchFailed: drained.fetchFailed || assessment.fetchFailed || void 0
89
95
  };
90
96
  }
91
97
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"searchPullRequests.js","names":[],"sources":["../../../../../plus/integrations/src/reads/searchPullRequests.ts"],"sourcesContent":["import type { PullRequestSearchCriteria, PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { ProviderReposInput } from '../providers/models.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport { resolvePullRequestSearchCriteria, resolvePullRequestSearchScope } from './filters.js';\nimport {\n\tdrainFlatPagesToRequestedPage,\n\trefusedPage,\n\tresolveContinuation,\n\tresolveCurrentPage,\n\tusableCursor,\n} from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\totherWarning,\n\tpullRequestSearchCapResultWarning,\n\ttruncationWarning,\n\tunsupportedPullRequestSearchCriteriaWarning,\n\tunusableSearchScopeMessage,\n} from './warnings.js';\n\n/**\n * Pull requests matching structured criteria over a repository/organization or current-user relationship scope.\n *\n * The read is deliberately separate from `listPullRequestsPage`: the list has provider-specific \"my PRs\"\n * semantics and no text channel, while this method promises a sanitized query, an ordering the caller chooses\n * (`criteria.sort`, defaulting to most-recently-updated-first) and that is applied to the merged page as a whole,\n * one upstream request per cursor-threaded page, and a quantified omission at the provider's result ceiling that\n * names the order the reachable window was selected under.\n */\nexport async function searchPullRequestsPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\trepos?: ProviderReposInput;\n\t\torg?: string;\n\t\tcriteria?: PullRequestSearchCriteria;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\t/** Per relationship × state facet, not per page; see {@link IntegrationManager.searchPullRequestsPage}. */\n\t\titemsPerPage?: number;\n\t\tforceSync?: boolean;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t\t/** See {@link IntegrationManager.searchPullRequestsPage}. */\n\t\tsummary?: boolean;\n\t},\n): Promise<ProviderPagedResult<PullRequestShape>> {\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tconst refused = (warning: ProviderWarning) => refusedPage<PullRequestShape>(1, [warning], true);\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Filtered pull request search',\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(1, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Filtered pull request search',\n\t\t\t),\n\t\t);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\tconst resolved = resolvePullRequestSearchCriteria(options.providerId, options.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tresolved.rejection,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst scope = resolvePullRequestSearchScope(options.providerId, options.repos, options.org, options.criteria);\n\tswitch (scope.rejection?.reason) {\n\t\tcase 'unscoped':\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t'A filtered pull request search must be scoped: pass `repos`, `org`, or at least one current-user relationship.',\n\t\t\t\t),\n\t\t\t);\n\t\tcase 'repo-ids':\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t'A filtered pull request search cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.',\n\t\t\t\t),\n\t\t\t);\n\t\tcase 'unsupported-repository-scope':\n\t\tcase 'unsupported-organization-scope':\n\t\t\treturn refused(\n\t\t\t\tunsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\t\treason: 'unsupported-criteria',\n\t\t\t\t\tcriteria: [\n\t\t\t\t\t\tscope.rejection.reason === 'unsupported-repository-scope'\n\t\t\t\t\t\t\t? 'repositoryScope'\n\t\t\t\t\t\t\t: 'organizationScope',\n\t\t\t\t\t],\n\t\t\t\t}),\n\t\t\t);\n\t\t// A scope the query cannot spell, refused rather than sanitized — see `isUsableSearchScopeName`.\n\t\tcase 'unusable-scope':\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\tunusableSearchScopeMessage('pull request search scopes', scope.rejection.scopes),\n\t\t\t\t),\n\t\t\t);\n\t}\n\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.searchPullRequestsPageResult(\n\t\t\t\t\t{\n\t\t\t\t\t\trepos: scope.repos,\n\t\t\t\t\t\torg: options.org,\n\t\t\t\t\t\tcriteria: options.criteria,\n\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\t\tsummary: options.summary,\n\t\t\t\t\t},\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\tconst first = await readPage(options.cursor);\n\tconst warnings = first.warning != null ? [first.warning] : [];\n\tlet totalCount = first.value?.totalCount;\n\tconst drained = await drainFlatPagesToRequestedPage(first, {\n\t\trequestedPage: page,\n\t\tsuppliedCursor: options.cursor,\n\t\twarnings: warnings,\n\t\treadPage: readPage,\n\t\tfold: p => {\n\t\t\tif (p.totalCount != null) {\n\t\t\t\ttotalCount = Math.max(totalCount ?? 0, p.totalCount);\n\t\t\t}\n\t\t},\n\t});\n\tconst { value, currentPage, requestedPageMissing } = drained;\n\n\tif (value == null && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\tconst items = requestedPageMissing ? [] : (value?.values ?? []);\n\tconst continuation = resolveContinuation(\n\t\t{\n\t\t\thasMore: requestedPageMissing ? false : (value?.hasMore ?? false),\n\t\t\tcursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),\n\t\t\ttruncated: drained.truncated,\n\t\t},\n\t\tundefined,\n\t);\n\tconst truncated = continuation.truncated;\n\tif (truncated && warnings.length === 0) {\n\t\twarnings.push(\n\t\t\tpullRequestSearchCapResultWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\ttotalCount,\n\t\t\t\toptions.criteria?.sort ?? defaultPullRequestSort,\n\t\t\t) ??\n\t\t\t\ttruncationWarning(options.providerId, domain, options.connectionId, 'Pull request search', 'exhausted'),\n\t\t);\n\t}\n\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\tcurrentPage: requestedPageMissing\n\t\t\t\t? page\n\t\t\t\t: resolveCurrentPage({\n\t\t\t\t\t\tproviderPage: currentPage,\n\t\t\t\t\t\trequestedPage: page,\n\t\t\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\t\t\tpageAdvanceable: false,\n\t\t\t\t\t}),\n\t\t\titemsPerPage: items.length,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: drained.fetchFailed || undefined,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAsCA,eAAsB,uBACrB,KACA,SAeiD;CACjD,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,QAAQ,CAAC,CAAC;CACtD,MAAM,WAAW,YAA6B,YAA8B,GAAG,CAAC,OAAO,GAAG,IAAI;CAE9F,IAAI,0BAA0B,QAAQ,UAAU,GAC/C,OAAO,QACN,0BACC,QAAQ,YACR,KAAA,GACA,QAAQ,cACR,8BACD,CACD;CAGD,MAAM,cAAc,MAAM,IAAI,sBAAsB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAC5G,IAAI,eAAe,MAAM;EACxB,MAAM,QAAQ,IAAI,8BAA8B,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;EACxG,OAAO,YAAY,GAAG,MAAM,UAAU,MAAM,WAAW;CACxD;CACA,IAAI,CAAC,qBAAqB,WAAW,GACpC,OAAO,QACN,0BACC,QAAQ,YACR,KAAA,GACA,QAAQ,cACR,8BACD,CACD;CAGD,MAAM,IAAI,wBAAwB,aAAa,QAAQ,WAAW,QAAQ,YAAY;CAEtF,MAAM,SAAS,IAAI,cAAc,aAAa,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CACtG,MAAM,uBAAuB,8BAA8B,QAAQ,YAAY,QAAQ,MAAM;CAE7F,MAAM,WAAW,iCAAiC,QAAQ,YAAY,QAAQ,QAAQ;CACtF,IAAI,SAAS,aAAa,MACzB,OAAO,QACN,4CACC,QAAQ,YACR,QACA,QAAQ,cACR,SAAS,SACV,CACD;CAGD,MAAM,QAAQ,8BAA8B,QAAQ,YAAY,QAAQ,OAAO,QAAQ,KAAK,QAAQ,QAAQ;CAC5G,QAAQ,MAAM,WAAW,QAAzB;EACC,KAAK,YACJ,OAAO,QACN,aACC,QAAQ,YACR,QACA,QAAQ,cACR,gHACD,CACD;EACD,KAAK,YACJ,OAAO,QACN,aACC,QAAQ,YACR,QACA,QAAQ,cACR,2HACD,CACD;EACD,KAAK;EACL,KAAK,kCACJ,OAAO,QACN,4CAA4C,QAAQ,YAAY,QAAQ,QAAQ,cAAc;GAC7F,QAAQ;GACR,UAAU,CACT,MAAM,UAAU,WAAW,iCACxB,oBACA,mBACJ;EACD,CAAC,CACF;EAED,KAAK,kBACJ,OAAO,QACN,aACC,QAAQ,YACR,QACA,QAAQ,cACR,2BAA2B,8BAA8B,MAAM,UAAU,MAAM,CAChF,CACD;CACF;CAEA,MAAM,YAAY,WACjB,YACC,QAAQ,YACR,QACA,QAAQ,oBAEP,YAAY,6BACX;EACC,OAAO,MAAM;EACb,KAAK,QAAQ;EACb,UAAU,QAAQ;EACV;EACR,UAAU,QAAQ;EAClB,SAAS,QAAQ;CAClB,GACA,KAAA,GACA,QAAQ,YACT,GACD,EAAwB,qBAAqB,CAC9C;CAED,MAAM,QAAQ,MAAM,SAAS,QAAQ,MAAM;CAC3C,MAAM,WAAW,MAAM,WAAW,OAAO,CAAC,MAAM,OAAO,IAAI,CAAC;CAC5D,IAAI,aAAa,MAAM,OAAO;CAC9B,MAAM,UAAU,MAAM,8BAA8B,OAAO;EAC1D,eAAe;EACf,gBAAgB,QAAQ;EACd;EACA;EACV,OAAM,MAAK;GACV,IAAI,EAAE,cAAc,MACnB,aAAa,KAAK,IAAI,cAAc,GAAG,EAAE,UAAU;EAErD;CACD,CAAC;CACD,MAAM,EAAE,OAAO,aAAa,yBAAyB;CAErD,IAAI,SAAS,QAAQ,SAAS,WAAW,GACxC,OAAO,QACN,4CAA4C,QAAQ,YAAY,QAAQ,QAAQ,cAAc,EAC7F,QAAQ,qBACT,CAAC,CACF;CAGD,MAAM,QAAQ,uBAAuB,CAAC,IAAK,OAAO,UAAU,CAAC;CAC7D,MAAM,eAAe,oBACpB;EACC,SAAS,uBAAuB,QAAS,OAAO,WAAW;EAC3D,QAAQ,uBAAuB,KAAA,IAAY,aAAa,OAAO,MAAM;EACrE,WAAW,QAAQ;CACpB,GACA,KAAA,CACD;CACA,MAAM,YAAY,aAAa;CAC/B,IAAI,aAAa,SAAS,WAAW,GACpC,SAAS,KACR,kCACC,QAAQ,YACR,QACA,QAAQ,cACR,YACA,QAAQ,UAAU,QAAA,cACnB,KACC,kBAAkB,QAAQ,YAAY,QAAQ,QAAQ,cAAc,uBAAuB,WAAW,CACxG;CAGD,OAAO;EACC;EACG;EACV,MAAM;GACL,aAAa,uBACV,OACA,mBAAmB;IACnB,cAAc;IACd,eAAe;IACf,gBAAgB,QAAQ;IACxB,iBAAiB;GAClB,CAAC;GACH,cAAc,MAAM;GACpB,WAAW,aAAa,KAAA;EACzB;EACA,SAAS,aAAa;EACtB,QAAQ,aAAa;EACrB,aAAa,QAAQ,eAAe,KAAA;CACrC;AACD"}
1
+ {"version":3,"file":"searchPullRequests.js","names":[],"sources":["../../../../../plus/integrations/src/reads/searchPullRequests.ts"],"sourcesContent":["import type { PullRequestSearchCriteria, PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort } from '@gitlens/git/models/pullRequest.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport type { ProviderReposInput } from '../providers/models.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport { resolvePullRequestSearchCriteria, resolvePullRequestSearchScope } from './filters.js';\nimport {\n\tdrainFlatPagesToRequestedPage,\n\trefusedPage,\n\tresolveContinuation,\n\tresolveCurrentPage,\n\tusableCursor,\n} from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\totherWarning,\n\tpullRequestSearchCapResultWarning,\n\ttruncationWarning,\n\tunsupportedPullRequestSearchCriteriaWarning,\n\tunusableSearchScopeMessage,\n} from './warnings.js';\n\n/**\n * Pull requests matching structured criteria over a repository/organization or current-user relationship scope.\n *\n * The read is deliberately separate from `listPullRequestsPage`: the list has provider-specific \"my PRs\"\n * semantics and no text channel, while this method promises a sanitized query, an ordering the caller chooses\n * (`criteria.sort`, defaulting to most-recently-updated-first) and that is applied to the merged page as a whole,\n * one upstream request per cursor-threaded page, and a quantified omission at the provider's result ceiling that\n * names the order the reachable window was selected under.\n */\nexport async function searchPullRequestsPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\trepos?: ProviderReposInput;\n\t\torg?: string;\n\t\tcriteria?: PullRequestSearchCriteria;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\t/** Per relationship × state facet, not per page; see {@link IntegrationManager.searchPullRequestsPage}. */\n\t\titemsPerPage?: number;\n\t\tforceSync?: boolean;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t\t/** See {@link IntegrationManager.searchPullRequestsPage}. */\n\t\tsummary?: boolean;\n\t},\n): Promise<ProviderPagedResult<PullRequestShape>> {\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tconst refused = (warning: ProviderWarning) => refusedPage<PullRequestShape>(1, [warning], true);\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Filtered pull request search',\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(1, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'Filtered pull request search',\n\t\t\t),\n\t\t);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\tconst resolved = resolvePullRequestSearchCriteria(options.providerId, options.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tresolved.rejection,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst scope = resolvePullRequestSearchScope(options.providerId, options.repos, options.org, options.criteria);\n\tswitch (scope.rejection?.reason) {\n\t\tcase 'unscoped':\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t'A filtered pull request search must be scoped: pass `repos`, `org`, or at least one current-user relationship.',\n\t\t\t\t),\n\t\t\t);\n\t\tcase 'repo-ids':\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t'A filtered pull request search cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.',\n\t\t\t\t),\n\t\t\t);\n\t\tcase 'unsupported-repository-scope':\n\t\tcase 'unsupported-organization-scope':\n\t\t\treturn refused(\n\t\t\t\tunsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\t\treason: 'unsupported-criteria',\n\t\t\t\t\tcriteria: [\n\t\t\t\t\t\tscope.rejection.reason === 'unsupported-repository-scope'\n\t\t\t\t\t\t\t? 'repositoryScope'\n\t\t\t\t\t\t\t: 'organizationScope',\n\t\t\t\t\t],\n\t\t\t\t}),\n\t\t\t);\n\t\t// A scope the query cannot spell, refused rather than sanitized — see `isUsableSearchScopeName`.\n\t\tcase 'unusable-scope':\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\tunusableSearchScopeMessage('pull request search scopes', scope.rejection.scopes),\n\t\t\t\t),\n\t\t\t);\n\t}\n\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.searchPullRequestsPageResult(\n\t\t\t\t\t{\n\t\t\t\t\t\trepos: scope.repos,\n\t\t\t\t\t\torg: options.org,\n\t\t\t\t\t\tcriteria: options.criteria,\n\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\tpageSize: options.itemsPerPage,\n\t\t\t\t\t\tsummary: options.summary,\n\t\t\t\t\t},\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\tconst first = await readPage(options.cursor);\n\tconst warnings = first.warning != null ? [first.warning] : [];\n\tlet totalCount = first.value?.totalCount;\n\t// Facets whose rows are missing while their siblings answered. A page reports the walk's completeness so far, not\n\t// just its own requests: a failure dropped for good repeats on every later page, while one that recovered on a\n\t// retry drops out because its rows came back. So the latest page read is the one to assess, including when a later\n\t// page fails the walk outright — never union the pages, which would bring a recovered failure back.\n\tlet lastMetadata = first.value?.metadata;\n\tlet limitReached = first.value?.limitReached === true;\n\tconst drained = await drainFlatPagesToRequestedPage(first, {\n\t\trequestedPage: page,\n\t\tsuppliedCursor: options.cursor,\n\t\twarnings: warnings,\n\t\treadPage: readPage,\n\t\tfold: p => {\n\t\t\tif (p.totalCount != null) {\n\t\t\t\ttotalCount = Math.max(totalCount ?? 0, p.totalCount);\n\t\t\t}\n\t\t\tlastMetadata = p.metadata;\n\t\t\tlimitReached ||= p.limitReached === true;\n\t\t},\n\t});\n\tconst { value, currentPage, requestedPageMissing } = drained;\n\tconst assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, lastMetadata);\n\n\tif (value == null && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\tconst items = requestedPageMissing ? [] : (value?.values ?? []);\n\tconst continuation = resolveContinuation(\n\t\t{\n\t\t\thasMore: requestedPageMissing ? false : (value?.hasMore ?? false),\n\t\t\tcursor: requestedPageMissing ? undefined : usableCursor(value?.cursor),\n\t\t\ttruncated: drained.truncated,\n\t\t},\n\t\tundefined,\n\t);\n\t// A facet that failed while its siblings answered leaves the result incomplete, as `listPullRequestsPage` reports\n\t// it; its warning is already in `warnings`, so it adds no generic one below.\n\tconst truncated = continuation.truncated || assessment.truncated;\n\tif (truncated && warnings.length === 0) {\n\t\twarnings.push(\n\t\t\tpullRequestSearchCapResultWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\ttotalCount,\n\t\t\t\toptions.criteria?.sort ?? defaultPullRequestSort,\n\t\t\t\tlimitReached,\n\t\t\t) ??\n\t\t\t\ttruncationWarning(options.providerId, domain, options.connectionId, 'Pull request search', 'exhausted'),\n\t\t);\n\t}\n\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\tpage: {\n\t\t\tcurrentPage: requestedPageMissing\n\t\t\t\t? page\n\t\t\t\t: resolveCurrentPage({\n\t\t\t\t\t\tproviderPage: currentPage,\n\t\t\t\t\t\trequestedPage: page,\n\t\t\t\t\t\tsuppliedCursor: options.cursor,\n\t\t\t\t\t\tpageAdvanceable: false,\n\t\t\t\t\t}),\n\t\t\titemsPerPage: items.length,\n\t\t\ttruncated: truncated || undefined,\n\t\t},\n\t\thasMore: continuation.hasMore,\n\t\tcursor: continuation.cursor,\n\t\tfetchFailed: drained.fetchFailed || assessment.fetchFailed || undefined,\n\t};\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { mapBounded } from "../../../utils/promise.js";
2
- import { appendDedupedWarning } from "../results.js";
3
2
  import "../constants.js";
3
+ import { appendDedupedWarning } from "../results.js";
4
4
  import { isGitHostIntegration, isIssuesHostIntegrationId } from "../utils/integration.utils.js";
5
5
  import { fromProviderPullRequest } from "../providers/models.js";
6
6
  import { gitHostOnlySurfaceWarning, noConnectionWarning, unsupportedAccountWidePullRequestFiltersWarning, unsupportedFiltersWarning } from "./warnings.js";
@@ -12,11 +12,14 @@ export interface TrackerIssueResult {
12
12
  issue?: IssueShape;
13
13
  }
14
14
  /**
15
- * Takes no `domain`: the point read resolves a host-keyed tracker's primary connection, which is only safe
16
- * while no such tracker implements `getProviderIssueByResourceId` (the `supportsIssueLookupByResourceId`
17
- * guard below refuses before a host is resolved). Thread `domain` through, as `listIssueTrackerIssuesPage`
18
- * now does, before adding that point read to a self-managed tracker — otherwise the read silently answers
19
- * from whichever host happens to be primary (#5872).
15
+ * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for
16
+ * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host.
17
+ *
18
+ * Unlike its siblings, this read never falls back to the primary connection for a self-managed tracker: it
19
+ * requires a `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted
20
+ * instances routinely issue the same project and issue keys, and this read's `issue: undefined` is a proven
21
+ * absence a caller may cache — an answer from whichever host happens to be primary would be cached under a key
22
+ * that names a different instance (#5872).
20
23
  */
21
24
  export declare function getTrackerIssue(ctx: ProviderReadContext, options: {
22
25
  providerId: IntegrationIds;
@@ -24,6 +27,7 @@ export declare function getTrackerIssue(ctx: ProviderReadContext, options: {
24
27
  resourceUrl?: string;
25
28
  key: string;
26
29
  connectionId?: string;
30
+ domain?: string;
27
31
  }): Promise<ProviderResult<TrackerIssueResult>>;
28
32
  //#endregion
29
33
  //# sourceMappingURL=trackerIssue.d.ts.map