import type { GitBranch, GitHostingProvider, GitStatusLocalResult, GitStatusRemoteResult, GitStatusResult, GitStatusStreamEvent,} from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Random from "effect/Random"; export const WORKTREE_BRANCH_PREFIX = "t3code";const TEMP_WORKTREE_BRANCH_PATTERN = new RegExp(`^${WORKTREE_BRANCH_PREFIX}\\/[0-9a-f]{8}$`); /** * 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);} export function buildTemporaryWorktreeBranchName(): string { const token = Effect.runSync(Random.nextUUIDv4).replace(/-/g, "").slice(0, 8).toLowerCase(); return `${WORKTREE_BRANCH_PREFIX}/${token}`;} export function isTemporaryWorktreeBranch(branch: string): boolean { return TEMP_WORKTREE_BRANCH_PATTERN.test(branch.trim().toLowerCase());} /** * Normalize a git remote URL into a stable comparison key. */export function normalizeGitRemoteUrl(value: string): string { const normalized = value .trim() .replace(/\/+$/g, "") .replace(/\.git$/i, "") .toLowerCase(); if (/^(?:ssh|https?|git):\/\//i.test(normalized)) { try { const url = new URL(normalized); const repositoryPath = url.pathname .split("/") .filter((segment) => segment.length > 0) .join("/"); if (url.hostname && repositoryPath.includes("/")) { return `${url.hostname}/${repositoryPath}`; } } catch { return normalized; } } const scpStyleHostAndPath = /^git@([^:/\s]+)[:/]([^/\s]+(?:\/[^/\s]+)+)$/i.exec(normalized); if (scpStyleHostAndPath?.[1] && scpStyleHostAndPath[2]) { return `${scpStyleHostAndPath[1]}/${scpStyleHostAndPath[2]}`; } return normalized;} /** * Best-effort parse of a GitHub `owner/repo` identifier from common remote URL shapes. */export function parseGitHubRepositoryNameWithOwnerFromRemoteUrl(url: string | null): string | null { const trimmed = url?.trim() ?? ""; if (trimmed.length === 0) { return null; } const match = /^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/github\.com\/|git:\/\/github\.com\/)([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i.exec( trimmed, ); const repositoryNameWithOwner = match?.[1]?.trim() ?? ""; return repositoryNameWithOwner.length > 0 ? repositoryNameWithOwner : null;} 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)); });} function parseGitRemoteHost(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).hostname.toLowerCase(); } catch { return null; }} 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");} export function detectGitHostingProviderFromRemoteUrl( remoteUrl: string,): GitHostingProvider | null { const host = parseGitRemoteHost(remoteUrl); if (!host) { return null; } if (isGitHubHost(host)) { return { kind: "github", name: host === "github.com" ? "GitHub" : "GitHub Self-Hosted", baseUrl: toBaseUrl(host), }; } if (isGitLabHost(host)) { return { kind: "gitlab", name: host === "gitlab.com" ? "GitLab" : "GitLab Self-Hosted", baseUrl: toBaseUrl(host), }; } return { kind: "unknown", name: host, baseUrl: toBaseUrl(host), };} const EMPTY_GIT_STATUS_REMOTE: GitStatusRemoteResult = { hasUpstream: false, aheadCount: 0, behindCount: 0, pr: null,}; export function mergeGitStatusParts( local: GitStatusLocalResult, remote: GitStatusRemoteResult | null,): GitStatusResult { return { ...local, ...(remote ?? EMPTY_GIT_STATUS_REMOTE), };} function toRemoteStatusPart(status: GitStatusResult): GitStatusRemoteResult { return { hasUpstream: status.hasUpstream, aheadCount: status.aheadCount, behindCount: status.behindCount, pr: status.pr, };} function toLocalStatusPart(status: GitStatusResult): GitStatusLocalResult { return { isRepo: status.isRepo, ...(status.hostingProvider ? { hostingProvider: status.hostingProvider } : {}), hasOriginRemote: status.hasOriginRemote, isDefaultBranch: status.isDefaultBranch, branch: status.branch, hasWorkingTreeChanges: status.hasWorkingTreeChanges, workingTree: status.workingTree, };} export function applyGitStatusStreamEvent( current: GitStatusResult | null, event: GitStatusStreamEvent,): GitStatusResult { switch (event._tag) { case "snapshot": return mergeGitStatusParts(event.local, event.remote); case "localUpdated": return mergeGitStatusParts(event.local, current ? toRemoteStatusPart(current) : null); case "remoteUpdated": if (current === null) { return mergeGitStatusParts( { isRepo: true, hasOriginRemote: false, isDefaultBranch: false, branch: null, hasWorkingTreeChanges: false, workingTree: { files: [], insertions: 0, deletions: 0 }, }, event.remote, ); } return mergeGitStatusParts(toLocalStatusPart(current), event.remote); }}
import type {
GitBranch,
GitHostingProvider,
GitStatusLocalResult,
GitStatusRemoteResult,
GitStatusResult,
GitStatusStreamEvent,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Random from "effect/Random";
export const WORKTREE_BRANCH_PREFIX = "t3code";
const TEMP_WORKTREE_BRANCH_PATTERN = new RegExp(`^${WORKTREE_BRANCH_PREFIX}\\/[0-9a-f]{8}$`);
/**
* 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);
}
export function buildTemporaryWorktreeBranchName(): string {
const token = Effect.runSync(Random.nextUUIDv4).replace(/-/g, "").slice(0, 8).toLowerCase();
return `${WORKTREE_BRANCH_PREFIX}/${token}`;
}
export function isTemporaryWorktreeBranch(branch: string): boolean {
return TEMP_WORKTREE_BRANCH_PATTERN.test(branch.trim().toLowerCase());
}
/**
* Normalize a git remote URL into a stable comparison key.
*/
export function normalizeGitRemoteUrl(value: string): string {
const normalized = value
.trim()
.replace(/\/+$/g, "")
.replace(/\.git$/i, "")
.toLowerCase();
if (/^(?:ssh|https?|git):\/\//i.test(normalized)) {
try {
const url = new URL(normalized);
const repositoryPath = url.pathname
.split("/")
.filter((segment) => segment.length > 0)
.join("/");
if (url.hostname && repositoryPath.includes("/")) {
return `${url.hostname}/${repositoryPath}`;
}
} catch {
return normalized;
}
}
const scpStyleHostAndPath = /^git@([^:/\s]+)[:/]([^/\s]+(?:\/[^/\s]+)+)$/i.exec(normalized);
if (scpStyleHostAndPath?.[1] && scpStyleHostAndPath[2]) {
return `${scpStyleHostAndPath[1]}/${scpStyleHostAndPath[2]}`;
}
return normalized;
}
/**
* Best-effort parse of a GitHub `owner/repo` identifier from common remote URL shapes.
*/
export function parseGitHubRepositoryNameWithOwnerFromRemoteUrl(url: string | null): string | null {
const trimmed = url?.trim() ?? "";
if (trimmed.length === 0) {
return null;
}
const match =
/^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/github\.com\/|git:\/\/github\.com\/)([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i.exec(
trimmed,
);
const repositoryNameWithOwner = match?.[1]?.trim() ?? "";
return repositoryNameWithOwner.length > 0 ? repositoryNameWithOwner : null;
}
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));
});
}
function parseGitRemoteHost(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).hostname.toLowerCase();
} catch {
return null;
}
}
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");
}
export function detectGitHostingProviderFromRemoteUrl(
remoteUrl: string,
): GitHostingProvider | null {
const host = parseGitRemoteHost(remoteUrl);
if (!host) {
return null;
}
if (isGitHubHost(host)) {
return {
kind: "github",
name: host === "github.com" ? "GitHub" : "GitHub Self-Hosted",
baseUrl: toBaseUrl(host),
};
}
if (isGitLabHost(host)) {
return {
kind: "gitlab",
name: host === "gitlab.com" ? "GitLab" : "GitLab Self-Hosted",
baseUrl: toBaseUrl(host),
};
}
return {
kind: "unknown",
name: host,
baseUrl: toBaseUrl(host),
};
}
const EMPTY_GIT_STATUS_REMOTE: GitStatusRemoteResult = {
hasUpstream: false,
aheadCount: 0,
behindCount: 0,
pr: null,
};
export function mergeGitStatusParts(
local: GitStatusLocalResult,
remote: GitStatusRemoteResult | null,
): GitStatusResult {
return {
...local,
...(remote ?? EMPTY_GIT_STATUS_REMOTE),
};
}
function toRemoteStatusPart(status: GitStatusResult): GitStatusRemoteResult {
return {
hasUpstream: status.hasUpstream,
aheadCount: status.aheadCount,
behindCount: status.behindCount,
pr: status.pr,
};
}
function toLocalStatusPart(status: GitStatusResult): GitStatusLocalResult {
return {
isRepo: status.isRepo,
...(status.hostingProvider ? { hostingProvider: status.hostingProvider } : {}),
hasOriginRemote: status.hasOriginRemote,
isDefaultBranch: status.isDefaultBranch,
branch: status.branch,
hasWorkingTreeChanges: status.hasWorkingTreeChanges,
workingTree: status.workingTree,
};
}
export function applyGitStatusStreamEvent(
current: GitStatusResult | null,
event: GitStatusStreamEvent,
): GitStatusResult {
switch (event._tag) {
case "snapshot":
return mergeGitStatusParts(event.local, event.remote);
case "localUpdated":
return mergeGitStatusParts(event.local, current ? toRemoteStatusPart(current) : null);
case "remoteUpdated":
if (current === null) {
return mergeGitStatusParts(
{
isRepo: true,
hasOriginRemote: false,
isDefaultBranch: false,
branch: null,
hasWorkingTreeChanges: false,
workingTree: { files: [], insertions: 0, deletions: 0 },
},
event.remote,
);
}
return mergeGitStatusParts(toLocalStatusPart(current), event.remote);
}
}