@gitkraken/core-gitlens 0.7.0 → 0.7.2
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 +28 -2
- package/dist/git/remotes/azure-devops.js +3 -3
- package/dist/git/remotes/azure-devops.js.map +1 -1
- package/dist/plus/integrations/authentication/configuredIntegrationService.js +9 -2
- package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +48 -3
- package/dist/plus/integrations/collectionMetadata.js +138 -8
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/errors.d.ts +3 -1
- package/dist/plus/integrations/errors.js +7 -3
- package/dist/plus/integrations/errors.js.map +1 -1
- package/dist/plus/integrations/index.d.ts +3 -3
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +35 -1
- package/dist/plus/integrations/integrationService.js +90 -43
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/lite.js +5 -4
- package/dist/plus/integrations/lite.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +68 -23
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +31 -15
- package/dist/plus/integrations/models/gitHostIntegration.js +71 -22
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +70 -12
- package/dist/plus/integrations/models/integration.js +56 -1
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +17 -0
- package/dist/plus/integrations/models/issuesIntegration.js +2 -0
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/models/pullRequestReads.d.ts +26 -0
- package/dist/plus/integrations/providers/azure/azure.d.ts +66 -1
- package/dist/plus/integrations/providers/azure/azure.js +192 -12
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/search.d.ts +144 -0
- package/dist/plus/integrations/providers/azure/search.js +250 -0
- package/dist/plus/integrations/providers/azure/search.js.map +1 -0
- package/dist/plus/integrations/providers/azureDevOps.d.ts +73 -36
- package/dist/plus/integrations/providers/azureDevOps.js +191 -122
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +176 -0
- package/dist/plus/integrations/providers/azureDevOpsServer.js +560 -0
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.d.ts +12 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.js +128 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +10 -1
- package/dist/plus/integrations/providers/bitbucket-server/models.js +3 -2
- package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +58 -0
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +485 -0
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +61 -3
- package/dist/plus/integrations/providers/bitbucket-server.js +114 -20
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +9 -1
- package/dist/plus/integrations/providers/bitbucket.js +13 -2
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +6 -6
- package/dist/plus/integrations/providers/github.js +35 -34
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +6 -4
- package/dist/plus/integrations/providers/gitlab.js +32 -28
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
- package/dist/plus/integrations/providers/jira-server.d.ts +9 -0
- package/dist/plus/integrations/providers/jira-server.js +15 -7
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +9 -0
- package/dist/plus/integrations/providers/jira.js +14 -2
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.js +2 -2
- package/dist/plus/integrations/providers/models.d.ts +10 -1
- package/dist/plus/integrations/providers/models.js +86 -0
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +10 -0
- package/dist/plus/integrations/providers/providersApi.js +18 -3
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/utils.d.ts +12 -1
- package/dist/plus/integrations/providers/utils.js +36 -6
- package/dist/plus/integrations/providers/utils.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.js +6 -6
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/context.d.ts +1 -1
- package/dist/plus/integrations/reads/counts.d.ts +18 -4
- package/dist/plus/integrations/reads/counts.js +66 -15
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.js +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +2 -1
- package/dist/plus/integrations/reads/drains.js +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +1 -1
- package/dist/plus/integrations/reads/filters.js +17 -12
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +2 -2
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.utils.js +1 -1
- package/dist/plus/integrations/reads/issueBatch.js +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +2 -2
- package/dist/plus/integrations/reads/issues.js +2 -2
- package/dist/plus/integrations/reads/paging.js +1 -1
- package/dist/plus/integrations/reads/resolveRepository.d.ts +12 -11
- package/dist/plus/integrations/reads/resolveRepository.js +110 -19
- package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
- package/dist/plus/integrations/reads/searchIssues.js +4 -2
- package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.js +9 -3
- package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/sweeps.js +1 -1
- package/dist/plus/integrations/reads/trackerIssue.d.ts +9 -5
- package/dist/plus/integrations/reads/trackerIssue.js +15 -9
- package/dist/plus/integrations/reads/trackerIssue.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +6 -2
- package/dist/plus/integrations/reads/warnings.js +15 -19
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +84 -6
- package/dist/plus/integrations/results.js +9 -2
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/plus/integrations/utils/domain.utils.d.ts +19 -0
- package/dist/plus/integrations/utils/domain.utils.js +36 -1
- package/dist/plus/integrations/utils/domain.utils.js.map +1 -1
- package/dist/plus/integrations/utils/integration.utils.d.ts +1 -0
- package/dist/plus/integrations/utils/integration.utils.js +12 -1
- package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
- package/docs/integrations.md +263 -74
- package/docs/kepler-read-api-parity.md +19 -6
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issuesIntegration.js","names":[],"sources":["../../../../../plus/integrations/src/models/issuesIntegration.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { gate } from '@gitlens/utils/decorators/gate.js';\nimport { trace } from '@gitlens/utils/decorators/log.js';\nimport { getScopedLogger } from '@gitlens/utils/logger.scoped.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { IntegrationReadUnavailableError, toError } from '../errors.js';\nimport type { ProviderApiCollectionResult } from '../providers/models.js';\nimport type { Integration, IntegrationResult, IntegrationType } from './integration.js';\nimport { IntegrationBase } from './integration.js';\nimport type { IssuesForProjectOptions, ProjectIssuesDrain } from './issueReads.js';\n\nexport function isIssuesIntegration(integration: Integration): integration is IssuesIntegration {\n\treturn integration.type === 'issues';\n}\n\nexport abstract class IssuesIntegration<\n\tID extends IntegrationIds = IntegrationIds,\n\tT extends ResourceDescriptor = ResourceDescriptor,\n> extends IntegrationBase<ID> {\n\treadonly type: IntegrationType = 'issues';\n\n\tget supportsIssueLookupByResourceId(): boolean {\n\t\treturn this.getProviderIssueByResourceId != null;\n\t}\n\n\tgetIssueByResourceIdResult(\n\t\tresourceId: string,\n\t\tid: string,\n\t\toptions?: { connectionId?: string; expiryOverride?: boolean | number; resourceUrl?: string },\n\t): Promise<IntegrationResult<Issue | undefined>> {\n\t\tconst getProviderIssue = this.getProviderIssueByResourceId;\n\t\tif (getProviderIssue == null) {\n\t\t\treturn Promise.resolve({\n\t\t\t\terror: new IntegrationReadUnavailableError(\n\t\t\t\t\tthis.name,\n\t\t\t\t\t'does not support direct issue reads by resource id',\n\t\t\t\t),\n\t\t\t});\n\t\t}\n\n\t\tconst { resourceUrl, ...readOptions } = options ?? {};\n\t\tconst resource = { key: resourceId, resourceId: resourceId, resourceUrl: resourceUrl };\n\t\treturn this.getIssueResultCore(resource, id, readOptions, session =>\n\t\t\tgetProviderIssue.call(this, session, resourceId, id, resourceUrl),\n\t\t);\n\t}\n\n\tprotected getProviderIssueByResourceId?(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resourceId: string,\n\t\t_id: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined>;\n\n\t@trace()\n\tasync getAccountForResource(resource: T, connectionId?: string): Promise<Account | undefined> {\n\t\treturn (await this.getAccountForResourceResult(resource, connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getAccountForResource}. Recovers a thrown error into `{ error }` so\n\t * callers can preserve its classification (e.g. a 401/403 → an `auth` warning that drives re-auth)\n\t * instead of collapsing every failure into an untyped `undefined`. Gated here (not on the wrapper) so\n\t * direct callers such as the ProviderBackend facade share the same dedup as `getAccountForResource`.\n\t */\n\t@gate()\n\tasync getAccountForResourceResult(\n\t\tresource: T,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<Account | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst account = await this.getProviderAccountForResource(session, resource);\n\t\t\tthis.resetRequestExceptionCount('getAccountForResource');\n\t\t\treturn { value: account };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getAccountForResource', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: T,\n\t): Promise<Account | undefined>;\n\n\t@trace()\n\tasync getResourcesForUser(connectionId?: string): Promise<T[] | undefined> {\n\t\treturn (await this.getResourcesForUserResult(connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getResourcesForUser}. Recovers thrown errors into `{ error }` so callers\n\t * can surface them as warnings rather than silently swallowing them to `undefined`. Gated here (not on the\n\t * wrapper) so direct callers such as the ProviderBackend facade share the same dedup as `getResourcesForUser`.\n\t */\n\t@gate()\n\tasync getResourcesForUserResult(connectionId?: string): Promise<IntegrationResult<T[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst resources = await this.getProviderResourcesForUser(session);\n\t\t\tthis.resetRequestExceptionCount('getResourcesForUser');\n\t\t\treturn { value: resources };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getResourcesForUser', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderResourcesForUser(session: ProviderAuthenticationSession): Promise<T[] | undefined>;\n\n\t/**\n\t * Project discovery for a set of resources, returning the SDK collection `{ values, metadata }` (completeness\n\t * + per-resource failures) so callers can warn on failed resources and set `fetchFailed` without discarding\n\t * the resources that succeeded. Thrown errors are recovered into `{ error }` for the same reason: a caller\n\t * surfaces them as a warning rather than swallowing them to `undefined`.\n\t */\n\t@trace()\n\tasync getProjectsForResourcesWithMetadataResult(\n\t\tresources: T[],\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<ProviderApiCollectionResult<T> | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst projects = await this.getProviderProjectsForResourcesWithMetadata(session, resources);\n\t\t\tthis.resetRequestExceptionCount('getProjectsForResources');\n\t\t\treturn { value: projects };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getProjectsForResources', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t/**\n\t * Metadata-aware provider project discovery. The default wraps the array-returning\n\t * {@link getProviderProjectsForResources} in `{ values }` with no metadata, so providers without a\n\t * fan-out completeness signal (Linear, Trello) need no change; Jira overrides it to preserve the SDK's\n\t * per-resource completeness/failures.\n\t */\n\tprotected async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: T[],\n\t): Promise<ProviderApiCollectionResult<T>> {\n\t\tconst projects = await this.getProviderProjectsForResources(session, resources);\n\t\treturn { values: projects ?? [] };\n\t}\n\n\tprotected abstract getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: T[],\n\t): Promise<T[] | undefined>;\n\n\t@trace()\n\tasync getIssuesForProject(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getIssuesForProjectResult(project, options, connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getIssuesForProject}. Recovers thrown errors into `{ error }` so callers\n\t * (e.g. the ProviderBackend facade) can surface a per-provider warning instead of a silent empty read —\n\t * important for providers that throw on unsupported operations (e.g. Linear's not-implemented issue read).\n\t */\n\tasync getIssuesForProjectResult(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<IssueShape[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst issues = await this.getProviderIssuesForProject(session, project, options);\n\t\t\tthis.resetRequestExceptionCount('getIssuesForProject');\n\t\t\treturn { value: issues };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t/**\n\t * Truncation-aware variant of {@link getIssuesForProjectResult}. A provider that drains a project's issues\n\t * with an internal page backstop (Jira/Linear) overrides {@link getProviderIssuesForProjectWithTruncation}\n\t * to report when that backstop was hit, so the facade can surface an incomplete project read instead of\n\t * publishing it as complete. The default reports `truncated: false`.\n\t */\n\tasync getIssuesForProjectWithTruncationResult(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<ProjectIssuesDrain | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst result = await this.getProviderIssuesForProjectWithTruncation(session, project, options);\n\t\t\tthis.resetRequestExceptionCount('getIssuesForProject');\n\t\t\treturn { value: result };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined>;\n\n\t/**\n\t * Truncation-aware core of {@link getProviderIssuesForProject}. The default wraps the plain read and\n\t * reports `truncated: false`; a provider whose per-project drain is capped by a page backstop overrides\n\t * this to report incompleteness. Optional metadata lets providers surface structured per-project failures\n\t * (e.g. a page-level auth rejection) without discarding the already-fetched prefix.\n\t */\n\tprotected async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst values = await this.getProviderIssuesForProject(session, project, options);\n\t\tif (values == null) return undefined;\n\t\treturn { values: values, truncated: false };\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"issuesIntegration.js","names":[],"sources":["../../../../../plus/integrations/src/models/issuesIntegration.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { gate } from '@gitlens/utils/decorators/gate.js';\nimport { trace } from '@gitlens/utils/decorators/log.js';\nimport { getScopedLogger } from '@gitlens/utils/logger.scoped.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { IntegrationReadUnavailableError, toError } from '../errors.js';\nimport type { ProviderApiCollectionResult } from '../providers/models.js';\nimport type { Integration, IntegrationResult, IntegrationType } from './integration.js';\nimport { IntegrationBase } from './integration.js';\nimport type { IssuesForProjectOptions, ProjectIssuesDrain } from './issueReads.js';\n\nexport function isIssuesIntegration(integration: Integration): integration is IssuesIntegration {\n\treturn integration.type === 'issues';\n}\n\nexport abstract class IssuesIntegration<\n\tID extends IntegrationIds = IntegrationIds,\n\tT extends ResourceDescriptor = ResourceDescriptor,\n> extends IntegrationBase<ID> {\n\treadonly type: IntegrationType = 'issues';\n\n\tget supportsIssueLookupByResourceId(): boolean {\n\t\treturn this.getProviderIssueByResourceId != null;\n\t}\n\n\tgetIssueByResourceIdResult(\n\t\tresourceId: string,\n\t\tid: string,\n\t\toptions?: { connectionId?: string; expiryOverride?: boolean | number; resourceUrl?: string },\n\t): Promise<IntegrationResult<Issue | undefined>> {\n\t\tconst getProviderIssue = this.getProviderIssueByResourceId;\n\t\tif (getProviderIssue == null) {\n\t\t\treturn Promise.resolve({\n\t\t\t\terror: new IntegrationReadUnavailableError(\n\t\t\t\t\tthis.name,\n\t\t\t\t\t'does not support direct issue reads by resource id',\n\t\t\t\t),\n\t\t\t});\n\t\t}\n\n\t\tconst { resourceUrl, ...readOptions } = options ?? {};\n\t\tconst resource = { key: resourceId, resourceId: resourceId, resourceUrl: resourceUrl };\n\t\treturn this.getIssueResultCore(resource, id, readOptions, session =>\n\t\t\tgetProviderIssue.call(this, session, resourceId, id, resourceUrl),\n\t\t);\n\t}\n\n\tprotected getProviderIssueByResourceId?(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resourceId: string,\n\t\t_id: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined>;\n\n\t@trace()\n\tasync getAccountForResource(resource: T, connectionId?: string): Promise<Account | undefined> {\n\t\treturn (await this.getAccountForResourceResult(resource, connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getAccountForResource}. Recovers a thrown error into `{ error }` so\n\t * callers can preserve its classification (e.g. a 401/403 → an `auth` warning that drives re-auth)\n\t * instead of collapsing every failure into an untyped `undefined`. Gated here (not on the wrapper) so\n\t * direct callers such as the ProviderBackend facade share the same dedup as `getAccountForResource`.\n\t */\n\t@gate()\n\tasync getAccountForResourceResult(\n\t\tresource: T,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<Account | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst account = await this.getProviderAccountForResource(session, resource);\n\t\t\tthis.resetRequestExceptionCount('getAccountForResource');\n\t\t\treturn { value: account };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getAccountForResource', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: T,\n\t): Promise<Account | undefined>;\n\n\t@trace()\n\tasync getResourcesForUser(connectionId?: string): Promise<T[] | undefined> {\n\t\treturn (await this.getResourcesForUserResult(connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getResourcesForUser}. Recovers thrown errors into `{ error }` so callers\n\t * can surface them as warnings rather than silently swallowing them to `undefined`. Gated here (not on the\n\t * wrapper) so direct callers such as the ProviderBackend facade share the same dedup as `getResourcesForUser`.\n\t */\n\t@gate()\n\tasync getResourcesForUserResult(connectionId?: string): Promise<IntegrationResult<T[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst resources = await this.getProviderResourcesForUser(session);\n\t\t\tthis.resetRequestExceptionCount('getResourcesForUser');\n\t\t\treturn { value: resources };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getResourcesForUser', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderResourcesForUser(session: ProviderAuthenticationSession): Promise<T[] | undefined>;\n\n\t/**\n\t * Project discovery for a set of resources, returning the SDK collection `{ values, metadata }` (completeness\n\t * + per-resource failures) so callers can warn on failed resources and set `fetchFailed` without discarding\n\t * the resources that succeeded. Thrown errors are recovered into `{ error }` for the same reason: a caller\n\t * surfaces them as a warning rather than swallowing them to `undefined`.\n\t */\n\t@trace()\n\tasync getProjectsForResourcesWithMetadataResult(\n\t\tresources: T[],\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<ProviderApiCollectionResult<T> | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst projects = await this.getProviderProjectsForResourcesWithMetadata(session, resources);\n\t\t\tawait this.confirmScopedAuthFailures(session, projects.metadata);\n\t\t\tthis.resetRequestExceptionCount('getProjectsForResources');\n\t\t\treturn { value: projects };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getProjectsForResources', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t/**\n\t * Metadata-aware provider project discovery. The default wraps the array-returning\n\t * {@link getProviderProjectsForResources} in `{ values }` with no metadata, so providers without a\n\t * fan-out completeness signal (Linear, Trello) need no change; Jira overrides it to preserve the SDK's\n\t * per-resource completeness/failures.\n\t */\n\tprotected async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: T[],\n\t): Promise<ProviderApiCollectionResult<T>> {\n\t\tconst projects = await this.getProviderProjectsForResources(session, resources);\n\t\treturn { values: projects ?? [] };\n\t}\n\n\tprotected abstract getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: T[],\n\t): Promise<T[] | undefined>;\n\n\t@trace()\n\tasync getIssuesForProject(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getIssuesForProjectResult(project, options, connectionId))?.value;\n\t}\n\n\t/**\n\t * Result-returning core of {@link getIssuesForProject}. Recovers thrown errors into `{ error }` so callers\n\t * (e.g. the ProviderBackend facade) can surface a per-provider warning instead of a silent empty read —\n\t * important for providers that throw on unsupported operations (e.g. Linear's not-implemented issue read).\n\t */\n\tasync getIssuesForProjectResult(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<IssueShape[] | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\t// `connectionId` targets a specific account (multi-account); omitted reads the primary.\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst issues = await this.getProviderIssuesForProject(session, project, options);\n\t\t\tthis.resetRequestExceptionCount('getIssuesForProject');\n\t\t\treturn { value: issues };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\t/**\n\t * Truncation-aware variant of {@link getIssuesForProjectResult}. A provider that drains a project's issues\n\t * with an internal page backstop (Jira/Linear) overrides {@link getProviderIssuesForProjectWithTruncation}\n\t * to report when that backstop was hit, so the facade can surface an incomplete project read instead of\n\t * publishing it as complete. The default reports `truncated: false`.\n\t */\n\tasync getIssuesForProjectWithTruncationResult(\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t\tconnectionId?: string,\n\t): Promise<IntegrationResult<ProjectIssuesDrain | undefined>> {\n\t\tconst scope = getScopedLogger();\n\t\tconst session = await this.resolveReadSession(connectionId, scope);\n\t\tif (session == null) return undefined;\n\n\t\ttry {\n\t\t\tconst result = await this.getProviderIssuesForProjectWithTruncation(session, project, options);\n\t\t\tawait this.confirmScopedAuthFailures(session, result?.metadata);\n\t\t\tthis.resetRequestExceptionCount('getIssuesForProject');\n\t\t\treturn { value: result };\n\t\t} catch (ex) {\n\t\t\tthis.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });\n\t\t\treturn { error: toError(ex) };\n\t\t}\n\t}\n\n\tprotected abstract getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined>;\n\n\t/**\n\t * Truncation-aware core of {@link getProviderIssuesForProject}. The default wraps the plain read and\n\t * reports `truncated: false`; a provider whose per-project drain is capped by a page backstop overrides\n\t * this to report incompleteness. Optional metadata lets providers surface structured per-project failures\n\t * (e.g. a page-level auth rejection) without discarding the already-fetched prefix.\n\t */\n\tprotected async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: T,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst values = await this.getProviderIssuesForProject(session, project, options);\n\t\tif (values == null) return undefined;\n\t\treturn { values: values, truncated: false };\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PullRequestShape } from "../../../git/models/pullRequest.js";
|
|
2
|
+
import { CollectionMetadata } from "@gitkraken/provider-apis";
|
|
2
3
|
//#region ../plus/integrations/src/models/pullRequestReads.d.ts
|
|
3
4
|
/**
|
|
4
5
|
* One provider page of the filtered pull-request search, before the facade turns it into a neutral
|
|
@@ -7,6 +8,14 @@ import { PullRequestShape } from "../../../git/models/pullRequest.js";
|
|
|
7
8
|
* `totalCount` is the provider-reported pre-ceiling facet count, not the number of rows that remain reachable
|
|
8
9
|
* after a provider limit. It stays optional because absence means "not reported", never zero. Together with
|
|
9
10
|
* `truncated` it lets the facade distinguish GitHub's quantified ceiling from another unusable continuation.
|
|
11
|
+
*
|
|
12
|
+
* `metadata` carries the facets whose rows are missing from the walk so far — Bitbucket Data Center reads every
|
|
13
|
+
* facet as its own request, so one can be refused (a repository the token can't see) without the others. It
|
|
14
|
+
* describes the whole walk rather than one page: a facet that failed on this page is reported here and retried on
|
|
15
|
+
* the next, and one whose retry failed too is dropped and reported on every later page, since its hole is still in
|
|
16
|
+
* the result. A retry that succeeds recovers the rows, so it drops out. The last page read is therefore the one to
|
|
17
|
+
* assess; the facade reports each failure as a warning with `fetchFailed`, and a page where EVERY facet failed
|
|
18
|
+
* throws instead.
|
|
10
19
|
*/
|
|
11
20
|
export type ProviderPullRequestSearchPage = {
|
|
12
21
|
values: PullRequestShape[];
|
|
@@ -15,6 +24,23 @@ export type ProviderPullRequestSearchPage = {
|
|
|
15
24
|
hasMore: boolean;
|
|
16
25
|
page: number;
|
|
17
26
|
totalCount?: number;
|
|
27
|
+
metadata?: CollectionMetadata;
|
|
28
|
+
/** See `ProviderIssueSearchPage.limitReached`. */
|
|
29
|
+
limitReached?: boolean;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* One scope's pull-request count, as the provider reports it.
|
|
33
|
+
*
|
|
34
|
+
* `lowerBound` marks a count that stopped before the scope was exhausted: a provider with no count query
|
|
35
|
+
* (Bitbucket Data Center) counts by reading, within a budget, so past it `count` is how many matched in what was
|
|
36
|
+
* read — a floor, never the total.
|
|
37
|
+
*
|
|
38
|
+
* An `Error` in its place refuses that ONE scope for its own reasons (Azure DevOps Server refusing a scope it can't
|
|
39
|
+
* search): the facade warns and drops only it, as it does for a scope it refuses itself.
|
|
40
|
+
*/
|
|
41
|
+
export type ProviderPullRequestCount = {
|
|
42
|
+
count: number | undefined;
|
|
43
|
+
lowerBound?: boolean;
|
|
18
44
|
};
|
|
19
45
|
//#endregion
|
|
20
46
|
//# sourceMappingURL=pullRequestReads.d.ts.map
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Disposable } from "../../../../utils/disposable.js";
|
|
2
2
|
import { Provider } from "../../../../git/models/remoteProvider.js";
|
|
3
3
|
import { IssueOrPullRequest, IssueOrPullRequestType } from "../../../../git/models/issueOrPullRequest.js";
|
|
4
|
-
import { Issue } from "../../../../git/models/issue.js";
|
|
4
|
+
import { Issue, IssueSearchCriteria } from "../../../../git/models/issue.js";
|
|
5
5
|
import { PullRequest } from "../../../../git/models/pullRequest.js";
|
|
6
6
|
import { UnidentifiedAuthor } from "../../../../git/models/author.js";
|
|
7
7
|
import { DefaultBranch } from "../../../../git/models/defaultBranch.js";
|
|
8
8
|
import { RepositoryMetadata } from "../../../../git/models/repositoryMetadata.js";
|
|
9
9
|
import { TokenWithInfo } from "../../authentication/models.js";
|
|
10
|
+
import { ProviderIssueSearchPage } from "../../models/issueReads.js";
|
|
10
11
|
import { ProviderApiConfig } from "../apiConfig.js";
|
|
11
12
|
import { AzureProjectDescriptor, AzureWorkItemStateCategory } from "./models.js";
|
|
12
13
|
import { IntegrationServiceContext } from "../../context.js";
|
|
@@ -16,6 +17,12 @@ export declare class AzureDevOpsApi implements Disposable {
|
|
|
16
17
|
private readonly _disposable;
|
|
17
18
|
private _workItemStates;
|
|
18
19
|
private readonly _forkRepositoryUrls;
|
|
20
|
+
/**
|
|
21
|
+
* The ordered ids each filtered work-item search's first page queried, paged through by its continuations. Kept
|
|
22
|
+
* alive while a pagination keeps reading them (see `workItemSearchSnapshotTtl`). Every first page adds one, so
|
|
23
|
+
* the capacity leaves room for repeated first pages before an idle pagination's snapshot is the one evicted.
|
|
24
|
+
*/
|
|
25
|
+
private readonly _workItemSearches;
|
|
19
26
|
constructor(config: ProviderApiConfig);
|
|
20
27
|
dispose(): void;
|
|
21
28
|
private resetCaches;
|
|
@@ -35,6 +42,19 @@ export declare class AzureDevOpsApi implements Disposable {
|
|
|
35
42
|
getAccountForCommit(provider: Provider, token: TokenWithInfo, owner: string, repo: string, rev: string, baseUrl: string, _options?: {
|
|
36
43
|
avatarSize?: number;
|
|
37
44
|
}): Promise<UnidentifiedAuthor | undefined>;
|
|
45
|
+
/**
|
|
46
|
+
* The collection an Azure DevOps Server address names, or `undefined` when it names the installation.
|
|
47
|
+
*
|
|
48
|
+
* Below a collection, `connectionData` describes that collection: its `instanceId` is the collection's id, and its
|
|
49
|
+
* `webApplicationRelativeDirectory` ends in the collection's path segment (`DefaultCollection/`); it is empty or
|
|
50
|
+
* absent at the server level. As measured against Azure DevOps Server 2020. The directory may also carry the
|
|
51
|
+
* installation's own virtual directory before the collection (`tfs/DefaultCollection/`), so only its LAST segment
|
|
52
|
+
* names the collection, and only when it is the last segment of the address asked about.
|
|
53
|
+
*/
|
|
54
|
+
getAddressedCollection(provider: Provider, token: TokenWithInfo, baseUrl: string): Promise<{
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
} | undefined>;
|
|
38
58
|
getCurrentUserOnServer(provider: Provider, token: TokenWithInfo, baseUrl: string): Promise<{
|
|
39
59
|
id: string;
|
|
40
60
|
name?: string;
|
|
@@ -52,6 +72,51 @@ export declare class AzureDevOpsApi implements Disposable {
|
|
|
52
72
|
getDefaultBranch(provider: Provider, token: TokenWithInfo, owner: string, repo: string, options: {
|
|
53
73
|
baseUrl: string;
|
|
54
74
|
}, cancellation?: AbortSignal): Promise<DefaultBranch | undefined>;
|
|
75
|
+
/**
|
|
76
|
+
* One page of a filtered work-item search over a collection: a WIQL query for the ordered ids, then one detail
|
|
77
|
+
* read for the page's slice of them.
|
|
78
|
+
*
|
|
79
|
+
* The ids a first page queried are kept as the snapshot its continuations page through, for as long as the
|
|
80
|
+
* pagination keeps reading it, so it reads a consistent set rather than re-running a query whose rows move under
|
|
81
|
+
* an `updated` order. Each first page queries its own snapshot, so paginations of one query never share one, and
|
|
82
|
+
* a continuation whose snapshot is gone is refused (see {@link AzureWorkItemSearchCursor}).
|
|
83
|
+
*
|
|
84
|
+
* The query asks for one id more than {@link azureWorkItemSearchResultLimit}, bounded by `$top`: the extra id is
|
|
85
|
+
* what tells "exactly the limit" from "past it". Past it, the page is `truncated` with `limitReached` and no total,
|
|
86
|
+
* since the true count is unknowable here. A server that refuses the match set even so fails the page with a
|
|
87
|
+
* "narrow the search" error: no bounded query could serve that order's first window.
|
|
88
|
+
*
|
|
89
|
+
* `resolveProject` maps each result's project name to its descriptor. A work item in a project it doesn't know
|
|
90
|
+
* fails the page rather than being dropped or mis-attributed, since either would leave the page disagreeing with
|
|
91
|
+
* the count of the same query; the caller refreshes its project discovery before a search so that means a
|
|
92
|
+
* project created during the pagination.
|
|
93
|
+
*/
|
|
94
|
+
searchWorkItemsPage(provider: Provider, token: TokenWithInfo, collection: string, options: {
|
|
95
|
+
baseUrl: string;
|
|
96
|
+
criteria?: IssueSearchCriteria;
|
|
97
|
+
projectNames?: readonly string[];
|
|
98
|
+
resolveProject: (name: string) => AzureProjectDescriptor | undefined;
|
|
99
|
+
cursor?: string;
|
|
100
|
+
pageSize?: number;
|
|
101
|
+
}, cancellation?: AbortSignal): Promise<ProviderIssueSearchPage>;
|
|
102
|
+
/**
|
|
103
|
+
* How many work items the same WIQL {@link searchWorkItemsPage} would page through match, reading no details.
|
|
104
|
+
*
|
|
105
|
+
* Asks for one id more than the result ceiling, so the count is exact up to it and `'exceeds-limit'` past it —
|
|
106
|
+
* never the ceiling itself, which would understate the match set.
|
|
107
|
+
*/
|
|
108
|
+
countWorkItems(provider: Provider, token: TokenWithInfo, collection: string, options: {
|
|
109
|
+
baseUrl: string;
|
|
110
|
+
criteria?: IssueSearchCriteria;
|
|
111
|
+
projectNames?: readonly string[];
|
|
112
|
+
}, cancellation?: AbortSignal): Promise<number | 'exceeds-limit'>;
|
|
113
|
+
/**
|
|
114
|
+
* The ids a collection-level WIQL query matches, in its order.
|
|
115
|
+
*
|
|
116
|
+
* `api-version=5.0` is the oldest version every Azure DevOps Server release this search supports (2019 and later)
|
|
117
|
+
* answers; `timePrecision=true` is what lets the criteria's dates compare as instants — see `toWiqlDate`.
|
|
118
|
+
*/
|
|
119
|
+
private queryWorkItemIds;
|
|
55
120
|
private getRepository;
|
|
56
121
|
private toPullRequest;
|
|
57
122
|
/**
|
|
@@ -7,13 +7,27 @@ import __decorate from "../../../../_virtual/@oxc-project+runtime/helpers/esm/de
|
|
|
7
7
|
import { AuthenticationError, RequestClientError, RequestNotFoundError } from "../../../../git/errors.js";
|
|
8
8
|
import { maybeStopWatch } from "../../../../utils/stopwatch.js";
|
|
9
9
|
import { base64 } from "../../../../utils/base64.js";
|
|
10
|
-
import { sha256 } from "../../../../utils/crypto.js";
|
|
10
|
+
import { sha256, uuid } from "../../../../utils/crypto.js";
|
|
11
|
+
import { effectiveIssueSort } from "../../../../git/utils/issue.utils.js";
|
|
11
12
|
import { ProviderFetchError, isRateLimitResponse, toRateLimitError } from "../../errors.js";
|
|
13
|
+
import { azureWorkItemSearchResultLimit, parseAzureWorkItemSearchCursor, toAzureSearchPageSize, toAzureWorkItemSearchCursorKey, toAzureWorkItemSearchWiql } from "./search.js";
|
|
14
|
+
import { decodePathSegment } from "../../utils/domain.utils.js";
|
|
12
15
|
import { baseProviderApiConfig } from "../apiConfig.js";
|
|
13
16
|
import { azurePullRequestStatusToState, azureWorkItemsStateCategoryToState, fromAzurePullRequest, fromAzureWorkItem, getAzurePullRequestWebUrl, isClosedAzurePullRequestStatus, isClosedAzureWorkItemStateCategory, normalizeAzureBranchName, sanitizeAzureRepositoryUrl } from "./models.js";
|
|
14
17
|
import * as l10n from "@vscode/l10n";
|
|
15
18
|
//#region ../plus/integrations/src/providers/azure/azure.ts
|
|
16
19
|
const forkRepositoryUrlCacheTtl = 3e5;
|
|
20
|
+
/** How long a work-item search's id snapshot outlives the last page read from it, and the most it lives at all. */
|
|
21
|
+
const workItemSearchSnapshotTtl = 3e5;
|
|
22
|
+
const workItemSearchSnapshotMaxTtl = 18e5;
|
|
23
|
+
function encodePathSegment(value) {
|
|
24
|
+
if (value === "." || value === "..") throw new Error(`Invalid Azure path segment '${value}'.`);
|
|
25
|
+
return encodeURIComponent(value);
|
|
26
|
+
}
|
|
27
|
+
/** Whether Azure refused a WIQL query for matching more work items than it will return (`VS402337`). */
|
|
28
|
+
function isWorkItemLimitRefusal(ex) {
|
|
29
|
+
return ex instanceof RequestClientError && ex.message.includes("VS402337");
|
|
30
|
+
}
|
|
17
31
|
function parseAzureRepositoryDescriptor(repo) {
|
|
18
32
|
const parts = repo.split("/");
|
|
19
33
|
const [projectName, segment, repoName] = parts;
|
|
@@ -65,6 +79,16 @@ var AzureDevOpsApi = class {
|
|
|
65
79
|
capacity: 100,
|
|
66
80
|
createTTL: forkRepositoryUrlCacheTtl
|
|
67
81
|
});
|
|
82
|
+
/**
|
|
83
|
+
* The ordered ids each filtered work-item search's first page queried, paged through by its continuations. Kept
|
|
84
|
+
* alive while a pagination keeps reading them (see `workItemSearchSnapshotTtl`). Every first page adds one, so
|
|
85
|
+
* the capacity leaves room for repeated first pages before an idle pagination's snapshot is the one evicted.
|
|
86
|
+
*/
|
|
87
|
+
_workItemSearches = new PromiseCache({
|
|
88
|
+
capacity: 50,
|
|
89
|
+
accessTTL: workItemSearchSnapshotTtl,
|
|
90
|
+
createTTL: workItemSearchSnapshotMaxTtl
|
|
91
|
+
});
|
|
68
92
|
constructor(config) {
|
|
69
93
|
this.config = config;
|
|
70
94
|
this._disposable = config.onConfigChanged?.(() => this.resetCaches());
|
|
@@ -75,12 +99,13 @@ var AzureDevOpsApi = class {
|
|
|
75
99
|
resetCaches() {
|
|
76
100
|
this._workItemStates.clear();
|
|
77
101
|
this._forkRepositoryUrls.clear();
|
|
102
|
+
this._workItemSearches.clear();
|
|
78
103
|
}
|
|
79
104
|
async getPullRequestForBranch(provider, token, owner, repo, branch, options) {
|
|
80
105
|
const scope = getScopedLogger();
|
|
81
106
|
const { projectName, repoName } = parseAzureRepositoryDescriptor(repo);
|
|
82
107
|
try {
|
|
83
|
-
const pr = ((await this.request(provider, token, options?.baseUrl, `${owner}/${projectName}/_apis/git/repositories/${repoName}/pullRequests?searchCriteria.status=all&searchCriteria.sourceRefName
|
|
108
|
+
const pr = ((await this.request(provider, token, options?.baseUrl, `${encodePathSegment(owner)}/${encodePathSegment(projectName)}/_apis/git/repositories/${encodePathSegment(repoName)}/pullRequests?searchCriteria.status=all&searchCriteria.sourceRefName=${encodeURIComponent(`refs/heads/${branch}`)}`, { method: "GET" }, scope))?.value.sort((a, b) => {
|
|
84
109
|
const aIsOpen = a.status === "active" || a.status === "notSet";
|
|
85
110
|
if (aIsOpen !== (b.status === "active" || b.status === "notSet")) return aIsOpen ? -1 : 1;
|
|
86
111
|
const aDate = new Date(a.closedDate || a.creationDate);
|
|
@@ -98,7 +123,7 @@ var AzureDevOpsApi = class {
|
|
|
98
123
|
const scope = getScopedLogger();
|
|
99
124
|
const { projectName, repoName } = parseAzureRepositoryDescriptor(repo);
|
|
100
125
|
try {
|
|
101
|
-
const pr = (await this.request(provider, token, baseUrl, `${owner}/${projectName}/_apis/git/repositories/${repoName}/pullrequestquery?api-version=4.1`, {
|
|
126
|
+
const pr = (await this.request(provider, token, baseUrl, `${encodePathSegment(owner)}/${encodePathSegment(projectName)}/_apis/git/repositories/${encodePathSegment(repoName)}/pullrequestquery?api-version=4.1`, {
|
|
102
127
|
method: "POST",
|
|
103
128
|
body: JSON.stringify({ queries: [{
|
|
104
129
|
items: [rev],
|
|
@@ -106,7 +131,7 @@ var AzureDevOpsApi = class {
|
|
|
106
131
|
}] })
|
|
107
132
|
}, scope, cancellation))?.results[0]?.[rev]?.[0];
|
|
108
133
|
if (pr == null) return void 0;
|
|
109
|
-
const pullRequest = await this.request(provider, token, baseUrl, `${owner}/${
|
|
134
|
+
const pullRequest = await this.request(provider, token, baseUrl, `${encodePathSegment(owner)}/${encodePathSegment(pr.repository.project.id)}/_apis/git/repositories/${encodePathSegment(pr.repository.id)}/pullRequests/${encodePathSegment(pr.pullRequestId.toString())}`, { method: "GET" }, scope, cancellation);
|
|
110
135
|
if (pullRequest == null) return void 0;
|
|
111
136
|
return await this.toPullRequest(pullRequest, provider, token, owner, baseUrl, scope, cancellation);
|
|
112
137
|
} catch (ex) {
|
|
@@ -119,7 +144,7 @@ var AzureDevOpsApi = class {
|
|
|
119
144
|
const scope = getScopedLogger();
|
|
120
145
|
const { projectName, repoName } = parseAzureRepositoryDescriptor(repo);
|
|
121
146
|
if (options?.type === void 0 || options?.type === "issue") try {
|
|
122
|
-
const issueResult = await this.request(provider, token, options?.baseUrl, `${owner}/${projectName}/_apis/wit/workItems/${id}`, { method: "GET" }, scope);
|
|
147
|
+
const issueResult = await this.request(provider, token, options?.baseUrl, `${encodePathSegment(owner)}/${encodePathSegment(projectName)}/_apis/wit/workItems/${encodePathSegment(id)}`, { method: "GET" }, scope);
|
|
123
148
|
if (issueResult != null) {
|
|
124
149
|
const issueType = issueResult.fields["System.WorkItemType"];
|
|
125
150
|
const state = issueResult.fields["System.State"];
|
|
@@ -145,7 +170,7 @@ var AzureDevOpsApi = class {
|
|
|
145
170
|
}
|
|
146
171
|
}
|
|
147
172
|
if (options?.type === void 0 || options?.type === "pullrequest") try {
|
|
148
|
-
const prResult = await this.request(provider, token, options?.baseUrl, `${owner}/${projectName}/_apis/git/repositories/${repoName}/pullRequests/${id}`, { method: "GET" }, scope);
|
|
173
|
+
const prResult = await this.request(provider, token, options?.baseUrl, `${encodePathSegment(owner)}/${encodePathSegment(projectName)}/_apis/git/repositories/${encodePathSegment(repoName)}/pullRequests/${encodePathSegment(id)}`, { method: "GET" }, scope);
|
|
149
174
|
if (prResult != null) return {
|
|
150
175
|
id: prResult.pullRequestId.toString(),
|
|
151
176
|
type: "pullrequest",
|
|
@@ -170,7 +195,7 @@ var AzureDevOpsApi = class {
|
|
|
170
195
|
async getIssue(provider, token, project, id, options) {
|
|
171
196
|
const scope = getScopedLogger();
|
|
172
197
|
try {
|
|
173
|
-
const issueResult = await this.request(provider, token, options?.baseUrl, `${project.resourceName}/${project.name}/_apis/wit/workItems/${id}`, { method: "GET" }, scope);
|
|
198
|
+
const issueResult = await this.request(provider, token, options?.baseUrl, `${encodePathSegment(project.resourceName)}/${encodePathSegment(project.name)}/_apis/wit/workItems/${encodePathSegment(id)}`, { method: "GET" }, scope);
|
|
174
199
|
if (issueResult != null) {
|
|
175
200
|
const issueType = issueResult.fields["System.WorkItemType"];
|
|
176
201
|
const state = issueResult.fields["System.State"];
|
|
@@ -189,7 +214,7 @@ var AzureDevOpsApi = class {
|
|
|
189
214
|
const scope = getScopedLogger();
|
|
190
215
|
const { projectName, repoName } = parseAzureRepositoryDescriptor(repo);
|
|
191
216
|
try {
|
|
192
|
-
const author = (await this.request(provider, token, baseUrl, `${owner}/${projectName}/_apis/git/repositories/${repoName}/commits/${rev}`, { method: "GET" }, scope))?.author;
|
|
217
|
+
const author = (await this.request(provider, token, baseUrl, `${encodePathSegment(owner)}/${encodePathSegment(projectName)}/_apis/git/repositories/${encodePathSegment(repoName)}/commits/${encodePathSegment(rev)}`, { method: "GET" }, scope))?.author;
|
|
193
218
|
if (!author) return;
|
|
194
219
|
return {
|
|
195
220
|
provider,
|
|
@@ -207,6 +232,28 @@ var AzureDevOpsApi = class {
|
|
|
207
232
|
}
|
|
208
233
|
}
|
|
209
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* The collection an Azure DevOps Server address names, or `undefined` when it names the installation.
|
|
237
|
+
*
|
|
238
|
+
* Below a collection, `connectionData` describes that collection: its `instanceId` is the collection's id, and its
|
|
239
|
+
* `webApplicationRelativeDirectory` ends in the collection's path segment (`DefaultCollection/`); it is empty or
|
|
240
|
+
* absent at the server level. As measured against Azure DevOps Server 2020. The directory may also carry the
|
|
241
|
+
* installation's own virtual directory before the collection (`tfs/DefaultCollection/`), so only its LAST segment
|
|
242
|
+
* names the collection, and only when it is the last segment of the address asked about.
|
|
243
|
+
*/
|
|
244
|
+
async getAddressedCollection(provider, token, baseUrl) {
|
|
245
|
+
const scope = getScopedLogger();
|
|
246
|
+
const connectionData = await this.request(provider, token, baseUrl, "_apis/connectionData", { method: "GET" }, scope);
|
|
247
|
+
const segment = connectionData?.webApplicationRelativeDirectory?.split("/").findLast(Boolean);
|
|
248
|
+
const addressed = new URL(baseUrl).pathname.split("/").findLast(Boolean);
|
|
249
|
+
if (segment == null || addressed == null || connectionData?.instanceId == null) return void 0;
|
|
250
|
+
const name = decodePathSegment(segment);
|
|
251
|
+
if (name.toLowerCase() !== decodePathSegment(addressed).toLowerCase()) return void 0;
|
|
252
|
+
return {
|
|
253
|
+
id: connectionData.instanceId,
|
|
254
|
+
name
|
|
255
|
+
};
|
|
256
|
+
}
|
|
210
257
|
async getCurrentUserOnServer(provider, token, baseUrl) {
|
|
211
258
|
const scope = getScopedLogger();
|
|
212
259
|
try {
|
|
@@ -226,7 +273,11 @@ var AzureDevOpsApi = class {
|
|
|
226
273
|
}
|
|
227
274
|
}
|
|
228
275
|
async getWorkItemStateCategory(issueType, state, provider, token, owner, projectName, options) {
|
|
229
|
-
const project =
|
|
276
|
+
const project = JSON.stringify([
|
|
277
|
+
options.baseUrl,
|
|
278
|
+
owner,
|
|
279
|
+
projectName
|
|
280
|
+
]);
|
|
230
281
|
const category = this._workItemStates.getStateCategory(project, issueType, state);
|
|
231
282
|
if (category != null) return category;
|
|
232
283
|
const states = await this.retrieveWorkItemTypeStates(issueType, provider, token, owner, projectName, options);
|
|
@@ -236,7 +287,7 @@ var AzureDevOpsApi = class {
|
|
|
236
287
|
async retrieveWorkItemTypeStates(workItemType, provider, token, owner, projectName, options) {
|
|
237
288
|
const scope = getScopedLogger();
|
|
238
289
|
try {
|
|
239
|
-
return (await this.request(provider, token, options?.baseUrl, `${owner}/${projectName}/_apis/wit/workItemTypes/${workItemType}/states`, { method: "GET" }, scope))?.value ?? [];
|
|
290
|
+
return (await this.request(provider, token, options?.baseUrl, `${encodePathSegment(owner)}/${encodePathSegment(projectName)}/_apis/wit/workItemTypes/${encodePathSegment(workItemType)}/states`, { method: "GET" }, scope))?.value ?? [];
|
|
240
291
|
} catch (ex) {
|
|
241
292
|
if (ex instanceof AuthenticationError) throw ex;
|
|
242
293
|
scope?.error(ex);
|
|
@@ -279,9 +330,123 @@ var AzureDevOpsApi = class {
|
|
|
279
330
|
return;
|
|
280
331
|
}
|
|
281
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* One page of a filtered work-item search over a collection: a WIQL query for the ordered ids, then one detail
|
|
335
|
+
* read for the page's slice of them.
|
|
336
|
+
*
|
|
337
|
+
* The ids a first page queried are kept as the snapshot its continuations page through, for as long as the
|
|
338
|
+
* pagination keeps reading it, so it reads a consistent set rather than re-running a query whose rows move under
|
|
339
|
+
* an `updated` order. Each first page queries its own snapshot, so paginations of one query never share one, and
|
|
340
|
+
* a continuation whose snapshot is gone is refused (see {@link AzureWorkItemSearchCursor}).
|
|
341
|
+
*
|
|
342
|
+
* The query asks for one id more than {@link azureWorkItemSearchResultLimit}, bounded by `$top`: the extra id is
|
|
343
|
+
* what tells "exactly the limit" from "past it". Past it, the page is `truncated` with `limitReached` and no total,
|
|
344
|
+
* since the true count is unknowable here. A server that refuses the match set even so fails the page with a
|
|
345
|
+
* "narrow the search" error: no bounded query could serve that order's first window.
|
|
346
|
+
*
|
|
347
|
+
* `resolveProject` maps each result's project name to its descriptor. A work item in a project it doesn't know
|
|
348
|
+
* fails the page rather than being dropped or mis-attributed, since either would leave the page disagreeing with
|
|
349
|
+
* the count of the same query; the caller refreshes its project discovery before a search so that means a
|
|
350
|
+
* project created during the pagination.
|
|
351
|
+
*/
|
|
352
|
+
async searchWorkItemsPage(provider, token, collection, options, cancellation) {
|
|
353
|
+
const scope = getScopedLogger();
|
|
354
|
+
const wiql = toAzureWorkItemSearchWiql(options.criteria, effectiveIssueSort(options.criteria?.sort), options.projectNames);
|
|
355
|
+
const key = toAzureWorkItemSearchCursorKey(collection, wiql);
|
|
356
|
+
const pageSize = toAzureSearchPageSize(options.pageSize, 100, 200);
|
|
357
|
+
const cursor = parseAzureWorkItemSearchCursor(options.cursor, key, pageSize);
|
|
358
|
+
const page = cursor?.page ?? 1;
|
|
359
|
+
const snapshot = cursor?.snapshot ?? uuid();
|
|
360
|
+
const snapshotKey = JSON.stringify([
|
|
361
|
+
await sha256(token.accessToken),
|
|
362
|
+
options.baseUrl,
|
|
363
|
+
collection,
|
|
364
|
+
wiql,
|
|
365
|
+
snapshot
|
|
366
|
+
]);
|
|
367
|
+
const snapshotFound = cursor?.snapshot != null ? this._workItemSearches.get(snapshotKey) : void 0;
|
|
368
|
+
if (cursor?.snapshot != null && snapshotFound == null) throw new Error("Work item search results expired; restart the read without a cursor");
|
|
369
|
+
const result = await (snapshotFound ?? this._workItemSearches.getOrCreate(snapshotKey, (_cacheable, signal) => this.queryWorkItemIds(provider, token, collection, options.baseUrl, wiql, 20001, scope, signal).catch((ex) => {
|
|
370
|
+
if (isWorkItemLimitRefusal(ex)) throw new Error(`${provider.name} refused a work item search matching more than 20000 results; narrow the search`);
|
|
371
|
+
throw ex;
|
|
372
|
+
}), { cancellation }));
|
|
373
|
+
const limitReached = result.length > azureWorkItemSearchResultLimit;
|
|
374
|
+
const ids = limitReached ? result.slice(0, azureWorkItemSearchResultLimit) : result;
|
|
375
|
+
const offset = cursor?.offset ?? 0;
|
|
376
|
+
const slice = ids.slice(offset, offset + pageSize);
|
|
377
|
+
const values = [];
|
|
378
|
+
if (slice.length > 0) {
|
|
379
|
+
const workItems = await this.request(provider, token, options.baseUrl, `${encodePathSegment(collection)}/_apis/wit/workitemsbatch?api-version=5.0`, {
|
|
380
|
+
method: "POST",
|
|
381
|
+
body: JSON.stringify({
|
|
382
|
+
ids: slice,
|
|
383
|
+
$expand: "Links",
|
|
384
|
+
errorPolicy: "Omit"
|
|
385
|
+
})
|
|
386
|
+
}, scope, cancellation);
|
|
387
|
+
const byId = new Map((workItems?.value ?? []).filter((w) => w != null).map((w) => [w.id, w]));
|
|
388
|
+
for (const id of slice) {
|
|
389
|
+
const workItem = byId.get(id);
|
|
390
|
+
if (workItem == null) continue;
|
|
391
|
+
const projectName = workItem.fields["System.TeamProject"];
|
|
392
|
+
const project = options.resolveProject(projectName);
|
|
393
|
+
if (project == null) throw new Error(`Azure DevOps work item ${id} is in project '${projectName}', which isn't visible`);
|
|
394
|
+
const stateCategory = await this.getWorkItemStateCategory(workItem.fields["System.WorkItemType"], workItem.fields["System.State"], provider, token, collection, project.name, options);
|
|
395
|
+
values.push(fromAzureWorkItem(workItem, provider, project, stateCategory));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const nextOffset = offset + slice.length;
|
|
399
|
+
const hasMore = nextOffset < ids.length;
|
|
400
|
+
const next = {
|
|
401
|
+
key,
|
|
402
|
+
snapshot,
|
|
403
|
+
offset: nextOffset,
|
|
404
|
+
page: page + 1
|
|
405
|
+
};
|
|
406
|
+
return {
|
|
407
|
+
values,
|
|
408
|
+
cursor: hasMore ? JSON.stringify(next) : void 0,
|
|
409
|
+
hasMore,
|
|
410
|
+
page,
|
|
411
|
+
truncated: limitReached,
|
|
412
|
+
totalCount: limitReached ? void 0 : ids.length,
|
|
413
|
+
limitReached: limitReached || void 0
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* How many work items the same WIQL {@link searchWorkItemsPage} would page through match, reading no details.
|
|
418
|
+
*
|
|
419
|
+
* Asks for one id more than the result ceiling, so the count is exact up to it and `'exceeds-limit'` past it —
|
|
420
|
+
* never the ceiling itself, which would understate the match set.
|
|
421
|
+
*/
|
|
422
|
+
async countWorkItems(provider, token, collection, options, cancellation) {
|
|
423
|
+
const scope = getScopedLogger();
|
|
424
|
+
const wiql = toAzureWorkItemSearchWiql(options.criteria, effectiveIssueSort(options.criteria?.sort), options.projectNames);
|
|
425
|
+
try {
|
|
426
|
+
const ids = await this.queryWorkItemIds(provider, token, collection, options.baseUrl, wiql, azureWorkItemSearchResultLimit + 1, scope, cancellation);
|
|
427
|
+
return ids.length > 2e4 ? "exceeds-limit" : ids.length;
|
|
428
|
+
} catch (ex) {
|
|
429
|
+
if (isWorkItemLimitRefusal(ex)) return "exceeds-limit";
|
|
430
|
+
throw ex;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* The ids a collection-level WIQL query matches, in its order.
|
|
435
|
+
*
|
|
436
|
+
* `api-version=5.0` is the oldest version every Azure DevOps Server release this search supports (2019 and later)
|
|
437
|
+
* answers; `timePrecision=true` is what lets the criteria's dates compare as instants — see `toWiqlDate`.
|
|
438
|
+
*/
|
|
439
|
+
async queryWorkItemIds(provider, token, collection, baseUrl, wiql, top, scope, cancellation) {
|
|
440
|
+
const result = await this.request(provider, token, baseUrl, `${encodePathSegment(collection)}/_apis/wit/wiql?$top=${top}&timePrecision=true&api-version=5.0`, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
body: JSON.stringify({ query: wiql })
|
|
443
|
+
}, scope, cancellation);
|
|
444
|
+
if (result?.workItems == null) throw new Error("Azure DevOps returned no work item query result");
|
|
445
|
+
return result.workItems.map((w) => w.id);
|
|
446
|
+
}
|
|
282
447
|
getRepository(provider, token, owner, repo, baseUrl, scope, cancellation) {
|
|
283
448
|
const { projectName, repoName } = parseAzureRepositoryDescriptor(repo);
|
|
284
|
-
return this.request(provider, token, baseUrl, `${owner}/${projectName}/_apis/git/repositories/${repoName}?api-version=7.1`, { method: "GET" }, scope, cancellation);
|
|
449
|
+
return this.request(provider, token, baseUrl, `${encodePathSegment(owner)}/${encodePathSegment(projectName)}/_apis/git/repositories/${encodePathSegment(repoName)}?api-version=7.1`, { method: "GET" }, scope, cancellation);
|
|
285
450
|
}
|
|
286
451
|
async toPullRequest(pr, provider, token, owner, baseUrl, scope, cancellation) {
|
|
287
452
|
const forkRepositoryUrls = await this.getForkRepositoryUrls(provider, token, owner, baseUrl, pr.forkSource?.repository, scope, cancellation);
|
|
@@ -314,7 +479,7 @@ var AzureDevOpsApi = class {
|
|
|
314
479
|
}
|
|
315
480
|
async fetchForkRepositoryUrls(provider, token, owner, baseUrl, repositoryId, cacheable, scope, cancellation) {
|
|
316
481
|
try {
|
|
317
|
-
const response = await this.request(provider, token, baseUrl, `${owner}/_apis/git/repositories/${
|
|
482
|
+
const response = await this.request(provider, token, baseUrl, `${encodePathSegment(owner)}/_apis/git/repositories/${encodePathSegment(repositoryId)}?api-version=4.1`, { method: "GET" }, scope, cancellation);
|
|
318
483
|
const webUrl = sanitizeAzureRepositoryUrl(response?.webUrl, baseUrl, owner);
|
|
319
484
|
const cloneHttps = sanitizeAzureRepositoryUrl(response?.remoteUrl, baseUrl, owner);
|
|
320
485
|
const url = webUrl ?? cloneHttps;
|
|
@@ -430,6 +595,11 @@ __decorate([trace({ args: (provider, token, owner, repo, rev, baseUrl) => ({
|
|
|
430
595
|
rev,
|
|
431
596
|
baseUrl
|
|
432
597
|
}) })], AzureDevOpsApi.prototype, "getAccountForCommit", null);
|
|
598
|
+
__decorate([trace({ args: (provider, token, baseUrl) => ({
|
|
599
|
+
provider: provider.name,
|
|
600
|
+
token: `<token:${token.microHash}>`,
|
|
601
|
+
baseUrl
|
|
602
|
+
}) })], AzureDevOpsApi.prototype, "getAddressedCollection", null);
|
|
433
603
|
__decorate([trace({ args: (provider, token, baseUrl) => ({
|
|
434
604
|
provider: provider.name,
|
|
435
605
|
token: `<token:${token.microHash}>`,
|
|
@@ -447,6 +617,16 @@ __decorate([trace({ args: (provider, token, owner, repo) => ({
|
|
|
447
617
|
owner,
|
|
448
618
|
repo
|
|
449
619
|
}) })], AzureDevOpsApi.prototype, "getDefaultBranch", null);
|
|
620
|
+
__decorate([trace({ args: (provider, token, collection) => ({
|
|
621
|
+
provider: provider.name,
|
|
622
|
+
token: `<token:${token.microHash}>`,
|
|
623
|
+
collection
|
|
624
|
+
}) })], AzureDevOpsApi.prototype, "searchWorkItemsPage", null);
|
|
625
|
+
__decorate([trace({ args: (provider, token, collection) => ({
|
|
626
|
+
provider: provider.name,
|
|
627
|
+
token: `<token:${token.microHash}>`,
|
|
628
|
+
collection
|
|
629
|
+
}) })], AzureDevOpsApi.prototype, "countWorkItems", null);
|
|
450
630
|
/** Wires an {@link AzureDevOpsApi} from the full runtime context, mapping `ctx` down to the narrow config. */
|
|
451
631
|
function createAzureDevOpsApi(ctx) {
|
|
452
632
|
return new AzureDevOpsApi(baseProviderApiConfig(ctx));
|