@gitkraken/core-gitlens 0.15.2 → 0.15.4
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 +35 -1
- package/dist/git/cache.d.ts +24 -11
- package/dist/git/cache.js +44 -16
- package/dist/git/cache.js.map +1 -1
- package/dist/git/models/issue.d.ts +2 -2
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/models/issueOrPullRequest.d.ts +2 -1
- package/dist/git/models/pullRequest.d.ts +8 -5
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/git/models/reference.d.ts +8 -0
- package/dist/git/providers/branches.d.ts +43 -0
- package/dist/git/watching/changeEvent.d.ts +4 -2
- package/dist/git/watching/changeEvent.js +3 -1
- package/dist/git/watching/changeEvent.js.map +1 -1
- package/dist/git/watching/watchGroup.d.ts +3 -3
- package/dist/git/watching/watchGroup.js +1 -0
- package/dist/git/watching/watchGroup.js.map +1 -1
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git/watching/watchSession.d.ts +2 -1
- package/dist/git/watching/watchSession.js +3 -2
- package/dist/git/watching/watchSession.js.map +1 -1
- package/dist/git/watching/watcherPatterns.js +1 -0
- package/dist/git/watching/watcherPatterns.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +19 -3
- package/dist/git/watching/worktreeSummary.js +107 -17
- package/dist/git/watching/worktreeSummary.js.map +1 -1
- package/dist/git-cli/parsers/refParser.js +2 -1
- package/dist/git-cli/parsers/refParser.js.map +1 -1
- package/dist/git-cli/providers/branches.d.ts +8 -1
- package/dist/git-cli/providers/branches.js +84 -12
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/config.d.ts +32 -9
- package/dist/git-cli/providers/config.js +113 -24
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +29 -14
- package/dist/plus/integrations/models/gitHostIntegration.js +41 -19
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +1 -1
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.d.ts +16 -15
- package/dist/plus/integrations/providers/azure/azure.js +72 -23
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.utils.d.ts +12 -0
- package/dist/plus/integrations/providers/azure/azure.utils.js +17 -1
- package/dist/plus/integrations/providers/azure/azure.utils.js.map +1 -1
- package/dist/plus/integrations/providers/azure/models.d.ts +3 -1
- package/dist/plus/integrations/providers/azure/models.js +8 -5
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azure/search.d.ts +10 -4
- package/dist/plus/integrations/providers/azure/search.js +74 -11
- package/dist/plus/integrations/providers/azure/search.js.map +1 -1
- package/dist/plus/integrations/providers/azure/workItemSearch.d.ts +11 -0
- package/dist/plus/integrations/providers/azure/workItemSearch.js +54 -0
- package/dist/plus/integrations/providers/azure/workItemSearch.js.map +1 -0
- package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +2 -2
- package/dist/plus/integrations/providers/azure/workItemStateCategories.js +19 -15
- package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +33 -20
- package/dist/plus/integrations/providers/azureDevOps.js +109 -48
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +16 -47
- package/dist/plus/integrations/providers/azureDevOpsServer.js +51 -90
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
- package/dist/plus/integrations/providers/models.d.ts +5 -0
- package/dist/plus/integrations/providers/models.js +25 -11
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +3 -2
- package/dist/plus/integrations/reads/drains.js +4 -4
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/issues.js +7 -4
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/resolveRepository.js +5 -5
- package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/azure/search.ts"],"sourcesContent":["import { escapeWiqlString } from '@gitkraken/provider-apis';\nimport type { IssueSearchCriteria, IssueSearchRelationship, IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSearchCriteria, PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport { fnv1aHash64 } from '@gitlens/utils/hash.js';\nimport type { ProviderPullRequest } from '../models.js';\nimport { parsePageCursor } from '../utils/providerPaging.js';\n\n/**\n * The most work items one filtered search can reach, however it is paged.\n *\n * Azure DevOps documents 20,000 work items as its query result limit, and may refuse a larger WIQL result set\n * (`VS402337`) rather than truncate it. The page read therefore asks for a window bounded by `$top`, the first\n * 20,000 matches under the requested order. Azure DevOps Server 2020 answers that bounded query with 20,014 matches\n * (it even answered one without `$top`), so the ceiling is this read's, kept at the documented limit.\n */\nexport const azureWorkItemSearchResultLimit = 20000;\n\n/** The work items one detail read can resolve: `workitemsbatch` rejects more than 200 ids. */\nexport const azureWorkItemBatchLimit = 200;\n\n/**\n * A search page size from a caller's `itemsPerPage`, within `[1, max]`. A value that isn't a finite number takes the\n * default: `NaN` would survive the clamp and serve empty pages that never advance.\n */\nexport function toAzureSearchPageSize(value: number | undefined, fallback: number, max: number): number {\n\tconst size = value != null && Number.isFinite(value) ? Math.trunc(value) : fallback;\n\treturn Math.min(max, Math.max(1, size));\n}\n\n/**\n * Each filtered-search relationship as the WIQL clause that expresses it.\n *\n * `@Me` rather than an identity looked up per read: Azure resolves the macro to the identity the credential\n * authenticates as, which is exactly the \"current user\" the relationship names, and needs no account read.\n * `mentioned` is absent. `@RecentMentions` only covers the last 30 days, so declaring it would serve an arbitrary\n * window under a relationship that promises every mention.\n */\nconst relationshipClauses: Partial<Record<IssueSearchRelationship, string>> = {\n\tauthored: '[System.CreatedBy] = @Me',\n\tassigned: '[System.AssignedTo] = @Me',\n\t'any-assignee': \"[System.AssignedTo] <> ''\",\n\tunassigned: \"[System.AssignedTo] = ''\",\n};\n\n/** The relationships {@link toAzureWorkItemSearchWiql} can express. */\nexport const azureWorkItemSearchRelationships = Object.keys(relationshipClauses) as IssueSearchRelationship[];\n\n/**\n * Each orderable key as its WIQL `ORDER BY` column.\n *\n * A strict subset of the SDK's WIQL columns: `priority` and `resolved` are left out because their columns are\n * process-template fields. On an on-premises collection running the XML process model, a field its process never\n * declared fails the whole query with `TF51005` rather than ordering nothing, so declaring them would turn a\n * working search into a refused one on exactly the installations this search exists for. Every key kept orders a\n * system field, except `closed`, whose column the open/closed filter below already depends on.\n */\nconst sortColumns: Partial<Record<IssueSorting, string>> = {\n\t'updated:desc': '[System.ChangedDate] Desc',\n\t'updated:asc': '[System.ChangedDate] Asc',\n\t'created:desc': '[System.CreatedDate] Desc',\n\t'created:asc': '[System.CreatedDate] Asc',\n\t'closed:desc': '[Microsoft.VSTS.Common.ClosedDate] Desc',\n\t'closed:asc': '[Microsoft.VSTS.Common.ClosedDate] Asc',\n\t'comments:desc': '[System.CommentCount] Desc',\n\t'comments:asc': '[System.CommentCount] Asc',\n\t'title:desc': '[System.Title] Desc',\n\t'title:asc': '[System.Title] Asc',\n};\n\n/** The sort keys {@link toAzureWorkItemSearchWiql} can express. */\nexport const azureWorkItemSearchSorts = Object.keys(sortColumns) as IssueSorting[];\n\n/**\n * Translates filtered-search criteria into ONE WIQL query over a collection.\n *\n * Relationships are OR-ed inside one parenthesized group rather than run as separate queries, which is what lets a\n * single query both page and count their union exactly: an item matching two relationships is one row, never two.\n * The projects bound the query by name through `[System.TeamProject]`, so a repository scope reaches the work items\n * of its project; with no project the collection itself is the scope.\n *\n * The open/closed split mirrors the SDK's WIQL, so this search and the ordinary project read agree on which work\n * items are open. Free text is a `Contains` substring match on the title, and each label a `Contains` over the\n * tags, which Azure matches against whole tags — AND-ed, like the criteria model's labels.\n *\n * Throws for a relationship or sort key it cannot express. The facade validates both against the declared\n * capability first, so reaching here with one means the declaration and this translation disagree.\n */\nexport function toAzureWorkItemSearchWiql(\n\tcriteria: IssueSearchCriteria | undefined,\n\tsort: IssueSorting,\n\tprojects: readonly string[] | undefined,\n): string {\n\tconst clauses: string[] = [];\n\tif (projects?.length) {\n\t\tclauses.push(`[System.TeamProject] IN (${projects.map(p => `'${escapeWiqlString(p)}'`).join(', ')})`);\n\t}\n\n\tswitch (criteria?.state ?? 'open') {\n\t\tcase 'open':\n\t\t\tclauses.push(\"[Microsoft.VSTS.Common.ClosedDate] = ''\", \"[Microsoft.VSTS.Common.ResolvedDate] = ''\");\n\t\t\tbreak;\n\t\tcase 'closed':\n\t\t\tclauses.push(\"([Microsoft.VSTS.Common.ClosedDate] <> '' OR [Microsoft.VSTS.Common.ResolvedDate] <> '')\");\n\t\t\tbreak;\n\t\tcase 'all':\n\t\t\tbreak;\n\t}\n\n\tconst relationships = [...new Set(criteria?.relationships ?? [])];\n\tif (relationships.length > 0) {\n\t\tconst expressions = relationships.map(r => {\n\t\t\tconst clause = relationshipClauses[r];\n\t\t\tif (clause == null) throw new Error(`Azure DevOps cannot search work items by the '${r}' relationship`);\n\n\t\t\treturn clause;\n\t\t});\n\t\tclauses.push(`(${expressions.join(' OR ')})`);\n\t}\n\n\tconst text = criteria?.text?.trim();\n\tif (text) {\n\t\tclauses.push(`[System.Title] Contains '${escapeWiqlString(text)}'`);\n\t}\n\tfor (const label of criteria?.labels ?? []) {\n\t\tclauses.push(`[System.Tags] Contains '${escapeWiqlString(label)}'`);\n\t}\n\tif (criteria?.updatedAfter != null) {\n\t\tclauses.push(`[System.ChangedDate] >= '${toWiqlDate(criteria.updatedAfter, 'updatedAfter')}'`);\n\t}\n\tif (criteria?.createdAfter != null) {\n\t\tclauses.push(`[System.CreatedDate] >= '${toWiqlDate(criteria.createdAfter, 'createdAfter')}'`);\n\t}\n\n\tconst column = sortColumns[sort];\n\tif (column == null) throw new Error(`Azure DevOps cannot order a work item search by '${sort}'`);\n\n\t// `[System.Id]` makes the order total, so a work item tying on the sort column can't cross a page boundary\n\t// between two requests and be served twice or never.\n\tconst where = clauses.length > 0 ? ` WHERE ${clauses.join(' AND ')}` : '';\n\treturn `SELECT [System.Id] FROM WorkItems${where} ORDER BY ${column}, [System.Id] Desc`;\n}\n\n/**\n * A criteria date as the UTC-midnight literal WIQL compares against. A literal with a time is only accepted by a\n * query sent with `timePrecision=true`, which is how the search sends every query: without it WIQL compares whole\n * days in the server's time zone, and the instant would differ from the one the pull request filter applies.\n */\nfunction toWiqlDate(value: string, name: string): string {\n\treturn parseCriteriaDate(value, name).toISOString().replace('.000Z', 'Z');\n}\n\n/**\n * A criteria date as UTC midnight.\n *\n * Validated to the `YYYY-MM-DD` the criteria model documents rather than parsed leniently: the WIQL value lands\n * inside a quoted literal, where any other shape is either refused or read in the server's locale, and the pull\n * request filter must apply the same instant the work-item query does.\n */\nfunction parseCriteriaDate(value: string, name: string): Date {\n\tconst date = toCriteriaDate(value);\n\tif (date == null) throw new Error(`Invalid ${name} '${value}'; expected an ISO date (YYYY-MM-DD)`);\n\n\treturn date;\n}\n\nfunction toCriteriaDate(value: string): Date | undefined {\n\tconst trimmed = value.trim();\n\tconst match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(trimmed);\n\tconst date = match != null ? new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))) : null;\n\treturn date == null || Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== trimmed\n\t\t? undefined\n\t\t: date;\n}\n\n/**\n * Why a search's dates can't be applied, or `undefined` when they can. The search checks this before any request,\n * so a date it would refuse costs nothing, and in a count refuses only its own scope.\n */\nexport function getInvalidAzureSearchDate(\n\tcriteria: { updatedAfter?: string; createdAfter?: string } | undefined,\n): string | undefined {\n\tfor (const [name, value] of [\n\t\t['updatedAfter', criteria?.updatedAfter],\n\t\t['createdAfter', criteria?.createdAfter],\n\t] as const) {\n\t\tif (value != null && toCriteriaDate(value) == null) {\n\t\t\treturn `invalid ${name} '${value}'; expected an ISO date (YYYY-MM-DD)`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The criteria a pull-request search applies AFTER the provider read, as one predicate.\n *\n * Azure's pull request query takes a creator, a reviewer, a status and a repository, and nothing else, so text,\n * draft state and dates are matched here against each fully drained facet. Applying them to a complete facet is\n * exact rather than a post-filter of a page: nothing the predicate removes was ever counted toward a page or a\n * total, so a count and its list always agree.\n *\n * What each criterion matches is bounded by what the list endpoint returns, which is narrower than the web UI:\n * - text is a case-insensitive substring of the title or of the description — which Azure truncates to its first\n * 400 characters in a pull request list, so a term only found past that is not matched;\n * - `updatedAfter` compares Azure's `updatedDate`, which the SDK derives from the close date, or the creation date\n * for a pull request still open, since Azure reports no last-activity date. An old but active pull request is\n * therefore matched by its creation date.\n */\nexport function toAzurePullRequestSearchFilter(\n\tcriteria: PullRequestSearchCriteria | undefined,\n): (pr: ProviderPullRequest) => boolean {\n\tconst text = criteria?.text?.trim().toLowerCase();\n\tconst draft = criteria?.draft;\n\tconst updatedAfter =\n\t\tcriteria?.updatedAfter != null ? parseCriteriaDate(criteria.updatedAfter, 'updatedAfter') : undefined;\n\tconst createdAfter =\n\t\tcriteria?.createdAfter != null ? parseCriteriaDate(criteria.createdAfter, 'createdAfter') : undefined;\n\n\treturn pr =>\n\t\t(!text || pr.title.toLowerCase().includes(text) || (pr.description?.toLowerCase().includes(text) ?? false)) &&\n\t\t(draft == null || pr.isDraft === draft) &&\n\t\t(updatedAfter == null || pr.updatedDate >= updatedAfter) &&\n\t\t(createdAfter == null || pr.createdDate >= createdAfter);\n}\n\n/** The pull-request sort keys the search can order its merged facets by. */\nexport const azurePullRequestSearchSorts: PullRequestSorting[] = [\n\t'updated:desc',\n\t'updated:asc',\n\t'created:desc',\n\t'created:asc',\n];\n\n/**\n * A pull request's position in the merged search result: the sort date plus the identity that breaks ties.\n *\n * The identity makes the order TOTAL, which is what lets a cursor resume strictly after the last row served rather\n * than at a numeric offset. An offset would skip or repeat rows whenever a pull request changes state or date\n * between two pages; a keyset position only moves past rows that sort before it.\n */\nexport interface AzurePullRequestSearchPosition {\n\ttime: number;\n\tidentity: string;\n}\n\nexport function toAzurePullRequestSearchPosition(\n\tpr: ProviderPullRequest,\n\tsort: PullRequestSorting,\n\tidentity: string,\n): AzurePullRequestSearchPosition {\n\treturn { time: (sort.startsWith('created') ? pr.createdDate : pr.updatedDate).getTime(), identity: identity };\n}\n\n/** Orders two positions under `sort`, ties broken by identity so no two rows ever compare equal. */\nexport function compareAzurePullRequestSearchPositions(\n\ta: AzurePullRequestSearchPosition,\n\tb: AzurePullRequestSearchPosition,\n\tsort: PullRequestSorting,\n): number {\n\tif (a.time !== b.time) return sort.endsWith(':asc') ? a.time - b.time : b.time - a.time;\n\n\treturn a.identity < b.identity ? -1 : a.identity > b.identity ? 1 : 0;\n}\n\n/**\n * Where a filtered pull-request search resumes: after `after`, in the drain its first page read, under the query\n * `key` fingerprints. Bound to its query and served only from its own drain for the same reasons\n * {@link AzureWorkItemSearchCursor} is bound to its query and snapshot.\n */\nexport interface AzurePullRequestSearchCursor {\n\tkey: string;\n\tdrain: string;\n\tpage: number;\n\tafter: AzurePullRequestSearchPosition;\n}\n\n/**\n * The fingerprint {@link AzurePullRequestSearchCursor.key} carries for a query.\n *\n * It only guards a caller threading a cursor under a different query, and keeps the query itself out of the opaque\n * cursor. It never selects cached results: those are keyed by the full query (see the callers), so a fingerprint\n * collision can at worst accept a cursor, never serve another query's rows.\n */\nexport function toAzurePullRequestSearchCursorKey(query: unknown): string {\n\treturn fnv1aHash64(JSON.stringify(query));\n}\n\n/** Reads a caller-supplied cursor for the query fingerprinted as `key`; see {@link parseAzureWorkItemSearchCursor}. */\nexport function parseAzurePullRequestSearchCursor(\n\tcursor: string | undefined,\n\tkey: string,\n): AzurePullRequestSearchCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\tlet parsed: Partial<AzurePullRequestSearchCursor> | undefined;\n\ttry {\n\t\tparsed = JSON.parse(cursor) as Partial<AzurePullRequestSearchCursor>;\n\t} catch {}\n\n\tconst after = parsed?.after;\n\tif (\n\t\tparsed?.key !== key ||\n\t\ttypeof parsed.drain !== 'string' ||\n\t\t!parsed.drain ||\n\t\t!Number.isSafeInteger(parsed.page) ||\n\t\tparsed.page! < 1 ||\n\t\ttypeof after?.time !== 'number' ||\n\t\t!Number.isFinite(after.time) ||\n\t\ttypeof after.identity !== 'string'\n\t) {\n\t\tthrow new Error(\n\t\t\t'Pull request search cursor was produced by a different query; restart the read without a cursor',\n\t\t);\n\t}\n\n\treturn { key: key, drain: parsed.drain, page: parsed.page!, after: { time: after.time, identity: after.identity } };\n}\n\n/**\n * Where a filtered work-item search resumes: an offset into the id snapshot its first page queried, the id of that\n * snapshot, and the fingerprint of the query that produced it.\n *\n * A continuation is only served from ITS snapshot, where the offset is exact. Each first page queries a snapshot of\n * its own, so a pagination of the same query started later never replaces the one an earlier pagination is reading.\n * The snapshot is kept for as long as the pagination keeps reading it; once it is gone the continuation is REFUSED\n * rather than resumed against a fresh query, since under an `updated` order a re-query can move an unseen work item\n * ahead of the offset, where it would be skipped without anything saying so. A cursor without a `snapshot` is the\n * facade's own page marker, which names a page by number with no snapshot behind it and is read against a fresh\n * query as exactly that.\n *\n * The fingerprint binds the cursor to its query (scope, criteria and order) without publishing any of them inside\n * the opaque cursor. A cursor for another query is REFUSED rather than silently restarted: this read is cursor-only,\n * so a restart would be served under the page number the caller supplied alongside it — page 1's rows labelled as\n * page N.\n */\nexport interface AzureWorkItemSearchCursor {\n\tkey: string;\n\tsnapshot?: string;\n\toffset: number;\n\tpage: number;\n}\n\n/** The fingerprint {@link AzureWorkItemSearchCursor.key} carries for a query; see {@link toAzurePullRequestSearchCursorKey}. */\nexport function toAzureWorkItemSearchCursorKey(collection: string, wiql: string): string {\n\treturn fnv1aHash64(JSON.stringify([collection, wiql]));\n}\n\n/**\n * Whether `cursor` starts a work-item search's first page: none at all, or the facade's page-1 marker, which\n * {@link parseAzureWorkItemSearchCursor} reads the same way.\n */\nexport function isAzureWorkItemSearchFirstPage(cursor: string | undefined): boolean {\n\treturn cursor == null || parsePageCursor(cursor) === 1;\n}\n\n/**\n * Reads a caller-supplied cursor for the query fingerprinted as `key`.\n *\n * Returns `undefined` when there is no cursor, and throws when there is one this query can't resume, since treating\n * a foreign or malformed cursor as page 1 would publish the first page's rows as the page the caller asked for.\n *\n * The one cursor it accepts without a fingerprint is the facade's own page marker (`{ value, type: 'page' }`), which\n * `broadenIssues` sends to retry or reach a numbered page it holds no cursor for. It names a position rather than a\n * query, so it is honored as the offset of that page at `pageSize` — page 1 being no cursor at all.\n */\nexport function parseAzureWorkItemSearchCursor(\n\tcursor: string | undefined,\n\tkey: string,\n\tpageSize: number,\n): AzureWorkItemSearchCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\tconst pageMarker = parsePageCursor(cursor);\n\tif (pageMarker != null && Number.isSafeInteger(pageMarker) && pageMarker >= 1) {\n\t\treturn pageMarker === 1 ? undefined : { key: key, offset: (pageMarker - 1) * pageSize, page: pageMarker };\n\t}\n\n\tlet parsed: Partial<AzureWorkItemSearchCursor> | undefined;\n\ttry {\n\t\tparsed = JSON.parse(cursor) as Partial<AzureWorkItemSearchCursor>;\n\t} catch {}\n\n\tif (\n\t\tparsed?.key !== key ||\n\t\ttypeof parsed.snapshot !== 'string' ||\n\t\t!parsed.snapshot ||\n\t\t!Number.isSafeInteger(parsed.offset) ||\n\t\tparsed.offset! < 0 ||\n\t\t!Number.isSafeInteger(parsed.page) ||\n\t\tparsed.page! < 1\n\t) {\n\t\tthrow new Error('Work item search cursor was produced by a different query; restart the read without a cursor');\n\t}\n\n\treturn { key: key, snapshot: parsed.snapshot, offset: parsed.offset!, page: parsed.page! };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"search.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/azure/search.ts"],"sourcesContent":["import type { AzureWorkItemTypeStates } from '@gitkraken/provider-apis';\nimport { escapeWiqlString } from '@gitkraken/provider-apis';\nimport type { IssueSearchCriteria, IssueSearchRelationship, IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSearchCriteria, PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport { fnv1aHash64 } from '@gitlens/utils/hash.js';\nimport type { ProviderPullRequest } from '../models.js';\nimport { parsePageCursor } from '../utils/providerPaging.js';\n\n/**\n * The most work items one filtered search can reach, however it is paged.\n *\n * Azure DevOps documents 20,000 work items as its query result limit, and may refuse a larger WIQL result set\n * (`VS402337`) rather than truncate it. The page read therefore asks for a window bounded by `$top`, the first\n * 20,000 matches under the requested order. Azure DevOps Server 2020 answers that bounded query with 20,014 matches\n * (it even answered one without `$top`), so the ceiling is this read's, kept at the documented limit.\n */\nexport const azureWorkItemSearchResultLimit = 20000;\n\n/** The work items one detail read can resolve: `workitemsbatch` rejects more than 200 ids. */\nexport const azureWorkItemBatchLimit = 200;\n\nexport const azureWorkItemWiqlLengthLimit = 32000;\nconst workItemAsOfLength = \" ASOF '9999-12-31T23:59:59.9999999+23:59'\".length;\n\n/**\n * A search page size from a caller's `itemsPerPage`, within `[1, max]`. A value that isn't a finite number takes the\n * default: `NaN` would survive the clamp and serve empty pages that never advance.\n */\nexport function toAzureSearchPageSize(value: number | undefined, fallback: number, max: number): number {\n\tconst size = value != null && Number.isFinite(value) ? Math.trunc(value) : fallback;\n\treturn Math.min(max, Math.max(1, size));\n}\n\n/**\n * Each filtered-search relationship as the WIQL clause that expresses it.\n *\n * `@Me` rather than an identity looked up per read: Azure resolves the macro to the identity the credential\n * authenticates as, which is exactly the \"current user\" the relationship names, and needs no account read.\n * `mentioned` is absent. `@RecentMentions` only covers the last 30 days, so declaring it would serve an arbitrary\n * window under a relationship that promises every mention.\n */\nconst relationshipClauses: Partial<Record<IssueSearchRelationship, string>> = {\n\tauthored: '[System.CreatedBy] = @Me',\n\tassigned: '[System.AssignedTo] = @Me',\n\t'any-assignee': \"[System.AssignedTo] <> ''\",\n\tunassigned: \"[System.AssignedTo] = ''\",\n};\n\n/** The relationships {@link toAzureWorkItemSearchWiql} can express. */\nexport const azureWorkItemSearchRelationships = Object.keys(relationshipClauses) as IssueSearchRelationship[];\n\n/**\n * Each orderable key as its WIQL `ORDER BY` column.\n *\n * A strict subset of the SDK's WIQL columns: `priority` and `resolved` are left out because their columns are\n * process-template fields. On an on-premises collection running the XML process model, a field its process never\n * declared fails the whole query with `TF51005` rather than ordering nothing, so declaring them would turn a\n * working search into a refused one on exactly the installations this search exists for. Every key kept orders a\n * system field, except `closed`, which explicitly requests the process-template close-date field.\n */\nconst sortColumns: Partial<Record<IssueSorting, string>> = {\n\t'updated:desc': '[System.ChangedDate] Desc',\n\t'updated:asc': '[System.ChangedDate] Asc',\n\t'created:desc': '[System.CreatedDate] Desc',\n\t'created:asc': '[System.CreatedDate] Asc',\n\t'closed:desc': '[Microsoft.VSTS.Common.ClosedDate] Desc',\n\t'closed:asc': '[Microsoft.VSTS.Common.ClosedDate] Asc',\n\t'comments:desc': '[System.CommentCount] Desc',\n\t'comments:asc': '[System.CommentCount] Asc',\n\t'title:desc': '[System.Title] Desc',\n\t'title:asc': '[System.Title] Asc',\n};\n\n/** The sort keys {@link toAzureWorkItemSearchWiql} can express. */\nexport const azureWorkItemSearchSorts = Object.keys(sortColumns) as IssueSorting[];\n\n/**\n * Translates filtered-search criteria into ONE WIQL query over a collection.\n *\n * Relationships are OR-ed inside one parenthesized group rather than run as separate queries, which is what lets a\n * single query both page and count their union exactly: an item matching two relationships is one row, never two.\n * The projects bound the query by name through `[System.TeamProject]`, so a repository scope reaches the work items\n * of its project; with no project the collection itself is the scope.\n *\n * Workflow metadata selects state names per project and work item type: a name can belong to different categories\n * in different workflows, and Removed work items need not have a closed date. Free text is a `Contains` substring\n * match on the title, and each label a `Contains` over the tags, which Azure matches against whole tags, AND-ed like\n * the criteria model's labels.\n *\n * Throws for a relationship or sort key it cannot express. The facade validates both against the declared\n * capability first, so reaching here with one means the declaration and this translation disagree.\n */\nexport function toAzureWorkItemSearchWiql(\n\tcriteria: IssueSearchCriteria | undefined,\n\tsort: IssueSorting,\n\tprojects: readonly string[] | undefined,\n\tworkflows?: ReadonlyMap<string, AzureWorkItemTypeStates>,\n): string {\n\tconst clauses: string[] = [];\n\tif (projects?.length) {\n\t\tclauses.push(`[System.TeamProject] IN (${projects.map(p => `'${escapeWiqlString(p)}'`).join(', ')})`);\n\t}\n\n\tconst state = criteria?.state ?? 'open';\n\tif (state !== 'all') {\n\t\tif (workflows == null) throw new Error('Azure DevOps state-filtered search requires workflow metadata');\n\n\t\tconst states: string[] = [];\n\t\tfor (const [project, workflow] of workflows) {\n\t\t\tif (workflow.metadata != null) throw new Error('Azure DevOps returned incomplete workflow metadata');\n\n\t\t\tconst groups = new Map<string, { names: string[]; types: string[] }>();\n\t\t\tfor (const type of workflow.types) {\n\t\t\t\tif (type.states == null) throw new Error('Azure DevOps returned no work item states');\n\n\t\t\t\tconst names = type.states.filter(s => {\n\t\t\t\t\tswitch (s.category) {\n\t\t\t\t\t\tcase 'Completed':\n\t\t\t\t\t\tcase 'Resolved':\n\t\t\t\t\t\tcase 'Removed':\n\t\t\t\t\t\t\treturn state === 'closed';\n\t\t\t\t\t\tcase 'Proposed':\n\t\t\t\t\t\tcase 'InProgress':\n\t\t\t\t\t\t\treturn state === 'open';\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tthrow new Error('Azure DevOps returned an unknown work item state category');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tif (!names.length) continue;\n\n\t\t\t\tconst selected = [...new Set(names.map(s => s.name))];\n\t\t\t\tconst key = JSON.stringify([...selected].sort());\n\t\t\t\tconst group = groups.get(key);\n\t\t\t\tif (group != null) {\n\t\t\t\t\tgroup.types.push(type.name);\n\t\t\t\t} else {\n\t\t\t\t\tgroups.set(key, { names: selected, types: [type.name] });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const group of groups.values()) {\n\t\t\t\tconst types = group.types.map(name => `'${escapeWiqlString(name)}'`);\n\t\t\t\tconst typeClause = types.length === 1 ? `= ${types[0]}` : `IN (${types.join(', ')})`;\n\t\t\t\tstates.push(\n\t\t\t\t\t`([System.TeamProject] = '${escapeWiqlString(project)}' AND [System.WorkItemType] ${typeClause} AND [System.State] IN (${group.names.map(name => `'${escapeWiqlString(name)}'`).join(', ')}))`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tclauses.push(states.length ? `(${states.join(' OR ')})` : '[System.Id] = 0');\n\t}\n\n\tconst relationships = [...new Set(criteria?.relationships ?? [])];\n\tif (relationships.length > 0) {\n\t\tconst expressions = relationships.map(r => {\n\t\t\tconst clause = relationshipClauses[r];\n\t\t\tif (clause == null) throw new Error(`Azure DevOps cannot search work items by the '${r}' relationship`);\n\n\t\t\treturn clause;\n\t\t});\n\t\tclauses.push(`(${expressions.join(' OR ')})`);\n\t}\n\n\tconst text = criteria?.text?.trim();\n\tif (text) {\n\t\tclauses.push(`[System.Title] Contains '${escapeWiqlString(text)}'`);\n\t}\n\tfor (const label of criteria?.labels ?? []) {\n\t\tclauses.push(`[System.Tags] Contains '${escapeWiqlString(label)}'`);\n\t}\n\tif (criteria?.updatedAfter != null) {\n\t\tclauses.push(`[System.ChangedDate] >= '${toWiqlDate(criteria.updatedAfter, 'updatedAfter')}'`);\n\t}\n\tif (criteria?.createdAfter != null) {\n\t\tclauses.push(`[System.CreatedDate] >= '${toWiqlDate(criteria.createdAfter, 'createdAfter')}'`);\n\t}\n\n\tconst column = sortColumns[sort];\n\tif (column == null) throw new Error(`Azure DevOps cannot order a work item search by '${sort}'`);\n\n\t// `[System.Id]` makes the order total, so a work item tying on the sort column can't cross a page boundary\n\t// between two requests and be served twice or never.\n\tconst where = clauses.length > 0 ? ` WHERE ${clauses.join(' AND ')}` : '';\n\treturn `SELECT [System.Id] FROM WorkItems${where} ORDER BY ${column}, [System.Id] Desc`;\n}\n\n/** Splits only disjoint scope predicates, leaving every caller criterion on every query. */\nexport function toAzureWorkItemSearchQueries(\n\tcriteria: IssueSearchCriteria | undefined,\n\tsort: IssueSorting,\n\tprojects: readonly string[],\n\tworkflows?: ReadonlyMap<string, AzureWorkItemTypeStates>,\n): string[] {\n\tconst query = toAzureWorkItemSearchWiql(criteria, sort, projects, workflows);\n\tif (query.length + workItemAsOfLength <= azureWorkItemWiqlLengthLimit) return [query];\n\n\tif (projects.length > 1) {\n\t\tconst middle = Math.ceil(projects.length / 2);\n\t\treturn [projects.slice(0, middle), projects.slice(middle)].flatMap(names =>\n\t\t\ttoAzureWorkItemSearchQueries(\n\t\t\t\tcriteria,\n\t\t\t\tsort,\n\t\t\t\tnames,\n\t\t\t\tworkflows == null ? undefined : new Map(names.map(name => [name, workflows.get(name)!])),\n\t\t\t),\n\t\t);\n\t}\n\n\tconst project = projects[0];\n\tconst workflow = workflows?.get(project);\n\tif (workflow != null && workflow.types.length > 1) {\n\t\tconst middle = Math.ceil(workflow.types.length / 2);\n\t\treturn [workflow.types.slice(0, middle), workflow.types.slice(middle)].flatMap(types =>\n\t\t\ttoAzureWorkItemSearchQueries(criteria, sort, projects, new Map([[project, { types: types }]])),\n\t\t);\n\t}\n\n\tconst type = workflow?.types[0];\n\tif (type?.states != null && type.states.length > 1) {\n\t\tconst middle = Math.ceil(type.states.length / 2);\n\t\treturn [type.states.slice(0, middle), type.states.slice(middle)].flatMap(states =>\n\t\t\ttoAzureWorkItemSearchQueries(\n\t\t\t\tcriteria,\n\t\t\t\tsort,\n\t\t\t\tprojects,\n\t\t\t\tnew Map([[project, { types: [{ ...type, states: states }] }]]),\n\t\t\t),\n\t\t);\n\t}\n\n\tthrow new Error('Azure DevOps work item search query is too long; narrow the search');\n}\n\nexport function toAzureWorkItemOrderQuery(ids: readonly number[], sort: IssueSorting): string {\n\tconst column = sortColumns[sort];\n\tif (column == null) throw new Error(`Azure DevOps cannot order a work item search by '${sort}'`);\n\n\treturn `SELECT [System.Id] FROM WorkItems WHERE [System.Id] IN (${ids.join(', ')}) ORDER BY ${column}, [System.Id] Desc`;\n}\n\n/**\n * A criteria date as the UTC-midnight literal WIQL compares against. A literal with a time is only accepted by a\n * query sent with `timePrecision=true`, which is how the search sends every query: without it WIQL compares whole\n * days in the server's time zone, and the instant would differ from the one the pull request filter applies.\n */\nfunction toWiqlDate(value: string, name: string): string {\n\treturn parseCriteriaDate(value, name).toISOString().replace('.000Z', 'Z');\n}\n\n/**\n * A criteria date as UTC midnight.\n *\n * Validated to the `YYYY-MM-DD` the criteria model documents rather than parsed leniently: the WIQL value lands\n * inside a quoted literal, where any other shape is either refused or read in the server's locale, and the pull\n * request filter must apply the same instant the work-item query does.\n */\nfunction parseCriteriaDate(value: string, name: string): Date {\n\tconst date = toCriteriaDate(value);\n\tif (date == null) throw new Error(`Invalid ${name} '${value}'; expected an ISO date (YYYY-MM-DD)`);\n\n\treturn date;\n}\n\nfunction toCriteriaDate(value: string): Date | undefined {\n\tconst trimmed = value.trim();\n\tconst match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(trimmed);\n\tconst date = match != null ? new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))) : null;\n\treturn date == null || Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== trimmed\n\t\t? undefined\n\t\t: date;\n}\n\n/**\n * Why a search's dates can't be applied, or `undefined` when they can. The search checks this before any request,\n * so a date it would refuse costs nothing, and in a count refuses only its own scope.\n */\nexport function getInvalidAzureSearchDate(\n\tcriteria: { updatedAfter?: string; createdAfter?: string } | undefined,\n): string | undefined {\n\tfor (const [name, value] of [\n\t\t['updatedAfter', criteria?.updatedAfter],\n\t\t['createdAfter', criteria?.createdAfter],\n\t] as const) {\n\t\tif (value != null && toCriteriaDate(value) == null) {\n\t\t\treturn `invalid ${name} '${value}'; expected an ISO date (YYYY-MM-DD)`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The criteria a pull-request search applies AFTER the provider read, as one predicate.\n *\n * Azure's pull request query takes a creator, a reviewer, a status and a repository, and nothing else, so text,\n * draft state and dates are matched here against each fully drained facet. Applying them to a complete facet is\n * exact rather than a post-filter of a page: nothing the predicate removes was ever counted toward a page or a\n * total, so a count and its list always agree.\n *\n * What each criterion matches is bounded by what the list endpoint returns, which is narrower than the web UI:\n * - text is a case-insensitive substring of the title or of the description — which Azure truncates to its first\n * 400 characters in a pull request list, so a term only found past that is not matched;\n * - `updatedAfter` compares Azure's `updatedDate`, which the SDK derives from the close date, or the creation date\n * for a pull request still open, since Azure reports no last-activity date. An old but active pull request is\n * therefore matched by its creation date.\n */\nexport function toAzurePullRequestSearchFilter(\n\tcriteria: PullRequestSearchCriteria | undefined,\n): (pr: ProviderPullRequest) => boolean {\n\tconst text = criteria?.text?.trim().toLowerCase();\n\tconst draft = criteria?.draft;\n\tconst updatedAfter =\n\t\tcriteria?.updatedAfter != null ? parseCriteriaDate(criteria.updatedAfter, 'updatedAfter') : undefined;\n\tconst createdAfter =\n\t\tcriteria?.createdAfter != null ? parseCriteriaDate(criteria.createdAfter, 'createdAfter') : undefined;\n\n\treturn pr =>\n\t\t(!text || pr.title.toLowerCase().includes(text) || (pr.description?.toLowerCase().includes(text) ?? false)) &&\n\t\t(draft == null || pr.isDraft === draft) &&\n\t\t(updatedAfter == null || pr.updatedDate >= updatedAfter) &&\n\t\t(createdAfter == null || pr.createdDate >= createdAfter);\n}\n\n/** The pull-request sort keys the search can order its merged facets by. */\nexport const azurePullRequestSearchSorts: PullRequestSorting[] = [\n\t'updated:desc',\n\t'updated:asc',\n\t'created:desc',\n\t'created:asc',\n];\n\n/**\n * A pull request's position in the merged search result: the sort date plus the identity that breaks ties.\n *\n * The identity makes the order TOTAL, which is what lets a cursor resume strictly after the last row served rather\n * than at a numeric offset. An offset would skip or repeat rows whenever a pull request changes state or date\n * between two pages; a keyset position only moves past rows that sort before it.\n */\nexport interface AzurePullRequestSearchPosition {\n\ttime: number;\n\tidentity: string;\n}\n\nexport function toAzurePullRequestSearchPosition(\n\tpr: ProviderPullRequest,\n\tsort: PullRequestSorting,\n\tidentity: string,\n): AzurePullRequestSearchPosition {\n\treturn { time: (sort.startsWith('created') ? pr.createdDate : pr.updatedDate).getTime(), identity: identity };\n}\n\n/** Orders two positions under `sort`, ties broken by identity so no two rows ever compare equal. */\nexport function compareAzurePullRequestSearchPositions(\n\ta: AzurePullRequestSearchPosition,\n\tb: AzurePullRequestSearchPosition,\n\tsort: PullRequestSorting,\n): number {\n\tif (a.time !== b.time) return sort.endsWith(':asc') ? a.time - b.time : b.time - a.time;\n\n\treturn a.identity < b.identity ? -1 : a.identity > b.identity ? 1 : 0;\n}\n\n/**\n * Where a filtered pull-request search resumes: after `after`, in the drain its first page read, under the query\n * `key` fingerprints. Bound to its query and served only from its own drain for the same reasons\n * {@link AzureWorkItemSearchCursor} is bound to its query and snapshot.\n */\nexport interface AzurePullRequestSearchCursor {\n\tkey: string;\n\tdrain: string;\n\tpage: number;\n\tafter: AzurePullRequestSearchPosition;\n}\n\n/**\n * The fingerprint {@link AzurePullRequestSearchCursor.key} carries for a query.\n *\n * It only guards a caller threading a cursor under a different query, and keeps the query itself out of the opaque\n * cursor. It never selects cached results: those are keyed by the full query (see the callers), so a fingerprint\n * collision can at worst accept a cursor, never serve another query's rows.\n */\nexport function toAzurePullRequestSearchCursorKey(query: unknown): string {\n\treturn fnv1aHash64(JSON.stringify(query));\n}\n\n/** Reads a caller-supplied cursor for the query fingerprinted as `key`; see {@link parseAzureWorkItemSearchCursor}. */\nexport function parseAzurePullRequestSearchCursor(\n\tcursor: string | undefined,\n\tkey: string,\n): AzurePullRequestSearchCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\tlet parsed: Partial<AzurePullRequestSearchCursor> | undefined;\n\ttry {\n\t\tparsed = JSON.parse(cursor) as Partial<AzurePullRequestSearchCursor>;\n\t} catch {}\n\n\tconst after = parsed?.after;\n\tif (\n\t\tparsed?.key !== key ||\n\t\ttypeof parsed.drain !== 'string' ||\n\t\t!parsed.drain ||\n\t\t!Number.isSafeInteger(parsed.page) ||\n\t\tparsed.page! < 1 ||\n\t\ttypeof after?.time !== 'number' ||\n\t\t!Number.isFinite(after.time) ||\n\t\ttypeof after.identity !== 'string'\n\t) {\n\t\tthrow new Error(\n\t\t\t'Pull request search cursor was produced by a different query; restart the read without a cursor',\n\t\t);\n\t}\n\n\treturn { key: key, drain: parsed.drain, page: parsed.page!, after: { time: after.time, identity: after.identity } };\n}\n\n/**\n * Where a filtered work-item search resumes: an offset into the id snapshot its first page queried, the id of that\n * snapshot, and the fingerprint of the query that produced it.\n *\n * A continuation is only served from ITS snapshot, where the offset is exact. Each first page queries a snapshot of\n * its own, so a pagination of the same query started later never replaces the one an earlier pagination is reading.\n * The snapshot is kept for as long as the pagination keeps reading it; once it is gone the continuation is REFUSED\n * rather than resumed against a fresh query, since under an `updated` order a re-query can move an unseen work item\n * ahead of the offset, where it would be skipped without anything saying so. A cursor without a `snapshot` is the\n * facade's own page marker, which names a page by number with no snapshot behind it and is read against a fresh\n * query as exactly that.\n *\n * The fingerprint binds the cursor to its query (scope, criteria and order) without publishing any of them inside\n * the opaque cursor. A cursor for another query is REFUSED rather than silently restarted: this read is cursor-only,\n * so a restart would be served under the page number the caller supplied alongside it — page 1's rows labelled as\n * page N.\n */\nexport interface AzureWorkItemSearchCursor {\n\tkey: string;\n\tsnapshot?: string;\n\toffset: number;\n\tpage: number;\n}\n\n/** The fingerprint {@link AzureWorkItemSearchCursor.key} carries for a query; see {@link toAzurePullRequestSearchCursorKey}. */\nexport function toAzureWorkItemSearchCursorKey(collection: string, wiql: string): string {\n\treturn fnv1aHash64(JSON.stringify([collection, wiql]));\n}\n\n/**\n * Whether `cursor` starts a work-item search's first page: none at all, or the facade's page-1 marker, which\n * {@link parseAzureWorkItemSearchCursor} reads the same way.\n */\nexport function isAzureWorkItemSearchFirstPage(cursor: string | undefined): boolean {\n\treturn cursor == null || parsePageCursor(cursor) === 1;\n}\n\n/**\n * Reads a caller-supplied cursor for the query fingerprinted as `key`.\n *\n * Returns `undefined` when there is no cursor, and throws when there is one this query can't resume, since treating\n * a foreign or malformed cursor as page 1 would publish the first page's rows as the page the caller asked for.\n *\n * The one cursor it accepts without a fingerprint is the facade's own page marker (`{ value, type: 'page' }`), which\n * `broadenIssues` sends to retry or reach a numbered page it holds no cursor for. It names a position rather than a\n * query, so it is honored as the offset of that page at `pageSize` — page 1 being no cursor at all.\n */\nexport function parseAzureWorkItemSearchCursor(\n\tcursor: string | undefined,\n\tkey: string,\n\tpageSize: number,\n): AzureWorkItemSearchCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\tconst pageMarker = parsePageCursor(cursor);\n\tif (pageMarker != null && Number.isSafeInteger(pageMarker) && pageMarker >= 1) {\n\t\treturn pageMarker === 1 ? undefined : { key: key, offset: (pageMarker - 1) * pageSize, page: pageMarker };\n\t}\n\n\tlet parsed: Partial<AzureWorkItemSearchCursor> | undefined;\n\ttry {\n\t\tparsed = JSON.parse(cursor) as Partial<AzureWorkItemSearchCursor>;\n\t} catch {}\n\n\tif (\n\t\tparsed?.key !== key ||\n\t\ttypeof parsed.snapshot !== 'string' ||\n\t\t!parsed.snapshot ||\n\t\t!Number.isSafeInteger(parsed.offset) ||\n\t\tparsed.offset! < 0 ||\n\t\t!Number.isSafeInteger(parsed.page) ||\n\t\tparsed.page! < 1\n\t) {\n\t\tthrow new Error('Work item search cursor was produced by a different query; restart the read without a cursor');\n\t}\n\n\treturn { key: key, snapshot: parsed.snapshot, offset: parsed.offset!, page: parsed.page! };\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,MAAa,iCAAiC;;AAG9C,MAAa,0BAA0B;AAEvC,MAAa,+BAA+B;AAC5C,MAAM,qBAAqB;;;;;AAM3B,SAAgB,sBAAsB,OAA2B,UAAkB,KAAqB;CAEvG,OAAO,KAAK,IAAI,KAAK,KAAK,IAAI,GADjB,SAAS,QAAQ,OAAO,SAAS,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI,QACtC,CAAC;AACvC;;;;;;;;;AAUA,MAAM,sBAAwE;CAC7E,UAAU;CACV,UAAU;CACV,gBAAgB;CAChB,YAAY;AACb;;AAGA,MAAa,mCAAmC,OAAO,KAAK,mBAAmB;;;;;;;;;;AAW/E,MAAM,cAAqD;CAC1D,gBAAgB;CAChB,eAAe;CACf,gBAAgB;CAChB,eAAe;CACf,eAAe;CACf,cAAc;CACd,iBAAiB;CACjB,gBAAgB;CAChB,cAAc;CACd,aAAa;AACd;;AAGA,MAAa,2BAA2B,OAAO,KAAK,WAAW;;;;;;;;;;;;;;;;;AAkB/D,SAAgB,0BACf,UACA,MACA,UACA,WACS;CACT,MAAM,UAAoB,CAAC;CAC3B,IAAI,UAAU,QACb,QAAQ,KAAK,4BAA4B,SAAS,KAAI,MAAK,IAAI,iBAAiB,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;CAGrG,MAAM,QAAQ,UAAU,SAAS;CACjC,IAAI,UAAU,OAAO;EACpB,IAAI,aAAa,MAAM,MAAM,IAAI,MAAM,+DAA+D;EAEtG,MAAM,SAAmB,CAAC;EAC1B,KAAK,MAAM,CAAC,SAAS,aAAa,WAAW;GAC5C,IAAI,SAAS,YAAY,MAAM,MAAM,IAAI,MAAM,oDAAoD;GAEnG,MAAM,yBAAS,IAAI,IAAkD;GACrE,KAAK,MAAM,QAAQ,SAAS,OAAO;IAClC,IAAI,KAAK,UAAU,MAAM,MAAM,IAAI,MAAM,2CAA2C;IAEpF,MAAM,QAAQ,KAAK,OAAO,QAAO,MAAK;KACrC,QAAQ,EAAE,UAAV;MACC,KAAK;MACL,KAAK;MACL,KAAK,WACJ,OAAO,UAAU;MAClB,KAAK;MACL,KAAK,cACJ,OAAO,UAAU;MAClB,SACC,MAAM,IAAI,MAAM,2DAA2D;KAC7E;IACD,CAAC;IACD,IAAI,CAAC,MAAM,QAAQ;IAEnB,MAAM,WAAW,CAAC,GAAG,IAAI,IAAI,MAAM,KAAI,MAAK,EAAE,IAAI,CAAC,CAAC;IACpD,MAAM,MAAM,KAAK,UAAU,CAAC,GAAG,QAAQ,CAAC,CAAC,KAAK,CAAC;IAC/C,MAAM,QAAQ,OAAO,IAAI,GAAG;IAC5B,IAAI,SAAS,MACZ,MAAM,MAAM,KAAK,KAAK,IAAI;SAE1B,OAAO,IAAI,KAAK;KAAE,OAAO;KAAU,OAAO,CAAC,KAAK,IAAI;IAAE,CAAC;GAEzD;GACA,KAAK,MAAM,SAAS,OAAO,OAAO,GAAG;IACpC,MAAM,QAAQ,MAAM,MAAM,KAAI,SAAQ,IAAI,iBAAiB,IAAI,EAAE,EAAE;IACnE,MAAM,aAAa,MAAM,WAAW,IAAI,KAAK,MAAM,OAAO,OAAO,MAAM,KAAK,IAAI,EAAE;IAClF,OAAO,KACN,4BAA4B,iBAAiB,OAAO,EAAE,8BAA8B,WAAW,0BAA0B,MAAM,MAAM,KAAI,SAAQ,IAAI,iBAAiB,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,GAC5L;GACD;EACD;EACA,QAAQ,KAAK,OAAO,SAAS,IAAI,OAAO,KAAK,MAAM,EAAE,KAAK,iBAAiB;CAC5E;CAEA,MAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,UAAU,iBAAiB,CAAC,CAAC,CAAC;CAChE,IAAI,cAAc,SAAS,GAAG;EAC7B,MAAM,cAAc,cAAc,KAAI,MAAK;GAC1C,MAAM,SAAS,oBAAoB;GACnC,IAAI,UAAU,MAAM,MAAM,IAAI,MAAM,iDAAiD,EAAE,eAAe;GAEtG,OAAO;EACR,CAAC;EACD,QAAQ,KAAK,IAAI,YAAY,KAAK,MAAM,EAAE,EAAE;CAC7C;CAEA,MAAM,OAAO,UAAU,MAAM,KAAK;CAClC,IAAI,MACH,QAAQ,KAAK,4BAA4B,iBAAiB,IAAI,EAAE,EAAE;CAEnE,KAAK,MAAM,SAAS,UAAU,UAAU,CAAC,GACxC,QAAQ,KAAK,2BAA2B,iBAAiB,KAAK,EAAE,EAAE;CAEnE,IAAI,UAAU,gBAAgB,MAC7B,QAAQ,KAAK,4BAA4B,WAAW,SAAS,cAAc,cAAc,EAAE,EAAE;CAE9F,IAAI,UAAU,gBAAgB,MAC7B,QAAQ,KAAK,4BAA4B,WAAW,SAAS,cAAc,cAAc,EAAE,EAAE;CAG9F,MAAM,SAAS,YAAY;CAC3B,IAAI,UAAU,MAAM,MAAM,IAAI,MAAM,oDAAoD,KAAK,EAAE;CAK/F,OAAO,oCADO,QAAQ,SAAS,IAAI,UAAU,QAAQ,KAAK,OAAO,MAAM,GACtB,YAAY,OAAO;AACrE;;AAGA,SAAgB,6BACf,UACA,MACA,UACA,WACW;CACX,MAAM,QAAQ,0BAA0B,UAAU,MAAM,UAAU,SAAS;CAC3E,IAAI,MAAM,SAAS,sBAAA,MAAoD,OAAO,CAAC,KAAK;CAEpF,IAAI,SAAS,SAAS,GAAG;EACxB,MAAM,SAAS,KAAK,KAAK,SAAS,SAAS,CAAC;EAC5C,OAAO,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,MAAM,CAAC,CAAC,CAAC,SAAQ,UAClE,6BACC,UACA,MACA,OACA,aAAa,OAAO,KAAA,IAAY,IAAI,IAAI,MAAM,KAAI,SAAQ,CAAC,MAAM,UAAU,IAAI,IAAI,CAAE,CAAC,CAAC,CACxF,CACD;CACD;CAEA,MAAM,UAAU,SAAS;CACzB,MAAM,WAAW,WAAW,IAAI,OAAO;CACvC,IAAI,YAAY,QAAQ,SAAS,MAAM,SAAS,GAAG;EAClD,MAAM,SAAS,KAAK,KAAK,SAAS,MAAM,SAAS,CAAC;EAClD,OAAO,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,GAAG,SAAS,MAAM,MAAM,MAAM,CAAC,CAAC,CAAC,SAAQ,UAC9E,6BAA6B,UAAU,MAAM,0BAAU,IAAI,IAAI,CAAC,CAAC,SAAS,EAAS,MAAM,CAAC,CAAC,CAAC,CAAC,CAC9F;CACD;CAEA,MAAM,OAAO,UAAU,MAAM;CAC7B,IAAI,MAAM,UAAU,QAAQ,KAAK,OAAO,SAAS,GAAG;EACnD,MAAM,SAAS,KAAK,KAAK,KAAK,OAAO,SAAS,CAAC;EAC/C,OAAO,CAAC,KAAK,OAAO,MAAM,GAAG,MAAM,GAAG,KAAK,OAAO,MAAM,MAAM,CAAC,CAAC,CAAC,SAAQ,WACxE,6BACC,UACA,MACA,0BACA,IAAI,IAAI,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC;GAAE,GAAG;GAAc;EAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAC9D,CACD;CACD;CAEA,MAAM,IAAI,MAAM,oEAAoE;AACrF;AAEA,SAAgB,0BAA0B,KAAwB,MAA4B;CAC7F,MAAM,SAAS,YAAY;CAC3B,IAAI,UAAU,MAAM,MAAM,IAAI,MAAM,oDAAoD,KAAK,EAAE;CAE/F,OAAO,2DAA2D,IAAI,KAAK,IAAI,EAAE,aAAa,OAAO;AACtG;;;;;;AAOA,SAAS,WAAW,OAAe,MAAsB;CACxD,OAAO,kBAAkB,OAAO,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,QAAQ,SAAS,GAAG;AACzE;;;;;;;;AASA,SAAS,kBAAkB,OAAe,MAAoB;CAC7D,MAAM,OAAO,eAAe,KAAK;CACjC,IAAI,QAAQ,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK,IAAI,MAAM,qCAAqC;CAEjG,OAAO;AACR;AAEA,SAAS,eAAe,OAAiC;CACxD,MAAM,UAAU,MAAM,KAAK;CAC3B,MAAM,QAAQ,4BAA4B,KAAK,OAAO;CACtD,MAAM,OAAO,SAAS,OAAO,IAAI,KAAK,KAAK,IAAI,OAAO,MAAM,EAAE,GAAG,OAAO,MAAM,EAAE,IAAI,GAAG,OAAO,MAAM,EAAE,CAAC,CAAC,IAAI;CAC5G,OAAO,QAAQ,QAAQ,OAAO,MAAM,KAAK,QAAQ,CAAC,KAAK,KAAK,YAAY,CAAC,CAAC,MAAM,GAAG,EAAE,MAAM,UACxF,KAAA,IACA;AACJ;;;;;AAMA,SAAgB,0BACf,UACqB;CACrB,KAAK,MAAM,CAAC,MAAM,UAAU,CAC3B,CAAC,gBAAgB,UAAU,YAAY,GACvC,CAAC,gBAAgB,UAAU,YAAY,CACxC,GACC,IAAI,SAAS,QAAQ,eAAe,KAAK,KAAK,MAC7C,OAAO,WAAW,KAAK,IAAI,MAAM;AAIpC;;;;;;;;;;;;;;;;AAiBA,SAAgB,+BACf,UACuC;CACvC,MAAM,OAAO,UAAU,MAAM,KAAK,CAAC,CAAC,YAAY;CAChD,MAAM,QAAQ,UAAU;CACxB,MAAM,eACL,UAAU,gBAAgB,OAAO,kBAAkB,SAAS,cAAc,cAAc,IAAI,KAAA;CAC7F,MAAM,eACL,UAAU,gBAAgB,OAAO,kBAAkB,SAAS,cAAc,cAAc,IAAI,KAAA;CAE7F,QAAO,QACL,CAAC,QAAQ,GAAG,MAAM,YAAY,CAAC,CAAC,SAAS,IAAI,MAAM,GAAG,aAAa,YAAY,CAAC,CAAC,SAAS,IAAI,KAAK,YACnG,SAAS,QAAQ,GAAG,YAAY,WAChC,gBAAgB,QAAQ,GAAG,eAAe,kBAC1C,gBAAgB,QAAQ,GAAG,eAAe;AAC7C;;AAGA,MAAa,8BAAoD;CAChE;CACA;CACA;CACA;AACD;AAcA,SAAgB,iCACf,IACA,MACA,UACiC;CACjC,OAAO;EAAE,OAAO,KAAK,WAAW,SAAS,IAAI,GAAG,cAAc,GAAG,YAAA,CAAa,QAAQ;EAAa;CAAS;AAC7G;;AAGA,SAAgB,uCACf,GACA,GACA,MACS;CACT,IAAI,EAAE,SAAS,EAAE,MAAM,OAAO,KAAK,SAAS,MAAM,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE;CAEnF,OAAO,EAAE,WAAW,EAAE,WAAW,KAAK,EAAE,WAAW,EAAE,WAAW,IAAI;AACrE;;;;;;;;AAqBA,SAAgB,kCAAkC,OAAwB;CACzE,OAAO,YAAY,KAAK,UAAU,KAAK,CAAC;AACzC;;AAGA,SAAgB,kCACf,QACA,KAC2C;CAC3C,IAAI,UAAU,MAAM,OAAO,KAAA;CAE3B,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,MAAM;CAC3B,QAAQ,CAAC;CAET,MAAM,QAAQ,QAAQ;CACtB,IACC,QAAQ,QAAQ,OAChB,OAAO,OAAO,UAAU,YACxB,CAAC,OAAO,SACR,CAAC,OAAO,cAAc,OAAO,IAAI,KACjC,OAAO,OAAQ,KACf,OAAO,OAAO,SAAS,YACvB,CAAC,OAAO,SAAS,MAAM,IAAI,KAC3B,OAAO,MAAM,aAAa,UAE1B,MAAM,IAAI,MACT,iGACD;CAGD,OAAO;EAAO;EAAK,OAAO,OAAO;EAAO,MAAM,OAAO;EAAO,OAAO;GAAE,MAAM,MAAM;GAAM,UAAU,MAAM;EAAS;CAAE;AACnH;;AA2BA,SAAgB,+BAA+B,YAAoB,MAAsB;CACxF,OAAO,YAAY,KAAK,UAAU,CAAC,YAAY,IAAI,CAAC,CAAC;AACtD;;;;;AAMA,SAAgB,+BAA+B,QAAqC;CACnF,OAAO,UAAU,QAAQ,gBAAgB,MAAM,MAAM;AACtD;;;;;;;;;;;AAYA,SAAgB,+BACf,QACA,KACA,UACwC;CACxC,IAAI,UAAU,MAAM,OAAO,KAAA;CAE3B,MAAM,aAAa,gBAAgB,MAAM;CACzC,IAAI,cAAc,QAAQ,OAAO,cAAc,UAAU,KAAK,cAAc,GAC3E,OAAO,eAAe,IAAI,KAAA,IAAY;EAAO;EAAK,SAAS,aAAa,KAAK;EAAU,MAAM;CAAW;CAGzG,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,MAAM;CAC3B,QAAQ,CAAC;CAET,IACC,QAAQ,QAAQ,OAChB,OAAO,OAAO,aAAa,YAC3B,CAAC,OAAO,YACR,CAAC,OAAO,cAAc,OAAO,MAAM,KACnC,OAAO,SAAU,KACjB,CAAC,OAAO,cAAc,OAAO,IAAI,KACjC,OAAO,OAAQ,GAEf,MAAM,IAAI,MAAM,8FAA8F;CAG/G,OAAO;EAAO;EAAK,UAAU,OAAO;EAAU,QAAQ,OAAO;EAAS,MAAM,OAAO;CAAM;AAC1F"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { IssueSorting } from "../../../../git/models/issue.js";
|
|
2
|
+
//#region ../plus/integrations/src/providers/azure/workItemSearch.d.ts
|
|
3
|
+
type QueryResult = {
|
|
4
|
+
ids: number[];
|
|
5
|
+
asOf?: string;
|
|
6
|
+
};
|
|
7
|
+
type Query = (wiql: string, top: number, asOf?: string) => Promise<QueryResult>;
|
|
8
|
+
/** All partitions and ranking windows use the first query's server instant and ordering. */
|
|
9
|
+
export declare function queryAzureWorkItemSearch(queries: readonly string[], sort: IssueSorting, ordered: boolean, query: Query, cancellation?: AbortSignal): Promise<number[]>;
|
|
10
|
+
//#endregion
|
|
11
|
+
//# sourceMappingURL=workItemSearch.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { CancellationError } from "../../../../utils/cancellation.js";
|
|
2
|
+
import { azureWorkItemSearchResultLimit, toAzureWorkItemOrderQuery } from "./search.js";
|
|
3
|
+
//#region ../plus/integrations/src/providers/azure/workItemSearch.ts
|
|
4
|
+
/** All partitions and ranking windows use the first query's server instant and ordering. */
|
|
5
|
+
async function queryAzureWorkItemSearch(queries, sort, ordered, query, cancellation) {
|
|
6
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
7
|
+
const limit = azureWorkItemSearchResultLimit + 1;
|
|
8
|
+
const first = await query(queries[0], limit);
|
|
9
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
10
|
+
if (queries.length === 1) return [...new Set(first.ids)].slice(0, limit);
|
|
11
|
+
const asOf = first.asOf;
|
|
12
|
+
if (typeof asOf !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:Z|[+-]\d{2}:\d{2})$/.test(asOf) || Number.isNaN(new Date(asOf).getTime())) throw new Error("Azure DevOps returned no snapshot date for the partitioned work item search");
|
|
13
|
+
let ids = [...new Set(first.ids)].slice(0, limit);
|
|
14
|
+
for (const wiql of queries.slice(1)) {
|
|
15
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
16
|
+
const next = [...new Set((await query(wiql, limit, asOf)).ids)].slice(0, limit);
|
|
17
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
18
|
+
ids = ordered ? await mergeWorkItemIds(ids, next, sort, asOf, query, cancellation) : [.../* @__PURE__ */ new Set([...ids, ...next])].slice(0, limit);
|
|
19
|
+
if (!ordered && ids.length === limit) return ids;
|
|
20
|
+
}
|
|
21
|
+
return ids;
|
|
22
|
+
}
|
|
23
|
+
async function mergeWorkItemIds(left, right, sort, asOf, query, cancellation) {
|
|
24
|
+
const limit = azureWorkItemSearchResultLimit + 1;
|
|
25
|
+
const merged = [];
|
|
26
|
+
let l = 0;
|
|
27
|
+
let r = 0;
|
|
28
|
+
while (l < left.length && r < right.length && merged.length < limit) {
|
|
29
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
30
|
+
const lEnd = Math.min(l + 200, left.length);
|
|
31
|
+
const rEnd = Math.min(r + 200, right.length);
|
|
32
|
+
const a = new Set(left.slice(l, lEnd));
|
|
33
|
+
const b = new Set(right.slice(r, rEnd));
|
|
34
|
+
const candidates = [.../* @__PURE__ */ new Set([...a, ...b])];
|
|
35
|
+
const ranked = (await query(toAzureWorkItemOrderQuery(candidates, sort), candidates.length, asOf)).ids;
|
|
36
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
37
|
+
if (ranked.length !== candidates.length || new Set(ranked).size !== candidates.length || ranked.some((id) => !a.has(id) && !b.has(id))) throw new Error("Azure DevOps returned an incomplete work item search ranking");
|
|
38
|
+
for (const id of ranked) {
|
|
39
|
+
merged.push(id);
|
|
40
|
+
if (a.has(id)) l++;
|
|
41
|
+
if (b.has(id)) r++;
|
|
42
|
+
if (l === lEnd || r === rEnd || merged.length === limit) break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [
|
|
46
|
+
...merged,
|
|
47
|
+
...left.slice(l),
|
|
48
|
+
...right.slice(r)
|
|
49
|
+
].slice(0, limit);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { queryAzureWorkItemSearch };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=workItemSearch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workItemSearch.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/azure/workItemSearch.ts"],"sourcesContent":["import type { IssueSorting } from '@gitlens/git/models/issue.js';\nimport { CancellationError } from '@gitlens/utils/cancellation.js';\nimport { azureWorkItemSearchResultLimit, toAzureWorkItemOrderQuery } from './search.js';\n\ntype QueryResult = { ids: number[]; asOf?: string };\ntype Query = (wiql: string, top: number, asOf?: string) => Promise<QueryResult>;\n\n/** All partitions and ranking windows use the first query's server instant and ordering. */\nexport async function queryAzureWorkItemSearch(\n\tqueries: readonly string[],\n\tsort: IssueSorting,\n\tordered: boolean,\n\tquery: Query,\n\tcancellation?: AbortSignal,\n): Promise<number[]> {\n\tif (cancellation?.aborted) throw new CancellationError();\n\n\tconst limit = azureWorkItemSearchResultLimit + 1;\n\tconst first = await query(queries[0], limit);\n\tif (cancellation?.aborted) throw new CancellationError();\n\n\tif (queries.length === 1) return [...new Set(first.ids)].slice(0, limit);\n\n\tconst asOf = first.asOf;\n\tif (\n\t\ttypeof asOf !== 'string' ||\n\t\t!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,7})?(?:Z|[+-]\\d{2}:\\d{2})$/.test(asOf) ||\n\t\tNumber.isNaN(new Date(asOf).getTime())\n\t) {\n\t\tthrow new Error('Azure DevOps returned no snapshot date for the partitioned work item search');\n\t}\n\n\tlet ids = [...new Set(first.ids)].slice(0, limit);\n\tfor (const wiql of queries.slice(1)) {\n\t\tif (cancellation?.aborted) throw new CancellationError();\n\n\t\tconst next = [...new Set((await query(wiql, limit, asOf)).ids)].slice(0, limit);\n\t\tif (cancellation?.aborted) throw new CancellationError();\n\n\t\tids = ordered\n\t\t\t? await mergeWorkItemIds(ids, next, sort, asOf, query, cancellation)\n\t\t\t: [...new Set([...ids, ...next])].slice(0, limit);\n\t\tif (!ordered && ids.length === limit) return ids;\n\t}\n\treturn ids;\n}\n\nasync function mergeWorkItemIds(\n\tleft: readonly number[],\n\tright: readonly number[],\n\tsort: IssueSorting,\n\tasOf: string,\n\tquery: Query,\n\tcancellation?: AbortSignal,\n): Promise<number[]> {\n\tconst limit = azureWorkItemSearchResultLimit + 1;\n\tconst merged: number[] = [];\n\tlet l = 0;\n\tlet r = 0;\n\twhile (l < left.length && r < right.length && merged.length < limit) {\n\t\tif (cancellation?.aborted) throw new CancellationError();\n\n\t\t// Small windows bound the escaped query even with the largest Azure work item ids.\n\t\tconst lEnd = Math.min(l + 200, left.length);\n\t\tconst rEnd = Math.min(r + 200, right.length);\n\t\tconst a = new Set(left.slice(l, lEnd));\n\t\tconst b = new Set(right.slice(r, rEnd));\n\t\tconst candidates = [...new Set([...a, ...b])];\n\t\tconst ranked = (await query(toAzureWorkItemOrderQuery(candidates, sort), candidates.length, asOf)).ids;\n\t\tif (cancellation?.aborted) throw new CancellationError();\n\n\t\tif (\n\t\t\tranked.length !== candidates.length ||\n\t\t\tnew Set(ranked).size !== candidates.length ||\n\t\t\tranked.some(id => !a.has(id) && !b.has(id))\n\t\t) {\n\t\t\tthrow new Error('Azure DevOps returned an incomplete work item search ranking');\n\t\t}\n\n\t\tfor (const id of ranked) {\n\t\t\tmerged.push(id);\n\t\t\tif (a.has(id)) {\n\t\t\t\tl++;\n\t\t\t}\n\t\t\tif (b.has(id)) {\n\t\t\t\tr++;\n\t\t\t}\n\t\t\t// An unseen row of an exhausted window can precede the other window's remaining rows.\n\t\t\tif (l === lEnd || r === rEnd || merged.length === limit) break;\n\t\t}\n\t}\n\treturn [...merged, ...left.slice(l), ...right.slice(r)].slice(0, limit);\n}\n"],"mappings":";;;;AAQA,eAAsB,yBACrB,SACA,MACA,SACA,OACA,cACoB;CACpB,IAAI,cAAc,SAAS,MAAM,IAAI,kBAAkB;CAEvD,MAAM,QAAQ,iCAAiC;CAC/C,MAAM,QAAQ,MAAM,MAAM,QAAQ,IAAI,KAAK;CAC3C,IAAI,cAAc,SAAS,MAAM,IAAI,kBAAkB;CAEvD,IAAI,QAAQ,WAAW,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK;CAEvE,MAAM,OAAO,MAAM;CACnB,IACC,OAAO,SAAS,YAChB,CAAC,2EAA2E,KAAK,IAAI,KACrF,OAAO,MAAM,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ,CAAC,GAErC,MAAM,IAAI,MAAM,6EAA6E;CAG9F,IAAI,MAAM,CAAC,GAAG,IAAI,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK;CAChD,KAAK,MAAM,QAAQ,QAAQ,MAAM,CAAC,GAAG;EACpC,IAAI,cAAc,SAAS,MAAM,IAAI,kBAAkB;EAEvD,MAAM,OAAO,CAAC,GAAG,IAAI,KAAK,MAAM,MAAM,MAAM,OAAO,IAAI,EAAA,CAAG,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK;EAC9E,IAAI,cAAc,SAAS,MAAM,IAAI,kBAAkB;EAEvD,MAAM,UACH,MAAM,iBAAiB,KAAK,MAAM,MAAM,MAAM,OAAO,YAAY,IACjE,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK;EACjD,IAAI,CAAC,WAAW,IAAI,WAAW,OAAO,OAAO;CAC9C;CACA,OAAO;AACR;AAEA,eAAe,iBACd,MACA,OACA,MACA,MACA,OACA,cACoB;CACpB,MAAM,QAAQ,iCAAiC;CAC/C,MAAM,SAAmB,CAAC;CAC1B,IAAI,IAAI;CACR,IAAI,IAAI;CACR,OAAO,IAAI,KAAK,UAAU,IAAI,MAAM,UAAU,OAAO,SAAS,OAAO;EACpE,IAAI,cAAc,SAAS,MAAM,IAAI,kBAAkB;EAGvD,MAAM,OAAO,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM;EAC1C,MAAM,OAAO,KAAK,IAAI,IAAI,KAAK,MAAM,MAAM;EAC3C,MAAM,IAAI,IAAI,IAAI,KAAK,MAAM,GAAG,IAAI,CAAC;EACrC,MAAM,IAAI,IAAI,IAAI,MAAM,MAAM,GAAG,IAAI,CAAC;EACtC,MAAM,aAAa,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC;EAC5C,MAAM,UAAU,MAAM,MAAM,0BAA0B,YAAY,IAAI,GAAG,WAAW,QAAQ,IAAI,EAAA,CAAG;EACnG,IAAI,cAAc,SAAS,MAAM,IAAI,kBAAkB;EAEvD,IACC,OAAO,WAAW,WAAW,UAC7B,IAAI,IAAI,MAAM,CAAC,CAAC,SAAS,WAAW,UACpC,OAAO,MAAK,OAAM,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,GAE1C,MAAM,IAAI,MAAM,8DAA8D;EAG/E,KAAK,MAAM,MAAM,QAAQ;GACxB,OAAO,KAAK,EAAE;GACd,IAAI,EAAE,IAAI,EAAE,GACX;GAED,IAAI,EAAE,IAAI,EAAE,GACX;GAGD,IAAI,MAAM,QAAQ,MAAM,QAAQ,OAAO,WAAW,OAAO;EAC1D;CACD;CACA,OAAO;EAAC,GAAG;EAAQ,GAAG,KAAK,MAAM,CAAC;EAAG,GAAG,MAAM,MAAM,CAAC;CAAC,CAAC,CAAC,MAAM,GAAG,KAAK;AACvE"}
|
|
@@ -20,8 +20,8 @@ export declare class AzureWorkItemStatesCache {
|
|
|
20
20
|
private readonly types;
|
|
21
21
|
private readonly states;
|
|
22
22
|
clear(): void;
|
|
23
|
-
getTypes(scope: AzureWorkItemStatesScope, read: () => Promise<unknown
|
|
24
|
-
getStates(scope: AzureWorkItemStatesScope, type: string, read: () => Promise<unknown
|
|
23
|
+
getTypes(scope: AzureWorkItemStatesScope, read: (signal?: AbortSignal) => Promise<unknown>, cancellation?: AbortSignal): Promise<string[]>;
|
|
24
|
+
getStates(scope: AzureWorkItemStatesScope, type: string, read: (signal?: AbortSignal) => Promise<unknown>, cancellation?: AbortSignal): Promise<WorkItemTypeStates>;
|
|
25
25
|
private read;
|
|
26
26
|
}
|
|
27
27
|
//#endregion
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isCancellationError } from "../../../../utils/cancellation.js";
|
|
1
2
|
import { PromiseCache } from "../../../../utils/promiseCache.js";
|
|
2
3
|
import { azureIssueStateCategory, encodeAzurePathSegment } from "./models.js";
|
|
3
4
|
//#region ../plus/integrations/src/providers/azure/workItemStateCategories.ts
|
|
@@ -61,24 +62,27 @@ var AzureWorkItemStatesCache = class {
|
|
|
61
62
|
this.types.clear();
|
|
62
63
|
this.states.clear();
|
|
63
64
|
}
|
|
64
|
-
getTypes(scope, read) {
|
|
65
|
-
return this.read(this.types, scopeKey(scope), async () => parseWorkItemTypes(await read()));
|
|
65
|
+
getTypes(scope, read, cancellation) {
|
|
66
|
+
return this.read(this.types, scopeKey(scope), async (signal) => parseWorkItemTypes(await read(signal)), cancellation);
|
|
66
67
|
}
|
|
67
|
-
getStates(scope, type, read) {
|
|
68
|
-
return this.read(this.states, scopeKey(scope, type), async () => parseWorkItemStates(await read()));
|
|
68
|
+
getStates(scope, type, read, cancellation) {
|
|
69
|
+
return this.read(this.states, scopeKey(scope, type), async (signal) => parseWorkItemStates(await read(signal)), cancellation);
|
|
69
70
|
}
|
|
70
|
-
async read(cache, key, load) {
|
|
71
|
-
const result = await cache.getOrCreate(key, async () => ({
|
|
71
|
+
async read(cache, key, load, cancellation) {
|
|
72
|
+
const result = await cache.getOrCreate(key, async (_cacheable, signal) => ({
|
|
72
73
|
status: "fulfilled",
|
|
73
|
-
value: await load()
|
|
74
|
-
}), {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
value: await load(signal)
|
|
75
|
+
}), {
|
|
76
|
+
cancellation,
|
|
77
|
+
onError: (error) => isCancellationError(error) || cache.get(key) == null ? void 0 : {
|
|
78
|
+
value: {
|
|
79
|
+
status: "rejected",
|
|
80
|
+
reason: error
|
|
81
|
+
},
|
|
82
|
+
createTTL: 3e4,
|
|
83
|
+
suppress: true
|
|
84
|
+
}
|
|
85
|
+
});
|
|
82
86
|
if (result.status === "rejected") throw result.reason;
|
|
83
87
|
return result.value;
|
|
84
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workItemStateCategories.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/azure/workItemStateCategories.ts"],"sourcesContent":["import type { AzureWorkItemTypeStates } from '@gitkraken/provider-apis';\nimport { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type { AzureWorkItemStateCategory } from './models.js';\nimport { azureIssueStateCategory, encodeAzurePathSegment } from './models.js';\n\ntype WorkItemTypeStates = NonNullable<AzureWorkItemTypeStates['types'][number]['states']>;\n\nexport type AzureWorkItemStatesScope = {\n\tbaseUrl: string;\n\tnamespace: string;\n\tproject: string;\n\ttoken: string;\n\tisPAT: boolean;\n};\n\nexport function getAzureWorkItemTypesUrl(baseUrl: string, namespace: string, project: string): string {\n\treturn `${baseUrl.replace(/\\/$/, '')}/${encodeAzurePathSegment(namespace)}/${encodeAzurePathSegment(project)}/_apis/wit/workItemTypes?api-version=6.0`;\n}\n\n// Server 2020 serves this endpoint only as a preview; Services accepts the same version.\nexport function getAzureWorkItemTypeStatesPath(namespace: string, project: string, type: string): string {\n\treturn `${encodeAzurePathSegment(namespace)}/${encodeAzurePathSegment(project)}/_apis/wit/workItemTypes/${encodeAzurePathSegment(type)}/states?api-version=6.0-preview.1`;\n}\n\nexport function getAzureWorkItemTypeStatesUrl(\n\tbaseUrl: string,\n\tnamespace: string,\n\tproject: string,\n\ttype: string,\n): string {\n\treturn `${baseUrl.replace(/\\/$/, '')}/${getAzureWorkItemTypeStatesPath(namespace, project, type)}`;\n}\n\nfunction parseWorkItemTypes(body: unknown): string[] {\n\tconst types = (body as { value?: unknown } | null)?.value;\n\tif (!Array.isArray(types)) throw new Error('Azure DevOps returned no work item types');\n\n\tconst names: string[] = [];\n\tfor (const type of types as { name?: unknown }[]) {\n\t\tif (typeof type?.name !== 'string' || !type.name.trim()) {\n\t\t\tthrow new Error('Azure DevOps returned an invalid work item type');\n\t\t}\n\n\t\tnames.push(type.name);\n\t}\n\treturn [...new Set(names)];\n}\n\nfunction parseWorkItemStates(body: unknown): WorkItemTypeStates {\n\tconst states = (body as { value?: unknown } | null)?.value;\n\tif (!Array.isArray(states) || states.length === 0) throw new Error('Azure DevOps returned no work item states');\n\n\tconst parsed: { name: string; category: string }[] = [];\n\tfor (const state of states as { name?: unknown; category?: unknown }[]) {\n\t\tif (\n\t\t\ttypeof state?.name !== 'string' ||\n\t\t\t!state.name.trim() ||\n\t\t\ttypeof state.category !== 'string' ||\n\t\t\tazureIssueStateCategory(state.category as AzureWorkItemStateCategory) == null ||\n\t\t\tparsed.some(s => s.name === state.name && s.category !== state.category)\n\t\t) {\n\t\t\tthrow new Error('Azure DevOps returned an invalid work item state');\n\t\t}\n\n\t\tparsed.push({ name: state.name, category: state.category });\n\t}\n\treturn parsed;\n}\n\nfunction scopeKey(scope: AzureWorkItemStatesScope, type?: string): string {\n\treturn JSON.stringify([\n\t\tscope.baseUrl.replace(/\\/$/, ''),\n\t\tscope.namespace.toLowerCase(),\n\t\tscope.project.toLowerCase(),\n\t\ttype?.toLowerCase(),\n\t\tscope.token,\n\t\tscope.isPAT,\n\t]);\n}\n\n/**\n * Workflow metadata per project, work item type and token, kept for 5 minutes. A failed read is kept for 30 seconds,\n * so the pages and per-type reads of a listing fail fast with the same error instead of each asking a failing server\n * again; the first read after that retries.\n */\nexport class AzureWorkItemStatesCache {\n\tprivate readonly types = new PromiseCache<string, PromiseSettledResult<string[]>>({\n\t\tcreateTTL: 5 * 60 * 1000,\n\t\tcapacity: 256,\n\t});\n\tprivate readonly states = new PromiseCache<string, PromiseSettledResult<WorkItemTypeStates>>({\n\t\tcreateTTL: 5 * 60 * 1000,\n\t\tcapacity: 256,\n\t});\n\n\tclear(): void {\n\t\tthis.types.clear();\n\t\tthis.states.clear();\n\t}\n\n\tgetTypes(
|
|
1
|
+
{"version":3,"file":"workItemStateCategories.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/azure/workItemStateCategories.ts"],"sourcesContent":["import type { AzureWorkItemTypeStates } from '@gitkraken/provider-apis';\nimport { isCancellationError } from '@gitlens/utils/cancellation.js';\nimport { PromiseCache } from '@gitlens/utils/promiseCache.js';\nimport type { AzureWorkItemStateCategory } from './models.js';\nimport { azureIssueStateCategory, encodeAzurePathSegment } from './models.js';\n\ntype WorkItemTypeStates = NonNullable<AzureWorkItemTypeStates['types'][number]['states']>;\n\nexport type AzureWorkItemStatesScope = {\n\tbaseUrl: string;\n\tnamespace: string;\n\tproject: string;\n\ttoken: string;\n\tisPAT: boolean;\n};\n\nexport function getAzureWorkItemTypesUrl(baseUrl: string, namespace: string, project: string): string {\n\treturn `${baseUrl.replace(/\\/$/, '')}/${encodeAzurePathSegment(namespace)}/${encodeAzurePathSegment(project)}/_apis/wit/workItemTypes?api-version=6.0`;\n}\n\n// Server 2020 serves this endpoint only as a preview; Services accepts the same version.\nexport function getAzureWorkItemTypeStatesPath(namespace: string, project: string, type: string): string {\n\treturn `${encodeAzurePathSegment(namespace)}/${encodeAzurePathSegment(project)}/_apis/wit/workItemTypes/${encodeAzurePathSegment(type)}/states?api-version=6.0-preview.1`;\n}\n\nexport function getAzureWorkItemTypeStatesUrl(\n\tbaseUrl: string,\n\tnamespace: string,\n\tproject: string,\n\ttype: string,\n): string {\n\treturn `${baseUrl.replace(/\\/$/, '')}/${getAzureWorkItemTypeStatesPath(namespace, project, type)}`;\n}\n\nfunction parseWorkItemTypes(body: unknown): string[] {\n\tconst types = (body as { value?: unknown } | null)?.value;\n\tif (!Array.isArray(types)) throw new Error('Azure DevOps returned no work item types');\n\n\tconst names: string[] = [];\n\tfor (const type of types as { name?: unknown }[]) {\n\t\tif (typeof type?.name !== 'string' || !type.name.trim()) {\n\t\t\tthrow new Error('Azure DevOps returned an invalid work item type');\n\t\t}\n\n\t\tnames.push(type.name);\n\t}\n\treturn [...new Set(names)];\n}\n\nfunction parseWorkItemStates(body: unknown): WorkItemTypeStates {\n\tconst states = (body as { value?: unknown } | null)?.value;\n\tif (!Array.isArray(states) || states.length === 0) throw new Error('Azure DevOps returned no work item states');\n\n\tconst parsed: { name: string; category: string }[] = [];\n\tfor (const state of states as { name?: unknown; category?: unknown }[]) {\n\t\tif (\n\t\t\ttypeof state?.name !== 'string' ||\n\t\t\t!state.name.trim() ||\n\t\t\ttypeof state.category !== 'string' ||\n\t\t\tazureIssueStateCategory(state.category as AzureWorkItemStateCategory) == null ||\n\t\t\tparsed.some(s => s.name === state.name && s.category !== state.category)\n\t\t) {\n\t\t\tthrow new Error('Azure DevOps returned an invalid work item state');\n\t\t}\n\n\t\tparsed.push({ name: state.name, category: state.category });\n\t}\n\treturn parsed;\n}\n\nfunction scopeKey(scope: AzureWorkItemStatesScope, type?: string): string {\n\treturn JSON.stringify([\n\t\tscope.baseUrl.replace(/\\/$/, ''),\n\t\tscope.namespace.toLowerCase(),\n\t\tscope.project.toLowerCase(),\n\t\ttype?.toLowerCase(),\n\t\tscope.token,\n\t\tscope.isPAT,\n\t]);\n}\n\n/**\n * Workflow metadata per project, work item type and token, kept for 5 minutes. A failed read is kept for 30 seconds,\n * so the pages and per-type reads of a listing fail fast with the same error instead of each asking a failing server\n * again; the first read after that retries.\n */\nexport class AzureWorkItemStatesCache {\n\tprivate readonly types = new PromiseCache<string, PromiseSettledResult<string[]>>({\n\t\tcreateTTL: 5 * 60 * 1000,\n\t\tcapacity: 256,\n\t});\n\tprivate readonly states = new PromiseCache<string, PromiseSettledResult<WorkItemTypeStates>>({\n\t\tcreateTTL: 5 * 60 * 1000,\n\t\tcapacity: 256,\n\t});\n\n\tclear(): void {\n\t\tthis.types.clear();\n\t\tthis.states.clear();\n\t}\n\n\tgetTypes(\n\t\tscope: AzureWorkItemStatesScope,\n\t\tread: (signal?: AbortSignal) => Promise<unknown>,\n\t\tcancellation?: AbortSignal,\n\t): Promise<string[]> {\n\t\treturn this.read(\n\t\t\tthis.types,\n\t\t\tscopeKey(scope),\n\t\t\tasync signal => parseWorkItemTypes(await read(signal)),\n\t\t\tcancellation,\n\t\t);\n\t}\n\n\tgetStates(\n\t\tscope: AzureWorkItemStatesScope,\n\t\ttype: string,\n\t\tread: (signal?: AbortSignal) => Promise<unknown>,\n\t\tcancellation?: AbortSignal,\n\t): Promise<WorkItemTypeStates> {\n\t\treturn this.read(\n\t\t\tthis.states,\n\t\t\tscopeKey(scope, type),\n\t\t\tasync signal => parseWorkItemStates(await read(signal)),\n\t\t\tcancellation,\n\t\t);\n\t}\n\n\tprivate async read<T>(\n\t\tcache: PromiseCache<string, PromiseSettledResult<T>>,\n\t\tkey: string,\n\t\tload: (signal?: AbortSignal) => Promise<T>,\n\t\tcancellation?: AbortSignal,\n\t): Promise<T> {\n\t\tconst result = await cache.getOrCreate(\n\t\t\tkey,\n\t\t\tasync (_cacheable, signal) => ({ status: 'fulfilled', value: await load(signal) }),\n\t\t\t{\n\t\t\t\tcancellation: cancellation,\n\t\t\t\t// Every caller must still see the failure; caching undefined would turn later pages into successes.\n\t\t\t\tonError: error =>\n\t\t\t\t\tisCancellationError(error) || cache.get(key) == null\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: { value: { status: 'rejected', reason: error }, createTTL: 30 * 1000, suppress: true },\n\t\t\t},\n\t\t);\n\t\tif (result.status === 'rejected') throw result.reason;\n\n\t\treturn result.value;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -19,11 +19,6 @@ import { AzureDevOpsApi } from "./azure/azure.js";
|
|
|
19
19
|
import { IntegrationAuthenticationProviderDescriptor } from "../authentication/integrationAuthenticationProvider.js";
|
|
20
20
|
import { CollectionScopeFailure, GitPullRequestState } from "@gitkraken/provider-apis";
|
|
21
21
|
//#region ../plus/integrations/src/providers/azureDevOps.d.ts
|
|
22
|
-
export declare function getAzureRepositoryIdentity(repo: Pick<AzureRepositoryDescriptor, 'owner' | 'name' | 'project'>): {
|
|
23
|
-
resourceName: string;
|
|
24
|
-
projectName?: string;
|
|
25
|
-
repositoryName: string;
|
|
26
|
-
};
|
|
27
22
|
export declare function getAzureRepositoryApiBaseUrl(baseUrl: string, repo: Pick<AzureRepositoryDescriptor, 'owner' | 'virtualDirectory'>): string;
|
|
28
23
|
/**
|
|
29
24
|
* {@link getAzureRepositoryApiBaseUrl}, `undefined` when the connection doesn't serve `repo`'s virtual directory or it
|
|
@@ -35,13 +30,13 @@ export declare function findAzureRepositoryApiBaseUrl(baseUrl: string, repo: Pic
|
|
|
35
30
|
* connection `baseUrl` addresses: that reading moves the parsed owner into the virtual directory, so its directory and
|
|
36
31
|
* owner must be one of {@link getAzureProjectlessDirectories}.
|
|
37
32
|
*/
|
|
38
|
-
export declare function isAzureProjectlessCandidate(baseUrl: string, repo: Pick<AzureRepositoryDescriptor, 'owner' | 'name' | 'virtualDirectory'
|
|
33
|
+
export declare function isAzureProjectlessCandidate(baseUrl: string, repo: Pick<AzureRepositoryDescriptor, 'owner' | 'name' | 'virtualDirectory'>, belowCollection?: boolean): boolean;
|
|
39
34
|
/**
|
|
40
35
|
* The virtual directory ahead of `owner` (empty when nothing is) in whichever of {@link getAzureProjectlessDirectories}
|
|
41
36
|
* ends with it, `undefined` when none does. A caller-supplied repository scope carries only its owner, so this places
|
|
42
37
|
* it where {@link isAzureProjectlessCandidate} expects it.
|
|
43
38
|
*/
|
|
44
|
-
export declare function getAzureScopeVirtualDirectory(baseUrl: string, owner: string): string | undefined;
|
|
39
|
+
export declare function getAzureScopeVirtualDirectory(baseUrl: string, owner: string, belowCollection?: boolean): string | undefined;
|
|
45
40
|
/**
|
|
46
41
|
* Whether two collection or project names name the same scope. Azure DevOps treats them case-insensitively, so a
|
|
47
42
|
* name typed by hand must still select the collection or project discovery reported.
|
|
@@ -121,13 +116,17 @@ export declare abstract class AzureDevOpsIntegrationBase<TIntegrationId extends
|
|
|
121
116
|
protected readRepository<R>(session: ProviderAuthenticationSession, repo: AzureRepositoryDescriptor, read: (azure: AzureDevOpsApi, target: AzureRepositoryTarget<AzureRepositoryDescriptor>) => Promise<R | undefined>, cancellation?: AbortSignal): Promise<R | undefined>;
|
|
122
117
|
/** Reads the organizations (Azure DevOps Server: the collections) the account can see. */
|
|
123
118
|
protected requestResourcesForUser(session: ProviderAuthenticationSession, userId: string): Promise<ProviderAzureResource[] | undefined>;
|
|
119
|
+
private readonly _organizationUserIds;
|
|
124
120
|
/**
|
|
125
|
-
* The identity id
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
121
|
+
* The current user's identity id in `organization` (Azure DevOps Server: the collection), which is what Azure's
|
|
122
|
+
* creator and reviewer filters, and its pull requests' authors and reviewers, compare against. Never the
|
|
123
|
+
* account's own id: Azure DevOps Services can know a person in an organization by an id other than their profile
|
|
124
|
+
* id, and Azure DevOps Server gives them a different id in each collection than at the server level. Cached per
|
|
125
|
+
* credential and organization, since it can't change for either.
|
|
129
126
|
*/
|
|
130
|
-
protected getFilterUserId(session: ProviderAuthenticationSession,
|
|
127
|
+
protected getFilterUserId(session: ProviderAuthenticationSession, organization: string): Promise<string | undefined>;
|
|
128
|
+
/** Reads the current user's identity id from `organization`'s own connection data; see {@link getFilterUserId}. */
|
|
129
|
+
protected requestOrganizationUserId(session: ProviderAuthenticationSession, organization: string): Promise<string | undefined>;
|
|
131
130
|
/**
|
|
132
131
|
* The current user's groups per credential, collection and user, kept for {@link discoveryCacheTtl} like the other
|
|
133
132
|
* discovered sets: membership changes rarely, and a sweep that reads every few minutes would otherwise pay for it
|
|
@@ -142,16 +141,28 @@ export declare abstract class AzureDevOpsIntegrationBase<TIntegrationId extends
|
|
|
142
141
|
protected getReviewerGroupIds(session: ProviderAuthenticationSession, collection: string, userId: string): Promise<Set<string>>;
|
|
143
142
|
invalidateDiscoveryCaches(): void;
|
|
144
143
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
|
|
144
|
+
* The viewer each row of an account-wide read was read for (see {@link getProviderMyPullRequestsForUser}), so its
|
|
145
|
+
* mapping uses the identity that read resolved instead of resolving it again.
|
|
146
|
+
*/
|
|
147
|
+
private readonly _rowViewers;
|
|
148
|
+
/**
|
|
149
|
+
* Resolved per organization, which a raw row names as its repository owner: an organization's pull requests carry
|
|
150
|
+
* the id {@link getFilterUserId} resolves there, not necessarily the account's. A row an account-wide read returned
|
|
151
|
+
* keeps the one that read resolved, even once its session is gone. A row that names no organization or no one to
|
|
152
|
+
* match gets no viewer, nor does one in an organization whose id can't be read: its authorship stays unknown.
|
|
148
153
|
*/
|
|
149
|
-
|
|
154
|
+
getPullRequestViewers(pullRequests: readonly (ProviderPullRequest | PullRequest)[], connectionId?: string): Promise<({
|
|
150
155
|
id: string;
|
|
151
156
|
username?: string;
|
|
152
|
-
} | undefined)
|
|
157
|
+
} | undefined)[]>;
|
|
158
|
+
/**
|
|
159
|
+
* For one read, the current user as each organization's pull requests identify them (see
|
|
160
|
+
* {@link getFilterUserId}). An organization whose id can't be read resolves `undefined`, since authorship is
|
|
161
|
+
* optional enrichment and one organization's failed lookup must not fail the read or blank the others; it is
|
|
162
|
+
* asked once per read, as a miss isn't cached.
|
|
163
|
+
*/
|
|
164
|
+
protected getOrganizationViewers(session: ProviderAuthenticationSession): (organization: string) => Promise<{
|
|
153
165
|
id: string;
|
|
154
|
-
username?: string;
|
|
155
166
|
} | undefined>;
|
|
156
167
|
/**
|
|
157
168
|
* What the discovery caches (account, organizations, projects, and their stored copies) are keyed by. Azure DevOps
|
|
@@ -246,7 +257,7 @@ export declare abstract class AzureDevOpsIntegrationBase<TIntegrationId extends
|
|
|
246
257
|
repo: string;
|
|
247
258
|
number: number;
|
|
248
259
|
project?: string;
|
|
249
|
-
}[],
|
|
260
|
+
}[], _options: {
|
|
250
261
|
currentAccount?: {
|
|
251
262
|
id: string;
|
|
252
263
|
username?: string;
|
|
@@ -359,7 +370,9 @@ export declare abstract class AzureDevOpsIntegrationBase<TIntegrationId extends
|
|
|
359
370
|
protected searchProviderMyIssuesWithTruncation(session: ProviderAuthenticationSession, _resources?: ResourceDescriptor[], _cancellation?: AbortSignal, searchOptions?: SearchMyIssuesOptions): Promise<AccountWideIssuesResult | undefined>;
|
|
360
371
|
protected providerOnConnect(): Promise<void>;
|
|
361
372
|
protected providerOnDisconnect(): void;
|
|
362
|
-
protected fromAzureProviderPullRequest(azurePullRequest: ProviderPullRequest, repoDescriptors: AzureRemoteRepositoryDescriptor[], projectDescriptors: AzureProjectDescriptor[], projection: PullRequestProjection
|
|
373
|
+
protected fromAzureProviderPullRequest(azurePullRequest: ProviderPullRequest, repoDescriptors: AzureRemoteRepositoryDescriptor[], projectDescriptors: AzureProjectDescriptor[], projection: PullRequestProjection, currentAccount?: {
|
|
374
|
+
id: string;
|
|
375
|
+
}): PullRequest;
|
|
363
376
|
}
|
|
364
377
|
export declare class AzureDevOpsIntegration extends AzureDevOpsIntegrationBase<GitCloudHostIntegrationId.AzureDevOps> {
|
|
365
378
|
readonly authProvider: IntegrationAuthenticationProviderDescriptor;
|