2581 lines · 83.3 KB · TypeScript
RawDownload
import * as Arr from "effect/Array";import * as Cache from "effect/Cache";import * as Data from "effect/Data";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as Exit from "effect/Exit";import * as FileSystem from "effect/FileSystem";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as PlatformError from "effect/PlatformError";import * as Ref from "effect/Ref";import * as Result from "effect/Result";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Semaphore from "effect/Semaphore";import * as Stream from "effect/Stream";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import {  GitCommandError,  type ReviewDiffPreviewInput,  type ReviewDiffPreviewSource,  type VcsRef,} from "@t3tools/contracts";import { dedupeRemoteBranchesWithLocalMatches, normalizeGitRemoteUrl } from "@t3tools/shared/git";import { compactTraceAttributes } from "@t3tools/shared/observability";import { decodeJsonResult } from "@t3tools/shared/schemaJson";import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../observability/Metrics.ts";import * as GitVcsDriver from "./GitVcsDriver.ts";import {  parseRemoteNames,  parseRemoteNamesInGitOrder,  parseRemoteRefWithRemoteNames,} from "../git/remoteRefs.ts";import { ServerConfig } from "../config.ts"; 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 REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 120_000;const REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES = 80_000;const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 120_000;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 STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({  GCM_INTERACTIVE: "never",  GIT_ASKPASS: "",  GIT_TERMINAL_PROMPT: "0",  SSH_ASKPASS: "",  SSH_ASKPASS_REQUIRE: "never",} satisfies NodeJS.ProcessEnv);const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const;const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100;const NON_REPOSITORY_STATUS_DETAILS = Object.freeze<GitVcsDriver.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,  aheadOfDefaultCount: 0,});const NON_REPOSITORY_REMOTE_STATUS_DETAILS = Object.freeze<GitVcsDriver.GitRemoteStatusDetails>({  isRepo: false,  isDefaultBranch: false,  branch: null,  upstreamRef: null,  hasUpstream: false,  aheadCount: 0,  behindCount: 0,  aheadOfDefaultCount: 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;  fallbackErrorDetail?: string | undefined;  env?: NodeJS.ProcessEnv | undefined;  maxOutputBytes?: number | undefined;  appendTruncationMarker?: boolean | undefined;  progress?: GitVcsDriver.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 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(  refs: ReadonlyArray<VcsRef>,  query?: string,): ReadonlyArray<VcsRef> {  if (!query) {    return refs;  }   const normalizedQuery = query.toLowerCase();  return refs.filter((refName) => refName.name.toLowerCase().includes(normalizedQuery));} function paginateBranches(input: {  refs: ReadonlyArray<VcsRef>;  cursor?: number | undefined;  limit?: number | undefined;}): {  refs: ReadonlyArray<VcsRef>;  nextCursor: number | null;  totalCount: number;} {  const cursor = input.cursor ?? 0;  const limit = input.limit ?? GIT_LIST_BRANCHES_DEFAULT_LIMIT;  const totalCount = input.refs.length;  const refs = input.refs.slice(cursor, cursor + limit);  const nextCursor = cursor + refs.length < totalCount ? cursor + refs.length : null;   return {    refs,    nextCursor,    totalCount,  };} 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);} export function splitNullSeparatedGitStdoutPaths(  result: Pick<GitVcsDriver.ExecuteGitResult, "stdout" | "stdoutTruncated">,): string[] {  return splitNullSeparatedPaths(result.stdout, result.stdoutTruncated);} function sanitizeRemoteName(value: string): string {  const sanitized = value    .trim()    .replace(/[^A-Za-z0-9._-]+/g, "-")    .replace(/^-+|-+$/g, "");  return sanitized.length > 0 ? sanitized : "fork";} 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; branchName: string } | null {  const parsed = parseRemoteRefWithRemoteNames(upstreamRef, remoteNames);  if (!parsed) {    return null;  }   return {    upstreamRef,    remoteName: parsed.remoteName,    branchName: parsed.branchName,  };} function parseUpstreamRefByFirstSeparator(  upstreamRef: string,): { upstreamRef: string; remoteName: string; branchName: string } | null {  const separatorIndex = upstreamRef.indexOf("/");  if (separatorIndex <= 0 || separatorIndex === upstreamRef.length - 1) {    return null;  }   const remoteName = upstreamRef.slice(0, separatorIndex).trim();  const branchName = upstreamRef.slice(separatorIndex + 1).trim();  if (remoteName.length === 0 || branchName.length === 0) {    return null;  }   return {    upstreamRef,    remoteName,    branchName,  };} 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 candidateUpstreamRef = upstreamBranchRaw.trim();    if (branchName.length === 0 || candidateUpstreamRef.length === 0) {      continue;    }    if (candidateUpstreamRef === 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 gitCommandContext(  input: Pick<GitVcsDriver.ExecuteGitInput, "operation" | "cwd" | "args">,) {  return {    operation: input.operation,    command: "git",    cwd: input.cwd,    argumentCount: input.args.length,  } as const;} function parseDefaultBranchFromRemoteHeadRef(value: string, remoteName: string): string | null {  const trimmed = value.trim();  const prefix = `refs/remotes/${remoteName}/`;  if (!trimmed.startsWith(prefix)) {    return null;  }  const refName = trimmed.slice(prefix.length).trim();  return refName.length > 0 ? refName : null;} function isMissingGitCwdError(error: GitCommandError): boolean {  if (!(error.cause instanceof PlatformError.PlatformError)) {    return false;  }   const reason = error.cause.reason;  if (reason._tag === "NotFound") {    return reason.pathOrDescriptor === error.cwd;  }   return (    reason._tag === "BadResource" &&    reason.pathOrDescriptor === error.cwd &&    typeof reason.cause === "object" &&    reason.cause !== null &&    "code" in reason.cause &&    reason.cause.code === "ENOTDIR"  );} function isNonRepositoryGitStderr(stderr: string): boolean {  return stderr.toLowerCase().includes("not a git repository");} interface Trace2Monitor {  readonly env: NodeJS.ProcessEnv;  readonly flush: Effect.Effect<void, never>;} const nowUnixNano = DateTime.now.pipe(  Effect.map((now) => BigInt(DateTime.toEpochMillis(now)) * 1_000_000n),); const addCurrentSpanEvent = (name: string, attributes: Record<string, unknown>) =>  Effect.gen(function* () {    const span = yield* Effect.currentSpan;    const timestamp = yield* nowUnixNano;    yield* Effect.sync(() => {      span.event(name, timestamp, compactTraceAttributes(attributes));    });  }).pipe(    Effect.catchTags({      NoSuchElementError: () => 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<GitVcsDriver.ExecuteGitInput, "operation" | "cwd" | "args">,  progress: GitVcsDriver.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(        `GitVcsDriver.trace2: failed to parse trace line for ${input.operation} in ${input.cwd} (${input.args.length} arguments)`,        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") {      const now = yield* DateTime.now;      hookStartByChildKey.set(childKey, { hookName, startedAtMs: DateTime.toEpochMillis(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.exitCode;      const exitCode = typeof code === "number" && Number.isInteger(code) ? code : null;      const now = yield* DateTime.now;      const durationMs = started        ? Math.max(0, DateTime.toEpochMillis(now) - started.startedAtMs)        : null;      yield* addCurrentSpanEvent("git.hook.finished", {        hookName: started?.hookName ?? hookName,        exitCode,        durationMs,      });      if (progress.onHookFinished) {        yield* progress.onHookFinished({          hookName: started?.hookName ?? hookName,          exitCode,          durationMs,        });      }    }  });   const deltaMutex = yield* Semaphore.make(1);  const readTraceDelta = deltaMutex.withPermit(    fs.readFileString(traceFilePath).pipe(      Effect.flatMap((contents) =>        Effect.uninterruptible(          Ref.modify(traceTailState, ({ processedChars, remainder }) => {            if (contents.length <= processedChars) {              return [[], { processedChars, remainder }];            }

Lines 1–500 of 2581.