877 lines · 26.2 KB · TypeScript
RawDownload
import * as NodeCrypto from "node:crypto"; import * as Context from "effect/Context";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import { ChildProcessSpawner } from "effect/unstable/process"; import {  GitCommandError,  VcsProcessExitError,  type VcsSwitchRefInput,  type VcsSwitchRefResult,  type VcsCreateRefInput,  type VcsCreateRefResult,  type VcsCreateWorktreeInput,  type VcsCreateWorktreeResult,  type ReviewDiffPreviewInput,  type ReviewDiffPreviewResult,  type VcsInitInput,  type VcsListRefsInput,  type VcsListRefsResult,  type VcsPullResult,  type VcsRemoveWorktreeInput,  type VcsStatusInput,  type VcsStatusResult,} from "@t3tools/contracts";import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts";import * as VcsDriver from "./VcsDriver.ts";import * as VcsProcess from "./VcsProcess.ts"; export interface ExecuteGitInput {  readonly operation: string;  readonly cwd: string;  readonly args: ReadonlyArray<string>;  readonly stdin?: string;  readonly env?: NodeJS.ProcessEnv;  readonly allowNonZeroExit?: boolean;  readonly timeoutMs?: number;  readonly maxOutputBytes?: number;  readonly appendTruncationMarker?: boolean;  readonly progress?: ExecuteGitProgress;} export interface ExecuteGitResult {  readonly exitCode: ChildProcessSpawner.ExitCode;  readonly stdout: string;  readonly stderr: string;  readonly stdoutTruncated: boolean;  readonly stderrTruncated: boolean;} export interface GitStatusDetails {  isRepo: boolean;  sourceControlProvider?: VcsStatusResult["sourceControlProvider"];  hasOriginRemote: boolean;  isDefaultBranch: boolean;  branch: string | null;  upstreamRef: string | null;  hasWorkingTreeChanges: boolean;  workingTree: VcsStatusResult["workingTree"];  hasUpstream: boolean;  aheadCount: number;  behindCount: number;  aheadOfDefaultCount: number;} export interface GitRemoteStatusDetails {  isRepo: boolean;  isDefaultBranch: boolean;  branch: string | null;  upstreamRef: string | null;  hasUpstream: boolean;  aheadCount: number;  behindCount: number;  aheadOfDefaultCount: number;} export interface GitPreparedCommitContext {  stagedSummary: string;  stagedPatch: string;} export interface ExecuteGitProgress {  readonly onStdoutLine?: (line: string) => Effect.Effect<void, never>;  readonly onStderrLine?: (line: string) => Effect.Effect<void, never>;  readonly onHookStarted?: (hookName: string) => Effect.Effect<void, never>;  readonly onHookFinished?: (input: {    hookName: string;    exitCode: number | null;    durationMs: number | null;  }) => Effect.Effect<void, never>;} export interface GitCommitProgress {  readonly onOutputLine?: (input: {    stream: "stdout" | "stderr";    text: string;  }) => Effect.Effect<void, never>;  readonly onHookStarted?: (hookName: string) => Effect.Effect<void, never>;  readonly onHookFinished?: (input: {    hookName: string;    exitCode: number | null;    durationMs: number | null;  }) => Effect.Effect<void, never>;} export interface GitCommitOptions {  readonly timeoutMs?: number;  readonly progress?: GitCommitProgress;} export interface GitPushResult {  status: "pushed" | "skipped_up_to_date";  branch: string;  upstreamBranch?: string | undefined;  setUpstream?: boolean | undefined;} export interface GitRangeContext {  commitSummary: string;  diffSummary: string;  diffPatch: string;} export interface GitRenameBranchInput {  cwd: string;  oldBranch: string;  newBranch: string;} export interface GitRenameBranchResult {  branch: string;} export interface GitFetchPullRequestBranchInput {  cwd: string;  prNumber: number;  branch: string;} export interface GitEnsureRemoteInput {  cwd: string;  preferredName: string;  url: string;} export interface GitFetchRemoteBranchInput {  cwd: string;  remoteName: string;  remoteBranch: string;  localBranch: string;} export interface GitFetchRemoteTrackingBranchInput {  cwd: string;  remoteName: string;  remoteBranch: string;} export interface GitFetchRemoteInput {  cwd: string;  remoteName: string;} export interface GitResolveRemoteTrackingCommitInput {  cwd: string;  refName: string;  fallbackRemoteName: string;} export interface GitResolveRemoteTrackingCommitResult {  commitSha: string;  remoteRefName: string;} export interface GitSetBranchUpstreamInput {  cwd: string;  branch: string;  remoteName: string;  remoteBranch: string;} export interface GitRemoteStatusOptions {  readonly refreshUpstream?: boolean;} export class GitVcsDriver extends Context.Service<  GitVcsDriver,  {    readonly execute: (input: ExecuteGitInput) => Effect.Effect<ExecuteGitResult, GitCommandError>;    readonly status: (input: VcsStatusInput) => Effect.Effect<VcsStatusResult, GitCommandError>;    readonly statusDetails: (cwd: string) => Effect.Effect<GitStatusDetails, GitCommandError>;    readonly statusDetailsLocal: (cwd: string) => Effect.Effect<GitStatusDetails, GitCommandError>;    readonly statusDetailsRemote: (      cwd: string,      options?: GitRemoteStatusOptions,    ) => Effect.Effect<GitRemoteStatusDetails, GitCommandError>;    readonly prepareCommitContext: (      cwd: string,      filePaths?: readonly string[],    ) => Effect.Effect<GitPreparedCommitContext | null, GitCommandError>;    readonly commit: (      cwd: string,      subject: string,      body: string,      options?: GitCommitOptions,    ) => Effect.Effect<{ commitSha: string }, GitCommandError>;    readonly pushCurrentBranch: (      cwd: string,      fallbackBranch: string | null,      options?: { readonly remoteName?: string | null },    ) => Effect.Effect<GitPushResult, GitCommandError>;    readonly readRangeContext: (      cwd: string,      baseRef: string,    ) => Effect.Effect<GitRangeContext, GitCommandError>;    readonly getReviewDiffPreview: (      input: ReviewDiffPreviewInput,    ) => Effect.Effect<ReviewDiffPreviewResult, GitCommandError>;    readonly readConfigValue: (      cwd: string,      key: string,    ) => Effect.Effect<string | null, GitCommandError>;    readonly listRefs: (      input: VcsListRefsInput,    ) => Effect.Effect<VcsListRefsResult, GitCommandError>;    readonly pullCurrentBranch: (cwd: string) => Effect.Effect<VcsPullResult, GitCommandError>;    readonly createWorktree: (      input: VcsCreateWorktreeInput,    ) => Effect.Effect<VcsCreateWorktreeResult, GitCommandError>;    readonly fetchPullRequestBranch: (      input: GitFetchPullRequestBranchInput,    ) => Effect.Effect<void, GitCommandError>;    readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect<string, GitCommandError>;    readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect<string, GitCommandError>;    readonly fetchRemote: (input: GitFetchRemoteInput) => Effect.Effect<void, GitCommandError>;    readonly resolveRemoteTrackingCommit: (      input: GitResolveRemoteTrackingCommitInput,    ) => Effect.Effect<GitResolveRemoteTrackingCommitResult, GitCommandError>;    readonly fetchRemoteBranch: (      input: GitFetchRemoteBranchInput,    ) => Effect.Effect<void, GitCommandError>;    readonly fetchRemoteTrackingBranch: (      input: GitFetchRemoteTrackingBranchInput,    ) => Effect.Effect<void, GitCommandError>;    readonly setBranchUpstream: (      input: GitSetBranchUpstreamInput,    ) => Effect.Effect<void, GitCommandError>;    readonly removeWorktree: (      input: VcsRemoveWorktreeInput,    ) => Effect.Effect<void, GitCommandError>;    readonly renameBranch: (      input: GitRenameBranchInput,    ) => Effect.Effect<GitRenameBranchResult, GitCommandError>;    readonly createRef: (      input: VcsCreateRefInput,    ) => Effect.Effect<VcsCreateRefResult, GitCommandError>;    readonly switchRef: (      input: VcsSwitchRefInput,    ) => Effect.Effect<VcsSwitchRefResult, GitCommandError>;    readonly initRepo: (input: VcsInitInput) => Effect.Effect<void, GitCommandError>;    readonly listLocalBranchNames: (cwd: string) => Effect.Effect<string[], GitCommandError>;  }>()("t3/vcs/GitVcsDriver") {} const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024;const CHECKPOINT_DIFF_MAX_OUTPUT_BYTES = 10_000_000;const WORKSPACE_GIT_HARDENED_CONFIG_ARGS = [  "-c",  "core.fsmonitor=false",  "-c",  "core.untrackedCache=false",] as const; const nowFreshness = Effect.fn("GitVcsDriver.nowFreshness")(function* () {  const now = yield* DateTime.now;  return {    source: "live-local" as const,    observedAt: now,    expiresAt: Option.none(),  };}); 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: ReadonlyArray<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 parseGitRemoteVerboseOutput(  output: string,): Map<string, { url?: string; pushUrl?: string }> {  const remotes = new Map<string, { url?: string; pushUrl?: string }>();  for (const line of output.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 name = match[1];    const url = match[2];    const direction = match[3];    if (!name || !url || !direction) {      continue;    }    const remote = remotes.get(name) ?? {};    if (direction === "fetch") {      remote.url = url;    } else {      remote.pushUrl = url;    }    remotes.set(name, remote);  }  return remotes;} const gitCommand = (  process: VcsProcess.VcsProcess["Service"],  operation: string,  cwd: string,  args: ReadonlyArray<string>,  options?: {    readonly stdin?: string;    readonly env?: NodeJS.ProcessEnv;    readonly allowNonZeroExit?: boolean;    readonly timeoutMs?: number;    readonly maxOutputBytes?: number;    readonly appendTruncationMarker?: boolean;  },) =>  process.run({    operation,    command: "git",    args: ["-C", cwd, ...args],    cwd,    spawnCwd: globalThis.process.cwd(),    ...(options?.stdin !== undefined ? { stdin: options.stdin } : {}),    ...(options?.env !== undefined ? { env: options.env } : {}),    ...(options?.allowNonZeroExit !== undefined      ? { allowNonZeroExit: options.allowNonZeroExit }      : {}),    ...(options?.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),    ...(options?.maxOutputBytes !== undefined ? { maxOutputBytes: options.maxOutputBytes } : {}),    ...(options?.appendTruncationMarker !== undefined      ? { appendTruncationMarker: options.appendTruncationMarker }      : {}),  }); export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* () {  const fileSystem = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const vcsProcess = yield* VcsProcess.VcsProcess;  const capabilities = {    kind: "git" as const,    supportsWorktrees: true,    supportsBookmarks: false,    supportsAtomicSnapshot: false,    supportsPushDefaultRemote: true,    ignoreClassifier: "native" as const,  };   const isInsideWorkTree: VcsDriver.VcsDriver["Service"]["isInsideWorkTree"] = (cwd) =>    gitCommand(      vcsProcess,      "GitVcsDriver.isInsideWorkTree",      cwd,      ["rev-parse", "--is-inside-work-tree"],      {        allowNonZeroExit: true,        timeoutMs: 5_000,        maxOutputBytes: 4_096,      },    ).pipe(Effect.map((result) => result.exitCode === 0 && result.stdout.trim() === "true"));   const execute: VcsDriver.VcsDriver["Service"]["execute"] = (input) =>    gitCommand(vcsProcess, input.operation, input.cwd, input.args, {      ...(input.stdin !== undefined ? { stdin: input.stdin } : {}),      ...(input.env !== undefined ? { env: input.env } : {}),      ...(input.allowNonZeroExit !== undefined ? { allowNonZeroExit: input.allowNonZeroExit } : {}),      ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),      ...(input.maxOutputBytes !== undefined ? { maxOutputBytes: input.maxOutputBytes } : {}),      ...(input.appendTruncationMarker !== undefined        ? { appendTruncationMarker: input.appendTruncationMarker }        : {}),    });   const detectRepository: VcsDriver.VcsDriver["Service"]["detectRepository"] = Effect.fn(    "detectRepository",  )(function* (cwd) {    if (!(yield* isInsideWorkTree(cwd))) {      return null;    }     const root = yield* gitCommand(vcsProcess, "GitVcsDriver.detectRepository.root", cwd, [      "rev-parse",      "--show-toplevel",    ]);    const gitCommonDir = yield* gitCommand(      vcsProcess,      "GitVcsDriver.detectRepository.commonDir",      cwd,      ["rev-parse", "--git-common-dir"],    ).pipe(Effect.orElseSucceed(() => null));     return {      kind: "git" as const,      rootPath: root.stdout.trim(),      metadataPath: gitCommonDir?.stdout.trim() || null,      freshness: yield* nowFreshness(),    };  });   const listWorkspaceFiles: VcsDriver.VcsDriver["Service"]["listWorkspaceFiles"] = (cwd) =>    gitCommand(      vcsProcess,      "GitVcsDriver.listWorkspaceFiles",      cwd,      [        ...WORKSPACE_GIT_HARDENED_CONFIG_ARGS,        "ls-files",        "--cached",        "--others",        "--exclude-standard",        "-z",      ],      {        allowNonZeroExit: true,        timeoutMs: 20_000,        maxOutputBytes: WORKSPACE_FILES_MAX_OUTPUT_BYTES,        appendTruncationMarker: true,      },    ).pipe(      Effect.flatMap((result) =>        result.exitCode === 0          ? Effect.gen(function* () {              const freshness = yield* nowFreshness();              return {                paths: splitNullSeparatedPaths(result.stdout, result.stdoutTruncated),                truncated: result.stdoutTruncated,                freshness,              };            })          : Effect.fail(              new VcsProcessExitError({                operation: "GitVcsDriver.listWorkspaceFiles",                command: "git ls-files",                cwd,                exitCode: result.exitCode,                detail: result.stderr.trim() || "git ls-files failed",              }),            ),      ),    );   const listRemotes: VcsDriver.VcsDriver["Service"]["listRemotes"] = Effect.fn("listRemotes")(

Lines 1–500 of 877.