import { randomUUID } 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 * as GitVcsDriverCore 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 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 GitSetBranchUpstreamInput { cwd: string; branch: string; remoteName: string; remoteBranch: string;} export interface GitVcsDriverShape { 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 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 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>;} export class GitVcsDriver extends Context.Service<GitVcsDriver, GitVcsDriverShape>()( "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.VcsProcessShape, 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.VcsDriverShape["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.VcsDriverShape["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.VcsDriverShape["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.VcsDriverShape["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.VcsDriverShape["listRemotes"] = Effect.fn("listRemotes")( function* (cwd) { const result = yield* gitCommand( vcsProcess, "GitVcsDriver.listRemotes", cwd, ["remote", "-v"], { allowNonZeroExit: true, timeoutMs: 5_000, maxOutputBytes: 64 * 1024, }, ); if (result.exitCode !== 0) { return yield* new VcsProcessExitError({ operation: "GitVcsDriver.listRemotes", command: "git remote -v", cwd, exitCode: result.exitCode, detail: result.stderr.trim() || "git remote -v failed", }); } const parsed = parseGitRemoteVerboseOutput(result.stdout); const remotes = Array.from(parsed.entries()).flatMap(([name, remote]) => { if (!remote.url) { return []; } return [ { name, url: remote.url, pushUrl: remote.pushUrl ? Option.some(remote.pushUrl) : Option.none(), isPrimary: name === "origin", }, ]; }); return { remotes, freshness: yield* nowFreshness(), };Lines 1–500 of 832. Load more
import { randomUUID } 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 * as GitVcsDriverCore 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 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 GitSetBranchUpstreamInput { cwd: string; branch: string; remoteName: string; remoteBranch: string; } export interface GitVcsDriverShape { 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 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 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>; } export class GitVcsDriver extends Context.Service<GitVcsDriver, GitVcsDriverShape>()( "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.VcsProcessShape, 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.VcsDriverShape["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.VcsDriverShape["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.VcsDriverShape["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.VcsDriverShape["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.VcsDriverShape["listRemotes"] = Effect.fn("listRemotes")( function* (cwd) { const result = yield* gitCommand( vcsProcess, "GitVcsDriver.listRemotes", cwd, ["remote", "-v"], { allowNonZeroExit: true, timeoutMs: 5_000, maxOutputBytes: 64 * 1024, }, ); if (result.exitCode !== 0) { return yield* new VcsProcessExitError({ operation: "GitVcsDriver.listRemotes", command: "git remote -v", cwd, exitCode: result.exitCode, detail: result.stderr.trim() || "git remote -v failed", }); } const parsed = parseGitRemoteVerboseOutput(result.stdout); const remotes = Array.from(parsed.entries()).flatMap(([name, remote]) => { if (!remote.url) { return []; } return [ { name, url: remote.url, pushUrl: remote.pushUrl ? Option.some(remote.pushUrl) : Option.none(), isPrimary: name === "origin", }, ]; }); return { remotes, freshness: yield* nowFreshness(), };