@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { mapBounded } from '../../../utils/promise.js';
|
|
2
2
|
import { mergeAssessmentInto } from '../collectionMetadata.js';
|
|
3
|
+
import type { IntegrationIds } from '../constants.js';
|
|
3
4
|
import {
|
|
4
5
|
GitCloudHostIntegrationId,
|
|
5
6
|
GitSelfManagedHostIntegrationId,
|
|
6
|
-
type IntegrationIds,
|
|
7
7
|
providerFanOutConcurrency,
|
|
8
8
|
supportedOrderedCloudIntegrationIds,
|
|
9
9
|
supportedOrderedCloudIssuesIntegrationIds,
|
|
@@ -28,6 +28,7 @@ import type { ProviderReadContext } from './context.js';
|
|
|
28
28
|
import { runCaptured } from './drains.js';
|
|
29
29
|
import {
|
|
30
30
|
assertHierarchyReadTarget,
|
|
31
|
+
mergeHierarchyResults,
|
|
31
32
|
orgForProject,
|
|
32
33
|
resourceMatchesOrg,
|
|
33
34
|
resourceToOrg,
|
|
@@ -132,25 +133,7 @@ export async function listOrgs(
|
|
|
132
133
|
return { items: items, warnings: warnings, fetchFailed: fetchFailed };
|
|
133
134
|
});
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
const warnings: ProviderWarning[] = [];
|
|
137
|
-
let fetchFailed = false;
|
|
138
|
-
for (const result of results) {
|
|
139
|
-
if (result == null) {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
items.push(...result.items);
|
|
144
|
-
// Dedupe across the multi-provider fan-out (matches listProjects): a warning that repeats verbatim
|
|
145
|
-
// across scopes — e.g. the same account surfaced under two ids — shouldn't be reported twice.
|
|
146
|
-
for (const w of result.warnings) {
|
|
147
|
-
appendDedupedWarning(warnings, w);
|
|
148
|
-
}
|
|
149
|
-
if (result.fetchFailed) {
|
|
150
|
-
fetchFailed = true;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };
|
|
136
|
+
return mergeHierarchyResults(results);
|
|
154
137
|
}
|
|
155
138
|
|
|
156
139
|
export async function listProjects(
|
|
@@ -281,23 +264,7 @@ export async function listProjects(
|
|
|
281
264
|
return { items: items, warnings: warnings, fetchFailed: fetchFailed };
|
|
282
265
|
});
|
|
283
266
|
|
|
284
|
-
|
|
285
|
-
const warnings: ProviderWarning[] = [];
|
|
286
|
-
let fetchFailed = false;
|
|
287
|
-
for (const result of results) {
|
|
288
|
-
if (result == null) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
items.push(...result.items);
|
|
293
|
-
for (const w of result.warnings) {
|
|
294
|
-
appendDedupedWarning(warnings, w);
|
|
295
|
-
}
|
|
296
|
-
if (result.fetchFailed) {
|
|
297
|
-
fetchFailed = true;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };
|
|
267
|
+
return mergeHierarchyResults(results);
|
|
301
268
|
}
|
|
302
269
|
|
|
303
270
|
export async function listRepos(
|
|
@@ -365,128 +332,141 @@ export async function listRepos(
|
|
|
365
332
|
}
|
|
366
333
|
|
|
367
334
|
const org = options.org;
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
{ warnOnMissingSession: warnOnMissingSessionForDomain(options.providerId, options.domain) },
|
|
385
|
-
);
|
|
335
|
+
const warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);
|
|
336
|
+
const readPage = (cursor: string | undefined) =>
|
|
337
|
+
runCaptured(
|
|
338
|
+
options.providerId,
|
|
339
|
+
domain,
|
|
340
|
+
options.connectionId,
|
|
341
|
+
() =>
|
|
342
|
+
org == null
|
|
343
|
+
? integration.getRepositoriesForUserResult({ cursor: cursor, connectionId: options.connectionId })
|
|
344
|
+
: integration.getRepositoriesForOrgResult(org, {
|
|
345
|
+
project: options.project,
|
|
346
|
+
cursor: cursor,
|
|
347
|
+
connectionId: options.connectionId,
|
|
348
|
+
}),
|
|
349
|
+
{ warnOnMissingSession: warnOnMissingSession },
|
|
350
|
+
);
|
|
386
351
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Builds one page's result from what the provider returned. Shared by the initial read and by every round of
|
|
354
|
+
* the cursor walk below, so a walked page N is described by exactly the same rules as a directly-requested one
|
|
355
|
+
* (position, continuation, truncation) rather than by a second copy of them.
|
|
356
|
+
*/
|
|
357
|
+
const toResult = (
|
|
358
|
+
{ value, warning }: Awaited<ReturnType<typeof readPage>>,
|
|
359
|
+
requestedPage: number,
|
|
360
|
+
suppliedCursor: string | undefined,
|
|
361
|
+
): ProviderPagedResult<ProviderRepositoryShape> => {
|
|
362
|
+
const items = value?.values ?? [];
|
|
363
|
+
const warnings = warning != null ? [warning] : [];
|
|
364
|
+
// The repos read core is cursor-only and can't accept a page size, so don't echo the requested
|
|
365
|
+
// `itemsPerPage` as if it were applied — report what the provider returned (its own pageSize when
|
|
366
|
+
// available, else the actual item count).
|
|
367
|
+
const paged = toProviderPageInfo(items.length, value?.paging);
|
|
368
|
+
// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending
|
|
369
|
+
// them to any captured thrown-error warning without discarding the partial result's items.
|
|
370
|
+
const assessment = mergeAssessmentInto(
|
|
371
|
+
warnings,
|
|
372
|
+
options.providerId,
|
|
373
|
+
domain,
|
|
374
|
+
options.connectionId,
|
|
375
|
+
value?.metadata,
|
|
376
|
+
);
|
|
377
|
+
// Whether this read honors a page NUMBER, decided from what the provider actually reported rather than
|
|
378
|
+
// guessed from the absence of a cursor. A numbered-page repos host reports its position (`paging.page`)
|
|
379
|
+
// and/or its successor (`paging.nextPage`) — Bitbucket's workspace walk consumes `page` as a real 1-based
|
|
380
|
+
// page and reports `nextPage` without echoing `currentPage`, so both signals have to count. Every wired
|
|
381
|
+
// cursor-based repos read (GitHub's org/user walks, GitLab's user walk, Bitbucket's cursor read) reports
|
|
382
|
+
// neither. Keying off `paged.cursor == null` instead read the paging layer's "reported another page,
|
|
383
|
+
// handed back no continuation" sentinel as evidence of a numbered host, which is exactly backwards.
|
|
384
|
+
const pageAdvanceable = value?.paging?.page != null || value?.paging?.nextPage != null;
|
|
385
|
+
// Numbered-page hosts that don't echo `currentPage` may still be advanced by the requested `page` (initial
|
|
386
|
+
// read) or by the cursor the caller threaded back. A cursor-only host ignores an UNTHREADED `page`
|
|
387
|
+
// request, so its page-less first page is page 1.
|
|
388
|
+
const currentPage = resolveCurrentPage({
|
|
389
|
+
providerPage: paged.page.currentPage,
|
|
390
|
+
requestedPage: requestedPage,
|
|
391
|
+
suppliedCursor: suppliedCursor,
|
|
392
|
+
pageAdvanceable: pageAdvanceable,
|
|
393
|
+
});
|
|
394
|
+
// Never advertise `hasMore` without a continuation the caller can act on — the same contract
|
|
395
|
+
// `listPullRequestsPage`, `listIssuesPage` and `broadenIssues` hold, via the same helper. A provider that
|
|
396
|
+
// reports another page but hands back neither `endCursor` nor `nextPage` (surfaced as the `'{}'` sentinel,
|
|
397
|
+
// which `toProviderPageInfo` drops) is terminal-but-incomplete: `hasMore: false` + `page.truncated`.
|
|
398
|
+
// Synthesizing a page-number cursor for a cursor-only host instead — which is what this read used to do
|
|
399
|
+
// unconditionally — handed back a continuation the provider ignores, so it answered with its FIRST page
|
|
400
|
+
// again while still reporting `hasMore: true`. A consumer draining until `hasMore` clears (Kepler's repo
|
|
401
|
+
// drain does exactly that) then looped to its own page cap, accumulating a duplicate copy of every repo
|
|
402
|
+
// per round, and never saw a truncation signal.
|
|
403
|
+
const continuation = resolveContinuation(paged, pageAdvanceable ? currentPage + 1 : undefined);
|
|
404
|
+
// The org-hierarchy read can also stop at a defensive backstop with more repos unlisted and no cursor to
|
|
405
|
+
// resume (top-level `truncated`, or `paging.truncated` on a single-page read). Metadata incompleteness and
|
|
406
|
+
// a demoted continuation are two further, independent sources of the same signal.
|
|
407
|
+
const truncated =
|
|
408
|
+
(value?.truncated ?? false) ||
|
|
409
|
+
(value?.paging?.truncated ?? false) ||
|
|
410
|
+
assessment.truncated ||
|
|
411
|
+
continuation.truncated;
|
|
412
|
+
return {
|
|
413
|
+
// Normalize the raw provider-apis repos to the GitLens-owned shape at the surface boundary.
|
|
414
|
+
items: items.map(toProviderRepositoryShape),
|
|
415
|
+
warnings: warnings,
|
|
416
|
+
page: { ...paged.page, currentPage: currentPage, truncated: truncated || undefined },
|
|
417
|
+
hasMore: continuation.hasMore,
|
|
418
|
+
cursor: continuation.cursor,
|
|
419
|
+
fetchFailed: assessment.fetchFailed || (warning != null && value == null) || undefined,
|
|
420
|
+
};
|
|
439
421
|
};
|
|
440
422
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
let previous = result;
|
|
449
|
-
|
|
450
|
-
for (let currentPage = 2; currentPage <= page; currentPage++) {
|
|
451
|
-
if (!previous.hasMore || previous.cursor == null) {
|
|
452
|
-
return {
|
|
453
|
-
items: [],
|
|
454
|
-
warnings: traversalWarnings,
|
|
455
|
-
page: {
|
|
456
|
-
currentPage: page,
|
|
457
|
-
itemsPerPage: 0,
|
|
458
|
-
truncated: traversalTruncated || undefined,
|
|
459
|
-
},
|
|
460
|
-
hasMore: false,
|
|
461
|
-
fetchFailed: traversalFetchFailed || undefined,
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const requested = await listRepos(ctx, {
|
|
466
|
-
...options,
|
|
467
|
-
page: currentPage,
|
|
468
|
-
cursor: previous.cursor,
|
|
469
|
-
});
|
|
470
|
-
for (const traversalWarning of requested.warnings) {
|
|
471
|
-
appendDedupedWarning(traversalWarnings, traversalWarning);
|
|
472
|
-
}
|
|
473
|
-
traversalFetchFailed ||= requested.fetchFailed === true;
|
|
474
|
-
traversalTruncated ||= requested.page.truncated === true;
|
|
423
|
+
const first = await readPage(options.cursor ?? pageToCursor(page));
|
|
424
|
+
const result = toResult(first, page, options.cursor);
|
|
425
|
+
// A numbered-page host honored the synthesized cursor, so this IS page N; a cursor-only host ignored it and
|
|
426
|
+
// answered with page 1, which the walk below advances. Decided from the provider's own paging metadata rather
|
|
427
|
+
// than assumed, hence after the read.
|
|
428
|
+
const pageAdvanceable = first.value?.paging?.page != null || first.value?.paging?.nextPage != null;
|
|
429
|
+
if (options.cursor != null || page === 1 || pageAdvanceable) return result;
|
|
475
430
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
431
|
+
// Walk the provider's real continuations to the requested page. Only the last page's items are kept — pages
|
|
432
|
+
// 1..N returned as "page N" would duplicate items for a normal paged consumer — while warnings, truncation,
|
|
433
|
+
// and failure accumulate across the whole walked prefix, since an earlier round's problem still means this
|
|
434
|
+
// result isn't authoritative. The provider read is re-issued per round, but the integration is resolved once:
|
|
435
|
+
// re-entering this function per page would repeat its auth resolution N times for page N.
|
|
436
|
+
const walkedWarnings = [...result.warnings];
|
|
437
|
+
let walkedFetchFailed = result.fetchFailed === true;
|
|
438
|
+
let walkedTruncated = result.page.truncated === true;
|
|
439
|
+
let previous = result;
|
|
440
|
+
for (let currentPage = 2; currentPage <= page; currentPage++) {
|
|
441
|
+
if (!previous.hasMore || previous.cursor == null) {
|
|
442
|
+
// The requested page lies past the provider's terminal cursor: an EMPTY page N, never the last
|
|
443
|
+
// available page relabeled (see ProviderPageInfo.currentPage).
|
|
444
|
+
return {
|
|
445
|
+
items: [],
|
|
446
|
+
warnings: walkedWarnings,
|
|
447
|
+
page: { currentPage: page, itemsPerPage: 0, truncated: walkedTruncated || undefined },
|
|
448
|
+
hasMore: false,
|
|
449
|
+
fetchFailed: walkedFetchFailed || undefined,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
487
452
|
|
|
488
|
-
|
|
453
|
+
const suppliedCursor = previous.cursor;
|
|
454
|
+
const requested = toResult(await readPage(suppliedCursor), currentPage, suppliedCursor);
|
|
455
|
+
for (const warning of requested.warnings) {
|
|
456
|
+
appendDedupedWarning(walkedWarnings, warning);
|
|
489
457
|
}
|
|
458
|
+
walkedFetchFailed ||= requested.fetchFailed === true;
|
|
459
|
+
walkedTruncated ||= requested.page.truncated === true;
|
|
460
|
+
if (currentPage === page) {
|
|
461
|
+
return {
|
|
462
|
+
...requested,
|
|
463
|
+
warnings: walkedWarnings,
|
|
464
|
+
page: { ...requested.page, truncated: walkedTruncated || undefined },
|
|
465
|
+
fetchFailed: walkedFetchFailed || undefined,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
previous = requested;
|
|
490
470
|
}
|
|
491
471
|
|
|
492
472
|
return result;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { IssueResourceDescriptor, ResourceDescriptor } from '../../../git/models/resourceDescriptor.js';
|
|
2
2
|
import { IssuesCloudHostIntegrationId } from '../constants.js';
|
|
3
3
|
import type { IntegrationIds } from '../constants.js';
|
|
4
|
-
import type { ProviderOrganization } from '../results.js';
|
|
4
|
+
import type { ProviderOrganization, ProviderResult, ProviderWarning } from '../results.js';
|
|
5
|
+
import { appendDedupedWarning } from '../results.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Mapping of a provider's own hierarchy vocabulary onto the single {@link ProviderOrganization} shape the facade
|
|
@@ -30,6 +31,26 @@ export function resourceMatchesOrg(resource: ResourceDescriptor, org: string): b
|
|
|
30
31
|
return resource.key === org || typed.id === org || typed.name === org;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The id of the RESOURCE a project belongs to, used to look up which account (and therefore which user) that
|
|
36
|
+
* project's issues should be scoped to. Falls back through the project's own identifiers because some providers
|
|
37
|
+
* (and test doubles) return project descriptors without naming their parent.
|
|
38
|
+
*/
|
|
39
|
+
export function resourceIdForProject(project: ResourceDescriptor): string | undefined {
|
|
40
|
+
const typed = project as IssueResourceDescriptor & { resourceId?: string };
|
|
41
|
+
return typed.resourceId ?? typed.id ?? project.key;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A stable identity for one project across facade pages, for the retry/completed bookkeeping an issue-tracker
|
|
46
|
+
* cursor carries. All three identifiers are included rather than one preferred value: two trackers can reuse the
|
|
47
|
+
* same project id, so keying on a single field would collapse them into one entry.
|
|
48
|
+
*/
|
|
49
|
+
export function projectKey(project: ResourceDescriptor): string {
|
|
50
|
+
const typed = project as IssueResourceDescriptor & { resourceId?: string };
|
|
51
|
+
return JSON.stringify([typed.resourceId ?? '', typed.id ?? '', project.key]);
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
/**
|
|
34
55
|
* Maps an issue-tracker resource descriptor to the unified {@link ProviderOrganization} org shape. `url` is
|
|
35
56
|
* synthesized when the descriptor carries none, rather than widening the shared `ProviderOrganization.url` to
|
|
@@ -90,6 +111,34 @@ export function withProviderContext(providerId: IntegrationIds, item: ProviderOr
|
|
|
90
111
|
};
|
|
91
112
|
}
|
|
92
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Merges the per-provider slices of a hierarchy fan-out into one flat result.
|
|
116
|
+
*
|
|
117
|
+
* `ProviderResult` has no page object, so `fetchFailed` is its only non-authoritative signal — any slice that
|
|
118
|
+
* failed or came back incomplete has to set it, or a caller would treat a short list as the whole account.
|
|
119
|
+
* Warnings are deduped because the same account can surface under two ids in one fan-out.
|
|
120
|
+
*/
|
|
121
|
+
export function mergeHierarchyResults<T>(
|
|
122
|
+
results: readonly ({ items: T[]; warnings: ProviderWarning[]; fetchFailed: boolean } | undefined)[],
|
|
123
|
+
): ProviderResult<T> {
|
|
124
|
+
const items: T[] = [];
|
|
125
|
+
const warnings: ProviderWarning[] = [];
|
|
126
|
+
let fetchFailed = false;
|
|
127
|
+
for (const result of results) {
|
|
128
|
+
if (result == null) continue;
|
|
129
|
+
|
|
130
|
+
items.push(...result.items);
|
|
131
|
+
for (const warning of result.warnings) {
|
|
132
|
+
appendDedupedWarning(warnings, warning);
|
|
133
|
+
}
|
|
134
|
+
if (result.fetchFailed) {
|
|
135
|
+
fetchFailed = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };
|
|
140
|
+
}
|
|
141
|
+
|
|
93
142
|
/**
|
|
94
143
|
* Rejects a hierarchy read whose scoped selectors have no provider to apply to. `connectionId` and `domain`
|
|
95
144
|
* both select among the connections OF a provider, so either one without `providerId` is ambiguous rather than
|
|
@@ -3,7 +3,8 @@ import type { IssueShape } from '../../../git/models/issue.js';
|
|
|
3
3
|
import type { ResourceDescriptor } from '../../../git/models/resourceDescriptor.js';
|
|
4
4
|
import { mapBounded } from '../../../utils/promise.js';
|
|
5
5
|
import { mergeAssessmentInto } from '../collectionMetadata.js';
|
|
6
|
-
import {
|
|
6
|
+
import type { IntegrationIds } from '../constants.js';
|
|
7
|
+
import { providerFanOutConcurrency } from '../constants.js';
|
|
7
8
|
import { isIssuesIntegration } from '../models/issuesIntegration.js';
|
|
8
9
|
import { IssueFilter, providersMetadata } from '../providers/models.js';
|
|
9
10
|
import { mergeCollectionMetadata, parsePageCursor } from '../providers/utils/providerPaging.js';
|
|
@@ -12,7 +13,7 @@ import { isIssuesHostIntegrationId } from '../utils/integration.utils.js';
|
|
|
12
13
|
import type { ProviderReadContext } from './context.js';
|
|
13
14
|
import { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from './cursors.js';
|
|
14
15
|
import { runCaptured } from './drains.js';
|
|
15
|
-
import { resourceMatchesOrg } from './hierarchy.utils.js';
|
|
16
|
+
import { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from './hierarchy.utils.js';
|
|
16
17
|
import { issueTrackerOnlySurfaceWarning, otherWarning } from './warnings.js';
|
|
17
18
|
|
|
18
19
|
export async function listIssueTrackerIssuesPage(
|
|
@@ -219,19 +220,6 @@ export async function listIssueTrackerIssuesPage(
|
|
|
219
220
|
return emptyPage(true);
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
const resourceIdForProject = (project: ResourceDescriptor): string | undefined => {
|
|
223
|
-
const issueProject = project as { id?: string; key: string; resourceId?: string };
|
|
224
|
-
return issueProject.resourceId ?? issueProject.id ?? issueProject.key;
|
|
225
|
-
};
|
|
226
|
-
const retryKeyForProject = (project: ResourceDescriptor): string => {
|
|
227
|
-
const issueProject = project as { id?: string; key: string; resourceId?: string };
|
|
228
|
-
return JSON.stringify([issueProject.resourceId ?? '', issueProject.id ?? '', issueProject.key]);
|
|
229
|
-
};
|
|
230
|
-
const labelForResource = (resource: ResourceDescriptor): string => {
|
|
231
|
-
const issueResource = resource as { id?: string; key: string; name?: string };
|
|
232
|
-
return issueResource.name ?? issueResource.id ?? issueResource.key;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
223
|
// Scope to the current user's assigned issues unless the caller broadens to all assignees. Resolve the
|
|
236
224
|
// handle from each resource's own account (multi-account safe), capturing any error so its kind
|
|
237
225
|
// (e.g. auth) is preserved rather than collapsed to a generic warning.
|
|
@@ -259,7 +247,7 @@ export async function listIssueTrackerIssuesPage(
|
|
|
259
247
|
options.providerId,
|
|
260
248
|
domain,
|
|
261
249
|
options.connectionId,
|
|
262
|
-
`Could not resolve the current user for '${
|
|
250
|
+
`Could not resolve the current user for '${resourceLabel(resource)}'; skipping that resource to avoid returning issues assigned to others.`,
|
|
263
251
|
),
|
|
264
252
|
);
|
|
265
253
|
accountLookupFailed = true;
|
|
@@ -294,22 +282,22 @@ export async function listIssueTrackerIssuesPage(
|
|
|
294
282
|
? matchedProjects.filter(project => {
|
|
295
283
|
if (userForProject(project) != null) return true;
|
|
296
284
|
|
|
297
|
-
const
|
|
298
|
-
retryProjectKeys.add(
|
|
299
|
-
completedProjectKeys.delete(
|
|
285
|
+
const key = projectKey(project);
|
|
286
|
+
retryProjectKeys.add(key);
|
|
287
|
+
completedProjectKeys.delete(key);
|
|
300
288
|
return false;
|
|
301
289
|
})
|
|
302
290
|
: matchedProjects;
|
|
303
291
|
|
|
304
292
|
// Select the untouched window plus any earlier windows/projects that explicitly need retrying. Keys
|
|
305
293
|
// include the parent resource so two trackers can reuse the same project id without collapsing.
|
|
306
|
-
const projectsByRetryKey = new Map(scopedProjectsWithUsers.map(project => [
|
|
294
|
+
const projectsByRetryKey = new Map(scopedProjectsWithUsers.map(project => [projectKey(project), project]));
|
|
307
295
|
const scopedProjectsByKey = new Map<string, ResourceDescriptor>();
|
|
308
296
|
const addScopedProject = (project: ResourceDescriptor): void => {
|
|
309
|
-
const
|
|
310
|
-
if (completedProjectKeys.has(
|
|
297
|
+
const key = projectKey(project);
|
|
298
|
+
if (completedProjectKeys.has(key)) return;
|
|
311
299
|
|
|
312
|
-
scopedProjectsByKey.set(
|
|
300
|
+
scopedProjectsByKey.set(key, project);
|
|
313
301
|
};
|
|
314
302
|
for (const retryProject of priorRetryProjects) {
|
|
315
303
|
const project = projectsByRetryKey.get(retryProject);
|
|
@@ -396,7 +384,7 @@ export async function listIssueTrackerIssuesPage(
|
|
|
396
384
|
let projectTruncated = projectDiscoveryTruncated;
|
|
397
385
|
let drainMetadata: CollectionMetadata | undefined;
|
|
398
386
|
for (const { project, value: result, warning } of perProject) {
|
|
399
|
-
const
|
|
387
|
+
const key = projectKey(project);
|
|
400
388
|
const retryProject = result == null;
|
|
401
389
|
if (warning != null) {
|
|
402
390
|
warnings.push(warning);
|
|
@@ -418,13 +406,13 @@ export async function listIssueTrackerIssuesPage(
|
|
|
418
406
|
}
|
|
419
407
|
}
|
|
420
408
|
if (retryProject) {
|
|
421
|
-
retryProjectKeys.add(
|
|
422
|
-
completedProjectKeys.delete(
|
|
409
|
+
retryProjectKeys.add(key);
|
|
410
|
+
completedProjectKeys.delete(key);
|
|
423
411
|
} else {
|
|
424
412
|
// A usable partial/truncated project is emitted once and remains structurally incomplete.
|
|
425
413
|
// Re-running the same project cursor would normally return the same prefix and duplicate it
|
|
426
414
|
// across facade pages; only a project that returned no value is safe to retry automatically.
|
|
427
|
-
completedProjectKeys.add(
|
|
415
|
+
completedProjectKeys.add(key);
|
|
428
416
|
}
|
|
429
417
|
}
|
|
430
418
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { IssueShape } from '../../../git/models/issue.js';
|
|
2
2
|
import { mergeAssessmentInto } from '../collectionMetadata.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import type { IntegrationIds } from '../constants.js';
|
|
4
|
+
import { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';
|
|
5
|
+
import type { IssueFilter, ProviderReposInput } from '../providers/models.js';
|
|
6
|
+
import { PagingMode, providersMetadata } from '../providers/models.js';
|
|
5
7
|
import { mergeCollectionMetadata } from '../providers/utils/providerPaging.js';
|
|
6
8
|
import type { ProviderPagedResult, ProviderWarning } from '../results.js';
|
|
7
9
|
import { appendDedupedWarning } from '../results.js';
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import type { PullRequestShape, PullRequestStateFilter } from '../../../git/models/pullRequest.js';
|
|
2
2
|
import { mergeAssessmentInto } from '../collectionMetadata.js';
|
|
3
3
|
import type { IntegrationIds } from '../constants.js';
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
6
|
-
fromProviderPullRequest,
|
|
7
|
-
PagingMode,
|
|
8
|
-
providersMetadata,
|
|
9
|
-
type ProviderPullRequest,
|
|
10
|
-
type ProviderReposInput,
|
|
11
|
-
type PullRequestFilter,
|
|
12
|
-
} from '../providers/models.js';
|
|
4
|
+
import type { ProviderPullRequest, ProviderReposInput, PullRequestFilter } from '../providers/models.js';
|
|
5
|
+
import { fromProviderPullRequest, PagingMode, providersMetadata } from '../providers/models.js';
|
|
13
6
|
import type { ProviderPagedResult } from '../results.js';
|
|
14
7
|
import {
|
|
15
8
|
isGitHostIntegration,
|