@gitkraken/core-gitlens 0.17.0 → 0.19.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 +15 -1
- package/dist/git/cache.d.ts +7 -0
- package/dist/git/cache.js +21 -3
- package/dist/git/cache.js.map +1 -1
- package/dist/git/models/issue.d.ts +13 -3
- package/dist/git/models/issue.js +3 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/models/pullRequest.d.ts +13 -2
- package/dist/git/models/pullRequest.js +3 -1
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/git/utils/issue.utils.js +2 -1
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/pullRequest.utils.js +1 -0
- package/dist/git/utils/pullRequest.utils.js.map +1 -1
- package/dist/git/watching/gitIgnoreFilter.d.ts +21 -3
- package/dist/git/watching/gitIgnoreFilter.js +74 -14
- package/dist/git/watching/gitIgnoreFilter.js.map +1 -1
- package/dist/git/watching/provider.d.ts +9 -1
- package/dist/git/watching/watchService.d.ts +14 -1
- package/dist/git/watching/watchService.js +57 -16
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +38 -10
- package/dist/git/watching/worktreeSummary.js +164 -50
- package/dist/git/watching/worktreeSummary.js.map +1 -1
- package/dist/git-cli/cliGitProvider.js +4 -4
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/providers/branches.js +1 -32
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/config.d.ts +21 -0
- package/dist/git-cli/providers/config.js +72 -1
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +7 -6
- package/dist/plus/integrations/models/gitHostIntegration.js +4 -3
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +5 -0
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.d.ts +8 -6
- 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 +4 -2
- package/dist/plus/integrations/providers/azure/models.js +6 -5
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +15 -13
- package/dist/plus/integrations/providers/azureDevOps.js +26 -22
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.js +6 -3
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +26 -4
- package/dist/plus/integrations/providers/models.js +27 -5
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +1 -1
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/etag.js +3 -1
- package/dist/plus/integrations/reads/etag.js.map +1 -1
- package/docs/integrations.md +21 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drains.js","names":[],"sources":["../../../../../plus/integrations/src/reads/drains.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { PullRequest, PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort } from '@gitlens/git/models/pullRequest.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { supportedOrderedCloudIntegrationIds } from '../constants.js';\nimport type { ProviderSweepTarget, PullRequestSweepOptions } from '../manager.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { IntegrationResult } from '../models/integration.js';\nimport type { IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { PullRequestFilter } from '../providerFilters.js';\nimport type { ProviderPullRequest, ProviderReposInput, ProviderRepository } from '../providers/models.js';\nimport { getProviderPullRequestIdentity, pullRequestNamesPeople } from '../providers/models.js';\nimport type { ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport {\n\tcappedBesideBudgetWarning,\n\tnoConnectionWarning,\n\tpullRequestSearchCapResultWarning,\n\ttruncationWarning,\n} from './warnings.js';\n\n/**\n * The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page\n * reads the facade's paged surface exposes. `sweepPullRequests` and `broadenIssues` are built on these.\n *\n * A drain's job is to keep going where a single page would stop, so what distinguishes it is its failure\n * bookkeeping rather than its loop: it must return the prefix it did read plus enough signal for the caller to\n * know the result is not authoritative — `truncated` when pages remain unread (a page backstop, or a provider\n * claiming another page while handing back no usable cursor), `fetchFailed` when a page or scope failed\n * outright. Discarding the prefix on a late failure, or returning it as if complete, are the two mistakes these\n * exist to prevent.\n */\n\n/**\n * Runs a result-returning read and captures failure as a neutral {@link ProviderWarning} rather than\n * letting it throw or silently vanish. Handles both a returned `{ error }` (the read cores' contract)\n * and a hard throw; a soft warning (`{ value, error }`) yields the value *and* a warning.\n */\nexport async function runCaptured<T>(\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tfn: () => Promise<IntegrationResult<T>>,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: T; warning?: ProviderWarning }> {\n\ttry {\n\t\tconst result = await fn();\n\t\tif (result == null) {\n\t\t\t// The read core returns undefined only when it couldn't resolve a session. For a per-connection\n\t\t\t// or explicit-domain read that means the requested target is gone or its authentication is invalid,\n\t\t\t// which must not be reported as an empty account. The untargeted primary path legitimately yields\n\t\t\t// nothing when the provider isn't connected, so leave it as an empty result.\n\t\t\treturn connectionId != null || options?.warnOnMissingSession\n\t\t\t\t? { warning: noConnectionWarning(id, domain, connectionId) }\n\t\t\t\t: {};\n\t\t}\n\t\tif (result.error != null) {\n\t\t\treturn { value: result.value, warning: toProviderWarning(id, domain, connectionId, result.error) };\n\t\t}\n\t\treturn { value: result.value };\n\t} catch (ex) {\n\t\treturn { warning: toProviderWarning(id, domain, connectionId, ex) };\n\t}\n}\n\n/**\n * {@link runCaptured} over an issue tracker's resource (site, organization, board) discovery. A read that resolved a\n * session yet answered with no data and no error carries `completeness: 'unknown'` metadata, so it reads as an\n * incomplete discovery rather than as an account with no resources, which `runCaptured` alone can't tell apart from\n * an untargeted read with no session.\n */\nexport async function discoverResources(\n\tintegration: IssuesIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: ResourceDescriptor[]; warning?: ProviderWarning; metadata?: CollectionMetadata }> {\n\tlet noData = false;\n\tconst captured = await runCaptured(\n\t\tid,\n\t\tdomain,\n\t\tconnectionId,\n\t\tasync () => {\n\t\t\tconst result = await integration.getResourcesForUserResult(connectionId);\n\t\t\tnoData = result != null && result.error == null && result.value == null;\n\t\t\treturn result;\n\t\t},\n\t\toptions,\n\t);\n\treturn noData ? { ...captured, metadata: { completeness: 'unknown' } } : captured;\n}\n\n/**\n * Drains every page of the user's pull requests for one git-host integration, threading the opaque\n * next-cursor the provider returns (so it works for both page- and cursor-based hosts). Stops at\n * `maxPages` (marking `truncated`) or on a hard read failure (marking `fetchFailed`), keeping the\n * pages fetched so far. A soft warning (`{ value, error }`) is recorded but the drain continues.\n */\nexport async function drainPullRequests(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\trepos: ProviderReposInput,\n\tstate: PullRequestStateFilter[] | undefined,\n\tfilters: PullRequestFilter[] | undefined,\n\t/**\n\t * Breadth opt-ins that only the account-wide read can express. Grouped rather than passed positionally\n\t * because they are same-typed neighbors a transposition would silently swap, and picked from the sweep\n\t * options rather than restated so a third opt-in is declared once.\n\t */\n\taccountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n\tattributeUnavailableProvider: boolean,\n): Promise<{\n\titems: (ProviderPullRequest | PullRequest)[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tfailedProvider: boolean;\n}> {\n\tconst items: (ProviderPullRequest | PullRequest)[] = [];\n\tconst itemIndexByIdentity = new Map<string, number>();\n\tconst warnings: ProviderWarning[] = [];\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\t// SDK metadata failures across pages mean the collection is incomplete even when no page threw; carry\n\t// this through the terminal returns instead of resetting it to false at the last page.\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\t// A page the provider capped or couldn't vouch for, and which no other warning already explains — decides\n\t// whether this drain raises one of its own for that part.\n\tlet unexplainedTruncation = false;\n\t// The largest match count reported on a truncated page, from which the drain derives the provider's cap.\n\tlet truncatedTotalCount: number | undefined;\n\tconst capWarning = (): ProviderWarning | undefined =>\n\t\tpullRequestSearchCapResultWarning(id, domain, connectionId, truncatedTotalCount, defaultPullRequestSort);\n\tconst appendTruncationWarnings = (cause: 'interrupted' | 'exhausted'): void => {\n\t\tconst cap = capWarning();\n\t\tif (cap != null) {\n\t\t\tappendDedupedWarning(warnings, cap);\n\t\t}\n\t\t// A cap explains the missing tail of a read that succeeded; a failed read still owes its own warning.\n\t\tif (unexplainedTruncation && (cap == null || cause === 'interrupted')) {\n\t\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, 'Pull request', cause));\n\t\t}\n\t};\n\n\t// With no repos this is an account-wide \"my PRs\" sweep. The repo-scoped core rejects an empty `repos`\n\t// input, so read the provider-native account-wide core instead.\n\tconst accountWide = repos.length === 0;\n\t// The breadth opt-ins are only expressible on the account-wide core, so gate them here — the single place\n\t// that already knows which core it is about to call.\n\tconst includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);\n\tconst includeReviews = accountWide && (accountWideOptions?.includeReviews ?? false);\n\t/** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */\n\tconst seenCursors = new Set<string>();\n\n\tfor (;;) {\n\t\tpage++;\n\t\t// Snapshot the mutable loop cursor so the read closure doesn't capture a later-reassigned value.\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\taccountWide\n\t\t\t\t? integration.getMyPullRequestsForUserResult(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstate: state,\n\t\t\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\t\t\tincludeReviewRequested: includeReviewRequested,\n\t\t\t\t\t\t\tfilters: filters,\n\t\t\t\t\t\t\t// Lite by default; the full projection (reviews) only when a caller opts in.\n\t\t\t\t\t\t\tsummary: !includeReviews,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t)\n\t\t\t\t: integration.getMyPullRequestsForReposResult(\n\t\t\t\t\t\trepos,\n\t\t\t\t\t\t{ state: state, filters: filters, cursor: pageCursor },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// An implicit sweep may silently skip a provider that has no session before it yields data.\n\t\t\t// Once a page has been returned, however, losing that session leaves an unread tail and must\n\t\t\t// be attributed even when the provider wasn't explicitly requested.\n\t\t\tconst sessionLostAfterProgress = warning == null && page > 1;\n\t\t\tconst unavailable = warning == null && (attributeUnavailableProvider || sessionLostAfterProgress);\n\t\t\tif (unavailable) {\n\t\t\t\tappendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));\n\t\t\t}\n\t\t\t// `warning` set → a hard read failure (incomplete items); otherwise not connected / no session.\n\t\t\tconst failed = fetchFailed || warning != null || unavailable;\n\t\t\t// A cap seen on an earlier page still left results out, so it is still worth saying — but as part\n\t\t\t// of a read that failed, never as an omission. (`failed` is always true here when anything was\n\t\t\t// latched: reaching this exit past page 1 means a later page was lost.)\n\t\t\tappendTruncationWarnings(failed ? 'interrupted' : 'exhausted');\n\t\t\t// An earlier page may already have emitted an omission before this one died; it asserts the read\n\t\t\t// succeeded, which is no longer true.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || sessionLostAfterProgress,\n\t\t\t\t// Only a top-level first-page rejection means the provider itself failed. A later-page or\n\t\t\t\t// per-scope failure still yielded a usable provider slice and stays represented separately.\n\t\t\t\tfailedProvider: page === 1 && (warning != null || unavailable),\n\t\t\t};\n\t\t}\n\n\t\t// Composite account-wide queries can surface the same PR on different relationship/state pages\n\t\t// (for example authored + review-requested, or closed + merged). Keep the first stable position but\n\t\t// replace its value with the latest representation so a later, richer merged state wins. Repository\n\t\t// identity is what keys a row (see `getProviderPullRequestIdentity`), falling back to the canonical\n\t\t// URL; a row with neither stays unkeyed and is retained so unrelated incomplete rows are never\n\t\t// collapsed.\n\t\tfor (const pullRequest of value.values) {\n\t\t\tconst identity = getProviderPullRequestIdentity(pullRequest);\n\t\t\tif (identity == null) {\n\t\t\t\titems.push(pullRequest);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst existingIndex = itemIndexByIdentity.get(identity);\n\t\t\tif (existingIndex == null) {\n\t\t\t\titemIndexByIdentity.set(identity, items.length);\n\t\t\t\titems.push(pullRequest);\n\t\t\t} else {\n\t\t\t\titems[existingIndex] = pullRequest;\n\t\t\t}\n\t\t}\n\n\t\t// Assess this page's SDK metadata: append scope-aware warnings (deduped across pages), and remember\n\t\t// whether a structured failure or incompleteness occurred.\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\tconst pageTruncated =\n\t\t\t(value as { truncated?: boolean }).truncated === true ||\n\t\t\tvalue.paging?.truncated === true ||\n\t\t\tassessment.truncated;\n\t\ttruncated = truncated || pageTruncated;\n\t\t// Whether this drain owes its OWN warning for a capped page — `mergeAssessmentInto` has already appended\n\t\t// one when the fact came from SDK metadata, and repeating it would be noise.\n\t\tunexplainedTruncation = unexplainedTruncation || (pageTruncated && !assessment.truncated);\n\t\t// Only the account-wide read reports a search match count here; the repo-scoped one reports the item\n\t\t// total, which no cap is derived from. Latched even when SDK metadata explains the page: a composite page\n\t\t// reports a failed or cursorless sibling facet there, which says nothing about the cap another reached.\n\t\tif (accountWide && pageTruncated && value.paging?.totalCount != null) {\n\t\t\ttruncatedTotalCount = Math.max(truncatedTotalCount ?? 0, value.paging.totalCount);\n\t\t}\n\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\t// A read that can't confirm completeness (single-page provider reads with no `hasNextPage`)\n\t\t\t// sets `paging.truncated`; propagate it (and any top-level `truncated` and SDK incompleteness)\n\t\t\t// so the sweep doesn't claim an all-pages result.\n\t\t\tappendTruncationWarnings(fetchFailed ? 'interrupted' : 'exhausted');\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\t// Resolve the continuation BEFORE deciding why the drain stops. `page-budget` claims the missing items\n\t\t// are reachable, which is only true with a usable cursor in hand — and a provider can report another\n\t\t// page while handing back none (Bitbucket Server does, when it omits `nextPageStart`). Checking the\n\t\t// budget first would label that unreachable tail as merely unfetched, so raising `maxPages` would\n\t\t// return the identical set.\n\t\tconst nextCursor = value.paging?.cursor;\n\t\t// A cursor already used isn't a continuation either — following it refetches a page we have, and a\n\t\t// provider that cycles (A→B→A) would otherwise burn the whole budget and then be reported as merely\n\t\t// out of budget. Tracked as a SET rather than compared one-back, matching the SDK's own `followCursors`:\n\t\t// `drainToRequestedPage` and `collectProviderPagedResult` compare only the previous cursor, which a\n\t\t// cycle slips past. That is tolerable there and not here, because only this drain reports `page-budget`.\n\t\tconst continuable = nextCursor != null && nextCursor !== '{}' && !seenCursors.has(nextCursor);\n\t\tif (!continuable || page >= maxPages) {\n\t\t\t// A usable cursor in hand means `page-budget`, even when an earlier page was capped: a composite\n\t\t\t// read caps its facets independently, so one facet reaching the provider's ceiling leaves its\n\t\t\t// siblings' cursors live, and raising the budget returns more of them. What no budget returns is the\n\t\t\t// capped part, so that is reported BESIDE the budget stop rather than in place of it — unless an SDK\n\t\t\t// omission already says so. Neither warning then misstates the part the other describes.\n\t\t\tconst budgetStop = !fetchFailed && continuable;\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\ttruncationWarning(\n\t\t\t\t\tid,\n\t\t\t\t\tdomain,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t'Pull request',\n\t\t\t\t\tfetchFailed ? 'interrupted' : budgetStop ? 'page-budget' : 'exhausted',\n\t\t\t\t),\n\t\t\t);\n\t\t\tconst cap = capWarning();\n\t\t\tif (cap != null) {\n\t\t\t\tappendDedupedWarning(warnings, cap);\n\t\t\t} else if (budgetStop && unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(warnings, cappedBesideBudgetWarning(id, domain, connectionId, 'Pull request'));\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: true,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\tseenCursors.add(nextCursor);\n\t\tcursor = nextCursor;\n\t}\n}\n\n/**\n * Drains every page of repositories under an org for one git-host integration, threading the opaque\n * next-cursor the provider returns. Stops at `maxPages` (marking `truncated`) or on a hard read failure\n * (marking `fetchFailed`), keeping the pages fetched so far.\n */\nexport async function drainRepositories(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\torg: string,\n\tproject: string | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n): Promise<{\n\trepos: ProviderRepository[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n}> {\n\tconst repos: ProviderRepository[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\n\tfor (;;) {\n\t\tpage++;\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() =>\n\t\t\t\tintegration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\tproject: project,\n\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: true },\n\t\t);\n\t\tif (warning != null) {\n\t\t\t// Deduped, like every other accumulation here: a soft warning (`{ value, error }`) repeats verbatim on\n\t\t\t// each page that hits the same condition, and the array's contract is that no two entries are equal.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tconst interruptedAfterProgress = page > 1;\n\t\t\tif (interruptedAfterProgress && warning == null) {\n\t\t\t\t// Not a backstop: the read was cut short mid-drain, which is why it also sets `fetchFailed`\n\t\t\t\t// below. A retry may complete it, so this must not claim the succeeded-but-capped omission.\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Repository', 'interrupted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst failed = fetchFailed || warning != null || interruptedAfterProgress;\n\t\t\t// An SDK omission from an earlier page asserts the read succeeded; this one didn't.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || interruptedAfterProgress,\n\t\t\t};\n\t\t}\n\n\t\trepos.push(...value.values);\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\ttruncated = truncated || value.truncated === true || value.paging?.truncated === true || assessment.truncated;\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t};\n\t\t}\n\n\t\tif (page >= maxPages) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tconst nextCursor = value.paging?.cursor;\n\t\tif (nextCursor == null || nextCursor === '{}') {\n\t\t\t// Provider says there is more but didn't return a usable cursor; stop rather than refetch the same page.\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tcursor = nextCursor;\n\t}\n}\n\nexport function resolvePullRequestSweepTargets(options: PullRequestSweepOptions | undefined): {\n\ttargets: readonly ProviderSweepTarget[];\n\tattributeUnavailableProviders: boolean;\n} {\n\tif (options?.targets != null) {\n\t\tif (options.providerIds != null || options.connectionId != null) {\n\t\t\tthrow new TypeError(\"Pull request sweep 'targets' cannot be combined with 'providerIds' or 'connectionId'\");\n\t\t}\n\n\t\tconst seenProviderIds = new Set<IntegrationIds>();\n\t\tfor (const target of options.targets) {\n\t\t\tif (seenProviderIds.has(target.providerId)) {\n\t\t\t\tthrow new TypeError(\n\t\t\t\t\t`Pull request sweep targets must contain at most one target per provider; duplicate '${target.providerId}'`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tseenProviderIds.add(target.providerId);\n\t\t}\n\n\t\treturn { targets: options.targets, attributeUnavailableProviders: true };\n\t}\n\n\tconst providerIds =\n\t\toptions?.providerIds ?? supportedOrderedCloudIntegrationIds.filter(id => !isIssuesHostIntegrationId(id));\n\tconst connectionId = providerIds.length === 1 ? options?.connectionId : undefined;\n\treturn {\n\t\ttargets: providerIds.map(providerId => ({ providerId: providerId, connectionId: connectionId })),\n\t\tattributeUnavailableProviders: options?.providerIds != null,\n\t};\n}\n\nexport async function getCurrentAccountIdentity(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n): Promise<{ id: string; username?: string } | undefined> {\n\ttry {\n\t\tconst account = await integration.getCurrentAccount({ connectionId: connectionId });\n\t\treturn account != null ? { id: account.id, username: account.username } : undefined;\n\t} catch {\n\t\t// Authorship is optional enrichment; don't turn a successful PR read into a failure if identity lookup fails.\n\t\treturn undefined;\n\t}\n}\n\n/**\n * The current account to map each raw row against, in order (see {@link GitHostIntegration.getPullRequestViewers}).\n * Rows that name no one can't be matched, so a read of only those resolves nothing; a failed lookup leaves every\n * row's authorship unknown rather than failing the read.\n */\nexport async function getPullRequestViewers(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n\tpullRequests: readonly (ProviderPullRequest | PullRequest)[],\n): Promise<({ id: string; username?: string } | undefined)[]> {\n\tif (!pullRequests.some(pullRequestNamesPeople)) return pullRequests.map(() => undefined);\n\n\ttry {\n\t\treturn await integration.getPullRequestViewers(pullRequests, connectionId);\n\t} catch {\n\t\treturn pullRequests.map(() => undefined);\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"drains.js","names":[],"sources":["../../../../../plus/integrations/src/reads/drains.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { PullRequest, PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort } from '@gitlens/git/models/pullRequest.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { supportedOrderedCloudIntegrationIds } from '../constants.js';\nimport type { ProviderSweepTarget, PullRequestSweepOptions } from '../manager.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { IntegrationResult } from '../models/integration.js';\nimport type { IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { PullRequestFilter } from '../providerFilters.js';\nimport type { ProviderPullRequest, ProviderReposInput, ProviderRepository } from '../providers/models.js';\nimport { getProviderPullRequestIdentity, pullRequestNamesPeople } from '../providers/models.js';\nimport type { ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport {\n\tcappedBesideBudgetWarning,\n\tnoConnectionWarning,\n\tpullRequestSearchCapResultWarning,\n\ttruncationWarning,\n} from './warnings.js';\n\n/**\n * The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page\n * reads the facade's paged surface exposes. `sweepPullRequests` and `broadenIssues` are built on these.\n *\n * A drain's job is to keep going where a single page would stop, so what distinguishes it is its failure\n * bookkeeping rather than its loop: it must return the prefix it did read plus enough signal for the caller to\n * know the result is not authoritative — `truncated` when pages remain unread (a page backstop, or a provider\n * claiming another page while handing back no usable cursor), `fetchFailed` when a page or scope failed\n * outright. Discarding the prefix on a late failure, or returning it as if complete, are the two mistakes these\n * exist to prevent.\n */\n\n/**\n * Runs a result-returning read and captures failure as a neutral {@link ProviderWarning} rather than\n * letting it throw or silently vanish. Handles both a returned `{ error }` (the read cores' contract)\n * and a hard throw; a soft warning (`{ value, error }`) yields the value *and* a warning.\n */\nexport async function runCaptured<T>(\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tfn: () => Promise<IntegrationResult<T>>,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: T; warning?: ProviderWarning }> {\n\ttry {\n\t\tconst result = await fn();\n\t\tif (result == null) {\n\t\t\t// The read core returns undefined only when it couldn't resolve a session. For a per-connection\n\t\t\t// or explicit-domain read that means the requested target is gone or its authentication is invalid,\n\t\t\t// which must not be reported as an empty account. The untargeted primary path legitimately yields\n\t\t\t// nothing when the provider isn't connected, so leave it as an empty result.\n\t\t\treturn connectionId != null || options?.warnOnMissingSession\n\t\t\t\t? { warning: noConnectionWarning(id, domain, connectionId) }\n\t\t\t\t: {};\n\t\t}\n\t\tif (result.error != null) {\n\t\t\treturn { value: result.value, warning: toProviderWarning(id, domain, connectionId, result.error) };\n\t\t}\n\t\treturn { value: result.value };\n\t} catch (ex) {\n\t\treturn { warning: toProviderWarning(id, domain, connectionId, ex) };\n\t}\n}\n\n/**\n * {@link runCaptured} over an issue tracker's resource (site, organization, board) discovery. A read that resolved a\n * session yet answered with no data and no error carries `completeness: 'unknown'` metadata, so it reads as an\n * incomplete discovery rather than as an account with no resources, which `runCaptured` alone can't tell apart from\n * an untargeted read with no session.\n */\nexport async function discoverResources(\n\tintegration: IssuesIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: ResourceDescriptor[]; warning?: ProviderWarning; metadata?: CollectionMetadata }> {\n\tlet noData = false;\n\tconst captured = await runCaptured(\n\t\tid,\n\t\tdomain,\n\t\tconnectionId,\n\t\tasync () => {\n\t\t\tconst result = await integration.getResourcesForUserResult(connectionId);\n\t\t\tnoData = result != null && result.error == null && result.value == null;\n\t\t\treturn result;\n\t\t},\n\t\toptions,\n\t);\n\treturn noData ? { ...captured, metadata: { completeness: 'unknown' } } : captured;\n}\n\n/**\n * Drains every page of the user's pull requests for one git-host integration, threading the opaque\n * next-cursor the provider returns (so it works for both page- and cursor-based hosts). Stops at\n * `maxPages` (marking `truncated`) or on a hard read failure (marking `fetchFailed`), keeping the\n * pages fetched so far. A soft warning (`{ value, error }`) is recorded but the drain continues.\n */\nexport async function drainPullRequests(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\trepos: ProviderReposInput,\n\tstate: PullRequestStateFilter[] | undefined,\n\tfilters: PullRequestFilter[] | undefined,\n\t/**\n\t * Breadth opt-ins that only the account-wide read can express. Grouped rather than passed positionally\n\t * because they are same-typed neighbors a transposition would silently swap, and picked from the sweep\n\t * options rather than restated so a third opt-in is declared once.\n\t */\n\taccountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n\tattributeUnavailableProvider: boolean,\n): Promise<{\n\titems: (ProviderPullRequest | PullRequest)[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tfailedProvider: boolean;\n}> {\n\tconst items: (ProviderPullRequest | PullRequest)[] = [];\n\tconst itemIndexByIdentity = new Map<string, number>();\n\tconst warnings: ProviderWarning[] = [];\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\t// SDK metadata failures across pages mean the collection is incomplete even when no page threw; carry\n\t// this through the terminal returns instead of resetting it to false at the last page.\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\t// A page the provider capped or couldn't vouch for, and which no other warning already explains — decides\n\t// whether this drain raises one of its own for that part.\n\tlet unexplainedTruncation = false;\n\t// The largest match count reported on a truncated page, from which the drain derives the provider's cap.\n\tlet truncatedTotalCount: number | undefined;\n\tconst capWarning = (): ProviderWarning | undefined =>\n\t\tpullRequestSearchCapResultWarning(id, domain, connectionId, truncatedTotalCount, defaultPullRequestSort);\n\tconst appendTruncationWarnings = (cause: 'interrupted' | 'exhausted'): void => {\n\t\tconst cap = capWarning();\n\t\tif (cap != null) {\n\t\t\tappendDedupedWarning(warnings, cap);\n\t\t}\n\t\t// A cap explains the missing tail of a read that succeeded; a failed read still owes its own warning.\n\t\tif (unexplainedTruncation && (cap == null || cause === 'interrupted')) {\n\t\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, 'Pull request', cause));\n\t\t}\n\t};\n\n\t// With no repos this is an account-wide \"my PRs\" sweep. The repo-scoped core rejects an empty `repos`\n\t// input, so read the provider-native account-wide core instead.\n\tconst accountWide = repos.length === 0;\n\t// The breadth opt-ins are only expressible on the account-wide core, so gate them here — the single place\n\t// that already knows which core it is about to call.\n\tconst includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);\n\tconst includeReviews = accountWide && (accountWideOptions?.includeReviews ?? false);\n\t/** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */\n\tconst seenCursors = new Set<string>();\n\n\tfor (;;) {\n\t\tpage++;\n\t\t// Snapshot the mutable loop cursor so the read closure doesn't capture a later-reassigned value.\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\taccountWide\n\t\t\t\t? integration.getMyPullRequestsForUserResult(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstate: state,\n\t\t\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\t\t\tincludeReviewRequested: includeReviewRequested,\n\t\t\t\t\t\t\tfilters: filters,\n\t\t\t\t\t\t\t// Lite by default; the full projection (reviews) only when a caller opts in.\n\t\t\t\t\t\t\tsummary: !includeReviews,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t)\n\t\t\t\t: integration.getMyPullRequestsForReposResult(\n\t\t\t\t\t\trepos,\n\t\t\t\t\t\t{ state: state, filters: filters, cursor: pageCursor },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// An implicit sweep may silently skip a provider that has no session before it yields data.\n\t\t\t// Once a page has been returned, however, losing that session leaves an unread tail and must\n\t\t\t// be attributed even when the provider wasn't explicitly requested.\n\t\t\tconst sessionLostAfterProgress = warning == null && page > 1;\n\t\t\tconst unavailable = warning == null && (attributeUnavailableProvider || sessionLostAfterProgress);\n\t\t\tif (unavailable) {\n\t\t\t\tappendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));\n\t\t\t}\n\t\t\t// `warning` set → a hard read failure (incomplete items); otherwise not connected / no session.\n\t\t\tconst failed = fetchFailed || warning != null || unavailable;\n\t\t\t// A cap seen on an earlier page still left results out, so it is still worth saying — but as part\n\t\t\t// of a read that failed, never as an omission. (`failed` is always true here when anything was\n\t\t\t// latched: reaching this exit past page 1 means a later page was lost.)\n\t\t\tappendTruncationWarnings(failed ? 'interrupted' : 'exhausted');\n\t\t\t// An earlier page may already have emitted an omission before this one died; it asserts the read\n\t\t\t// succeeded, which is no longer true.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || sessionLostAfterProgress,\n\t\t\t\t// Only a top-level first-page rejection means the provider itself failed. A later-page or\n\t\t\t\t// per-scope failure still yielded a usable provider slice and stays represented separately.\n\t\t\t\tfailedProvider: page === 1 && (warning != null || unavailable),\n\t\t\t};\n\t\t}\n\n\t\t// Composite account-wide queries can surface the same PR on different relationship/state pages\n\t\t// (for example authored + review-requested, or closed + merged). Keep the first stable position but\n\t\t// replace its value with the latest representation so a later, richer merged state wins. Repository\n\t\t// identity is what keys a row (see `getProviderPullRequestIdentity`), falling back to the canonical\n\t\t// URL; a row with neither stays unkeyed and is retained so unrelated incomplete rows are never\n\t\t// collapsed.\n\t\tfor (const pullRequest of value.values) {\n\t\t\tconst identity = getProviderPullRequestIdentity(pullRequest);\n\t\t\tif (identity == null) {\n\t\t\t\titems.push(pullRequest);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst existingIndex = itemIndexByIdentity.get(identity);\n\t\t\tif (existingIndex == null) {\n\t\t\t\titemIndexByIdentity.set(identity, items.length);\n\t\t\t\titems.push(pullRequest);\n\t\t\t} else {\n\t\t\t\titems[existingIndex] = pullRequest;\n\t\t\t}\n\t\t}\n\n\t\t// Assess this page's SDK metadata: append scope-aware warnings (deduped across pages), and remember\n\t\t// whether a structured failure or incompleteness occurred.\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\tconst pageTruncated =\n\t\t\t(value as { truncated?: boolean }).truncated === true ||\n\t\t\tvalue.paging?.truncated === true ||\n\t\t\tassessment.truncated;\n\t\ttruncated = truncated || pageTruncated;\n\t\t// Whether this drain owes its OWN warning for a capped page — `mergeAssessmentInto` has already appended\n\t\t// one when the fact came from SDK metadata, and repeating it would be noise.\n\t\tunexplainedTruncation = unexplainedTruncation || (pageTruncated && !assessment.truncated);\n\t\t// Only the account-wide read reports a search match count here; the repo-scoped one reports the item\n\t\t// total, which no cap is derived from. Latched even when SDK metadata explains the page: a composite page\n\t\t// reports a failed or cursorless sibling facet there, which says nothing about the cap another reached.\n\t\tif (accountWide && pageTruncated && value.paging?.totalCount != null) {\n\t\t\ttruncatedTotalCount = Math.max(truncatedTotalCount ?? 0, value.paging.totalCount);\n\t\t}\n\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\t// A read that can't confirm completeness (single-page provider reads with no `hasNextPage`)\n\t\t\t// sets `paging.truncated`; propagate it (and any top-level `truncated` and SDK incompleteness)\n\t\t\t// so the sweep doesn't claim an all-pages result.\n\t\t\tappendTruncationWarnings(fetchFailed ? 'interrupted' : 'exhausted');\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\t// Resolve the continuation BEFORE deciding why the drain stops. `page-budget` claims the missing items\n\t\t// are reachable, which is only true with a usable cursor in hand — and a provider can report another\n\t\t// page while handing back none (Bitbucket Server does, when it omits `nextPageStart`). Checking the\n\t\t// budget first would label that unreachable tail as merely unfetched, so raising `maxPages` would\n\t\t// return the identical set.\n\t\tconst nextCursor = value.paging?.cursor;\n\t\t// A cursor already used isn't a continuation either — following it refetches a page we have, and a\n\t\t// provider that cycles (A→B→A) would otherwise burn the whole budget and then be reported as merely\n\t\t// out of budget. Tracked as a SET rather than compared one-back, matching the SDK's own `followCursors`:\n\t\t// `drainToRequestedPage` and `collectProviderPagedResult` compare only the previous cursor, which a\n\t\t// cycle slips past. That is tolerable there and not here, because only this drain reports `page-budget`.\n\t\tconst continuable = nextCursor != null && nextCursor !== '{}' && !seenCursors.has(nextCursor);\n\t\tif (!continuable || page >= maxPages) {\n\t\t\t// A usable cursor in hand means `page-budget`, even when an earlier page was capped: a composite\n\t\t\t// read caps its facets independently, so one facet reaching the provider's ceiling leaves its\n\t\t\t// siblings' cursors live, and raising the budget returns more of them. What no budget returns is the\n\t\t\t// capped part, so that is reported BESIDE the budget stop rather than in place of it — unless an SDK\n\t\t\t// omission already says so. Neither warning then misstates the part the other describes.\n\t\t\tconst budgetStop = !fetchFailed && continuable;\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\ttruncationWarning(\n\t\t\t\t\tid,\n\t\t\t\t\tdomain,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t'Pull request',\n\t\t\t\t\tfetchFailed ? 'interrupted' : budgetStop ? 'page-budget' : 'exhausted',\n\t\t\t\t),\n\t\t\t);\n\t\t\tconst cap = capWarning();\n\t\t\tif (cap != null) {\n\t\t\t\tappendDedupedWarning(warnings, cap);\n\t\t\t} else if (budgetStop && unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(warnings, cappedBesideBudgetWarning(id, domain, connectionId, 'Pull request'));\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: true,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\tseenCursors.add(nextCursor);\n\t\tcursor = nextCursor;\n\t}\n}\n\n/**\n * Drains every page of repositories under an org for one git-host integration, threading the opaque\n * next-cursor the provider returns. Stops at `maxPages` (marking `truncated`) or on a hard read failure\n * (marking `fetchFailed`), keeping the pages fetched so far.\n */\nexport async function drainRepositories(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\torg: string,\n\tproject: string | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n): Promise<{\n\trepos: ProviderRepository[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n}> {\n\tconst repos: ProviderRepository[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\n\tfor (;;) {\n\t\tpage++;\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() =>\n\t\t\t\tintegration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\tproject: project,\n\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: true },\n\t\t);\n\t\tif (warning != null) {\n\t\t\t// Deduped, like every other accumulation here: a soft warning (`{ value, error }`) repeats verbatim on\n\t\t\t// each page that hits the same condition, and the array's contract is that no two entries are equal.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tconst interruptedAfterProgress = page > 1;\n\t\t\tif (interruptedAfterProgress && warning == null) {\n\t\t\t\t// Not a backstop: the read was cut short mid-drain, which is why it also sets `fetchFailed`\n\t\t\t\t// below. A retry may complete it, so this must not claim the succeeded-but-capped omission.\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Repository', 'interrupted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst failed = fetchFailed || warning != null || interruptedAfterProgress;\n\t\t\t// An SDK omission from an earlier page asserts the read succeeded; this one didn't.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || interruptedAfterProgress,\n\t\t\t};\n\t\t}\n\n\t\trepos.push(...value.values);\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\ttruncated = truncated || value.truncated === true || value.paging?.truncated === true || assessment.truncated;\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t};\n\t\t}\n\n\t\tif (page >= maxPages) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tconst nextCursor = value.paging?.cursor;\n\t\tif (nextCursor == null || nextCursor === '{}') {\n\t\t\t// Provider says there is more but didn't return a usable cursor; stop rather than refetch the same page.\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tcursor = nextCursor;\n\t}\n}\n\nexport function resolvePullRequestSweepTargets(options: PullRequestSweepOptions | undefined): {\n\ttargets: readonly ProviderSweepTarget[];\n\tattributeUnavailableProviders: boolean;\n} {\n\tif (options?.targets != null) {\n\t\tif (options.providerIds != null || options.connectionId != null) {\n\t\t\tthrow new TypeError(\"Pull request sweep 'targets' cannot be combined with 'providerIds' or 'connectionId'\");\n\t\t}\n\n\t\tconst seenProviderIds = new Set<IntegrationIds>();\n\t\tfor (const target of options.targets) {\n\t\t\tif (seenProviderIds.has(target.providerId)) {\n\t\t\t\tthrow new TypeError(\n\t\t\t\t\t`Pull request sweep targets must contain at most one target per provider; duplicate '${target.providerId}'`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tseenProviderIds.add(target.providerId);\n\t\t}\n\n\t\treturn { targets: options.targets, attributeUnavailableProviders: true };\n\t}\n\n\tconst providerIds =\n\t\toptions?.providerIds ?? supportedOrderedCloudIntegrationIds.filter(id => !isIssuesHostIntegrationId(id));\n\tconst connectionId = providerIds.length === 1 ? options?.connectionId : undefined;\n\treturn {\n\t\ttargets: providerIds.map(providerId => ({ providerId: providerId, connectionId: connectionId })),\n\t\tattributeUnavailableProviders: options?.providerIds != null,\n\t};\n}\n\nexport async function getCurrentAccountIdentity(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n): Promise<{ id: string; username?: string } | undefined> {\n\ttry {\n\t\tconst account = await integration.getCurrentAccount({ connectionId: connectionId });\n\t\treturn account != null ? { id: account.id, username: account.username } : undefined;\n\t} catch {\n\t\t// Authorship is optional enrichment; don't turn a successful PR read into a failure if identity lookup fails.\n\t\treturn undefined;\n\t}\n}\n\n/**\n * The current account to map each raw row against, in order (see {@link GitHostIntegration.getPullRequestViewers}).\n * Rows that name no one can't be matched, so a read of only those resolves nothing; a failed lookup leaves every\n * row's authorship unknown rather than failing the read.\n */\nexport async function getPullRequestViewers(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n\tpullRequests: readonly (ProviderPullRequest | PullRequest)[],\n): Promise<({ id: string; username?: string } | null | undefined)[]> {\n\tif (!pullRequests.some(pullRequestNamesPeople)) return pullRequests.map(() => undefined);\n\n\ttry {\n\t\treturn await integration.getPullRequestViewers(pullRequests, connectionId);\n\t} catch {\n\t\treturn pullRequests.map(() => undefined);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -33,6 +33,7 @@ function pullRequestEtagFieldsFromShape(pr) {
|
|
|
33
33
|
statusCheckRollupState: "statusCheckRollupState" in pr ? pr.statusCheckRollupState : void 0
|
|
34
34
|
};
|
|
35
35
|
if (pr.provider != null && azureProviderIds.has(pr.provider.id)) fields.revision = pullRequestRevision(pr);
|
|
36
|
+
if (pr.viewerUnresolved) fields.viewerUnresolved = true;
|
|
36
37
|
return fields;
|
|
37
38
|
}
|
|
38
39
|
function issueEtagFieldsFromShape(issue) {
|
|
@@ -43,7 +44,7 @@ function issueEtagFieldsFromShape(issue) {
|
|
|
43
44
|
if (issue.thumbsUpCount != null && issue.provider != null && getIssueFieldPresence(issue)?.reactions === "fetched") fields.thumbsUpCount = issue.thumbsUpCount;
|
|
44
45
|
if (issue.provider != null && azureProviderIds.has(issue.provider.id)) {
|
|
45
46
|
fields.descriptionRule = 1;
|
|
46
|
-
if (issue.viewer != null || !issueNamesPeople(issue)) fields.viewerRule = 1;
|
|
47
|
+
if (!issue.viewerUnresolved && (issue.viewer != null || !issueNamesPeople(issue))) fields.viewerRule = 1;
|
|
47
48
|
}
|
|
48
49
|
return fields;
|
|
49
50
|
}
|
|
@@ -71,6 +72,7 @@ function pullRequestEtag(fields, includes) {
|
|
|
71
72
|
fields.headSha ?? null
|
|
72
73
|
];
|
|
73
74
|
if (fields.revision != null) state.push(fields.revision);
|
|
75
|
+
if (fields.viewerUnresolved) state.push("viewer-unresolved");
|
|
74
76
|
if (covered.length === 0) return `pr1:${JSON.stringify(state)}`;
|
|
75
77
|
for (const include of covered) switch (include) {
|
|
76
78
|
case "mergeable":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"etag.js","names":[],"sources":["../../../../../plus/integrations/src/reads/etag.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { PullRequest, PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';\nimport { getIssueFieldPresence } from '../fieldPresence.js';\nimport type {\n\tIssueEtagFields,\n\tIssueEtagInclude,\n\tPullRequestEtagFields,\n\tPullRequestEtagInclude,\n} from '../models/integration.js';\nimport {\n\tazureWorkItemDescriptionRule,\n\tazureWorkItemViewerRule,\n\tissueEtagIncludes,\n\tpullRequestEtagIncludes,\n\tpullRequestRevision,\n} from '../models/integration.js';\nimport { issueNamesPeople } from '../providers/models.js';\n\n/**\n * The etags the batch reads hand back (`getPullRequestsBatch`, `getIssuesBatch`): an opaque stamp of an item's\n * change state, which a caller sends back so a cheap check can answer \"unchanged\" without a full read.\n *\n * Computed by core, never the provider's HTTP ETag, and from the fields alone — not the item's id, since the\n * caller's key already identifies the target and ids cross vocabularies between reads. Not hashed: the stamp IS\n * the serialized fields, so two different change states can never collide into a false `unchanged` — except for an\n * Azure DevOps pull request's `revision`, a 64-bit hash of the fields Azure changes without a timestamp, where a\n * collision (about 2^-64) is the only way a change can hide. The prefix names the scheme (`pr1`, `is1`) and every\n * include it covers ({@link PullRequestEtagInclude}, {@link IssueEtagInclude}), in canonical order\n * (`pr1+mergeable+checks:`, `is1+reactions:`), so two different sets can never collide even when their values do (a\n * `null` mergeability and a `null` rollup). A stamp from another scheme or another set just compares unequal and\n * costs a full read.\n */\n\n/**\n * Azure DevOps: a pull request there keeps no update time, so its etag adds a {@link pullRequestRevision}, and a work\n * item's etag adds the rule its body is read by (see `azureWorkItemDescriptionRule`).\n */\nconst azureProviderIds: ReadonlySet<string> = new Set([\n\tGitCloudHostIntegrationId.AzureDevOps,\n\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n]);\n\nexport function pullRequestEtagFieldsFromShape(pr: PullRequestShape): PullRequestEtagFields {\n\tconst fields: PullRequestEtagFields = {\n\t\tstate: pr.state,\n\t\tisDraft: pr.isDraft,\n\t\tupdatedDate: pr.updatedDate,\n\t\theadSha: pr.refs?.head.sha,\n\t\tmergeableState: pr.mergeableState,\n\t\treviewDecision: pr.reviewDecision,\n\t\t// On the class, not the shape: every provider's mapper builds a `PullRequest`. Read by name rather than by\n\t\t// `instanceof`, which a second copy of the model module fails, silently dropping the rollup from every full\n\t\t// row's etag so that no cheap check ever matches it again.\n\t\tstatusCheckRollupState: 'statusCheckRollupState' in pr ? (pr as PullRequest).statusCheckRollupState : undefined,\n\t};\n\t// Read defensively, as `timeOf` reads the date: a row without a provider must not fail the read.\n\tif (pr.provider != null && azureProviderIds.has(pr.provider.id)) {\n\t\tfields.revision = pullRequestRevision(pr);\n\t}\n\treturn fields;\n}\n\nexport function issueEtagFieldsFromShape(issue: IssueShape): IssueEtagFields {\n\tconst fields: IssueEtagFields = { state: issue.state, updatedDate: issue.updatedDate };\n\t// Only where the row's read really fetched reactions: elsewhere a count is a placeholder or a tracker's votes,\n\t// which no cheap check reads, so taking it would fail every check of that row under `'reactions'`.\n\tif (\n\t\tissue.thumbsUpCount != null &&\n\t\tissue.provider != null &&\n\t\tgetIssueFieldPresence(issue)?.reactions === 'fetched'\n\t) {\n\t\tfields.thumbsUpCount = issue.thumbsUpCount;\n\t}\n\tif (issue.provider != null && azureProviderIds.has(issue.provider.id)) {\n\t\tfields.descriptionRule = azureWorkItemDescriptionRule;\n\t\t// A row that names someone and has no viewer was read while its organization's identity couldn't be: it\n\t\t// must not compare unchanged, or it would be kept without one until the work item changes.\n\t\tif (issue.viewer != null || !issueNamesPeople(issue)) {\n\t\t\tfields.viewerRule = azureWorkItemViewerRule;\n\t\t}\n\t}\n\treturn fields;\n}\n\n/**\n * The distinct `includes` in canonical order, whatever order and repetition the caller gave. Values it doesn't know\n * are dropped, so a caller validates them first (see {@link findInvalidPullRequestEtagInclude}).\n */\nexport function normalizePullRequestEtagIncludes(\n\tincludes: readonly PullRequestEtagInclude[],\n): readonly PullRequestEtagInclude[] {\n\treturn pullRequestEtagIncludes.filter(include => includes.includes(include));\n}\n\n/** The first of `includes` that isn't a {@link PullRequestEtagInclude}, if any. */\nexport function findInvalidPullRequestEtagInclude(includes: readonly string[]): string | undefined {\n\treturn includes.find(include => !(pullRequestEtagIncludes as readonly string[]).includes(include));\n}\n\n/**\n * Each of `includes` adds one input — mergeability, review decision or check rollup — which a host may change\n * without moving `updatedDate`. An input that isn't listed is left out, so its churn never forces a full read.\n */\nexport function pullRequestEtag(fields: PullRequestEtagFields, includes: readonly PullRequestEtagInclude[]): string {\n\tconst covered = normalizePullRequestEtagIncludes(includes);\n\tconst state: unknown[] = [fields.state, fields.isDraft ?? null, timeOf(fields.updatedDate), fields.headSha ?? null];\n\t// Only where it is set, so every other host's etag stays exactly what it was.\n\tif (fields.revision != null) {\n\t\tstate.push(fields.revision);\n\t}\n\n\tif (covered.length === 0) return `pr1:${JSON.stringify(state)}`;\n\n\tfor (const include of covered) {\n\t\tswitch (include) {\n\t\t\tcase 'mergeable':\n\t\t\t\tstate.push(fields.mergeableState ?? null);\n\t\t\t\tbreak;\n\t\t\tcase 'reviewDecision':\n\t\t\t\tstate.push(fields.reviewDecision ?? null);\n\t\t\t\tbreak;\n\t\t\tcase 'checks':\n\t\t\t\tstate.push(fields.statusCheckRollupState ?? null);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn `pr1+${covered.join('+')}:${JSON.stringify(state)}`;\n}\n\n/** The issue twin of {@link normalizePullRequestEtagIncludes}; validate with {@link findInvalidIssueEtagInclude}. */\nexport function normalizeIssueEtagIncludes(includes: readonly IssueEtagInclude[]): readonly IssueEtagInclude[] {\n\treturn issueEtagIncludes.filter(include => includes.includes(include));\n}\n\n/** The first of `includes` that isn't an {@link IssueEtagInclude}, if any. */\nexport function findInvalidIssueEtagInclude(includes: readonly string[]): string | undefined {\n\treturn includes.find(include => !(issueEtagIncludes as readonly string[]).includes(include));\n}\n\n/** `'reactions'` adds the thumbs-up count, which a host changes without moving `updatedDate`. */\nexport function issueEtag(fields: IssueEtagFields, includes: readonly IssueEtagInclude[]): string {\n\tconst covered = normalizeIssueEtagIncludes(includes);\n\tconst state: unknown[] = [fields.state, timeOf(fields.updatedDate)];\n\t// Only where it is set, so every other provider's etag stays exactly what it was.\n\tif (fields.descriptionRule != null) {\n\t\tstate.push(fields.descriptionRule);\n\t}\n\tif (fields.viewerRule != null) {\n\t\tstate.push(fields.viewerRule);\n\t}\n\tif (covered.length === 0) return `is1:${JSON.stringify(state)}`;\n\n\tfor (const include of covered) {\n\t\tswitch (include) {\n\t\t\tcase 'reactions':\n\t\t\t\tstate.push(fields.thumbsUpCount ?? null);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn `is1+${covered.join('+')}:${JSON.stringify(state)}`;\n}\n\n/**\n * Never throws: an etag is computed for every fully read row, so a mapper that hands over a missing or unparseable\n * date must not fail the whole read. Such a row's etag just won't match a cheap check's, which always has a date,\n * so the cost is a full read, never a false `unchanged`.\n */\nfunction timeOf(date: Date): number | null {\n\tconst time = new Date(date).getTime();\n\treturn Number.isNaN(time) ? null : time;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsCA,MAAM,mCAAwC,IAAI,IAAI,CAAA,eAAA,qBAGtD,CAAC;AAED,SAAgB,+BAA+B,IAA6C;CAC3F,MAAM,SAAgC;EACrC,OAAO,GAAG;EACV,SAAS,GAAG;EACZ,aAAa,GAAG;EAChB,SAAS,GAAG,MAAM,KAAK;EACvB,gBAAgB,GAAG;EACnB,gBAAgB,GAAG;EAInB,wBAAwB,4BAA4B,KAAM,GAAmB,yBAAyB,KAAA;CACvG;CAEA,IAAI,GAAG,YAAY,QAAQ,iBAAiB,IAAI,GAAG,SAAS,EAAE,GAC7D,OAAO,WAAW,oBAAoB,EAAE;CAEzC,OAAO;AACR;AAEA,SAAgB,yBAAyB,OAAoC;CAC5E,MAAM,SAA0B;EAAE,OAAO,MAAM;EAAO,aAAa,MAAM;CAAY;CAGrF,IACC,MAAM,iBAAiB,QACvB,MAAM,YAAY,QAClB,sBAAsB,KAAK,CAAC,EAAE,cAAc,WAE5C,OAAO,gBAAgB,MAAM;CAE9B,IAAI,MAAM,YAAY,QAAQ,iBAAiB,IAAI,MAAM,SAAS,EAAE,GAAG;EACtE,OAAO,kBAAA;EAGP,IAAI,MAAM,UAAU,QAAQ,CAAC,iBAAiB,KAAK,GAClD,OAAO,aAAA;CAET;CACA,OAAO;AACR;;;;;AAMA,SAAgB,iCACf,UACoC;CACpC,OAAO,wBAAwB,QAAO,YAAW,SAAS,SAAS,OAAO,CAAC;AAC5E;;AAGA,SAAgB,kCAAkC,UAAiD;CAClG,OAAO,SAAS,MAAK,YAAW,CAAE,wBAA8C,SAAS,OAAO,CAAC;AAClG;;;;;AAMA,SAAgB,gBAAgB,QAA+B,UAAqD;CACnH,MAAM,UAAU,iCAAiC,QAAQ;CACzD,MAAM,QAAmB;EAAC,OAAO;EAAO,OAAO,WAAW;EAAM,OAAO,OAAO,WAAW;EAAG,OAAO,WAAW;CAAI;CAElH,IAAI,OAAO,YAAY,MACtB,MAAM,KAAK,OAAO,QAAQ;CAG3B,IAAI,QAAQ,WAAW,GAAG,OAAO,OAAO,KAAK,UAAU,KAAK;CAE5D,KAAK,MAAM,WAAW,SACrB,QAAQ,SAAR;EACC,KAAK;GACJ,MAAM,KAAK,OAAO,kBAAkB,IAAI;GACxC;EACD,KAAK;GACJ,MAAM,KAAK,OAAO,kBAAkB,IAAI;GACxC;EACD,KAAK,UACJ,MAAM,KAAK,OAAO,0BAA0B,IAAI;CAElD;CAGD,OAAO,OAAO,QAAQ,KAAK,GAAG,EAAE,GAAG,KAAK,UAAU,KAAK;AACxD;;AAGA,SAAgB,2BAA2B,UAAoE;CAC9G,OAAO,kBAAkB,QAAO,YAAW,SAAS,SAAS,OAAO,CAAC;AACtE;;AAGA,SAAgB,4BAA4B,UAAiD;CAC5F,OAAO,SAAS,MAAK,YAAW,CAAE,kBAAwC,SAAS,OAAO,CAAC;AAC5F;;AAGA,SAAgB,UAAU,QAAyB,UAA+C;CACjG,MAAM,UAAU,2BAA2B,QAAQ;CACnD,MAAM,QAAmB,CAAC,OAAO,OAAO,OAAO,OAAO,WAAW,CAAC;CAElE,IAAI,OAAO,mBAAmB,MAC7B,MAAM,KAAK,OAAO,eAAe;CAElC,IAAI,OAAO,cAAc,MACxB,MAAM,KAAK,OAAO,UAAU;CAE7B,IAAI,QAAQ,WAAW,GAAG,OAAO,OAAO,KAAK,UAAU,KAAK;CAE5D,KAAK,MAAM,WAAW,SACrB,QAAQ,SAAR;EACC,KAAK,aACJ,MAAM,KAAK,OAAO,iBAAiB,IAAI;CAEzC;CAGD,OAAO,OAAO,QAAQ,KAAK,GAAG,EAAE,GAAG,KAAK,UAAU,KAAK;AACxD;;;;;;AAOA,SAAS,OAAO,MAA2B;CAC1C,MAAM,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;CACpC,OAAO,OAAO,MAAM,IAAI,IAAI,OAAO;AACpC"}
|
|
1
|
+
{"version":3,"file":"etag.js","names":[],"sources":["../../../../../plus/integrations/src/reads/etag.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { PullRequest, PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';\nimport { getIssueFieldPresence } from '../fieldPresence.js';\nimport type {\n\tIssueEtagFields,\n\tIssueEtagInclude,\n\tPullRequestEtagFields,\n\tPullRequestEtagInclude,\n} from '../models/integration.js';\nimport {\n\tazureWorkItemDescriptionRule,\n\tazureWorkItemViewerRule,\n\tissueEtagIncludes,\n\tpullRequestEtagIncludes,\n\tpullRequestRevision,\n} from '../models/integration.js';\nimport { issueNamesPeople } from '../providers/models.js';\n\n/**\n * The etags the batch reads hand back (`getPullRequestsBatch`, `getIssuesBatch`): an opaque stamp of an item's\n * change state, which a caller sends back so a cheap check can answer \"unchanged\" without a full read.\n *\n * Computed by core, never the provider's HTTP ETag, and from the fields alone — not the item's id, since the\n * caller's key already identifies the target and ids cross vocabularies between reads. Not hashed: the stamp IS\n * the serialized fields, so two different change states can never collide into a false `unchanged` — except for an\n * Azure DevOps pull request's `revision`, a 64-bit hash of the fields Azure changes without a timestamp, where a\n * collision (about 2^-64) is the only way a change can hide. The prefix names the scheme (`pr1`, `is1`) and every\n * include it covers ({@link PullRequestEtagInclude}, {@link IssueEtagInclude}), in canonical order\n * (`pr1+mergeable+checks:`, `is1+reactions:`), so two different sets can never collide even when their values do (a\n * `null` mergeability and a `null` rollup). A stamp from another scheme or another set just compares unequal and\n * costs a full read.\n */\n\n/**\n * Azure DevOps: a pull request there keeps no update time, so its etag adds a {@link pullRequestRevision}, and a work\n * item's etag adds the rule its body is read by (see `azureWorkItemDescriptionRule`).\n */\nconst azureProviderIds: ReadonlySet<string> = new Set([\n\tGitCloudHostIntegrationId.AzureDevOps,\n\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n]);\n\nexport function pullRequestEtagFieldsFromShape(pr: PullRequestShape): PullRequestEtagFields {\n\tconst fields: PullRequestEtagFields = {\n\t\tstate: pr.state,\n\t\tisDraft: pr.isDraft,\n\t\tupdatedDate: pr.updatedDate,\n\t\theadSha: pr.refs?.head.sha,\n\t\tmergeableState: pr.mergeableState,\n\t\treviewDecision: pr.reviewDecision,\n\t\t// On the class, not the shape: every provider's mapper builds a `PullRequest`. Read by name rather than by\n\t\t// `instanceof`, which a second copy of the model module fails, silently dropping the rollup from every full\n\t\t// row's etag so that no cheap check ever matches it again.\n\t\tstatusCheckRollupState: 'statusCheckRollupState' in pr ? (pr as PullRequest).statusCheckRollupState : undefined,\n\t};\n\t// Read defensively, as `timeOf` reads the date: a row without a provider must not fail the read.\n\tif (pr.provider != null && azureProviderIds.has(pr.provider.id)) {\n\t\tfields.revision = pullRequestRevision(pr);\n\t}\n\tif (pr.viewerUnresolved) {\n\t\tfields.viewerUnresolved = true;\n\t}\n\treturn fields;\n}\n\nexport function issueEtagFieldsFromShape(issue: IssueShape): IssueEtagFields {\n\tconst fields: IssueEtagFields = { state: issue.state, updatedDate: issue.updatedDate };\n\t// Only where the row's read really fetched reactions: elsewhere a count is a placeholder or a tracker's votes,\n\t// which no cheap check reads, so taking it would fail every check of that row under `'reactions'`.\n\tif (\n\t\tissue.thumbsUpCount != null &&\n\t\tissue.provider != null &&\n\t\tgetIssueFieldPresence(issue)?.reactions === 'fetched'\n\t) {\n\t\tfields.thumbsUpCount = issue.thumbsUpCount;\n\t}\n\tif (issue.provider != null && azureProviderIds.has(issue.provider.id)) {\n\t\tfields.descriptionRule = azureWorkItemDescriptionRule;\n\t\t// A row read while its organization's identity couldn't be must not compare unchanged, or it would be kept\n\t\t// without a viewer until the work item changes. Also a row that names someone and has neither field: one held\n\t\t// from before `viewerUnresolved` existed.\n\t\tif (!issue.viewerUnresolved && (issue.viewer != null || !issueNamesPeople(issue))) {\n\t\t\tfields.viewerRule = azureWorkItemViewerRule;\n\t\t}\n\t}\n\treturn fields;\n}\n\n/**\n * The distinct `includes` in canonical order, whatever order and repetition the caller gave. Values it doesn't know\n * are dropped, so a caller validates them first (see {@link findInvalidPullRequestEtagInclude}).\n */\nexport function normalizePullRequestEtagIncludes(\n\tincludes: readonly PullRequestEtagInclude[],\n): readonly PullRequestEtagInclude[] {\n\treturn pullRequestEtagIncludes.filter(include => includes.includes(include));\n}\n\n/** The first of `includes` that isn't a {@link PullRequestEtagInclude}, if any. */\nexport function findInvalidPullRequestEtagInclude(includes: readonly string[]): string | undefined {\n\treturn includes.find(include => !(pullRequestEtagIncludes as readonly string[]).includes(include));\n}\n\n/**\n * Each of `includes` adds one input — mergeability, review decision or check rollup — which a host may change\n * without moving `updatedDate`. An input that isn't listed is left out, so its churn never forces a full read.\n */\nexport function pullRequestEtag(fields: PullRequestEtagFields, includes: readonly PullRequestEtagInclude[]): string {\n\tconst covered = normalizePullRequestEtagIncludes(includes);\n\tconst state: unknown[] = [fields.state, fields.isDraft ?? null, timeOf(fields.updatedDate), fields.headSha ?? null];\n\t// Only where it is set, so every other host's etag stays exactly what it was.\n\tif (fields.revision != null) {\n\t\tstate.push(fields.revision);\n\t}\n\tif (fields.viewerUnresolved) {\n\t\tstate.push('viewer-unresolved');\n\t}\n\n\tif (covered.length === 0) return `pr1:${JSON.stringify(state)}`;\n\n\tfor (const include of covered) {\n\t\tswitch (include) {\n\t\t\tcase 'mergeable':\n\t\t\t\tstate.push(fields.mergeableState ?? null);\n\t\t\t\tbreak;\n\t\t\tcase 'reviewDecision':\n\t\t\t\tstate.push(fields.reviewDecision ?? null);\n\t\t\t\tbreak;\n\t\t\tcase 'checks':\n\t\t\t\tstate.push(fields.statusCheckRollupState ?? null);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn `pr1+${covered.join('+')}:${JSON.stringify(state)}`;\n}\n\n/** The issue twin of {@link normalizePullRequestEtagIncludes}; validate with {@link findInvalidIssueEtagInclude}. */\nexport function normalizeIssueEtagIncludes(includes: readonly IssueEtagInclude[]): readonly IssueEtagInclude[] {\n\treturn issueEtagIncludes.filter(include => includes.includes(include));\n}\n\n/** The first of `includes` that isn't an {@link IssueEtagInclude}, if any. */\nexport function findInvalidIssueEtagInclude(includes: readonly string[]): string | undefined {\n\treturn includes.find(include => !(issueEtagIncludes as readonly string[]).includes(include));\n}\n\n/** `'reactions'` adds the thumbs-up count, which a host changes without moving `updatedDate`. */\nexport function issueEtag(fields: IssueEtagFields, includes: readonly IssueEtagInclude[]): string {\n\tconst covered = normalizeIssueEtagIncludes(includes);\n\tconst state: unknown[] = [fields.state, timeOf(fields.updatedDate)];\n\t// Only where it is set, so every other provider's etag stays exactly what it was.\n\tif (fields.descriptionRule != null) {\n\t\tstate.push(fields.descriptionRule);\n\t}\n\tif (fields.viewerRule != null) {\n\t\tstate.push(fields.viewerRule);\n\t}\n\tif (covered.length === 0) return `is1:${JSON.stringify(state)}`;\n\n\tfor (const include of covered) {\n\t\tswitch (include) {\n\t\t\tcase 'reactions':\n\t\t\t\tstate.push(fields.thumbsUpCount ?? null);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn `is1+${covered.join('+')}:${JSON.stringify(state)}`;\n}\n\n/**\n * Never throws: an etag is computed for every fully read row, so a mapper that hands over a missing or unparseable\n * date must not fail the whole read. Such a row's etag just won't match a cheap check's, which always has a date,\n * so the cost is a full read, never a false `unchanged`.\n */\nfunction timeOf(date: Date): number | null {\n\tconst time = new Date(date).getTime();\n\treturn Number.isNaN(time) ? null : time;\n}\n"],"mappings":"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"}
|
package/docs/integrations.md
CHANGED
|
@@ -214,7 +214,8 @@ The hosts with a cheap check select only the change state:
|
|
|
214
214
|
comment or a label, which its full row doesn't carry either, or a vote that keeps the reviewer's state, such as an
|
|
215
215
|
approval becoming an approval with suggestions. A hash collision is the only way an edit it does see can hide,
|
|
216
216
|
at about 2^-64. An Azure DevOps pull request etag from before the revision existed compares unequal once and costs
|
|
217
|
-
one full read.
|
|
217
|
+
one full read. A pull request or work item row marked `viewerUnresolved` (see `viewer` and `viewerUnresolved` below)
|
|
218
|
+
never compares unchanged, so it is read in full again rather than kept without its viewer until it changes.
|
|
218
219
|
- **Bitbucket Cloud:** one list request per repository per 50 pull requests, by id and in every state, where the
|
|
219
220
|
full read sends one request per pull request. A pull request missing from the list is proven absent, as the full
|
|
220
221
|
read's 404 would prove it. Bitbucket Cloud's rows carry no mergeability or check rollup, so its etag
|
|
@@ -281,7 +282,25 @@ and `latestReviews` carry the collection's. So the pull request reads resolve th
|
|
|
281
282
|
row whose `authoredByMe` was resolved also carries `viewer`, the identity it was matched against: per collection on
|
|
282
283
|
Azure DevOps Server, the account itself everywhere else. To tell whether a review was requested from the current
|
|
283
284
|
user, or which review is theirs, compare the reviewer entries against `viewer`, not against `getCurrentAccount`. A row
|
|
284
|
-
whose identity couldn't be resolved has neither field, and its authorship is unknown rather than `false
|
|
285
|
+
whose identity couldn't be resolved has neither field, and its authorship is unknown rather than `false`; on Azure
|
|
286
|
+
DevOps it also says so (see `viewerUnresolved` below).
|
|
287
|
+
|
|
288
|
+
Azure DevOps (Services, per organization) and Azure DevOps Server (per collection) work items carry `viewer` the same
|
|
289
|
+
way: the identity a work item's `author` and `assignees` were read with, which is what "assigned to me" and "authored
|
|
290
|
+
by me" compare against. Every other provider's issues have none, and are matched against `getCurrentAccount`.
|
|
291
|
+
|
|
292
|
+
On Azure DevOps and Azure DevOps Server, that identity is read from the organization's `ConnectionData`, which a read
|
|
293
|
+
that only enriches its rows with it waits on for at most 10 seconds. When that read fails, answers without an identity
|
|
294
|
+
or doesn't answer in time, a pull request or work item row that names someone carries `viewerUnresolved: true` instead
|
|
295
|
+
of `viewer` (and, on a pull request, `authoredByMe`): nobody can be matched against its people, `getCurrentAccount`
|
|
296
|
+
included, since on those hosts that account's id is the one that may not match. Treat such a row's "mine" questions as
|
|
297
|
+
unknown, not as "no". A later read resolves it once the organization answers; one that never does stays unresolved until
|
|
298
|
+
its request fails at the transport (the host's `fetch` timeout; about 5 minutes on Node's) or the connection re-syncs,
|
|
299
|
+
since reads don't wait on the same hung request again. `viewerUnresolved` is set on no other row, so a row with neither
|
|
300
|
+
field still means what it did: a provider with no per-row identity, or a read that resolved none for another reason. The
|
|
301
|
+
account-wide pull request reads (`listPullRequestsPage` and the sweeps without `repos`) and a relationship search are
|
|
302
|
+
different: they need the identity to filter by, so they wait on it without that bound, and an organization whose
|
|
303
|
+
identity can't be read is reported as a failed scope with its rows left out, never as marked rows.
|
|
285
304
|
|
|
286
305
|
### Field presence
|
|
287
306
|
|
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.19.0",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "GitKraken",
|