@gitkraken/core-gitlens 0.4.100 → 0.5.100
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 +12 -4
- package/dist/git/models/graph.d.ts +63 -33
- package/dist/git/models/graph.d.ts.map +1 -1
- package/dist/git/models/graph.js.map +1 -1
- package/dist/git/models/graphSession.d.ts +3 -84
- package/dist/git/models/graphSession.d.ts.map +1 -1
- package/dist/git/models/graphSession.js +1 -6
- package/dist/git/models/graphSession.js.map +1 -1
- package/dist/git/providers/graph.d.ts +1 -15
- package/dist/git/providers/graph.d.ts.map +1 -1
- package/dist/git/providers/pausedOperations.d.ts +2 -0
- package/dist/git/providers/pausedOperations.d.ts.map +1 -1
- package/dist/git/utils/reachability.utils.d.ts +1 -1
- package/dist/git/utils/reachability.utils.js +1 -1
- package/dist/git-cli/providers/graph.d.ts +1 -3
- package/dist/git-cli/providers/graph.d.ts.map +1 -1
- package/dist/git-cli/providers/graph.js +37 -214
- package/dist/git-cli/providers/graph.js.map +1 -1
- package/dist/git-cli/providers/pausedOperations.d.ts +1 -0
- package/dist/git-cli/providers/pausedOperations.d.ts.map +1 -1
- package/dist/git-cli/providers/pausedOperations.js +21 -1
- package/dist/git-cli/providers/pausedOperations.js.map +1 -1
- package/dist/git-cli/providers/staging.d.ts.map +1 -1
- package/dist/git-cli/providers/staging.js +10 -1
- package/dist/git-cli/providers/staging.js.map +1 -1
- package/dist/plus/git-github/providers/github/graph.d.ts +1 -3
- package/dist/plus/git-github/providers/github/graph.d.ts.map +1 -1
- package/dist/plus/git-github/providers/github/graph.js +8 -10
- package/dist/plus/git-github/providers/github/graph.js.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts +38 -48
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +22 -5
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
- package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts +39 -0
- package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts.map +1 -0
- package/dist/plus/integrations/authentication/rejectedTokenTracker.js +53 -0
- package/dist/plus/integrations/authentication/rejectedTokenTracker.js.map +1 -0
- 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 +57 -94
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +20 -0
- package/dist/plus/integrations/models/integration.d.ts.map +1 -1
- package/dist/plus/integrations/models/integration.js +61 -4
- package/dist/plus/integrations/models/integration.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 +10 -52
- 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/dist/utils/date.d.ts +1 -1
- package/dist/utils/date.d.ts.map +1 -1
- package/dist/utils/date.js +5 -3
- package/dist/utils/date.js.map +1 -1
- package/package.json +1 -1
- package/src/git/models/graph.ts +59 -41
- package/src/git/models/graphSession.ts +3 -106
- package/src/git/providers/graph.ts +1 -15
- package/src/git/providers/pausedOperations.ts +8 -1
- package/src/git/utils/reachability.utils.ts +1 -1
- package/src/git-cli/providers/graph.ts +37 -250
- package/src/git-cli/providers/pausedOperations.ts +29 -2
- package/src/git-cli/providers/staging.ts +8 -1
- package/src/plus/git-github/providers/github/graph.ts +8 -15
- package/src/plus/integrations/authentication/integrationAuthenticationProvider.ts +62 -40
- package/src/plus/integrations/authentication/rejectedTokenTracker.ts +56 -0
- package/src/plus/integrations/errors.ts +6 -2
- package/src/plus/integrations/integrationService.ts +27 -801
- package/src/plus/integrations/models/gitHostIntegration.ts +90 -99
- package/src/plus/integrations/models/integration.ts +68 -5
- package/src/plus/integrations/models/issuesIntegration.ts +10 -57
- 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
- package/src/utils/date.ts +6 -3
|
@@ -104,6 +104,21 @@ export type SearchMyPullRequestsOptions = {
|
|
|
104
104
|
includeReviewRequested?: boolean;
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Rejects a read the provider can't serve as asked: logs the reason and returns it as the `{ error }` half of an
|
|
109
|
+
* {@link IntegrationResult}, timed from `start`.
|
|
110
|
+
*
|
|
111
|
+
* A single builder because these refusals are a contract, not incidental returns — the read cores below reject on
|
|
112
|
+
* unsupported input, an inexpressible filter set, or an unresolvable filter account, and each one must reach the
|
|
113
|
+
* caller as an error it can surface as a warning rather than as an empty success. Writing the message once also
|
|
114
|
+
* keeps the logged text and the thrown text from drifting apart, which is exactly what happened while each site
|
|
115
|
+
* spelled its message out twice.
|
|
116
|
+
*/
|
|
117
|
+
function unsupportedRead<T>(message: string, start: number, logContext?: string): IntegrationResult<T> {
|
|
118
|
+
Logger.warn(message, logContext);
|
|
119
|
+
return { error: new Error(message), duration: performance.now() - start };
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
export abstract class GitHostIntegration<
|
|
108
123
|
ID extends IntegrationIds = IntegrationIds,
|
|
109
124
|
T extends ResourceDescriptor = ResourceDescriptor,
|
|
@@ -289,15 +304,6 @@ export abstract class GitHostIntegration<
|
|
|
289
304
|
cancellation?: AbortSignal,
|
|
290
305
|
): Promise<RepositoryMetadata | undefined>;
|
|
291
306
|
|
|
292
|
-
/**
|
|
293
|
-
* Lists the organizations (orgs/workspaces/groups) the current user belongs to on this host.
|
|
294
|
-
* `truncated === true` means the defensive page-drain backstop stopped before the upstream listing
|
|
295
|
-
* was exhausted.
|
|
296
|
-
*/
|
|
297
|
-
async getOrganizationsForUser(): Promise<ProviderHierarchyResult<ProviderOrganization> | undefined> {
|
|
298
|
-
return (await this.getOrganizationsForUserResult())?.value;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
307
|
/**
|
|
302
308
|
* Whether this git host implements generic org discovery. False for providers that register no
|
|
303
309
|
* {@link getProviderOrganizationsForUser} hook (e.g. Bitbucket Data Center) — the facade uses this to
|
|
@@ -344,9 +350,13 @@ export abstract class GitHostIntegration<
|
|
|
344
350
|
}
|
|
345
351
|
|
|
346
352
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
353
|
+
* Lists the organizations (orgs/workspaces/groups) the current user belongs to on this host.
|
|
354
|
+
* `truncated === true` means the defensive page-drain backstop stopped before the upstream listing was
|
|
355
|
+
* exhausted.
|
|
356
|
+
*
|
|
357
|
+
* Resolves the session for `connectionId` (or the primary connection when omitted, honoring multi-account
|
|
358
|
+
* reads) and recovers a thrown error into `{ error }` so callers can surface it as a warning instead of
|
|
359
|
+
* swallowing it to `undefined`.
|
|
350
360
|
*/
|
|
351
361
|
@trace()
|
|
352
362
|
async getOrganizationsForUserResult(
|
|
@@ -363,7 +373,10 @@ export abstract class GitHostIntegration<
|
|
|
363
373
|
this.resetRequestExceptionCount('getOrganizationsForUser');
|
|
364
374
|
return { value: result, duration: performance.now() - start };
|
|
365
375
|
} catch (ex) {
|
|
366
|
-
this.handleProviderException('getOrganizationsForUser', ex, {
|
|
376
|
+
this.handleProviderException('getOrganizationsForUser', ex, {
|
|
377
|
+
scope: scope,
|
|
378
|
+
connectionId: connectionId,
|
|
379
|
+
});
|
|
367
380
|
return { error: toError(ex), duration: performance.now() - start };
|
|
368
381
|
}
|
|
369
382
|
}
|
|
@@ -399,7 +412,7 @@ export abstract class GitHostIntegration<
|
|
|
399
412
|
this.resetRequestExceptionCount('getProjectsForOrg');
|
|
400
413
|
return { value: result, duration: performance.now() - start };
|
|
401
414
|
} catch (ex) {
|
|
402
|
-
this.handleProviderException('getProjectsForOrg', ex, { scope: scope });
|
|
415
|
+
this.handleProviderException('getProjectsForOrg', ex, { scope: scope, connectionId: connectionId });
|
|
403
416
|
return { error: toError(ex), duration: performance.now() - start };
|
|
404
417
|
}
|
|
405
418
|
}
|
|
@@ -416,17 +429,9 @@ export abstract class GitHostIntegration<
|
|
|
416
429
|
* every other host ignores it. (Azure without a project can't page its cross-project merge, so it
|
|
417
430
|
* returns all matches in a single page.) `truncated === true` means the defensive page-drain
|
|
418
431
|
* backstop stopped before the upstream listing was exhausted.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
options?: { project?: string; cursor?: string },
|
|
423
|
-
): Promise<ProviderHierarchyResult<ProviderRepository> | undefined> {
|
|
424
|
-
return (await this.getRepositoriesForOrgResult(org, options))?.value;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Result-returning core of {@link getRepositoriesForOrg}. Resolves the session for `connectionId`
|
|
429
|
-
* (or the primary connection when omitted) and recovers a thrown error into `{ error }` for warnings.
|
|
432
|
+
*
|
|
433
|
+
* Resolves the session for `connectionId` (or the primary connection when omitted) and recovers a thrown
|
|
434
|
+
* error into `{ error }`, so a caller surfaces it as a warning instead of a silent `undefined`.
|
|
430
435
|
*/
|
|
431
436
|
@trace()
|
|
432
437
|
async getRepositoriesForOrgResult(
|
|
@@ -444,7 +449,10 @@ export abstract class GitHostIntegration<
|
|
|
444
449
|
this.resetRequestExceptionCount('getRepositoriesForOrg');
|
|
445
450
|
return { value: result, duration: performance.now() - start };
|
|
446
451
|
} catch (ex) {
|
|
447
|
-
this.handleProviderException('getRepositoriesForOrg', ex, {
|
|
452
|
+
this.handleProviderException('getRepositoriesForOrg', ex, {
|
|
453
|
+
scope: scope,
|
|
454
|
+
connectionId: options?.connectionId,
|
|
455
|
+
});
|
|
448
456
|
return { error: toError(ex), duration: performance.now() - start };
|
|
449
457
|
}
|
|
450
458
|
}
|
|
@@ -482,7 +490,10 @@ export abstract class GitHostIntegration<
|
|
|
482
490
|
this.resetRequestExceptionCount('getRepositoriesForUser');
|
|
483
491
|
return { value: result, duration: performance.now() - start };
|
|
484
492
|
} catch (ex) {
|
|
485
|
-
this.handleProviderException('getRepositoriesForUser', ex, {
|
|
493
|
+
this.handleProviderException('getRepositoriesForUser', ex, {
|
|
494
|
+
scope: scope,
|
|
495
|
+
connectionId: options?.connectionId,
|
|
496
|
+
});
|
|
486
497
|
return { error: toError(ex), duration: performance.now() - start };
|
|
487
498
|
}
|
|
488
499
|
}
|
|
@@ -706,11 +717,7 @@ export abstract class GitHostIntegration<
|
|
|
706
717
|
(isAzureDevOpsProvider(providerId) &&
|
|
707
718
|
!reposOrRepoIds.every(repo => repo.project != null && repo.namespace != null)))
|
|
708
719
|
) {
|
|
709
|
-
|
|
710
|
-
return {
|
|
711
|
-
error: new Error(`Unsupported input for provider ${providerId}`),
|
|
712
|
-
duration: performance.now() - start,
|
|
713
|
-
};
|
|
720
|
+
return unsupportedRead(`Unsupported input for provider ${providerId}`, start, 'getIssuesForRepos');
|
|
714
721
|
}
|
|
715
722
|
|
|
716
723
|
let getIssuesOptions: GetIssuesOptions | undefined;
|
|
@@ -723,28 +730,24 @@ export abstract class GitHostIntegration<
|
|
|
723
730
|
}
|
|
724
731
|
|
|
725
732
|
if (organizations.size > 1) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
733
|
+
return unsupportedRead(
|
|
734
|
+
`Multiple organizations not supported for provider ${providerId}`,
|
|
735
|
+
start,
|
|
736
|
+
'getIssuesForRepos',
|
|
737
|
+
);
|
|
731
738
|
} else if (organizations.size === 0) {
|
|
732
|
-
|
|
733
|
-
return {
|
|
734
|
-
error: new Error(`No organizations found for provider ${providerId}`),
|
|
735
|
-
duration: performance.now() - start,
|
|
736
|
-
};
|
|
739
|
+
return unsupportedRead(`No organizations found for provider ${providerId}`, start, 'getIssuesForRepos');
|
|
737
740
|
}
|
|
738
741
|
|
|
739
742
|
const organization: string = first(organizations.values())!;
|
|
740
743
|
|
|
741
744
|
if (options?.filters != null) {
|
|
742
745
|
if (!api.providerSupportsIssueFilters(providerId, options.filters)) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
746
|
+
return unsupportedRead(
|
|
747
|
+
`Unsupported filters for provider ${providerId}`,
|
|
748
|
+
start,
|
|
749
|
+
'getIssuesForRepos',
|
|
750
|
+
);
|
|
748
751
|
}
|
|
749
752
|
|
|
750
753
|
let userAccount: ProviderAccount | undefined;
|
|
@@ -756,21 +759,17 @@ export abstract class GitHostIntegration<
|
|
|
756
759
|
}
|
|
757
760
|
|
|
758
761
|
if (userAccount == null) {
|
|
759
|
-
|
|
760
|
-
return {
|
|
761
|
-
error: new Error(`Unable to get current user for ${providerId}`),
|
|
762
|
-
duration: performance.now() - start,
|
|
763
|
-
};
|
|
762
|
+
return unsupportedRead(`Unable to get current user for ${providerId}`, start, 'getIssuesForRepos');
|
|
764
763
|
}
|
|
765
764
|
|
|
766
765
|
const userFilterProperty = userAccount.name;
|
|
767
766
|
|
|
768
767
|
if (userFilterProperty == null) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
768
|
+
return unsupportedRead(
|
|
769
|
+
`Unable to get user property for filter for ${providerId}`,
|
|
770
|
+
start,
|
|
771
|
+
'getIssuesForRepos',
|
|
772
|
+
);
|
|
774
773
|
}
|
|
775
774
|
|
|
776
775
|
getIssuesOptions = {
|
|
@@ -889,11 +888,7 @@ export abstract class GitHostIntegration<
|
|
|
889
888
|
// branch above applies. Without it an unsupported filter (e.g. GitLab has no Mention endpoint) would
|
|
890
889
|
// resolve to no filter property being set and silently degrade to an unfiltered, project-wide read.
|
|
891
890
|
if (!api.providerSupportsIssueFilters(providerId, options.filters)) {
|
|
892
|
-
|
|
893
|
-
return {
|
|
894
|
-
error: new Error(`Unsupported filters for provider ${providerId}`),
|
|
895
|
-
duration: performance.now() - start,
|
|
896
|
-
};
|
|
891
|
+
return unsupportedRead(`Unsupported filters for provider ${providerId}`, start, 'getIssuesForRepos');
|
|
897
892
|
}
|
|
898
893
|
|
|
899
894
|
let userAccount: ProviderAccount | undefined;
|
|
@@ -905,20 +900,16 @@ export abstract class GitHostIntegration<
|
|
|
905
900
|
}
|
|
906
901
|
|
|
907
902
|
if (userAccount == null) {
|
|
908
|
-
|
|
909
|
-
return {
|
|
910
|
-
error: new Error(`Unable to get current user for ${providerId}`),
|
|
911
|
-
duration: performance.now() - start,
|
|
912
|
-
};
|
|
903
|
+
return unsupportedRead(`Unable to get current user for ${providerId}`, start, 'getIssuesForRepos');
|
|
913
904
|
}
|
|
914
905
|
|
|
915
906
|
const userFilterProperty = userAccount.username;
|
|
916
907
|
if (userFilterProperty == null) {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
908
|
+
return unsupportedRead(
|
|
909
|
+
`Unable to get user property for filter for ${providerId}`,
|
|
910
|
+
start,
|
|
911
|
+
'getIssuesForRepos',
|
|
912
|
+
);
|
|
922
913
|
}
|
|
923
914
|
|
|
924
915
|
getIssuesOptions = {
|
|
@@ -1124,21 +1115,17 @@ export abstract class GitHostIntegration<
|
|
|
1124
1115
|
(isAzureDevOpsProvider(providerId) &&
|
|
1125
1116
|
!reposOrRepoIds.every(repo => repo.project != null && repo.namespace != null)))
|
|
1126
1117
|
) {
|
|
1127
|
-
|
|
1128
|
-
return {
|
|
1129
|
-
error: new Error(`Unsupported input for provider ${providerId}`),
|
|
1130
|
-
duration: performance.now() - start,
|
|
1131
|
-
};
|
|
1118
|
+
return unsupportedRead(`Unsupported input for provider ${providerId}`, start);
|
|
1132
1119
|
}
|
|
1133
1120
|
|
|
1134
1121
|
let getPullRequestsOptions: GetPullRequestsOptions | undefined;
|
|
1135
1122
|
if (options?.filters != null) {
|
|
1136
1123
|
if (!api.providerSupportsPullRequestFilters(providerId, options.filters)) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1124
|
+
return unsupportedRead(
|
|
1125
|
+
`Unsupported filters for provider ${providerId}`,
|
|
1126
|
+
start,
|
|
1127
|
+
'getPullRequestsForRepos',
|
|
1128
|
+
);
|
|
1142
1129
|
}
|
|
1143
1130
|
|
|
1144
1131
|
let userAccount: ProviderAccount | undefined;
|
|
@@ -1158,11 +1145,11 @@ export abstract class GitHostIntegration<
|
|
|
1158
1145
|
duration: performance.now() - start,
|
|
1159
1146
|
};
|
|
1160
1147
|
} else if (organizations.size === 0) {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1148
|
+
return unsupportedRead(
|
|
1149
|
+
`No organizations found for provider ${providerId}`,
|
|
1150
|
+
start,
|
|
1151
|
+
'getPullRequestsForRepos',
|
|
1152
|
+
);
|
|
1166
1153
|
}
|
|
1167
1154
|
|
|
1168
1155
|
const organization: string = first(organizations.values())!;
|
|
@@ -1182,11 +1169,11 @@ export abstract class GitHostIntegration<
|
|
|
1182
1169
|
}
|
|
1183
1170
|
|
|
1184
1171
|
if (userAccount == null) {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1172
|
+
return unsupportedRead(
|
|
1173
|
+
`Unable to get current user for ${providerId}`,
|
|
1174
|
+
start,
|
|
1175
|
+
'getPullRequestsForRepos',
|
|
1176
|
+
);
|
|
1190
1177
|
}
|
|
1191
1178
|
|
|
1192
1179
|
let userFilterProperty: string | null;
|
|
@@ -1202,11 +1189,11 @@ export abstract class GitHostIntegration<
|
|
|
1202
1189
|
}
|
|
1203
1190
|
|
|
1204
1191
|
if (userFilterProperty == null) {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1192
|
+
return unsupportedRead(
|
|
1193
|
+
`Unable to get user property for filter for ${providerId}`,
|
|
1194
|
+
start,
|
|
1195
|
+
'getPullRequestsForRepos',
|
|
1196
|
+
);
|
|
1210
1197
|
}
|
|
1211
1198
|
|
|
1212
1199
|
// Route the "review requested from me" filter to the field each provider actually reads:
|
|
@@ -1436,6 +1423,7 @@ export abstract class GitHostIntegration<
|
|
|
1436
1423
|
this.handleProviderException('searchMyPullRequests', ex, {
|
|
1437
1424
|
scope: scope,
|
|
1438
1425
|
silent: true,
|
|
1426
|
+
connectionId: connectionId,
|
|
1439
1427
|
});
|
|
1440
1428
|
return {
|
|
1441
1429
|
error: toError(ex),
|
|
@@ -1478,7 +1466,10 @@ export abstract class GitHostIntegration<
|
|
|
1478
1466
|
this.resetRequestExceptionCount('getMyPullRequestsForUser');
|
|
1479
1467
|
return { value: result, duration: performance.now() - start };
|
|
1480
1468
|
} catch (ex) {
|
|
1481
|
-
this.handleProviderException('getMyPullRequestsForUser', ex, {
|
|
1469
|
+
this.handleProviderException('getMyPullRequestsForUser', ex, {
|
|
1470
|
+
scope: scope,
|
|
1471
|
+
connectionId: connectionId,
|
|
1472
|
+
});
|
|
1482
1473
|
return { error: toError(ex), duration: performance.now() - start };
|
|
1483
1474
|
}
|
|
1484
1475
|
}
|
|
@@ -1610,7 +1601,7 @@ export abstract class GitHostIntegration<
|
|
|
1610
1601
|
this.resetRequestExceptionCount('searchPullRequests');
|
|
1611
1602
|
return prs;
|
|
1612
1603
|
} catch (ex) {
|
|
1613
|
-
this.handleProviderException('searchPullRequests', ex, { scope: scope });
|
|
1604
|
+
this.handleProviderException('searchPullRequests', ex, { scope: scope, connectionId: connectionId });
|
|
1614
1605
|
return undefined;
|
|
1615
1606
|
}
|
|
1616
1607
|
}
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from '../authentication/integrationAuthenticationProvider.js';
|
|
21
21
|
import type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';
|
|
22
22
|
import type { ProviderAuthenticationSession } from '../authentication/models.js';
|
|
23
|
+
import { RejectedTokenTracker } from '../authentication/rejectedTokenTracker.js';
|
|
23
24
|
import type { IntegrationIds, IssuesCloudHostIntegrationId } from '../constants.js';
|
|
24
25
|
import { GitCloudHostIntegrationId } from '../constants.js';
|
|
25
26
|
import type { IntegrationServiceContext } from '../context.js';
|
|
@@ -240,13 +241,18 @@ export abstract class IntegrationBase<
|
|
|
240
241
|
// A truthy connectionId targets a specific account; an empty string is not a real target, so it falls
|
|
241
242
|
// through to the primary path below.
|
|
242
243
|
if (connectionId) {
|
|
244
|
+
// A read of this connection previously failed with an AuthenticationError, so the token in
|
|
245
|
+
// storage is known-refused. Ask for a refresh through the GK cloud before reading again, which
|
|
246
|
+
// is this branch's equivalent of the primary path's `refreshSessionIfExpired`. Claimed here so
|
|
247
|
+
// the forced refresh runs at most once per rejection.
|
|
248
|
+
const refreshRejectedToken = this._rejectedTokens.claimRefresh(connectionId);
|
|
243
249
|
// Degrade to "no results" on failure, matching the primary path (whose ensureSession/
|
|
244
250
|
// refreshSessionIfExpired swallow errors) so read methods keep their never-throws contract.
|
|
245
251
|
try {
|
|
246
252
|
const authProvider = await this.authenticationService.get(this.authProvider.id);
|
|
247
253
|
const session = await authProvider.getSession(
|
|
248
254
|
{ ...this.authProviderDescriptor, connectionId: connectionId, cloud: true },
|
|
249
|
-
{ source: source },
|
|
255
|
+
{ source: source, refreshRejectedToken: refreshRejectedToken },
|
|
250
256
|
);
|
|
251
257
|
return session != null && this.isSessionForIntegrationHost(session) ? session : undefined;
|
|
252
258
|
} catch (ex) {
|
|
@@ -326,6 +332,10 @@ export abstract class IntegrationBase<
|
|
|
326
332
|
|
|
327
333
|
@debug()
|
|
328
334
|
async reauthenticate(): Promise<void> {
|
|
335
|
+
// `forceNewSession` below deletes the stored secrets to reconnect. Ahead of the guard — see
|
|
336
|
+
// `onStoredTokensReplaced`.
|
|
337
|
+
this.onStoredTokensReplaced();
|
|
338
|
+
|
|
329
339
|
if (this._session === undefined) return;
|
|
330
340
|
|
|
331
341
|
this._session = undefined;
|
|
@@ -343,6 +353,30 @@ export abstract class IntegrationBase<
|
|
|
343
353
|
requestSessionSyncForUsecase(syncReqUsecase: SyncReqUsecase): void {
|
|
344
354
|
this._syncRequestsPerFailedUsecase.add(syncReqUsecase);
|
|
345
355
|
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The per-connection counterpart of the primary session's expire-and-resync recovery: a token the
|
|
359
|
+
* provider refused is refreshed once through the GK cloud, which exchanges the refresh token server-side
|
|
360
|
+
* (the client never holds one). Armed by {@link handleProviderException} and consumed by
|
|
361
|
+
* {@link resolveReadSession}; a rejection it declines belongs to {@link trackRequestException} instead.
|
|
362
|
+
*/
|
|
363
|
+
private readonly _rejectedTokens = new RejectedTokenTracker();
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Called by every path that replaces or removes the stored tokens — a disconnect, a forced re-sync, a
|
|
367
|
+
* reauthentication, or the connection set changing. A rejection names a specific credential, so once that
|
|
368
|
+
* credential is gone the rejection describes nothing and must not force a refresh (or, for a deleted
|
|
369
|
+
* connection, outlive it).
|
|
370
|
+
*
|
|
371
|
+
* Deliberately NOT conditioned on `_session`. A per-connection read resolves through the auth provider and
|
|
372
|
+
* never populates the cached primary session, so the connections this recovery exists for are exactly the
|
|
373
|
+
* ones with no `_session` to inspect — the same blind spot the recovery itself was added to fix. Callers
|
|
374
|
+
* that guard on `_session` therefore invoke this ahead of that guard.
|
|
375
|
+
*/
|
|
376
|
+
protected onStoredTokensReplaced(): void {
|
|
377
|
+
this._rejectedTokens.clear();
|
|
378
|
+
}
|
|
379
|
+
|
|
346
380
|
private static readonly requestExceptionLimit = 5;
|
|
347
381
|
private requestExceptionCount = 0;
|
|
348
382
|
|
|
@@ -350,6 +384,8 @@ export abstract class IntegrationBase<
|
|
|
350
384
|
this.requestExceptionCount = 0;
|
|
351
385
|
if (syncReqUsecase === 'all') {
|
|
352
386
|
this._syncRequestsPerFailedUsecase.clear();
|
|
387
|
+
// 'all' is the whole-integration reset: a disconnect, or a re-sync that produced a new access token.
|
|
388
|
+
this.onStoredTokensReplaced();
|
|
353
389
|
} else {
|
|
354
390
|
this._syncRequestsPerFailedUsecase.delete(syncReqUsecase);
|
|
355
391
|
}
|
|
@@ -374,6 +410,10 @@ export abstract class IntegrationBase<
|
|
|
374
410
|
* `deleteConnection`). Unlike {@link reset}/{@link disconnect}, it deletes nothing from storage.
|
|
375
411
|
*/
|
|
376
412
|
switchConnection(): void {
|
|
413
|
+
// A connection was deleted, or a different one became primary. Ahead of the guard — see
|
|
414
|
+
// `onStoredTokensReplaced`.
|
|
415
|
+
this.onStoredTokensReplaced();
|
|
416
|
+
|
|
377
417
|
if (this._session === undefined) return;
|
|
378
418
|
|
|
379
419
|
const wasConnected = this._session != null;
|
|
@@ -419,6 +459,9 @@ export abstract class IntegrationBase<
|
|
|
419
459
|
// Reset our stored session so that we get a new one from the cloud
|
|
420
460
|
const authProvider = await this.authenticationService.get(this.authProvider.id);
|
|
421
461
|
await authProvider.deleteSession(this.authProviderDescriptor);
|
|
462
|
+
// The stored token was just deleted. Not left to the token-changed check below, which needs
|
|
463
|
+
// an `oldSession` — see `onStoredTokensReplaced`.
|
|
464
|
+
this.onStoredTokensReplaced();
|
|
422
465
|
// Reset the session and clear our "stay disconnected" flag
|
|
423
466
|
this._session = undefined;
|
|
424
467
|
await this.ctx.storage.deleteWorkspace(this.connectedKey);
|
|
@@ -474,12 +517,29 @@ export abstract class IntegrationBase<
|
|
|
474
517
|
protected handleProviderException(
|
|
475
518
|
syncReqUsecase: SyncReqUsecase,
|
|
476
519
|
ex: Error,
|
|
477
|
-
options?: { scope?: ScopedLogger | undefined; silent?: boolean },
|
|
520
|
+
options?: { scope?: ScopedLogger | undefined; silent?: boolean; connectionId?: string },
|
|
478
521
|
): void {
|
|
479
522
|
if (isCancellationError(ex)) return;
|
|
480
523
|
|
|
481
524
|
options?.scope?.error(ex);
|
|
482
525
|
|
|
526
|
+
// A per-connection (multi-account) read resolved its session through `resolveReadSession`'s
|
|
527
|
+
// `connectionId` branch, which deliberately never touches the cached primary `_session`. So the
|
|
528
|
+
// primary-session recovery below cannot apply to it: expiring `_session` would mark a session this
|
|
529
|
+
// read never used, while the rejected connection kept its stored token and re-sent it on every
|
|
530
|
+
// later read — a token the provider has already refused (expired scopes, a revoked grant, an
|
|
531
|
+
// uninstalled app) is not self-healing, so the read failed identically until the user reconnected
|
|
532
|
+
// by hand. Record the rejection against the connection instead; `resolveReadSession` consumes it
|
|
533
|
+
// and forces the cloud `/refresh` on the next read of that connection. When there is nothing to
|
|
534
|
+
// recover, fall through to the shared failure budget, which disconnects after
|
|
535
|
+
// `requestExceptionLimit` and surfaces the reconnect prompt.
|
|
536
|
+
if (ex instanceof AuthenticationError && options?.connectionId) {
|
|
537
|
+
if (!this._rejectedTokens.recordRejection(options.connectionId)) {
|
|
538
|
+
this.trackRequestException(options);
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
483
543
|
if (ex instanceof AuthenticationError && this._session?.cloud) {
|
|
484
544
|
if (!this.hasSessionSyncRequests()) {
|
|
485
545
|
this.requestSessionSyncForUsecase(syncReqUsecase);
|
|
@@ -686,7 +746,7 @@ export abstract class IntegrationBase<
|
|
|
686
746
|
this.resetRequestExceptionCount('searchMyIssues');
|
|
687
747
|
return { value: issues };
|
|
688
748
|
} catch (ex) {
|
|
689
|
-
this.handleProviderException('searchMyIssues', ex, { scope: scope });
|
|
749
|
+
this.handleProviderException('searchMyIssues', ex, { scope: scope, connectionId: connectionId });
|
|
690
750
|
return { error: toError(ex) };
|
|
691
751
|
}
|
|
692
752
|
}
|
|
@@ -738,7 +798,7 @@ export abstract class IntegrationBase<
|
|
|
738
798
|
this.resetRequestExceptionCount('searchMyIssues');
|
|
739
799
|
return { value: result };
|
|
740
800
|
} catch (ex) {
|
|
741
|
-
this.handleProviderException('searchMyIssues', ex, { scope: scope });
|
|
801
|
+
this.handleProviderException('searchMyIssues', ex, { scope: scope, connectionId: connectionId });
|
|
742
802
|
return { error: toError(ex) };
|
|
743
803
|
}
|
|
744
804
|
}
|
|
@@ -858,7 +918,10 @@ export abstract class IntegrationBase<
|
|
|
858
918
|
return undefined;
|
|
859
919
|
}
|
|
860
920
|
|
|
861
|
-
this.handleProviderException('getCurrentAccount', ex, {
|
|
921
|
+
this.handleProviderException('getCurrentAccount', ex, {
|
|
922
|
+
scope: scope,
|
|
923
|
+
connectionId: connectionId,
|
|
924
|
+
});
|
|
862
925
|
|
|
863
926
|
// Invalidate the cache on error, except for auth errors
|
|
864
927
|
if (!(ex instanceof AuthenticationError)) {
|
|
@@ -48,7 +48,7 @@ export abstract class IssuesIntegration<
|
|
|
48
48
|
this.resetRequestExceptionCount('getAccountForResource');
|
|
49
49
|
return { value: account };
|
|
50
50
|
} catch (ex) {
|
|
51
|
-
this.handleProviderException('getAccountForResource', ex);
|
|
51
|
+
this.handleProviderException('getAccountForResource', ex, { connectionId: connectionId });
|
|
52
52
|
return { error: toError(ex) };
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -80,38 +80,20 @@ export abstract class IssuesIntegration<
|
|
|
80
80
|
this.resetRequestExceptionCount('getResourcesForUser');
|
|
81
81
|
return { value: resources };
|
|
82
82
|
} catch (ex) {
|
|
83
|
-
this.handleProviderException('getResourcesForUser', ex);
|
|
83
|
+
this.handleProviderException('getResourcesForUser', ex, { connectionId: connectionId });
|
|
84
84
|
return { error: toError(ex) };
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
protected abstract getProviderResourcesForUser(session: ProviderAuthenticationSession): Promise<T[] | undefined>;
|
|
89
89
|
|
|
90
|
-
@trace()
|
|
91
|
-
async getProjectsForResources(resources: T[], connectionId?: string): Promise<T[] | undefined> {
|
|
92
|
-
return (await this.getProjectsForResourcesResult(resources, connectionId))?.value;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Result-returning core of {@link getProjectsForResources}. Recovers thrown errors into `{ error }` so callers
|
|
97
|
-
* can surface them as warnings rather than silently swallowing them to `undefined`. Implemented by unwrapping
|
|
98
|
-
* the metadata-aware path's `values` so the array-returning contract stays backward compatible.
|
|
99
|
-
*/
|
|
100
|
-
async getProjectsForResourcesResult(
|
|
101
|
-
resources: T[],
|
|
102
|
-
connectionId?: string,
|
|
103
|
-
): Promise<IntegrationResult<T[] | undefined>> {
|
|
104
|
-
const result = await this.getProjectsForResourcesWithMetadataResult(resources, connectionId);
|
|
105
|
-
if (result == null) return undefined;
|
|
106
|
-
if (result.error != null) return { value: result.value?.values, error: result.error };
|
|
107
|
-
return { value: result.value?.values };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
90
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
91
|
+
* Project discovery for a set of resources, returning the SDK collection `{ values, metadata }` (completeness
|
|
92
|
+
* + per-resource failures) so callers can warn on failed resources and set `fetchFailed` without discarding
|
|
93
|
+
* the resources that succeeded. Thrown errors are recovered into `{ error }` for the same reason: a caller
|
|
94
|
+
* surfaces them as a warning rather than swallowing them to `undefined`.
|
|
114
95
|
*/
|
|
96
|
+
@trace()
|
|
115
97
|
async getProjectsForResourcesWithMetadataResult(
|
|
116
98
|
resources: T[],
|
|
117
99
|
connectionId?: string,
|
|
@@ -126,40 +108,11 @@ export abstract class IssuesIntegration<
|
|
|
126
108
|
this.resetRequestExceptionCount('getProjectsForResources');
|
|
127
109
|
return { value: projects };
|
|
128
110
|
} catch (ex) {
|
|
129
|
-
this.handleProviderException('getProjectsForResources', ex);
|
|
111
|
+
this.handleProviderException('getProjectsForResources', ex, { connectionId: connectionId });
|
|
130
112
|
return { error: toError(ex) };
|
|
131
113
|
}
|
|
132
114
|
}
|
|
133
115
|
|
|
134
|
-
async getProjectsForUser(connectionId?: string): Promise<T[] | undefined> {
|
|
135
|
-
return (await this.getProjectsForUserResult(connectionId))?.value;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Result-returning core of {@link getProjectsForUser}. Composes the resource and project result methods so
|
|
140
|
-
* callers can surface per-step warnings. Unwraps the metadata-aware path for the array-returning contract.
|
|
141
|
-
*/
|
|
142
|
-
async getProjectsForUserResult(connectionId?: string): Promise<IntegrationResult<T[] | undefined>> {
|
|
143
|
-
const result = await this.getProjectsForUserWithMetadataResult(connectionId);
|
|
144
|
-
if (result == null) return undefined;
|
|
145
|
-
if (result.error != null) return { value: result.value?.values, error: result.error };
|
|
146
|
-
return { value: result.value?.values };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Metadata-aware counterpart of {@link getProjectsForUserResult}: composes resource discovery with the
|
|
151
|
-
* metadata-aware project read so ProviderBackend consumers get completeness/failures across resources.
|
|
152
|
-
*/
|
|
153
|
-
async getProjectsForUserWithMetadataResult(
|
|
154
|
-
connectionId?: string,
|
|
155
|
-
): Promise<IntegrationResult<ProviderApiCollectionResult<T> | undefined>> {
|
|
156
|
-
const resources = await this.getResourcesForUserResult(connectionId);
|
|
157
|
-
if (resources?.error != null) return { error: resources.error };
|
|
158
|
-
if (resources?.value == null) return undefined;
|
|
159
|
-
|
|
160
|
-
return this.getProjectsForResourcesWithMetadataResult(resources.value, connectionId);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
116
|
/**
|
|
164
117
|
* Metadata-aware provider project discovery. The default wraps the array-returning
|
|
165
118
|
* {@link getProviderProjectsForResources} in `{ values }` with no metadata, so providers without a
|
|
@@ -208,7 +161,7 @@ export abstract class IssuesIntegration<
|
|
|
208
161
|
this.resetRequestExceptionCount('getIssuesForProject');
|
|
209
162
|
return { value: issues };
|
|
210
163
|
} catch (ex) {
|
|
211
|
-
this.handleProviderException('getIssuesForProject', ex);
|
|
164
|
+
this.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });
|
|
212
165
|
return { error: toError(ex) };
|
|
213
166
|
}
|
|
214
167
|
}
|
|
@@ -235,7 +188,7 @@ export abstract class IssuesIntegration<
|
|
|
235
188
|
this.resetRequestExceptionCount('getIssuesForProject');
|
|
236
189
|
return { value: result };
|
|
237
190
|
} catch (ex) {
|
|
238
|
-
this.handleProviderException('getIssuesForProject', ex);
|
|
191
|
+
this.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });
|
|
239
192
|
return { error: toError(ex) };
|
|
240
193
|
}
|
|
241
194
|
}
|
|
@@ -1062,7 +1062,7 @@ export abstract class AzureDevOpsIntegrationBase<
|
|
|
1062
1062
|
{ providerId: this.id, resourceId: p.resourceId, projectId: p.name },
|
|
1063
1063
|
);
|
|
1064
1064
|
return {
|
|
1065
|
-
issues: result.values.map(i => fromProviderIssue(i, this
|
|
1065
|
+
issues: result.values.map(i => fromProviderIssue(i, this, { project: p })),
|
|
1066
1066
|
// Azure work-item ids are organization-scoped. Include both org and project so the
|
|
1067
1067
|
// assigned/authored passes dedupe the same item without collapsing another org's item.
|
|
1068
1068
|
projectKey: `${p.resourceId}:${p.id}`,
|