@gitkraken/core-gitlens 0.5.100 → 0.5.102
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 +36 -1
- package/dist/git/models/issue.d.ts +4 -3
- package/dist/git/models/issue.d.ts.map +1 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/utils/issue.utils.d.ts.map +1 -1
- package/dist/git/utils/issue.utils.js +8 -6
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/remote.utils.d.ts +3 -0
- package/dist/git/utils/remote.utils.d.ts.map +1 -1
- package/dist/git/utils/remote.utils.js +5 -0
- package/dist/git/utils/remote.utils.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +18 -5
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/models.d.ts +6 -4
- package/dist/plus/git-github/models.d.ts.map +1 -1
- package/dist/plus/git-github/models.js +19 -26
- package/dist/plus/git-github/models.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +12 -0
- package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
- package/dist/plus/integrations/collectionMetadata.js +111 -6
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/index.d.ts +1 -1
- package/dist/plus/integrations/index.d.ts.map +1 -1
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +3 -3
- package/dist/plus/integrations/integrationService.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.js +19 -3
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts.map +1 -1
- package/dist/plus/integrations/models/integration.js +3 -2
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +1 -0
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
- package/dist/plus/integrations/providers/providersApi.js +4 -2
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/trello.d.ts +2 -0
- package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
- package/dist/plus/integrations/providers/trello.js +1 -1
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts +4 -2
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js +34 -21
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts.map +1 -1
- package/dist/plus/integrations/reads/drains.js +59 -20
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +8 -2
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issues.js +13 -3
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.d.ts.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +7 -1
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +41 -6
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +54 -7
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +115 -1
- package/dist/plus/integrations/results.d.ts.map +1 -1
- package/dist/plus/integrations/results.js +78 -3
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +81 -7
- package/package.json +2 -2
- package/src/git/models/issue.ts +3 -2
- package/src/git/utils/issue.utils.ts +9 -6
- package/src/git/utils/remote.utils.ts +6 -0
- package/src/plus/git-github/api/github.ts +19 -9
- package/src/plus/git-github/models.ts +27 -31
- package/src/plus/integrations/collectionMetadata.ts +125 -8
- package/src/plus/integrations/index.ts +4 -0
- package/src/plus/integrations/integrationService.ts +37 -9
- package/src/plus/integrations/models/integration.ts +3 -2
- package/src/plus/integrations/providers/models.ts +4 -1
- package/src/plus/integrations/providers/providersApi.ts +6 -10
- package/src/plus/integrations/providers/trello.ts +3 -2
- package/src/plus/integrations/providers/utils/providerPaging.ts +46 -23
- package/src/plus/integrations/reads/drains.ts +84 -20
- package/src/plus/integrations/reads/issueTracker.ts +8 -2
- package/src/plus/integrations/reads/issues.ts +21 -4
- package/src/plus/integrations/reads/pullRequests.ts +15 -1
- package/src/plus/integrations/reads/warnings.ts +82 -12
- package/src/plus/integrations/results.ts +173 -4
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CollectionMetadata,
|
|
3
|
+
CollectionOmission,
|
|
4
|
+
CollectionScope,
|
|
5
|
+
CollectionScopeFailure,
|
|
6
|
+
} from '@gitkraken/provider-apis';
|
|
2
7
|
import { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '../../git/errors.js';
|
|
3
8
|
import type { IntegrationIds } from './constants.js';
|
|
4
9
|
import { isRateLimitResponse } from './errors.js';
|
|
5
|
-
import type { ProviderWarning } from './results.js';
|
|
10
|
+
import type { ProviderWarning, ProviderWarningOmission } from './results.js';
|
|
6
11
|
import { appendDedupedWarning } from './results.js';
|
|
7
12
|
|
|
8
13
|
/**
|
|
@@ -68,8 +73,8 @@ function toCollectionFailureWarningKind(failure: CollectionScopeFailure): Provid
|
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
/** ` (resource r, project p, repository o/n)` for the scope IDs present; empty when the scope names none. */
|
|
77
|
+
function collectionScopeText(scope: CollectionScope | undefined): string {
|
|
73
78
|
const parts: string[] = [];
|
|
74
79
|
if (scope?.resourceId != null) {
|
|
75
80
|
parts.push(`resource ${scope.resourceId}`);
|
|
@@ -81,9 +86,98 @@ function collectionFailureMessage(failure: CollectionScopeFailure): string {
|
|
|
81
86
|
parts.push(`repository ${scope.repositoryId}`);
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
return parts.length ? ` (${parts.join(', ')})` : '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function collectionFailureMessage(failure: CollectionScopeFailure): string {
|
|
85
93
|
const detail = failure.message != null ? `: ${failure.message}` : '';
|
|
86
|
-
return `Failed to read ${failure.kind} scope${
|
|
94
|
+
return `Failed to read ${failure.kind} scope${collectionScopeText(failure.scope)}${detail}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Explains one omission in the consumer's terms — what was left out and, where the SDK reports it, how much.
|
|
99
|
+
*
|
|
100
|
+
* An omission is a completeness fact, never a failure: the read succeeded, and the provider (or the SDK's own
|
|
101
|
+
* recovery budget) is what withheld results. That is why these never contribute to `fetchFailed`. Whether a
|
|
102
|
+
* retry would recover anything is a separate question this layer cannot answer — see the `recovery` note on
|
|
103
|
+
* {@link toProviderWarningOmission}.
|
|
104
|
+
*/
|
|
105
|
+
function collectionOmissionMessage(omission: CollectionOmission): string {
|
|
106
|
+
const scopeText = collectionScopeText(omission.scope);
|
|
107
|
+
switch (omission.kind) {
|
|
108
|
+
case 'provider-limit':
|
|
109
|
+
// `totalCount` is the only figure saying how much was withheld; both fields are optional, so degrade
|
|
110
|
+
// through the shapes the SDK can actually emit rather than printing `undefined`.
|
|
111
|
+
if (omission.totalCount != null && omission.limit != null) {
|
|
112
|
+
return `Search${scopeText} matched ${omission.totalCount} results, but the provider exposes at most ${omission.limit}`;
|
|
113
|
+
}
|
|
114
|
+
if (omission.limit != null) {
|
|
115
|
+
return `Search${scopeText} exceeded the provider limit of ${omission.limit} results`;
|
|
116
|
+
}
|
|
117
|
+
return `Search${scopeText} exceeded the provider's result limit`;
|
|
118
|
+
case 'recovery-budget':
|
|
119
|
+
return `Stopped recovering omitted results${scopeText} after reaching the request budget${
|
|
120
|
+
omission.limit != null ? ` of ${omission.limit} requests` : ''
|
|
121
|
+
}`;
|
|
122
|
+
case 'pagination-incomplete':
|
|
123
|
+
return `More results are available${scopeText} than this read returned`;
|
|
124
|
+
default:
|
|
125
|
+
// `CollectionOmissionKind` is a closed union, so this is unreachable today. Kept as a compile-time
|
|
126
|
+
// guard rather than a silent fallback: an SDK bump that adds a kind breaks the build here instead
|
|
127
|
+
// of quietly degrading it to a vague message.
|
|
128
|
+
omission.kind satisfies never;
|
|
129
|
+
return `Some results were omitted${scopeText}; the read is incomplete`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Forwards an SDK omission as the structured signal consumers read instead of parsing {@link
|
|
135
|
+
* collectionOmissionMessage}'s prose.
|
|
136
|
+
*
|
|
137
|
+
* `results.ts` re-spells `CollectionOmissionKind` rather than importing it, so the published warning surface
|
|
138
|
+
* carries no `@gitkraken/provider-apis` types (see the export block in `index.ts`). The return type is what
|
|
139
|
+
* keeps the two unions honest: an SDK bump that adds a member fails to compile here, alongside
|
|
140
|
+
* `collectionOmissionMessage`'s `satisfies never`.
|
|
141
|
+
*
|
|
142
|
+
* `scope` is copied because the SDK's own object is retained and re-merged across drained pages
|
|
143
|
+
* (`providerPaging.ts`), and this one crosses the package boundary.
|
|
144
|
+
*
|
|
145
|
+
* `recovery` is always `'none'`, deliberately, and a scoped `pagination-incomplete` is why it looks wrong: the
|
|
146
|
+
* SDK emits that one shape from situations with opposite remedies and says so itself — "Either the read
|
|
147
|
+
* deliberately took one page per scope, or the provider advertised another page it gave no way to reach." Its
|
|
148
|
+
* `collectAcrossScopes` producers (Azure, Bitbucket, Bitbucket Server) are the first, its `drainAcrossScopes`
|
|
149
|
+
* producers (GitLab, Jira) the second, and those report an omission only when a cursor STALLED — so re-reading
|
|
150
|
+
* that scope stalls at the identical page. Nothing in `CollectionOmission` separates the two, so claiming
|
|
151
|
+
* recoverability would ship the dead-end button `recovery` exists to prevent. Making the recoverable case
|
|
152
|
+
* claimable needs provider-apis to say whether a scope was drained or merely sampled: a fact to forward, not
|
|
153
|
+
* to guess here.
|
|
154
|
+
*/
|
|
155
|
+
function toProviderWarningOmission(omission: CollectionOmission): ProviderWarningOmission {
|
|
156
|
+
return {
|
|
157
|
+
kind: omission.kind,
|
|
158
|
+
recovery: 'none',
|
|
159
|
+
...(omission.limit != null ? { limit: omission.limit } : {}),
|
|
160
|
+
// The SDK types this `number | null | undefined`; normalize to one absence for consumers.
|
|
161
|
+
...(omission.totalCount != null ? { totalCount: omission.totalCount } : {}),
|
|
162
|
+
...(omission.scope != null ? { scope: { ...omission.scope } } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Whether SDK metadata describes a read that may be missing results.
|
|
168
|
+
*
|
|
169
|
+
* A reported omission counts on its own rather than deferring to `completeness`. The SDK does couple the
|
|
170
|
+
* two — every site that emits an omission degrades completeness in the same call — so today the omission
|
|
171
|
+
* clause changes no outcome. It is defensive: the two facts arrive as independent fields, and a read that
|
|
172
|
+
* names what it left out must not be publishable as whole if a future producer sets only one of them.
|
|
173
|
+
*
|
|
174
|
+
* This is the single definition of "may be missing results", shared with `getPagedResult`, so one metadata
|
|
175
|
+
* object cannot be truncated at one layer and complete at another.
|
|
176
|
+
*/
|
|
177
|
+
export function isIncompleteCollection(metadata: CollectionMetadata | undefined): boolean {
|
|
178
|
+
if (metadata == null) return false;
|
|
179
|
+
|
|
180
|
+
return metadata.completeness !== 'complete' || (metadata.omissions?.length ?? 0) > 0;
|
|
87
181
|
}
|
|
88
182
|
|
|
89
183
|
/** Converts internal SDK collection metadata into neutral provider facade signals. */
|
|
@@ -109,8 +203,31 @@ export function assessCollectionMetadata(
|
|
|
109
203
|
});
|
|
110
204
|
}
|
|
111
205
|
|
|
112
|
-
|
|
113
|
-
|
|
206
|
+
// Omissions explain WHY a read is incomplete when nothing failed — a provider cap, an exhausted recovery
|
|
207
|
+
// budget, an undrained scope. They classify as `other`, never `auth`, and are deliberately excluded from
|
|
208
|
+
// `fetchFailed` below: the request itself succeeded. `omission` carries that same fact structurally, so a
|
|
209
|
+
// consumer can act on it without parsing the message.
|
|
210
|
+
const omissions = metadata.omissions ?? [];
|
|
211
|
+
for (const omission of omissions) {
|
|
212
|
+
appendDedupedWarning(warnings, {
|
|
213
|
+
providerId: providerId,
|
|
214
|
+
domain: domain,
|
|
215
|
+
connectionId: connectionId,
|
|
216
|
+
message: collectionOmissionMessage(omission),
|
|
217
|
+
kind: 'other',
|
|
218
|
+
isAuth: false,
|
|
219
|
+
omission: toProviderWarningOmission(omission),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const incomplete = isIncompleteCollection(metadata);
|
|
224
|
+
// Only fall back to the generic message when nothing more specific was reported; an omission already
|
|
225
|
+
// explains the incompleteness in the consumer's terms, so adding this on top would be noise.
|
|
226
|
+
//
|
|
227
|
+
// This one deliberately carries no `omission`: it fires precisely when the SDK reported incompleteness
|
|
228
|
+
// WITHOUT saying what was left out, so there is no structured fact to forward and synthesizing one would
|
|
229
|
+
// assert a specificity this layer does not have.
|
|
230
|
+
if (incomplete && failures.length === 0 && omissions.length === 0) {
|
|
114
231
|
appendDedupedWarning(warnings, {
|
|
115
232
|
providerId: providerId,
|
|
116
233
|
domain: domain,
|
|
@@ -202,6 +202,10 @@ export type {
|
|
|
202
202
|
ProviderSweepResult,
|
|
203
203
|
ProviderWarning,
|
|
204
204
|
ProviderWarningKind,
|
|
205
|
+
ProviderWarningOmission,
|
|
206
|
+
ProviderWarningOmissionKind,
|
|
207
|
+
ProviderWarningOmissionRecovery,
|
|
208
|
+
ProviderWarningOmissionScope,
|
|
205
209
|
ProviderOrganization,
|
|
206
210
|
ProviderRepositoryShape,
|
|
207
211
|
RepositoryIdentity,
|
|
@@ -555,7 +555,7 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
|
|
|
555
555
|
async getMyIssues(
|
|
556
556
|
integrationIds?: (GitCloudHostIntegrationId | IssuesCloudHostIntegrationId | GitSelfManagedHostIntegrationId)[],
|
|
557
557
|
options?: { openRepositoriesOnly?: boolean; cancellation?: AbortSignal },
|
|
558
|
-
): Promise<IssueShape[] | undefined
|
|
558
|
+
): Promise<IntegrationResult<IssueShape[] | undefined>> {
|
|
559
559
|
const integrations: Map<Integration, ResourceDescriptor[] | undefined> = new Map();
|
|
560
560
|
const hostingIntegrationIds = integrationIds?.filter(
|
|
561
561
|
id => id in GitCloudHostIntegrationId || id in GitSelfManagedHostIntegrationId,
|
|
@@ -623,26 +623,54 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
|
|
|
623
623
|
private async getMyIssuesCore(
|
|
624
624
|
integrations: Map<Integration, ResourceDescriptor[] | undefined>,
|
|
625
625
|
cancellation?: AbortSignal,
|
|
626
|
-
): Promise<IssueShape[] | undefined
|
|
627
|
-
const
|
|
626
|
+
): Promise<IntegrationResult<IssueShape[] | undefined>> {
|
|
627
|
+
const start = performance.now();
|
|
628
|
+
|
|
629
|
+
const promises: Promise<IntegrationResult<IssueShape[] | undefined>>[] = [];
|
|
628
630
|
for (const [integration, repos] of integrations) {
|
|
629
631
|
if (integration == null) continue;
|
|
630
632
|
|
|
631
|
-
promises.push(integration.
|
|
633
|
+
promises.push(integration.searchMyIssuesResult(repos, cancellation));
|
|
632
634
|
}
|
|
633
635
|
|
|
634
636
|
const results = await Promise.allSettled(promises);
|
|
635
|
-
|
|
637
|
+
const successfulResults = [
|
|
638
|
+
...flatten(
|
|
639
|
+
filterMap(results, r =>
|
|
640
|
+
r.status === 'fulfilled' && r.value?.value != null ? r.value.value : undefined,
|
|
641
|
+
),
|
|
642
|
+
),
|
|
643
|
+
];
|
|
644
|
+
const errors = [
|
|
645
|
+
...filterMap(results, r =>
|
|
646
|
+
r.status === 'fulfilled' && r.value?.error != null ? r.value.error : undefined,
|
|
647
|
+
),
|
|
648
|
+
];
|
|
649
|
+
|
|
650
|
+
const error =
|
|
651
|
+
errors.length === 0
|
|
652
|
+
? undefined
|
|
653
|
+
: errors.length === 1
|
|
654
|
+
? errors[0]
|
|
655
|
+
: new AggregateError(errors, 'Failed to get some issues');
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
value: successfulResults,
|
|
659
|
+
error: error,
|
|
660
|
+
duration: performance.now() - start,
|
|
661
|
+
};
|
|
636
662
|
}
|
|
637
663
|
|
|
638
|
-
async getMyIssuesForRemotes(remote: GitRemote): Promise<IssueShape[] | undefined
|
|
639
|
-
async getMyIssuesForRemotes(remotes: GitRemote[]): Promise<IssueShape[] | undefined
|
|
664
|
+
async getMyIssuesForRemotes(remote: GitRemote): Promise<IntegrationResult<IssueShape[] | undefined>>;
|
|
665
|
+
async getMyIssuesForRemotes(remotes: GitRemote[]): Promise<IntegrationResult<IssueShape[] | undefined>>;
|
|
640
666
|
@trace({
|
|
641
667
|
args: (remoteOrRemotes: GitRemote | GitRemote[]) => ({
|
|
642
668
|
remoteOrRemotes: Array.isArray(remoteOrRemotes) ? remoteOrRemotes.map(rp => rp.name) : remoteOrRemotes.name,
|
|
643
669
|
}),
|
|
644
670
|
})
|
|
645
|
-
async getMyIssuesForRemotes(
|
|
671
|
+
async getMyIssuesForRemotes(
|
|
672
|
+
remoteOrRemotes: GitRemote | GitRemote[],
|
|
673
|
+
): Promise<IntegrationResult<IssueShape[] | undefined>> {
|
|
646
674
|
if (!Array.isArray(remoteOrRemotes)) {
|
|
647
675
|
remoteOrRemotes = [remoteOrRemotes];
|
|
648
676
|
}
|
|
@@ -653,7 +681,7 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
|
|
|
653
681
|
if (remote?.provider == null) return undefined;
|
|
654
682
|
|
|
655
683
|
const integration = await this.getByRemote(remote);
|
|
656
|
-
return integration?.
|
|
684
|
+
return integration?.searchMyIssuesResult(remote.provider.repoDesc);
|
|
657
685
|
}
|
|
658
686
|
|
|
659
687
|
const integrations = new Map<GitHostIntegration, ResourceDescriptor[]>();
|
|
@@ -737,6 +737,7 @@ export abstract class IntegrationBase<
|
|
|
737
737
|
const session = await this.resolveReadSession(connectionId, scope);
|
|
738
738
|
if (session == null) return undefined;
|
|
739
739
|
|
|
740
|
+
const start = performance.now();
|
|
740
741
|
try {
|
|
741
742
|
const issues = await this.searchProviderMyIssues(
|
|
742
743
|
session,
|
|
@@ -744,10 +745,10 @@ export abstract class IntegrationBase<
|
|
|
744
745
|
cancellation,
|
|
745
746
|
);
|
|
746
747
|
this.resetRequestExceptionCount('searchMyIssues');
|
|
747
|
-
return { value: issues };
|
|
748
|
+
return { value: issues, duration: performance.now() - start };
|
|
748
749
|
} catch (ex) {
|
|
749
750
|
this.handleProviderException('searchMyIssues', ex, { scope: scope, connectionId: connectionId });
|
|
750
|
-
return { error: toError(ex) };
|
|
751
|
+
return { error: toError(ex), duration: performance.now() - start };
|
|
751
752
|
}
|
|
752
753
|
}
|
|
753
754
|
|
|
@@ -470,7 +470,10 @@ export type GetIssueFn = (
|
|
|
470
470
|
export type GetIssuesForReposFn = (
|
|
471
471
|
input: (GetIssuesForReposInput | GetIssuesForRepoIdsInput) & PagingInput,
|
|
472
472
|
options?: EnterpriseOptions,
|
|
473
|
-
|
|
473
|
+
// GitHub's multi-repo issue search reports its 1,000-result cap — and, when it recovers past it by
|
|
474
|
+
// partitioning, what that recovery could not reach — through `metadata`. Documented on the contract for
|
|
475
|
+
// the reader; `getPagedResult` accepts and forwards `metadata` from any provider fn regardless.
|
|
476
|
+
) => Promise<{ data: ProviderIssue[]; pageInfo?: PageInfo; metadata?: CollectionMetadata }>;
|
|
474
477
|
|
|
475
478
|
export type GetIssuesForCurrentUserInput = PagingInput & {
|
|
476
479
|
// GitLab's account-wide REST read (`GET /issues`): `scope` controls breadth ('assigned_to_me' vs 'all') and
|
|
@@ -13,6 +13,7 @@ import type { PagedResult } from '../../../utils/paging.js';
|
|
|
13
13
|
import type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';
|
|
14
14
|
import type { TokenOptInfo, TokenWithInfo } from '../authentication/models.js';
|
|
15
15
|
import { toTokenWithInfo } from '../authentication/models.js';
|
|
16
|
+
import { isIncompleteCollection } from '../collectionMetadata.js';
|
|
16
17
|
import type { IntegrationIds } from '../constants.js';
|
|
17
18
|
import {
|
|
18
19
|
GitCloudHostIntegrationId,
|
|
@@ -32,7 +33,6 @@ import type {
|
|
|
32
33
|
GetIssueFn,
|
|
33
34
|
GetIssuesForReposFn,
|
|
34
35
|
GetIssuesOptions,
|
|
35
|
-
GetPullRequestsForRepoFn,
|
|
36
36
|
GetPullRequestsForReposFn,
|
|
37
37
|
GetPullRequestsForUserFn,
|
|
38
38
|
GetPullRequestsForUserOptions,
|
|
@@ -275,9 +275,7 @@ export class ProvidersApi {
|
|
|
275
275
|
getPullRequestsForReposFn: providerApis.gitlab.getPullRequestsForRepos.bind(
|
|
276
276
|
providerApis.gitlab,
|
|
277
277
|
) as GetPullRequestsForReposFn,
|
|
278
|
-
getPullRequestsForRepoFn: providerApis.gitlab.getPullRequestsForRepo.bind(
|
|
279
|
-
providerApis.gitlab,
|
|
280
|
-
) as GetPullRequestsForRepoFn,
|
|
278
|
+
getPullRequestsForRepoFn: providerApis.gitlab.getPullRequestsForRepo.bind(providerApis.gitlab),
|
|
281
279
|
getPullRequestsForUserFn: providerApis.gitlab.getPullRequestsAssociatedWithUser.bind(
|
|
282
280
|
providerApis.gitlab,
|
|
283
281
|
) as GetPullRequestsForUserFn,
|
|
@@ -302,9 +300,7 @@ export class ProvidersApi {
|
|
|
302
300
|
getPullRequestsForReposFn: providerApis.gitlab.getPullRequestsForRepos.bind(
|
|
303
301
|
providerApis.gitlab,
|
|
304
302
|
) as GetPullRequestsForReposFn,
|
|
305
|
-
getPullRequestsForRepoFn: providerApis.gitlab.getPullRequestsForRepo.bind(
|
|
306
|
-
providerApis.gitlab,
|
|
307
|
-
) as GetPullRequestsForRepoFn,
|
|
303
|
+
getPullRequestsForRepoFn: providerApis.gitlab.getPullRequestsForRepo.bind(providerApis.gitlab),
|
|
308
304
|
getPullRequestsForUserFn: providerApis.gitlab.getPullRequestsAssociatedWithUser.bind(
|
|
309
305
|
providerApis.gitlab,
|
|
310
306
|
) as GetPullRequestsForUserFn,
|
|
@@ -681,9 +677,9 @@ export class ProvidersApi {
|
|
|
681
677
|
// SDK collection completeness is independent from provider-native pagination: a result can expose a
|
|
682
678
|
// real next page (`more`) and still have a failed sibling scope (`partial`/`unknown`). Surface the
|
|
683
679
|
// latter as `truncated` so consumers treat the page as incomplete. Absent metadata (old providers,
|
|
684
|
-
// test doubles) leaves `truncated` unset for backward compatibility.
|
|
685
|
-
|
|
686
|
-
|
|
680
|
+
// test doubles) leaves `truncated` unset for backward compatibility. Shares the incompleteness
|
|
681
|
+
// predicate with the facade assessment so one metadata object can't be truncated there and whole here.
|
|
682
|
+
const truncated = isIncompleteCollection(normalizedMetadata) ? true : undefined;
|
|
687
683
|
|
|
688
684
|
return {
|
|
689
685
|
values: result.data,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CollectionMetadata } from '@gitkraken/provider-apis';
|
|
1
2
|
import type { Account } from '../../../git/models/author.js';
|
|
2
3
|
import type { Issue, IssueShape } from '../../../git/models/issue.js';
|
|
3
4
|
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../git/models/issueOrPullRequest.js';
|
|
@@ -103,7 +104,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
103
104
|
session: ProviderAuthenticationSession,
|
|
104
105
|
project: ResourceDescriptor,
|
|
105
106
|
options?: { user?: string; filters?: IssueFilter[] },
|
|
106
|
-
): Promise<{ values: IssueShape[]; truncated: boolean } | undefined> {
|
|
107
|
+
): Promise<{ values: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata } | undefined> {
|
|
107
108
|
// A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.
|
|
108
109
|
if (!isIssueResourceDescriptor(project)) return undefined;
|
|
109
110
|
|
|
@@ -139,7 +140,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
139
140
|
// never a cursor. Surface that as terminal truncation; there is no next page to fetch, so retrying the
|
|
140
141
|
// same read cannot recover the omitted cards (D11).
|
|
141
142
|
const truncated = result.metadata != null && result.metadata.completeness !== 'complete';
|
|
142
|
-
return { values: values, truncated: truncated };
|
|
143
|
+
return { values: values, truncated: truncated, metadata: result.metadata };
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
protected override searchProviderMyIssues(
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CollectionCompleteness,
|
|
3
|
+
CollectionMetadata,
|
|
4
|
+
CollectionOmission,
|
|
5
|
+
CollectionScopeFailure,
|
|
6
|
+
} from '@gitkraken/provider-apis';
|
|
2
7
|
import { isCancellationError } from '../../../../utils/cancellation.js';
|
|
8
|
+
import { uniqueBy } from '../../../../utils/iterable.js';
|
|
3
9
|
import { toCollectionScopeFailure } from '../../collectionMetadata.js';
|
|
10
|
+
import { collectionScopeKey } from '../../results.js';
|
|
4
11
|
import type { ProviderApiPagedResult, ProviderHierarchyResult } from '../models.js';
|
|
5
12
|
|
|
6
13
|
/**
|
|
@@ -42,21 +49,29 @@ const completenessRank: Record<CollectionCompleteness, number> = { partial: 2, u
|
|
|
42
49
|
|
|
43
50
|
/** A stable key for deduplicating structurally-identical scope failures accumulated across drained pages. */
|
|
44
51
|
function collectionFailureKey(failure: CollectionScopeFailure): string {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
return [failure.kind, collectionScopeKey(failure.scope), failure.message ?? ''].join(' ');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A stable key for collapsing omissions accumulated across drained pages, matching the SDK's own
|
|
57
|
+
* `dedupeOmissions`: kind plus scope IDs, deliberately WITHOUT `limit`/`totalCount`.
|
|
58
|
+
*
|
|
59
|
+
* Those counts are a re-measurement, not an identity. GitHub recomputes the match total on every request, so
|
|
60
|
+
* one repository drained over several pages reports the same cap with a drifting total; keying on the total
|
|
61
|
+
* would emit a near-identical warning per page ("matched 1393…", "matched 1402…") that
|
|
62
|
+
* `appendDedupedWarning` cannot collapse, since the messages genuinely differ. Independently-scoped omissions
|
|
63
|
+
* still stay distinct, which is the case that carries information.
|
|
64
|
+
*/
|
|
65
|
+
function collectionOmissionKey(omission: CollectionOmission): string {
|
|
66
|
+
return [omission.kind, collectionScopeKey(omission.scope)].join(' ');
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
/**
|
|
57
70
|
* Merges SDK collection metadata across drained pages. Completeness follows {@link completenessRank};
|
|
58
|
-
* failures are
|
|
59
|
-
*
|
|
71
|
+
* failures are deduplicated by kind, scope IDs, and message, and omissions are collapsed per kind and scope
|
|
72
|
+
* keeping the highest reported total ({@link collectionOmissionKey}). Both preserve first-reported order.
|
|
73
|
+
* Returns `undefined` when no page supplied metadata, so metadata-free providers and test doubles keep
|
|
74
|
+
* behaving as before.
|
|
60
75
|
*/
|
|
61
76
|
export function mergeCollectionMetadata(
|
|
62
77
|
base: CollectionMetadata | undefined,
|
|
@@ -70,17 +85,25 @@ export function mergeCollectionMetadata(
|
|
|
70
85
|
? next.completeness
|
|
71
86
|
: base.completeness;
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
// First occurrence wins: a repeated failure carries no new information, since the message is part of its key.
|
|
89
|
+
const failures = [
|
|
90
|
+
...uniqueBy([...(base.failures ?? []), ...(next.failures ?? [])], collectionFailureKey, original => original),
|
|
91
|
+
];
|
|
92
|
+
// Highest total wins, so re-measuring one cap across pages collapses to a single omission holding the
|
|
93
|
+
// largest figure reported for it — the same rule the SDK applies in `createCollectionMetadata`.
|
|
94
|
+
const omissions = [
|
|
95
|
+
...uniqueBy(
|
|
96
|
+
[...(base.omissions ?? []), ...(next.omissions ?? [])],
|
|
97
|
+
collectionOmissionKey,
|
|
98
|
+
(original, current) => ((current.totalCount ?? -1) > (original.totalCount ?? -1) ? current : undefined),
|
|
99
|
+
),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
completeness: completeness,
|
|
104
|
+
...(failures.length ? { failures: failures } : {}),
|
|
105
|
+
...(omissions.length ? { omissions: omissions } : {}),
|
|
106
|
+
};
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
/**
|
|
@@ -9,7 +9,7 @@ import type { PullRequestFilter } from '../providerFilters.js';
|
|
|
9
9
|
import type { ProviderPullRequest, ProviderReposInput, ProviderRepository } from '../providers/models.js';
|
|
10
10
|
import { getProviderPullRequestIdentity } from '../providers/models.js';
|
|
11
11
|
import type { ProviderWarning } from '../results.js';
|
|
12
|
-
import { appendDedupedWarning, toProviderWarning } from '../results.js';
|
|
12
|
+
import { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from '../results.js';
|
|
13
13
|
import { isIssuesHostIntegrationId } from '../utils/integration.utils.js';
|
|
14
14
|
import { noConnectionWarning, truncationWarning } from './warnings.js';
|
|
15
15
|
|
|
@@ -90,10 +90,18 @@ export async function drainPullRequests(
|
|
|
90
90
|
// this through the terminal returns instead of resetting it to false at the last page.
|
|
91
91
|
let fetchFailed = false;
|
|
92
92
|
let truncated = false;
|
|
93
|
+
// A page the provider capped or couldn't vouch for. Decides the CAUSE the terminal warning reports: a cap
|
|
94
|
+
// outranks a budget stop, because raising a budget cannot un-cap a page.
|
|
95
|
+
let providerTruncated = false;
|
|
96
|
+
// The subset of that which no other warning already explains — decides whether this drain raises one of
|
|
97
|
+
// its own on an otherwise clean exit.
|
|
98
|
+
let unexplainedTruncation = false;
|
|
93
99
|
|
|
94
100
|
// With no repos this is an account-wide "my PRs" sweep. The repo-scoped core rejects an empty `repos`
|
|
95
101
|
// input, so read the provider-native account-wide core instead.
|
|
96
102
|
const accountWide = repos.length === 0;
|
|
103
|
+
/** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */
|
|
104
|
+
const seenCursors = new Set<string>();
|
|
97
105
|
|
|
98
106
|
for (;;) {
|
|
99
107
|
page++;
|
|
@@ -130,10 +138,23 @@ export async function drainPullRequests(
|
|
|
130
138
|
appendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));
|
|
131
139
|
}
|
|
132
140
|
// `warning` set → a hard read failure (incomplete items); otherwise not connected / no session.
|
|
141
|
+
const failed = fetchFailed || warning != null || unavailable;
|
|
142
|
+
// A cap seen on an earlier page still left results out, so it is still worth saying — but as part
|
|
143
|
+
// of a read that failed, never as an omission. (`failed` is always true here when anything was
|
|
144
|
+
// latched: reaching this exit past page 1 means a later page was lost.)
|
|
145
|
+
if (unexplainedTruncation) {
|
|
146
|
+
appendDedupedWarning(
|
|
147
|
+
warnings,
|
|
148
|
+
truncationWarning(id, domain, connectionId, 'Pull request', failed ? 'interrupted' : 'exhausted'),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
// An earlier page may already have emitted an omission before this one died; it asserts the read
|
|
152
|
+
// succeeded, which is no longer true.
|
|
153
|
+
reconcileOmissionsWithFailure(warnings, failed);
|
|
133
154
|
return {
|
|
134
155
|
items: items,
|
|
135
156
|
warnings: warnings,
|
|
136
|
-
fetchFailed:
|
|
157
|
+
fetchFailed: failed,
|
|
137
158
|
truncated: truncated || sessionLostAfterProgress,
|
|
138
159
|
// Only a top-level first-page rejection means the provider itself failed. A later-page or
|
|
139
160
|
// per-scope failure still yielded a usable provider slice and stays represented separately.
|
|
@@ -171,14 +192,31 @@ export async function drainPullRequests(
|
|
|
171
192
|
value.paging?.truncated === true ||
|
|
172
193
|
assessment.truncated;
|
|
173
194
|
truncated = truncated || pageTruncated;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
// A page the provider itself capped, or one whose completeness it couldn't confirm. Latched from ANY
|
|
196
|
+
// source, SDK metadata included: no budget of ours un-caps a page, so a later `maxPages` hit must not
|
|
197
|
+
// claim raising it would help.
|
|
198
|
+
providerTruncated = providerTruncated || pageTruncated;
|
|
199
|
+
// Whether this drain owes its OWN warning for that is a separate question — `mergeAssessmentInto` has
|
|
200
|
+
// already appended one when the fact came from SDK metadata, and repeating it would be noise.
|
|
201
|
+
unexplainedTruncation = unexplainedTruncation || (pageTruncated && !assessment.truncated);
|
|
177
202
|
|
|
178
203
|
if (!(value.paging?.more ?? false)) {
|
|
179
204
|
// A read that can't confirm completeness (single-page provider reads with no `hasNextPage`)
|
|
180
205
|
// sets `paging.truncated`; propagate it (and any top-level `truncated` and SDK incompleteness)
|
|
181
206
|
// so the sweep doesn't claim an all-pages result.
|
|
207
|
+
if (unexplainedTruncation) {
|
|
208
|
+
appendDedupedWarning(
|
|
209
|
+
warnings,
|
|
210
|
+
truncationWarning(
|
|
211
|
+
id,
|
|
212
|
+
domain,
|
|
213
|
+
connectionId,
|
|
214
|
+
'Pull request',
|
|
215
|
+
fetchFailed ? 'interrupted' : 'exhausted',
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
182
220
|
return {
|
|
183
221
|
items: items,
|
|
184
222
|
warnings: warnings,
|
|
@@ -187,21 +225,34 @@ export async function drainPullRequests(
|
|
|
187
225
|
failedProvider: false,
|
|
188
226
|
};
|
|
189
227
|
}
|
|
190
|
-
if (page >= maxPages) {
|
|
191
|
-
appendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, 'Pull request'));
|
|
192
|
-
return {
|
|
193
|
-
items: items,
|
|
194
|
-
warnings: warnings,
|
|
195
|
-
fetchFailed: fetchFailed,
|
|
196
|
-
truncated: true,
|
|
197
|
-
failedProvider: false,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
228
|
|
|
229
|
+
// Resolve the continuation BEFORE deciding why the drain stops. `page-budget` claims the missing items
|
|
230
|
+
// are reachable, which is only true with a usable cursor in hand — and a provider can report another
|
|
231
|
+
// page while handing back none (Bitbucket Server does, when it omits `nextPageStart`). Checking the
|
|
232
|
+
// budget first would label that unreachable tail as merely unfetched, so raising `maxPages` would
|
|
233
|
+
// return the identical set.
|
|
201
234
|
const nextCursor = value.paging?.cursor;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
235
|
+
// A cursor already used isn't a continuation either — following it refetches a page we have, and a
|
|
236
|
+
// provider that cycles (A→B→A) would otherwise burn the whole budget and then be reported as merely
|
|
237
|
+
// out of budget. Tracked as a SET rather than compared one-back, matching the SDK's own `followCursors`:
|
|
238
|
+
// `drainToRequestedPage` and `collectProviderPagedResult` compare only the previous cursor, which a
|
|
239
|
+
// cycle slips past. That is tolerable there and not here, because only this drain reports `page-budget`.
|
|
240
|
+
const continuable = nextCursor != null && nextCursor !== '{}' && !seenCursors.has(nextCursor);
|
|
241
|
+
if (!continuable || page >= maxPages) {
|
|
242
|
+
// `providerTruncated` outranks the budget: a page the provider capped stays capped however many
|
|
243
|
+
// pages we are allowed to read, so promising `page-budget` on top of it would be a load-more that
|
|
244
|
+
// cannot deliver the capped part.
|
|
245
|
+
appendDedupedWarning(
|
|
246
|
+
warnings,
|
|
247
|
+
truncationWarning(
|
|
248
|
+
id,
|
|
249
|
+
domain,
|
|
250
|
+
connectionId,
|
|
251
|
+
'Pull request',
|
|
252
|
+
fetchFailed ? 'interrupted' : continuable && !providerTruncated ? 'page-budget' : 'exhausted',
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
205
256
|
return {
|
|
206
257
|
items: items,
|
|
207
258
|
warnings: warnings,
|
|
@@ -211,6 +262,7 @@ export async function drainPullRequests(
|
|
|
211
262
|
};
|
|
212
263
|
}
|
|
213
264
|
|
|
265
|
+
seenCursors.add(nextCursor);
|
|
214
266
|
cursor = nextCursor;
|
|
215
267
|
}
|
|
216
268
|
}
|
|
@@ -262,12 +314,20 @@ export async function drainRepositories(
|
|
|
262
314
|
if (value == null) {
|
|
263
315
|
const interruptedAfterProgress = page > 1;
|
|
264
316
|
if (interruptedAfterProgress && warning == null) {
|
|
265
|
-
|
|
317
|
+
// Not a backstop: the read was cut short mid-drain, which is why it also sets `fetchFailed`
|
|
318
|
+
// below. A retry may complete it, so this must not claim the succeeded-but-capped omission.
|
|
319
|
+
appendDedupedWarning(
|
|
320
|
+
warnings,
|
|
321
|
+
truncationWarning(id, domain, connectionId, 'Repository', 'interrupted'),
|
|
322
|
+
);
|
|
266
323
|
}
|
|
324
|
+
const failed = fetchFailed || warning != null || interruptedAfterProgress;
|
|
325
|
+
// An SDK omission from an earlier page asserts the read succeeded; this one didn't.
|
|
326
|
+
reconcileOmissionsWithFailure(warnings, failed);
|
|
267
327
|
return {
|
|
268
328
|
repos: repos,
|
|
269
329
|
warnings: warnings,
|
|
270
|
-
fetchFailed:
|
|
330
|
+
fetchFailed: failed,
|
|
271
331
|
truncated: truncated || interruptedAfterProgress,
|
|
272
332
|
};
|
|
273
333
|
}
|
|
@@ -277,6 +337,7 @@ export async function drainRepositories(
|
|
|
277
337
|
fetchFailed = fetchFailed || assessment.fetchFailed;
|
|
278
338
|
truncated = truncated || value.truncated === true || value.paging?.truncated === true || assessment.truncated;
|
|
279
339
|
if (!(value.paging?.more ?? false)) {
|
|
340
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
280
341
|
return {
|
|
281
342
|
repos: repos,
|
|
282
343
|
warnings: warnings,
|
|
@@ -284,13 +345,16 @@ export async function drainRepositories(
|
|
|
284
345
|
truncated: truncated,
|
|
285
346
|
};
|
|
286
347
|
}
|
|
348
|
+
|
|
287
349
|
if (page >= maxPages) {
|
|
350
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
288
351
|
return { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };
|
|
289
352
|
}
|
|
290
353
|
|
|
291
354
|
const nextCursor = value.paging?.cursor;
|
|
292
355
|
if (nextCursor == null || nextCursor === '{}') {
|
|
293
356
|
// Provider says there is more but didn't return a usable cursor; stop rather than refetch the same page.
|
|
357
|
+
reconcileOmissionsWithFailure(warnings, fetchFailed);
|
|
294
358
|
return { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };
|
|
295
359
|
}
|
|
296
360
|
|