1/**
2 * Azure DevOps's client definition. Browser- and React Native-safe.
3 *
4 * @module source-control-azure-devops/client/definition
5 */
6import { SourceControlProviderKind } from "@t3tools/contracts";
7import { canonicalRepositoryKey } from "@t3tools/shared/sourceControl";
8import {
9 defineSourceControlClient,
10 isChangeRequestAuthorityOfProject,
11 isChangeRequestPath,
12} from "@t3tools/source-control-core/client/definition";
13
14const CHECKOUT_COMMAND = /^az\s+repos\s+pr\s+checkout\s+(.+)$/i;
15const CHANGE_REQUEST_REFERENCE =
16 /^https:\/\/(?:dev\.azure\.com\/[^/\s]+\/[^/\s]+|[^/\s]+\.visualstudio\.com\/[^/\s]+)\/_git\/[^/\s]+\/pullrequest\/(\d+)(?:[/?#].*)?$/i;
17
18/** `az repos pr checkout` names the pull request by `--id`, `-i`, or its first plain argument. */
19function checkoutCommandId(args: string): string | null {
20 const parts = args.trim().split(/\s+/).filter(Boolean);
21 for (const [index, part] of parts.entries()) {
22 if (part === "--id" || part === "-i") return parts[index + 1] ?? null;
23 if (part.startsWith("--id=")) return part.slice("--id=".length) || null;
24 }
25 return parts.find((part) => !part.startsWith("-")) ?? null;
26}
27
28export const definition = defineSourceControlClient({
29 kind: SourceControlProviderKind.make("azure-devops"),
30 label: "Azure DevOps",
31 pickerLabel: "Azure DevOps",
32 icon: "azure-devops",
33 changeRequest: { shortLabel: "PR", singular: "pull request" },
34 repositoryPathHint: "project/repository",
35 // `az repos` reads need the checkout's organization and project, so a bare hostname names no
36 // repository this client can act on.
37 publicHost: null,
38 publishDescription: "dev.azure.com",
39 publishHost: () => "dev.azure.com",
40 // A new repository belongs to a project, which the account does not name.
41 newRepositoryOwner: () => null,
42 defaultCloneTransport: "ssh",
43 changeRequestUrl: ({ host, repository, number }) =>
44 `https://${canonicalRepositoryKey(`${host}/${repository}`.toLowerCase())}/pullrequest/${number}`,
45 changeRequestActions: new Set([
46 "merge",
47 "ready",
48 "draft",
49 "close",
50 "reopen",
51 "enable-auto-merge",
52 "disable-auto-merge",
53 ] as const),
54 checkoutCommand: ({ number }) => `az repos pr checkout --id ${number}`,
55 authorProfileUrl: () => null,
56 referenceAutolinkRepositoryUrl: () => null,
57 reviewSummaryRequired: () => false,
58 checkoutCommandArgument: (input) => {
59 const args = CHECKOUT_COMMAND.exec(input)?.[1];
60 return args ? checkoutCommandId(args) : null;
61 },
62 isChangeRequestReference: (url) => CHANGE_REQUEST_REFERENCE.test(url),
63 changeRequestUrlHost: (url) => url.hostname,
64 checkoutChangeRequestHost: () => null,
65 // The legacy and SSH spellings of one repository all canonicalise to dev.azure.com.
66 isChangeRequestInRepository: (identity, link) =>
67 isChangeRequestAuthorityOfProject(identity, link) &&
68 canonicalRepositoryKey(identity.canonicalKey.toLowerCase()) ===
69 canonicalRepositoryKey(`${link.host}/${link.repository}`.toLowerCase()),
70 // `az repos` reads use the checkout's organization and project, not host-wide credentials.
71 canReadChangeRequestOnHost: () => false,
72 isChangeRequestUrl: (url) => isChangeRequestPath(url, "/pullrequest/"),
73});