import type { SourceControlProviderInfo, SourceControlProviderKind } from "@t3tools/contracts"; export interface ChangeRequestPresentation { readonly icon: "github" | "gitlab" | "azure-devops" | "bitbucket" | "change-request"; readonly providerName: string; readonly shortName: string; readonly longName: string; readonly pluralLongName: string; readonly providerLongName: string; readonly checkoutCommandExample?: string; readonly urlExample: string;} export interface ChangeRequestTerminology { readonly shortLabel: string; readonly singular: string;} export const DEFAULT_CHANGE_REQUEST_TERMINOLOGY: ChangeRequestTerminology = { shortLabel: "PR", singular: "pull request",}; const GITHUB_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = { icon: "github", providerName: "GitHub", shortName: "PR", longName: "pull request", pluralLongName: "pull requests", providerLongName: "GitHub pull request", checkoutCommandExample: "gh pr checkout 123", urlExample: "https://github.com/owner/repo/pull/42",}; const GITLAB_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = { icon: "gitlab", providerName: "GitLab", shortName: "MR", longName: "merge request", pluralLongName: "merge requests", providerLongName: "GitLab merge request", checkoutCommandExample: "glab mr checkout 123", urlExample: "https://gitlab.com/group/project/-/merge_requests/42",}; const AZURE_DEVOPS_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = { icon: "azure-devops", providerName: "Azure DevOps", shortName: "PR", longName: "pull request", pluralLongName: "pull requests", providerLongName: "Azure DevOps pull request", checkoutCommandExample: "az repos pr checkout --id 123", urlExample: "https://dev.azure.com/org/project/_git/repo/pullrequest/42",}; const BITBUCKET_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = { icon: "bitbucket", providerName: "Bitbucket", shortName: "PR", longName: "pull request", pluralLongName: "pull requests", providerLongName: "Bitbucket pull request", urlExample: "https://bitbucket.org/workspace/repo/pull-requests/42",}; const GENERIC_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = { icon: "change-request", providerName: "source control", shortName: "change request", longName: "change request", pluralLongName: "change requests", providerLongName: "change request", urlExample: "#42",}; export function resolveChangeRequestPresentation( provider: SourceControlProviderInfo | null | undefined,): ChangeRequestPresentation { switch (provider?.kind) { case "github": case undefined: return GITHUB_CHANGE_REQUEST_PRESENTATION; case "gitlab": return GITLAB_CHANGE_REQUEST_PRESENTATION; case "azure-devops": return AZURE_DEVOPS_CHANGE_REQUEST_PRESENTATION; case "bitbucket": return BITBUCKET_CHANGE_REQUEST_PRESENTATION; case "unknown": return GENERIC_CHANGE_REQUEST_PRESENTATION; }} export function resolveChangeRequestPresentationForKind( kind: SourceControlProviderKind,): ChangeRequestPresentation { return resolveChangeRequestPresentation({ kind, name: "", baseUrl: "" });} export function formatChangeRequestAction( verb: "View" | "Create", presentation: ChangeRequestPresentation,): string { return `${verb} ${presentation.shortName}`;} export function formatCreateChangeRequestPhrase(presentation: ChangeRequestPresentation): string { return `create ${presentation.shortName}`;} export function getChangeRequestTerminology( provider: SourceControlProviderInfo | null | undefined,): ChangeRequestTerminology { if (!provider) { return DEFAULT_CHANGE_REQUEST_TERMINOLOGY; } const presentation = resolveChangeRequestPresentation(provider); return { shortLabel: presentation.shortName, singular: presentation.longName, };} export function getChangeRequestTerminologyForKind( kind: SourceControlProviderKind,): ChangeRequestTerminology { const presentation = resolveChangeRequestPresentationForKind(kind); return { shortLabel: presentation.shortName, singular: presentation.longName, };} function parseRemoteHost(remoteUrl: string): string | null { const trimmed = remoteUrl.trim(); if (trimmed.length === 0) { return null; } if (trimmed.startsWith("git@")) { const hostWithPath = trimmed.slice("git@".length); const separatorIndex = hostWithPath.search(/[:/]/); if (separatorIndex <= 0) { return null; } return hostWithPath.slice(0, separatorIndex).toLowerCase(); } try { return new URL(trimmed).host.toLowerCase(); } catch { return null; }} function parseHostName(host: string): string { try { return new URL(`https://${host}`).hostname.toLowerCase(); } catch { return host.replace(/:\d+$/u, "").toLowerCase(); }} function toBaseUrl(host: string): string { return `https://${host}`;} function isGitHubHost(host: string): boolean { return host === "github.com" || host.includes("github");} function isGitLabHost(host: string): boolean { return host === "gitlab.com" || host.includes("gitlab");} function isAzureDevOpsHost(host: string): boolean { return host === "dev.azure.com" || host.endsWith(".visualstudio.com");} function isBitbucketHost(host: string): boolean { return host === "bitbucket.org" || host.includes("bitbucket");} export function detectSourceControlProviderFromRemoteUrl( remoteUrl: string,): SourceControlProviderInfo | null { const host = parseRemoteHost(remoteUrl); if (!host) { return null; } const hostname = parseHostName(host); if (isGitHubHost(hostname)) { return { kind: "github", name: hostname === "github.com" ? "GitHub" : "GitHub Self-Hosted", baseUrl: toBaseUrl(host), }; } if (isGitLabHost(hostname)) { return { kind: "gitlab", name: hostname === "gitlab.com" ? "GitLab" : "GitLab Self-Hosted", baseUrl: toBaseUrl(host), }; } if (isAzureDevOpsHost(hostname)) { return { kind: "azure-devops", name: "Azure DevOps", baseUrl: toBaseUrl(host), }; } if (isBitbucketHost(hostname)) { return { kind: "bitbucket", name: hostname === "bitbucket.org" ? "Bitbucket" : "Bitbucket Self-Hosted", baseUrl: toBaseUrl(host), }; } return { kind: "unknown", name: host, baseUrl: toBaseUrl(host), };}
import type { SourceControlProviderInfo, SourceControlProviderKind } from "@t3tools/contracts";
export interface ChangeRequestPresentation {
readonly icon: "github" | "gitlab" | "azure-devops" | "bitbucket" | "change-request";
readonly providerName: string;
readonly shortName: string;
readonly longName: string;
readonly pluralLongName: string;
readonly providerLongName: string;
readonly checkoutCommandExample?: string;
readonly urlExample: string;
}
export interface ChangeRequestTerminology {
readonly shortLabel: string;
readonly singular: string;
}
export const DEFAULT_CHANGE_REQUEST_TERMINOLOGY: ChangeRequestTerminology = {
shortLabel: "PR",
singular: "pull request",
};
const GITHUB_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = {
icon: "github",
providerName: "GitHub",
shortName: "PR",
longName: "pull request",
pluralLongName: "pull requests",
providerLongName: "GitHub pull request",
checkoutCommandExample: "gh pr checkout 123",
urlExample: "https://github.com/owner/repo/pull/42",
};
const GITLAB_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = {
icon: "gitlab",
providerName: "GitLab",
shortName: "MR",
longName: "merge request",
pluralLongName: "merge requests",
providerLongName: "GitLab merge request",
checkoutCommandExample: "glab mr checkout 123",
urlExample: "https://gitlab.com/group/project/-/merge_requests/42",
};
const AZURE_DEVOPS_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = {
icon: "azure-devops",
providerName: "Azure DevOps",
shortName: "PR",
longName: "pull request",
pluralLongName: "pull requests",
providerLongName: "Azure DevOps pull request",
checkoutCommandExample: "az repos pr checkout --id 123",
urlExample: "https://dev.azure.com/org/project/_git/repo/pullrequest/42",
};
const BITBUCKET_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = {
icon: "bitbucket",
providerName: "Bitbucket",
shortName: "PR",
longName: "pull request",
pluralLongName: "pull requests",
providerLongName: "Bitbucket pull request",
urlExample: "https://bitbucket.org/workspace/repo/pull-requests/42",
};
const GENERIC_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = {
icon: "change-request",
providerName: "source control",
shortName: "change request",
longName: "change request",
pluralLongName: "change requests",
providerLongName: "change request",
urlExample: "#42",
};
export function resolveChangeRequestPresentation(
provider: SourceControlProviderInfo | null | undefined,
): ChangeRequestPresentation {
switch (provider?.kind) {
case "github":
case undefined:
return GITHUB_CHANGE_REQUEST_PRESENTATION;
case "gitlab":
return GITLAB_CHANGE_REQUEST_PRESENTATION;
case "azure-devops":
return AZURE_DEVOPS_CHANGE_REQUEST_PRESENTATION;
case "bitbucket":
return BITBUCKET_CHANGE_REQUEST_PRESENTATION;
case "unknown":
return GENERIC_CHANGE_REQUEST_PRESENTATION;
}
}
export function resolveChangeRequestPresentationForKind(
kind: SourceControlProviderKind,
): ChangeRequestPresentation {
return resolveChangeRequestPresentation({ kind, name: "", baseUrl: "" });
}
export function formatChangeRequestAction(
verb: "View" | "Create",
presentation: ChangeRequestPresentation,
): string {
return `${verb} ${presentation.shortName}`;
}
export function formatCreateChangeRequestPhrase(presentation: ChangeRequestPresentation): string {
return `create ${presentation.shortName}`;
}
export function getChangeRequestTerminology(
provider: SourceControlProviderInfo | null | undefined,
): ChangeRequestTerminology {
if (!provider) {
return DEFAULT_CHANGE_REQUEST_TERMINOLOGY;
}
const presentation = resolveChangeRequestPresentation(provider);
return {
shortLabel: presentation.shortName,
singular: presentation.longName,
};
}
export function getChangeRequestTerminologyForKind(
kind: SourceControlProviderKind,
): ChangeRequestTerminology {
const presentation = resolveChangeRequestPresentationForKind(kind);
return {
shortLabel: presentation.shortName,
singular: presentation.longName,
};
}
function parseRemoteHost(remoteUrl: string): string | null {
const trimmed = remoteUrl.trim();
if (trimmed.length === 0) {
return null;
}
if (trimmed.startsWith("git@")) {
const hostWithPath = trimmed.slice("git@".length);
const separatorIndex = hostWithPath.search(/[:/]/);
if (separatorIndex <= 0) {
return null;
}
return hostWithPath.slice(0, separatorIndex).toLowerCase();
}
try {
return new URL(trimmed).host.toLowerCase();
} catch {
return null;
}
}
function parseHostName(host: string): string {
try {
return new URL(`https://${host}`).hostname.toLowerCase();
} catch {
return host.replace(/:\d+$/u, "").toLowerCase();
}
}
function toBaseUrl(host: string): string {
return `https://${host}`;
}
function isGitHubHost(host: string): boolean {
return host === "github.com" || host.includes("github");
}
function isGitLabHost(host: string): boolean {
return host === "gitlab.com" || host.includes("gitlab");
}
function isAzureDevOpsHost(host: string): boolean {
return host === "dev.azure.com" || host.endsWith(".visualstudio.com");
}
function isBitbucketHost(host: string): boolean {
return host === "bitbucket.org" || host.includes("bitbucket");
}
export function detectSourceControlProviderFromRemoteUrl(
remoteUrl: string,
): SourceControlProviderInfo | null {
const host = parseRemoteHost(remoteUrl);
if (!host) {
return null;
}
const hostname = parseHostName(host);
if (isGitHubHost(hostname)) {
return {
kind: "github",
name: hostname === "github.com" ? "GitHub" : "GitHub Self-Hosted",
baseUrl: toBaseUrl(host),
};
}
if (isGitLabHost(hostname)) {
return {
kind: "gitlab",
name: hostname === "gitlab.com" ? "GitLab" : "GitLab Self-Hosted",
baseUrl: toBaseUrl(host),
};
}
if (isAzureDevOpsHost(hostname)) {
return {
kind: "azure-devops",
name: "Azure DevOps",
baseUrl: toBaseUrl(host),
};
}
if (isBitbucketHost(hostname)) {
return {
kind: "bitbucket",
name: hostname === "bitbucket.org" ? "Bitbucket" : "Bitbucket Self-Hosted",
baseUrl: toBaseUrl(host),
};
}
return {
kind: "unknown",
name: host,
baseUrl: toBaseUrl(host),
};
}