@gitkraken/core-gitlens 0.5.100 → 0.5.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 +19 -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 +21 -1
- package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
- package/dist/plus/integrations/collectionMetadata.js +82 -6
- package/dist/plus/integrations/collectionMetadata.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 -22
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +4 -2
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +4 -2
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- 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 +97 -7
- 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 -24
- package/src/plus/integrations/reads/warnings.ts +4 -2
|
@@ -2,7 +2,7 @@ import type { Endpoints } from '@octokit/types';
|
|
|
2
2
|
import { GitFileIndexStatus } from '../../git/models/fileStatus.js';
|
|
3
3
|
import type { IssueLabel } from '../../git/models/issue.js';
|
|
4
4
|
import { Issue, RepositoryAccessLevel } from '../../git/models/issue.js';
|
|
5
|
-
import type { PullRequestState } from '../../git/models/pullRequest.js';
|
|
5
|
+
import type { PullRequestMember, PullRequestState } from '../../git/models/pullRequest.js';
|
|
6
6
|
import {
|
|
7
7
|
PullRequest,
|
|
8
8
|
PullRequestMergeableState,
|
|
@@ -131,8 +131,9 @@ export type GitHubPullRequestState = 'OPEN' | 'CLOSED' | 'MERGED';
|
|
|
131
131
|
export type GitHubIssueOrPullRequestState = GitHubIssueState | GitHubPullRequestState;
|
|
132
132
|
|
|
133
133
|
export interface GitHubPullRequestLite extends Omit<GitHubIssueOrPullRequest, '__typename'> {
|
|
134
|
-
author
|
|
135
|
-
|
|
134
|
+
/** `Actor` is nullable in GitHub's schema — `null` once the author's account is deleted */
|
|
135
|
+
author: GitHubMember | null;
|
|
136
|
+
body: string | null;
|
|
136
137
|
|
|
137
138
|
baseRefName: string;
|
|
138
139
|
baseRefOid: string;
|
|
@@ -167,7 +168,8 @@ export interface GitHubPullRequestLite extends Omit<GitHubIssueOrPullRequest, '_
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
export interface GitHubIssue extends Omit<GitHubIssueOrPullRequest, '__typename'> {
|
|
170
|
-
author
|
|
171
|
+
/** `Actor` is nullable in GitHub's schema — `null` once the author's account is deleted */
|
|
172
|
+
author: GitHubMember | null;
|
|
171
173
|
assignees: { nodes: GitHubMember[] };
|
|
172
174
|
comments?: {
|
|
173
175
|
totalCount: number;
|
|
@@ -203,7 +205,7 @@ export interface GitHubPullRequest extends GitHubPullRequestLite {
|
|
|
203
205
|
reviewDecision: GitHubPullRequestReviewDecision;
|
|
204
206
|
latestReviews: {
|
|
205
207
|
nodes: {
|
|
206
|
-
author: GitHubMember;
|
|
208
|
+
author: GitHubMember | null;
|
|
207
209
|
state: GitHubPullRequestReviewState;
|
|
208
210
|
}[];
|
|
209
211
|
};
|
|
@@ -234,15 +236,17 @@ export type GitHubViewerPermission =
|
|
|
234
236
|
| 'READ' // Can read and clone this repository. Can also open and comment on issues and pull requests
|
|
235
237
|
| 'NONE';
|
|
236
238
|
|
|
239
|
+
/** `ghost` is how github.com renders an actor whose account was deleted */
|
|
240
|
+
function fromGitHubMemberOrGhost(member: GitHubMember | null | undefined): PullRequestMember {
|
|
241
|
+
if (member == null) return { id: 'ghost', name: 'ghost' };
|
|
242
|
+
|
|
243
|
+
return { id: member.login, name: member.login, avatarUrl: member.avatarUrl, url: member.url };
|
|
244
|
+
}
|
|
245
|
+
|
|
237
246
|
export function fromGitHubPullRequestLite(pr: GitHubPullRequestLite, provider: Provider): PullRequest {
|
|
238
247
|
return new PullRequest(
|
|
239
248
|
provider,
|
|
240
|
-
|
|
241
|
-
id: pr.author.login,
|
|
242
|
-
name: pr.author.login,
|
|
243
|
-
avatarUrl: pr.author.avatarUrl,
|
|
244
|
-
url: pr.author.url,
|
|
245
|
-
},
|
|
249
|
+
fromGitHubMemberOrGhost(pr.author),
|
|
246
250
|
String(pr.number),
|
|
247
251
|
pr.id,
|
|
248
252
|
pr.title,
|
|
@@ -296,7 +300,7 @@ export function fromGitHubPullRequestLite(pr: GitHubPullRequestLite, provider: P
|
|
|
296
300
|
undefined, // statusCheckRollupState
|
|
297
301
|
undefined, // project
|
|
298
302
|
undefined, // version
|
|
299
|
-
pr.body,
|
|
303
|
+
pr.body ?? undefined,
|
|
300
304
|
);
|
|
301
305
|
}
|
|
302
306
|
|
|
@@ -399,12 +403,7 @@ export function fromGitHubPullRequestStatusCheckRollupState(
|
|
|
399
403
|
export function fromGitHubPullRequest(pr: GitHubPullRequest, provider: Provider): PullRequest {
|
|
400
404
|
return new PullRequest(
|
|
401
405
|
provider,
|
|
402
|
-
|
|
403
|
-
id: pr.author.login,
|
|
404
|
-
name: pr.author.login,
|
|
405
|
-
avatarUrl: pr.author.avatarUrl,
|
|
406
|
-
url: pr.author.url,
|
|
407
|
-
},
|
|
406
|
+
fromGitHubMemberOrGhost(pr.author),
|
|
408
407
|
String(pr.number),
|
|
409
408
|
pr.id,
|
|
410
409
|
pr.title,
|
|
@@ -469,12 +468,7 @@ export function fromGitHubPullRequest(pr: GitHubPullRequest, provider: Provider)
|
|
|
469
468
|
)
|
|
470
469
|
.filter(<T>(r?: T): r is T => Boolean(r)),
|
|
471
470
|
pr.latestReviews.nodes.map(r => ({
|
|
472
|
-
reviewer:
|
|
473
|
-
id: r.author.login,
|
|
474
|
-
name: r.author.login,
|
|
475
|
-
avatarUrl: r.author.avatarUrl,
|
|
476
|
-
url: r.author.url,
|
|
477
|
-
},
|
|
471
|
+
reviewer: fromGitHubMemberOrGhost(r.author),
|
|
478
472
|
state: fromGitHubPullRequestReviewState(r.state),
|
|
479
473
|
})),
|
|
480
474
|
pr.assignees.nodes.map(r => ({
|
|
@@ -486,7 +480,7 @@ export function fromGitHubPullRequest(pr: GitHubPullRequest, provider: Provider)
|
|
|
486
480
|
fromGitHubPullRequestStatusCheckRollupState(pr.commits.nodes?.[0]?.commit.statusCheckRollup?.state),
|
|
487
481
|
undefined, // project
|
|
488
482
|
undefined, // version
|
|
489
|
-
pr.body,
|
|
483
|
+
pr.body ?? undefined,
|
|
490
484
|
);
|
|
491
485
|
}
|
|
492
486
|
|
|
@@ -506,12 +500,14 @@ export function fromGitHubIssue(value: GitHubIssue, provider: Provider): Issue {
|
|
|
506
500
|
new Date(value.updatedAt),
|
|
507
501
|
value.closed,
|
|
508
502
|
fromGitHubIssueOrPullRequestState(value.state),
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
503
|
+
value.author == null
|
|
504
|
+
? undefined
|
|
505
|
+
: {
|
|
506
|
+
id: value.author.login,
|
|
507
|
+
name: value.author.login,
|
|
508
|
+
avatarUrl: value.author.avatarUrl,
|
|
509
|
+
url: value.author.url,
|
|
510
|
+
},
|
|
515
511
|
value.assignees.nodes.map(assignee => ({
|
|
516
512
|
id: assignee.login,
|
|
517
513
|
name: assignee.login,
|
|
@@ -1,4 +1,9 @@
|
|
|
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';
|
|
@@ -68,8 +73,21 @@ function toCollectionFailureWarningKind(failure: CollectionScopeFailure): Provid
|
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
/**
|
|
77
|
+
* A scope's identity, as the stable string that keys it.
|
|
78
|
+
*
|
|
79
|
+
* The dedup keys in `providerPaging.ts` build on this, so failures and omissions can never disagree about
|
|
80
|
+
* what "the same scope" means. `providerId` is included: the same repository ID under two providers is two
|
|
81
|
+
* scopes.
|
|
82
|
+
*/
|
|
83
|
+
export function collectionScopeKey(scope: CollectionScope | undefined): string {
|
|
84
|
+
return [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(
|
|
85
|
+
' ',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** ` (resource r, project p, repository o/n)` for the scope IDs present; empty when the scope names none. */
|
|
90
|
+
function collectionScopeText(scope: CollectionScope | undefined): string {
|
|
73
91
|
const parts: string[] = [];
|
|
74
92
|
if (scope?.resourceId != null) {
|
|
75
93
|
parts.push(`resource ${scope.resourceId}`);
|
|
@@ -81,9 +99,64 @@ function collectionFailureMessage(failure: CollectionScopeFailure): string {
|
|
|
81
99
|
parts.push(`repository ${scope.repositoryId}`);
|
|
82
100
|
}
|
|
83
101
|
|
|
84
|
-
|
|
102
|
+
return parts.length ? ` (${parts.join(', ')})` : '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function collectionFailureMessage(failure: CollectionScopeFailure): string {
|
|
85
106
|
const detail = failure.message != null ? `: ${failure.message}` : '';
|
|
86
|
-
return `Failed to read ${failure.kind} scope${
|
|
107
|
+
return `Failed to read ${failure.kind} scope${collectionScopeText(failure.scope)}${detail}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Explains one omission in the consumer's terms — what was left out and, where the SDK reports it, how much.
|
|
112
|
+
*
|
|
113
|
+
* An omission is a completeness fact, never a failure: the read succeeded and the provider (or the SDK's own
|
|
114
|
+
* recovery budget) is what withheld results, so retrying the same request cannot recover them. That is why
|
|
115
|
+
* these never contribute to `fetchFailed`.
|
|
116
|
+
*/
|
|
117
|
+
function collectionOmissionMessage(omission: CollectionOmission): string {
|
|
118
|
+
const scopeText = collectionScopeText(omission.scope);
|
|
119
|
+
switch (omission.kind) {
|
|
120
|
+
case 'provider-limit':
|
|
121
|
+
// `totalCount` is the only figure saying how much was withheld; both fields are optional, so degrade
|
|
122
|
+
// through the shapes the SDK can actually emit rather than printing `undefined`.
|
|
123
|
+
if (omission.totalCount != null && omission.limit != null) {
|
|
124
|
+
return `Search${scopeText} matched ${omission.totalCount} results, but the provider exposes at most ${omission.limit}`;
|
|
125
|
+
}
|
|
126
|
+
if (omission.limit != null) {
|
|
127
|
+
return `Search${scopeText} exceeded the provider limit of ${omission.limit} results`;
|
|
128
|
+
}
|
|
129
|
+
return `Search${scopeText} exceeded the provider's result limit`;
|
|
130
|
+
case 'recovery-budget':
|
|
131
|
+
return `Stopped recovering omitted results${scopeText} after reaching the request budget${
|
|
132
|
+
omission.limit != null ? ` of ${omission.limit} requests` : ''
|
|
133
|
+
}`;
|
|
134
|
+
case 'pagination-incomplete':
|
|
135
|
+
return `More results are available${scopeText} than this read returned`;
|
|
136
|
+
default:
|
|
137
|
+
// `CollectionOmissionKind` is a closed union, so this is unreachable today. Kept as a compile-time
|
|
138
|
+
// guard rather than a silent fallback: an SDK bump that adds a kind breaks the build here instead
|
|
139
|
+
// of quietly degrading it to a vague message.
|
|
140
|
+
omission.kind satisfies never;
|
|
141
|
+
return `Some results were omitted${scopeText}; the read is incomplete`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether SDK metadata describes a read that may be missing results.
|
|
147
|
+
*
|
|
148
|
+
* A reported omission counts on its own rather than deferring to `completeness`. The SDK does couple the
|
|
149
|
+
* two — every site that emits an omission degrades completeness in the same call — so today the omission
|
|
150
|
+
* clause changes no outcome. It is defensive: the two facts arrive as independent fields, and a read that
|
|
151
|
+
* names what it left out must not be publishable as whole if a future producer sets only one of them.
|
|
152
|
+
*
|
|
153
|
+
* This is the single definition of "may be missing results", shared with `getPagedResult`, so one metadata
|
|
154
|
+
* object cannot be truncated at one layer and complete at another.
|
|
155
|
+
*/
|
|
156
|
+
export function isIncompleteCollection(metadata: CollectionMetadata | undefined): boolean {
|
|
157
|
+
if (metadata == null) return false;
|
|
158
|
+
|
|
159
|
+
return metadata.completeness !== 'complete' || (metadata.omissions?.length ?? 0) > 0;
|
|
87
160
|
}
|
|
88
161
|
|
|
89
162
|
/** Converts internal SDK collection metadata into neutral provider facade signals. */
|
|
@@ -109,8 +182,25 @@ export function assessCollectionMetadata(
|
|
|
109
182
|
});
|
|
110
183
|
}
|
|
111
184
|
|
|
112
|
-
|
|
113
|
-
|
|
185
|
+
// Omissions explain WHY a read is incomplete when nothing failed — a provider cap, an exhausted recovery
|
|
186
|
+
// budget, an undrained scope. They classify as `other`, never `auth`, and are deliberately excluded from
|
|
187
|
+
// `fetchFailed` below: the request succeeded, so a retry would return the same truncated set.
|
|
188
|
+
const omissions = metadata.omissions ?? [];
|
|
189
|
+
for (const omission of omissions) {
|
|
190
|
+
appendDedupedWarning(warnings, {
|
|
191
|
+
providerId: providerId,
|
|
192
|
+
domain: domain,
|
|
193
|
+
connectionId: connectionId,
|
|
194
|
+
message: collectionOmissionMessage(omission),
|
|
195
|
+
kind: 'other',
|
|
196
|
+
isAuth: false,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const incomplete = isIncompleteCollection(metadata);
|
|
201
|
+
// Only fall back to the generic message when nothing more specific was reported; an omission already
|
|
202
|
+
// explains the incompleteness in the consumer's terms, so adding this on top would be noise.
|
|
203
|
+
if (incomplete && failures.length === 0 && omissions.length === 0) {
|
|
114
204
|
appendDedupedWarning(warnings, {
|
|
115
205
|
providerId: providerId,
|
|
116
206
|
domain: domain,
|
|
@@ -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,12 @@
|
|
|
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';
|
|
3
|
-
import {
|
|
8
|
+
import { uniqueBy } from '../../../../utils/iterable.js';
|
|
9
|
+
import { collectionScopeKey, toCollectionScopeFailure } from '../../collectionMetadata.js';
|
|
4
10
|
import type { ProviderApiPagedResult, ProviderHierarchyResult } from '../models.js';
|
|
5
11
|
|
|
6
12
|
/**
|
|
@@ -42,21 +48,29 @@ const completenessRank: Record<CollectionCompleteness, number> = { partial: 2, u
|
|
|
42
48
|
|
|
43
49
|
/** A stable key for deduplicating structurally-identical scope failures accumulated across drained pages. */
|
|
44
50
|
function collectionFailureKey(failure: CollectionScopeFailure): string {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
return [failure.kind, collectionScopeKey(failure.scope), failure.message ?? ''].join(' ');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A stable key for collapsing omissions accumulated across drained pages, matching the SDK's own
|
|
56
|
+
* `dedupeOmissions`: kind plus scope IDs, deliberately WITHOUT `limit`/`totalCount`.
|
|
57
|
+
*
|
|
58
|
+
* Those counts are a re-measurement, not an identity. GitHub recomputes the match total on every request, so
|
|
59
|
+
* one repository drained over several pages reports the same cap with a drifting total; keying on the total
|
|
60
|
+
* would emit a near-identical warning per page ("matched 1393…", "matched 1402…") that
|
|
61
|
+
* `appendDedupedWarning` cannot collapse, since the messages genuinely differ. Independently-scoped omissions
|
|
62
|
+
* still stay distinct, which is the case that carries information.
|
|
63
|
+
*/
|
|
64
|
+
function collectionOmissionKey(omission: CollectionOmission): string {
|
|
65
|
+
return [omission.kind, collectionScopeKey(omission.scope)].join(' ');
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
/**
|
|
57
69
|
* Merges SDK collection metadata across drained pages. Completeness follows {@link completenessRank};
|
|
58
|
-
* failures are
|
|
59
|
-
*
|
|
70
|
+
* failures are deduplicated by kind, scope IDs, and message, and omissions are collapsed per kind and scope
|
|
71
|
+
* keeping the highest reported total ({@link collectionOmissionKey}). Both preserve first-reported order.
|
|
72
|
+
* Returns `undefined` when no page supplied metadata, so metadata-free providers and test doubles keep
|
|
73
|
+
* behaving as before.
|
|
60
74
|
*/
|
|
61
75
|
export function mergeCollectionMetadata(
|
|
62
76
|
base: CollectionMetadata | undefined,
|
|
@@ -70,17 +84,25 @@ export function mergeCollectionMetadata(
|
|
|
70
84
|
? next.completeness
|
|
71
85
|
: base.completeness;
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
// First occurrence wins: a repeated failure carries no new information, since the message is part of its key.
|
|
88
|
+
const failures = [
|
|
89
|
+
...uniqueBy([...(base.failures ?? []), ...(next.failures ?? [])], collectionFailureKey, original => original),
|
|
90
|
+
];
|
|
91
|
+
// Highest total wins, so re-measuring one cap across pages collapses to a single omission holding the
|
|
92
|
+
// largest figure reported for it — the same rule the SDK applies in `createCollectionMetadata`.
|
|
93
|
+
const omissions = [
|
|
94
|
+
...uniqueBy(
|
|
95
|
+
[...(base.omissions ?? []), ...(next.omissions ?? [])],
|
|
96
|
+
collectionOmissionKey,
|
|
97
|
+
(original, current) => ((current.totalCount ?? -1) > (original.totalCount ?? -1) ? current : undefined),
|
|
98
|
+
),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
completeness: completeness,
|
|
103
|
+
...(failures.length ? { failures: failures } : {}),
|
|
104
|
+
...(omissions.length ? { omissions: omissions } : {}),
|
|
105
|
+
};
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
/**
|
|
@@ -15,8 +15,10 @@ import type { ProviderWarning } from '../results.js';
|
|
|
15
15
|
/**
|
|
16
16
|
* The single builder for a provider-neutral, non-auth warning: an unsupported capability, a
|
|
17
17
|
* contradictory/inexpressible request, or a read that couldn't confirm completeness. Every `kind: 'other'`
|
|
18
|
-
* warning the facade
|
|
19
|
-
* recurring message), so the discriminant is assigned in
|
|
18
|
+
* warning the facade raises on its own terms goes through here (directly, or via one of the named builders
|
|
19
|
+
* below that pin a recurring message), so the discriminant is assigned in one place. The warnings mapped from
|
|
20
|
+
* SDK collection metadata are the documented exception — `collectionMetadata.ts` builds those from the
|
|
21
|
+
* structured failure/omission it is given.
|
|
20
22
|
*
|
|
21
23
|
* The kinds that carry a programmatic remedy — `auth`, `rate-limit`, `not-found`, `no-connection` — are derived
|
|
22
24
|
* from the caught error's type instead and never come from here; see `ProviderWarningKind` and
|