2206 lines · 69.4 KB · TypeScript
RawDownload
import {  Cache,  Data,  Duration,  Effect,  Exit,  FileSystem,  Layer,  Option,  Path,  PlatformError,  Ref,  Result,  Schema,  Scope,  Semaphore,  Stream,} from "effect";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { GitCommandError, type GitBranch } from "@t3tools/contracts";import { dedupeRemoteBranchesWithLocalMatches } from "@t3tools/shared/git";import { compactTraceAttributes } from "../../observability/Attributes.ts";import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../../observability/Metrics.ts";import {  GitCore,  type ExecuteGitProgress,  type GitCommitOptions,  type GitCoreShape,  type GitStatusDetails,  type ExecuteGitInput,  type ExecuteGitResult,} from "../Services/GitCore.ts";import {  parseRemoteNames,  parseRemoteNamesInGitOrder,  parseRemoteRefWithRemoteNames,} from "../remoteRefs.ts";import { ServerConfig } from "../../config.ts";import { decodeJsonResult } from "@t3tools/shared/schemaJson"; const DEFAULT_TIMEOUT_MS = 30_000;const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;const OUTPUT_TRUNCATED_MARKER = "\n\n[truncated]";const PREPARED_COMMIT_PATCH_MAX_OUTPUT_BYTES = 49_000;const RANGE_COMMIT_SUMMARY_MAX_OUTPUT_BYTES = 19_000;const RANGE_DIFF_SUMMARY_MAX_OUTPUT_BYTES = 19_000;const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59_000;const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024;const WORKSPACE_GIT_HARDENED_CONFIG_ARGS = [  "-c",  "core.fsmonitor=false",  "-c",  "core.untrackedCache=false",] as const;const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15);const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5);const STATUS_UPSTREAM_REFRESH_FAILURE_COOLDOWN = Duration.seconds(5);const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2_048;const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const;const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100;const NON_REPOSITORY_STATUS_DETAILS = Object.freeze<GitStatusDetails>({  isRepo: false,  hasOriginRemote: false,  isDefaultBranch: false,  branch: null,  upstreamRef: null,  hasWorkingTreeChanges: false,  workingTree: { files: [], insertions: 0, deletions: 0 },  hasUpstream: false,  aheadCount: 0,  behindCount: 0,}); type TraceTailState = {  processedChars: number;  remainder: string;}; class StatusRemoteRefreshCacheKey extends Data.Class<{  gitCommonDir: string;  remoteName: string;}> {} interface ExecuteGitOptions {  stdin?: string | undefined;  timeoutMs?: number | undefined;  allowNonZeroExit?: boolean | undefined;  fallbackErrorMessage?: string | undefined;  maxOutputBytes?: number | undefined;  truncateOutputAtMaxBytes?: boolean | undefined;  progress?: ExecuteGitProgress | 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 splitNullSeparatedPaths(input: string, truncated: boolean): string[] {  const parts = input.split("\0");  if (parts.length === 0) return [];   if (truncated && parts[parts.length - 1]?.length) {    parts.pop();  }   return parts.filter((value) => value.length > 0);} function chunkPathsForGitCheckIgnore(relativePaths: readonly string[]): string[][] {  const chunks: string[][] = [];  let chunk: string[] = [];  let chunkBytes = 0;   for (const relativePath of relativePaths) {    const relativePathBytes = Buffer.byteLength(relativePath) + 1;    if (chunk.length > 0 && chunkBytes + relativePathBytes > GIT_CHECK_IGNORE_MAX_STDIN_BYTES) {      chunks.push(chunk);      chunk = [];      chunkBytes = 0;    }     chunk.push(relativePath);    chunkBytes += relativePathBytes;     if (chunkBytes >= GIT_CHECK_IGNORE_MAX_STDIN_BYTES) {      chunks.push(chunk);      chunk = [];      chunkBytes = 0;    }  }   if (chunk.length > 0) {    chunks.push(chunk);  }   return chunks;} 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 filterBranchesForListQuery(  branches: ReadonlyArray<GitBranch>,  query?: string,): ReadonlyArray<GitBranch> {  if (!query) {    return branches;  }   const normalizedQuery = query.toLowerCase();  return branches.filter((branch) => branch.name.toLowerCase().includes(normalizedQuery));} function paginateBranches(input: {  branches: ReadonlyArray<GitBranch>;  cursor?: number | undefined;  limit?: number | undefined;}): {  branches: ReadonlyArray<GitBranch>;  nextCursor: number | null;  totalCount: number;} {  const cursor = input.cursor ?? 0;  const limit = input.limit ?? GIT_LIST_BRANCHES_DEFAULT_LIMIT;  const totalCount = input.branches.length;  const branches = input.branches.slice(cursor, cursor + limit);  const nextCursor = cursor + branches.length < totalCount ? cursor + branches.length : null;   return {    branches,    nextCursor,    totalCount,  };} 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 parseUpstreamRefWithRemoteNames(  upstreamRef: string,  remoteNames: ReadonlyArray<string>,): { upstreamRef: string; remoteName: string; upstreamBranch: string } | null {  const parsed = parseRemoteRefWithRemoteNames(upstreamRef, remoteNames);  if (!parsed) {    return null;  }   return {    upstreamRef,    remoteName: parsed.remoteName,    upstreamBranch: parsed.branchName,  };} function parseUpstreamRefByFirstSeparator(  upstreamRef: string,): { upstreamRef: string; remoteName: string; upstreamBranch: string } | null {  const separatorIndex = upstreamRef.indexOf("/");  if (separatorIndex <= 0 || separatorIndex === upstreamRef.length - 1) {    return null;  }   const remoteName = upstreamRef.slice(0, separatorIndex).trim();  const upstreamBranch = upstreamRef.slice(separatorIndex + 1).trim();  if (remoteName.length === 0 || upstreamBranch.length === 0) {    return null;  }   return {    upstreamRef,    remoteName,    upstreamBranch,  };} 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 } : {}),  });} function quoteGitCommand(args: ReadonlyArray<string>): string {  return `git ${args.join(" ")}`;} function isMissingGitCwdError(error: GitCommandError): boolean {  const normalized = `${error.detail}\n${error.message}`.toLowerCase();  return (    normalized.includes("no such file or directory") ||    normalized.includes("notfound: filesystem.access") ||    normalized.includes("enoent") ||    normalized.includes("not a directory")  );} function toGitCommandError(  input: Pick<ExecuteGitInput, "operation" | "cwd" | "args">,  detail: string,) {  return (cause: unknown) =>    Schema.is(GitCommandError)(cause)      ? cause      : new GitCommandError({          operation: input.operation,          command: quoteGitCommand(input.args),          cwd: input.cwd,          detail: `${cause instanceof Error && cause.message.length > 0 ? cause.message : "Unknown error"} - ${detail}`,          ...(cause !== undefined ? { cause } : {}),        });} interface Trace2Monitor {  readonly env: NodeJS.ProcessEnv;  readonly flush: Effect.Effect<void, never>;} const nowUnixNano = (): bigint => BigInt(Date.now()) * 1_000_000n; const addCurrentSpanEvent = (name: string, attributes: Record<string, unknown>) =>  Effect.currentSpan.pipe(    Effect.tap((span) =>      Effect.sync(() => {        span.event(name, nowUnixNano(), compactTraceAttributes(attributes));      }),    ),    Effect.catch(() => Effect.void),  ); function trace2ChildKey(record: Record<string, unknown>): string | null {  const childId = record.child_id;  if (typeof childId === "number" || typeof childId === "string") {    return String(childId);  }  const hookName = record.hook_name;  return typeof hookName === "string" && hookName.trim().length > 0 ? hookName.trim() : null;} const Trace2Record = Schema.Record(Schema.String, Schema.Unknown); const createTrace2Monitor = Effect.fn("createTrace2Monitor")(function* (  input: Pick<ExecuteGitInput, "operation" | "cwd" | "args">,  progress: ExecuteGitProgress | undefined,): Effect.fn.Return<  Trace2Monitor,  PlatformError.PlatformError,  Scope.Scope | FileSystem.FileSystem | Path.Path> {  if (!progress?.onHookStarted && !progress?.onHookFinished) {    return {      env: {},      flush: Effect.void,    };  }   const fs = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const traceFilePath = yield* fs.makeTempFileScoped({    prefix: `t3code-git-trace2-${process.pid}-`,    suffix: ".json",  });  const hookStartByChildKey = new Map<string, { hookName: string; startedAtMs: number }>();  const traceTailState = yield* Ref.make<TraceTailState>({    processedChars: 0,    remainder: "",  });   const handleTraceLine = Effect.fn("handleTraceLine")(function* (line: string) {    const trimmedLine = line.trim();    if (trimmedLine.length === 0) {      return;    }     const traceRecord = decodeJsonResult(Trace2Record)(trimmedLine);    if (Result.isFailure(traceRecord)) {      yield* Effect.logDebug(        `GitCore.trace2: failed to parse trace line for ${quoteGitCommand(input.args)} in ${input.cwd}`,        traceRecord.failure,      );      return;    }     if (traceRecord.success.child_class !== "hook") {      return;    }     const event = traceRecord.success.event;    const childKey = trace2ChildKey(traceRecord.success);    if (childKey === null) {      return;    }    const started = hookStartByChildKey.get(childKey);    const hookNameFromEvent =      typeof traceRecord.success.hook_name === "string" ? traceRecord.success.hook_name.trim() : "";    const hookName = hookNameFromEvent.length > 0 ? hookNameFromEvent : (started?.hookName ?? "");    if (hookName.length === 0) {      return;    }     if (event === "child_start") {      hookStartByChildKey.set(childKey, { hookName, startedAtMs: Date.now() });      yield* addCurrentSpanEvent("git.hook.started", {        hookName,      });      if (progress.onHookStarted) {        yield* progress.onHookStarted(hookName);      }      return;    }     if (event === "child_exit") {      hookStartByChildKey.delete(childKey);      const code = traceRecord.success.code;      const exitCode = typeof code === "number" && Number.isInteger(code) ? code : null;

Lines 1–500 of 2206.