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; 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; readonly onStderrLine?: (line: string) => Effect.Effect; readonly onHookStarted?: (hookName: string) => Effect.Effect; readonly onHookFinished?: (input: { hookName: string; exitCode: number | null; durationMs: number | null; }) => Effect.Effect; } export interface GitCommitProgress { readonly onOutputLine?: (input: { stream: "stdout" | "stderr"; text: string; }) => Effect.Effect; readonly onHookStarted?: (hookName: string) => Effect.Effect; readonly onHookFinished?: (input: { hookName: string; exitCode: number | null; durationMs: number | null; }) => Effect.Effect; } 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; readonly status: (input: VcsStatusInput) => Effect.Effect; readonly statusDetails: (cwd: string) => Effect.Effect; readonly statusDetailsLocal: (cwd: string) => Effect.Effect; readonly statusDetailsRemote: ( cwd: string, options?: GitRemoteStatusOptions, ) => Effect.Effect; readonly prepareCommitContext: ( cwd: string, filePaths?: readonly string[], ) => Effect.Effect; 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; readonly readRangeContext: ( cwd: string, baseRef: string, ) => Effect.Effect; readonly getReviewDiffPreview: ( input: ReviewDiffPreviewInput, ) => Effect.Effect; readonly readConfigValue: ( cwd: string, key: string, ) => Effect.Effect; readonly listRefs: ( input: VcsListRefsInput, ) => Effect.Effect; readonly pullCurrentBranch: (cwd: string) => Effect.Effect; readonly createWorktree: ( input: VcsCreateWorktreeInput, ) => Effect.Effect; readonly fetchPullRequestBranch: ( input: GitFetchPullRequestBranchInput, ) => Effect.Effect; readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect; readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect; readonly fetchRemote: (input: GitFetchRemoteInput) => Effect.Effect; readonly resolveRemoteTrackingCommit: ( input: GitResolveRemoteTrackingCommitInput, ) => Effect.Effect; readonly fetchRemoteBranch: ( input: GitFetchRemoteBranchInput, ) => Effect.Effect; readonly fetchRemoteTrackingBranch: ( input: GitFetchRemoteTrackingBranchInput, ) => Effect.Effect; readonly setBranchUpstream: ( input: GitSetBranchUpstreamInput, ) => Effect.Effect; readonly removeWorktree: ( input: VcsRemoveWorktreeInput, ) => Effect.Effect; readonly renameBranch: ( input: GitRenameBranchInput, ) => Effect.Effect; readonly createRef: ( input: VcsCreateRefInput, ) => Effect.Effect; readonly switchRef: ( input: VcsSwitchRefInput, ) => Effect.Effect; readonly initRepo: (input: VcsInitInput) => Effect.Effect; readonly listLocalBranchNames: (cwd: string) => Effect.Effect; } >()("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[][] { 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 { const remotes = new Map(); 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, 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")( 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(), }; }, ); const filterIgnoredPaths: VcsDriver.VcsDriver["Service"]["filterIgnoredPaths"] = Effect.fn( "filterIgnoredPaths", )(function* (cwd, relativePaths) { if (relativePaths.length === 0) { return relativePaths; } const ignoredPaths = new Set(); const chunks = chunkPathsForGitCheckIgnore(relativePaths); for (const chunk of chunks) { const result = yield* gitCommand( vcsProcess, "GitVcsDriver.filterIgnoredPaths", cwd, [...WORKSPACE_GIT_HARDENED_CONFIG_ARGS, "check-ignore", "--no-index", "-z", "--stdin"], { stdin: `${chunk.join("\0")}\0`, allowNonZeroExit: true, timeoutMs: 20_000, maxOutputBytes: WORKSPACE_FILES_MAX_OUTPUT_BYTES, appendTruncationMarker: true, }, ); if (result.exitCode !== 0 && result.exitCode !== 1) { return yield* new VcsProcessExitError({ operation: "GitVcsDriver.filterIgnoredPaths", command: "git check-ignore", cwd, exitCode: result.exitCode, detail: result.stderr.trim() || "git check-ignore failed", }); } for (const ignoredPath of splitNullSeparatedPaths(result.stdout, result.stdoutTruncated)) { ignoredPaths.add(ignoredPath); } } if (ignoredPaths.size === 0) { return relativePaths; } return relativePaths.filter((relativePath) => !ignoredPaths.has(relativePath)); }); const initRepository: VcsDriver.VcsDriver["Service"]["initRepository"] = (input) => gitCommand(vcsProcess, "GitVcsDriver.initRepository", input.cwd, ["init"], { timeoutMs: 10_000, maxOutputBytes: 64 * 1024, }).pipe(Effect.asVoid); const resolveHeadCommit = (cwd: string) => execute({ operation: "GitVcsDriver.checkpoints.resolveHeadCommit", cwd, args: ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"], allowNonZeroExit: true, }).pipe( Effect.map((result) => { if (result.exitCode !== 0) { return null; } const commit = result.stdout.trim(); return commit.length > 0 ? commit : null; }), ); const hasHeadCommit = (cwd: string) => execute({ operation: "GitVcsDriver.checkpoints.hasHeadCommit", cwd, args: ["rev-parse", "--verify", "HEAD"], allowNonZeroExit: true, }).pipe(Effect.map((result) => result.exitCode === 0)); const resolveCheckpointCommit = (cwd: string, checkpointRef: string) => execute({ operation: "GitVcsDriver.checkpoints.resolveCheckpointCommit", cwd, args: ["rev-parse", "--verify", "--quiet", `${checkpointRef}^{commit}`], allowNonZeroExit: true, }).pipe( Effect.map((result) => { if (result.exitCode !== 0) { return null; } const commit = result.stdout.trim(); return commit.length > 0 ? commit : null; }), ); const resolveGitCommonDir = (cwd: string) => Effect.gen(function* () { const result = yield* execute({ operation: "GitVcsDriver.checkpoints.resolveGitCommonDir", cwd, args: ["rev-parse", "--git-common-dir"], }); const gitCommonDir = result.stdout.trim(); return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir); }); const checkpoints: VcsDriver.VcsCheckpointOps = { captureCheckpoint: Effect.fn("GitVcsDriver.checkpoints.captureCheckpoint")(function* (input) { const operation = "GitVcsDriver.checkpoints.captureCheckpoint"; const gitCommonDir = yield* resolveGitCommonDir(input.cwd); const tempIndexPath = path.join( gitCommonDir, `t3-checkpoint-index-${NodeCrypto.randomUUID()}`, ); const commitEnv: NodeJS.ProcessEnv = { ...process.env, GIT_INDEX_FILE: tempIndexPath, GIT_AUTHOR_NAME: "T3 Code", GIT_AUTHOR_EMAIL: "t3code@users.noreply.github.com", GIT_COMMITTER_NAME: "T3 Code", GIT_COMMITTER_EMAIL: "t3code@users.noreply.github.com", }; const cleanupTempIndex = fileSystem .remove(tempIndexPath, { force: true }) .pipe(Effect.ignore); yield* Effect.gen(function* () { const headExists = yield* hasHeadCommit(input.cwd); if (headExists) { yield* execute({ operation, cwd: input.cwd, args: ["read-tree", "HEAD"], env: commitEnv, }); } yield* execute({ operation, cwd: input.cwd, args: ["add", "-A", "--", "."], env: commitEnv, }); const writeTreeResult = yield* execute({ operation, cwd: input.cwd, args: ["write-tree"], env: commitEnv, }); const treeOid = writeTreeResult.stdout.trim(); if (treeOid.length === 0) { return yield* new VcsProcessExitError({ operation, command: "git write-tree", cwd: input.cwd, exitCode: 0, detail: "git write-tree returned an empty tree oid.", }); } const message = `t3 checkpoint ref=${input.checkpointRef}`; const commitTreeResult = yield* execute({ operation, cwd: input.cwd, args: ["commit-tree", treeOid, "-m", message], env: commitEnv, }); const commitOid = commitTreeResult.stdout.trim(); if (commitOid.length === 0) { return yield* new VcsProcessExitError({ operation, command: "git commit-tree", cwd: input.cwd, exitCode: 0, detail: "git commit-tree returned an empty commit oid.", }); } yield* execute({ operation, cwd: input.cwd, args: ["update-ref", input.checkpointRef, commitOid], }); }).pipe(Effect.ensuring(cleanupTempIndex)); }), hasCheckpointRef: (input) => resolveCheckpointCommit(input.cwd, input.checkpointRef).pipe( Effect.map((commit) => commit !== null), ), restoreCheckpoint: Effect.fn("GitVcsDriver.checkpoints.restoreCheckpoint")(function* (input) { const operation = "GitVcsDriver.checkpoints.restoreCheckpoint"; let commitOid = yield* resolveCheckpointCommit(input.cwd, input.checkpointRef); if (!commitOid && input.fallbackToHead === true) { commitOid = yield* resolveHeadCommit(input.cwd); } if (!commitOid) { return false; } yield* execute({ operation, cwd: input.cwd, args: ["restore", "--source", commitOid, "--worktree", "--staged", "--", "."], }); yield* execute({ operation, cwd: input.cwd, args: ["clean", "-fd", "--", "."], }); const headExists = yield* hasHeadCommit(input.cwd); if (headExists) { yield* execute({ operation, cwd: input.cwd, args: ["reset", "--quiet", "--", "."], }); } return true; }), diffCheckpoints: Effect.fn("GitVcsDriver.checkpoints.diffCheckpoints")(function* (input) { const operation = "GitVcsDriver.checkpoints.diffCheckpoints"; yield* Effect.annotateCurrentSpan({ "checkpoint.cwd": input.cwd, "checkpoint.from_ref": input.fromCheckpointRef, "checkpoint.to_ref": input.toCheckpointRef, "checkpoint.ignore_whitespace": input.ignoreWhitespace, "checkpoint.fallback_from_to_head": input.fallbackFromToHead, }); let fromRevision: string = input.fromCheckpointRef; if (input.fallbackFromToHead === true) { const resolvedFromCommit = yield* resolveCheckpointCommit( input.cwd, input.fromCheckpointRef, ); if (resolvedFromCommit) { fromRevision = resolvedFromCommit; } else { const headCommit = yield* resolveHeadCommit(input.cwd); if (!headCommit) { return yield* new VcsProcessExitError({ operation, command: "git diff", cwd: input.cwd, exitCode: 1, detail: "Checkpoint ref is unavailable for diff operation.", }); } fromRevision = headCommit; } } const result = yield* execute({ operation, cwd: input.cwd, args: [ "diff", "--patch", "--no-color", "--no-ext-diff", "--no-textconv", ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []), `${fromRevision}^{commit}`, `${input.toCheckpointRef}^{commit}`, ], allowNonZeroExit: true, maxOutputBytes: CHECKPOINT_DIFF_MAX_OUTPUT_BYTES, }); if (result.exitCode !== 0) { return yield* new VcsProcessExitError({ operation, command: "git diff", cwd: input.cwd, exitCode: result.exitCode, detail: result.stderr.trim() || "Checkpoint ref is unavailable for diff operation.", }); } return result.stdout; }), deleteCheckpointRefs: Effect.fn("GitVcsDriver.checkpoints.deleteCheckpointRefs")( function* (input) { yield* Effect.forEach( input.checkpointRefs, (checkpointRef) => execute({ operation: "GitVcsDriver.checkpoints.deleteCheckpointRefs", cwd: input.cwd, args: ["update-ref", "-d", checkpointRef], allowNonZeroExit: true, }), { discard: true }, ); }, ), }; return { capabilities, execute, checkpoints, detectRepository, isInsideWorkTree, listWorkspaceFiles, listRemotes, filterIgnoredPaths, initRepository, }; }); export const makeVcsDriver = Effect.gen(function* () { const driver = yield* makeVcsDriverShape(); return VcsDriver.VcsDriver.of(driver); }); export const make = Effect.gen(function* () { const git = yield* makeGitVcsDriverCore(); return GitVcsDriver.of(git); }); export const vcsLayer = Layer.effect(VcsDriver.VcsDriver, makeVcsDriver); export const layer = Layer.effect(GitVcsDriver, make);