121 lines · 3.5 KB · TypeScript
RawDownload
import type { GitBranch } from "@t3tools/contracts"; /** * Sanitize an arbitrary string into a valid, lowercase git branch fragment. * Strips quotes, collapses separators, limits to 64 chars. */export function sanitizeBranchFragment(raw: string): string {  const normalized = raw    .trim()    .toLowerCase()    .replace(/['"`]/g, "")    .replace(/^[./\s_-]+|[./\s_-]+$/g, "");   const branchFragment = normalized    .replace(/[^a-z0-9/_-]+/g, "-")    .replace(/\/+/g, "/")    .replace(/-+/g, "-")    .replace(/^[./_-]+|[./_-]+$/g, "")    .slice(0, 64)    .replace(/[./_-]+$/g, "");   return branchFragment.length > 0 ? branchFragment : "update";} /** * Sanitize a string into a `feature/…` branch name. * Preserves an existing `feature/` prefix or slash-separated namespace. */export function sanitizeFeatureBranchName(raw: string): string {  const sanitized = sanitizeBranchFragment(raw);  if (sanitized.includes("/")) {    return sanitized.startsWith("feature/") ? sanitized : `feature/${sanitized}`;  }  return `feature/${sanitized}`;} const AUTO_FEATURE_BRANCH_FALLBACK = "feature/update"; /** * Resolve a unique `feature/…` branch name that doesn't collide with * any existing branch. Appends a numeric suffix when needed. */export function resolveAutoFeatureBranchName(  existingBranchNames: readonly string[],  preferredBranch?: string,): string {  const preferred = preferredBranch?.trim();  const resolvedBase = sanitizeFeatureBranchName(    preferred && preferred.length > 0 ? preferred : AUTO_FEATURE_BRANCH_FALLBACK,  );  const existingNames = new Set(existingBranchNames.map((branch) => branch.toLowerCase()));   if (!existingNames.has(resolvedBase)) {    return resolvedBase;  }   let suffix = 2;  while (existingNames.has(`${resolvedBase}-${suffix}`)) {    suffix += 1;  }   return `${resolvedBase}-${suffix}`;} /** * Strip the remote prefix from a remote ref such as `origin/feature/demo`. */export function deriveLocalBranchNameFromRemoteRef(branchName: string): string {  const firstSeparatorIndex = branchName.indexOf("/");  if (firstSeparatorIndex <= 0 || firstSeparatorIndex === branchName.length - 1) {    return branchName;  }  return branchName.slice(firstSeparatorIndex + 1);} function deriveLocalBranchNameCandidatesFromRemoteRef(  branchName: string,  remoteName?: string,): ReadonlyArray<string> {  const candidates = new Set<string>();  const firstSlashCandidate = deriveLocalBranchNameFromRemoteRef(branchName);  if (firstSlashCandidate.length > 0) {    candidates.add(firstSlashCandidate);  }   if (remoteName) {    const remotePrefix = `${remoteName}/`;    if (branchName.startsWith(remotePrefix) && branchName.length > remotePrefix.length) {      candidates.add(branchName.slice(remotePrefix.length));    }  }   return [...candidates];} /** * Hide `origin/*` remote refs when a matching local branch already exists. */export function dedupeRemoteBranchesWithLocalMatches(  branches: ReadonlyArray<GitBranch>,): ReadonlyArray<GitBranch> {  const localBranchNames = new Set(    branches.filter((branch) => !branch.isRemote).map((branch) => branch.name),  );   return branches.filter((branch) => {    if (!branch.isRemote) {      return true;    }     if (branch.remoteName !== "origin") {      return true;    }     const localBranchCandidates = deriveLocalBranchNameCandidatesFromRemoteRef(      branch.name,      branch.remoteName,    );    return !localBranchCandidates.some((candidate) => localBranchNames.has(candidate));  });}