1416 lines · 45.9 KB · TypeScript
RawDownload
import { Cache, Data, Duration, Effect, Exit, FileSystem, Layer, Path } from "effect"; import { GitCommandError } from "../Errors.ts";import { GitService } from "../Services/GitService.ts";import { GitCore, type GitCoreShape } from "../Services/GitCore.ts"; const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15);const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5);const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2_048;const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const; class StatusUpstreamRefreshCacheKey extends Data.Class<{  cwd: string;  upstreamRef: string;  remoteName: string;  upstreamBranch: string;}> {} interface ExecuteGitOptions {  timeoutMs?: number | undefined;  allowNonZeroExit?: boolean | undefined;  fallbackErrorMessage?: string | undefined;} function parseBranchAb(value: string): { ahead: number; behind: number } {  const match = value.match(/^\+(\d+)\s+-(\d+)$/);  if (!match) return { ahead: 0, behind: 0 };  return {    ahead: Number(match[1] ?? "0"),    behind: Number(match[2] ?? "0"),  };} function parseNumstatEntries(  stdout: string,): Array<{ path: string; insertions: number; deletions: number }> {  const entries: Array<{ path: string; insertions: number; deletions: number }> = [];  for (const line of stdout.split(/\r?\n/g)) {    if (line.trim().length === 0) continue;    const [addedRaw, deletedRaw, ...pathParts] = line.split("\t");    const rawPath =      pathParts.length > 1 ? (pathParts.at(-1) ?? "").trim() : pathParts.join("\t").trim();    if (rawPath.length === 0) continue;    const added = Number.parseInt(addedRaw ?? "0", 10);    const deleted = Number.parseInt(deletedRaw ?? "0", 10);    const renameArrowIndex = rawPath.indexOf(" => ");    const normalizedPath =      renameArrowIndex >= 0 ? rawPath.slice(renameArrowIndex + " => ".length).trim() : rawPath;    entries.push({      path: normalizedPath.length > 0 ? normalizedPath : rawPath,      insertions: Number.isFinite(added) ? added : 0,      deletions: Number.isFinite(deleted) ? deleted : 0,    });  }  return entries;} function parsePorcelainPath(line: string): string | null {  if (line.startsWith("? ") || line.startsWith("! ")) {    const simple = line.slice(2).trim();    return simple.length > 0 ? simple : null;  }   if (!(line.startsWith("1 ") || line.startsWith("2 ") || line.startsWith("u "))) {    return null;  }   const tabIndex = line.indexOf("\t");  if (tabIndex >= 0) {    const fromTab = line.slice(tabIndex + 1);    const [filePath] = fromTab.split("\t");    return filePath?.trim().length ? filePath.trim() : null;  }   const parts = line.trim().split(/\s+/g);  const filePath = parts.at(-1) ?? "";  return filePath.length > 0 ? filePath : null;} function parseBranchLine(line: string): { name: string; current: boolean } | null {  const trimmed = line.trim();  if (trimmed.length === 0) return null;   const name = trimmed.replace(/^[*+]\s+/, "");  // Exclude symbolic refs like: "origin/HEAD -> origin/main".  // Exclude detached HEAD pseudo-refs like: "(HEAD detached at origin/main)".  if (name.includes(" -> ") || name.startsWith("(")) return null;   return {    name,    current: trimmed.startsWith("* "),  };} function parseRemoteNames(stdout: string): ReadonlyArray<string> {  return stdout    .split("\n")    .map((line) => line.trim())    .filter((line) => line.length > 0)    .toSorted((a, b) => b.length - a.length);} function sanitizeRemoteName(value: string): string {  const sanitized = value    .trim()    .replace(/[^A-Za-z0-9._-]+/g, "-")    .replace(/^-+|-+$/g, "");  return sanitized.length > 0 ? sanitized : "fork";} function normalizeRemoteUrl(value: string): string {  return value    .trim()    .replace(/\/+$/g, "")    .replace(/\.git$/i, "")    .toLowerCase();} function parseRemoteFetchUrls(stdout: string): Map<string, string> {  const remotes = new Map<string, string>();  for (const line of stdout.split("\n")) {    const trimmed = line.trim();    if (trimmed.length === 0) continue;    const match = /^(\S+)\s+(\S+)\s+\((fetch|push)\)$/.exec(trimmed);    if (!match) continue;    const [, remoteName = "", remoteUrl = "", direction = ""] = match;    if (direction !== "fetch" || remoteName.length === 0 || remoteUrl.length === 0) {      continue;    }    remotes.set(remoteName, remoteUrl);  }  return remotes;} function parseRemoteRefWithRemoteNames(  branchName: string,  remoteNames: ReadonlyArray<string>,): { remoteRef: string; remoteName: string; localBranch: string } | null {  const trimmedBranchName = branchName.trim();  if (trimmedBranchName.length === 0) return null;   for (const remoteName of remoteNames) {    const remotePrefix = `${remoteName}/`;    if (!trimmedBranchName.startsWith(remotePrefix)) {      continue;    }    const localBranch = trimmedBranchName.slice(remotePrefix.length).trim();    if (localBranch.length === 0) {      return null;    }    return {      remoteRef: trimmedBranchName,      remoteName,      localBranch,    };  }   return null;} function parseTrackingBranchByUpstreamRef(stdout: string, upstreamRef: string): string | null {  for (const line of stdout.split("\n")) {    const trimmedLine = line.trim();    if (trimmedLine.length === 0) {      continue;    }    const [branchNameRaw, upstreamBranchRaw = ""] = trimmedLine.split("\t");    const branchName = branchNameRaw?.trim() ?? "";    const upstreamBranch = upstreamBranchRaw.trim();    if (branchName.length === 0 || upstreamBranch.length === 0) {      continue;    }    if (upstreamBranch === upstreamRef) {      return branchName;    }  }   return null;} function deriveLocalBranchNameFromRemoteRef(branchName: string): string | null {  const separatorIndex = branchName.indexOf("/");  if (separatorIndex <= 0 || separatorIndex === branchName.length - 1) {    return null;  }  const localBranch = branchName.slice(separatorIndex + 1).trim();  return localBranch.length > 0 ? localBranch : null;} function commandLabel(args: readonly string[]): string {  return `git ${args.join(" ")}`;} function parseDefaultBranchFromRemoteHeadRef(value: string, remoteName: string): string | null {  const trimmed = value.trim();  const prefix = `refs/remotes/${remoteName}/`;  if (!trimmed.startsWith(prefix)) {    return null;  }  const branch = trimmed.slice(prefix.length).trim();  return branch.length > 0 ? branch : null;} function createGitCommandError(  operation: string,  cwd: string,  args: readonly string[],  detail: string,  cause?: unknown,): GitCommandError {  return new GitCommandError({    operation,    command: commandLabel(args),    cwd,    detail,    ...(cause !== undefined ? { cause } : {}),  });} const makeGitCore = Effect.gen(function* () {  const git = yield* GitService;  const fileSystem = yield* FileSystem.FileSystem;  const path = yield* Path.Path;   const executeGit = (    operation: string,    cwd: string,    args: readonly string[],    options: ExecuteGitOptions = {},  ): Effect.Effect<{ code: number; stdout: string; stderr: string }, GitCommandError> =>    git      .execute({        operation,        cwd,        args,        allowNonZeroExit: true,        ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),      })      .pipe(        Effect.flatMap((result) => {          if (options.allowNonZeroExit || result.code === 0) {            return Effect.succeed(result);          }          const stderr = result.stderr.trim();          if (stderr.length > 0) {            return Effect.fail(createGitCommandError(operation, cwd, args, stderr));          }          if (options.fallbackErrorMessage) {            return Effect.fail(              createGitCommandError(operation, cwd, args, options.fallbackErrorMessage),            );          }          return Effect.fail(            createGitCommandError(              operation,              cwd,              args,              `${commandLabel(args)} failed: code=${result.code ?? "null"}`,            ),          );        }),      );   const runGit = (    operation: string,    cwd: string,    args: readonly string[],    allowNonZeroExit = false,  ): Effect.Effect<void, GitCommandError> =>    executeGit(operation, cwd, args, { allowNonZeroExit }).pipe(Effect.asVoid);   const runGitStdout = (    operation: string,    cwd: string,    args: readonly string[],    allowNonZeroExit = false,  ): Effect.Effect<string, GitCommandError> =>    executeGit(operation, cwd, args, { allowNonZeroExit }).pipe(      Effect.map((result) => result.stdout),    );   const branchExists = (cwd: string, branch: string): Effect.Effect<boolean, GitCommandError> =>    executeGit(      "GitCore.branchExists",      cwd,      ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],      {        allowNonZeroExit: true,        timeoutMs: 5_000,      },    ).pipe(Effect.map((result) => result.code === 0));   const resolveAvailableBranchName = (    cwd: string,    desiredBranch: string,  ): Effect.Effect<string, GitCommandError> =>    Effect.gen(function* () {      const isDesiredTaken = yield* branchExists(cwd, desiredBranch);      if (!isDesiredTaken) {        return desiredBranch;      }       for (let suffix = 1; suffix <= 100; suffix += 1) {        const candidate = `${desiredBranch}-${suffix}`;        const isCandidateTaken = yield* branchExists(cwd, candidate);        if (!isCandidateTaken) {          return candidate;        }      }       return yield* createGitCommandError(        "GitCore.renameBranch",        cwd,        ["branch", "-m", "--", desiredBranch],        `Could not find an available branch name for '${desiredBranch}'.`,      );    });   const resolveCurrentUpstream = (    cwd: string,  ): Effect.Effect<    { upstreamRef: string; remoteName: string; upstreamBranch: string } | null,    GitCommandError  > =>    Effect.gen(function* () {      const upstreamRef = yield* runGitStdout(        "GitCore.resolveCurrentUpstream",        cwd,        ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],        true,      ).pipe(Effect.map((stdout) => stdout.trim()));       if (upstreamRef.length === 0 || upstreamRef === "@{upstream}") {        return null;      }       const separatorIndex = upstreamRef.indexOf("/");      if (separatorIndex <= 0) {        return null;      }      const remoteName = upstreamRef.slice(0, separatorIndex);      const upstreamBranch = upstreamRef.slice(separatorIndex + 1);      if (remoteName.length === 0 || upstreamBranch.length === 0) {        return null;      }       return {        upstreamRef,        remoteName,        upstreamBranch,      };    });   const fetchUpstreamRef = (    cwd: string,    upstream: { upstreamRef: string; remoteName: string; upstreamBranch: string },  ): Effect.Effect<void, GitCommandError> => {    const refspec = `+refs/heads/${upstream.upstreamBranch}:refs/remotes/${upstream.upstreamRef}`;    return runGit(      "GitCore.fetchUpstreamRef",      cwd,      ["fetch", "--quiet", "--no-tags", upstream.remoteName, refspec],      true,    );  };   const fetchUpstreamRefForStatus = (    cwd: string,    upstream: { upstreamRef: string; remoteName: string; upstreamBranch: string },  ): Effect.Effect<void, GitCommandError> => {    const refspec = `+refs/heads/${upstream.upstreamBranch}:refs/remotes/${upstream.upstreamRef}`;    return executeGit(      "GitCore.fetchUpstreamRefForStatus",      cwd,      ["fetch", "--quiet", "--no-tags", upstream.remoteName, refspec],      {        allowNonZeroExit: true,        timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT),      },    ).pipe(Effect.asVoid);  };   const statusUpstreamRefreshCache = yield* Cache.makeWith({    capacity: STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY,    lookup: (cacheKey: StatusUpstreamRefreshCacheKey) =>      Effect.gen(function* () {        yield* fetchUpstreamRefForStatus(cacheKey.cwd, {          upstreamRef: cacheKey.upstreamRef,          remoteName: cacheKey.remoteName,          upstreamBranch: cacheKey.upstreamBranch,        });        return true as const;      }),    // Keep successful refreshes warm; drop failures immediately so next request can retry.    timeToLive: (exit) => (Exit.isSuccess(exit) ? STATUS_UPSTREAM_REFRESH_INTERVAL : Duration.zero),  });   const refreshStatusUpstreamIfStale = (cwd: string): Effect.Effect<void, GitCommandError> =>    Effect.gen(function* () {      const upstream = yield* resolveCurrentUpstream(cwd);      if (!upstream) return;      yield* Cache.get(        statusUpstreamRefreshCache,        new StatusUpstreamRefreshCacheKey({          cwd,          upstreamRef: upstream.upstreamRef,          remoteName: upstream.remoteName,          upstreamBranch: upstream.upstreamBranch,        }),      );    });   const refreshCheckedOutBranchUpstream = (cwd: string): Effect.Effect<void, GitCommandError> =>    Effect.gen(function* () {      const upstream = yield* resolveCurrentUpstream(cwd);      if (!upstream) return;      yield* fetchUpstreamRef(cwd, upstream);    });   const resolveDefaultBranchName = (    cwd: string,    remoteName: string,  ): Effect.Effect<string | null, GitCommandError> =>    executeGit(      "GitCore.resolveDefaultBranchName",      cwd,      ["symbolic-ref", `refs/remotes/${remoteName}/HEAD`],      { allowNonZeroExit: true },    ).pipe(      Effect.map((result) => {        if (result.code !== 0) {          return null;        }        return parseDefaultBranchFromRemoteHeadRef(result.stdout, remoteName);      }),    );   const remoteBranchExists = (    cwd: string,    remoteName: string,    branch: string,  ): Effect.Effect<boolean, GitCommandError> =>    executeGit(      "GitCore.remoteBranchExists",      cwd,      ["show-ref", "--verify", "--quiet", `refs/remotes/${remoteName}/${branch}`],      {        allowNonZeroExit: true,      },    ).pipe(Effect.map((result) => result.code === 0));   const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> =>    executeGit("GitCore.originRemoteExists", cwd, ["remote", "get-url", "origin"], {      allowNonZeroExit: true,    }).pipe(Effect.map((result) => result.code === 0));   const listRemoteNames = (cwd: string): Effect.Effect<ReadonlyArray<string>, GitCommandError> =>    runGitStdout("GitCore.listRemoteNames", cwd, ["remote"]).pipe(      Effect.map((stdout) => parseRemoteNames(stdout).toReversed()),    );   const resolvePrimaryRemoteName = (cwd: string): Effect.Effect<string, GitCommandError> =>    Effect.gen(function* () {      if (yield* originRemoteExists(cwd)) {        return "origin";      }      const remotes = yield* listRemoteNames(cwd);      const [firstRemote] = remotes;      if (firstRemote) {        return firstRemote;      }      return yield* createGitCommandError(        "GitCore.resolvePrimaryRemoteName",        cwd,        ["remote"],        "No git remote is configured for this repository.",      );    });   const resolvePushRemoteName = (    cwd: string,    branch: string,  ): Effect.Effect<string | null, GitCommandError> =>    Effect.gen(function* () {      const branchPushRemote = yield* runGitStdout(        "GitCore.resolvePushRemoteName.branchPushRemote",        cwd,        ["config", "--get", `branch.${branch}.pushRemote`],        true,      ).pipe(Effect.map((stdout) => stdout.trim()));      if (branchPushRemote.length > 0) {        return branchPushRemote;      }       const pushDefaultRemote = yield* runGitStdout(        "GitCore.resolvePushRemoteName.remotePushDefault",        cwd,        ["config", "--get", "remote.pushDefault"],        true,      ).pipe(Effect.map((stdout) => stdout.trim()));

Lines 1–500 of 1416.