@gitkraken/core-gitlens 0.11.0 → 0.12.0
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 +41 -1
- package/dist/git/cache.js +2 -0
- package/dist/git/cache.js.map +1 -1
- package/dist/git/models/issue.d.ts +3 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/providers/branches.d.ts +4 -2
- package/dist/git/utils/issue.utils.d.ts +3 -1
- package/dist/git/utils/issue.utils.js +5 -1
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +20 -9
- package/dist/git/watching/worktreeSummary.js +24 -22
- package/dist/git/watching/worktreeSummary.js.map +1 -1
- package/dist/git-cli/providers/branches.js +26 -4
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +3 -1
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.js +1 -1
- package/dist/plus/integrations/providers/azure/azure.js +3 -2
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/models.d.ts +27 -10
- package/dist/plus/integrations/providers/azure/models.js +39 -19
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +19 -0
- package/dist/plus/integrations/providers/azure/workItemStateCategories.js +32 -0
- package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -0
- package/dist/plus/integrations/providers/azureDevOps.js +5 -5
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
- package/dist/plus/integrations/providers/jira-server.js +11 -5
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +5 -2
- package/dist/plus/integrations/providers/jira.js +45 -30
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +3 -4
- package/dist/plus/integrations/providers/jiraIssueByKey.js +3 -4
- package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -1
- package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +5 -5
- package/dist/plus/integrations/providers/linearIssuesEtag.js +6 -6
- package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +11 -1
- package/dist/plus/integrations/providers/models.js +9 -9
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +30 -32
- package/dist/plus/integrations/providers/providersApi.js +149 -46
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +27 -15
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +72 -10
- package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
- package/dist/plus/integrations/reads/batchEtags.d.ts +5 -2
- package/dist/plus/integrations/reads/batchEtags.js +31 -10
- package/dist/plus/integrations/reads/batchEtags.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.js +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +0 -1
- package/dist/plus/integrations/reads/hierarchy.js +2 -2
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.utils.d.ts +13 -0
- package/dist/plus/integrations/reads/hierarchy.utils.js +26 -1
- package/dist/plus/integrations/reads/hierarchy.utils.js.map +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.js +1 -1
- package/dist/plus/integrations/reads/pullRequestBranches.js +10 -3
- package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +28 -3
- package/dist/plus/integrations/results.js +22 -3
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/plus/integrations/utils/integration.utils.d.ts +6 -1
- package/dist/plus/integrations/utils/integration.utils.js +8 -1
- package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
- package/docs/integrations.md +54 -19
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pullRequestBranches.js","names":[],"sources":["../../../../../plus/integrations/src/reads/pullRequestBranches.ts"],"sourcesContent":["import type { PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { equalsIgnoreCase } from '@gitlens/utils/string.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { GitSelfManagedHostIntegrationId } from '../constants.js';\nimport { isAzureProviderId } from '../providers/providerErrors.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport { isGitHostIntegration, isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport { appendBatchSlotWarning } from './batchEtags.js';\nimport type { ProviderReadContext } from './context.js';\nimport { getCurrentAccountIdentity, runCaptured } from './drains.js';\nimport { findDuplicateKey, trimCoordinateFields, unresolvedIntegration } from './issueBatch.js';\nimport { gitHostOnlySurfaceWarning, otherWarning, unsupportedWarning } from './warnings.js';\n\n/**\n * The pull-requests-by-branch read: for each branch, every pull request whose head is that branch, in any state.\n * A relationship-free point read — unlike the account-wide sweeps, it finds a teammate's pull request from the\n * user's branch — so an empty list is a PROVEN \"none\" a caller can cache.\n *\n * ONE integration call per invocation, whatever the target count, settling each target independently — see\n * `GitHostIntegration.getPullRequestsForBranchesResult`. Uncached, and not routed through the integration's\n * `getPullRequestForBranch`, which caches through `IntegrationCacheProvider.getPullRequestForBranch`, answers one\n * pull request, and on several hosts looks the branch ref up, answering \"none\" once a merged branch is deleted.\n */\n\n/** Pull requests returned per target. */\nconst pullRequestsForBranchLimit = 10;\n\n/** One branch whose pull requests to find, echoed back under the caller's own `key`. */\nexport interface PullRequestBranchTarget {\n\t/** Caller-owned identifier, echoed on the result so no positional matching is needed. Must be unique. */\n\tkey: string;\n\t/**\n\t * The repository the pull requests are OPENED AGAINST (their base), as in `PullRequestBatchTarget`: GitHub\n\t * owner, GitLab namespace path (may be nested), Bitbucket workspace, Bitbucket Data Center project key, or Azure\n\t * DevOps organization.\n\t */\n\towner: string;\n\t/** Repository name or slug. */\n\trepo: string;\n\t/** Azure DevOps project. Required there, ignored elsewhere. */\n\tproject?: string;\n\t/** The head branch's short name, without `refs/heads/`. */\n\tbranch: string;\n\t/**\n\t * Owner (GitHub user or org, GitLab namespace, Bitbucket workspace) of the repository the branch lives in, when\n\t * it's a fork. Omitted, or equal to `owner` (ignoring case), means the branch lives in the base repository itself,\n\t * so a caller can pass the owner of whichever remote the branch was pushed to.\n\t *\n\t * A different owner is refused on Bitbucket Data Center and Azure DevOps, where a fork can't be found by its\n\t * owner.\n\t */\n\theadOwner?: string;\n}\n\n/** The answer for one {@link PullRequestBranchTarget}. */\nexport interface PullRequestBranchResult {\n\tkey: string;\n\t/**\n\t * Every pull request whose head is this branch, in ANY state — open, closed and merged — most recently updated\n\t * first, up to a per-target cap. An empty array is a PROVEN \"none\" unless `truncated` is set. A target whose\n\t * read failed is not returned at all.\n\t */\n\tpullRequests: PullRequestShape[];\n\t/**\n\t * Set when a pull request that wasn't returned could still match: more matched than the cap, or the host held\n\t * more pull requests of this branch name than it returned and couldn't rule them out. GitHub/GHE rule them out\n\t * with a second request filtered by head owner, so there it means more than the cap truly match. That request\n\t * can't see a pull request from a since-deleted fork, so on GitHub/GHE such a pull request outside the first\n\t * request's page can be missing even when this is not set.\n\t */\n\ttruncated?: boolean;\n}\n\nexport async function getPullRequestsForBranches(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\ttargets: readonly PullRequestBranchTarget[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<PullRequestBranchResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<PullRequestBranchResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\tconst surface = 'Pull requests by branch';\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tif (options.targets.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.targets);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: two results under one key make matching ambiguous for EVERY\n\t\t// target, not just the repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate pull request branch target key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\t// `branch` is left alone: a git ref name can't carry surrounding whitespace, so a padded one is refused below\n\t// rather than being silently repaired.\n\tconst targets = options.targets.map(target => ({\n\t\t...trimCoordinateFields(target),\n\t\theadOwner: target.headOwner?.trim(),\n\t}));\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream to fail one by one.\n\tconst invalid = findInvalidTarget(options.providerId, targets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(options.providerId, undefined, options.connectionId, invalid));\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\treturn unresolvedIntegration(ctx, options.providerId, options.connectionId, options.domain);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst currentAccount = await getCurrentAccountIdentity(integration, options.connectionId);\n\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\tconst { value: slots, warning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() =>\n\t\t\tintegration.getPullRequestsForBranchesResult(\n\t\t\t\ttargets.map(t => ({\n\t\t\t\t\towner: t.owner,\n\t\t\t\t\trepo: t.repo,\n\t\t\t\t\tproject: t.project,\n\t\t\t\t\tbranch: t.branch,\n\t\t\t\t\theadOwner: forkOwner(t),\n\t\t\t\t})),\n\t\t\t\t{ currentAccount: currentAccount, limit: pullRequestsForBranchLimit },\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t// A missing session must surface as a connection warning, including on the primary path, which\n\t\t// otherwise reads it as \"not connected\" and says nothing.\n\t\t{ warnOnMissingSession: true },\n\t);\n\tif (warning != null) {\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\n\tif (slots == null) {\n\t\t// Dropped, never reported as \"none\": \"unknown\" and \"proven none\" must stay distinguishable.\n\t\tif (warning == null) {\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\tunsupportedWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t`${surface} is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\treturn { items: [], warnings: warnings, fetchFailed: true };\n\t}\n\n\tconst items: PullRequestBranchResult[] = [];\n\tfor (let i = 0; i < targets.length; i++) {\n\t\tconst slot = slots[i];\n\t\tif (slot.status === 'rejected') {\n\t\t\t// Dropped, never reported as \"none\", like a whole-call failure.\n\t\t\tappendBatchSlotWarning(warnings, options.providerId, domain, options.connectionId, slot);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\titems.push({\n\t\t\tkey: targets[i].key,\n\t\t\tpullRequests: slot.value.pullRequests,\n\t\t\t...(slot.value.truncated ? { truncated: true } : {}),\n\t\t});\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/** The owner of the fork the branch lives in, or `undefined` for a branch in the base repository. */\nfunction forkOwner(target: PullRequestBranchTarget): string | undefined {\n\treturn target.headOwner == null || equalsIgnoreCase(target.headOwner, target.owner) ? undefined : target.headOwner;\n}\n\nfunction findInvalidTarget(\n\tproviderId: IntegrationIds,\n\ttargets: readonly PullRequestBranchTarget[],\n): string | undefined {\n\tconst isAzure = isAzureProviderId(providerId);\n\tconst isBitbucketServer = providerId === GitSelfManagedHostIntegrationId.BitbucketServer;\n\tfor (const target of targets) {\n\t\tif (target.owner.length === 0 || target.repo.length === 0) {\n\t\t\treturn `Pull request branch target '${target.key}' requires a non-empty owner and repo.`;\n\t\t}\n\t\tif (target.branch.trim().length === 0) {\n\t\t\treturn `Pull request branch target '${target.key}' requires a non-empty branch.`;\n\t\t}\n\t\t// Sent as is, a padded name matches no branch and answers a cacheable \"none\".\n\t\tif (target.branch !== target.branch.trim()) {\n\t\t\treturn `Pull request branch target '${target.key}' has branch '${target.branch}'; a branch name can't start or end with whitespace.`;\n\t\t}\n\t\tif (target.branch.startsWith('refs/')) {\n\t\t\treturn `Pull request branch target '${target.key}' has branch '${target.branch}'; pass the branch's short name, without 'refs/heads/'.`;\n\t\t}\n\t\tif (isAzure && !target.project) {\n\t\t\treturn `Pull request branch target '${target.key}' requires a project for '${providerId}'.`;\n\t\t}\n\t\tif (target.headOwner?.length === 0) {\n\t\t\treturn `Pull request branch target '${target.key}' has an empty headOwner; omit it for a branch in the base repository.`;\n\t\t}\n\t\tif (forkOwner(target) != null) {\n\t\t\tif (isAzure) {\n\t\t\t\treturn `Pull request branch target '${target.key}' has a headOwner other than its owner, which '${providerId}' can't honor: an Azure DevOps fork shares its organization, so its owner can't identify it.`;\n\t\t\t}\n\t\t\tif (isBitbucketServer) {\n\t\t\t\treturn `Pull request branch target '${target.key}' has a headOwner other than its owner, which '${providerId}' can't honor: Bitbucket Data Center finds a branch's pull requests only through the repository the branch lives in, and a fork's repository can't be derived from its owner.`;\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0BA,MAAM,6BAA6B;AAgDnC,eAAsB,2BACrB,KACA,SAUmD;CACnD,MAAM,WAAW,aAAuE;EACvF,OAAO,CAAC;EACR,UAAU,CAAC,OAAO;EAClB,aAAa;CACd;CACA,MAAM,UAAU;CAEhB,IAAI,0BAA0B,QAAQ,UAAU,GAC/C,OAAO,QAAQ,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,OAAO,CAAC;CAGvG,IAAI,QAAQ,QAAQ,WAAW,GAAG,OAAO;EAAE,OAAO,CAAC;EAAG,UAAU,CAAC;CAAE;CAEnE,MAAM,eAAe,iBAAiB,QAAQ,OAAO;CACrD,IAAI,gBAAgB,MAGnB,OAAO,QACN,aACC,QAAQ,YACR,KAAA,GACA,QAAQ,cACR,6CAA6C,aAAa,kDAC3D,CACD;CAMD,MAAM,UAAU,QAAQ,QAAQ,KAAI,YAAW;EAC9C,GAAG,qBAAqB,MAAM;EAC9B,WAAW,OAAO,WAAW,KAAK;CACnC,EAAE;CAGF,MAAM,UAAU,kBAAkB,QAAQ,YAAY,OAAO;CAC7D,IAAI,WAAW,MACd,OAAO,QAAQ,aAAa,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,OAAO,CAAC;CAG1F,MAAM,cAAc,MAAM,IAAI,sBAAsB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAC5G,IAAI,eAAe,MAClB,OAAO,sBAAsB,KAAK,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAE3F,IAAI,CAAC,qBAAqB,WAAW,GACpC,OAAO,QAAQ,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,OAAO,CAAC;CAGvG,MAAM,SAAS,IAAI,cAAc,aAAa,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CACtG,MAAM,iBAAiB,MAAM,0BAA0B,aAAa,QAAQ,YAAY;CAExF,MAAM,WAA8B,CAAC;CACrC,IAAI,cAAc;CAElB,MAAM,EAAE,OAAO,OAAO,YAAY,MAAM,YACvC,QAAQ,YACR,QACA,QAAQ,oBAEP,YAAY,iCACX,QAAQ,KAAI,OAAM;EACjB,OAAO,EAAE;EACT,MAAM,EAAE;EACR,SAAS,EAAE;EACX,QAAQ,EAAE;EACV,WAAW,UAAU,CAAC;CACvB,EAAE,GACF;EAAkB;EAAgB,OAAO;CAA2B,GACpE,KAAA,GACA,QAAQ,YACT,GAGD,EAAE,sBAAsB,KAAK,CAC9B;CACA,IAAI,WAAW,MACd,qBAAqB,UAAU,OAAO;CAGvC,IAAI,SAAS,MAAM;EAElB,IAAI,WAAW,MACd,qBACC,UACA,mBACC,QAAQ,YACR,QACA,QAAQ,cACR,GAAG,QAAQ,wBAAwB,QAAQ,WAAW,GACvD,CACD;EAED,OAAO;GAAE,OAAO,CAAC;GAAa;GAAU,aAAa;EAAK;CAC3D;CAEA,MAAM,QAAmC,CAAC;CAC1C,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACxC,MAAM,OAAO,MAAM;EACnB,IAAI,KAAK,WAAW,YAAY;GAE/B,uBAAuB,UAAU,QAAQ,YAAY,QAAQ,QAAQ,cAAc,IAAI;GACvF,cAAc;GACd;EACD;EAEA,MAAM,KAAK;GACV,KAAK,QAAQ,EAAE,CAAC;GAChB,cAAc,KAAK,MAAM;GACzB,GAAI,KAAK,MAAM,YAAY,EAAE,WAAW,KAAK,IAAI,CAAC;EACnD,CAAC;CACF;CAEA,OAAO;EAAS;EAAiB;EAAU,aAAa,eAAe,KAAA;CAAU;AAClF;;AAGA,SAAS,UAAU,QAAqD;CACvE,OAAO,OAAO,aAAa,QAAQ,iBAAiB,OAAO,WAAW,OAAO,KAAK,IAAI,KAAA,IAAY,OAAO;AAC1G;AAEA,SAAS,kBACR,YACA,SACqB;CACrB,MAAM,UAAU,kBAAkB,UAAU;CAC5C,MAAM,oBAAoB,eAAA;CAC1B,KAAK,MAAM,UAAU,SAAS;EAC7B,IAAI,OAAO,MAAM,WAAW,KAAK,OAAO,KAAK,WAAW,GACvD,OAAO,+BAA+B,OAAO,IAAI;EAElD,IAAI,OAAO,OAAO,KAAK,CAAC,CAAC,WAAW,GACnC,OAAO,+BAA+B,OAAO,IAAI;EAGlD,IAAI,OAAO,WAAW,OAAO,OAAO,KAAK,GACxC,OAAO,+BAA+B,OAAO,IAAI,gBAAgB,OAAO,OAAO;EAEhF,IAAI,OAAO,OAAO,WAAW,OAAO,GACnC,OAAO,+BAA+B,OAAO,IAAI,gBAAgB,OAAO,OAAO;EAEhF,IAAI,WAAW,CAAC,OAAO,SACtB,OAAO,+BAA+B,OAAO,IAAI,4BAA4B,WAAW;EAEzF,IAAI,OAAO,WAAW,WAAW,GAChC,OAAO,+BAA+B,OAAO,IAAI;EAElD,IAAI,UAAU,MAAM,KAAK,MAAM;GAC9B,IAAI,SACH,OAAO,+BAA+B,OAAO,IAAI,iDAAiD,WAAW;GAE9G,IAAI,mBACH,OAAO,+BAA+B,OAAO,IAAI,iDAAiD,WAAW;EAE/G;CACD;AAED"}
|
|
1
|
+
{"version":3,"file":"pullRequestBranches.js","names":[],"sources":["../../../../../plus/integrations/src/reads/pullRequestBranches.ts"],"sourcesContent":["import type { PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { equalsIgnoreCase } from '@gitlens/utils/string.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { GitSelfManagedHostIntegrationId } from '../constants.js';\nimport { isAzureProviderId } from '../providers/providerErrors.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport { isGitHostIntegration, isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport { appendBatchSlotWarning } from './batchEtags.js';\nimport type { ProviderReadContext } from './context.js';\nimport { getCurrentAccountIdentity, runCaptured } from './drains.js';\nimport { findDuplicateKey, trimCoordinateFields, unresolvedIntegration } from './issueBatch.js';\nimport { gitHostOnlySurfaceWarning, otherWarning, unsupportedWarning } from './warnings.js';\n\n/**\n * The pull-requests-by-branch read: for each branch, every pull request whose head is that branch, in any state.\n * A relationship-free point read — unlike the account-wide sweeps, it finds a teammate's pull request from the\n * user's branch — so an empty list is a PROVEN \"none\" a caller can cache.\n *\n * ONE integration call per invocation, whatever the target count, settling each target independently — see\n * `GitHostIntegration.getPullRequestsForBranchesResult`. Uncached, and not routed through the integration's\n * `getPullRequestForBranch`, which caches through `IntegrationCacheProvider.getPullRequestForBranch`, answers one\n * pull request, and on several hosts looks the branch ref up, answering \"none\" once a merged branch is deleted.\n */\n\n/** Pull requests returned per target. */\nconst pullRequestsForBranchLimit = 10;\n\n/** One branch whose pull requests to find, echoed back under the caller's own `key`. */\nexport interface PullRequestBranchTarget {\n\t/** Caller-owned identifier, echoed on the result so no positional matching is needed. Must be unique. */\n\tkey: string;\n\t/**\n\t * The repository the pull requests are OPENED AGAINST (their base), as in `PullRequestBatchTarget`: GitHub\n\t * owner, GitLab namespace path (may be nested), Bitbucket workspace, Bitbucket Data Center project key, or Azure\n\t * DevOps organization.\n\t */\n\towner: string;\n\t/** Repository name or slug. */\n\trepo: string;\n\t/** Azure DevOps project. Required there, ignored elsewhere. */\n\tproject?: string;\n\t/** The head branch's short name, without `refs/heads/`. */\n\tbranch: string;\n\t/**\n\t * Owner (GitHub user or org, GitLab namespace, Bitbucket workspace) of the repository the branch lives in, when\n\t * it's a fork. Omitted, or equal to `owner` (ignoring case), means the branch lives in the base repository itself,\n\t * so a caller can pass the owner of whichever remote the branch was pushed to.\n\t *\n\t * A different owner is refused on Bitbucket Data Center and Azure DevOps, where a fork can't be found by its\n\t * owner.\n\t */\n\theadOwner?: string;\n}\n\n/** The answer for one {@link PullRequestBranchTarget}. */\nexport interface PullRequestBranchResult {\n\tkey: string;\n\t/**\n\t * Every pull request whose head is this branch, in ANY state — open, closed and merged — most recently updated\n\t * first, up to a per-target cap. An empty array is a PROVEN \"none\" unless `truncated` is set. A target whose\n\t * read failed is not returned at all.\n\t */\n\tpullRequests: PullRequestShape[];\n\t/**\n\t * Set when a pull request that wasn't returned could still match: more matched than the cap, or the host held\n\t * more pull requests of this branch name than it returned and couldn't rule them out. GitHub/GHE rule them out\n\t * with a second request filtered by head owner, so there it means more than the cap truly match. That request\n\t * can't see a pull request from a since-deleted fork, so on GitHub/GHE such a pull request outside the first\n\t * request's page can be missing even when this is not set.\n\t */\n\ttruncated?: boolean;\n}\n\nexport async function getPullRequestsForBranches(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\ttargets: readonly PullRequestBranchTarget[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<PullRequestBranchResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<PullRequestBranchResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\tconst surface = 'Pull requests by branch';\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tif (options.targets.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.targets);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: two results under one key make matching ambiguous for EVERY\n\t\t// target, not just the repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate pull request branch target key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\t// `branch` is left alone: a git ref name can't carry surrounding whitespace, so a padded one is refused below\n\t// rather than being silently repaired.\n\tconst targets = options.targets.map(target => ({\n\t\t...trimCoordinateFields(target),\n\t\theadOwner: target.headOwner?.trim(),\n\t}));\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream to fail one by one.\n\tconst invalid = findInvalidTarget(options.providerId, targets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(options.providerId, undefined, options.connectionId, invalid));\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\treturn unresolvedIntegration(ctx, options.providerId, options.connectionId, options.domain);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst currentAccount = await getCurrentAccountIdentity(integration, options.connectionId);\n\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\tconst { value: slots, warning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() =>\n\t\t\tintegration.getPullRequestsForBranchesResult(\n\t\t\t\ttargets.map(t => ({\n\t\t\t\t\towner: t.owner,\n\t\t\t\t\trepo: t.repo,\n\t\t\t\t\tproject: t.project,\n\t\t\t\t\tbranch: t.branch,\n\t\t\t\t\theadOwner: forkOwner(t),\n\t\t\t\t})),\n\t\t\t\t{ currentAccount: currentAccount, limit: pullRequestsForBranchLimit },\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t// A missing session must surface as a connection warning, including on the primary path, which\n\t\t// otherwise reads it as \"not connected\" and says nothing.\n\t\t{ warnOnMissingSession: true },\n\t);\n\tconst allKeys = targets.map(t => t.key);\n\tif (warning != null) {\n\t\tappendDedupedWarning(warnings, { ...warning, targetKeys: allKeys });\n\t}\n\n\tif (slots == null) {\n\t\t// Dropped, never reported as \"none\": \"unknown\" and \"proven none\" must stay distinguishable.\n\t\tif (warning == null) {\n\t\t\tappendDedupedWarning(warnings, {\n\t\t\t\t...unsupportedWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t`${surface} is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t\ttargetKeys: allKeys,\n\t\t\t});\n\t\t}\n\t\treturn { items: [], warnings: warnings, fetchFailed: true };\n\t}\n\n\tconst items: PullRequestBranchResult[] = [];\n\tfor (let i = 0; i < targets.length; i++) {\n\t\tconst slot = slots[i];\n\t\tif (slot.status === 'rejected') {\n\t\t\t// Dropped, never reported as \"none\", like a whole-call failure.\n\t\t\tappendBatchSlotWarning(warnings, options.providerId, domain, options.connectionId, slot, targets[i].key);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\titems.push({\n\t\t\tkey: targets[i].key,\n\t\t\tpullRequests: slot.value.pullRequests,\n\t\t\t...(slot.value.truncated ? { truncated: true } : {}),\n\t\t});\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/** The owner of the fork the branch lives in, or `undefined` for a branch in the base repository. */\nfunction forkOwner(target: PullRequestBranchTarget): string | undefined {\n\treturn target.headOwner == null || equalsIgnoreCase(target.headOwner, target.owner) ? undefined : target.headOwner;\n}\n\nfunction findInvalidTarget(\n\tproviderId: IntegrationIds,\n\ttargets: readonly PullRequestBranchTarget[],\n): string | undefined {\n\tconst isAzure = isAzureProviderId(providerId);\n\tconst isBitbucketServer = providerId === GitSelfManagedHostIntegrationId.BitbucketServer;\n\tfor (const target of targets) {\n\t\tif (target.owner.length === 0 || target.repo.length === 0) {\n\t\t\treturn `Pull request branch target '${target.key}' requires a non-empty owner and repo.`;\n\t\t}\n\t\tif (target.branch.trim().length === 0) {\n\t\t\treturn `Pull request branch target '${target.key}' requires a non-empty branch.`;\n\t\t}\n\t\t// Sent as is, a padded name matches no branch and answers a cacheable \"none\".\n\t\tif (target.branch !== target.branch.trim()) {\n\t\t\treturn `Pull request branch target '${target.key}' has branch '${target.branch}'; a branch name can't start or end with whitespace.`;\n\t\t}\n\t\tif (target.branch.startsWith('refs/')) {\n\t\t\treturn `Pull request branch target '${target.key}' has branch '${target.branch}'; pass the branch's short name, without 'refs/heads/'.`;\n\t\t}\n\t\tif (isAzure && !target.project) {\n\t\t\treturn `Pull request branch target '${target.key}' requires a project for '${providerId}'.`;\n\t\t}\n\t\tif (target.headOwner?.length === 0) {\n\t\t\treturn `Pull request branch target '${target.key}' has an empty headOwner; omit it for a branch in the base repository.`;\n\t\t}\n\t\tif (forkOwner(target) != null) {\n\t\t\tif (isAzure) {\n\t\t\t\treturn `Pull request branch target '${target.key}' has a headOwner other than its owner, which '${providerId}' can't honor: an Azure DevOps fork shares its organization, so its owner can't identify it.`;\n\t\t\t}\n\t\t\tif (isBitbucketServer) {\n\t\t\t\treturn `Pull request branch target '${target.key}' has a headOwner other than its owner, which '${providerId}' can't honor: Bitbucket Data Center finds a branch's pull requests only through the repository the branch lives in, and a fork's repository can't be derived from its owner.`;\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n"],"mappings":"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"}
|
|
@@ -53,9 +53,13 @@ export interface ProviderWarningScope {
|
|
|
53
53
|
/**
|
|
54
54
|
* The resource as the read addressed it: its id on most reads, its name on the few that address it by name
|
|
55
55
|
* (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both
|
|
56
|
-
* `ProviderOrganization.id` and `name
|
|
56
|
+
* `ProviderOrganization.id` and `name`, and a project's `orgId` where it has one.
|
|
57
57
|
*/
|
|
58
58
|
resourceId?: string;
|
|
59
|
+
/**
|
|
60
|
+
* The project as the read addressed it. For a Jira project read, the key on Jira Cloud (the project's `name` in
|
|
61
|
+
* `listProjects`) and the numeric id on Jira Data Center.
|
|
62
|
+
*/
|
|
59
63
|
projectId?: string;
|
|
60
64
|
repositoryId?: string;
|
|
61
65
|
}
|
|
@@ -196,6 +200,19 @@ export interface ProviderWarning {
|
|
|
196
200
|
* out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.
|
|
197
201
|
*/
|
|
198
202
|
omission?: ProviderWarningOmission;
|
|
203
|
+
/**
|
|
204
|
+
* The keys of the batch targets this warning concerns: the target a per-target failure was refused or failed
|
|
205
|
+
* for, or every target of an integration call that failed as a whole. So a caller that sends several callers'
|
|
206
|
+
* targets in one call can hand a failure only to the targets it belongs to.
|
|
207
|
+
*
|
|
208
|
+
* Its ABSENCE means the whole call. It is absent on every read that is not a batch read, and on a batch read's
|
|
209
|
+
* refusal of the whole call before any target is read (a duplicate key, an invalid target, an unknown etag
|
|
210
|
+
* include, no resolvable connection, the wrong surface), which concerns every target alike.
|
|
211
|
+
*
|
|
212
|
+
* It is not part of a warning's identity: two warnings that differ only here are one warning, for the union of
|
|
213
|
+
* their targets (see {@link appendDedupedWarning}).
|
|
214
|
+
*/
|
|
215
|
+
targetKeys?: string[];
|
|
199
216
|
}
|
|
200
217
|
export interface ProviderPageInfo {
|
|
201
218
|
/**
|
|
@@ -330,13 +347,19 @@ export interface ResolveRepositoryResult {
|
|
|
330
347
|
export declare function providerWarningMessage(ex: unknown): string;
|
|
331
348
|
/**
|
|
332
349
|
* Normalized org/workspace/group shape returned by the provider facade.
|
|
333
|
-
* `name` is the provider identifier to pass to follow-up reads, while `org` is an optional
|
|
350
|
+
* `name` is the provider identifier to pass to follow-up reads (a Jira project's key), while `org` is an optional
|
|
351
|
+
* display label of the parent. Where that label can't address the parent, `orgId` is the parent's stable id to pass
|
|
352
|
+
* as `org` instead (a Jira site's id, or a Data Center instance's domain).
|
|
353
|
+
*
|
|
354
|
+
* Jira site rows (`listOrgs`) are the exception: their `name` is the site's label, and a Jira read selects a site only
|
|
355
|
+
* by its `id` (or a project row's `orgId`).
|
|
334
356
|
*/
|
|
335
357
|
export interface ProviderOrganization {
|
|
336
358
|
id: string;
|
|
337
359
|
providerId: IntegrationIds;
|
|
338
360
|
name: string;
|
|
339
361
|
org?: string;
|
|
362
|
+
orgId?: string;
|
|
340
363
|
url: string;
|
|
341
364
|
}
|
|
342
365
|
/** Normalized repository shape returned by the provider facade. */
|
|
@@ -389,7 +412,9 @@ export declare function collectionScopeKey(scope: ProviderWarningOmissionScope |
|
|
|
389
412
|
export declare function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void;
|
|
390
413
|
/**
|
|
391
414
|
* Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the
|
|
392
|
-
* structured scope, cause and omission when present) is absent.
|
|
415
|
+
* structured scope, cause and omission when present) is absent. An equal warning already there merges its
|
|
416
|
+
* {@link ProviderWarning.targetKeys} with the new one's instead: the union of both, or none when either concerns
|
|
417
|
+
* the whole call. The existing warning is replaced, never mutated, since it may be shared.
|
|
393
418
|
*/
|
|
394
419
|
export declare function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void;
|
|
395
420
|
//#endregion
|
|
@@ -121,12 +121,31 @@ function providerWarningKey(warning) {
|
|
|
121
121
|
}
|
|
122
122
|
/**
|
|
123
123
|
* Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the
|
|
124
|
-
* structured scope, cause and omission when present) is absent.
|
|
124
|
+
* structured scope, cause and omission when present) is absent. An equal warning already there merges its
|
|
125
|
+
* {@link ProviderWarning.targetKeys} with the new one's instead: the union of both, or none when either concerns
|
|
126
|
+
* the whole call. The existing warning is replaced, never mutated, since it may be shared.
|
|
125
127
|
*/
|
|
126
128
|
function appendDedupedWarning(into, warning) {
|
|
127
129
|
const key = providerWarningKey(warning);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
const index = into.findIndex((existing) => providerWarningKey(existing) === key);
|
|
131
|
+
if (index === -1) {
|
|
132
|
+
into.push(warning);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const existing = into[index];
|
|
136
|
+
const existingKeys = existing.targetKeys;
|
|
137
|
+
if (existingKeys == null) return;
|
|
138
|
+
if (warning.targetKeys == null) {
|
|
139
|
+
const { targetKeys: _, ...wholeCall } = existing;
|
|
140
|
+
into[index] = wholeCall;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const added = warning.targetKeys.filter((k) => !existingKeys.includes(k));
|
|
144
|
+
if (!added.length) return;
|
|
145
|
+
into[index] = {
|
|
146
|
+
...existing,
|
|
147
|
+
targetKeys: [...existingKeys, ...new Set(added)]
|
|
148
|
+
};
|
|
130
149
|
}
|
|
131
150
|
//#endregion
|
|
132
151
|
export { appendDedupedWarning, collectionScopeKey, providerWarningMessage, reconcileOmissionsWithFailure, toProviderWarning };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"results.js","names":[],"sources":["../../../../plus/integrations/src/results.ts"],"sourcesContent":["import { isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '@gitlens/git/errors.js';\nimport type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from './constants.js';\nimport { isProviderUnreachableError } from './errors.js';\n\nexport interface ConnectionStateChangeEvent {\n\tkey: string;\n\treason: 'connected' | 'disconnected';\n}\n\n/**\n * A per-provider, per-connection warning surfaced alongside (partial) read results. Consumers use\n * these to drive auth recovery, retry, or truncation messaging without the read itself throwing.\n *\n * `kind` is the programmatic discriminant (a rate-limit is retryable, a 404 is not, an unsupported capability\n * never succeeds on retry, an auth failure needs re-connection unless {@link ProviderWarning.scope} confines\n * it); `isAuth` is retained as a convenience mirror of `kind === 'auth'`.\n *\n * `'unsupported'`: the provider — or this integration of it — lacks the capability the request asked for: a\n * surface (issues on Bitbucket, pull requests on an issue tracker, a batch read, org/repo discovery,\n * current-account lookup, project-scoped or account-wide issue reads), or a filter, sort, state, search\n * criterion or option combination it cannot express. Retrying the same request never succeeds; a different\n * provider, scope or request may. NOT unsupported (these stay `'other'`): malformed or contradictory caller\n * input the caller fixes in the request itself (a duplicate batch key, invalid target numbers, an empty owner,\n * an unusable scope), incompleteness or omission warnings, and unclassified errors.\n */\nexport type ProviderWarningKind = 'auth' | 'rate-limit' | 'not-found' | 'no-connection' | 'unsupported' | 'other';\n\n/**\n * Why a read that SUCCEEDED still withheld results:\n * - `provider-limit`: the provider refuses to serve past a cap it enforces (GitHub search's 1,000-result\n * ceiling, Trello's `cards_limit`), so the excess is unreachable through that query.\n * - `recovery-budget`: the internal partitioned recovery stopped spending upstream requests before it ran\n * out of partitions to visit.\n * - `pagination-incomplete`: pages were left unread — a sub-scope of a multi-scope read that was not drained,\n * a drain that stopped at its own page backstop, or a provider that advertised another page and gave no way\n * to reach it. These have DIFFERENT remedies, so the kind alone does not say whether more can be fetched;\n * read {@link ProviderWarningOmission.recovery} for that.\n *\n * Mirrors the vocabulary the SDK reports. Declared here rather than imported so this module stays free of\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`); `collectionMetadata.ts` holds the\n * compile-time link, so an SDK bump that adds a kind fails the build at that boundary.\n */\nexport type ProviderWarningOmissionKind = 'provider-limit' | 'recovery-budget' | 'pagination-incomplete';\n\n/**\n * How a consumer can recover omitted results. `page-budget` reruns with a larger budget; `narrow-scope`\n * requests a smaller server-side scope; `none` promises neither. Switch on the exact value because only\n * `page-budget` is a fetch-more action.\n */\nexport type ProviderWarningOmissionRecovery = 'none' | 'page-budget' | 'narrow-scope';\n\n/**\n * The part of a provider a warning is confined to: one resource (organization, workspace, site), project or\n * repository. All fields optional; see {@link ProviderWarning.scope} for when one is forwarded.\n */\nexport interface ProviderWarningScope {\n\t/**\n\t * The resource as the read addressed it: its id on most reads, its name on the few that address it by name\n\t * (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both\n\t * `ProviderOrganization.id` and `name`.\n\t */\n\tresourceId?: string;\n\tprojectId?: string;\n\trepositoryId?: string;\n}\n\n/**\n * Why a provider request failed, when the failure itself says so:\n * - `unreachable`: a transport failure or server-side error prevented the provider from answering. The connection\n * still exists; this accompanies `kind: 'other'`, never an authentication warning.\n * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the\n * organization policy \"Third-party application access via OAuth\", off by default for new organizations. An\n * organization admin enables it, or the user connects with a personal access token instead, which the policy\n * does not govern.\n * - `access-denied`: the account has no access to that organization or project (not a member, or lacking the\n * permission). Someone who administers it has to grant access.\n * - `conditional-access`: a Microsoft Entra Conditional Access policy blocked the request (Azure DevOps\n * `VS403463`). Its admin has to exempt the request, e.g. by location or device.\n *\n * None of these is healed by reconnecting, which is the point of naming them.\n */\nexport type ProviderWarningCauseReason =\n\t| 'oauth-app-not-allowed'\n\t| 'access-denied'\n\t| 'conditional-access'\n\t| 'unreachable';\n\n/** See {@link ProviderWarning.cause}. */\nexport interface ProviderWarningCause {\n\treason: ProviderWarningCauseReason;\n\t/** The provider's own error code, when it reports one (Azure DevOps `TF400813`, `VS403463`). */\n\tcode?: string;\n\t/** Where the setting behind the refusal is changed, when this layer can address it. */\n\tremedyUrl?: string;\n}\n\n/** Which repository / project / resource an omission is attributed to. All fields optional; a scope may name none. */\nexport interface ProviderWarningOmissionScope extends ProviderWarningScope {\n\tproviderId?: string;\n}\n\nexport interface ProviderWarningOmission {\n\tkind: ProviderWarningOmissionKind;\n\t/**\n\t * Whether anything would fetch the missing items, and what. Gate a \"load more\" affordance on this rather\n\t * than on `kind` — see {@link ProviderWarningOmissionRecovery}.\n\t */\n\trecovery: ProviderWarningOmissionRecovery;\n\t/**\n\t * The cap the provider enforces, when it reports one. NOT a result count for every kind: on\n\t * `recovery-budget` this is a REQUEST budget and must not be shown to a user as a number of results.\n\t */\n\tlimit?: number;\n\t/** Total matches the provider reported, when it reports one. Only GitHub's search cap does today. */\n\ttotalCount?: number;\n\t/**\n\t * The order the reachable window was selected under, as `field:direction`, when the omission is a result\n\t * ceiling.\n\t *\n\t * Present so a consumer can word its own \"N matched, showing the first M\" without assuming WHICH M: the\n\t * provider serves the top M under this key, so the sentence is only true if it names it. Absent for every\n\t * omission that isn't a ceiling, where no window was selected by an order.\n\t *\n\t * A union of both vocabularies rather than the issue one alone: `PullRequestSorting` happens to be a subset\n\t * today, so narrowing this to {@link IssueSorting} would still compile while describing only half the reads\n\t * that set it — and would break the day the PR search gains a key the issue search has no name for.\n\t */\n\tsort?: IssueSorting | PullRequestSorting;\n\t/** Which repository / project / resource was affected, when one is attributed. */\n\tscope?: ProviderWarningOmissionScope;\n}\n\nexport interface ProviderWarning {\n\tproviderId: IntegrationIds;\n\t/** Disambiguates connections on self-managed hosts (where one provider id spans multiple domains). */\n\tdomain?: string;\n\t/** The specific token/connection the read was pinned to, when a `connectionId` was supplied. */\n\tconnectionId?: string;\n\tmessage: string;\n\tkind: ProviderWarningKind;\n\t/** Convenience mirror of `kind === 'auth'`, scoped or not; read {@link scope} before prompting to reconnect. */\n\tisAuth: boolean;\n\t/**\n\t * Present when the failure was recorded against one sub-scope of the read (an organization, project or\n\t * repository) rather than against the connection. A scoped `auth` failure is that one scope refusing this\n\t * credential, e.g. an Azure DevOps organization that disallows third-party OAuth apps, one in another Entra\n\t * tenant, or a Conditional Access policy. Reconnecting cannot fix those, so this warning alone is no reason\n\t * to prompt for it.\n\t *\n\t * `kind` and `isAuth` are unaffected: a scoped 401 is still an authentication failure, and this field says\n\t * how far it reaches. It names at least one of its IDs, and is never set on an omission (which carries its\n\t * own {@link ProviderWarningOmission.scope}) or on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}), even when that call targeted a single organization. The exception is a batch\n\t * read's refused target, which names its scope: the Azure DevOps organization and project, the repository on\n\t * other git hosts, or the tracker's resource.\n\t *\n\t * Its ABSENCE means account-wide or unattributed, so existing handling stays correct without it.\n\t *\n\t * A scoped `auth` failure also means the credential itself was accepted. A dead token can come back as nothing\n\t * but scoped refusals wherever a read reaches its scopes without an uncached request to the connection first:\n\t * discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud) or an SDK fan-out across the\n\t * requested repositories (Bitbucket Data Center). So when a read's only auth failures are scoped, those providers\n\t * confirm the credential with one uncached check first, and a refused credential fails the whole read with an\n\t * unscoped `auth` warning. A batch read checks it only when every target was refused, since a target that\n\t * answered already proved it. A refusal the provider attributes to the credential itself, like Bitbucket's or Jira\n\t * Cloud's for a token missing the OAuth scopes a read needs, which a reconnect fixes, is published unscoped too,\n\t * once for the connection, while the scopes that answered keep their results.\n\t *\n\t * So the promise holds for a read that carries no unscoped `auth` warning of its own; one that does already asks\n\t * for a reconnect, and its other scoped refusals are not checked. Two exceptions stay scoped even then. A check\n\t * that could not complete or was denied (a network error, a throttle, a `403`, Bitbucket Data Center refusing a\n\t * credential it authenticated, or a Bitbucket Data Center project access token, whose user the check cannot\n\t * find) proves nothing, so the warnings are published unconfirmed, with no {@link cause}, and the next read\n\t * checks again. And a credential confirmed within the last minute is not probed again, so a revocation can take\n\t * up to a minute to surface.\n\t */\n\tscope?: ProviderWarningScope;\n\t/**\n\t * Why the provider request failed, when the failure itself says so: see {@link ProviderWarningCauseReason}. Switch on\n\t * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.\n\t *\n\t * `unreachable` accompanies a transport or server failure with `kind: 'other'`. Credential refusal causes are\n\t * set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the\n\t * refusals they name are indistinguishable from a dead credential until then: Azure DevOps answers a\n\t * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps\n\t * reports one today. Its absence proves nothing about the cause.\n\t */\n\tcause?: ProviderWarningCause;\n\t/**\n\t * Present when this warning describes results the read could not return even though the request itself\n\t * SUCCEEDED — a provider-enforced cap, an exhausted recovery budget, a page budget, or a sub-scope the read\n\t * did not drain.\n\t *\n\t * Its presence is the signal: an omission is not a failure. Message it as incompleteness rather than as a\n\t * failed read, and do NOT derive that from `message`, which is English prose and subject to rewording.\n\t * `kind` stays `'other'` for these, so the failure discriminant keeps meaning exactly what it meant before\n\t * this field existed.\n\t *\n\t * Whether anything would fetch the rest is a SEPARATE question — read {@link ProviderWarningOmission.recovery},\n\t * not `kind`, and never assume a retry of the same request returns more.\n\t *\n\t * `limit` / `totalCount` / `scope` are forwarded only when they are reported; most omissions carry none of\n\t * the three, so render correctly without them — and see {@link ProviderWarningOmission.limit} before\n\t * showing that figure as a result count.\n\t *\n\t * Its ABSENCE proves nothing. It is never set on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}) or from a structured scope failure — those are failures, and the field would\n\t * be a lie there. But it is also absent whenever incompleteness was reported without naming what was left\n\t * out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.\n\t */\n\tomission?: ProviderWarningOmission;\n}\n\nexport interface ProviderPageInfo {\n\t/**\n\t * The 1-based POSITION of the returned page. One convention across every paged read, page-number and\n\t * cursor paging alike, so a `getNextPageParam`-style consumer can key off it uniformly:\n\t * - the page number the provider itself reported, when it reports one (authoritative);\n\t * - else the position the caller addressed: the page encoded in a threaded page cursor, or the `page`\n\t * supplied alongside an opaque cursor (1 when the caller supplied no `page`);\n\t * - else, for a read that can be advanced by page number, the requested `page` — the provider either\n\t * honored it or the internal drain reached it (see the paging contract on `IntegrationManager`);\n\t * - else 1: a cursor-only read with no cursor threaded back ignores a requested page number and answers\n\t * with its first page, so echoing the request would mislabel page 1 as page N.\n\t *\n\t * A page past the provider's terminal cursor still reports the requested `page` with empty `items` — an\n\t * empty page N, never page N-1 relabeled.\n\t *\n\t * Continue a cursor-paged read from `cursor`, NOT from `currentPage + 1`: a cursor-only host can't be\n\t * addressed by number, so this is a position label and not always a resumable request parameter.\n\t */\n\tcurrentPage: number;\n\t/**\n\t * The page size IN EFFECT, which is not always the one requested — it is whichever of these the read can\n\t * actually vouch for:\n\t * - the size the provider reported for the page it served (numbered-page hosts), else\n\t * - the `itemsPerPage` that was applied to the request (repo-scoped reads, which forward it), else\n\t * - `items.length`, for a read that takes no page size at all: the account-wide PR/issue reads, `listRepos`\n\t * (cursor-only), and `listIssueTrackerIssuesPage`, whose `itemsPerPage` counts PROJECTS rather than issues.\n\t *\n\t * So it is a description of the page, not an echo of the request: don't derive a total from\n\t * `currentPage * itemsPerPage`, and don't read `items.length < itemsPerPage` as \"last page\" — `hasMore` /\n\t * `cursor` are the only continuation signals.\n\t */\n\titemsPerPage: number;\n\t/**\n\t * A COMPLETENESS assertion, not a mode flag: `true` only when a sweep drained every page of every target\n\t * without truncating or failing (`!truncated && !fetchFailed`). Set exclusively by `sweepPullRequests` /\n\t * `sweepClosedPullRequests` and absent on every paged read, so `allPages !== true` does NOT by itself mean\n\t * \"this was a single page\".\n\t *\n\t * This is the signal to gate on before caching a sweep as authoritative: unlike `truncated` it is `false`\n\t * for BOTH kinds of incompleteness, so it can't be misread as a benign cap.\n\t */\n\tallPages?: boolean;\n\t/**\n\t * True when the read stopped before it could confirm it had everything. Deliberately does NOT distinguish\n\t * WHY, and a consumer must not assume it means a provider ceiling — a sweep sets it from three different\n\t * origins:\n\t * - a provider-side ceiling or per-scope backstop propagated from below (e.g. GitHub Search's 1000-result\n\t * cap): the missing items are NOT reachable, so this really is \"complete to the provider's limit\";\n\t * - the sweep's OWN `maxPages` cap (default 100) with a usable cursor still in hand: the missing items ARE\n\t * reachable and were simply not fetched — raise `maxPages` to get them;\n\t * - the provider reporting another page but handing back no usable cursor: not reachable.\n\t *\n\t * Only the second is recoverable, and a sweep exposes no continuation cursor, so treating a truncated sweep\n\t * as a complete set silently drops reachable data in that case. Prefer `allPages` for the cache decision.\n\t */\n\ttruncated?: boolean;\n}\n\nexport interface ProviderResult<T> {\n\titems: T[];\n\twarnings: ProviderWarning[];\n\t/**\n\t * True when `items` is incomplete — distinguishes a failed or truncated flat read from a genuinely empty\n\t * result. Paged results additionally use `page.truncated` when a completed page could not confirm it\n\t * drained everything.\n\t */\n\tfetchFailed?: boolean;\n}\n\nexport interface ProviderPagedResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Opaque cursor for cursor-based paging (GraphQL, per-repo/per-project cursors, etc.). For hosts that\n\t * require the previous page's cursor, consumers should pass this value back to the next `cursor` option.\n\t * A retry-only cursor may remain when `hasMore` is false: `hasMore` means automatic forward progress,\n\t * while explicitly reusing such a cursor retries failed work without creating an infinite paging loop.\n\t */\n\tcursor?: string;\n}\n\nexport interface ProviderSweepResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Providers whose top-level read failed before returning any usable page. Partial per-scope failures stay\n\t * represented by `warnings` + `fetchFailed` and are intentionally excluded.\n\t */\n\tfailedProviderIds: IntegrationIds[];\n\t/**\n\t * Providers whose read started successfully but could not produce an authoritative complete slice (for\n\t * example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit).\n\t * Kept separate from\n\t * `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot\n\t * for a provider whose returned slice has a gap.\n\t */\n\tincompleteProviderIds: IntegrationIds[];\n}\n\nexport interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {\n\tbroadenedProviderIds: IntegrationIds[];\n\t/** Providers for which no requested org produced a usable issue result. */\n\tfailedProviderIds: IntegrationIds[];\n\t/** Providers with both a usable org result and at least one failed or truncated org slice. */\n\tincompleteProviderIds: IntegrationIds[];\n\tfanOutCount: number;\n}\n\nexport type RepositoryResolutionStatus =\n\t| 'resolved'\n\t| 'not-found'\n\t| 'unauthorized'\n\t| 'unsupported-provider'\n\t| 'invalid-remote-url'\n\t| 'host-mismatch'\n\t| 'undetermined';\n\nexport interface RepositoryIdentity {\n\tproviderId: IntegrationIds;\n\tdomain: string;\n\t/** The provider's canonical owner/namespace (follows renames), falling back to the parsed remote when omitted. */\n\towner: string;\n\t/** The provider's canonical repo name (follows renames), falling back to the parsed remote when omitted. */\n\tname: string;\n\tproject?: string;\n\t/** The original input remote URL, so the caller can key the resolution back to the remote it asked about. */\n\tremoteUrl: string;\n\t/**\n\t * True when the provider's canonical owner/name differ from what the input remote URL carried — i.e. the\n\t * repo was renamed/moved host-side and the local remote is stale. Case-insensitive, mirroring gkcli's\n\t * `EqualFold` compare so hosts that echo the input casing (e.g. Bitbucket Server/Azure) aren't flagged.\n\t */\n\trenamed: boolean;\n}\n\nexport interface RepositoryResolution {\n\tstatus: RepositoryResolutionStatus;\n\tidentity?: RepositoryIdentity;\n\twarning?: ProviderWarning;\n}\n\nexport interface ResolveRepositoryResult {\n\tresolution: RepositoryResolution;\n}\n\nconst maxProviderWarningMessageLength = 500;\n\n/** A caught error's message as a warning can carry it: an HTML page becomes its status, and prose is capped. */\nexport function providerWarningMessage(ex: unknown): string {\n\tconst raw = (ex instanceof Error ? ex.message : String(ex)).trim();\n\tconst carrier = ex as { status?: unknown; response?: { status?: unknown } } | null | undefined;\n\tconst status =\n\t\ttypeof carrier?.status === 'number'\n\t\t\t? carrier.status\n\t\t\t: typeof carrier?.response?.status === 'number'\n\t\t\t\t? carrier.response.status\n\t\t\t\t: undefined;\n\n\t// Gateways commonly answer API requests with a complete HTML error page. That payload is neither actionable\n\t// nor suitable for a warning DTO consumed by UIs and logs; preserve the status without leaking the document.\n\tif (/<(?:!doctype\\s+html|html|head|body)\\b/i.test(raw)) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\n\tif (!raw) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\treturn raw.length > maxProviderWarningMessageLength\n\t\t? `${raw.slice(0, maxProviderWarningMessageLength - 3)}...`\n\t\t: raw;\n}\n\n/**\n * Normalized org/workspace/group shape returned by the provider facade.\n * `name` is the provider identifier to pass to follow-up reads, while `org` is an optional display label.\n */\nexport interface ProviderOrganization {\n\tid: string;\n\tproviderId: IntegrationIds;\n\tname: string;\n\torg?: string;\n\turl: string;\n}\n\n/** Normalized repository shape returned by the provider facade. */\nexport interface ProviderRepositoryShape {\n\tid: string;\n\tnamespace: string;\n\tname: string;\n\t/** Azure DevOps project; `undefined` for hosts without a project layer. */\n\tproject?: string;\n\t/** Web (browser) URL, when the provider exposes it. */\n\turl?: string;\n\t/** HTTPS clone URL, when available. */\n\tcloneUrlHttps?: string;\n\t/** SSH clone URL, when available. */\n\tcloneUrlSsh?: string;\n\t/** Default branch name, when the provider reports it. */\n\tdefaultBranch?: string;\n}\n\n/**\n * Classifies a caught provider exception into a neutral {@link ProviderWarning}. Ordering matters:\n * `instanceof` is checked most-specific-first so a rate-limit isn't mislabeled as a generic error and\n * a 404 isn't mislabeled as auth.\n */\nexport function toProviderWarning(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tex: unknown,\n): ProviderWarning {\n\tlet kind: ProviderWarning['kind'];\n\tif (ex instanceof AuthenticationError) {\n\t\tkind = 'auth';\n\t} else if (ex instanceof RequestRateLimitError) {\n\t\tkind = 'rate-limit';\n\t} else if (ex instanceof RequestNotFoundError) {\n\t\tkind = 'not-found';\n\t} else if (isUnsupportedSortError(ex)) {\n\t\tkind = 'unsupported';\n\t} else {\n\t\tkind = 'other';\n\t}\n\n\treturn {\n\t\tproviderId: providerId,\n\t\tdomain: domain,\n\t\tconnectionId: connectionId,\n\t\tmessage: providerWarningMessage(ex),\n\t\tkind: kind,\n\t\tisAuth: kind === 'auth',\n\t\t...(kind === 'other' && isProviderUnreachableError(ex) ? { cause: { reason: 'unreachable' as const } } : {}),\n\t};\n}\n\n/**\n * A scope's identity, as the stable string that keys it.\n *\n * The single definition of what \"the same scope\" means, shared by the failure and omission dedup keys in\n * `providerPaging.ts` and by {@link providerWarningKey} below, so a scope gaining a field is one edit rather\n * than three. `providerId` is included: the same repository ID under two providers is two scopes.\n *\n * The parameter is structural rather than the SDK's `CollectionScope` so this module keeps naming no\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`) while still serving its SDK-facing\n * callers, which pass that type in unchanged.\n */\nexport function collectionScopeKey(scope: ProviderWarningOmissionScope | undefined): string {\n\treturn [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(\n\t\t' ',\n\t);\n}\n\n/**\n * The omission's contribution to a warning's identity — empty when there is none, so a warning without an\n * omission keeps deduping exactly as it did before the field existed.\n *\n * Two omissions of different kinds do produce different `message` values today, so message alone would still\n * separate them. That is incidental: the premise of `omission` is that consumers must not depend on prose\n * carrying the distinguishing fact, and this key must not either.\n */\nfunction providerWarningOmissionKey(omission: ProviderWarningOmission | undefined): string {\n\tif (omission == null) return '';\n\n\treturn [\n\t\tomission.kind,\n\t\tomission.recovery,\n\t\tomission.limit ?? '',\n\t\tomission.totalCount ?? '',\n\t\tcollectionScopeKey(omission.scope),\n\t].join(' ');\n}\n\n/**\n * Strips the omission from every warning in `warnings` when the read as a whole failed.\n *\n * An omission asserts the request SUCCEEDED, and a drain only learns it failed AFTER it may have emitted one:\n * an early page can report its own truncation and then a later page can die. Deciding per warning, at the\n * moment each is built, cannot see that future — so the aggregate is reconciled once, here, where\n * `fetchFailed` is final. Call it at the point a read returns its `fetchFailed`.\n *\n * Re-dedupes as it goes: the omission is part of a warning's identity, so two warnings that differed only\n * there become identical once it is gone, and the array's contract is that no two entries are equal.\n *\n * Mutates in place: the warning array is the one being returned, and callers accumulate into it across pages.\n */\nexport function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void {\n\tif (!fetchFailed || !warnings.some(w => w.omission != null)) return;\n\n\tconst reconciled: ProviderWarning[] = [];\n\tfor (const warning of warnings) {\n\t\tif (warning.omission == null) {\n\t\t\tappendDedupedWarning(reconciled, warning);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { omission: _omission, ...rest } = warning;\n\t\tappendDedupedWarning(reconciled, rest);\n\t}\n\n\twarnings.splice(0, warnings.length, ...reconciled);\n}\n\n/**\n * A stable key for deduplicating warnings accumulated across drained pages / fan-out scopes.\n *\n * The scope and the cause are keyed for the reason the omission is (see {@link providerWarningOmissionKey}): today's\n * failure messages happen to spell them out, but two failures that differ in either must stay two warnings even if\n * they don't.\n *\n * `message` stays LAST. It is the only free-form segment — provider prose, spaces and all — so anything\n * appended after it could be impersonated by a message that happens to end in the same text.\n */\nfunction providerWarningKey(warning: ProviderWarning): string {\n\treturn [\n\t\twarning.providerId,\n\t\twarning.connectionId ?? '',\n\t\twarning.domain ?? '',\n\t\twarning.kind,\n\t\tcollectionScopeKey(warning.scope),\n\t\twarning.cause?.reason ?? '',\n\t\tproviderWarningOmissionKey(warning.omission),\n\t\twarning.message,\n\t].join(' ');\n}\n\n/**\n * Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the\n * structured scope, cause and omission when present) is absent.\n */\nexport function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void {\n\tconst key = providerWarningKey(warning);\n\tif (into.some(existing => providerWarningKey(existing) === key)) return;\n\n\tinto.push(warning);\n}\n"],"mappings":";;;;AAyWA,MAAM,kCAAkC;;AAGxC,SAAgB,uBAAuB,IAAqB;CAC3D,MAAM,OAAO,cAAc,QAAQ,GAAG,UAAU,OAAO,EAAE,EAAA,CAAG,KAAK;CACjE,MAAM,UAAU;CAChB,MAAM,SACL,OAAO,SAAS,WAAW,WACxB,QAAQ,SACR,OAAO,SAAS,UAAU,WAAW,WACpC,QAAQ,SAAS,SACjB,KAAA;CAIL,IAAI,yCAAyC,KAAK,GAAG,GACpD,OAAO,UAAU,OAAO,uCAAuC,OAAO,KAAK;CAG5E,IAAI,CAAC,KACJ,OAAO,UAAU,OAAO,uCAAuC,OAAO,KAAK;CAE5E,OAAO,IAAI,SAAS,kCACjB,GAAG,IAAI,MAAM,GAAG,GAAmC,EAAE,OACrD;AACJ;;;;;;AAoCA,SAAgB,kBACf,YACA,QACA,cACA,IACkB;CAClB,IAAI;CACJ,IAAI,cAAc,qBACjB,OAAO;MACD,IAAI,cAAc,uBACxB,OAAO;MACD,IAAI,cAAc,sBACxB,OAAO;MACD,IAAI,uBAAuB,EAAE,GACnC,OAAO;MAEP,OAAO;CAGR,OAAO;EACM;EACJ;EACM;EACd,SAAS,uBAAuB,EAAE;EAC5B;EACN,QAAQ,SAAS;EACjB,GAAI,SAAS,WAAW,2BAA2B,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,cAAuB,EAAE,IAAI,CAAC;CAC3G;AACD;;;;;;;;;;;;AAaA,SAAgB,mBAAmB,OAAyD;CAC3F,OAAO;EAAC,OAAO,cAAc;EAAI,OAAO,cAAc;EAAI,OAAO,aAAa;EAAI,OAAO,gBAAgB;CAAE,CAAC,CAAC,KAC5G,GACD;AACD;;;;;;;;;AAUA,SAAS,2BAA2B,UAAuD;CAC1F,IAAI,YAAY,MAAM,OAAO;CAE7B,OAAO;EACN,SAAS;EACT,SAAS;EACT,SAAS,SAAS;EAClB,SAAS,cAAc;EACvB,mBAAmB,SAAS,KAAK;CAClC,CAAC,CAAC,KAAK,GAAG;AACX;;;;;;;;;;;;;;AAeA,SAAgB,8BAA8B,UAA6B,aAA4B;CACtG,IAAI,CAAC,eAAe,CAAC,SAAS,MAAK,MAAK,EAAE,YAAY,IAAI,GAAG;CAE7D,MAAM,aAAgC,CAAC;CACvC,KAAK,MAAM,WAAW,UAAU;EAC/B,IAAI,QAAQ,YAAY,MAAM;GAC7B,qBAAqB,YAAY,OAAO;GACxC;EACD;EAEA,MAAM,EAAE,UAAU,WAAW,GAAG,SAAS;EACzC,qBAAqB,YAAY,IAAI;CACtC;CAEA,SAAS,OAAO,GAAG,SAAS,QAAQ,GAAG,UAAU;AAClD;;;;;;;;;;;AAYA,SAAS,mBAAmB,SAAkC;CAC7D,OAAO;EACN,QAAQ;EACR,QAAQ,gBAAgB;EACxB,QAAQ,UAAU;EAClB,QAAQ;EACR,mBAAmB,QAAQ,KAAK;EAChC,QAAQ,OAAO,UAAU;EACzB,2BAA2B,QAAQ,QAAQ;EAC3C,QAAQ;CACT,CAAC,CAAC,KAAK,GAAG;AACX;;;;;AAMA,SAAgB,qBAAqB,MAAyB,SAAgC;CAC7F,MAAM,MAAM,mBAAmB,OAAO;CACtC,IAAI,KAAK,MAAK,aAAY,mBAAmB,QAAQ,MAAM,GAAG,GAAG;CAEjE,KAAK,KAAK,OAAO;AAClB"}
|
|
1
|
+
{"version":3,"file":"results.js","names":[],"sources":["../../../../plus/integrations/src/results.ts"],"sourcesContent":["import { isUnsupportedSortError } from '@gitkraken/provider-apis';\nimport { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '@gitlens/git/errors.js';\nimport type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport type { IntegrationIds } from './constants.js';\nimport { isProviderUnreachableError } from './errors.js';\n\nexport interface ConnectionStateChangeEvent {\n\tkey: string;\n\treason: 'connected' | 'disconnected';\n}\n\n/**\n * A per-provider, per-connection warning surfaced alongside (partial) read results. Consumers use\n * these to drive auth recovery, retry, or truncation messaging without the read itself throwing.\n *\n * `kind` is the programmatic discriminant (a rate-limit is retryable, a 404 is not, an unsupported capability\n * never succeeds on retry, an auth failure needs re-connection unless {@link ProviderWarning.scope} confines\n * it); `isAuth` is retained as a convenience mirror of `kind === 'auth'`.\n *\n * `'unsupported'`: the provider — or this integration of it — lacks the capability the request asked for: a\n * surface (issues on Bitbucket, pull requests on an issue tracker, a batch read, org/repo discovery,\n * current-account lookup, project-scoped or account-wide issue reads), or a filter, sort, state, search\n * criterion or option combination it cannot express. Retrying the same request never succeeds; a different\n * provider, scope or request may. NOT unsupported (these stay `'other'`): malformed or contradictory caller\n * input the caller fixes in the request itself (a duplicate batch key, invalid target numbers, an empty owner,\n * an unusable scope), incompleteness or omission warnings, and unclassified errors.\n */\nexport type ProviderWarningKind = 'auth' | 'rate-limit' | 'not-found' | 'no-connection' | 'unsupported' | 'other';\n\n/**\n * Why a read that SUCCEEDED still withheld results:\n * - `provider-limit`: the provider refuses to serve past a cap it enforces (GitHub search's 1,000-result\n * ceiling, Trello's `cards_limit`), so the excess is unreachable through that query.\n * - `recovery-budget`: the internal partitioned recovery stopped spending upstream requests before it ran\n * out of partitions to visit.\n * - `pagination-incomplete`: pages were left unread — a sub-scope of a multi-scope read that was not drained,\n * a drain that stopped at its own page backstop, or a provider that advertised another page and gave no way\n * to reach it. These have DIFFERENT remedies, so the kind alone does not say whether more can be fetched;\n * read {@link ProviderWarningOmission.recovery} for that.\n *\n * Mirrors the vocabulary the SDK reports. Declared here rather than imported so this module stays free of\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`); `collectionMetadata.ts` holds the\n * compile-time link, so an SDK bump that adds a kind fails the build at that boundary.\n */\nexport type ProviderWarningOmissionKind = 'provider-limit' | 'recovery-budget' | 'pagination-incomplete';\n\n/**\n * How a consumer can recover omitted results. `page-budget` reruns with a larger budget; `narrow-scope`\n * requests a smaller server-side scope; `none` promises neither. Switch on the exact value because only\n * `page-budget` is a fetch-more action.\n */\nexport type ProviderWarningOmissionRecovery = 'none' | 'page-budget' | 'narrow-scope';\n\n/**\n * The part of a provider a warning is confined to: one resource (organization, workspace, site), project or\n * repository. All fields optional; see {@link ProviderWarning.scope} for when one is forwarded.\n */\nexport interface ProviderWarningScope {\n\t/**\n\t * The resource as the read addressed it: its id on most reads, its name on the few that address it by name\n\t * (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both\n\t * `ProviderOrganization.id` and `name`, and a project's `orgId` where it has one.\n\t */\n\tresourceId?: string;\n\t/**\n\t * The project as the read addressed it. For a Jira project read, the key on Jira Cloud (the project's `name` in\n\t * `listProjects`) and the numeric id on Jira Data Center.\n\t */\n\tprojectId?: string;\n\trepositoryId?: string;\n}\n\n/**\n * Why a provider request failed, when the failure itself says so:\n * - `unreachable`: a transport failure or server-side error prevented the provider from answering. The connection\n * still exists; this accompanies `kind: 'other'`, never an authentication warning.\n * - `oauth-app-not-allowed`: the organization does not let third-party OAuth apps in. On Azure DevOps this is the\n * organization policy \"Third-party application access via OAuth\", off by default for new organizations. An\n * organization admin enables it, or the user connects with a personal access token instead, which the policy\n * does not govern.\n * - `access-denied`: the account has no access to that organization or project (not a member, or lacking the\n * permission). Someone who administers it has to grant access.\n * - `conditional-access`: a Microsoft Entra Conditional Access policy blocked the request (Azure DevOps\n * `VS403463`). Its admin has to exempt the request, e.g. by location or device.\n *\n * None of these is healed by reconnecting, which is the point of naming them.\n */\nexport type ProviderWarningCauseReason =\n\t| 'oauth-app-not-allowed'\n\t| 'access-denied'\n\t| 'conditional-access'\n\t| 'unreachable';\n\n/** See {@link ProviderWarning.cause}. */\nexport interface ProviderWarningCause {\n\treason: ProviderWarningCauseReason;\n\t/** The provider's own error code, when it reports one (Azure DevOps `TF400813`, `VS403463`). */\n\tcode?: string;\n\t/** Where the setting behind the refusal is changed, when this layer can address it. */\n\tremedyUrl?: string;\n}\n\n/** Which repository / project / resource an omission is attributed to. All fields optional; a scope may name none. */\nexport interface ProviderWarningOmissionScope extends ProviderWarningScope {\n\tproviderId?: string;\n}\n\nexport interface ProviderWarningOmission {\n\tkind: ProviderWarningOmissionKind;\n\t/**\n\t * Whether anything would fetch the missing items, and what. Gate a \"load more\" affordance on this rather\n\t * than on `kind` — see {@link ProviderWarningOmissionRecovery}.\n\t */\n\trecovery: ProviderWarningOmissionRecovery;\n\t/**\n\t * The cap the provider enforces, when it reports one. NOT a result count for every kind: on\n\t * `recovery-budget` this is a REQUEST budget and must not be shown to a user as a number of results.\n\t */\n\tlimit?: number;\n\t/** Total matches the provider reported, when it reports one. Only GitHub's search cap does today. */\n\ttotalCount?: number;\n\t/**\n\t * The order the reachable window was selected under, as `field:direction`, when the omission is a result\n\t * ceiling.\n\t *\n\t * Present so a consumer can word its own \"N matched, showing the first M\" without assuming WHICH M: the\n\t * provider serves the top M under this key, so the sentence is only true if it names it. Absent for every\n\t * omission that isn't a ceiling, where no window was selected by an order.\n\t *\n\t * A union of both vocabularies rather than the issue one alone: `PullRequestSorting` happens to be a subset\n\t * today, so narrowing this to {@link IssueSorting} would still compile while describing only half the reads\n\t * that set it — and would break the day the PR search gains a key the issue search has no name for.\n\t */\n\tsort?: IssueSorting | PullRequestSorting;\n\t/** Which repository / project / resource was affected, when one is attributed. */\n\tscope?: ProviderWarningOmissionScope;\n}\n\nexport interface ProviderWarning {\n\tproviderId: IntegrationIds;\n\t/** Disambiguates connections on self-managed hosts (where one provider id spans multiple domains). */\n\tdomain?: string;\n\t/** The specific token/connection the read was pinned to, when a `connectionId` was supplied. */\n\tconnectionId?: string;\n\tmessage: string;\n\tkind: ProviderWarningKind;\n\t/** Convenience mirror of `kind === 'auth'`, scoped or not; read {@link scope} before prompting to reconnect. */\n\tisAuth: boolean;\n\t/**\n\t * Present when the failure was recorded against one sub-scope of the read (an organization, project or\n\t * repository) rather than against the connection. A scoped `auth` failure is that one scope refusing this\n\t * credential, e.g. an Azure DevOps organization that disallows third-party OAuth apps, one in another Entra\n\t * tenant, or a Conditional Access policy. Reconnecting cannot fix those, so this warning alone is no reason\n\t * to prompt for it.\n\t *\n\t * `kind` and `isAuth` are unaffected: a scoped 401 is still an authentication failure, and this field says\n\t * how far it reaches. It names at least one of its IDs, and is never set on an omission (which carries its\n\t * own {@link ProviderWarningOmission.scope}) or on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}), even when that call targeted a single organization. The exception is a batch\n\t * read's refused target, which names its scope: the Azure DevOps organization and project, the repository on\n\t * other git hosts, or the tracker's resource.\n\t *\n\t * Its ABSENCE means account-wide or unattributed, so existing handling stays correct without it.\n\t *\n\t * A scoped `auth` failure also means the credential itself was accepted. A dead token can come back as nothing\n\t * but scoped refusals wherever a read reaches its scopes without an uncached request to the connection first:\n\t * discovery served from a per-token cache (Azure DevOps, Bitbucket, Jira Cloud) or an SDK fan-out across the\n\t * requested repositories (Bitbucket Data Center). So when a read's only auth failures are scoped, those providers\n\t * confirm the credential with one uncached check first, and a refused credential fails the whole read with an\n\t * unscoped `auth` warning. A batch read checks it only when every target was refused, since a target that\n\t * answered already proved it. A refusal the provider attributes to the credential itself, like Bitbucket's or Jira\n\t * Cloud's for a token missing the OAuth scopes a read needs, which a reconnect fixes, is published unscoped too,\n\t * once for the connection, while the scopes that answered keep their results.\n\t *\n\t * So the promise holds for a read that carries no unscoped `auth` warning of its own; one that does already asks\n\t * for a reconnect, and its other scoped refusals are not checked. Two exceptions stay scoped even then. A check\n\t * that could not complete or was denied (a network error, a throttle, a `403`, Bitbucket Data Center refusing a\n\t * credential it authenticated, or a Bitbucket Data Center project access token, whose user the check cannot\n\t * find) proves nothing, so the warnings are published unconfirmed, with no {@link cause}, and the next read\n\t * checks again. And a credential confirmed within the last minute is not probed again, so a revocation can take\n\t * up to a minute to surface.\n\t */\n\tscope?: ProviderWarningScope;\n\t/**\n\t * Why the provider request failed, when the failure itself says so: see {@link ProviderWarningCauseReason}. Switch on\n\t * `reason` to recommend a fix instead of a reconnect; `message` then carries the same explanation as prose.\n\t *\n\t * `unreachable` accompanies a transport or server failure with `kind: 'other'`. Credential refusal causes are\n\t * set only on a scoped `auth` warning whose credential was confirmed first (see {@link scope}), because the\n\t * refusals they name are indistinguishable from a dead credential until then: Azure DevOps answers a\n\t * third-party OAuth app its organization disallows exactly as it answers an expired token. Only Azure DevOps\n\t * reports one today. Its absence proves nothing about the cause.\n\t */\n\tcause?: ProviderWarningCause;\n\t/**\n\t * Present when this warning describes results the read could not return even though the request itself\n\t * SUCCEEDED — a provider-enforced cap, an exhausted recovery budget, a page budget, or a sub-scope the read\n\t * did not drain.\n\t *\n\t * Its presence is the signal: an omission is not a failure. Message it as incompleteness rather than as a\n\t * failed read, and do NOT derive that from `message`, which is English prose and subject to rewording.\n\t * `kind` stays `'other'` for these, so the failure discriminant keeps meaning exactly what it meant before\n\t * this field existed.\n\t *\n\t * Whether anything would fetch the rest is a SEPARATE question — read {@link ProviderWarningOmission.recovery},\n\t * not `kind`, and never assume a retry of the same request returns more.\n\t *\n\t * `limit` / `totalCount` / `scope` are forwarded only when they are reported; most omissions carry none of\n\t * the three, so render correctly without them — and see {@link ProviderWarningOmission.limit} before\n\t * showing that figure as a result count.\n\t *\n\t * Its ABSENCE proves nothing. It is never set on a warning derived from a caught exception (see\n\t * {@link toProviderWarning}) or from a structured scope failure — those are failures, and the field would\n\t * be a lie there. But it is also absent whenever incompleteness was reported without naming what was left\n\t * out, so treat a bare `kind: 'other'` warning as unclassified rather than as a proven failure.\n\t */\n\tomission?: ProviderWarningOmission;\n\t/**\n\t * The keys of the batch targets this warning concerns: the target a per-target failure was refused or failed\n\t * for, or every target of an integration call that failed as a whole. So a caller that sends several callers'\n\t * targets in one call can hand a failure only to the targets it belongs to.\n\t *\n\t * Its ABSENCE means the whole call. It is absent on every read that is not a batch read, and on a batch read's\n\t * refusal of the whole call before any target is read (a duplicate key, an invalid target, an unknown etag\n\t * include, no resolvable connection, the wrong surface), which concerns every target alike.\n\t *\n\t * It is not part of a warning's identity: two warnings that differ only here are one warning, for the union of\n\t * their targets (see {@link appendDedupedWarning}).\n\t */\n\ttargetKeys?: string[];\n}\n\nexport interface ProviderPageInfo {\n\t/**\n\t * The 1-based POSITION of the returned page. One convention across every paged read, page-number and\n\t * cursor paging alike, so a `getNextPageParam`-style consumer can key off it uniformly:\n\t * - the page number the provider itself reported, when it reports one (authoritative);\n\t * - else the position the caller addressed: the page encoded in a threaded page cursor, or the `page`\n\t * supplied alongside an opaque cursor (1 when the caller supplied no `page`);\n\t * - else, for a read that can be advanced by page number, the requested `page` — the provider either\n\t * honored it or the internal drain reached it (see the paging contract on `IntegrationManager`);\n\t * - else 1: a cursor-only read with no cursor threaded back ignores a requested page number and answers\n\t * with its first page, so echoing the request would mislabel page 1 as page N.\n\t *\n\t * A page past the provider's terminal cursor still reports the requested `page` with empty `items` — an\n\t * empty page N, never page N-1 relabeled.\n\t *\n\t * Continue a cursor-paged read from `cursor`, NOT from `currentPage + 1`: a cursor-only host can't be\n\t * addressed by number, so this is a position label and not always a resumable request parameter.\n\t */\n\tcurrentPage: number;\n\t/**\n\t * The page size IN EFFECT, which is not always the one requested — it is whichever of these the read can\n\t * actually vouch for:\n\t * - the size the provider reported for the page it served (numbered-page hosts), else\n\t * - the `itemsPerPage` that was applied to the request (repo-scoped reads, which forward it), else\n\t * - `items.length`, for a read that takes no page size at all: the account-wide PR/issue reads, `listRepos`\n\t * (cursor-only), and `listIssueTrackerIssuesPage`, whose `itemsPerPage` counts PROJECTS rather than issues.\n\t *\n\t * So it is a description of the page, not an echo of the request: don't derive a total from\n\t * `currentPage * itemsPerPage`, and don't read `items.length < itemsPerPage` as \"last page\" — `hasMore` /\n\t * `cursor` are the only continuation signals.\n\t */\n\titemsPerPage: number;\n\t/**\n\t * A COMPLETENESS assertion, not a mode flag: `true` only when a sweep drained every page of every target\n\t * without truncating or failing (`!truncated && !fetchFailed`). Set exclusively by `sweepPullRequests` /\n\t * `sweepClosedPullRequests` and absent on every paged read, so `allPages !== true` does NOT by itself mean\n\t * \"this was a single page\".\n\t *\n\t * This is the signal to gate on before caching a sweep as authoritative: unlike `truncated` it is `false`\n\t * for BOTH kinds of incompleteness, so it can't be misread as a benign cap.\n\t */\n\tallPages?: boolean;\n\t/**\n\t * True when the read stopped before it could confirm it had everything. Deliberately does NOT distinguish\n\t * WHY, and a consumer must not assume it means a provider ceiling — a sweep sets it from three different\n\t * origins:\n\t * - a provider-side ceiling or per-scope backstop propagated from below (e.g. GitHub Search's 1000-result\n\t * cap): the missing items are NOT reachable, so this really is \"complete to the provider's limit\";\n\t * - the sweep's OWN `maxPages` cap (default 100) with a usable cursor still in hand: the missing items ARE\n\t * reachable and were simply not fetched — raise `maxPages` to get them;\n\t * - the provider reporting another page but handing back no usable cursor: not reachable.\n\t *\n\t * Only the second is recoverable, and a sweep exposes no continuation cursor, so treating a truncated sweep\n\t * as a complete set silently drops reachable data in that case. Prefer `allPages` for the cache decision.\n\t */\n\ttruncated?: boolean;\n}\n\nexport interface ProviderResult<T> {\n\titems: T[];\n\twarnings: ProviderWarning[];\n\t/**\n\t * True when `items` is incomplete — distinguishes a failed or truncated flat read from a genuinely empty\n\t * result. Paged results additionally use `page.truncated` when a completed page could not confirm it\n\t * drained everything.\n\t */\n\tfetchFailed?: boolean;\n}\n\nexport interface ProviderPagedResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Opaque cursor for cursor-based paging (GraphQL, per-repo/per-project cursors, etc.). For hosts that\n\t * require the previous page's cursor, consumers should pass this value back to the next `cursor` option.\n\t * A retry-only cursor may remain when `hasMore` is false: `hasMore` means automatic forward progress,\n\t * while explicitly reusing such a cursor retries failed work without creating an infinite paging loop.\n\t */\n\tcursor?: string;\n}\n\nexport interface ProviderSweepResult<T> extends ProviderResult<T> {\n\tpage: ProviderPageInfo;\n\thasMore: boolean;\n\t/**\n\t * Providers whose top-level read failed before returning any usable page. Partial per-scope failures stay\n\t * represented by `warnings` + `fetchFailed` and are intentionally excluded.\n\t */\n\tfailedProviderIds: IntegrationIds[];\n\t/**\n\t * Providers whose read started successfully but could not produce an authoritative complete slice (for\n\t * example, a later page failed, collection metadata reported a partial scope, or a paging backstop was hit).\n\t * Kept separate from\n\t * `failedProviderIds` so consumers can accept healthy sibling providers while preserving an older snapshot\n\t * for a provider whose returned slice has a gap.\n\t */\n\tincompleteProviderIds: IntegrationIds[];\n}\n\nexport interface ProviderBroadenResult<T> extends ProviderPagedResult<T> {\n\tbroadenedProviderIds: IntegrationIds[];\n\t/** Providers for which no requested org produced a usable issue result. */\n\tfailedProviderIds: IntegrationIds[];\n\t/** Providers with both a usable org result and at least one failed or truncated org slice. */\n\tincompleteProviderIds: IntegrationIds[];\n\tfanOutCount: number;\n}\n\nexport type RepositoryResolutionStatus =\n\t| 'resolved'\n\t| 'not-found'\n\t| 'unauthorized'\n\t| 'unsupported-provider'\n\t| 'invalid-remote-url'\n\t| 'host-mismatch'\n\t| 'undetermined';\n\nexport interface RepositoryIdentity {\n\tproviderId: IntegrationIds;\n\tdomain: string;\n\t/** The provider's canonical owner/namespace (follows renames), falling back to the parsed remote when omitted. */\n\towner: string;\n\t/** The provider's canonical repo name (follows renames), falling back to the parsed remote when omitted. */\n\tname: string;\n\tproject?: string;\n\t/** The original input remote URL, so the caller can key the resolution back to the remote it asked about. */\n\tremoteUrl: string;\n\t/**\n\t * True when the provider's canonical owner/name differ from what the input remote URL carried — i.e. the\n\t * repo was renamed/moved host-side and the local remote is stale. Case-insensitive, mirroring gkcli's\n\t * `EqualFold` compare so hosts that echo the input casing (e.g. Bitbucket Server/Azure) aren't flagged.\n\t */\n\trenamed: boolean;\n}\n\nexport interface RepositoryResolution {\n\tstatus: RepositoryResolutionStatus;\n\tidentity?: RepositoryIdentity;\n\twarning?: ProviderWarning;\n}\n\nexport interface ResolveRepositoryResult {\n\tresolution: RepositoryResolution;\n}\n\nconst maxProviderWarningMessageLength = 500;\n\n/** A caught error's message as a warning can carry it: an HTML page becomes its status, and prose is capped. */\nexport function providerWarningMessage(ex: unknown): string {\n\tconst raw = (ex instanceof Error ? ex.message : String(ex)).trim();\n\tconst carrier = ex as { status?: unknown; response?: { status?: unknown } } | null | undefined;\n\tconst status =\n\t\ttypeof carrier?.status === 'number'\n\t\t\t? carrier.status\n\t\t\t: typeof carrier?.response?.status === 'number'\n\t\t\t\t? carrier.response.status\n\t\t\t\t: undefined;\n\n\t// Gateways commonly answer API requests with a complete HTML error page. That payload is neither actionable\n\t// nor suitable for a warning DTO consumed by UIs and logs; preserve the status without leaking the document.\n\tif (/<(?:!doctype\\s+html|html|head|body)\\b/i.test(raw)) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\n\tif (!raw) {\n\t\treturn status != null ? `Provider request failed with status ${status}.` : 'Provider request failed.';\n\t}\n\treturn raw.length > maxProviderWarningMessageLength\n\t\t? `${raw.slice(0, maxProviderWarningMessageLength - 3)}...`\n\t\t: raw;\n}\n\n/**\n * Normalized org/workspace/group shape returned by the provider facade.\n * `name` is the provider identifier to pass to follow-up reads (a Jira project's key), while `org` is an optional\n * display label of the parent. Where that label can't address the parent, `orgId` is the parent's stable id to pass\n * as `org` instead (a Jira site's id, or a Data Center instance's domain).\n *\n * Jira site rows (`listOrgs`) are the exception: their `name` is the site's label, and a Jira read selects a site only\n * by its `id` (or a project row's `orgId`).\n */\nexport interface ProviderOrganization {\n\tid: string;\n\tproviderId: IntegrationIds;\n\tname: string;\n\torg?: string;\n\torgId?: string;\n\turl: string;\n}\n\n/** Normalized repository shape returned by the provider facade. */\nexport interface ProviderRepositoryShape {\n\tid: string;\n\tnamespace: string;\n\tname: string;\n\t/** Azure DevOps project; `undefined` for hosts without a project layer. */\n\tproject?: string;\n\t/** Web (browser) URL, when the provider exposes it. */\n\turl?: string;\n\t/** HTTPS clone URL, when available. */\n\tcloneUrlHttps?: string;\n\t/** SSH clone URL, when available. */\n\tcloneUrlSsh?: string;\n\t/** Default branch name, when the provider reports it. */\n\tdefaultBranch?: string;\n}\n\n/**\n * Classifies a caught provider exception into a neutral {@link ProviderWarning}. Ordering matters:\n * `instanceof` is checked most-specific-first so a rate-limit isn't mislabeled as a generic error and\n * a 404 isn't mislabeled as auth.\n */\nexport function toProviderWarning(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tex: unknown,\n): ProviderWarning {\n\tlet kind: ProviderWarning['kind'];\n\tif (ex instanceof AuthenticationError) {\n\t\tkind = 'auth';\n\t} else if (ex instanceof RequestRateLimitError) {\n\t\tkind = 'rate-limit';\n\t} else if (ex instanceof RequestNotFoundError) {\n\t\tkind = 'not-found';\n\t} else if (isUnsupportedSortError(ex)) {\n\t\tkind = 'unsupported';\n\t} else {\n\t\tkind = 'other';\n\t}\n\n\treturn {\n\t\tproviderId: providerId,\n\t\tdomain: domain,\n\t\tconnectionId: connectionId,\n\t\tmessage: providerWarningMessage(ex),\n\t\tkind: kind,\n\t\tisAuth: kind === 'auth',\n\t\t...(kind === 'other' && isProviderUnreachableError(ex) ? { cause: { reason: 'unreachable' as const } } : {}),\n\t};\n}\n\n/**\n * A scope's identity, as the stable string that keys it.\n *\n * The single definition of what \"the same scope\" means, shared by the failure and omission dedup keys in\n * `providerPaging.ts` and by {@link providerWarningKey} below, so a scope gaining a field is one edit rather\n * than three. `providerId` is included: the same repository ID under two providers is two scopes.\n *\n * The parameter is structural rather than the SDK's `CollectionScope` so this module keeps naming no\n * `@gitkraken/provider-apis` types (see the export block in `index.ts`) while still serving its SDK-facing\n * callers, which pass that type in unchanged.\n */\nexport function collectionScopeKey(scope: ProviderWarningOmissionScope | undefined): string {\n\treturn [scope?.providerId ?? '', scope?.resourceId ?? '', scope?.projectId ?? '', scope?.repositoryId ?? ''].join(\n\t\t' ',\n\t);\n}\n\n/**\n * The omission's contribution to a warning's identity — empty when there is none, so a warning without an\n * omission keeps deduping exactly as it did before the field existed.\n *\n * Two omissions of different kinds do produce different `message` values today, so message alone would still\n * separate them. That is incidental: the premise of `omission` is that consumers must not depend on prose\n * carrying the distinguishing fact, and this key must not either.\n */\nfunction providerWarningOmissionKey(omission: ProviderWarningOmission | undefined): string {\n\tif (omission == null) return '';\n\n\treturn [\n\t\tomission.kind,\n\t\tomission.recovery,\n\t\tomission.limit ?? '',\n\t\tomission.totalCount ?? '',\n\t\tcollectionScopeKey(omission.scope),\n\t].join(' ');\n}\n\n/**\n * Strips the omission from every warning in `warnings` when the read as a whole failed.\n *\n * An omission asserts the request SUCCEEDED, and a drain only learns it failed AFTER it may have emitted one:\n * an early page can report its own truncation and then a later page can die. Deciding per warning, at the\n * moment each is built, cannot see that future — so the aggregate is reconciled once, here, where\n * `fetchFailed` is final. Call it at the point a read returns its `fetchFailed`.\n *\n * Re-dedupes as it goes: the omission is part of a warning's identity, so two warnings that differed only\n * there become identical once it is gone, and the array's contract is that no two entries are equal.\n *\n * Mutates in place: the warning array is the one being returned, and callers accumulate into it across pages.\n */\nexport function reconcileOmissionsWithFailure(warnings: ProviderWarning[], fetchFailed: boolean): void {\n\tif (!fetchFailed || !warnings.some(w => w.omission != null)) return;\n\n\tconst reconciled: ProviderWarning[] = [];\n\tfor (const warning of warnings) {\n\t\tif (warning.omission == null) {\n\t\t\tappendDedupedWarning(reconciled, warning);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { omission: _omission, ...rest } = warning;\n\t\tappendDedupedWarning(reconciled, rest);\n\t}\n\n\twarnings.splice(0, warnings.length, ...reconciled);\n}\n\n/**\n * A stable key for deduplicating warnings accumulated across drained pages / fan-out scopes.\n *\n * The scope and the cause are keyed for the reason the omission is (see {@link providerWarningOmissionKey}): today's\n * failure messages happen to spell them out, but two failures that differ in either must stay two warnings even if\n * they don't.\n *\n * `message` stays LAST. It is the only free-form segment — provider prose, spaces and all — so anything\n * appended after it could be impersonated by a message that happens to end in the same text.\n */\nfunction providerWarningKey(warning: ProviderWarning): string {\n\treturn [\n\t\twarning.providerId,\n\t\twarning.connectionId ?? '',\n\t\twarning.domain ?? '',\n\t\twarning.kind,\n\t\tcollectionScopeKey(warning.scope),\n\t\twarning.cause?.reason ?? '',\n\t\tproviderWarningOmissionKey(warning.omission),\n\t\twarning.message,\n\t].join(' ');\n}\n\n/**\n * Appends `warning` to `into` only when an equal warning (by provider/connection/domain/kind/message, plus the\n * structured scope, cause and omission when present) is absent. An equal warning already there merges its\n * {@link ProviderWarning.targetKeys} with the new one's instead: the union of both, or none when either concerns\n * the whole call. The existing warning is replaced, never mutated, since it may be shared.\n */\nexport function appendDedupedWarning(into: ProviderWarning[], warning: ProviderWarning): void {\n\tconst key = providerWarningKey(warning);\n\tconst index = into.findIndex(existing => providerWarningKey(existing) === key);\n\tif (index === -1) {\n\t\tinto.push(warning);\n\t\treturn;\n\t}\n\n\tconst existing = into[index];\n\tconst existingKeys = existing.targetKeys;\n\t// Already covers the whole call.\n\tif (existingKeys == null) return;\n\n\tif (warning.targetKeys == null) {\n\t\tconst { targetKeys: _, ...wholeCall } = existing;\n\t\tinto[index] = wholeCall;\n\t\treturn;\n\t}\n\n\tconst added = warning.targetKeys.filter(k => !existingKeys.includes(k));\n\tif (!added.length) return;\n\n\tinto[index] = { ...existing, targetKeys: [...existingKeys, ...new Set(added)] };\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RemoteProvider, RemoteProviderId } from "../../../git/models/remoteProvider.js";
|
|
2
|
-
import { CloudGitSelfManagedHostIntegrationIds, CloudSelfManagedHostIntegrationIds, GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId, IntegrationIds, IssuesHostIntegrationIds, IssuesSelfManagedHostIntegrationId, SelfManagedHostIntegrationIds } from "../constants.js";
|
|
2
|
+
import { CloudGitSelfManagedHostIntegrationIds, CloudSelfManagedHostIntegrationIds, GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId, IntegrationIds, IssuesCloudHostIntegrationId, IssuesHostIntegrationIds, IssuesSelfManagedHostIntegrationId, SelfManagedHostIntegrationIds } from "../constants.js";
|
|
3
3
|
import { GitHostIntegration } from "../models/gitHostIntegration.js";
|
|
4
4
|
import { Integration, IntegrationConnectedKey } from "../models/integration.js";
|
|
5
5
|
//#region ../plus/integrations/src/utils/integration.utils.d.ts
|
|
@@ -41,6 +41,11 @@ export declare function isIssuesSelfManagedHostIntegrationId(id: IntegrationIds)
|
|
|
41
41
|
* domain, collapsing every host of it into one bucket.
|
|
42
42
|
*/
|
|
43
43
|
export declare function isSelfManagedHostIntegrationId(id: IntegrationIds): id is SelfManagedHostIntegrationIds;
|
|
44
|
+
/**
|
|
45
|
+
* Whether this id is Jira, Cloud or Data Center: the two share their project and site identity rules and the SDK's
|
|
46
|
+
* status mapper. Takes any string, as a provider reference's `id` is one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isJiraIntegrationId(id: string): id is IssuesCloudHostIntegrationId.Jira | IssuesSelfManagedHostIntegrationId.JiraServer;
|
|
44
49
|
/**
|
|
45
50
|
* Whether this id belongs to a dedicated issue tracker (resource → project) rather than a git host.
|
|
46
51
|
*
|
|
@@ -117,6 +117,13 @@ function isSelfManagedHostIntegrationId(id) {
|
|
|
117
117
|
return isGitSelfManagedHostIntegrationId(id) || isIssuesSelfManagedHostIntegrationId(id);
|
|
118
118
|
}
|
|
119
119
|
/**
|
|
120
|
+
* Whether this id is Jira, Cloud or Data Center: the two share their project and site identity rules and the SDK's
|
|
121
|
+
* status mapper. Takes any string, as a provider reference's `id` is one.
|
|
122
|
+
*/
|
|
123
|
+
function isJiraIntegrationId(id) {
|
|
124
|
+
return id === "jira" || id === "jira-server";
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
120
127
|
* Whether this id belongs to a dedicated issue tracker (resource → project) rather than a git host.
|
|
121
128
|
*
|
|
122
129
|
* Decided from the id alone, so a read can refuse a mismatched surface — a repo/PR read asked of Jira, or an
|
|
@@ -181,6 +188,6 @@ function getSelfManagedBaseUrl(id, domain, protocol) {
|
|
|
181
188
|
}
|
|
182
189
|
}
|
|
183
190
|
//#endregion
|
|
184
|
-
export { convertRemoteProviderIdToIntegrationId, getIntegrationConnectedKey, getIntegrationIdForRemote, getSelfManagedBaseUrl, isCloudGitSelfManagedHostIntegrationId, isCloudSelfManagedHostIntegrationId, isGitCloudHostIntegrationId, isGitHostIntegration, isGitSelfManagedHostIntegrationId, isIssuesHostIntegrationId, isIssuesSelfManagedHostIntegrationId, isNonExpiringZeroTokenIntegrationId, isSelfManagedHostIntegrationId, remoteProviderTypeForConfig, remoteProviderTypeForIntegration, supportedIntegrationIds, warnOnMissingSessionForDomain };
|
|
191
|
+
export { convertRemoteProviderIdToIntegrationId, getIntegrationConnectedKey, getIntegrationIdForRemote, getSelfManagedBaseUrl, isCloudGitSelfManagedHostIntegrationId, isCloudSelfManagedHostIntegrationId, isGitCloudHostIntegrationId, isGitHostIntegration, isGitSelfManagedHostIntegrationId, isIssuesHostIntegrationId, isIssuesSelfManagedHostIntegrationId, isJiraIntegrationId, isNonExpiringZeroTokenIntegrationId, isSelfManagedHostIntegrationId, remoteProviderTypeForConfig, remoteProviderTypeForIntegration, supportedIntegrationIds, warnOnMissingSessionForDomain };
|
|
185
192
|
|
|
186
193
|
//# sourceMappingURL=integration.utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.utils.js","names":[],"sources":["../../../../../plus/integrations/src/utils/integration.utils.ts"],"sourcesContent":["import type { RemoteProvider, RemoteProviderId } from '@gitlens/git/models/remoteProvider.js';\nimport type {\n\tCloudGitSelfManagedHostIntegrationIds,\n\tCloudSelfManagedHostIntegrationIds,\n\tIntegrationIds,\n\tIssuesHostIntegrationIds,\n\tSelfManagedHostIntegrationIds,\n} from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n\tIssuesSelfManagedHostIntegrationId,\n} from '../constants.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { Integration, IntegrationConnectedKey } from '../models/integration.js';\n// These domain checks come from `providers/models.ts` (already imported here) rather than the individual\n// provider modules: those read `providersMetadata[...]` synchronously at module init, so importing from them\n// while `providers/models.ts` is still loading triggers a TDZ on `providersMetadata`. Importing the shared\n// predicates keeps one definition of each regex — duplicating them let a fix land in only one place.\nimport { isAzureCloudDomain, isBitbucketCloudDomain, isGitHubDotCom, isGitLabDotCom } from '../providers/models.js';\nimport { baseUrlFromDomain } from './domain.utils.js';\n\nconst selfHostedIntegrationIds: GitSelfManagedHostIntegrationId[] = [\n\tGitSelfManagedHostIntegrationId.CloudGitHubEnterprise,\n\tGitSelfManagedHostIntegrationId.CloudGitLabSelfHosted,\n\tGitSelfManagedHostIntegrationId.BitbucketServer,\n\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n] as const;\n\nconst selfHostedIssuesIntegrationIds: IssuesSelfManagedHostIntegrationId[] = [\n\tIssuesSelfManagedHostIntegrationId.JiraServer,\n] as const;\n\nexport const supportedIntegrationIds: IntegrationIds[] = [\n\tGitCloudHostIntegrationId.GitHub,\n\tGitCloudHostIntegrationId.GitLab,\n\tGitCloudHostIntegrationId.Bitbucket,\n\tGitCloudHostIntegrationId.AzureDevOps,\n\tIssuesCloudHostIntegrationId.Jira,\n\tIssuesCloudHostIntegrationId.Trello,\n\t...selfHostedIntegrationIds,\n\t...selfHostedIssuesIntegrationIds,\n] as const;\n\nexport function convertRemoteProviderIdToIntegrationId(\n\tremoteProviderId: RemoteProviderId,\n): GitCloudHostIntegrationId | GitSelfManagedHostIntegrationId | undefined {\n\tswitch (remoteProviderId) {\n\t\tcase 'azure-devops':\n\t\t\treturn GitCloudHostIntegrationId.AzureDevOps;\n\t\tcase 'bitbucket':\n\t\t\treturn GitCloudHostIntegrationId.Bitbucket;\n\t\tcase 'github':\n\t\t\treturn GitCloudHostIntegrationId.GitHub;\n\t\tcase 'gitlab':\n\t\t\treturn GitCloudHostIntegrationId.GitLab;\n\t\tcase 'bitbucket-server':\n\t\t\treturn GitSelfManagedHostIntegrationId.BitbucketServer;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nexport function getIntegrationConnectedKey<T extends IntegrationIds>(\n\tid: T,\n\tdomain?: string,\n): IntegrationConnectedKey<T> {\n\tif (isSelfManagedHostIntegrationId(id)) {\n\t\tif (!domain) {\n\t\t\tthrow new Error(`Domain is required for self-managed integration ID: ${id}`);\n\t\t}\n\t\treturn `connected:${id}:${domain}` as IntegrationConnectedKey<T>;\n\t}\n\n\treturn `connected:${id}` as IntegrationConnectedKey<T>;\n}\n\nexport function getIntegrationIdForRemote(\n\tprovider: RemoteProvider | undefined,\n): GitCloudHostIntegrationId | GitSelfManagedHostIntegrationId | undefined {\n\tswitch (provider?.id) {\n\t\tcase 'azure-devops':\n\t\t\tif (isAzureCloudDomain(provider.domain)) {\n\t\t\t\treturn GitCloudHostIntegrationId.AzureDevOps;\n\t\t\t}\n\t\t\treturn provider.custom ? undefined : GitSelfManagedHostIntegrationId.AzureDevOpsServer;\n\t\tcase 'bitbucket':\n\t\tcase 'bitbucket-server':\n\t\t\tif (isBitbucketCloudDomain(provider.domain)) {\n\t\t\t\treturn GitCloudHostIntegrationId.Bitbucket;\n\t\t\t}\n\t\t\treturn GitSelfManagedHostIntegrationId.BitbucketServer;\n\t\tcase 'github':\n\t\t\tif (provider.domain != null && !isGitHubDotCom(provider.domain)) {\n\t\t\t\treturn GitSelfManagedHostIntegrationId.CloudGitHubEnterprise;\n\t\t\t}\n\t\t\treturn GitCloudHostIntegrationId.GitHub;\n\t\tcase 'gitlab':\n\t\t\tif (provider.domain != null && !isGitLabDotCom(provider.domain)) {\n\t\t\t\treturn GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted;\n\t\t\t}\n\t\t\treturn GitCloudHostIntegrationId.GitLab;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nexport function isCloudGitSelfManagedHostIntegrationId(\n\tid: IntegrationIds,\n): id is CloudGitSelfManagedHostIntegrationIds {\n\tswitch (id) {\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitHubEnterprise:\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted:\n\t\tcase GitSelfManagedHostIntegrationId.BitbucketServer:\n\t\tcase GitSelfManagedHostIntegrationId.AzureDevOpsServer:\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * Whether this id is a self-managed host whose token comes from the GK cloud backend — the check behind\n * \"expand this provider to one instance per configured host\". Distinct from\n * {@link isCloudGitSelfManagedHostIntegrationId}, which stays git-only because its callers go on to ask the\n * instance for repositories.\n */\nexport function isCloudSelfManagedHostIntegrationId(id: IntegrationIds): id is CloudSelfManagedHostIntegrationIds {\n\treturn isCloudGitSelfManagedHostIntegrationId(id) || isIssuesSelfManagedHostIntegrationId(id);\n}\n\n/**\n * Whether a provider's cloud token uses `expiresIn: 0` to mean \"never expires\" (rather than \"already\n * expired\"). GitHub and the cloud self-managed hosts always return 0 for their non-expiring tokens; Trello\n * is issued with `expiration: never` (identity-service), so its cloud token comes back as 0 too. Callers\n * must map 0 → a far-future expiry for these, or the session is immediately treated as expired.\n *\n * Every backend-managed self-managed host qualifies, not just the git ones: a self-hosted tracker's token is\n * a PAT the backend stores as-is and cannot refresh, so reading 0 as \"already expired\" would re-resolve the\n * session on every single read.\n */\nexport function isNonExpiringZeroTokenIntegrationId(id: IntegrationIds): boolean {\n\treturn (\n\t\tid === GitCloudHostIntegrationId.GitHub ||\n\t\tid === IssuesCloudHostIntegrationId.Trello ||\n\t\tisCloudSelfManagedHostIntegrationId(id)\n\t);\n}\n\nexport function isGitHostIntegration(integration: Integration): integration is GitHostIntegration {\n\treturn integration.type === 'git';\n}\n\nexport function isGitCloudHostIntegrationId(id: IntegrationIds): id is GitCloudHostIntegrationId {\n\tswitch (id) {\n\t\tcase GitCloudHostIntegrationId.GitHub:\n\t\tcase GitCloudHostIntegrationId.GitLab:\n\t\tcase GitCloudHostIntegrationId.Bitbucket:\n\t\tcase GitCloudHostIntegrationId.AzureDevOps:\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nexport function isGitSelfManagedHostIntegrationId(id: IntegrationIds): id is GitSelfManagedHostIntegrationId {\n\treturn selfHostedIntegrationIds.includes(id as GitSelfManagedHostIntegrationId);\n}\n\nexport function isIssuesSelfManagedHostIntegrationId(id: IntegrationIds): id is IssuesSelfManagedHostIntegrationId {\n\treturn selfHostedIssuesIntegrationIds.includes(id as IssuesSelfManagedHostIntegrationId);\n}\n\n/**\n * Whether this id addresses ONE customer-run host, whatever it hosts — the predicate the domain-keyed\n * machinery runs on: the integration cache key, the secret key, the `connected:${id}:${domain}` flag, domain\n * normalization, per-host primary selection, and multi-account reconcile.\n *\n * Deliberately wider than {@link isGitSelfManagedHostIntegrationId}, which is reserved for reads that go on to\n * ask for repositories, pull requests, or a git remote. Widening THOSE to a tracker would attribute a Jira\n * host's remotes to Jira; narrowing THESE to git hosts would key a tracker's connections under an empty\n * domain, collapsing every host of it into one bucket.\n */\nexport function isSelfManagedHostIntegrationId(id: IntegrationIds): id is SelfManagedHostIntegrationIds {\n\treturn isGitSelfManagedHostIntegrationId(id) || isIssuesSelfManagedHostIntegrationId(id);\n}\n\n/**\n * Whether this id belongs to a dedicated issue tracker (resource → project) rather than a git host.\n *\n * Decided from the id alone, so a read can refuse a mismatched surface — a repo/PR read asked of Jira, or an\n * issue-tracker project read asked of GitHub — before resolving a connection. The instance-level\n * {@link isIssuesIntegration} answers the same question once an integration is in hand.\n */\nexport function isIssuesHostIntegrationId(id: IntegrationIds): id is IssuesHostIntegrationIds {\n\tswitch (id) {\n\t\tcase IssuesCloudHostIntegrationId.Jira:\n\t\tcase IssuesCloudHostIntegrationId.Linear:\n\t\tcase IssuesCloudHostIntegrationId.Trello:\n\t\tcase IssuesSelfManagedHostIntegrationId.JiraServer:\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * Whether a read targeted only by an explicit self-managed `domain` (no `connectionId`) must treat a\n * session-less core result as a broken target instead of an empty account. Without this, a self-managed host\n * addressed only by domain — the manual-token/external-auth case `domain` exists to cover — returns an empty\n * success with no warning and no `fetchFailed`, indistinguishable from \"this host has nothing\".\n */\nexport function warnOnMissingSessionForDomain(id: IntegrationIds, domain: string | undefined): boolean {\n\treturn domain != null && isSelfManagedHostIntegrationId(id);\n}\n\n/** Maps an integration id to the git-remote provider type used by the remote-URL matcher. */\nexport function remoteProviderTypeForIntegration(id: IntegrationIds): RemoteProviderId | undefined {\n\tswitch (id) {\n\t\tcase GitCloudHostIntegrationId.GitHub:\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitHubEnterprise:\n\t\t\treturn 'github';\n\t\tcase GitCloudHostIntegrationId.GitLab:\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted:\n\t\t\treturn 'gitlab';\n\t\tcase GitCloudHostIntegrationId.Bitbucket:\n\t\t\treturn 'bitbucket';\n\t\tcase GitSelfManagedHostIntegrationId.BitbucketServer:\n\t\t\treturn 'bitbucket-server';\n\t\tcase GitCloudHostIntegrationId.AzureDevOps:\n\t\tcase GitSelfManagedHostIntegrationId.AzureDevOpsServer:\n\t\t\treturn 'azure-devops';\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\n/** Normalizes a host remote-config `type` string (e.g. `'GitHub'`) to a git-remote provider type. */\nexport function remoteProviderTypeForConfig(type: string): RemoteProviderId | undefined {\n\tswitch (type.toLowerCase()) {\n\t\tcase 'github':\n\t\t\treturn 'github';\n\t\tcase 'gitlab':\n\t\t\treturn 'gitlab';\n\t\tcase 'bitbucket':\n\t\t\treturn 'bitbucket';\n\t\tcase 'bitbucket-server':\n\t\tcase 'bitbucketserver':\n\t\t\treturn 'bitbucket-server';\n\t\tcase 'azuredevops':\n\t\tcase 'azure-devops':\n\t\t\treturn 'azure-devops';\n\t\tcase 'gitea':\n\t\t\treturn 'gitea';\n\t\tcase 'gerrit':\n\t\t\treturn 'gerrit';\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nexport function getSelfManagedBaseUrl(\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tprotocol?: string,\n): string | undefined {\n\tconst baseUrl = baseUrlFromDomain(domain, protocol);\n\tif (baseUrl == null) return undefined;\n\n\tswitch (id) {\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitHubEnterprise:\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted:\n\t\t\treturn baseUrl.replace(/\\/api(?:\\/v\\d+)?$/, '');\n\t\tcase GitSelfManagedHostIntegrationId.BitbucketServer:\n\t\t\treturn baseUrl.replace(/\\/rest\\/api\\/1\\.0$/, '');\n\t\tdefault:\n\t\t\treturn baseUrl;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"integration.utils.js","names":[],"sources":["../../../../../plus/integrations/src/utils/integration.utils.ts"],"sourcesContent":["import type { RemoteProvider, RemoteProviderId } from '@gitlens/git/models/remoteProvider.js';\nimport type {\n\tCloudGitSelfManagedHostIntegrationIds,\n\tCloudSelfManagedHostIntegrationIds,\n\tIntegrationIds,\n\tIssuesHostIntegrationIds,\n\tSelfManagedHostIntegrationIds,\n} from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n\tIssuesSelfManagedHostIntegrationId,\n} from '../constants.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { Integration, IntegrationConnectedKey } from '../models/integration.js';\n// These domain checks come from `providers/models.ts` (already imported here) rather than the individual\n// provider modules: those read `providersMetadata[...]` synchronously at module init, so importing from them\n// while `providers/models.ts` is still loading triggers a TDZ on `providersMetadata`. Importing the shared\n// predicates keeps one definition of each regex — duplicating them let a fix land in only one place.\nimport { isAzureCloudDomain, isBitbucketCloudDomain, isGitHubDotCom, isGitLabDotCom } from '../providers/models.js';\nimport { baseUrlFromDomain } from './domain.utils.js';\n\nconst selfHostedIntegrationIds: GitSelfManagedHostIntegrationId[] = [\n\tGitSelfManagedHostIntegrationId.CloudGitHubEnterprise,\n\tGitSelfManagedHostIntegrationId.CloudGitLabSelfHosted,\n\tGitSelfManagedHostIntegrationId.BitbucketServer,\n\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n] as const;\n\nconst selfHostedIssuesIntegrationIds: IssuesSelfManagedHostIntegrationId[] = [\n\tIssuesSelfManagedHostIntegrationId.JiraServer,\n] as const;\n\nexport const supportedIntegrationIds: IntegrationIds[] = [\n\tGitCloudHostIntegrationId.GitHub,\n\tGitCloudHostIntegrationId.GitLab,\n\tGitCloudHostIntegrationId.Bitbucket,\n\tGitCloudHostIntegrationId.AzureDevOps,\n\tIssuesCloudHostIntegrationId.Jira,\n\tIssuesCloudHostIntegrationId.Trello,\n\t...selfHostedIntegrationIds,\n\t...selfHostedIssuesIntegrationIds,\n] as const;\n\nexport function convertRemoteProviderIdToIntegrationId(\n\tremoteProviderId: RemoteProviderId,\n): GitCloudHostIntegrationId | GitSelfManagedHostIntegrationId | undefined {\n\tswitch (remoteProviderId) {\n\t\tcase 'azure-devops':\n\t\t\treturn GitCloudHostIntegrationId.AzureDevOps;\n\t\tcase 'bitbucket':\n\t\t\treturn GitCloudHostIntegrationId.Bitbucket;\n\t\tcase 'github':\n\t\t\treturn GitCloudHostIntegrationId.GitHub;\n\t\tcase 'gitlab':\n\t\t\treturn GitCloudHostIntegrationId.GitLab;\n\t\tcase 'bitbucket-server':\n\t\t\treturn GitSelfManagedHostIntegrationId.BitbucketServer;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nexport function getIntegrationConnectedKey<T extends IntegrationIds>(\n\tid: T,\n\tdomain?: string,\n): IntegrationConnectedKey<T> {\n\tif (isSelfManagedHostIntegrationId(id)) {\n\t\tif (!domain) {\n\t\t\tthrow new Error(`Domain is required for self-managed integration ID: ${id}`);\n\t\t}\n\t\treturn `connected:${id}:${domain}` as IntegrationConnectedKey<T>;\n\t}\n\n\treturn `connected:${id}` as IntegrationConnectedKey<T>;\n}\n\nexport function getIntegrationIdForRemote(\n\tprovider: RemoteProvider | undefined,\n): GitCloudHostIntegrationId | GitSelfManagedHostIntegrationId | undefined {\n\tswitch (provider?.id) {\n\t\tcase 'azure-devops':\n\t\t\tif (isAzureCloudDomain(provider.domain)) {\n\t\t\t\treturn GitCloudHostIntegrationId.AzureDevOps;\n\t\t\t}\n\t\t\treturn provider.custom ? undefined : GitSelfManagedHostIntegrationId.AzureDevOpsServer;\n\t\tcase 'bitbucket':\n\t\tcase 'bitbucket-server':\n\t\t\tif (isBitbucketCloudDomain(provider.domain)) {\n\t\t\t\treturn GitCloudHostIntegrationId.Bitbucket;\n\t\t\t}\n\t\t\treturn GitSelfManagedHostIntegrationId.BitbucketServer;\n\t\tcase 'github':\n\t\t\tif (provider.domain != null && !isGitHubDotCom(provider.domain)) {\n\t\t\t\treturn GitSelfManagedHostIntegrationId.CloudGitHubEnterprise;\n\t\t\t}\n\t\t\treturn GitCloudHostIntegrationId.GitHub;\n\t\tcase 'gitlab':\n\t\t\tif (provider.domain != null && !isGitLabDotCom(provider.domain)) {\n\t\t\t\treturn GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted;\n\t\t\t}\n\t\t\treturn GitCloudHostIntegrationId.GitLab;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nexport function isCloudGitSelfManagedHostIntegrationId(\n\tid: IntegrationIds,\n): id is CloudGitSelfManagedHostIntegrationIds {\n\tswitch (id) {\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitHubEnterprise:\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted:\n\t\tcase GitSelfManagedHostIntegrationId.BitbucketServer:\n\t\tcase GitSelfManagedHostIntegrationId.AzureDevOpsServer:\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * Whether this id is a self-managed host whose token comes from the GK cloud backend — the check behind\n * \"expand this provider to one instance per configured host\". Distinct from\n * {@link isCloudGitSelfManagedHostIntegrationId}, which stays git-only because its callers go on to ask the\n * instance for repositories.\n */\nexport function isCloudSelfManagedHostIntegrationId(id: IntegrationIds): id is CloudSelfManagedHostIntegrationIds {\n\treturn isCloudGitSelfManagedHostIntegrationId(id) || isIssuesSelfManagedHostIntegrationId(id);\n}\n\n/**\n * Whether a provider's cloud token uses `expiresIn: 0` to mean \"never expires\" (rather than \"already\n * expired\"). GitHub and the cloud self-managed hosts always return 0 for their non-expiring tokens; Trello\n * is issued with `expiration: never` (identity-service), so its cloud token comes back as 0 too. Callers\n * must map 0 → a far-future expiry for these, or the session is immediately treated as expired.\n *\n * Every backend-managed self-managed host qualifies, not just the git ones: a self-hosted tracker's token is\n * a PAT the backend stores as-is and cannot refresh, so reading 0 as \"already expired\" would re-resolve the\n * session on every single read.\n */\nexport function isNonExpiringZeroTokenIntegrationId(id: IntegrationIds): boolean {\n\treturn (\n\t\tid === GitCloudHostIntegrationId.GitHub ||\n\t\tid === IssuesCloudHostIntegrationId.Trello ||\n\t\tisCloudSelfManagedHostIntegrationId(id)\n\t);\n}\n\nexport function isGitHostIntegration(integration: Integration): integration is GitHostIntegration {\n\treturn integration.type === 'git';\n}\n\nexport function isGitCloudHostIntegrationId(id: IntegrationIds): id is GitCloudHostIntegrationId {\n\tswitch (id) {\n\t\tcase GitCloudHostIntegrationId.GitHub:\n\t\tcase GitCloudHostIntegrationId.GitLab:\n\t\tcase GitCloudHostIntegrationId.Bitbucket:\n\t\tcase GitCloudHostIntegrationId.AzureDevOps:\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nexport function isGitSelfManagedHostIntegrationId(id: IntegrationIds): id is GitSelfManagedHostIntegrationId {\n\treturn selfHostedIntegrationIds.includes(id as GitSelfManagedHostIntegrationId);\n}\n\nexport function isIssuesSelfManagedHostIntegrationId(id: IntegrationIds): id is IssuesSelfManagedHostIntegrationId {\n\treturn selfHostedIssuesIntegrationIds.includes(id as IssuesSelfManagedHostIntegrationId);\n}\n\n/**\n * Whether this id addresses ONE customer-run host, whatever it hosts — the predicate the domain-keyed\n * machinery runs on: the integration cache key, the secret key, the `connected:${id}:${domain}` flag, domain\n * normalization, per-host primary selection, and multi-account reconcile.\n *\n * Deliberately wider than {@link isGitSelfManagedHostIntegrationId}, which is reserved for reads that go on to\n * ask for repositories, pull requests, or a git remote. Widening THOSE to a tracker would attribute a Jira\n * host's remotes to Jira; narrowing THESE to git hosts would key a tracker's connections under an empty\n * domain, collapsing every host of it into one bucket.\n */\nexport function isSelfManagedHostIntegrationId(id: IntegrationIds): id is SelfManagedHostIntegrationIds {\n\treturn isGitSelfManagedHostIntegrationId(id) || isIssuesSelfManagedHostIntegrationId(id);\n}\n\n/**\n * Whether this id is Jira, Cloud or Data Center: the two share their project and site identity rules and the SDK's\n * status mapper. Takes any string, as a provider reference's `id` is one.\n */\nexport function isJiraIntegrationId(\n\tid: string,\n): id is IssuesCloudHostIntegrationId.Jira | IssuesSelfManagedHostIntegrationId.JiraServer {\n\treturn id === IssuesCloudHostIntegrationId.Jira || id === IssuesSelfManagedHostIntegrationId.JiraServer;\n}\n\n/**\n * Whether this id belongs to a dedicated issue tracker (resource → project) rather than a git host.\n *\n * Decided from the id alone, so a read can refuse a mismatched surface — a repo/PR read asked of Jira, or an\n * issue-tracker project read asked of GitHub — before resolving a connection. The instance-level\n * {@link isIssuesIntegration} answers the same question once an integration is in hand.\n */\nexport function isIssuesHostIntegrationId(id: IntegrationIds): id is IssuesHostIntegrationIds {\n\tswitch (id) {\n\t\tcase IssuesCloudHostIntegrationId.Jira:\n\t\tcase IssuesCloudHostIntegrationId.Linear:\n\t\tcase IssuesCloudHostIntegrationId.Trello:\n\t\tcase IssuesSelfManagedHostIntegrationId.JiraServer:\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * Whether a read targeted only by an explicit self-managed `domain` (no `connectionId`) must treat a\n * session-less core result as a broken target instead of an empty account. Without this, a self-managed host\n * addressed only by domain — the manual-token/external-auth case `domain` exists to cover — returns an empty\n * success with no warning and no `fetchFailed`, indistinguishable from \"this host has nothing\".\n */\nexport function warnOnMissingSessionForDomain(id: IntegrationIds, domain: string | undefined): boolean {\n\treturn domain != null && isSelfManagedHostIntegrationId(id);\n}\n\n/** Maps an integration id to the git-remote provider type used by the remote-URL matcher. */\nexport function remoteProviderTypeForIntegration(id: IntegrationIds): RemoteProviderId | undefined {\n\tswitch (id) {\n\t\tcase GitCloudHostIntegrationId.GitHub:\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitHubEnterprise:\n\t\t\treturn 'github';\n\t\tcase GitCloudHostIntegrationId.GitLab:\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted:\n\t\t\treturn 'gitlab';\n\t\tcase GitCloudHostIntegrationId.Bitbucket:\n\t\t\treturn 'bitbucket';\n\t\tcase GitSelfManagedHostIntegrationId.BitbucketServer:\n\t\t\treturn 'bitbucket-server';\n\t\tcase GitCloudHostIntegrationId.AzureDevOps:\n\t\tcase GitSelfManagedHostIntegrationId.AzureDevOpsServer:\n\t\t\treturn 'azure-devops';\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\n/** Normalizes a host remote-config `type` string (e.g. `'GitHub'`) to a git-remote provider type. */\nexport function remoteProviderTypeForConfig(type: string): RemoteProviderId | undefined {\n\tswitch (type.toLowerCase()) {\n\t\tcase 'github':\n\t\t\treturn 'github';\n\t\tcase 'gitlab':\n\t\t\treturn 'gitlab';\n\t\tcase 'bitbucket':\n\t\t\treturn 'bitbucket';\n\t\tcase 'bitbucket-server':\n\t\tcase 'bitbucketserver':\n\t\t\treturn 'bitbucket-server';\n\t\tcase 'azuredevops':\n\t\tcase 'azure-devops':\n\t\t\treturn 'azure-devops';\n\t\tcase 'gitea':\n\t\t\treturn 'gitea';\n\t\tcase 'gerrit':\n\t\t\treturn 'gerrit';\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nexport function getSelfManagedBaseUrl(\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tprotocol?: string,\n): string | undefined {\n\tconst baseUrl = baseUrlFromDomain(domain, protocol);\n\tif (baseUrl == null) return undefined;\n\n\tswitch (id) {\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitHubEnterprise:\n\t\tcase GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted:\n\t\t\treturn baseUrl.replace(/\\/api(?:\\/v\\d+)?$/, '');\n\t\tcase GitSelfManagedHostIntegrationId.BitbucketServer:\n\t\t\treturn baseUrl.replace(/\\/rest\\/api\\/1\\.0$/, '');\n\t\tdefault:\n\t\t\treturn baseUrl;\n\t}\n}\n"],"mappings":"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"}
|
package/docs/integrations.md
CHANGED
|
@@ -190,6 +190,11 @@ and an `unchanged` pull request's always can. A row is in one of four states:
|
|
|
190
190
|
- `{ key }` — proven absent, as before.
|
|
191
191
|
- no row, with `fetchFailed` and a warning — unknown, as before.
|
|
192
192
|
|
|
193
|
+
A dropped target's warning names its key in `targetKeys` (a failed integration call names every target it carried), so
|
|
194
|
+
a caller that merges several callers' targets into one call can attribute each failure to the targets it concerns.
|
|
195
|
+
Equal warnings merge their keys instead of one being dropped. A warning with no `targetKeys` refused the whole call
|
|
196
|
+
before any target was read.
|
|
197
|
+
|
|
193
198
|
The hosts with a cheap check select only the change state:
|
|
194
199
|
|
|
195
200
|
- **GitHub/GHE:** one aliased document per 25 targets, like the full read.
|
|
@@ -199,8 +204,10 @@ The hosts with a cheap check select only the change state:
|
|
|
199
204
|
full read, more often than the merge request itself does.
|
|
200
205
|
|
|
201
206
|
- **Azure DevOps and Azure DevOps Server:** work items in one batch request per project per 200 ids; an id the batch
|
|
202
|
-
leaves out falls through to its own full read.
|
|
203
|
-
|
|
207
|
+
leaves out falls through to its own full read. Both reads classify the states they read through the same cached
|
|
208
|
+
work item type states (see [Tracker issue state](#tracker-issue-state)), so a check after a full read reads them
|
|
209
|
+
again only once that cache expires. Pull requests can't be batched by id, so the check is one request per pull
|
|
210
|
+
request instead of the full read's two. Azure DevOps keeps no update time on a pull request, so its etag
|
|
204
211
|
also carries a revision: a 64-bit hash of the title, description, target branch and every reviewer with their vote,
|
|
205
212
|
read off the same response at no extra cost. It sees state, draft, closing, new pushes and those edits, including
|
|
206
213
|
a reviewer added or removed and an optional reviewer's vote (plus mergeability when included). It can't see a
|
|
@@ -808,7 +815,9 @@ one extra check a minute, and a revocation can take up to a minute to surface as
|
|
|
808
815
|
|
|
809
816
|
`scope.resourceId` is the resource as the read addressed it: its id on most reads, its name on the few that
|
|
810
817
|
address it by name (Azure DevOps' repo-scoped reads, Bitbucket's workspace reads). Match it against both
|
|
811
|
-
`ProviderOrganization.id` and `name
|
|
818
|
+
`ProviderOrganization.id` and `name`, and against a project's `orgId` where it has one. A Jira project read's
|
|
819
|
+
`scope.projectId` is the project as the read addressed it: the key on Jira Cloud (a project's `name` in
|
|
820
|
+
`listProjects`), the numeric id on Jira Data Center.
|
|
812
821
|
|
|
813
822
|
Warnings also dedup on `scope` and `cause`, so failures of two scopes stay two warnings.
|
|
814
823
|
|
|
@@ -1115,9 +1124,9 @@ HTTPS links are rebuilt from the configured URL and an SSH link, whose host and
|
|
|
1115
1124
|
Jira Data Center inherits Jira Cloud's row, except that it refuses `getCurrentAccount`. Its reads are addressed per host, so `domain` selects the instance and
|
|
1116
1125
|
every read below is scoped to that one connection; a paged read that omits it gets the primary configured host,
|
|
1117
1126
|
while `getIssuesBatch` refuses instead (see §4). `listOrgs` returns exactly one resource — the
|
|
1118
|
-
instance itself, synthesized from the configured host rather than fetched — and its projects carry the
|
|
1119
|
-
as `key
|
|
1120
|
-
|
|
1127
|
+
instance itself, synthesized from the configured host rather than fetched — and its projects carry the project key
|
|
1128
|
+
as `key` (the numeric id when the server reports none), which is what `listProjects` publishes as `name` and what
|
|
1129
|
+
`project` selects; reads address the project by id regardless. No autolinks are registered yet.
|
|
1121
1130
|
|
|
1122
1131
|
¹ Branches in the base repository only: a target whose `headOwner` names another owner is refused, since Bitbucket DC
|
|
1123
1132
|
finds a branch's pull requests only through the repository the branch lives in, and an Azure DevOps fork shares its
|
|
@@ -1177,28 +1186,45 @@ DevOps Services could express most of it, so the gap is unimplemented rather tha
|
|
|
1177
1186
|
carry the provider's own workflow state in the optional `providerState` field; it is display data and is not added to
|
|
1178
1187
|
`IssueOrPullRequest`, because pull requests' normalized `'opened' | 'closed' | 'merged'` state is complete.
|
|
1179
1188
|
|
|
1180
|
-
The field mirrors what `provider-apis` provides
|
|
1181
|
-
omitted when the provider returns `null`, and `category` is only
|
|
1189
|
+
The field mirrors what `provider-apis` provides, except for Azure DevOps' `category` (below). `name` is the
|
|
1190
|
+
provider's untranslated display name, `color` is omitted when the provider returns `null`, and `category` is only
|
|
1191
|
+
present where the provider supplies it:
|
|
1182
1192
|
|
|
1183
1193
|
| Provider | `name` | `color` | `category` |
|
|
1184
1194
|
| ------------ | ------------------- | ------- | ---------------------------------------- |
|
|
1185
1195
|
| Jira Cloud | Status name | Yes | Yes, from the stable status-category key |
|
|
1186
|
-
| Jira DC | Status name | Yes |
|
|
1196
|
+
| Jira DC | Status name | Yes | Yes, from the stable status-category key |
|
|
1187
1197
|
| Linear | Workflow state name | Yes | Yes, mapped from the Linear state type |
|
|
1188
1198
|
| Trello | Card list name | No | No |
|
|
1189
|
-
| Azure DevOps | Work-item state | No |
|
|
1199
|
+
| Azure DevOps | Work-item state | No | Yes, from the work item type's states |
|
|
1190
1200
|
| GitHub | `open` / `closed` | No | No |
|
|
1191
1201
|
| GitLab | `opened` / `closed` | No | No |
|
|
1192
1202
|
|
|
1193
|
-
For Jira,
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1203
|
+
For Jira, every read maps the category from the status category's `key` (`new`, `indeterminate`, `done`), which Jira
|
|
1204
|
+
does not localize, never from its translated `name`, so a Spanish `Listo` closes an issue and `Por hacer` doesn't.
|
|
1205
|
+
Jira's fourth category, "No Category" (`undefined`), and an unknown key read as `TO_DO`: an issue in one is open, as
|
|
1206
|
+
the open-state JQL (`statusCategory != Done`) lists it. Jira never reports `closedDate`, so `closed` is decided by the
|
|
1207
|
+
category alone.
|
|
1208
|
+
|
|
1209
|
+
`category` is `TO_DO`, `IN_PROGRESS`, `DONE` or `CANCELED`. `CANCELED` is abandoned work (canceled, duplicate,
|
|
1210
|
+
removed): it closes an issue as `DONE` does, and stays its own value so a consumer can tell the two outcomes apart.
|
|
1211
|
+
Linear reports its canceled and duplicate states as `CANCELED`.
|
|
1212
|
+
|
|
1213
|
+
Azure DevOps reports a state in its `Removed` category as `CANCELED`, `Completed` and `Resolved` as `DONE`,
|
|
1214
|
+
`InProgress` as `IN_PROGRESS` and `Proposed` as `TO_DO`. No work item read carries the category, so every one (by id,
|
|
1215
|
+
the cheap etag check and the project list, on Azure DevOps Server too) classifies its rows from their type's states
|
|
1216
|
+
(`GET …/_apis/wit/workItemTypes/{type}/states?api-version=6.0-preview.1`, the preview version Azure DevOps Server 2020
|
|
1217
|
+
requires), keyed on the raw `System.State`, and caches each read for five minutes per host, project, type and
|
|
1218
|
+
credential. The category, never the name, decides: a state named `Canceled` in the `Completed` category is `DONE`, and
|
|
1219
|
+
a state its type doesn't list has no category. When a states read fails, a project list keeps its rows (the failed
|
|
1220
|
+
type's without a category) and reports the project as a partial collection with a failure scoped to it; a read by id
|
|
1221
|
+
and the cheap etag check fail instead. A project list keeps only the work items in the states it asked for: provider-apis
|
|
1222
|
+
selects open ones by their closed and resolved dates, so a `Removed` work item with neither date is left out of an open
|
|
1223
|
+
list rather than listed closed, and a closed-only list does not select it either. Snapshots cached before categories
|
|
1224
|
+
existed are not reclassified until the provider is read again.
|
|
1225
|
+
|
|
1226
|
+
The normalized `state` and `closed` fields keep their existing derivation, with `CANCELED` closing like `DONE`;
|
|
1227
|
+
`providerState` is additive. An Azure DevOps work item therefore closes on its category as well as its closed date.
|
|
1202
1228
|
|
|
1203
1229
|
### Sprints and iterations
|
|
1204
1230
|
|
|
@@ -1326,6 +1352,9 @@ Markdown by this package.
|
|
|
1326
1352
|
- **Jira (Cloud + Data Center) / Linear / Trello** — paged by **project**, not by issue: `itemsPerPage` counts projects (default
|
|
1327
1353
|
20), each drained in full. Passing none of `page`/`cursor`/`itemsPerPage` aggregates every matched project
|
|
1328
1354
|
in one page. A single project exceeding its internal drain backstop shows up as `page.truncated`.
|
|
1355
|
+
A Jira issue `@gitkraken/provider-apis` can't read is dropped from its page and reported instead: its siblings are
|
|
1356
|
+
served, with `fetchFailed`, a warning scoped to its project and `page.truncated` (never `narrow-scope`); a project
|
|
1357
|
+
that dropped one is not retried.
|
|
1329
1358
|
Trello's issue `id` is the card's `idShort` — unique per board only, so correlate across boards by
|
|
1330
1359
|
`nodeId`. For Trello, boards are both the resource and the project, so `listOrgs` and `listProjects`
|
|
1331
1360
|
return the same set; Linear's resource is the organization and its projects are teams.
|
|
@@ -1333,6 +1362,12 @@ Markdown by this package.
|
|
|
1333
1362
|
retries, preserve a caller's aggregate-all mode, and suppress projects already emitted before discovery
|
|
1334
1363
|
recovered. `hasMore` reports only untouched forward progress. A cursor can therefore remain with
|
|
1335
1364
|
`hasMore: false`; reusing it is an explicit manual retry of failed work, not a normal paging loop.
|
|
1365
|
+
`org` and `project` select by identifier. Linear and Trello match any of a resource's or project's id, name or
|
|
1366
|
+
key. Jira (Cloud and Data Center) matches only the canonical key, so a selection whose site or project was removed
|
|
1367
|
+
selects nothing instead of resolving to another one that carries the old value as a name or id: `org` is the site's
|
|
1368
|
+
id (`listOrgs`' `id`, or `listProjects`' `orgId`), never its label, and `project` is the project key, which is
|
|
1369
|
+
`listProjects`' `name` for Jira. `listProjects({ org })` itself still matches any identifier, since it only lists
|
|
1370
|
+
what a caller can then select.
|
|
1336
1371
|
|
|
1337
1372
|
Self-managed descriptors keep `domain` as the normalized host and expose the configured installation address
|
|
1338
1373
|
in `baseUrl`. Provider requests use the selected connection's address, including during account discovery.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitkraken/core-gitlens",
|
|
3
3
|
"description": "GitLens core — shared Git / AI / GitHub primitives for internal GitKraken consumption",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "GitKraken",
|
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
}
|
|
240
240
|
},
|
|
241
241
|
"dependencies": {
|
|
242
|
-
"@gitkraken/provider-apis": "0.
|
|
242
|
+
"@gitkraken/provider-apis": "0.65.1",
|
|
243
243
|
"@octokit/graphql": "9.0.4",
|
|
244
244
|
"@octokit/request": "10.0.14",
|
|
245
245
|
"@octokit/request-error": "7.1.1",
|