@gitkraken/core-gitlens 0.4.101 → 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 +29 -6
- 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/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/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/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/reachability.utils.d.ts +1 -1
- package/dist/git/utils/reachability.utils.js +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/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/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/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/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/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +19 -6
- 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 +64 -6
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.js +5 -5
- package/dist/plus/integrations/models/issuesIntegration.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/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 +2 -2
- package/src/git/models/graph.ts +59 -41
- package/src/git/models/graphSession.ts +3 -106
- package/src/git/models/issue.ts +3 -2
- package/src/git/providers/graph.ts +1 -15
- package/src/git/providers/pausedOperations.ts +8 -1
- package/src/git/utils/issue.utils.ts +9 -6
- package/src/git/utils/reachability.utils.ts +1 -1
- package/src/git/utils/remote.utils.ts +6 -0
- 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/api/github.ts +19 -9
- package/src/plus/git-github/models.ts +27 -31
- 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/collectionMetadata.ts +97 -7
- package/src/plus/integrations/integrationService.ts +37 -9
- package/src/plus/integrations/models/gitHostIntegration.ts +19 -6
- package/src/plus/integrations/models/integration.ts +71 -7
- package/src/plus/integrations/models/issuesIntegration.ts +5 -5
- 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
- package/src/utils/date.ts +6 -3
|
@@ -509,7 +509,10 @@ export class PausedOperationsGitSubProvider implements GitPausedOperationsSubPro
|
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
@debug()
|
|
512
|
-
async continuePausedOperation(
|
|
512
|
+
async continuePausedOperation(
|
|
513
|
+
repoPath: string,
|
|
514
|
+
options?: { skip?: boolean; messageEditor?: string },
|
|
515
|
+
): Promise<void> {
|
|
513
516
|
const scope = getScopedLogger();
|
|
514
517
|
|
|
515
518
|
const status = await this.getPausedOperationStatus(repoPath);
|
|
@@ -521,8 +524,32 @@ export class PausedOperationsGitSubProvider implements GitPausedOperationsSubPro
|
|
|
521
524
|
|
|
522
525
|
const args = [status.type, options?.skip ? '--skip' : '--continue'];
|
|
523
526
|
|
|
527
|
+
// Continuing after a conflicted step can open the commit-message editor (merge-backend rebases,
|
|
528
|
+
// cherry-picks, and reverts). `GIT_EDITOR` is what git honors for that step, so set it on the
|
|
529
|
+
// environment; also set `core.editor` config as a fallback.
|
|
530
|
+
const configs: string[] = [];
|
|
531
|
+
let env: Record<string, string | undefined> | undefined;
|
|
532
|
+
if (options?.messageEditor) {
|
|
533
|
+
configs.push('-c', `core.editor=${options.messageEditor}`);
|
|
534
|
+
env = { GIT_EDITOR: options.messageEditor };
|
|
535
|
+
}
|
|
536
|
+
|
|
524
537
|
try {
|
|
525
|
-
await this.git.run(
|
|
538
|
+
await this.git.run(
|
|
539
|
+
{
|
|
540
|
+
cwd: repoPath,
|
|
541
|
+
errors: 'throw',
|
|
542
|
+
configs: configs,
|
|
543
|
+
env: env,
|
|
544
|
+
// The runner dedups in-flight commands by args only — it ignores per-call `configs`/`env`.
|
|
545
|
+
// A headless continue (with `GIT_EDITOR`) and a plain `--continue` would therefore collapse
|
|
546
|
+
// into one execution, so the plain one could silently run headless (auto-accepting a commit
|
|
547
|
+
// message) or the headless one could ride a plain continue and open an editor that hangs the
|
|
548
|
+
// loop. Tag the headless variant so it can't share a pending command with the plain one.
|
|
549
|
+
correlationKey: options?.messageEditor ? 'paused-op-continue-headless' : undefined,
|
|
550
|
+
},
|
|
551
|
+
...args,
|
|
552
|
+
);
|
|
526
553
|
this.context.hooks?.cache?.onReset?.(repoPath, 'branches', 'status');
|
|
527
554
|
this.context.hooks?.repository?.onChanged?.(repoPath, ['head', 'heads', 'index']);
|
|
528
555
|
} catch (ex) {
|
|
@@ -58,7 +58,14 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
|
|
|
58
58
|
if (gitDir == null) throw new Error(`Unable to determine git directory for ${repoPath}`);
|
|
59
59
|
|
|
60
60
|
const currentIndex = joinPaths(gitDir.uri.fsPath, 'index');
|
|
61
|
-
|
|
61
|
+
try {
|
|
62
|
+
await fs.copyFile(currentIndex, tempIndex);
|
|
63
|
+
} catch (ex) {
|
|
64
|
+
// A repo that has never staged anything has no index file yet; git reads a missing
|
|
65
|
+
// `GIT_INDEX_FILE` as an empty index, which is what 'current' means there. Any other
|
|
66
|
+
// failure is real — don't silently hand back an index claiming everything is deleted.
|
|
67
|
+
if ((ex as NodeJS.ErrnoException)?.code !== 'ENOENT') throw ex;
|
|
68
|
+
}
|
|
62
69
|
break;
|
|
63
70
|
}
|
|
64
71
|
case 'ref': {
|
|
@@ -3638,7 +3638,7 @@ export class GitHubApi {
|
|
|
3638
3638
|
endCursor?: string | null;
|
|
3639
3639
|
hasNextPage: boolean;
|
|
3640
3640
|
};
|
|
3641
|
-
nodes: GitHubIssue[];
|
|
3641
|
+
nodes: (GitHubIssue | null)[] | null;
|
|
3642
3642
|
};
|
|
3643
3643
|
interface SearchResult {
|
|
3644
3644
|
authored?: SearchCategory;
|
|
@@ -3777,16 +3777,26 @@ export class GitHubApi {
|
|
|
3777
3777
|
cancellation,
|
|
3778
3778
|
);
|
|
3779
3779
|
|
|
3780
|
-
function toQueryResult(issue: GitHubIssue): IssueShape {
|
|
3781
|
-
return fromGitHubIssue(issue, provider);
|
|
3782
|
-
}
|
|
3783
|
-
|
|
3784
3780
|
if (rsp == null) return { values: [], hasMore: false, page: page, truncated: false };
|
|
3785
3781
|
|
|
3782
|
+
// Map node-by-node so one unmappable issue can't discard the whole result set
|
|
3783
|
+
const issues: IssueShape[] = [];
|
|
3784
|
+
for (const node of [
|
|
3785
|
+
...(rsp.assigned?.nodes ?? []),
|
|
3786
|
+
...(rsp.mentioned?.nodes ?? []),
|
|
3787
|
+
...(rsp.authored?.nodes ?? []),
|
|
3788
|
+
]) {
|
|
3789
|
+
if (node?.id == null) continue;
|
|
3790
|
+
|
|
3791
|
+
try {
|
|
3792
|
+
issues.push(fromGitHubIssue(node, provider));
|
|
3793
|
+
} catch (ex) {
|
|
3794
|
+
scope?.warn(`skipped unmappable issue; id=${node.id}, url=${node.url}, ex=${ex}`);
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3786
3798
|
const results: IterableIterator<IssueShape> = uniqueBy(
|
|
3787
|
-
|
|
3788
|
-
toQueryResult,
|
|
3789
|
-
),
|
|
3799
|
+
issues,
|
|
3790
3800
|
r => r.url,
|
|
3791
3801
|
(original, _current) => original,
|
|
3792
3802
|
);
|
|
@@ -3910,7 +3920,7 @@ export class GitHubApi {
|
|
|
3910
3920
|
scope,
|
|
3911
3921
|
cancellation,
|
|
3912
3922
|
);
|
|
3913
|
-
if (rsp == null) return
|
|
3923
|
+
if (rsp == null) return results;
|
|
3914
3924
|
|
|
3915
3925
|
const pageResults = filterPullRequestsBySearchState(
|
|
3916
3926
|
rsp.search.nodes.map(pr => fromGitHubPullRequest(pr, provider)),
|
|
@@ -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,
|
|
@@ -21,8 +21,6 @@ import type {
|
|
|
21
21
|
GitGraphSessionChangedChannels,
|
|
22
22
|
GitGraphSessionRefreshOptions,
|
|
23
23
|
GitGraphSessionRefreshResult,
|
|
24
|
-
GitGraphSessionSnapshot,
|
|
25
|
-
GraphSessionRestoreResult,
|
|
26
24
|
} from '../../../../git/models/graphSession.js';
|
|
27
25
|
import type { GitLog } from '../../../../git/models/log.js';
|
|
28
26
|
import type { GitRemote } from '../../../../git/models/remote.js';
|
|
@@ -52,14 +50,12 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
52
50
|
rev?: string;
|
|
53
51
|
limit?: number;
|
|
54
52
|
include?: { stats?: boolean };
|
|
55
|
-
restore?: GitGraphSessionSnapshot;
|
|
56
|
-
onRestore?: (result: GraphSessionRestoreResult) => void;
|
|
57
53
|
},
|
|
58
54
|
cancellation?: AbortSignal,
|
|
59
55
|
): Promise<GitGraphSession> {
|
|
60
56
|
// GitHub-backed graphs have no incremental machinery (and `getGraph` takes no row processor); the
|
|
61
|
-
// session is a thin window accumulator over full fetches. `rowProcessor`
|
|
62
|
-
//
|
|
57
|
+
// session is a thin window accumulator over full fetches. `rowProcessor` is accepted for interface
|
|
58
|
+
// parity and ignored — always a full walk.
|
|
63
59
|
const session = new GraphSession(this, repoPath);
|
|
64
60
|
await session.initialize(options, cancellation);
|
|
65
61
|
return session;
|
|
@@ -295,10 +291,12 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
295
291
|
date: commit.committer.date.getTime(),
|
|
296
292
|
commitDate: commit.committer.date.getTime(),
|
|
297
293
|
message: commit.message?.length ? commit.message : commit.summary,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
294
|
+
kind: commit.parents.length > 1 ? 'merge' : 'commit',
|
|
295
|
+
// Omitted rather than empty: an empty array still counts as "present" to the splice
|
|
296
|
+
// fingerprint, which defeats its bare-row skip and makes every ref-less row look changeable.
|
|
297
|
+
heads: refHeads.length ? refHeads : undefined,
|
|
298
|
+
remotes: refRemoteHeads.length ? refRemoteHeads : undefined,
|
|
299
|
+
tags: refTags.length ? refTags : undefined,
|
|
302
300
|
});
|
|
303
301
|
|
|
304
302
|
if (commit.stats != null) {
|
|
@@ -623,11 +621,6 @@ class GraphSession implements GitGraphSession {
|
|
|
623
621
|
return true;
|
|
624
622
|
}
|
|
625
623
|
|
|
626
|
-
serialize(): GitGraphSessionSnapshot | undefined {
|
|
627
|
-
// No incremental restore path (no ref-tip gate / reachability table), so nothing worth persisting.
|
|
628
|
-
return undefined;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
624
|
dispose(): void {
|
|
632
625
|
// No-op — the window lives entirely in memory.
|
|
633
626
|
}
|
|
@@ -32,6 +32,38 @@ export interface IntegrationAuthenticationSessionDescriptor {
|
|
|
32
32
|
[key: string]: unknown;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Options for {@link IntegrationAuthenticationProvider.getSession}. The two variants keep `sync` exclusive
|
|
37
|
+
* with the interactive flags: a sync resolves whatever the cloud already has, so it must never open a connect
|
|
38
|
+
* window.
|
|
39
|
+
*
|
|
40
|
+
* `refreshRejectedToken` marks the stored token as known-refused by the provider (an `AuthenticationError` on
|
|
41
|
+
* a previous read), which forces a cloud `/refresh` even when the token's expiry still claims it is valid —
|
|
42
|
+
* the case a purely time-based refresh cannot see. The refresh token itself never reaches the client: the GK
|
|
43
|
+
* cloud performs the exchange server-side.
|
|
44
|
+
*/
|
|
45
|
+
export type GetSessionOptions =
|
|
46
|
+
| {
|
|
47
|
+
createIfNeeded?: boolean;
|
|
48
|
+
forceNewSession?: boolean;
|
|
49
|
+
sync?: never;
|
|
50
|
+
refreshRejectedToken?: boolean;
|
|
51
|
+
source?: Sources;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
createIfNeeded?: never;
|
|
55
|
+
forceNewSession?: never;
|
|
56
|
+
sync: boolean;
|
|
57
|
+
refreshRejectedToken?: boolean;
|
|
58
|
+
source?: Sources;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What {@link IntegrationAuthenticationProviderBase.getSession} passes down once it has decided the stored
|
|
63
|
+
* session can't be served as-is. `refreshIfExpired` is its own conclusion, not a caller's option.
|
|
64
|
+
*/
|
|
65
|
+
type GetNewSessionOptions = GetSessionOptions & { refreshIfExpired?: boolean };
|
|
66
|
+
|
|
35
67
|
export interface IntegrationAuthenticationProvider extends Disposable {
|
|
36
68
|
/**
|
|
37
69
|
* Clears the stored secret for one connection only (never the whole provider). Unlike
|
|
@@ -46,11 +78,13 @@ export interface IntegrationAuthenticationProvider extends Disposable {
|
|
|
46
78
|
options?: { preserveConfigured?: boolean },
|
|
47
79
|
): Promise<void>;
|
|
48
80
|
deleteAllSessions(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Resolves the session for `descriptor`, from storage when it is still usable and from the cloud
|
|
83
|
+
* otherwise. See {@link GetSessionOptions}.
|
|
84
|
+
*/
|
|
49
85
|
getSession(
|
|
50
86
|
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
51
|
-
options?:
|
|
52
|
-
| { createIfNeeded?: boolean; forceNewSession?: boolean; sync?: never; source?: Sources }
|
|
53
|
-
| { createIfNeeded?: never; forceNewSession?: never; sync: boolean; source?: Sources },
|
|
87
|
+
options?: GetSessionOptions,
|
|
54
88
|
): Promise<ProviderAuthenticationSession | undefined>;
|
|
55
89
|
get onDidChange(): Event<void>;
|
|
56
90
|
}
|
|
@@ -120,9 +154,7 @@ abstract class IntegrationAuthenticationProviderBase<
|
|
|
120
154
|
@trace()
|
|
121
155
|
async getSession(
|
|
122
156
|
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
123
|
-
options?:
|
|
124
|
-
| { createIfNeeded?: boolean; forceNewSession?: boolean; sync?: never; source?: Sources }
|
|
125
|
-
| { createIfNeeded?: never; forceNewSession?: never; sync: boolean; source?: Sources },
|
|
157
|
+
options?: GetSessionOptions,
|
|
126
158
|
): Promise<ProviderAuthenticationSession | undefined> {
|
|
127
159
|
let session;
|
|
128
160
|
let previousToken;
|
|
@@ -143,10 +175,15 @@ abstract class IntegrationAuthenticationProviderBase<
|
|
|
143
175
|
}
|
|
144
176
|
|
|
145
177
|
const isExpiredSession = session?.expiresAt != null && new Date(session.expiresAt).getTime() < Date.now();
|
|
146
|
-
|
|
178
|
+
// A rejected token has to re-resolve even when the stored session looks perfectly healthy: that is
|
|
179
|
+
// the whole point — the provider refused it while its expiry still says valid. Without this the
|
|
180
|
+
// stored session short-circuits here and the refresh below is never reached.
|
|
181
|
+
const refreshRejectedToken = options?.refreshRejectedToken === true && session != null;
|
|
182
|
+
if (session == null || isExpiredSession || refreshRejectedToken) {
|
|
147
183
|
session = await this.getNewSession(descriptor, {
|
|
148
184
|
...options,
|
|
149
185
|
refreshIfExpired: isExpiredSession,
|
|
186
|
+
refreshRejectedToken: refreshRejectedToken,
|
|
150
187
|
});
|
|
151
188
|
|
|
152
189
|
if (session != null) {
|
|
@@ -163,21 +200,7 @@ abstract class IntegrationAuthenticationProviderBase<
|
|
|
163
200
|
|
|
164
201
|
protected abstract getNewSession(
|
|
165
202
|
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
166
|
-
options?:
|
|
167
|
-
| {
|
|
168
|
-
createIfNeeded?: boolean;
|
|
169
|
-
forceNewSession?: boolean;
|
|
170
|
-
sync?: never;
|
|
171
|
-
refreshIfExpired?: boolean;
|
|
172
|
-
source?: Sources;
|
|
173
|
-
}
|
|
174
|
-
| {
|
|
175
|
-
createIfNeeded?: never;
|
|
176
|
-
forceNewSession?: never;
|
|
177
|
-
sync: boolean;
|
|
178
|
-
refreshIfExpired?: boolean;
|
|
179
|
-
source?: Sources;
|
|
180
|
-
},
|
|
203
|
+
options?: GetNewSessionOptions,
|
|
181
204
|
): Promise<ProviderAuthenticationSession | undefined>;
|
|
182
205
|
|
|
183
206
|
protected fireChange(): void {
|
|
@@ -202,21 +225,7 @@ export class CloudIntegrationAuthenticationProvider<
|
|
|
202
225
|
|
|
203
226
|
protected override async getNewSession(
|
|
204
227
|
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
205
|
-
options?:
|
|
206
|
-
| {
|
|
207
|
-
createIfNeeded?: boolean;
|
|
208
|
-
forceNewSession?: boolean;
|
|
209
|
-
sync?: never;
|
|
210
|
-
refreshIfExpired?: boolean;
|
|
211
|
-
source?: Sources;
|
|
212
|
-
}
|
|
213
|
-
| {
|
|
214
|
-
createIfNeeded?: never;
|
|
215
|
-
forceNewSession?: never;
|
|
216
|
-
sync: boolean;
|
|
217
|
-
refreshIfExpired?: boolean;
|
|
218
|
-
source?: Sources;
|
|
219
|
-
},
|
|
228
|
+
options?: GetNewSessionOptions,
|
|
220
229
|
): Promise<ProviderAuthenticationSession | undefined> {
|
|
221
230
|
if (options?.forceNewSession) {
|
|
222
231
|
if ((await this.disconnectCloudSession()) === 'failure') {
|
|
@@ -230,9 +239,15 @@ export class CloudIntegrationAuthenticationProvider<
|
|
|
230
239
|
// TODO: This is a stopgap to make sure we're not hammering the api on automatic calls to get the session.
|
|
231
240
|
// Ultimately we want to timestamp calls to syncCloudIntegrations and use that to determine whether we should
|
|
232
241
|
// make the call or not.
|
|
242
|
+
// `refreshRejectedToken` joins the list for the same reason `refreshIfExpired` is on it: both mean the
|
|
243
|
+
// session in hand is known-unusable, so skipping the cloud round trip would just re-serve it.
|
|
233
244
|
let session =
|
|
234
|
-
options?.refreshIfExpired ||
|
|
235
|
-
|
|
245
|
+
options?.refreshIfExpired ||
|
|
246
|
+
options?.refreshRejectedToken ||
|
|
247
|
+
options?.createIfNeeded ||
|
|
248
|
+
options?.forceNewSession ||
|
|
249
|
+
options?.sync
|
|
250
|
+
? await this.getCloudSession(descriptor, { refreshRejectedToken: options?.refreshRejectedToken })
|
|
236
251
|
: undefined;
|
|
237
252
|
|
|
238
253
|
const shouldCreateSession = options?.createIfNeeded && session == null;
|
|
@@ -269,6 +284,7 @@ export class CloudIntegrationAuthenticationProvider<
|
|
|
269
284
|
|
|
270
285
|
private async getCloudSession(
|
|
271
286
|
descriptor: IntegrationAuthenticationSessionDescriptor,
|
|
287
|
+
options?: { refreshRejectedToken?: boolean },
|
|
272
288
|
): Promise<ProviderAuthenticationSession | undefined> {
|
|
273
289
|
const loggedIn = (await this.authenticationService.ctx.account.getAccount()) != null;
|
|
274
290
|
if (!loggedIn) return undefined;
|
|
@@ -304,7 +320,13 @@ export class CloudIntegrationAuthenticationProvider<
|
|
|
304
320
|
session.expiresIn = maxSmallIntegerV8; // maximum expiration length
|
|
305
321
|
}
|
|
306
322
|
|
|
307
|
-
|
|
323
|
+
// `expiresIn < 60` is a PREDICTION that the token is about to lapse, and it is the only trigger the
|
|
324
|
+
// refresh used to have. It cannot see a token the provider has already refused while the backend
|
|
325
|
+
// still believes it is valid — a revoked grant, retired scopes, an app removed from the org — where
|
|
326
|
+
// `expiresIn` stays high and the doomed token is re-sent on every read. `refreshRejectedToken` is
|
|
327
|
+
// the observed counterpart: the caller saw the provider reject this exact token, so refresh it
|
|
328
|
+
// regardless of what its expiry claims.
|
|
329
|
+
if (session != null && (session.expiresIn < 60 || options?.refreshRejectedToken)) {
|
|
308
330
|
session = await cloudIntegrations.getConnectionSession(
|
|
309
331
|
this.authProviderId,
|
|
310
332
|
session.accessToken,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-connection recovery state for a cloud token the provider REFUSED — a revoked grant, retired scopes, an
|
|
3
|
+
* app removed from the org. A purely predictive refresh (`expiresIn < 60`) cannot see that case, because the
|
|
4
|
+
* backend still believes the token is valid, so without this the refused token is re-sent on every read and
|
|
5
|
+
* the failure only clears when the user reconnects by hand.
|
|
6
|
+
*
|
|
7
|
+
* One recovery refresh per connection, then the connection is the failure budget's problem:
|
|
8
|
+
* - {@link recordRejection} on an `AuthenticationError` — arms the refresh, or reports that the rejection is
|
|
9
|
+
* not recoverable so the caller counts it as a real failure instead.
|
|
10
|
+
* - {@link claimRefresh} before the next read resolves its session — spends the rejection, so the refresh
|
|
11
|
+
* fires once and no further read of that connection asks the cloud again.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT re-armed when the refresh hands back a different token. The GK cloud mints a new access
|
|
14
|
+
* token on every `/refresh`, so a credential the provider refuses for a reason a refresh cannot fix (retired
|
|
15
|
+
* scopes being the plain case) would look "rotated" on each attempt: re-arming on that would refresh on every
|
|
16
|
+
* read forever and the failure budget — the thing that disconnects and prompts a reconnect — would never
|
|
17
|
+
* advance. What resets this instead is {@link clear}, called by every path that replaces or removes the stored
|
|
18
|
+
* tokens; a connection that is simply working never reaches here at all.
|
|
19
|
+
*/
|
|
20
|
+
export class RejectedTokenTracker {
|
|
21
|
+
private readonly _rejections = new Map<string, 'armed' | 'spent'>();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether a read of `connectionId` must force the cloud refresh of a token known to be refused, spending
|
|
25
|
+
* the rejection so the refresh runs at most once. Spent here rather than after the read because a resolve
|
|
26
|
+
* that throws must not leave the refresh armed and ask the cloud again on the next read.
|
|
27
|
+
*/
|
|
28
|
+
claimRefresh(connectionId: string): boolean {
|
|
29
|
+
if (this._rejections.get(connectionId) !== 'armed') return false;
|
|
30
|
+
|
|
31
|
+
this._rejections.set(connectionId, 'spent');
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Arms the recovery refresh for `connectionId`. Returns `false` once this connection has had its refresh
|
|
37
|
+
* (or already has one pending), so the caller falls back to its own failure handling: a token still
|
|
38
|
+
* refused after the refresh is not self-healing, and only the failure budget ends in a reconnect prompt.
|
|
39
|
+
*/
|
|
40
|
+
recordRejection(connectionId: string): boolean {
|
|
41
|
+
if (this._rejections.has(connectionId)) return false;
|
|
42
|
+
|
|
43
|
+
this._rejections.set(connectionId, 'armed');
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Drops all state, for when the stored tokens a rejection could name are replaced or gone. */
|
|
48
|
+
clear(): void {
|
|
49
|
+
this._rejections.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether any connection is holding a rejection. For assertions; the read path uses {@link claimRefresh}. */
|
|
53
|
+
get empty(): boolean {
|
|
54
|
+
return this._rejections.size === 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -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,
|