1084 lines · 34.1 KB · TypeScript
RawDownload
import { randomUUID } from "node:crypto";import { realpathSync } from "node:fs"; import { Effect, FileSystem, Layer, Path } from "effect";import {  resolveAutoFeatureBranchName,  sanitizeBranchFragment,  sanitizeFeatureBranchName,} from "@t3tools/shared/git"; import { GitManagerError } from "../Errors.ts";import { GitManager, type GitManagerShape } from "../Services/GitManager.ts";import { GitCore } from "../Services/GitCore.ts";import { GitHubCli } from "../Services/GitHubCli.ts";import { TextGeneration } from "../Services/TextGeneration.ts"; interface OpenPrInfo {  number: number;  title: string;  url: string;  baseRefName: string;  headRefName: string;} interface PullRequestInfo extends OpenPrInfo {  state: "open" | "closed" | "merged";  updatedAt: string | null;} interface ResolvedPullRequest {  number: number;  title: string;  url: string;  baseBranch: string;  headBranch: string;  state: "open" | "closed" | "merged";} interface PullRequestHeadRemoteInfo {  isCrossRepository?: boolean;  headRepositoryNameWithOwner?: string | null;  headRepositoryOwnerLogin?: string | null;} interface BranchHeadContext {  localBranch: string;  headBranch: string;  headSelectors: ReadonlyArray<string>;  preferredHeadSelector: string;  remoteName: string | null;  headRepositoryNameWithOwner: string | null;  headRepositoryOwnerLogin: string | null;  isCrossRepository: boolean;} function parseRepositoryNameFromPullRequestUrl(url: string): string | null {  const trimmed = url.trim();  const match = /^https:\/\/github\.com\/[^/]+\/([^/]+)\/pull\/\d+(?:\/.*)?$/i.exec(trimmed);  const repositoryName = match?.[1]?.trim() ?? "";  return repositoryName.length > 0 ? repositoryName : null;} function resolveHeadRepositoryNameWithOwner(  pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo,): string | null {  const explicitRepository = pullRequest.headRepositoryNameWithOwner?.trim() ?? "";  if (explicitRepository.length > 0) {    return explicitRepository;  }   if (!pullRequest.isCrossRepository) {    return null;  }   const ownerLogin = pullRequest.headRepositoryOwnerLogin?.trim() ?? "";  const repositoryName = parseRepositoryNameFromPullRequestUrl(pullRequest.url);  if (ownerLogin.length === 0 || !repositoryName) {    return null;  }   return `${ownerLogin}/${repositoryName}`;} function resolvePullRequestWorktreeLocalBranchName(  pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo,): string {  if (!pullRequest.isCrossRepository) {    return pullRequest.headBranch;  }   const sanitizedHeadBranch = sanitizeBranchFragment(pullRequest.headBranch).trim();  const suffix = sanitizedHeadBranch.length > 0 ? sanitizedHeadBranch : "head";  return `t3code/pr-${pullRequest.number}/${suffix}`;} 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 parseRepositoryOwnerLogin(nameWithOwner: string | null): string | null {  const trimmed = nameWithOwner?.trim() ?? "";  if (trimmed.length === 0) {    return null;  }  const [ownerLogin] = trimmed.split("/");  const normalizedOwnerLogin = ownerLogin?.trim() ?? "";  return normalizedOwnerLogin.length > 0 ? normalizedOwnerLogin : null;} function parsePullRequestList(raw: unknown): PullRequestInfo[] {  if (!Array.isArray(raw)) return [];   const parsed: PullRequestInfo[] = [];  for (const entry of raw) {    if (!entry || typeof entry !== "object") continue;    const record = entry as Record<string, unknown>;    const number = record.number;    const title = record.title;    const url = record.url;    const baseRefName = record.baseRefName;    const headRefName = record.headRefName;    const state = record.state;    const mergedAt = record.mergedAt;    const updatedAt = record.updatedAt;    if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) {      continue;    }    if (      typeof title !== "string" ||      typeof url !== "string" ||      typeof baseRefName !== "string" ||      typeof headRefName !== "string"    ) {      continue;    }     let normalizedState: "open" | "closed" | "merged";    if ((typeof mergedAt === "string" && mergedAt.trim().length > 0) || state === "MERGED") {      normalizedState = "merged";    } else if (state === "OPEN" || state === undefined || state === null) {      normalizedState = "open";    } else if (state === "CLOSED") {      normalizedState = "closed";    } else {      continue;    }     parsed.push({      number,      title,      url,      baseRefName,      headRefName,      state: normalizedState,      updatedAt: typeof updatedAt === "string" && updatedAt.trim().length > 0 ? updatedAt : null,    });  }  return parsed;} function gitManagerError(operation: string, detail: string, cause?: unknown): GitManagerError {  return new GitManagerError({    operation,    detail,    ...(cause !== undefined ? { cause } : {}),  });} function limitContext(value: string, maxChars: number): string {  if (value.length <= maxChars) return value;  return `${value.slice(0, maxChars)}\n\n[truncated]`;} function sanitizeCommitMessage(generated: {  subject: string;  body: string;  branch?: string | undefined;}): {  subject: string;  body: string;  branch?: string | undefined;} {  const rawSubject = generated.subject.trim().split(/\r?\n/g)[0]?.trim() ?? "";  const subject = rawSubject.replace(/[.]+$/g, "").trim();  const safeSubject = subject.length > 0 ? subject.slice(0, 72).trimEnd() : "Update project files";  return {    subject: safeSubject,    body: generated.body.trim(),    ...(generated.branch !== undefined ? { branch: generated.branch } : {}),  };} interface CommitAndBranchSuggestion {  subject: string;  body: string;  branch?: string | undefined;  commitMessage: string;} function formatCommitMessage(subject: string, body: string): string {  const trimmedBody = body.trim();  if (trimmedBody.length === 0) {    return subject;  }  return `${subject}\n\n${trimmedBody}`;} function parseCustomCommitMessage(raw: string): { subject: string; body: string } | null {  const normalized = raw.replace(/\r\n/g, "\n").trim();  if (normalized.length === 0) {    return null;  }   const [firstLine, ...rest] = normalized.split("\n");  const subject = firstLine?.trim() ?? "";  if (subject.length === 0) {    return null;  }   return {    subject,    body: rest.join("\n").trim(),  };} function extractBranchFromRef(ref: string): string {  const normalized = ref.trim();   if (normalized.startsWith("refs/remotes/")) {    const withoutPrefix = normalized.slice("refs/remotes/".length);    const firstSlash = withoutPrefix.indexOf("/");    if (firstSlash === -1) {      return withoutPrefix.trim();    }    return withoutPrefix.slice(firstSlash + 1).trim();  }   const firstSlash = normalized.indexOf("/");  if (firstSlash === -1) {    return normalized;  }  return normalized.slice(firstSlash + 1).trim();} function appendUnique(values: string[], next: string | null | undefined): void {  const trimmed = next?.trim() ?? "";  if (trimmed.length === 0 || values.includes(trimmed)) {    return;  }  values.push(trimmed);} function toStatusPr(pr: PullRequestInfo): {  number: number;  title: string;  url: string;  baseBranch: string;  headBranch: string;  state: "open" | "closed" | "merged";} {  return {    number: pr.number,    title: pr.title,    url: pr.url,    baseBranch: pr.baseRefName,    headBranch: pr.headRefName,    state: pr.state,  };} function normalizePullRequestReference(reference: string): string {  const trimmed = reference.trim();  const hashNumber = /^#(\d+)$/.exec(trimmed);  return hashNumber?.[1] ?? trimmed;} function canonicalizeExistingPath(value: string): string {  try {    return realpathSync.native(value);  } catch {    return value;  }} function toResolvedPullRequest(pr: {  number: number;  title: string;  url: string;  baseRefName: string;  headRefName: string;  state?: "open" | "closed" | "merged";}): ResolvedPullRequest {  return {    number: pr.number,    title: pr.title,    url: pr.url,    baseBranch: pr.baseRefName,    headBranch: pr.headRefName,    state: pr.state ?? "open",  };} function shouldPreferSshRemote(url: string | null): boolean {  if (!url) return false;  const trimmed = url.trim();  return trimmed.startsWith("git@") || trimmed.startsWith("ssh://");} function toPullRequestHeadRemoteInfo(pr: {  isCrossRepository?: boolean;  headRepositoryNameWithOwner?: string | null;  headRepositoryOwnerLogin?: string | null;}): PullRequestHeadRemoteInfo {  return {    ...(pr.isCrossRepository !== undefined ? { isCrossRepository: pr.isCrossRepository } : {}),    ...(pr.headRepositoryNameWithOwner !== undefined      ? { headRepositoryNameWithOwner: pr.headRepositoryNameWithOwner }      : {}),    ...(pr.headRepositoryOwnerLogin !== undefined      ? { headRepositoryOwnerLogin: pr.headRepositoryOwnerLogin }      : {}),  };} export const makeGitManager = Effect.gen(function* () {  const gitCore = yield* GitCore;  const gitHubCli = yield* GitHubCli;  const textGeneration = yield* TextGeneration;   const configurePullRequestHeadUpstream = (    cwd: string,    pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo,    localBranch = pullRequest.headBranch,  ) =>    Effect.gen(function* () {      const repositoryNameWithOwner = resolveHeadRepositoryNameWithOwner(pullRequest) ?? "";      if (repositoryNameWithOwner.length === 0) {        return;      }       const cloneUrls = yield* gitHubCli.getRepositoryCloneUrls({        cwd,        repository: repositoryNameWithOwner,      });      const originRemoteUrl = yield* gitCore.readConfigValue(cwd, "remote.origin.url");      const remoteUrl = shouldPreferSshRemote(originRemoteUrl) ? cloneUrls.sshUrl : cloneUrls.url;      const preferredRemoteName =        pullRequest.headRepositoryOwnerLogin?.trim() ||        repositoryNameWithOwner.split("/")[0]?.trim() ||        "fork";      const remoteName = yield* gitCore.ensureRemote({        cwd,        preferredName: preferredRemoteName,        url: remoteUrl,      });       yield* gitCore.setBranchUpstream({        cwd,        branch: localBranch,        remoteName,        remoteBranch: pullRequest.headBranch,      });    }).pipe(      Effect.catch((error) =>        Effect.logWarning(          `GitManager.configurePullRequestHeadUpstream: failed to configure upstream for ${localBranch} -> ${pullRequest.headBranch} in ${cwd}: ${error.message}`,        ).pipe(Effect.asVoid),      ),    );   const materializePullRequestHeadBranch = (    cwd: string,    pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo,    localBranch = pullRequest.headBranch,  ) =>    Effect.gen(function* () {      const repositoryNameWithOwner = resolveHeadRepositoryNameWithOwner(pullRequest) ?? "";       if (repositoryNameWithOwner.length === 0) {        yield* gitCore.fetchPullRequestBranch({          cwd,          prNumber: pullRequest.number,          branch: localBranch,        });        return;      }       const cloneUrls = yield* gitHubCli.getRepositoryCloneUrls({        cwd,        repository: repositoryNameWithOwner,      });      const originRemoteUrl = yield* gitCore.readConfigValue(cwd, "remote.origin.url");      const remoteUrl = shouldPreferSshRemote(originRemoteUrl) ? cloneUrls.sshUrl : cloneUrls.url;      const preferredRemoteName =        pullRequest.headRepositoryOwnerLogin?.trim() ||        repositoryNameWithOwner.split("/")[0]?.trim() ||        "fork";      const remoteName = yield* gitCore.ensureRemote({        cwd,        preferredName: preferredRemoteName,        url: remoteUrl,      });       yield* gitCore.fetchRemoteBranch({        cwd,        remoteName,        remoteBranch: pullRequest.headBranch,        localBranch,      });      yield* gitCore.setBranchUpstream({        cwd,        branch: localBranch,        remoteName,        remoteBranch: pullRequest.headBranch,      });    }).pipe(      Effect.catch(() =>        gitCore.fetchPullRequestBranch({          cwd,          prNumber: pullRequest.number,          branch: localBranch,        }),      ),    );  const fileSystem = yield* FileSystem.FileSystem;  const path = yield* Path.Path;   const tempDir = process.env.TMPDIR ?? process.env.TEMP ?? process.env.TMP ?? "/tmp";   const readConfigValueNullable = (cwd: string, key: string) =>    gitCore.readConfigValue(cwd, key).pipe(Effect.catch(() => Effect.succeed(null)));   const resolveRemoteRepositoryContext = (cwd: string, remoteName: string | null) =>    Effect.gen(function* () {      if (!remoteName) {        return {          repositoryNameWithOwner: null,          ownerLogin: null,        };      }       const remoteUrl = yield* readConfigValueNullable(cwd, `remote.${remoteName}.url`);      const repositoryNameWithOwner = parseGitHubRepositoryNameWithOwnerFromRemoteUrl(remoteUrl);      return {        repositoryNameWithOwner,        ownerLogin: parseRepositoryOwnerLogin(repositoryNameWithOwner),      };    });   const resolveBranchHeadContext = (    cwd: string,    details: { branch: string; upstreamRef: string | null },  ) =>    Effect.gen(function* () {      const remoteName = yield* readConfigValueNullable(cwd, `branch.${details.branch}.remote`);      const headBranchFromUpstream = details.upstreamRef        ? extractBranchFromRef(details.upstreamRef)        : "";      const headBranch =        headBranchFromUpstream.length > 0 ? headBranchFromUpstream : details.branch;       const [remoteRepository, originRepository] = yield* Effect.all(        [          resolveRemoteRepositoryContext(cwd, remoteName),          resolveRemoteRepositoryContext(cwd, "origin"),        ],        { concurrency: "unbounded" },      );       const isCrossRepository =        remoteRepository.repositoryNameWithOwner !== null &&        originRepository.repositoryNameWithOwner !== null          ? remoteRepository.repositoryNameWithOwner.toLowerCase() !==            originRepository.repositoryNameWithOwner.toLowerCase()          : remoteName !== null &&            remoteName !== "origin" &&            remoteRepository.repositoryNameWithOwner !== null;       const ownerHeadSelector =        remoteRepository.ownerLogin && headBranch.length > 0          ? `${remoteRepository.ownerLogin}:${headBranch}`          : null;      const remoteAliasHeadSelector =        remoteName && headBranch.length > 0 ? `${remoteName}:${headBranch}` : null;      const shouldProbeRemoteOwnedSelectors =        isCrossRepository || (remoteName !== null && remoteName !== "origin");       const headSelectors: string[] = [];      if (isCrossRepository && shouldProbeRemoteOwnedSelectors) {        appendUnique(headSelectors, ownerHeadSelector);

Lines 1–500 of 1084.