@gitkraken/core-gitlens 0.4.100 → 0.4.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -1
- package/dist/plus/integrations/errors.d.ts.map +1 -1
- package/dist/plus/integrations/errors.js +6 -2
- package/dist/plus/integrations/errors.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +8 -5
- package/dist/plus/integrations/integrationService.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.js +20 -652
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +10 -17
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +38 -88
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.d.ts +4 -21
- package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.js +5 -47
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.d.ts +20 -0
- package/dist/plus/integrations/reads/broaden.d.ts.map +1 -0
- package/dist/plus/integrations/reads/broaden.js +326 -0
- package/dist/plus/integrations/reads/broaden.js.map +1 -0
- package/dist/plus/integrations/reads/context.d.ts +22 -0
- package/dist/plus/integrations/reads/context.d.ts.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.d.ts +1 -1
- package/dist/plus/integrations/reads/hierarchy.d.ts.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +109 -137
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.utils.d.ts +25 -1
- package/dist/plus/integrations/reads/hierarchy.utils.d.ts.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.utils.js +43 -0
- package/dist/plus/integrations/reads/hierarchy.utils.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +13 -25
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts +2 -2
- package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.d.ts +1 -1
- package/dist/plus/integrations/reads/pullRequests.d.ts.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +1 -1
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/resolveRepository.d.ts +25 -0
- package/dist/plus/integrations/reads/resolveRepository.d.ts.map +1 -0
- package/dist/plus/integrations/reads/resolveRepository.js +235 -0
- package/dist/plus/integrations/reads/resolveRepository.js.map +1 -0
- package/dist/plus/integrations/reads/sweeps.d.ts +11 -0
- package/dist/plus/integrations/reads/sweeps.d.ts.map +1 -0
- package/dist/plus/integrations/reads/sweeps.js +125 -0
- package/dist/plus/integrations/reads/sweeps.js.map +1 -0
- package/package.json +1 -1
- package/src/plus/integrations/errors.ts +6 -2
- package/src/plus/integrations/integrationService.ts +27 -801
- package/src/plus/integrations/models/gitHostIntegration.ts +71 -93
- package/src/plus/integrations/models/issuesIntegration.ts +5 -52
- package/src/plus/integrations/providers/azureDevOps.ts +1 -1
- package/src/plus/integrations/reads/broaden.ts +452 -0
- package/src/plus/integrations/reads/context.ts +24 -0
- package/src/plus/integrations/reads/hierarchy.ts +133 -153
- package/src/plus/integrations/reads/hierarchy.utils.ts +50 -1
- package/src/plus/integrations/reads/issueTracker.ts +15 -27
- package/src/plus/integrations/reads/issues.ts +4 -2
- package/src/plus/integrations/reads/pullRequests.ts +2 -9
- package/src/plus/integrations/reads/resolveRepository.ts +272 -0
- package/src/plus/integrations/reads/sweeps.ts +185 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { RemoteProviderConfig } from '../../../git/remotes/matcher.js';
|
|
2
|
+
import { createRemoteProviderMatcher } from '../../../git/remotes/matcher.js';
|
|
3
|
+
import { parseGitRemoteUrl } from '../../../git/utils/remote.utils.js';
|
|
4
|
+
import type { IntegrationIds } from '../constants.js';
|
|
5
|
+
import { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';
|
|
6
|
+
import { AuthenticationError, RequestNotFoundError } from '../errors.js';
|
|
7
|
+
import { isIssuesIntegration } from '../models/issuesIntegration.js';
|
|
8
|
+
import { isAzureCloudDomain, isBitbucketCloudDomain, isGitHubDotCom, isGitLabDotCom } from '../providers/models.js';
|
|
9
|
+
import type { RepositoryIdentity, RepositoryResolution, ResolveRepositoryResult } from '../results.js';
|
|
10
|
+
import { toProviderWarning } from '../results.js';
|
|
11
|
+
import { hostFromDomain } from '../utils/domain.utils.js';
|
|
12
|
+
import {
|
|
13
|
+
getIntegrationIdForRemote,
|
|
14
|
+
isGitCloudHostIntegrationId,
|
|
15
|
+
isGitSelfManagedHostIntegrationId,
|
|
16
|
+
remoteProviderTypeForConfig,
|
|
17
|
+
remoteProviderTypeForIntegration,
|
|
18
|
+
} from '../utils/integration.utils.js';
|
|
19
|
+
import type { RepositoryResolutionContext } from './context.js';
|
|
20
|
+
import { noConnectionWarning } from './warnings.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolution of a remote URL to a provider repository identity — the facade's equivalent of `gk repo resolve`.
|
|
24
|
+
*
|
|
25
|
+
* Unlike every other read here, this one's input is REPOSITORY data, so most of the code below is about not
|
|
26
|
+
* letting that data choose the credentials: an explicit cloud `providerId` must agree with the remote's canonical
|
|
27
|
+
* host, and a self-managed host resolves only against a domain the user already authenticated. The
|
|
28
|
+
* `host-mismatch` returns are those guards, not edge cases — resolving `owner/repo` against the wrong account
|
|
29
|
+
* would hand back a confidently wrong identity and seed the domain-keyed instance cache from remote data.
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveRepository(
|
|
32
|
+
ctx: RepositoryResolutionContext,
|
|
33
|
+
options: {
|
|
34
|
+
providerId?: IntegrationIds;
|
|
35
|
+
remoteUrl: string;
|
|
36
|
+
host?: string;
|
|
37
|
+
connectionId?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Explicit self-managed host domain used to select the integration instance. Used only when the requested
|
|
40
|
+
* connection has no configured domain; it must come from the trusted authentication configuration, not
|
|
41
|
+
* repository or remote data.
|
|
42
|
+
*/
|
|
43
|
+
domain?: string;
|
|
44
|
+
},
|
|
45
|
+
): Promise<ResolveRepositoryResult> {
|
|
46
|
+
const result = (status: RepositoryResolution['status']): ResolveRepositoryResult => ({
|
|
47
|
+
resolution: { status: status },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const [scheme, parsedDomain, path] = parseGitRemoteUrl(options.remoteUrl);
|
|
51
|
+
const parsedHost = hostFromDomain(parsedDomain);
|
|
52
|
+
const explicitHost = hostFromDomain(options.host);
|
|
53
|
+
if (parsedHost != null && explicitHost != null && parsedHost !== explicitHost) {
|
|
54
|
+
return result('host-mismatch');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const matcherDomain = parsedDomain || options.host || '';
|
|
58
|
+
|
|
59
|
+
// An explicit cloud provider must agree with the remote's canonical cloud host. Without this guard the
|
|
60
|
+
// synthetic matcher below can reinterpret, for example, a gitlab.com URL as GitHub and then resolve a
|
|
61
|
+
// homonymous owner/repo through the wrong account. Match once without caller configs/synthetic entries;
|
|
62
|
+
// Azure's matcher also normalizes ssh.dev.azure.com and vs-ssh.visualstudio.com to their web host.
|
|
63
|
+
if (options.providerId != null && isGitCloudHostIntegrationId(options.providerId)) {
|
|
64
|
+
const nativeProvider = createRemoteProviderMatcher([])(options.remoteUrl, matcherDomain, path, scheme);
|
|
65
|
+
const nativeId = nativeProvider != null ? getIntegrationIdForRemote(nativeProvider) : undefined;
|
|
66
|
+
const nativeDomain = nativeProvider?.domain ?? matcherDomain;
|
|
67
|
+
const canonicalHost =
|
|
68
|
+
options.providerId === GitCloudHostIntegrationId.GitHub
|
|
69
|
+
? isGitHubDotCom(nativeDomain)
|
|
70
|
+
: options.providerId === GitCloudHostIntegrationId.GitLab
|
|
71
|
+
? isGitLabDotCom(nativeDomain)
|
|
72
|
+
: options.providerId === GitCloudHostIntegrationId.Bitbucket
|
|
73
|
+
? isBitbucketCloudDomain(nativeDomain)
|
|
74
|
+
: isAzureCloudDomain(nativeDomain);
|
|
75
|
+
if (nativeId !== options.providerId || !canonicalHost) return result('host-mismatch');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const provider = createRemoteProviderMatcher(buildMatcherConfigs(ctx, options, parsedDomain))(
|
|
79
|
+
options.remoteUrl,
|
|
80
|
+
matcherDomain,
|
|
81
|
+
path,
|
|
82
|
+
scheme,
|
|
83
|
+
);
|
|
84
|
+
if (provider == null) return result('invalid-remote-url');
|
|
85
|
+
|
|
86
|
+
let id = options.providerId ?? getIntegrationIdForRemote(provider);
|
|
87
|
+
// Custom Azure DevOps Server domains matched via getRemoteConfigs return undefined from
|
|
88
|
+
// getIntegrationIdForRemote because the provider is marked custom; map them to the server id so the
|
|
89
|
+
// project-scoped lookup uses that host's configured connection and API base URL.
|
|
90
|
+
if (id == null && provider.id === 'azure-devops' && provider.custom) {
|
|
91
|
+
id = GitSelfManagedHostIntegrationId.AzureDevOpsServer;
|
|
92
|
+
}
|
|
93
|
+
if (id == null) return result('unsupported-provider');
|
|
94
|
+
|
|
95
|
+
const owner = provider.owner;
|
|
96
|
+
const name = provider.repoName;
|
|
97
|
+
if (owner == null || name == null) return result('invalid-remote-url');
|
|
98
|
+
|
|
99
|
+
// On a self-managed host, resolve only against a TRUSTED host: the pinned connection's configured
|
|
100
|
+
// domain, the explicit `domain`, or — when neither was supplied — a configured host matching the
|
|
101
|
+
// remote's. That last case keeps `remoteUrl` (repository-supplied) out of the trusted path: it selects
|
|
102
|
+
// among hosts the user already authenticated and can never introduce a new one. Resolving `owner/repo`
|
|
103
|
+
// against some other host's account would, if that host has the same owner/repo, return a confidently
|
|
104
|
+
// wrong identity — and would seed the domain-keyed integration cache (never evicted before dispose) with
|
|
105
|
+
// an entry derived from repository data.
|
|
106
|
+
const urlHost = hostFromDomain(provider.domain);
|
|
107
|
+
// Normalize BOTH sides before comparing: a stored connection domain is usually a full URL
|
|
108
|
+
// (`https://git.example.com`) while `urlHost` is already a bare host, so a raw compare would fail on
|
|
109
|
+
// scheme/trailing-slash alone and wrongly reject a correctly-configured connection.
|
|
110
|
+
let trustedHost =
|
|
111
|
+
options.connectionId != null || options.domain != null
|
|
112
|
+
? hostFromDomain(ctx.resolveDomainForRead(id, options.connectionId, options.domain))
|
|
113
|
+
: undefined;
|
|
114
|
+
if (isGitSelfManagedHostIntegrationId(id)) {
|
|
115
|
+
const configuredHosts = ctx.getConfigured(id).map(c => hostFromDomain(c.domain));
|
|
116
|
+
if (trustedHost == null && urlHost != null) {
|
|
117
|
+
trustedHost = configuredHosts.find(host => host === urlHost);
|
|
118
|
+
}
|
|
119
|
+
// A configured host that doesn't match the remote's is a genuine mismatch. With nothing configured
|
|
120
|
+
// there is no host to mismatch against — it's simply not connected, so fall through to the
|
|
121
|
+
// `unauthorized` path below (still WITHOUT resolving by the URL domain, so repo data can't seed the
|
|
122
|
+
// cache).
|
|
123
|
+
if (trustedHost == null && configuredHosts.length !== 0) return result('host-mismatch');
|
|
124
|
+
if (trustedHost != null && urlHost != null && trustedHost !== urlHost) return result('host-mismatch');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let integration;
|
|
128
|
+
try {
|
|
129
|
+
// Resolve the instance through the trusted target (as `getIntegrationForRead` does):
|
|
130
|
+
// `resolveReadSession` looks the session up against the instance's domain-scoped descriptor, so
|
|
131
|
+
// selecting the instance by the URL domain could miss the session and degrade to `no-connection`.
|
|
132
|
+
integration = isGitSelfManagedHostIntegrationId(id)
|
|
133
|
+
? trustedHost != null
|
|
134
|
+
? await ctx.getIntegrationForRead(id, options.connectionId, trustedHost)
|
|
135
|
+
: undefined
|
|
136
|
+
: await ctx.getIntegrationForDomain(id, provider.domain);
|
|
137
|
+
} catch {
|
|
138
|
+
integration = undefined;
|
|
139
|
+
}
|
|
140
|
+
if (integration == null) {
|
|
141
|
+
// Attach a warning, as the thrown-AuthenticationError branch below does: a consumer driving auth
|
|
142
|
+
// recovery off `warning.kind` would otherwise get nothing for an unresolvable target.
|
|
143
|
+
return {
|
|
144
|
+
resolution: {
|
|
145
|
+
status: 'unauthorized',
|
|
146
|
+
warning: noConnectionWarning(id, provider.domain, options.connectionId),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Issue trackers have no `getRepo` client; a git host without `getRepoFn` leaves `getRepoInfo`
|
|
151
|
+
// undefined. Either way this provider can't resolve repositories.
|
|
152
|
+
if (isIssuesIntegration(integration) || integration.getRepoInfo == null) {
|
|
153
|
+
return result('unsupported-provider');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Azure repos are org + project scoped; the remote provider exposes project as `providerDesc.repoDomain`.
|
|
157
|
+
const project = provider.id === 'azure-devops' ? provider.providerDesc?.repoDomain : undefined;
|
|
158
|
+
const domain = provider.domain;
|
|
159
|
+
|
|
160
|
+
// Azure's lookup is project-scoped and returns `undefined` without one, which the connection-gap branch
|
|
161
|
+
// below would misreport as `unauthorized` (driving a pointless reconnect). A remote URL that carries no
|
|
162
|
+
// project simply can't address an Azure repo.
|
|
163
|
+
if (provider.id === 'azure-devops' && project == null) return result('invalid-remote-url');
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const repo = await integration.getRepoInfo({
|
|
167
|
+
owner: owner,
|
|
168
|
+
name: name,
|
|
169
|
+
project: project,
|
|
170
|
+
connectionId: options.connectionId,
|
|
171
|
+
});
|
|
172
|
+
if (repo == null) {
|
|
173
|
+
// `getRepoInfo` returns undefined only when no session could be resolved (not connected, or the
|
|
174
|
+
// requested connection is gone) — a real 404 throws below. So this is a connection gap. Attach a
|
|
175
|
+
// warning so a consumer driving auth recovery off `warning.kind` sees the same signal it gets from
|
|
176
|
+
// the thrown-AuthenticationError branch below; without it, the most common unauthorized case is
|
|
177
|
+
// silent.
|
|
178
|
+
return {
|
|
179
|
+
resolution: {
|
|
180
|
+
status: 'unauthorized',
|
|
181
|
+
warning: noConnectionWarning(id, domain, options.connectionId),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Prefer the provider's canonical namespace/name (GitHub's REST/GraphQL lookup follows the 301
|
|
187
|
+
// rename redirect, so a stale old name resolves to the new canonical identity), falling back to the
|
|
188
|
+
// parsed remote when the response omits them. `renamed` is a case-insensitive compare of input vs
|
|
189
|
+
// canonical, mirroring gkcli's `EqualFold`, so hosts that merely echo the input casing (e.g.
|
|
190
|
+
// Bitbucket Server/Azure) don't get spuriously flagged.
|
|
191
|
+
const canonicalOwner = repo.namespace || owner;
|
|
192
|
+
const canonicalName = repo.name || name;
|
|
193
|
+
const renamed =
|
|
194
|
+
canonicalOwner.toLowerCase() !== owner.toLowerCase() || canonicalName.toLowerCase() !== name.toLowerCase();
|
|
195
|
+
|
|
196
|
+
const identity: RepositoryIdentity = {
|
|
197
|
+
providerId: id,
|
|
198
|
+
domain: domain,
|
|
199
|
+
owner: canonicalOwner,
|
|
200
|
+
name: canonicalName,
|
|
201
|
+
project: project,
|
|
202
|
+
remoteUrl: options.remoteUrl,
|
|
203
|
+
renamed: renamed,
|
|
204
|
+
};
|
|
205
|
+
return { resolution: { status: 'resolved', identity: identity } };
|
|
206
|
+
} catch (ex) {
|
|
207
|
+
// Order matters: 404 throws RequestNotFoundError (not `undefined`), so check not-found before auth
|
|
208
|
+
// and before the generic 5xx/unknown bucket — never classify a 401/403 as not-found.
|
|
209
|
+
let resolution: RepositoryResolution;
|
|
210
|
+
if (ex instanceof RequestNotFoundError) {
|
|
211
|
+
resolution = { status: 'not-found' };
|
|
212
|
+
} else if (ex instanceof AuthenticationError) {
|
|
213
|
+
resolution = { status: 'unauthorized', warning: toProviderWarning(id, domain, options.connectionId, ex) };
|
|
214
|
+
} else {
|
|
215
|
+
resolution = { status: 'undetermined', warning: toProviderWarning(id, domain, options.connectionId, ex) };
|
|
216
|
+
}
|
|
217
|
+
return { resolution: resolution };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The remote-matcher configs for this resolution: the user's own host remote configs (self-managed/custom
|
|
223
|
+
* domains), plus a synthetic exact-domain entry for an explicit `providerId` + host so a custom domain still maps
|
|
224
|
+
* to the right provider for path parsing.
|
|
225
|
+
*/
|
|
226
|
+
function buildMatcherConfigs(
|
|
227
|
+
ctx: RepositoryResolutionContext,
|
|
228
|
+
options: { providerId?: IntegrationIds; host?: string },
|
|
229
|
+
parsedDomain: string,
|
|
230
|
+
): RemoteProviderConfig[] {
|
|
231
|
+
const configs: RemoteProviderConfig[] = [];
|
|
232
|
+
for (const cfg of ctx.getRemoteConfigs()) {
|
|
233
|
+
const type = remoteProviderTypeForConfig(cfg.type);
|
|
234
|
+
if (type == null) continue;
|
|
235
|
+
|
|
236
|
+
// Forward both domain- and regex-based custom remotes (carrying any protocol override), so a
|
|
237
|
+
// regex-configured host resolves instead of falling through to `unsupported`.
|
|
238
|
+
if (cfg.domain) {
|
|
239
|
+
configs.push({ type: type, domain: cfg.domain, protocol: cfg.protocol });
|
|
240
|
+
} else if (cfg.regex) {
|
|
241
|
+
configs.push({ type: type, regex: cfg.regex, protocol: cfg.protocol });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (options.providerId == null) return configs;
|
|
245
|
+
|
|
246
|
+
const type = remoteProviderTypeForIntegration(options.providerId);
|
|
247
|
+
const domain = parsedDomain || options.host;
|
|
248
|
+
if (type == null || !domain) return configs;
|
|
249
|
+
|
|
250
|
+
// The synthetic exact-domain entry is unshifted to the front, so it wins the match over the user's own
|
|
251
|
+
// config for the same host. Carry that config's protocol override across (matched by domain or regex,
|
|
252
|
+
// mirroring `ignoreSSLErrors`) so a self-managed host configured for a custom protocol — e.g. plain
|
|
253
|
+
// `http` — isn't silently downgraded to the provider default here.
|
|
254
|
+
const lowerDomain = domain.toLowerCase();
|
|
255
|
+
const protocol = configs.find(c => {
|
|
256
|
+
if (c.type !== type) return false;
|
|
257
|
+
if (c.domain != null) return c.domain.toLowerCase() === lowerDomain;
|
|
258
|
+
|
|
259
|
+
// Truthy (not just non-null): an empty regex would compile to a match-everything pattern.
|
|
260
|
+
if (c.regex) {
|
|
261
|
+
try {
|
|
262
|
+
return new RegExp(c.regex, 'i').test(lowerDomain);
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return false;
|
|
269
|
+
})?.protocol;
|
|
270
|
+
configs.unshift({ type: type, domain: domain, protocol: protocol });
|
|
271
|
+
return configs;
|
|
272
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { PullRequestShape } from '../../../git/models/pullRequest.js';
|
|
2
|
+
import { mapBounded } from '../../../utils/promise.js';
|
|
3
|
+
import type { IntegrationIds } from '../constants.js';
|
|
4
|
+
import { providerFanOutConcurrency } from '../constants.js';
|
|
5
|
+
import type { ClosedPullRequestSweepOptions, PullRequestSweepOptions } from '../manager.js';
|
|
6
|
+
import { fromProviderPullRequest } from '../providers/models.js';
|
|
7
|
+
import type { ProviderSweepResult, ProviderWarning } from '../results.js';
|
|
8
|
+
import { appendDedupedWarning } from '../results.js';
|
|
9
|
+
import { isGitHostIntegration, isIssuesHostIntegrationId } from '../utils/integration.utils.js';
|
|
10
|
+
import type { ProviderReadContext } from './context.js';
|
|
11
|
+
import { drainPullRequests, getCurrentAccountId, resolvePullRequestSweepTargets } from './drains.js';
|
|
12
|
+
import { resolveAccountWidePullRequestFilters, resolvePullRequestFilters } from './filters.js';
|
|
13
|
+
import {
|
|
14
|
+
gitHostOnlySurfaceWarning,
|
|
15
|
+
noConnectionWarning,
|
|
16
|
+
unsupportedAccountWidePullRequestFiltersWarning,
|
|
17
|
+
unsupportedFiltersWarning,
|
|
18
|
+
} from './warnings.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The all-pages pull request reads: one drain per provider target, aggregated into a single sweep result.
|
|
22
|
+
*
|
|
23
|
+
* A sweep exposes no continuation of its own — it drains everything it can reach — so its whole contract lives in
|
|
24
|
+
* the completeness signals it returns: `page.allPages` asserts every page of every target was read,
|
|
25
|
+
* `page.truncated` says pages remain unread, and `failedProviderIds`/`incompleteProviderIds` separate a provider
|
|
26
|
+
* that produced nothing from one that produced a slice with a gap. That split is why the aggregation below can't
|
|
27
|
+
* be collapsed into the generic fan-out merge the hierarchy reads use.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** One provider target's drained slice, before the cross-provider aggregation. */
|
|
31
|
+
interface SweepSlice {
|
|
32
|
+
items: PullRequestShape[];
|
|
33
|
+
warnings: ProviderWarning[];
|
|
34
|
+
fetchFailed: boolean;
|
|
35
|
+
truncated: boolean;
|
|
36
|
+
providerId: IntegrationIds;
|
|
37
|
+
failedProvider: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function sweepPullRequests(
|
|
41
|
+
ctx: ProviderReadContext,
|
|
42
|
+
options?: PullRequestSweepOptions,
|
|
43
|
+
): Promise<ProviderSweepResult<PullRequestShape>> {
|
|
44
|
+
const { targets, attributeUnavailableProviders } = resolvePullRequestSweepTargets(options);
|
|
45
|
+
const maxPages = options?.maxPages ?? 100;
|
|
46
|
+
const repos = options?.repos ?? [];
|
|
47
|
+
|
|
48
|
+
const results = await mapBounded(targets, providerFanOutConcurrency, async target => {
|
|
49
|
+
const { providerId: id, connectionId, domain: requestedDomain } = target;
|
|
50
|
+
/** A target that never reached a drain: the provider itself failed, so its slice is empty and attributed. */
|
|
51
|
+
const rejectedTarget = (warnings: ProviderWarning[]): SweepSlice => ({
|
|
52
|
+
items: [],
|
|
53
|
+
warnings: warnings,
|
|
54
|
+
fetchFailed: true,
|
|
55
|
+
truncated: false,
|
|
56
|
+
providerId: id,
|
|
57
|
+
failedProvider: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (isIssuesHostIntegrationId(id)) {
|
|
61
|
+
return rejectedTarget([
|
|
62
|
+
gitHostOnlySurfaceWarning(id, requestedDomain, connectionId, 'pull request sweeps'),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);
|
|
67
|
+
if (integration == null) {
|
|
68
|
+
// A requested connection that can't be resolved is a broken connection — surface it as a
|
|
69
|
+
// warning + fetchFailed rather than dropping the provider's slice silently.
|
|
70
|
+
const early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);
|
|
71
|
+
if (early.warnings.length === 0 && !attributeUnavailableProviders) return undefined;
|
|
72
|
+
|
|
73
|
+
return rejectedTarget(
|
|
74
|
+
early.warnings.length !== 0 ? early.warnings : [noConnectionWarning(id, requestedDomain, connectionId)],
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (!isGitHostIntegration(integration)) {
|
|
78
|
+
return rejectedTarget([
|
|
79
|
+
gitHostOnlySurfaceWarning(id, requestedDomain, connectionId, 'pull request sweeps'),
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await ctx.forceRefreshIfRequested(integration, options?.forceSync, connectionId);
|
|
84
|
+
|
|
85
|
+
const domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);
|
|
86
|
+
const accountWide = repos.length === 0;
|
|
87
|
+
const requestedFilters = target.filters ?? options?.filters;
|
|
88
|
+
const resolved = accountWide
|
|
89
|
+
? resolveAccountWidePullRequestFilters(id, requestedFilters)
|
|
90
|
+
: resolvePullRequestFilters(id, requestedFilters);
|
|
91
|
+
if (resolved.unsupported) {
|
|
92
|
+
return rejectedTarget([
|
|
93
|
+
accountWide
|
|
94
|
+
? unsupportedAccountWidePullRequestFiltersWarning(id, domain, connectionId, requestedFilters ?? [])
|
|
95
|
+
: unsupportedFiltersWarning(id, domain, connectionId),
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const drain = await drainPullRequests(
|
|
100
|
+
integration,
|
|
101
|
+
id,
|
|
102
|
+
domain,
|
|
103
|
+
repos,
|
|
104
|
+
options?.states,
|
|
105
|
+
resolved.filters,
|
|
106
|
+
accountWide ? (options?.includeReviewRequested ?? false) : false,
|
|
107
|
+
connectionId,
|
|
108
|
+
maxPages,
|
|
109
|
+
attributeUnavailableProviders,
|
|
110
|
+
);
|
|
111
|
+
const currentAccountId = drain.items.some(pr => pr.author != null)
|
|
112
|
+
? await getCurrentAccountId(integration, connectionId)
|
|
113
|
+
: undefined;
|
|
114
|
+
// Normalize the raw provider-apis PRs to the GitLens-owned shape here, where the per-provider
|
|
115
|
+
// `integration` (the mapper's provider reference) is in scope; the aggregation below only sees drains.
|
|
116
|
+
return {
|
|
117
|
+
...drain,
|
|
118
|
+
items: drain.items.map(pr =>
|
|
119
|
+
fromProviderPullRequest(pr, integration, { currentAccountId: currentAccountId }),
|
|
120
|
+
),
|
|
121
|
+
providerId: id,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const items: PullRequestShape[] = [];
|
|
126
|
+
const warnings: ProviderWarning[] = [];
|
|
127
|
+
const failedProviderIds = new Set<IntegrationIds>();
|
|
128
|
+
const incompleteProviderIds = new Set<IntegrationIds>();
|
|
129
|
+
let fetchFailed = false;
|
|
130
|
+
let truncated = false;
|
|
131
|
+
for (const drain of results) {
|
|
132
|
+
if (drain == null) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
items.push(...drain.items);
|
|
137
|
+
for (const w of drain.warnings) {
|
|
138
|
+
appendDedupedWarning(warnings, w);
|
|
139
|
+
}
|
|
140
|
+
if (drain.fetchFailed) {
|
|
141
|
+
fetchFailed = true;
|
|
142
|
+
}
|
|
143
|
+
if (drain.failedProvider) {
|
|
144
|
+
failedProviderIds.add(drain.providerId);
|
|
145
|
+
} else if (drain.fetchFailed || drain.truncated) {
|
|
146
|
+
incompleteProviderIds.add(drain.providerId);
|
|
147
|
+
}
|
|
148
|
+
if (drain.truncated) {
|
|
149
|
+
truncated = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
items: items,
|
|
155
|
+
warnings: warnings,
|
|
156
|
+
// `allPages` asserts completeness — it must be false when any provider truncated (a single-page
|
|
157
|
+
// account-wide read that couldn't confirm it drained everything) OR a drain aborted on a read
|
|
158
|
+
// failure (its slice is incomplete). Either way the sweep did not read every page.
|
|
159
|
+
page: {
|
|
160
|
+
currentPage: 1,
|
|
161
|
+
itemsPerPage: items.length,
|
|
162
|
+
allPages: !truncated && !fetchFailed,
|
|
163
|
+
truncated: truncated || undefined,
|
|
164
|
+
},
|
|
165
|
+
// A sweep drains every page itself and exposes no cursor to resume — so `hasMore` must be false even
|
|
166
|
+
// when the read was incomplete. Terminal incompleteness is expressed through `page.truncated` +
|
|
167
|
+
// `allPages: false` + warnings; setting `hasMore: true` here would make a consumer that drains while
|
|
168
|
+
// `hasMore` re-run the identical sweep forever with no cursor to advance.
|
|
169
|
+
hasMore: false,
|
|
170
|
+
fetchFailed: fetchFailed || undefined,
|
|
171
|
+
failedProviderIds: [...failedProviderIds],
|
|
172
|
+
incompleteProviderIds: [...incompleteProviderIds],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Closed/merged counterpart of {@link sweepPullRequests}, feeding Kepler's Kanban "done" column. Applies
|
|
178
|
+
* the native cross-provider state filter (`Closed` + `Merged`) so it works beyond GitHub.
|
|
179
|
+
*/
|
|
180
|
+
export function sweepClosedPullRequests(
|
|
181
|
+
ctx: ProviderReadContext,
|
|
182
|
+
options?: ClosedPullRequestSweepOptions,
|
|
183
|
+
): Promise<ProviderSweepResult<PullRequestShape>> {
|
|
184
|
+
return sweepPullRequests(ctx, { ...options, states: ['closed', 'merged'] });
|
|
185
|
+
}
|