1209 lines · 38.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 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): string | null {  const trimmed = value.trim();  const prefix = "refs/remotes/origin/";  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): Effect.Effect<string | null, GitCommandError> =>    executeGit(      "GitCore.resolveDefaultBranchName",      cwd,      ["symbolic-ref", "refs/remotes/origin/HEAD"],      { allowNonZeroExit: true },    ).pipe(      Effect.map((result) => {        if (result.code !== 0) {          return null;        }        return parseDefaultBranchFromRemoteHeadRef(result.stdout);      }),    );   const remoteBranchExists = (    cwd: string,    branch: string,  ): Effect.Effect<boolean, GitCommandError> =>    executeGit(      "GitCore.remoteBranchExists",      cwd,      ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${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 resolveBaseBranchForNoUpstream = (    cwd: string,    branch: string,  ): Effect.Effect<string | null, GitCommandError> =>    Effect.gen(function* () {      const configuredBaseBranch = yield* runGitStdout(        "GitCore.resolveBaseBranchForNoUpstream.config",        cwd,        ["config", "--get", `branch.${branch}.gh-merge-base`],        true,      ).pipe(Effect.map((stdout) => stdout.trim()));       const defaultBranch = yield* resolveDefaultBranchName(cwd);      const candidates = [        configuredBaseBranch.length > 0 ? configuredBaseBranch : null,        defaultBranch,        ...DEFAULT_BASE_BRANCH_CANDIDATES,      ];       for (const candidate of candidates) {        if (!candidate) {          continue;        }         const normalizedCandidate = candidate.startsWith("origin/")          ? candidate.slice("origin/".length)          : candidate;        if (normalizedCandidate.length === 0 || normalizedCandidate === branch) {          continue;        }         if (yield* branchExists(cwd, normalizedCandidate)) {          return normalizedCandidate;        }         if (yield* remoteBranchExists(cwd, normalizedCandidate)) {          return `origin/${normalizedCandidate}`;        }      }       return null;    });   const computeAheadCountAgainstBase = (    cwd: string,    branch: string,  ): Effect.Effect<number, GitCommandError> =>    Effect.gen(function* () {      const baseBranch = yield* resolveBaseBranchForNoUpstream(cwd, branch);      if (!baseBranch) {        return 0;      }       const result = yield* executeGit(        "GitCore.computeAheadCountAgainstBase",        cwd,        ["rev-list", "--count", `${baseBranch}..HEAD`],        { allowNonZeroExit: true },      );      if (result.code !== 0) {        return 0;      }       const parsed = Number.parseInt(result.stdout.trim(), 10);      return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;    });   const readBranchRecency = (cwd: string): Effect.Effect<Map<string, number>, GitCommandError> =>    Effect.gen(function* () {      const branchRecency = yield* executeGit(        "GitCore.readBranchRecency",        cwd,        [          "for-each-ref",          "--format=%(refname:short)%09%(committerdate:unix)",          "refs/heads",          "refs/remotes",        ],        {          timeoutMs: 15_000,

Lines 1–500 of 1209.