@gitkraken/core-gitlens 0.9.4 → 0.9.6
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 -1
- package/dist/git/models/issue.d.ts +25 -2
- package/dist/git/models/issue.js +3 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/models/pullRequest.d.ts +62 -7
- package/dist/git/models/pullRequest.js +5 -1
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/git/utils/issue.utils.d.ts +4 -0
- package/dist/git/utils/issue.utils.js +4 -0
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/pullRequest.utils.d.ts +4 -0
- package/dist/git/utils/pullRequest.utils.js +8 -0
- package/dist/git/utils/pullRequest.utils.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +64 -4
- package/dist/plus/git-github/api/github.js +232 -15
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/models.d.ts +48 -6
- package/dist/plus/git-github/models.js +11 -10
- package/dist/plus/git-github/models.js.map +1 -1
- package/dist/plus/integrations/fieldPresence.d.ts +75 -0
- package/dist/plus/integrations/fieldPresence.js +381 -0
- package/dist/plus/integrations/fieldPresence.js.map +1 -0
- package/dist/plus/integrations/index.d.ts +6 -4
- package/dist/plus/integrations/index.js +2 -1
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +7 -3
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +56 -6
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +103 -9
- package/dist/plus/integrations/models/gitHostIntegration.js +116 -5
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +91 -4
- package/dist/plus/integrations/models/integration.js +56 -7
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.d.ts +44 -3
- package/dist/plus/integrations/models/issuesIntegration.js +51 -1
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.js +3 -3
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/models.d.ts +36 -4
- package/dist/plus/integrations/providers/azure/models.js +118 -8
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +102 -5
- package/dist/plus/integrations/providers/azureDevOps.js +264 -45
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +30 -6
- package/dist/plus/integrations/providers/azureDevOpsServer.js +82 -21
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts +21 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js +74 -10
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/models.d.ts +40 -3
- package/dist/plus/integrations/providers/bitbucket/models.js +30 -7
- package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +2 -6
- package/dist/plus/integrations/providers/bitbucket-server/models.js +1 -3
- package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +3 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +4 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +2 -2
- package/dist/plus/integrations/providers/bitbucket-server.js +7 -5
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +22 -2
- package/dist/plus/integrations/providers/bitbucket.js +29 -6
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +40 -3
- package/dist/plus/integrations/providers/github.js +133 -11
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.d.ts +34 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.js +118 -4
- package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab/models.d.ts +70 -5
- package/dist/plus/integrations/providers/gitlab/models.js +106 -13
- package/dist/plus/integrations/providers/gitlab/models.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +31 -2
- package/dist/plus/integrations/providers/gitlab.js +48 -8
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/jira-server.d.ts +1 -0
- package/dist/plus/integrations/providers/jira-server.js +9 -6
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +20 -1
- package/dist/plus/integrations/providers/jira.js +101 -6
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +79 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.js +58 -7
- package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +19 -1
- package/dist/plus/integrations/providers/linear.js +97 -5
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +42 -0
- package/dist/plus/integrations/providers/linearIssuesEtag.js +94 -0
- package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -0
- package/dist/plus/integrations/providers/models.d.ts +51 -7
- package/dist/plus/integrations/providers/models.js +46 -17
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.js +8 -1
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +62 -3
- package/dist/plus/integrations/providers/providersApi.js +145 -4
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/pullRequestReviews.d.ts +13 -0
- package/dist/plus/integrations/providers/pullRequestReviews.js +30 -6
- package/dist/plus/integrations/providers/pullRequestReviews.js.map +1 -1
- package/dist/plus/integrations/providers/trello.js +10 -7
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts +11 -0
- package/dist/plus/integrations/providers/utils/providerPaging.js +47 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/providers/utils.d.ts +1 -1
- package/dist/plus/integrations/reads/batchEtags.d.ts +69 -0
- package/dist/plus/integrations/reads/batchEtags.js +114 -0
- package/dist/plus/integrations/reads/batchEtags.js.map +1 -0
- package/dist/plus/integrations/reads/counts.js +2 -2
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/currentAccount.js +2 -2
- package/dist/plus/integrations/reads/currentAccount.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +12 -3
- package/dist/plus/integrations/reads/drains.js +29 -5
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/etag.d.ts +26 -0
- package/dist/plus/integrations/reads/etag.js +108 -0
- package/dist/plus/integrations/reads/etag.js.map +1 -0
- package/dist/plus/integrations/reads/hierarchy.js +3 -3
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/issueBatch.d.ts +47 -16
- package/dist/plus/integrations/reads/issueBatch.js +55 -87
- package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +4 -4
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts +1 -1
- package/dist/plus/integrations/reads/issues.js +4 -4
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBatch.d.ts +44 -6
- package/dist/plus/integrations/reads/pullRequestBatch.js +31 -34
- package/dist/plus/integrations/reads/pullRequestBatch.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBranches.d.ts +5 -2
- package/dist/plus/integrations/reads/pullRequestBranches.js +4 -3
- package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +8 -4
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/sweeps.js +8 -4
- package/dist/plus/integrations/reads/sweeps.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +23 -8
- package/dist/plus/integrations/reads/warnings.js +43 -19
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +12 -4
- package/dist/plus/integrations/results.js +2 -0
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +385 -23
- package/docs/kepler-read-api-parity.md +16 -2
- package/package.json +1 -1
|
@@ -5,9 +5,10 @@ import { Issue, IssueShape } from "../../../git/models/issue.js";
|
|
|
5
5
|
import { Account } from "../../../git/models/author.js";
|
|
6
6
|
import { ProviderAuthenticationSession } from "../authentication/models.js";
|
|
7
7
|
import { IssuesCloudHostIntegrationId } from "../constants.js";
|
|
8
|
+
import { ProviderApiCollectionResult } from "./models.js";
|
|
8
9
|
import { AccountWideIssuesResult, IssuesForProjectOptions, ProjectIssuesDrain, SearchMyIssuesOptions } from "../models/issueReads.js";
|
|
9
10
|
import { IssuesIntegration } from "../models/issuesIntegration.js";
|
|
10
|
-
import {
|
|
11
|
+
import { IssueEtagFields } from "../models/integration.js";
|
|
11
12
|
import { IntegrationAuthenticationProviderDescriptor } from "../authentication/integrationAuthenticationProvider.js";
|
|
12
13
|
//#region ../plus/integrations/src/providers/linear.d.ts
|
|
13
14
|
export interface LinearTeamDescriptor extends IssueResourceDescriptor {
|
|
@@ -74,6 +75,23 @@ export declare class LinearIntegration extends IssuesIntegration<IssuesCloudHost
|
|
|
74
75
|
}, _type: undefined | IssueOrPullRequestType): Promise<IssueOrPullRequest | undefined>;
|
|
75
76
|
protected getProviderIssue(session: ProviderAuthenticationSession, resource: ResourceDescriptor, id: string): Promise<Issue | undefined>;
|
|
76
77
|
protected getProviderIssueByResourceId(session: ProviderAuthenticationSession, resourceId: string, id: string, _resourceUrl: string | undefined): Promise<Issue | undefined>;
|
|
78
|
+
/**
|
|
79
|
+
* The cheap check behind the batch issue read's etags: one `issues` query per team and
|
|
80
|
+
* {@link linearIssuesEtagMaxNumbers} distinct numbers, where {@link getProviderIssueByResourceId} sends one request
|
|
81
|
+
* per issue. A request that throws rejects only its own targets' slots, with its error classified as the full
|
|
82
|
+
* read's would be.
|
|
83
|
+
*
|
|
84
|
+
* Never proves an absence. The query filters by the team's CURRENT key and asks for archived issues too, but the
|
|
85
|
+
* full read's `issue(id:)` may also resolve an identifier the issue no longer carries (its team was renamed, or it
|
|
86
|
+
* moved to another team), so an issue the query leaves out may still exist. Its target's slot is rejected, as is a
|
|
87
|
+
* target whose identifier isn't a team key and a number, so the full read decides. When no target was answered and
|
|
88
|
+
* nothing failed, the check declines instead, so a batch of only such targets costs a full read, not a failure.
|
|
89
|
+
*/
|
|
90
|
+
protected getProviderIssuesEtagFieldsByResourceId(session: ProviderAuthenticationSession, targets: readonly {
|
|
91
|
+
resourceId: string;
|
|
92
|
+
identifier: string;
|
|
93
|
+
resourceUrl?: string;
|
|
94
|
+
}[]): Promise<PromiseSettledResult<IssueEtagFields | undefined>[] | undefined>;
|
|
77
95
|
private getRawProviderIssue;
|
|
78
96
|
private getIssueAutolinkLikeUrl;
|
|
79
97
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { chunk } from "../../../utils/array.js";
|
|
1
2
|
import { Logger as _logger } from "../../../utils/logger.js";
|
|
3
|
+
import { mapSettledBounded } from "../../../utils/promise.js";
|
|
2
4
|
import { isIssueResourceDescriptor } from "../../../git/utils/resourceDescriptor.utils.js";
|
|
3
5
|
import { IntegrationReadUnavailableError } from "../errors.js";
|
|
4
6
|
import "../constants.js";
|
|
@@ -8,6 +10,7 @@ import { mergeCollectionMetadata } from "./utils/providerPaging.js";
|
|
|
8
10
|
import { fromProviderIssue, providersMetadata, toIssueShape, toProviderIssueStates } from "./models.js";
|
|
9
11
|
import { IssuesIntegration } from "../models/issuesIntegration.js";
|
|
10
12
|
import { DiscoveryCache } from "./utils/discoveryCache.js";
|
|
13
|
+
import { toLinearIssueEtagFields } from "./linearIssuesEtag.js";
|
|
11
14
|
import * as l10n from "@vscode/l10n";
|
|
12
15
|
//#region ../plus/integrations/src/providers/linear.ts
|
|
13
16
|
const metadata = providersMetadata["linear"];
|
|
@@ -32,6 +35,8 @@ const maxPagesPerRequest = 10;
|
|
|
32
35
|
*/
|
|
33
36
|
const maxAccountWidePagesPerRequest = 50;
|
|
34
37
|
const linearImplicitTeamsPageSize = 50;
|
|
38
|
+
/** A team key and issue number, as Linear writes an issue's identifier (`ENG-123`). */
|
|
39
|
+
const linearIdentifier = /^([A-Za-z0-9]+)-(\d+)$/;
|
|
35
40
|
var LinearIntegration = class extends IssuesIntegration {
|
|
36
41
|
/**
|
|
37
42
|
* Built from the teams, so it follows them: rebuilt when the team list changes or the caches are dropped, and once
|
|
@@ -204,7 +209,7 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
204
209
|
truncated ||= result.paging?.truncated === true;
|
|
205
210
|
collectionMetadata = mergeCollectionMetadata(collectionMetadata, result.metadata);
|
|
206
211
|
for (const issue of result.values) {
|
|
207
|
-
const shape = toIssueShape(issue, this);
|
|
212
|
+
const shape = toIssueShape(issue, this, { projection: "project" });
|
|
208
213
|
if (shape != null) issues.push(shape);
|
|
209
214
|
}
|
|
210
215
|
if (hasMore && (nextCursor == null || nextCursor === cursor)) {
|
|
@@ -294,7 +299,7 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
294
299
|
requestCount += 1;
|
|
295
300
|
hasMore = result.paging?.more ?? false;
|
|
296
301
|
const nextCursor = result.paging?.cursor;
|
|
297
|
-
const formattedIssues = result.values.map((issue) => toIssueShape(issue, this)).filter((result) => result != null);
|
|
302
|
+
const formattedIssues = result.values.map((issue) => toIssueShape(issue, this, { projection: "account" })).filter((result) => result != null);
|
|
298
303
|
if (formattedIssues.length > 0) issues.push(...formattedIssues);
|
|
299
304
|
if (hasMore && (nextCursor == null || nextCursor === cursor)) {
|
|
300
305
|
truncated = true;
|
|
@@ -319,18 +324,105 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
319
324
|
...issue,
|
|
320
325
|
url: this.getIssueAutolinkLikeUrl(issue)
|
|
321
326
|
};
|
|
322
|
-
return autolinkableIssue && toIssueShape(autolinkableIssue, this);
|
|
327
|
+
return autolinkableIssue && toIssueShape(autolinkableIssue, this, { projection: "point" });
|
|
323
328
|
}
|
|
324
329
|
async getProviderIssue(session, resource, id) {
|
|
325
330
|
const result = await this.getRawProviderIssue(session, resource, id);
|
|
326
|
-
return result && fromProviderIssue(result, this);
|
|
331
|
+
return result && fromProviderIssue(result, this, { projection: "point" });
|
|
327
332
|
}
|
|
328
333
|
async getProviderIssueByResourceId(session, resourceId, id, _resourceUrl) {
|
|
329
334
|
const result = await (await this.getProvidersApi()).getIssue(toTokenWithInfo(this.id, session), {
|
|
330
335
|
resourceId,
|
|
331
336
|
number: id
|
|
332
337
|
});
|
|
333
|
-
return result && fromProviderIssue(result, this);
|
|
338
|
+
return result && fromProviderIssue(result, this, { projection: "batch" });
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The cheap check behind the batch issue read's etags: one `issues` query per team and
|
|
342
|
+
* {@link linearIssuesEtagMaxNumbers} distinct numbers, where {@link getProviderIssueByResourceId} sends one request
|
|
343
|
+
* per issue. A request that throws rejects only its own targets' slots, with its error classified as the full
|
|
344
|
+
* read's would be.
|
|
345
|
+
*
|
|
346
|
+
* Never proves an absence. The query filters by the team's CURRENT key and asks for archived issues too, but the
|
|
347
|
+
* full read's `issue(id:)` may also resolve an identifier the issue no longer carries (its team was renamed, or it
|
|
348
|
+
* moved to another team), so an issue the query leaves out may still exist. Its target's slot is rejected, as is a
|
|
349
|
+
* target whose identifier isn't a team key and a number, so the full read decides. When no target was answered and
|
|
350
|
+
* nothing failed, the check declines instead, so a batch of only such targets costs a full read, not a failure.
|
|
351
|
+
*/
|
|
352
|
+
async getProviderIssuesEtagFieldsByResourceId(session, targets) {
|
|
353
|
+
const slots = new Array(targets.length);
|
|
354
|
+
let answered = false;
|
|
355
|
+
let failed = false;
|
|
356
|
+
const byTeam = /* @__PURE__ */ new Map();
|
|
357
|
+
for (const [index, target] of targets.entries()) {
|
|
358
|
+
const match = linearIdentifier.exec(target.identifier);
|
|
359
|
+
const number = match != null ? Number(match[2]) : NaN;
|
|
360
|
+
if (match == null || !Number.isSafeInteger(number) || String(number) !== match[2]) {
|
|
361
|
+
slots[index] = {
|
|
362
|
+
status: "rejected",
|
|
363
|
+
reason: /* @__PURE__ */ new Error(`Not a Linear issue identifier: ${target.identifier}`)
|
|
364
|
+
};
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const teamKey = match[1].toUpperCase();
|
|
368
|
+
const groupKey = `${target.resourceId}\n${teamKey}`;
|
|
369
|
+
let group = byTeam.get(groupKey);
|
|
370
|
+
if (group == null) {
|
|
371
|
+
group = {
|
|
372
|
+
teamKey,
|
|
373
|
+
numbers: /* @__PURE__ */ new Map()
|
|
374
|
+
};
|
|
375
|
+
byTeam.set(groupKey, group);
|
|
376
|
+
}
|
|
377
|
+
let indices = group.numbers.get(number);
|
|
378
|
+
if (indices == null) {
|
|
379
|
+
indices = [];
|
|
380
|
+
group.numbers.set(number, indices);
|
|
381
|
+
}
|
|
382
|
+
indices.push(index);
|
|
383
|
+
}
|
|
384
|
+
const requests = [...byTeam.values()].flatMap(({ teamKey, numbers }) => chunk([...numbers], 50).map((entries) => ({
|
|
385
|
+
teamKey,
|
|
386
|
+
entries
|
|
387
|
+
})));
|
|
388
|
+
if (requests.length) {
|
|
389
|
+
const api = await this.getProvidersApi();
|
|
390
|
+
const tokenWithInfo = toTokenWithInfo(this.id, session);
|
|
391
|
+
const answers = await mapSettledBounded(requests, 6, (r) => api.getLinearIssuesEtagFields(tokenWithInfo, r.teamKey, r.entries.map(([number]) => number)));
|
|
392
|
+
for (const [i, answer] of answers.entries()) {
|
|
393
|
+
const { teamKey, entries } = requests[i];
|
|
394
|
+
if (answer.status === "rejected") {
|
|
395
|
+
failed = true;
|
|
396
|
+
for (const [, indices] of entries) for (const index of indices) slots[index] = answer;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const byNumber = /* @__PURE__ */ new Map();
|
|
400
|
+
for (const issue of answer.value) byNumber.set(issue.number, issue);
|
|
401
|
+
for (const [number, indices] of entries) {
|
|
402
|
+
const issue = byNumber.get(number);
|
|
403
|
+
let slot;
|
|
404
|
+
if (issue == null) slot = {
|
|
405
|
+
status: "rejected",
|
|
406
|
+
reason: /* @__PURE__ */ new Error(`Linear did not return ${teamKey}-${number}`)
|
|
407
|
+
};
|
|
408
|
+
else try {
|
|
409
|
+
slot = {
|
|
410
|
+
status: "fulfilled",
|
|
411
|
+
value: toLinearIssueEtagFields(issue)
|
|
412
|
+
};
|
|
413
|
+
answered = true;
|
|
414
|
+
} catch (ex) {
|
|
415
|
+
slot = {
|
|
416
|
+
status: "rejected",
|
|
417
|
+
reason: ex
|
|
418
|
+
};
|
|
419
|
+
failed = true;
|
|
420
|
+
}
|
|
421
|
+
for (const index of indices) slots[index] = slot;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return answered || failed ? slots : void 0;
|
|
334
426
|
}
|
|
335
427
|
async getRawProviderIssue(session, resource, id) {
|
|
336
428
|
const api = await this.getProvidersApi();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linear.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linear.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport * as l10n from '@vscode/l10n';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor, ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderApiCollectionResult, ProviderIssue } from './models.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape, toProviderIssueStates } from './models.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.js';\nimport { mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Linear];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n/**\n * The account-wide drain's own backstop, separate from {@link maxPagesPerRequest}.\n *\n * Raised to 50 (5,000 issues at the SDK's 100-per-page) because 10 no longer means what it used to. That read\n * once fanned out over four overlapping relationship queries and merged them, so most of what a page cost was\n * rows already returned — a low budget capped the waste. It is now a single server-ordered query over an `or`\n * filter, so every page is 100 issues the caller has not seen, and the same budget just truncates real results\n * at 1,000.\n *\n * Reaching it is reported rather than silent: {@link LinearIntegration.searchProviderMyIssuesWithTruncation}\n * returns `truncated`, so the facade can say the read was incomplete instead of publishing a capped list as a\n * whole account. The bound stays — a runaway cursor must not spend requests forever — but it is now set where a\n * real Linear account is unlikely to reach it rather than where the duplication used to hurt.\n */\nconst maxAccountWidePagesPerRequest = 50;\nconst linearImplicitTeamsPageSize = 50;\n\nexport interface LinearTeamDescriptor extends IssueResourceDescriptor {\n\tavatarUrl: string | undefined;\n}\n\nexport interface LinearOrganizationDescriptor extends IssueResourceDescriptor {\n\turl: string;\n}\n\nexport interface LinearProjectDescriptor extends IssueResourceDescriptor {}\n\nexport class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Linear> {\n\t/**\n\t * Built from the teams, so it follows them: rebuilt when the team list changes or the caches are dropped, and once\n\t * older than {@link discoveryCacheTtl}. An expired set is still served while it is rebuilt in the background, because\n\t * autolinks are read on every render of a commit message and must not wait on the network (#5907).\n\t */\n\tprivate readonly _autolinks = new Map<\n\t\tstring,\n\t\t{ autolinks: (AutolinkReference | DynamicAutolinkReference)[]; builtAt: number }\n\t>();\n\tprivate readonly _autolinksBuilds = new Map<string, Promise<(AutolinkReference | DynamicAutolinkReference)[]>>();\n\n\toverride async autolinks(): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected || this._session == null) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst session = this._session;\n\t\tconst cached = this._autolinks.get(session.accessToken);\n\t\tif (cached == null) return this.buildAutolinks(session);\n\n\t\tif (Date.now() - cached.builtAt >= discoveryCacheTtl) {\n\t\t\tvoid this.buildAutolinks(session).catch(() => {});\n\t\t}\n\t\treturn cached.autolinks;\n\t}\n\n\t/** One build per token at a time: concurrent renders share it rather than each re-reading the teams. */\n\tprivate buildAutolinks(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tlet build = this._autolinksBuilds.get(accessToken);\n\t\tif (build == null) {\n\t\t\tbuild = this.buildAutolinksCore(session).finally(() => this._autolinksBuilds.delete(accessToken));\n\t\t\tthis._autolinksBuilds.set(accessToken, build);\n\t\t}\n\t\treturn build;\n\t}\n\n\tprivate async buildAutolinksCore(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst generation = this._teams.generation;\n\t\tconst organization = await this.getOrganization(session);\n\t\tif (organization == null) return [];\n\n\t\tconst autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];\n\n\t\tconst teams = await this.getTeams(session);\n\t\tfor (const team of teams ?? []) {\n\t\t\tconst dashedPrefix = `${team.key}-`;\n\t\t\tconst underscoredPrefix = `${team.key}_`;\n\n\t\t\tautolinks.push({\n\t\t\t\tprefix: dashedPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t\tautolinks.push({\n\t\t\t\tprefix: underscoredPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\treferenceType: 'branch',\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t}\n\n\t\t// Not kept when built from teams read before the caches were dropped.\n\t\tif (this._teams.generation === generation) {\n\t\t\tthis._autolinks.set(session.accessToken, { autolinks: autolinks, builtAt: Date.now() });\n\t\t}\n\t\treturn autolinks;\n\t}\n\n\tprivate _organizations: Map<string, LinearOrganizationDescriptor | undefined> | undefined;\n\tprivate async getOrganization(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearOrganizationDescriptor | undefined> {\n\t\tconst { accessToken } = session;\n\t\tthis._organizations ??= new Map<string, LinearOrganizationDescriptor | undefined>();\n\n\t\tconst cachedResources = this._organizations.get(accessToken);\n\n\t\tif (cachedResources == null || force) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst organization = await api.getLinearOrganization(toTokenWithInfo(this.id, session));\n\t\t\tconst descriptor: LinearOrganizationDescriptor | undefined = organization && {\n\t\t\t\tid: organization.id,\n\t\t\t\tkey: organization.key,\n\t\t\t\tname: organization.name,\n\t\t\t\turl: organization.url,\n\t\t\t};\n\t\t\tif (descriptor) {\n\t\t\t\tthis._organizations.set(accessToken, descriptor);\n\t\t\t}\n\t\t}\n\n\t\treturn this._organizations.get(accessToken);\n\t}\n\n\t/** Expires, and is dropped on a re-sync, so a team created or left mid-session is picked up (#5907). */\n\tprivate readonly _teams = new DiscoveryCache<LinearTeamDescriptor[]>();\n\tprivate async getTeams(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearTeamDescriptor[] | undefined> {\n\t\treturn (await this.getTeamsWithMetadata(session, force))?.values;\n\t}\n\n\tprivate async getTeamsWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<ProviderApiCollectionResult<LinearTeamDescriptor> | undefined> {\n\t\tconst { accessToken } = session;\n\n\t\tconst cachedResources = this._teams.get(accessToken);\n\t\tif (cachedResources != null && !force) return { values: cachedResources };\n\n\t\tconst generation = this._teams.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst teams = await api.getLinearTeamsForCurrentUser(toTokenWithInfo(this.id, session));\n\t\tconst descriptors: LinearTeamDescriptor[] | undefined = teams?.map(t => ({\n\t\t\tid: t.id,\n\t\t\tkey: t.key,\n\t\t\tname: t.name,\n\t\t\tavatarUrl: t.iconUrl,\n\t\t}));\n\t\tif (descriptors == null) return undefined;\n\n\t\t// provider-apis currently requests Linear's teams connection without pageInfo or a cursor. Linear's\n\t\t// implicit page size is 50, so exactly a full page cannot prove there is no team 51. Preserve the useful\n\t\t// prefix, but mark it unknown and leave it uncached so the next read retries instead of treating it as\n\t\t// authoritative. Once provider-apis exposes a paged primitive, replace this fail-closed guard with a drain.\n\t\tif (descriptors.length >= linearImplicitTeamsPageSize) {\n\t\t\treturn { values: descriptors, metadata: { completeness: 'unknown' } };\n\t\t}\n\n\t\tif (this._teams.set(accessToken, descriptors, { generation: generation })) {\n\t\t\t// Built from the teams; a new set means new prefixes.\n\t\t\tthis._autolinks.delete(accessToken);\n\t\t}\n\t\treturn { values: descriptors };\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\tconst organization = await this.getOrganization(session);\n\t\treturn organization != null ? [organization] : undefined;\n\t}\n\tprotected override getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\treturn this.getTeams(session);\n\t}\n\tprotected override async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ProviderApiCollectionResult<ResourceDescriptor>> {\n\t\treturn (await this.getTeamsWithMetadata(session)) ?? { values: [] };\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._organizations = undefined;\n\t\tthis._teams.clear();\n\t\tthis._autolinks.clear();\n\t}\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\t// Linear's viewer isn't a ProviderAccount (no username/avatar), so build the Account manually\n\t\t// (Trello-style) from the fields the viewer query returns.\n\t\tconst user = await api.getLinearCurrentUser(toTokenWithInfo(this.id, session));\n\t\tif (user == null) return undefined;\n\n\t\treturn {\n\t\t\tprovider: this,\n\t\t\tid: user.id,\n\t\t\tname: user.name ?? user.displayName ?? undefined,\n\t\t\tusername: user.displayName ?? undefined,\n\t\t\temail: user.email ?? undefined,\n\t\t\tavatarUrl: undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\tconst api = await this.getProvidersApi();\n\t\t// `getProviderProjectsForResources` returns Linear teams, so `project.id` is a team id here. Drain the\n\t\t// team's issues (Linear pages by cursor); bounded by maxPagesPerRequest as a backstop. `truncated` is\n\t\t// set when that backstop stopped the drain with more pages still available.\n\t\t//\n\t\t// The state is narrowed server-side, not after the drain: a team's completed and canceled issues would\n\t\t// otherwise count against the backstop, so a team with enough done work runs out of pages before its open\n\t\t// issues, and the viewer filter below would then hand back a page that looks complete.\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tlet cursor: string | undefined;\n\t\tlet hasMore: boolean;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tlet collectionMetadata: CollectionMetadata | undefined;\n\t\tconst issues: IssueShape[] = [];\n\t\tdo {\n\t\t\tlet result: Awaited<ReturnType<typeof api.getLinearIssues>>;\n\t\t\ttry {\n\t\t\t\tresult = await api.getLinearIssues(\n\t\t\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\t\t\t{ teams: [project.id], states: states },\n\t\t\t\t\t{ cursor: cursor, sort: options?.sort },\n\t\t\t\t);\n\t\t\t} catch (ex) {\n\t\t\t\t// A page failure after the first page leaves the already-drained prefix intact; record the\n\t\t\t\t// failure at the project scope instead of re-throwing and discarding the prefix. If nothing was\n\t\t\t\t// fetched yet, preserve the original throw behavior so the caller sees a hard error.\n\t\t\t\tif (issues.length === 0) throw ex;\n\n\t\t\t\ttruncated = true;\n\t\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, {\n\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\tfailures: [toCollectionScopeFailure({ providerId: this.id, projectId: project.id }, ex)],\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trequestCount += 1;\n\t\t\thasMore = result.paging?.more ?? false;\n\t\t\tconst nextCursor = result.paging?.cursor;\n\t\t\ttruncated ||= result.paging?.truncated === true;\n\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, result.metadata);\n\t\t\tfor (const issue of result.values) {\n\t\t\t\tconst shape = toIssueShape(issue, this);\n\t\t\t\tif (shape != null) {\n\t\t\t\t\tissues.push(shape);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// The provider claims more but returns no advancing cursor: we can't continue without re-reading the\n\t\t\t// same page, so the drain is incomplete — flag it rather than silently stopping.\n\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcursor = nextCursor;\n\t\t\tif (hasMore && requestCount >= maxPagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} while (requestCount < maxPagesPerRequest && hasMore);\n\n\t\t// Linear's issue list has no server-side author/assignee filter, so scope to the current user\n\t\t// client-side when a user was requested (the assignee filter is what \"my issues\" means here).\n\t\t// Match on the viewer's stable id, not the passed display name: Linear's `name` (full name) and\n\t\t// `displayName` (nickname) are distinct fields, and assignees are normalized with `.name` = the full\n\t\t// name while the caller's `user` is the displayName — so a name string can miss. The assignee `.id`\n\t\t// is the Linear user id, which is unambiguous.\n\t\tif (options?.user != null) {\n\t\t\tconst viewerId = (await api.getLinearCurrentUser(toTokenWithInfo(this.id, session)))?.id;\n\t\t\t// If the viewer can't be resolved we can't scope to \"my issues\" — returning the unfiltered team\n\t\t\t// issues would leak everyone else's, and returning [] is indistinguishable from \"no issues assigned\n\t\t\t// to me\". Throw so the facade (getIssuesForProjectResult → runCaptured) surfaces a warning +\n\t\t\t// fetchFailed the caller can act on, instead of a silent empty.\n\t\t\tif (viewerId == null) {\n\t\t\t\tthrow new IntegrationReadUnavailableError(\n\t\t\t\t\tmetadata.name,\n\t\t\t\t\t'could not resolve the current user to scope issues to',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst values = issues.filter(issue => issue.assignees?.some(a => a.id === viewerId));\n\t\t\treturn truncated\n\t\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: collectionMetadata }\n\t\t\t\t: { values: values, truncated: false, metadata: collectionMetadata };\n\t\t}\n\n\t\treturn truncated\n\t\t\t? { values: issues, truncated: true, recovery: 'none', metadata: collectionMetadata }\n\t\t\t: { values: issues, truncated: false, metadata: collectionMetadata };\n\t}\n\n\toverride get id(): IssuesCloudHostIntegrationId.Linear {\n\t\treturn IssuesCloudHostIntegrationId.Linear;\n\t}\n\tprotected override get key(): 'linear' {\n\t\treturn 'linear';\n\t}\n\toverride get name(): string {\n\t\treturn metadata.name;\n\t}\n\toverride get domain(): string {\n\t\treturn metadata.domain;\n\t}\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;\n\t}\n\n\t/**\n\t * Account-wide \"my issues\" for Linear, drained to exhaustion and reporting whether it got there.\n\t *\n\t * The SDK read is one server-ordered query over an `or` filter across the four relationships (assigned,\n\t * created, named in an issue's content, named in a comment), so each page is 100 issues the caller has not\n\t * seen and the pages are ordered as one sequence. Draining it is therefore just following the cursor.\n\t *\n\t * Overridden rather than left to the base default because that default hardcodes `truncated: false`, and a\n\t * bounded drain that stops early is exactly the case a caller must be able to see. Three things end the\n\t * drain, and only one of them is completion:\n\t *\n\t * - the provider says there is no next page — complete;\n\t * - the backstop is reached — `truncated`, because issues beyond it exist and were not read;\n\t * - the provider claims a next page but hands back a cursor that does not advance — also `truncated`, since\n\t * the read cannot continue and the remainder is unreachable rather than absent.\n\t *\n\t * Cancellation is deliberately NOT truncation: the caller asked for the read to stop, so the partial list is\n\t * what it asked for, and flagging it would surface an incompleteness warning for a user action.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tif (resources != null) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tlet cursor = undefined;\n\t\t// Starts false so an immediate cancellation, which leaves the loop before the first response, reads as\n\t\t// \"no more pages known\" rather than as an unfinished drain.\n\t\tlet hasMore = false;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tconst issues = [];\n\t\ttry {\n\t\t\tdo {\n\t\t\t\tif (cancellation?.aborted) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst result = await api.getIssuesForCurrentUser(toTokenWithInfo(this.id, session), {\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options?.sort,\n\t\t\t\t});\n\t\t\t\trequestCount += 1;\n\t\t\t\thasMore = result.paging?.more ?? false;\n\t\t\t\tconst nextCursor = result.paging?.cursor;\n\n\t\t\t\t// Keep this page before deciding whether to continue: the request is already paid for, so\n\t\t\t\t// dropping its rows would lose real results to save nothing.\n\t\t\t\tconst formattedIssues = result.values\n\t\t\t\t\t.map(issue => toIssueShape(issue, this))\n\t\t\t\t\t.filter((result): result is IssueShape => result != null);\n\t\t\t\tif (formattedIssues.length > 0) {\n\t\t\t\t\tissues.push(...formattedIssues);\n\t\t\t\t}\n\n\t\t\t\t// The provider claims more but hands back no advancing cursor: continuing would re-read the page\n\t\t\t\t// just returned. `ProvidersApi.getPagedResult` already normalizes this into `more: false`, so\n\t\t\t\t// this is a backstop for if that ever stops holding rather than a case seen today -- but the rest\n\t\t\t\t// of the account is unreachable either way, which is what `truncated` says.\n\t\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\t\ttruncated = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcursor = nextCursor;\n\t\t\t} while (requestCount < maxAccountWidePagesPerRequest && hasMore);\n\n\t\t\t// Ran out of budget with pages still to come.\n\t\t\tif (hasMore && requestCount >= maxAccountWidePagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} catch (ex) {\n\t\t\tif (issues.length === 0) {\n\t\t\t\tthrow ex;\n\t\t\t}\n\n\t\t\t// Kept what was already fetched, so the list is real but short of the account.\n\t\t\ttruncated = true;\n\t\t\tLogger.error(ex, 'searchProviderMyIssues');\n\t\t}\n\t\treturn { values: issues, truncated: truncated };\n\t}\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst issue = await this.getRawProviderIssue(session, resource, key);\n\t\tconst autolinkableIssue: ProviderIssue | undefined = issue && {\n\t\t\t...issue,\n\t\t\turl: this.getIssueAutolinkLikeUrl(issue),\n\t\t};\n\t\treturn autolinkableIssue && toIssueShape(autolinkableIssue, this);\n\t}\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tconst result = await this.getRawProviderIssue(session, resource, id);\n\t\treturn result && fromProviderIssue(result, this);\n\t}\n\n\tprotected override async getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresourceId: string,\n\t\tid: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst result = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resourceId,\n\t\t\tnumber: id,\n\t\t});\n\t\treturn result && fromProviderIssue(result, this);\n\t}\n\n\tprivate async getRawProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<ProviderIssue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tif (!isIssueResourceDescriptor(resource)) {\n\t\t\tLogger.error(undefined, 'getProviderIssue: resource is not an IssueResourceDescriptor');\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: id,\n\t\t});\n\t}\n\tprivate getIssueAutolinkLikeUrl(issue: ProviderIssue): string | null {\n\t\tconst url = issue.url;\n\t\tif (url == null) return null;\n\n\t\tconst lastSegment = url.split('/').pop();\n\t\tif (!lastSegment || issue.number === lastSegment) {\n\t\t\treturn url;\n\t\t}\n\t\treturn url.substring(0, url.length - lastSegment.length - 1);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AA2BA,MAAM,WAAW,kBAAA;AACjB,MAAM,eAAe,OAAO,OAAO;CAAE,IAAI,SAAS;CAAI,QAAQ,SAAS;AAAO,CAAC;AAC/E,MAAM,qBAAqB;;;;;;;;;;;;;;;AAe3B,MAAM,gCAAgC;AACtC,MAAM,8BAA8B;AAYpC,IAAa,oBAAb,cAAuC,kBAAuD;;;;;;CAM7F,6BAA8B,IAAI,IAGhC;CACF,mCAAoC,IAAI,IAAuE;CAE/G,MAAe,YAAuE;EAErF,IAAI,EADc,KAAK,kBAAmB,MAAM,KAAK,YAAY,MAC/C,KAAK,YAAY,MAClC,OAAO,CAAC;EAGT,MAAM,UAAU,KAAK;EACrB,MAAM,SAAS,KAAK,WAAW,IAAI,QAAQ,WAAW;EACtD,IAAI,UAAU,MAAM,OAAO,KAAK,eAAe,OAAO;EAEtD,IAAI,KAAK,IAAI,IAAI,OAAO,WAAA,KACvB,KAAU,eAAe,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;EAEjD,OAAO,OAAO;CACf;;CAGA,eACC,SAC4D;EAC5D,MAAM,EAAE,gBAAgB;EACxB,IAAI,QAAQ,KAAK,iBAAiB,IAAI,WAAW;EACjD,IAAI,SAAS,MAAM;GAClB,QAAQ,KAAK,mBAAmB,OAAO,CAAC,CAAC,cAAc,KAAK,iBAAiB,OAAO,WAAW,CAAC;GAChG,KAAK,iBAAiB,IAAI,aAAa,KAAK;EAC7C;EACA,OAAO;CACR;CAEA,MAAc,mBACb,SAC4D;EAC5D,MAAM,aAAa,KAAK,OAAO;EAC/B,MAAM,eAAe,MAAM,KAAK,gBAAgB,OAAO;EACvD,IAAI,gBAAgB,MAAM,OAAO,CAAC;EAElC,MAAM,YAA8D,CAAC;EAErE,MAAM,QAAQ,MAAM,KAAK,SAAS,OAAO;EACzC,KAAK,MAAM,QAAQ,SAAS,CAAC,GAAG;GAC/B,MAAM,eAAe,GAAG,KAAK,IAAI;GACjC,MAAM,oBAAoB,GAAG,KAAK,IAAI;GAEtC,UAAU,KAAK;IACd,QAAQ;IACR,KAAK,GAAG,aAAa,IAAI,SAAS,aAAa;IAC/C,cAAc;IACd,YAAY;IACZ,OAAO,KAAK,EAAE,yBAAyB,GAAG,aAAa,QAAQ,aAAa,IAAI;IAEhF,MAAM;IACN,aAAa,KAAK,EAAE,iBAAiB,aAAa,MAAM,GAAG,aAAa,MAAM;IAC9E,YAAY,EAAE,GAAG,aAAa;GAC/B,CAAC;GACD,UAAU,KAAK;IACd,QAAQ;IACR,KAAK,GAAG,aAAa,IAAI,SAAS,aAAa;IAC/C,cAAc;IACd,YAAY;IACZ,eAAe;IACf,OAAO,KAAK,EAAE,yBAAyB,GAAG,aAAa,QAAQ,aAAa,IAAI;IAEhF,MAAM;IACN,aAAa,KAAK,EAAE,iBAAiB,aAAa,MAAM,GAAG,aAAa,MAAM;IAC9E,YAAY,EAAE,GAAG,aAAa;GAC/B,CAAC;EACF;EAGA,IAAI,KAAK,OAAO,eAAe,YAC9B,KAAK,WAAW,IAAI,QAAQ,aAAa;GAAa;GAAW,SAAS,KAAK,IAAI;EAAE,CAAC;EAEvF,OAAO;CACR;CAEA;CACA,MAAc,gBACb,SACA,QAAiB,OACmC;EACpD,MAAM,EAAE,gBAAgB;EACxB,KAAK,mCAAmB,IAAI,IAAsD;EAIlF,IAFwB,KAAK,eAAe,IAAI,WAE9B,KAAK,QAAQ,OAAO;GAErC,MAAM,eAAe,OAAM,MADT,KAAK,gBAAgB,EAAA,CACR,sBAAsB,gBAAgB,KAAK,IAAI,OAAO,CAAC;GACtF,MAAM,aAAuD,gBAAgB;IAC5E,IAAI,aAAa;IACjB,KAAK,aAAa;IAClB,MAAM,aAAa;IACnB,KAAK,aAAa;GACnB;GACA,IAAI,YACH,KAAK,eAAe,IAAI,aAAa,UAAU;EAEjD;EAEA,OAAO,KAAK,eAAe,IAAI,WAAW;CAC3C;;CAGA,SAA0B,IAAI,eAAuC;CACrE,MAAc,SACb,SACA,QAAiB,OAC6B;EAC9C,QAAQ,MAAM,KAAK,qBAAqB,SAAS,KAAK,EAAA,EAAI;CAC3D;CAEA,MAAc,qBACb,SACA,QAAiB,OACwD;EACzE,MAAM,EAAE,gBAAgB;EAExB,MAAM,kBAAkB,KAAK,OAAO,IAAI,WAAW;EACnD,IAAI,mBAAmB,QAAQ,CAAC,OAAO,OAAO,EAAE,QAAQ,gBAAgB;EAExE,MAAM,aAAa,KAAK,OAAO;EAG/B,MAAM,eAAkD,OADpC,MADF,KAAK,gBAAgB,EAAA,CACf,6BAA6B,gBAAgB,KAAK,IAAI,OAAO,CAAC,EAAA,EACvB,KAAI,OAAM;GACxE,IAAI,EAAE;GACN,KAAK,EAAE;GACP,MAAM,EAAE;GACR,WAAW,EAAE;EACd,EAAE;EACF,IAAI,eAAe,MAAM,OAAO,KAAA;EAMhC,IAAI,YAAY,UAAU,6BACzB,OAAO;GAAE,QAAQ;GAAa,UAAU,EAAE,cAAc,UAAU;EAAE;EAGrE,IAAI,KAAK,OAAO,IAAI,aAAa,aAAa,EAAc,WAAW,CAAC,GAEvE,KAAK,WAAW,OAAO,WAAW;EAEnC,OAAO,EAAE,QAAQ,YAAY;CAC9B;CAEA,MAAyB,4BACxB,SAC4C;EAC5C,MAAM,eAAe,MAAM,KAAK,gBAAgB,OAAO;EACvD,OAAO,gBAAgB,OAAO,CAAC,YAAY,IAAI,KAAA;CAChD;CACA,gCACC,SACA,YAC4C;EAC5C,OAAO,KAAK,SAAS,OAAO;CAC7B;CACA,MAAyB,4CACxB,SACA,YAC2D;EAC3D,OAAQ,MAAM,KAAK,qBAAqB,OAAO,KAAM,EAAE,QAAQ,CAAC,EAAE;CACnE;CAEA,4BAA2C;EAC1C,KAAK,iBAAiB,KAAA;EACtB,KAAK,OAAO,MAAM;EAClB,KAAK,WAAW,MAAM;CACvB;CACA,eAAqE;CAErE,MAAyB,8BACxB,SACA,WAC+B;EAI/B,MAAM,OAAO,OAAM,MAHD,KAAK,gBAAgB,EAAA,CAGhB,qBAAqB,gBAAgB,KAAK,IAAI,OAAO,CAAC;EAC7E,IAAI,QAAQ,MAAM,OAAO,KAAA;EAEzB,OAAO;GACN,UAAU;GACV,IAAI,KAAK;GACT,MAAM,KAAK,QAAQ,KAAK,eAAe,KAAA;GACvC,UAAU,KAAK,eAAe,KAAA;GAC9B,OAAO,KAAK,SAAS,KAAA;GACrB,WAAW,KAAA;EACZ;CACD;CAEA,MAAyB,4BACxB,SACA,SACA,SACoC;EACpC,QAAQ,MAAM,KAAK,0CAA0C,SAAS,SAAS,OAAO,EAAA,EAAI;CAC3F;CAEA,MAAyB,0CACxB,SACA,SACA,SAC0C;EAC1C,IAAI,CAAC,0BAA0B,OAAO,GAAG,OAAO,KAAA;EAEhD,MAAM,MAAM,MAAM,KAAK,gBAAgB;EAQvC,MAAM,SAAS,sBAAsB,SAAS,KAAK;EACnD,IAAI;EACJ,IAAI;EACJ,IAAI,eAAe;EACnB,IAAI,YAAY;EAChB,IAAI;EACJ,MAAM,SAAuB,CAAC;EAC9B,GAAG;GACF,IAAI;GACJ,IAAI;IACH,SAAS,MAAM,IAAI,gBAClB,gBAAgB,KAAK,IAAI,OAAO,GAChC;KAAE,OAAO,CAAC,QAAQ,EAAE;KAAW;IAAO,GACtC;KAAU;KAAQ,MAAM,SAAS;IAAK,CACvC;GACD,SAAS,IAAI;IAIZ,IAAI,OAAO,WAAW,GAAG,MAAM;IAE/B,YAAY;IACZ,qBAAqB,wBAAwB,oBAAoB;KAChE,cAAc;KACd,UAAU,CAAC,yBAAyB;MAAE,YAAY,KAAK;MAAI,WAAW,QAAQ;KAAG,GAAG,EAAE,CAAC;IACxF,CAAC;IACD;GACD;GACA,gBAAgB;GAChB,UAAU,OAAO,QAAQ,QAAQ;GACjC,MAAM,aAAa,OAAO,QAAQ;GAClC,cAAc,OAAO,QAAQ,cAAc;GAC3C,qBAAqB,wBAAwB,oBAAoB,OAAO,QAAQ;GAChF,KAAK,MAAM,SAAS,OAAO,QAAQ;IAClC,MAAM,QAAQ,aAAa,OAAO,IAAI;IACtC,IAAI,SAAS,MACZ,OAAO,KAAK,KAAK;GAEnB;GAGA,IAAI,YAAY,cAAc,QAAQ,eAAe,SAAS;IAC7D,YAAY;IACZ;GACD;GAEA,SAAS;GACT,IAAI,WAAW,gBAAgB,oBAC9B,YAAY;EAEd,SAAS,eAAe,sBAAsB;EAQ9C,IAAI,SAAS,QAAQ,MAAM;GAC1B,MAAM,YAAY,MAAM,IAAI,qBAAqB,gBAAgB,KAAK,IAAI,OAAO,CAAC,EAAA,EAAI;GAKtF,IAAI,YAAY,MACf,MAAM,IAAI,gCACT,SAAS,MACT,uDACD;GAGD,MAAM,SAAS,OAAO,QAAO,UAAS,MAAM,WAAW,MAAK,MAAK,EAAE,OAAO,QAAQ,CAAC;GACnF,OAAO,YACJ;IAAU;IAAQ,WAAW;IAAM,UAAU;IAAQ,UAAU;GAAmB,IAClF;IAAU;IAAQ,WAAW;IAAO,UAAU;GAAmB;EACrE;EAEA,OAAO,YACJ;GAAE,QAAQ;GAAQ,WAAW;GAAM,UAAU;GAAQ,UAAU;EAAmB,IAClF;GAAE,QAAQ;GAAQ,WAAW;GAAO,UAAU;EAAmB;CACrE;CAEA,IAAa,KAA0C;EACtD,OAAA;CACD;CACA,IAAuB,MAAgB;EACtC,OAAO;CACR;CACA,IAAa,OAAe;EAC3B,OAAO,SAAS;CACjB;CACA,IAAa,SAAiB;EAC7B,OAAO,SAAS;CACjB;CACA,MAAyB,uBACxB,SACA,WACA,cACoC;EACpC,QAAQ,MAAM,KAAK,qCAAqC,SAAS,WAAW,YAAY,EAAA,EAAI;CAC7F;;;;;;;;;;;;;;;;;;;;CAqBA,MAAyB,qCACxB,SACA,WACA,cACA,SAC+C;EAC/C,IAAI,aAAa,MAChB;EAGD,MAAM,MAAM,MAAM,KAAK,gBAAgB;EACvC,MAAM,SAAS,sBAAsB,SAAS,KAAK;EACnD,IAAI,SAAS,KAAA;EAGb,IAAI,UAAU;EACd,IAAI,eAAe;EACnB,IAAI,YAAY;EAChB,MAAM,SAAS,CAAC;EAChB,IAAI;GACH,GAAG;IACF,IAAI,cAAc,SACjB;IAGD,MAAM,SAAS,MAAM,IAAI,wBAAwB,gBAAgB,KAAK,IAAI,OAAO,GAAG;KAC3E;KACA;KACR,MAAM,SAAS;IAChB,CAAC;IACD,gBAAgB;IAChB,UAAU,OAAO,QAAQ,QAAQ;IACjC,MAAM,aAAa,OAAO,QAAQ;IAIlC,MAAM,kBAAkB,OAAO,OAC7B,KAAI,UAAS,aAAa,OAAO,IAAI,CAAC,CAAC,CACvC,QAAQ,WAAiC,UAAU,IAAI;IACzD,IAAI,gBAAgB,SAAS,GAC5B,OAAO,KAAK,GAAG,eAAe;IAO/B,IAAI,YAAY,cAAc,QAAQ,eAAe,SAAS;KAC7D,YAAY;KACZ;IACD;IAEA,SAAS;GACV,SAAS,eAAe,iCAAiC;GAGzD,IAAI,WAAW,gBAAgB,+BAC9B,YAAY;EAEd,SAAS,IAAI;GACZ,IAAI,OAAO,WAAW,GACrB,MAAM;GAIP,YAAY;GACZ,QAAO,MAAM,IAAI,wBAAwB;EAC1C;EACA,OAAO;GAAE,QAAQ;GAAmB;EAAU;CAC/C;CACA,MAAyB,oCACxB,SACA,UACA,EAAE,OACF,OAC0C;EAC1C,MAAM,QAAQ,MAAM,KAAK,oBAAoB,SAAS,UAAU,GAAG;EACnE,MAAM,oBAA+C,SAAS;GAC7D,GAAG;GACH,KAAK,KAAK,wBAAwB,KAAK;EACxC;EACA,OAAO,qBAAqB,aAAa,mBAAmB,IAAI;CACjE;CACA,MAAyB,iBACxB,SACA,UACA,IAC6B;EAC7B,MAAM,SAAS,MAAM,KAAK,oBAAoB,SAAS,UAAU,EAAE;EACnE,OAAO,UAAU,kBAAkB,QAAQ,IAAI;CAChD;CAEA,MAAyB,6BACxB,SACA,YACA,IACA,cAC6B;EAE7B,MAAM,SAAS,OAAM,MADH,KAAK,gBAAgB,EAAA,CACd,SAAS,gBAAgB,KAAK,IAAI,OAAO,GAAG;GACxD;GACZ,QAAQ;EACT,CAAC;EACD,OAAO,UAAU,kBAAkB,QAAQ,IAAI;CAChD;CAEA,MAAc,oBACb,SACA,UACA,IACqC;EACrC,MAAM,MAAM,MAAM,KAAK,gBAAgB;EACvC,IAAI,CAAC,0BAA0B,QAAQ,GAAG;GACzC,QAAO,MAAM,KAAA,GAAW,8DAA8D;GACtF;EACD;EAEA,OAAO,IAAI,SAAS,gBAAgB,KAAK,IAAI,OAAO,GAAG;GACtD,YAAY,SAAS;GACrB,QAAQ;EACT,CAAC;CACF;CACA,wBAAgC,OAAqC;EACpE,MAAM,MAAM,MAAM;EAClB,IAAI,OAAO,MAAM,OAAO;EAExB,MAAM,cAAc,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI;EACvC,IAAI,CAAC,eAAe,MAAM,WAAW,aACpC,OAAO;EAER,OAAO,IAAI,UAAU,GAAG,IAAI,SAAS,YAAY,SAAS,CAAC;CAC5D;AACD"}
|
|
1
|
+
{"version":3,"file":"linear.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linear.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport * as l10n from '@vscode/l10n';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor, ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.js';\nimport type { IssueEtagFields } from '../models/integration.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { LinearIssueEtagNode } from './linearIssuesEtag.js';\nimport { linearIssuesEtagMaxNumbers, toLinearIssueEtagFields } from './linearIssuesEtag.js';\nimport type { ProviderApiCollectionResult, ProviderIssue } from './models.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape, toProviderIssueStates } from './models.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.js';\nimport { mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Linear];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n/**\n * The account-wide drain's own backstop, separate from {@link maxPagesPerRequest}.\n *\n * Raised to 50 (5,000 issues at the SDK's 100-per-page) because 10 no longer means what it used to. That read\n * once fanned out over four overlapping relationship queries and merged them, so most of what a page cost was\n * rows already returned — a low budget capped the waste. It is now a single server-ordered query over an `or`\n * filter, so every page is 100 issues the caller has not seen, and the same budget just truncates real results\n * at 1,000.\n *\n * Reaching it is reported rather than silent: {@link LinearIntegration.searchProviderMyIssuesWithTruncation}\n * returns `truncated`, so the facade can say the read was incomplete instead of publishing a capped list as a\n * whole account. The bound stays — a runaway cursor must not spend requests forever — but it is now set where a\n * real Linear account is unlikely to reach it rather than where the duplication used to hurt.\n */\nconst maxAccountWidePagesPerRequest = 50;\nconst linearImplicitTeamsPageSize = 50;\n/** A team key and issue number, as Linear writes an issue's identifier (`ENG-123`). */\nconst linearIdentifier = /^([A-Za-z0-9]+)-(\\d+)$/;\n\nexport interface LinearTeamDescriptor extends IssueResourceDescriptor {\n\tavatarUrl: string | undefined;\n}\n\nexport interface LinearOrganizationDescriptor extends IssueResourceDescriptor {\n\turl: string;\n}\n\nexport interface LinearProjectDescriptor extends IssueResourceDescriptor {}\n\nexport class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Linear> {\n\t/**\n\t * Built from the teams, so it follows them: rebuilt when the team list changes or the caches are dropped, and once\n\t * older than {@link discoveryCacheTtl}. An expired set is still served while it is rebuilt in the background, because\n\t * autolinks are read on every render of a commit message and must not wait on the network (#5907).\n\t */\n\tprivate readonly _autolinks = new Map<\n\t\tstring,\n\t\t{ autolinks: (AutolinkReference | DynamicAutolinkReference)[]; builtAt: number }\n\t>();\n\tprivate readonly _autolinksBuilds = new Map<string, Promise<(AutolinkReference | DynamicAutolinkReference)[]>>();\n\n\toverride async autolinks(): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected || this._session == null) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst session = this._session;\n\t\tconst cached = this._autolinks.get(session.accessToken);\n\t\tif (cached == null) return this.buildAutolinks(session);\n\n\t\tif (Date.now() - cached.builtAt >= discoveryCacheTtl) {\n\t\t\tvoid this.buildAutolinks(session).catch(() => {});\n\t\t}\n\t\treturn cached.autolinks;\n\t}\n\n\t/** One build per token at a time: concurrent renders share it rather than each re-reading the teams. */\n\tprivate buildAutolinks(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tlet build = this._autolinksBuilds.get(accessToken);\n\t\tif (build == null) {\n\t\t\tbuild = this.buildAutolinksCore(session).finally(() => this._autolinksBuilds.delete(accessToken));\n\t\t\tthis._autolinksBuilds.set(accessToken, build);\n\t\t}\n\t\treturn build;\n\t}\n\n\tprivate async buildAutolinksCore(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst generation = this._teams.generation;\n\t\tconst organization = await this.getOrganization(session);\n\t\tif (organization == null) return [];\n\n\t\tconst autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];\n\n\t\tconst teams = await this.getTeams(session);\n\t\tfor (const team of teams ?? []) {\n\t\t\tconst dashedPrefix = `${team.key}-`;\n\t\t\tconst underscoredPrefix = `${team.key}_`;\n\n\t\t\tautolinks.push({\n\t\t\t\tprefix: dashedPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t\tautolinks.push({\n\t\t\t\tprefix: underscoredPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\treferenceType: 'branch',\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t}\n\n\t\t// Not kept when built from teams read before the caches were dropped.\n\t\tif (this._teams.generation === generation) {\n\t\t\tthis._autolinks.set(session.accessToken, { autolinks: autolinks, builtAt: Date.now() });\n\t\t}\n\t\treturn autolinks;\n\t}\n\n\tprivate _organizations: Map<string, LinearOrganizationDescriptor | undefined> | undefined;\n\tprivate async getOrganization(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearOrganizationDescriptor | undefined> {\n\t\tconst { accessToken } = session;\n\t\tthis._organizations ??= new Map<string, LinearOrganizationDescriptor | undefined>();\n\n\t\tconst cachedResources = this._organizations.get(accessToken);\n\n\t\tif (cachedResources == null || force) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst organization = await api.getLinearOrganization(toTokenWithInfo(this.id, session));\n\t\t\tconst descriptor: LinearOrganizationDescriptor | undefined = organization && {\n\t\t\t\tid: organization.id,\n\t\t\t\tkey: organization.key,\n\t\t\t\tname: organization.name,\n\t\t\t\turl: organization.url,\n\t\t\t};\n\t\t\tif (descriptor) {\n\t\t\t\tthis._organizations.set(accessToken, descriptor);\n\t\t\t}\n\t\t}\n\n\t\treturn this._organizations.get(accessToken);\n\t}\n\n\t/** Expires, and is dropped on a re-sync, so a team created or left mid-session is picked up (#5907). */\n\tprivate readonly _teams = new DiscoveryCache<LinearTeamDescriptor[]>();\n\tprivate async getTeams(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearTeamDescriptor[] | undefined> {\n\t\treturn (await this.getTeamsWithMetadata(session, force))?.values;\n\t}\n\n\tprivate async getTeamsWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<ProviderApiCollectionResult<LinearTeamDescriptor> | undefined> {\n\t\tconst { accessToken } = session;\n\n\t\tconst cachedResources = this._teams.get(accessToken);\n\t\tif (cachedResources != null && !force) return { values: cachedResources };\n\n\t\tconst generation = this._teams.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst teams = await api.getLinearTeamsForCurrentUser(toTokenWithInfo(this.id, session));\n\t\tconst descriptors: LinearTeamDescriptor[] | undefined = teams?.map(t => ({\n\t\t\tid: t.id,\n\t\t\tkey: t.key,\n\t\t\tname: t.name,\n\t\t\tavatarUrl: t.iconUrl,\n\t\t}));\n\t\tif (descriptors == null) return undefined;\n\n\t\t// provider-apis currently requests Linear's teams connection without pageInfo or a cursor. Linear's\n\t\t// implicit page size is 50, so exactly a full page cannot prove there is no team 51. Preserve the useful\n\t\t// prefix, but mark it unknown and leave it uncached so the next read retries instead of treating it as\n\t\t// authoritative. Once provider-apis exposes a paged primitive, replace this fail-closed guard with a drain.\n\t\tif (descriptors.length >= linearImplicitTeamsPageSize) {\n\t\t\treturn { values: descriptors, metadata: { completeness: 'unknown' } };\n\t\t}\n\n\t\tif (this._teams.set(accessToken, descriptors, { generation: generation })) {\n\t\t\t// Built from the teams; a new set means new prefixes.\n\t\t\tthis._autolinks.delete(accessToken);\n\t\t}\n\t\treturn { values: descriptors };\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\tconst organization = await this.getOrganization(session);\n\t\treturn organization != null ? [organization] : undefined;\n\t}\n\tprotected override getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\treturn this.getTeams(session);\n\t}\n\tprotected override async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ProviderApiCollectionResult<ResourceDescriptor>> {\n\t\treturn (await this.getTeamsWithMetadata(session)) ?? { values: [] };\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._organizations = undefined;\n\t\tthis._teams.clear();\n\t\tthis._autolinks.clear();\n\t}\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\t// Linear's viewer isn't a ProviderAccount (no username/avatar), so build the Account manually\n\t\t// (Trello-style) from the fields the viewer query returns.\n\t\tconst user = await api.getLinearCurrentUser(toTokenWithInfo(this.id, session));\n\t\tif (user == null) return undefined;\n\n\t\treturn {\n\t\t\tprovider: this,\n\t\t\tid: user.id,\n\t\t\tname: user.name ?? user.displayName ?? undefined,\n\t\t\tusername: user.displayName ?? undefined,\n\t\t\temail: user.email ?? undefined,\n\t\t\tavatarUrl: undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\tconst api = await this.getProvidersApi();\n\t\t// `getProviderProjectsForResources` returns Linear teams, so `project.id` is a team id here. Drain the\n\t\t// team's issues (Linear pages by cursor); bounded by maxPagesPerRequest as a backstop. `truncated` is\n\t\t// set when that backstop stopped the drain with more pages still available.\n\t\t//\n\t\t// The state is narrowed server-side, not after the drain: a team's completed and canceled issues would\n\t\t// otherwise count against the backstop, so a team with enough done work runs out of pages before its open\n\t\t// issues, and the viewer filter below would then hand back a page that looks complete.\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tlet cursor: string | undefined;\n\t\tlet hasMore: boolean;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tlet collectionMetadata: CollectionMetadata | undefined;\n\t\tconst issues: IssueShape[] = [];\n\t\tdo {\n\t\t\tlet result: Awaited<ReturnType<typeof api.getLinearIssues>>;\n\t\t\ttry {\n\t\t\t\tresult = await api.getLinearIssues(\n\t\t\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\t\t\t{ teams: [project.id], states: states },\n\t\t\t\t\t{ cursor: cursor, sort: options?.sort },\n\t\t\t\t);\n\t\t\t} catch (ex) {\n\t\t\t\t// A page failure after the first page leaves the already-drained prefix intact; record the\n\t\t\t\t// failure at the project scope instead of re-throwing and discarding the prefix. If nothing was\n\t\t\t\t// fetched yet, preserve the original throw behavior so the caller sees a hard error.\n\t\t\t\tif (issues.length === 0) throw ex;\n\n\t\t\t\ttruncated = true;\n\t\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, {\n\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\tfailures: [toCollectionScopeFailure({ providerId: this.id, projectId: project.id }, ex)],\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trequestCount += 1;\n\t\t\thasMore = result.paging?.more ?? false;\n\t\t\tconst nextCursor = result.paging?.cursor;\n\t\t\ttruncated ||= result.paging?.truncated === true;\n\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, result.metadata);\n\t\t\tfor (const issue of result.values) {\n\t\t\t\tconst shape = toIssueShape(issue, this, { projection: 'project' });\n\t\t\t\tif (shape != null) {\n\t\t\t\t\tissues.push(shape);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// The provider claims more but returns no advancing cursor: we can't continue without re-reading the\n\t\t\t// same page, so the drain is incomplete — flag it rather than silently stopping.\n\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcursor = nextCursor;\n\t\t\tif (hasMore && requestCount >= maxPagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} while (requestCount < maxPagesPerRequest && hasMore);\n\n\t\t// Linear's issue list has no server-side author/assignee filter, so scope to the current user\n\t\t// client-side when a user was requested (the assignee filter is what \"my issues\" means here).\n\t\t// Match on the viewer's stable id, not the passed display name: Linear's `name` (full name) and\n\t\t// `displayName` (nickname) are distinct fields, and assignees are normalized with `.name` = the full\n\t\t// name while the caller's `user` is the displayName — so a name string can miss. The assignee `.id`\n\t\t// is the Linear user id, which is unambiguous.\n\t\tif (options?.user != null) {\n\t\t\tconst viewerId = (await api.getLinearCurrentUser(toTokenWithInfo(this.id, session)))?.id;\n\t\t\t// If the viewer can't be resolved we can't scope to \"my issues\" — returning the unfiltered team\n\t\t\t// issues would leak everyone else's, and returning [] is indistinguishable from \"no issues assigned\n\t\t\t// to me\". Throw so the facade (getIssuesForProjectResult → runCaptured) surfaces a warning +\n\t\t\t// fetchFailed the caller can act on, instead of a silent empty.\n\t\t\tif (viewerId == null) {\n\t\t\t\tthrow new IntegrationReadUnavailableError(\n\t\t\t\t\tmetadata.name,\n\t\t\t\t\t'could not resolve the current user to scope issues to',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst values = issues.filter(issue => issue.assignees?.some(a => a.id === viewerId));\n\t\t\treturn truncated\n\t\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: collectionMetadata }\n\t\t\t\t: { values: values, truncated: false, metadata: collectionMetadata };\n\t\t}\n\n\t\treturn truncated\n\t\t\t? { values: issues, truncated: true, recovery: 'none', metadata: collectionMetadata }\n\t\t\t: { values: issues, truncated: false, metadata: collectionMetadata };\n\t}\n\n\toverride get id(): IssuesCloudHostIntegrationId.Linear {\n\t\treturn IssuesCloudHostIntegrationId.Linear;\n\t}\n\tprotected override get key(): 'linear' {\n\t\treturn 'linear';\n\t}\n\toverride get name(): string {\n\t\treturn metadata.name;\n\t}\n\toverride get domain(): string {\n\t\treturn metadata.domain;\n\t}\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;\n\t}\n\n\t/**\n\t * Account-wide \"my issues\" for Linear, drained to exhaustion and reporting whether it got there.\n\t *\n\t * The SDK read is one server-ordered query over an `or` filter across the four relationships (assigned,\n\t * created, named in an issue's content, named in a comment), so each page is 100 issues the caller has not\n\t * seen and the pages are ordered as one sequence. Draining it is therefore just following the cursor.\n\t *\n\t * Overridden rather than left to the base default because that default hardcodes `truncated: false`, and a\n\t * bounded drain that stops early is exactly the case a caller must be able to see. Three things end the\n\t * drain, and only one of them is completion:\n\t *\n\t * - the provider says there is no next page — complete;\n\t * - the backstop is reached — `truncated`, because issues beyond it exist and were not read;\n\t * - the provider claims a next page but hands back a cursor that does not advance — also `truncated`, since\n\t * the read cannot continue and the remainder is unreachable rather than absent.\n\t *\n\t * Cancellation is deliberately NOT truncation: the caller asked for the read to stop, so the partial list is\n\t * what it asked for, and flagging it would surface an incompleteness warning for a user action.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tif (resources != null) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tlet cursor = undefined;\n\t\t// Starts false so an immediate cancellation, which leaves the loop before the first response, reads as\n\t\t// \"no more pages known\" rather than as an unfinished drain.\n\t\tlet hasMore = false;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tconst issues = [];\n\t\ttry {\n\t\t\tdo {\n\t\t\t\tif (cancellation?.aborted) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst result = await api.getIssuesForCurrentUser(toTokenWithInfo(this.id, session), {\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options?.sort,\n\t\t\t\t});\n\t\t\t\trequestCount += 1;\n\t\t\t\thasMore = result.paging?.more ?? false;\n\t\t\t\tconst nextCursor = result.paging?.cursor;\n\n\t\t\t\t// Keep this page before deciding whether to continue: the request is already paid for, so\n\t\t\t\t// dropping its rows would lose real results to save nothing.\n\t\t\t\tconst formattedIssues = result.values\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t.filter((result): result is IssueShape => result != null);\n\t\t\t\tif (formattedIssues.length > 0) {\n\t\t\t\t\tissues.push(...formattedIssues);\n\t\t\t\t}\n\n\t\t\t\t// The provider claims more but hands back no advancing cursor: continuing would re-read the page\n\t\t\t\t// just returned. `ProvidersApi.getPagedResult` already normalizes this into `more: false`, so\n\t\t\t\t// this is a backstop for if that ever stops holding rather than a case seen today -- but the rest\n\t\t\t\t// of the account is unreachable either way, which is what `truncated` says.\n\t\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\t\ttruncated = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcursor = nextCursor;\n\t\t\t} while (requestCount < maxAccountWidePagesPerRequest && hasMore);\n\n\t\t\t// Ran out of budget with pages still to come.\n\t\t\tif (hasMore && requestCount >= maxAccountWidePagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} catch (ex) {\n\t\t\tif (issues.length === 0) {\n\t\t\t\tthrow ex;\n\t\t\t}\n\n\t\t\t// Kept what was already fetched, so the list is real but short of the account.\n\t\t\ttruncated = true;\n\t\t\tLogger.error(ex, 'searchProviderMyIssues');\n\t\t}\n\t\treturn { values: issues, truncated: truncated };\n\t}\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst issue = await this.getRawProviderIssue(session, resource, key);\n\t\tconst autolinkableIssue: ProviderIssue | undefined = issue && {\n\t\t\t...issue,\n\t\t\turl: this.getIssueAutolinkLikeUrl(issue),\n\t\t};\n\t\treturn autolinkableIssue && toIssueShape(autolinkableIssue, this, { projection: 'point' });\n\t}\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tconst result = await this.getRawProviderIssue(session, resource, id);\n\t\treturn result && fromProviderIssue(result, this, { projection: 'point' });\n\t}\n\n\tprotected override async getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresourceId: string,\n\t\tid: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst result = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resourceId,\n\t\t\tnumber: id,\n\t\t});\n\t\treturn result && fromProviderIssue(result, this, { projection: 'batch' });\n\t}\n\n\t/**\n\t * The cheap check behind the batch issue read's etags: one `issues` query per team and\n\t * {@link linearIssuesEtagMaxNumbers} distinct numbers, where {@link getProviderIssueByResourceId} sends one request\n\t * per issue. A request that throws rejects only its own targets' slots, with its error classified as the full\n\t * read's would be.\n\t *\n\t * Never proves an absence. The query filters by the team's CURRENT key and asks for archived issues too, but the\n\t * full read's `issue(id:)` may also resolve an identifier the issue no longer carries (its team was renamed, or it\n\t * moved to another team), so an issue the query leaves out may still exist. Its target's slot is rejected, as is a\n\t * target whose identifier isn't a team key and a number, so the full read decides. When no target was answered and\n\t * nothing failed, the check declines instead, so a batch of only such targets costs a full read, not a failure.\n\t */\n\tprotected override async getProviderIssuesEtagFieldsByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t): Promise<PromiseSettledResult<IssueEtagFields | undefined>[] | undefined> {\n\t\tconst slots = new Array<PromiseSettledResult<IssueEtagFields | undefined>>(targets.length);\n\t\tlet answered = false;\n\t\tlet failed = false;\n\n\t\t// Each workspace and team's distinct numbers, with the targets asking for each.\n\t\tconst byTeam = new Map<string, { teamKey: string; numbers: Map<number, number[]> }>();\n\t\tfor (const [index, target] of targets.entries()) {\n\t\t\tconst match = linearIdentifier.exec(target.identifier);\n\t\t\tconst number = match != null ? Number(match[2]) : Number.NaN;\n\t\t\t// Only a number written as Linear writes it, so `ENG-012` never answers for `ENG-12`.\n\t\t\tif (match == null || !Number.isSafeInteger(number) || String(number) !== match[2]) {\n\t\t\t\tslots[index] = {\n\t\t\t\t\tstatus: 'rejected',\n\t\t\t\t\treason: new Error(`Not a Linear issue identifier: ${target.identifier}`),\n\t\t\t\t};\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst teamKey = match[1].toUpperCase();\n\t\t\tconst groupKey = `${target.resourceId}\\n${teamKey}`;\n\t\t\tlet group = byTeam.get(groupKey);\n\t\t\tif (group == null) {\n\t\t\t\tgroup = { teamKey: teamKey, numbers: new Map() };\n\t\t\t\tbyTeam.set(groupKey, group);\n\t\t\t}\n\n\t\t\tlet indices = group.numbers.get(number);\n\t\t\tif (indices == null) {\n\t\t\t\tindices = [];\n\t\t\t\tgroup.numbers.set(number, indices);\n\t\t\t}\n\t\t\tindices.push(index);\n\t\t}\n\n\t\tconst requests = [...byTeam.values()].flatMap(({ teamKey, numbers }) =>\n\t\t\tchunk([...numbers], linearIssuesEtagMaxNumbers).map(entries => ({ teamKey: teamKey, entries: entries })),\n\t\t);\n\t\tif (requests.length) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\t\tconst answers = await mapSettledBounded(requests, providerFanOutConcurrency, r =>\n\t\t\t\tapi.getLinearIssuesEtagFields(\n\t\t\t\t\ttokenWithInfo,\n\t\t\t\t\tr.teamKey,\n\t\t\t\t\tr.entries.map(([number]) => number),\n\t\t\t\t),\n\t\t\t);\n\n\t\t\tfor (const [i, answer] of answers.entries()) {\n\t\t\t\tconst { teamKey, entries } = requests[i];\n\t\t\t\tif (answer.status === 'rejected') {\n\t\t\t\t\tfailed = true;\n\t\t\t\t\tfor (const [, indices] of entries) {\n\t\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\t\tslots[index] = answer;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// By number, never by position: Linear returns the issues in its own order, and only the ones it found.\n\t\t\t\tconst byNumber = new Map<number, LinearIssueEtagNode>();\n\t\t\t\tfor (const issue of answer.value) {\n\t\t\t\t\tbyNumber.set(issue.number, issue);\n\t\t\t\t}\n\n\t\t\t\tfor (const [number, indices] of entries) {\n\t\t\t\t\tconst issue = byNumber.get(number);\n\t\t\t\t\tlet slot: PromiseSettledResult<IssueEtagFields | undefined>;\n\t\t\t\t\tif (issue == null) {\n\t\t\t\t\t\tslot = { status: 'rejected', reason: new Error(`Linear did not return ${teamKey}-${number}`) };\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tslot = { status: 'fulfilled', value: toLinearIssueEtagFields(issue) };\n\t\t\t\t\t\t\tanswered = true;\n\t\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\t\tslot = { status: 'rejected', reason: ex };\n\t\t\t\t\t\t\tfailed = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\tslots[index] = slot;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn answered || failed ? slots : undefined;\n\t}\n\n\tprivate async getRawProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<ProviderIssue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tif (!isIssueResourceDescriptor(resource)) {\n\t\t\tLogger.error(undefined, 'getProviderIssue: resource is not an IssueResourceDescriptor');\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: id,\n\t\t});\n\t}\n\tprivate getIssueAutolinkLikeUrl(issue: ProviderIssue): string | null {\n\t\tconst url = issue.url;\n\t\tif (url == null) return null;\n\n\t\tconst lastSegment = url.split('/').pop();\n\t\tif (!lastSegment || issue.number === lastSegment) {\n\t\t\treturn url;\n\t\t}\n\t\treturn url.substring(0, url.length - lastSegment.length - 1);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ProviderRequestFunction } from "./models.js";
|
|
2
|
+
import { IssueEtagFields } from "../models/integration.js";
|
|
3
|
+
//#region ../plus/integrations/src/providers/linearIssuesEtag.d.ts
|
|
4
|
+
/** The most issue numbers one cheap check asks of one team. */
|
|
5
|
+
export declare const linearIssuesEtagMaxNumbers = 50;
|
|
6
|
+
export type LinearIssueEtagNode = {
|
|
7
|
+
identifier: string;
|
|
8
|
+
number: number;
|
|
9
|
+
updatedAt?: string | null;
|
|
10
|
+
archivedAt?: string | null;
|
|
11
|
+
state?: {
|
|
12
|
+
type?: string | null;
|
|
13
|
+
} | null;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The cheap check behind the batch issue read's etags: the change state of up to {@link linearIssuesEtagMaxNumbers}
|
|
17
|
+
* issues of one team in ONE GraphQL request, where the full read sends provider-apis' `issue(id:)` once per issue.
|
|
18
|
+
* Linear returns only the issues it found, in its own order, so a caller matches them by number, never by position.
|
|
19
|
+
*
|
|
20
|
+
* Sent as provider-apis sends every Linear request: the token as the whole `Authorization` header, with no scheme.
|
|
21
|
+
* GraphQL `errors` reject the whole request, as provider-apis rejects them, but keep the response so a throttling
|
|
22
|
+
* code is still classified as one. A next page also rejects it: with every number unique within a team, more
|
|
23
|
+
* issues than were asked for means the filter was not applied as written.
|
|
24
|
+
*/
|
|
25
|
+
export declare function requestLinearIssuesEtagFields(request: ProviderRequestFunction, accessToken: string, teamKey: string, numbers: readonly number[]): Promise<LinearIssueEtagNode[]>;
|
|
26
|
+
/**
|
|
27
|
+
* A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'
|
|
28
|
+
* Linear issue normalizer (0.61.0) then `fromProviderIssue`, so this mirrors both:
|
|
29
|
+
*
|
|
30
|
+
* - provider-apis maps `state.type` to a category — `completed` and `canceled` to `DONE`, `started` to `IN_PROGRESS`,
|
|
31
|
+
* anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none — and sets `closedDate` from
|
|
32
|
+
* `archivedAt` (a falsy value reads as none);
|
|
33
|
+
* - `fromProviderIssue` closes an issue that has a `closedDate` or a `DONE` category, so an archived issue is closed
|
|
34
|
+
* whatever its state.
|
|
35
|
+
*
|
|
36
|
+
* Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an
|
|
37
|
+
* etag the full row would compute from a fallback date (`fromProviderIssue` falls back to `closedDate`, then
|
|
38
|
+
* `createdDate`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function toLinearIssueEtagFields(issue: LinearIssueEtagNode): IssueEtagFields;
|
|
41
|
+
//#endregion
|
|
42
|
+
//# sourceMappingURL=linearIssuesEtag.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region ../plus/integrations/src/providers/linearIssuesEtag.ts
|
|
2
|
+
const linearGraphQLUrl = "https://api.linear.app/graphql";
|
|
3
|
+
/** The most issue numbers one cheap check asks of one team. */
|
|
4
|
+
const linearIssuesEtagMaxNumbers = 50;
|
|
5
|
+
/**
|
|
6
|
+
* Archived issues are left out of Linear's `issues` connection unless asked for, so `includeArchived` keeps an archived
|
|
7
|
+
* issue answerable here as the full read's `issue(id:)` answers it.
|
|
8
|
+
*/
|
|
9
|
+
const linearIssuesEtagQuery = `
|
|
10
|
+
query GetIssuesEtagFields($teamKey: String!, $numbers: [Float!]!) {
|
|
11
|
+
issues(
|
|
12
|
+
first: 50
|
|
13
|
+
includeArchived: true
|
|
14
|
+
filter: { team: { key: { eq: $teamKey } }, number: { in: $numbers } }
|
|
15
|
+
) {
|
|
16
|
+
pageInfo {
|
|
17
|
+
hasNextPage
|
|
18
|
+
}
|
|
19
|
+
nodes {
|
|
20
|
+
identifier
|
|
21
|
+
number
|
|
22
|
+
updatedAt
|
|
23
|
+
archivedAt
|
|
24
|
+
state {
|
|
25
|
+
type
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
/**
|
|
32
|
+
* The cheap check behind the batch issue read's etags: the change state of up to {@link linearIssuesEtagMaxNumbers}
|
|
33
|
+
* issues of one team in ONE GraphQL request, where the full read sends provider-apis' `issue(id:)` once per issue.
|
|
34
|
+
* Linear returns only the issues it found, in its own order, so a caller matches them by number, never by position.
|
|
35
|
+
*
|
|
36
|
+
* Sent as provider-apis sends every Linear request: the token as the whole `Authorization` header, with no scheme.
|
|
37
|
+
* GraphQL `errors` reject the whole request, as provider-apis rejects them, but keep the response so a throttling
|
|
38
|
+
* code is still classified as one. A next page also rejects it: with every number unique within a team, more
|
|
39
|
+
* issues than were asked for means the filter was not applied as written.
|
|
40
|
+
*/
|
|
41
|
+
async function requestLinearIssuesEtagFields(request, accessToken, teamKey, numbers) {
|
|
42
|
+
const response = await request({
|
|
43
|
+
url: linearGraphQLUrl,
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: accessToken,
|
|
47
|
+
"Content-Type": "application/json"
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
query: linearIssuesEtagQuery,
|
|
51
|
+
variables: {
|
|
52
|
+
teamKey,
|
|
53
|
+
numbers
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
const body = response.body;
|
|
58
|
+
if (body?.errors != null) {
|
|
59
|
+
const error = /* @__PURE__ */ new Error(`Linear GraphQL errors: ${JSON.stringify(body.errors)}`);
|
|
60
|
+
Object.assign(error, { response });
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const issues = body?.data?.issues;
|
|
64
|
+
if (!Array.isArray(issues?.nodes)) throw new Error("Linear returned no issues");
|
|
65
|
+
if (issues.pageInfo?.hasNextPage !== false) throw new Error("Linear returned more issues than were asked for");
|
|
66
|
+
return issues.nodes;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'
|
|
70
|
+
* Linear issue normalizer (0.61.0) then `fromProviderIssue`, so this mirrors both:
|
|
71
|
+
*
|
|
72
|
+
* - provider-apis maps `state.type` to a category — `completed` and `canceled` to `DONE`, `started` to `IN_PROGRESS`,
|
|
73
|
+
* anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none — and sets `closedDate` from
|
|
74
|
+
* `archivedAt` (a falsy value reads as none);
|
|
75
|
+
* - `fromProviderIssue` closes an issue that has a `closedDate` or a `DONE` category, so an archived issue is closed
|
|
76
|
+
* whatever its state.
|
|
77
|
+
*
|
|
78
|
+
* Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an
|
|
79
|
+
* etag the full row would compute from a fallback date (`fromProviderIssue` falls back to `closedDate`, then
|
|
80
|
+
* `createdDate`).
|
|
81
|
+
*/
|
|
82
|
+
function toLinearIssueEtagFields(issue) {
|
|
83
|
+
const updatedDate = new Date(issue.updatedAt || NaN);
|
|
84
|
+
if (Number.isNaN(updatedDate.getTime())) throw new Error(`Linear returned no update time for ${issue.identifier}`);
|
|
85
|
+
const type = issue.state?.type;
|
|
86
|
+
return {
|
|
87
|
+
state: Boolean(issue.archivedAt) || type === "completed" || type === "canceled" ? "closed" : "opened",
|
|
88
|
+
updatedDate
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { linearIssuesEtagMaxNumbers, requestLinearIssuesEtagFields, toLinearIssueEtagFields };
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=linearIssuesEtag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linearIssuesEtag.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linearIssuesEtag.ts"],"sourcesContent":["import type { IssueEtagFields } from '../models/integration.js';\nimport type { ProviderRequestFunction } from './models.js';\n\nconst linearGraphQLUrl = 'https://api.linear.app/graphql';\n\n/** The most issue numbers one cheap check asks of one team. */\nexport const linearIssuesEtagMaxNumbers = 50;\n\n/**\n * Archived issues are left out of Linear's `issues` connection unless asked for, so `includeArchived` keeps an archived\n * issue answerable here as the full read's `issue(id:)` answers it.\n */\nconst linearIssuesEtagQuery = `\n\tquery GetIssuesEtagFields($teamKey: String!, $numbers: [Float!]!) {\n\t\tissues(\n\t\t\tfirst: ${linearIssuesEtagMaxNumbers}\n\t\t\tincludeArchived: true\n\t\t\tfilter: { team: { key: { eq: $teamKey } }, number: { in: $numbers } }\n\t\t) {\n\t\t\tpageInfo {\n\t\t\t\thasNextPage\n\t\t\t}\n\t\t\tnodes {\n\t\t\t\tidentifier\n\t\t\t\tnumber\n\t\t\t\tupdatedAt\n\t\t\t\tarchivedAt\n\t\t\t\tstate {\n\t\t\t\t\ttype\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n`;\n\nexport type LinearIssueEtagNode = {\n\tidentifier: string;\n\tnumber: number;\n\tupdatedAt?: string | null;\n\tarchivedAt?: string | null;\n\tstate?: { type?: string | null } | null;\n};\n\ntype LinearIssuesEtagResponse = {\n\tdata?: {\n\t\tissues?: { pageInfo?: { hasNextPage?: boolean }; nodes?: LinearIssueEtagNode[] } | null;\n\t} | null;\n\terrors?: unknown[];\n};\n\n/**\n * The cheap check behind the batch issue read's etags: the change state of up to {@link linearIssuesEtagMaxNumbers}\n * issues of one team in ONE GraphQL request, where the full read sends provider-apis' `issue(id:)` once per issue.\n * Linear returns only the issues it found, in its own order, so a caller matches them by number, never by position.\n *\n * Sent as provider-apis sends every Linear request: the token as the whole `Authorization` header, with no scheme.\n * GraphQL `errors` reject the whole request, as provider-apis rejects them, but keep the response so a throttling\n * code is still classified as one. A next page also rejects it: with every number unique within a team, more\n * issues than were asked for means the filter was not applied as written.\n */\nexport async function requestLinearIssuesEtagFields(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tteamKey: string,\n\tnumbers: readonly number[],\n): Promise<LinearIssueEtagNode[]> {\n\tconst response = await request<LinearIssuesEtagResponse | null>({\n\t\turl: linearGraphQLUrl,\n\t\tmethod: 'POST',\n\t\theaders: { Authorization: accessToken, 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ query: linearIssuesEtagQuery, variables: { teamKey: teamKey, numbers: numbers } }),\n\t});\n\n\tconst body = response.body;\n\tif (body?.errors != null) {\n\t\tconst error = new Error(`Linear GraphQL errors: ${JSON.stringify(body.errors)}`);\n\t\tObject.assign(error, { response: response });\n\t\tthrow error;\n\t}\n\n\tconst issues = body?.data?.issues;\n\tif (!Array.isArray(issues?.nodes)) throw new Error('Linear returned no issues');\n\tif (issues.pageInfo?.hasNextPage !== false) throw new Error('Linear returned more issues than were asked for');\n\n\treturn issues.nodes;\n}\n\n/**\n * A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'\n * Linear issue normalizer (0.61.0) then `fromProviderIssue`, so this mirrors both:\n *\n * - provider-apis maps `state.type` to a category — `completed` and `canceled` to `DONE`, `started` to `IN_PROGRESS`,\n * anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none — and sets `closedDate` from\n * `archivedAt` (a falsy value reads as none);\n * - `fromProviderIssue` closes an issue that has a `closedDate` or a `DONE` category, so an archived issue is closed\n * whatever its state.\n *\n * Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an\n * etag the full row would compute from a fallback date (`fromProviderIssue` falls back to `closedDate`, then\n * `createdDate`).\n */\nexport function toLinearIssueEtagFields(issue: LinearIssueEtagNode): IssueEtagFields {\n\tconst updatedDate = new Date(issue.updatedAt || Number.NaN);\n\tif (Number.isNaN(updatedDate.getTime())) {\n\t\tthrow new Error(`Linear returned no update time for ${issue.identifier}`);\n\t}\n\n\tconst type = issue.state?.type;\n\tconst closed = Boolean(issue.archivedAt) || type === 'completed' || type === 'canceled';\n\treturn { state: closed ? 'closed' : 'opened', updatedDate: updatedDate };\n}\n"],"mappings":";AAGA,MAAM,mBAAmB;;AAGzB,MAAa,6BAA6B;;;;;AAM1C,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgD9B,eAAsB,8BACrB,SACA,aACA,SACA,SACiC;CACjC,MAAM,WAAW,MAAM,QAAyC;EAC/D,KAAK;EACL,QAAQ;EACR,SAAS;GAAE,eAAe;GAAa,gBAAgB;EAAmB;EAC1E,MAAM,KAAK,UAAU;GAAE,OAAO;GAAuB,WAAW;IAAW;IAAkB;GAAQ;EAAE,CAAC;CACzG,CAAC;CAED,MAAM,OAAO,SAAS;CACtB,IAAI,MAAM,UAAU,MAAM;EACzB,MAAM,wBAAQ,IAAI,MAAM,0BAA0B,KAAK,UAAU,KAAK,MAAM,GAAG;EAC/E,OAAO,OAAO,OAAO,EAAY,SAAS,CAAC;EAC3C,MAAM;CACP;CAEA,MAAM,SAAS,MAAM,MAAM;CAC3B,IAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,GAAG,MAAM,IAAI,MAAM,2BAA2B;CAC9E,IAAI,OAAO,UAAU,gBAAgB,OAAO,MAAM,IAAI,MAAM,iDAAiD;CAE7G,OAAO,OAAO;AACf;;;;;;;;;;;;;;;AAgBA,SAAgB,wBAAwB,OAA6C;CACpF,MAAM,cAAc,IAAI,KAAK,MAAM,aAAa,GAAU;CAC1D,IAAI,OAAO,MAAM,YAAY,QAAQ,CAAC,GACrC,MAAM,IAAI,MAAM,sCAAsC,MAAM,YAAY;CAGzE,MAAM,OAAO,MAAM,OAAO;CAE1B,OAAO;EAAE,OADM,QAAQ,MAAM,UAAU,KAAK,SAAS,eAAe,SAAS,aACpD,WAAW;EAAuB;CAAY;AACxE"}
|
|
@@ -2,15 +2,15 @@ import { PagedResult } from "../../../utils/paging.js";
|
|
|
2
2
|
import { ProviderScope } from "../../../git/models/resourceDescriptor.js";
|
|
3
3
|
import { Provider, ProviderReference } from "../../../git/models/remoteProvider.js";
|
|
4
4
|
import { IssueOrPullRequestState } from "../../../git/models/issueOrPullRequest.js";
|
|
5
|
-
import { Issue as Issue$1, IssueProject, IssueSearchCapabilities, IssueSearchCriteria, IssueSearchRelationship, IssueShape, IssueSortField, IssueSorting, IssueStateFilter } from "../../../git/models/issue.js";
|
|
6
|
-
import { PullRequest, PullRequestFilter, PullRequestMergeableState, PullRequestRefs, PullRequestRepositoryIdentityDescriptor, PullRequestSearchCapabilities, PullRequestSearchCriteria, PullRequestSortField, PullRequestSorting, PullRequestStateFilter, PullRequestStatusCheckRollupState } from "../../../git/models/pullRequest.js";
|
|
5
|
+
import { Issue as Issue$1, IssueProject, IssueProjection, IssueSearchCapabilities, IssueSearchCriteria, IssueSearchRelationship, IssueShape, IssueSortField, IssueSorting, IssueStateFilter } from "../../../git/models/issue.js";
|
|
6
|
+
import { PullRequest, PullRequestFilter, PullRequestMergeableState, PullRequestProjection, PullRequestRefs, PullRequestRepositoryIdentityDescriptor, PullRequestSearchCapabilities, PullRequestSearchCriteria, PullRequestSortField, PullRequestSorting, PullRequestStateFilter, PullRequestStatusCheckRollupState } from "../../../git/models/pullRequest.js";
|
|
7
7
|
import { Account as Account$1 } from "../../../git/models/author.js";
|
|
8
8
|
import { IntegrationIds } from "../constants.js";
|
|
9
|
-
import { IssueFilter } from "../providerFilters.js";
|
|
10
9
|
import { ProviderOrganization, ProviderRepositoryShape } from "../results.js";
|
|
11
|
-
import {
|
|
10
|
+
import { IssueFilter } from "../providerFilters.js";
|
|
12
11
|
import { ProviderPullRequestReviews } from "./pullRequestReviews.js";
|
|
13
|
-
import {
|
|
12
|
+
import { Integration, IntegrationType } from "../models/integration.js";
|
|
13
|
+
import { Account, ActionablePullRequest, AnyEntityIdentifierInput, AzureDevOps, AzureOrganization, AzureProject, AzureSetPullRequestInput, Bitbucket, BitbucketServer, BitbucketWorkspaceStub, CollectionMetadata, CursorPageInput, EnterpriseOptions, GetRepoInput, GitBuildStatus, GitBuildStatusState as GitBuildStatusState$1, GitHub, GitHubPullRequestFieldMap, GitIssueState as GitIssueState$1, GitLab, GitLabGroup, GitMergeStrategy, GitPullRequest, GitPullRequestMergeableState as GitPullRequestMergeableState$1, GitPullRequestReviewState as GitPullRequestReviewState$1, GitPullRequestState as GitPullRequestState$1, GitRepository, Issue, Jira, JiraProject, JiraResource, JiraServer, JiraServerProject, Linear, LinearOrganization, LinearTeam, NumberedPageInput, Organization, PullRequestWithUniqueID, RequestFunction, RequestOptions, Response, SetPullRequestInput, Trello } from "@gitkraken/provider-apis";
|
|
14
14
|
//#region ../plus/integrations/src/providers/models.d.ts
|
|
15
15
|
type GitBuildStatusState = GitBuildStatusState$1;
|
|
16
16
|
type GitIssueState = GitIssueState$1;
|
|
@@ -77,8 +77,13 @@ export type ProviderRepoInput = GetRepoInput;
|
|
|
77
77
|
* from the parameter type.
|
|
78
78
|
*/
|
|
79
79
|
export declare function isRepoIdsInput(input: unknown): input is (string | number)[];
|
|
80
|
-
|
|
80
|
+
/**
|
|
81
|
+
* provider-apis' pull request. `mergeableState` is optional because {@link toProviderPullRequest} also converts
|
|
82
|
+
* GitLens' own rows, and a read that didn't select it has none to give.
|
|
83
|
+
*/
|
|
84
|
+
export type ProviderPullRequest = Omit<GitPullRequest, 'reviews' | 'mergeableState'> & {
|
|
81
85
|
reviews: ProviderPullRequestReviews;
|
|
86
|
+
mergeableState?: GitPullRequestMergeableState;
|
|
82
87
|
};
|
|
83
88
|
export type ProviderRepository = GitRepository;
|
|
84
89
|
export type ProviderIssue = Issue;
|
|
@@ -98,7 +103,7 @@ export type ProviderGitLabGroup = GitLabGroup;
|
|
|
98
103
|
* so an id alone is never safe for deduplication across a multi-repository fan-out. Repository identity wins
|
|
99
104
|
* when available so the same row still deduplicates when a later facet adds or removes its URL.
|
|
100
105
|
*/
|
|
101
|
-
export declare function getProviderPullRequestIdentity(pr: Pick<ProviderPullRequest, 'id' | 'repository' | 'url'>): string | undefined;
|
|
106
|
+
export declare function getProviderPullRequestIdentity(pr: Pick<ProviderPullRequest, 'id' | 'repository' | 'url'> | Pick<PullRequest, 'id' | 'repository' | 'url'>): string | undefined;
|
|
102
107
|
export type ProviderHierarchyResult<T> = PagedResult<T> & {
|
|
103
108
|
readonly truncated?: boolean;
|
|
104
109
|
/**
|
|
@@ -710,6 +715,7 @@ export declare const providersMetadata: ProvidersMetadata;
|
|
|
710
715
|
export declare function getReasonsForUserIssue(issue: ProviderIssue, userLogin: string): string[];
|
|
711
716
|
export declare function toIssueShape(issue: ProviderIssue, provider: ProviderReference, options?: {
|
|
712
717
|
reliableStateCategory?: boolean;
|
|
718
|
+
projection?: IssueProjection;
|
|
713
719
|
}): IssueShape | undefined;
|
|
714
720
|
/**
|
|
715
721
|
* Maps a raw provider-apis {@link ProviderRepository} to the GitLens-owned {@link ProviderRepositoryShape}
|
|
@@ -737,6 +743,13 @@ export declare const fromProviderBuildStatusState: {
|
|
|
737
743
|
[GitBuildStatusState.Running]: undefined;
|
|
738
744
|
[GitBuildStatusState.Warning]: undefined;
|
|
739
745
|
};
|
|
746
|
+
/** Rolls every check context up into one state, the way a provider's own rollup does: failed, errored, or
|
|
747
|
+
* action-required fails; else pending or running is pending; else success — reading only the first context
|
|
748
|
+
* would call a pull request passing on the strength of whichever check happens to come back first. States
|
|
749
|
+
* carrying no verdict (cancelled, skipped, optional action required, warning) don't vote. Reads the raw
|
|
750
|
+
* state rather than `fromProviderBuildStatusState`, which drops errored and running to `undefined` and
|
|
751
|
+
* would let a lone success outvote a check still in flight. */
|
|
752
|
+
export declare function toStatusCheckRollupState(statuses: GitBuildStatus[] | null | undefined): PullRequestStatusCheckRollupState | undefined;
|
|
740
753
|
export declare const toProviderPullRequestMergeableState: {
|
|
741
754
|
Mergeable: GitPullRequestMergeableState$1;
|
|
742
755
|
Conflicting: GitPullRequestMergeableState$1;
|
|
@@ -788,9 +801,40 @@ export declare function fromProviderPullRequest(pr: ProviderPullRequest, provide
|
|
|
788
801
|
id: string;
|
|
789
802
|
username?: string;
|
|
790
803
|
};
|
|
804
|
+
projection?: PullRequestProjection;
|
|
805
|
+
}): PullRequest;
|
|
806
|
+
/**
|
|
807
|
+
* A GitLens-native row (GitHub's own reads) tagged for the read that returned it, with `authoredByMe` and `viewer`
|
|
808
|
+
* resolved by the same rule {@link fromProviderPullRequest} applies. Returns a copy: the mapper's row stays as it was
|
|
809
|
+
* built.
|
|
810
|
+
*/
|
|
811
|
+
export declare function stampNativePullRequest(pr: PullRequest, options: {
|
|
812
|
+
currentAccount?: {
|
|
813
|
+
id: string;
|
|
814
|
+
username?: string;
|
|
815
|
+
};
|
|
816
|
+
projection?: PullRequestProjection;
|
|
791
817
|
}): PullRequest;
|
|
818
|
+
/**
|
|
819
|
+
* A row of an account-wide read as a `PullRequest`. GitHub's are GitLens-native, which {@link stampNativePullRequest}
|
|
820
|
+
* tags; every other host's are provider-apis', which {@link fromProviderPullRequest} converts.
|
|
821
|
+
*/
|
|
822
|
+
export declare function toPullRequestRow(pr: ProviderPullRequest | PullRequest, provider: Provider, options: {
|
|
823
|
+
currentAccount?: {
|
|
824
|
+
id: string;
|
|
825
|
+
username?: string;
|
|
826
|
+
};
|
|
827
|
+
projection?: PullRequestProjection;
|
|
828
|
+
}): PullRequest;
|
|
829
|
+
/**
|
|
830
|
+
* By shape rather than by class, which a second copy of the model module fails: a native row is a `'pullrequest'` that
|
|
831
|
+
* names its provider, which provider-apis' rows never do. Both, so a key provider-apis adds later can't reroute its
|
|
832
|
+
* rows.
|
|
833
|
+
*/
|
|
834
|
+
export declare function isNativePullRequest(pr: ProviderPullRequest | PullRequest): pr is PullRequest;
|
|
792
835
|
export declare function fromProviderIssue(issue: ProviderIssue, integration: Integration, options?: {
|
|
793
836
|
project?: IssueProject;
|
|
837
|
+
projection?: IssueProjection;
|
|
794
838
|
}): Issue$1;
|
|
795
839
|
export declare function toProviderPullRequestWithUniqueId(pr: PullRequest): PullRequestWithUniqueID;
|
|
796
840
|
export type ProviderActionablePullRequest = ActionablePullRequest;
|