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 ReviewDiffFileContentsInput, type ReviewDiffFileContentsResult, 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 getReviewDiffFileContents: ( input: ReviewDiffFileContentsInput, ) => Effect.Effect<ReviewDiffFileContentsResult, 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", }),Lines 1–500 of 882. Load more
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 ReviewDiffFileContentsInput, type ReviewDiffFileContentsResult, 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 getReviewDiffFileContents: ( input: ReviewDiffFileContentsInput, ) => Effect.Effect<ReviewDiffFileContentsResult, 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", }),