import { Cache, Data, Duration, Effect, Exit, FileSystem, Layer, Option, Path, PlatformError, Ref, Result, Schema, Scope, Semaphore, Stream,} from "effect";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { GitCommandError, type GitBranch } from "@t3tools/contracts";import { dedupeRemoteBranchesWithLocalMatches } from "@t3tools/shared/git";import { compactTraceAttributes } from "../../observability/Attributes.ts";import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../../observability/Metrics.ts";import { GitCore, type ExecuteGitProgress, type GitCommitOptions, type GitCoreShape, type ExecuteGitInput, type ExecuteGitResult,} from "../Services/GitCore.ts";import { parseRemoteNames, parseRemoteNamesInGitOrder, parseRemoteRefWithRemoteNames,} from "../remoteRefs.ts";import { ServerConfig } from "../../config.ts";import { decodeJsonResult } from "@t3tools/shared/schemaJson"; 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 WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024;const WORKSPACE_GIT_HARDENED_CONFIG_ARGS = [ "-c", "core.fsmonitor=false", "-c", "core.untrackedCache=false",] as const;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 DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const;const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100; type TraceTailState = { processedChars: number; remainder: string;}; class StatusUpstreamRefreshCacheKey extends Data.Class<{ gitCommonDir: string; upstreamRef: string; remoteName: string; upstreamBranch: string;}> {} interface ExecuteGitOptions { stdin?: string | undefined; timeoutMs?: number | undefined; allowNonZeroExit?: boolean | undefined; fallbackErrorMessage?: string | undefined; maxOutputBytes?: number | undefined; truncateOutputAtMaxBytes?: boolean | undefined; progress?: 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 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: readonly 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 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( branches: ReadonlyArray<GitBranch>, query?: string,): ReadonlyArray<GitBranch> { if (!query) { return branches; } const normalizedQuery = query.toLowerCase(); return branches.filter((branch) => branch.name.toLowerCase().includes(normalizedQuery));} function paginateBranches(input: { branches: ReadonlyArray<GitBranch>; cursor?: number | undefined; limit?: number | undefined;}): { branches: ReadonlyArray<GitBranch>; nextCursor: number | null; totalCount: number;} { const cursor = input.cursor ?? 0; const limit = input.limit ?? GIT_LIST_BRANCHES_DEFAULT_LIMIT; const totalCount = input.branches.length; const branches = input.branches.slice(cursor, cursor + limit); const nextCursor = cursor + branches.length < totalCount ? cursor + branches.length : null; return { branches, nextCursor, totalCount, };} function sanitizeRemoteName(value: string): string { const sanitized = value .trim() .replace(/[^A-Za-z0-9._-]+/g, "-") .replace(/^-+|-+$/g, ""); return sanitized.length > 0 ? sanitized : "fork";} function normalizeRemoteUrl(value: string): string { return value .trim() .replace(/\/+$/g, "") .replace(/\.git$/i, "") .toLowerCase();} 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; upstreamBranch: string } | null { const parsed = parseRemoteRefWithRemoteNames(upstreamRef, remoteNames); if (!parsed) { return null; } return { upstreamRef, remoteName: parsed.remoteName, upstreamBranch: parsed.branchName, };} function parseUpstreamRefByFirstSeparator( upstreamRef: string,): { upstreamRef: string; remoteName: string; upstreamBranch: string } | null { const separatorIndex = upstreamRef.indexOf("/"); if (separatorIndex <= 0 || separatorIndex === upstreamRef.length - 1) { return null; } const remoteName = upstreamRef.slice(0, separatorIndex).trim(); const upstreamBranch = upstreamRef.slice(separatorIndex + 1).trim(); if (remoteName.length === 0 || upstreamBranch.length === 0) { return null; } return { upstreamRef, remoteName, upstreamBranch, };} 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 upstreamBranch = upstreamBranchRaw.trim(); if (branchName.length === 0 || upstreamBranch.length === 0) { continue; } if (upstreamBranch === 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 commandLabel(args: readonly string[]): string { return `git ${args.join(" ")}`;} function parseDefaultBranchFromRemoteHeadRef(value: string, remoteName: string): string | null { const trimmed = value.trim(); const prefix = `refs/remotes/${remoteName}/`; if (!trimmed.startsWith(prefix)) { return null; } const branch = trimmed.slice(prefix.length).trim(); return branch.length > 0 ? branch : null;} function createGitCommandError( operation: string, cwd: string, args: readonly string[], detail: string, cause?: unknown,): GitCommandError { return new GitCommandError({ operation, command: commandLabel(args), cwd, detail, ...(cause !== undefined ? { cause } : {}), });} function quoteGitCommand(args: ReadonlyArray<string>): string { return `git ${args.join(" ")}`;} function toGitCommandError( input: Pick<ExecuteGitInput, "operation" | "cwd" | "args">, detail: string,) { return (cause: unknown) => Schema.is(GitCommandError)(cause) ? cause : new GitCommandError({ operation: input.operation, command: quoteGitCommand(input.args), cwd: input.cwd, detail: `${cause instanceof Error && cause.message.length > 0 ? cause.message : "Unknown error"} - ${detail}`, ...(cause !== undefined ? { cause } : {}), });} interface Trace2Monitor { readonly env: NodeJS.ProcessEnv; readonly flush: Effect.Effect<void, never>;} const nowUnixNano = (): bigint => BigInt(Date.now()) * 1_000_000n; const addCurrentSpanEvent = (name: string, attributes: Record<string, unknown>) => Effect.currentSpan.pipe( Effect.tap((span) => Effect.sync(() => { span.event(name, nowUnixNano(), compactTraceAttributes(attributes)); }), ), Effect.catch(() => 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<ExecuteGitInput, "operation" | "cwd" | "args">, progress: 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( `GitCore.trace2: failed to parse trace line for ${quoteGitCommand(input.args)} in ${input.cwd}`, 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") { hookStartByChildKey.set(childKey, { hookName, startedAtMs: Date.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.code; const exitCode = typeof code === "number" && Number.isInteger(code) ? code : null; const durationMs = started ? Math.max(0, Date.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(Lines 1–500 of 2175. Load more
import { Cache, Data, Duration, Effect, Exit, FileSystem, Layer, Option, Path, PlatformError, Ref, Result, Schema, Scope, Semaphore, Stream, } from "effect"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { GitCommandError, type GitBranch } from "@t3tools/contracts"; import { dedupeRemoteBranchesWithLocalMatches } from "@t3tools/shared/git"; import { compactTraceAttributes } from "../../observability/Attributes.ts"; import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../../observability/Metrics.ts"; import { GitCore, type ExecuteGitProgress, type GitCommitOptions, type GitCoreShape, type ExecuteGitInput, type ExecuteGitResult, } from "../Services/GitCore.ts"; import { parseRemoteNames, parseRemoteNamesInGitOrder, parseRemoteRefWithRemoteNames, } from "../remoteRefs.ts"; import { ServerConfig } from "../../config.ts"; import { decodeJsonResult } from "@t3tools/shared/schemaJson"; 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 WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024; const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024; const WORKSPACE_GIT_HARDENED_CONFIG_ARGS = [ "-c", "core.fsmonitor=false", "-c", "core.untrackedCache=false", ] as const; 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 DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const; const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100; type TraceTailState = { processedChars: number; remainder: string; }; class StatusUpstreamRefreshCacheKey extends Data.Class<{ gitCommonDir: string; upstreamRef: string; remoteName: string; upstreamBranch: string; }> {} interface ExecuteGitOptions { stdin?: string | undefined; timeoutMs?: number | undefined; allowNonZeroExit?: boolean | undefined; fallbackErrorMessage?: string | undefined; maxOutputBytes?: number | undefined; truncateOutputAtMaxBytes?: boolean | undefined; progress?: 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 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: readonly 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 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( branches: ReadonlyArray<GitBranch>, query?: string, ): ReadonlyArray<GitBranch> { if (!query) { return branches; } const normalizedQuery = query.toLowerCase(); return branches.filter((branch) => branch.name.toLowerCase().includes(normalizedQuery)); } function paginateBranches(input: { branches: ReadonlyArray<GitBranch>; cursor?: number | undefined; limit?: number | undefined; }): { branches: ReadonlyArray<GitBranch>; nextCursor: number | null; totalCount: number; } { const cursor = input.cursor ?? 0; const limit = input.limit ?? GIT_LIST_BRANCHES_DEFAULT_LIMIT; const totalCount = input.branches.length; const branches = input.branches.slice(cursor, cursor + limit); const nextCursor = cursor + branches.length < totalCount ? cursor + branches.length : null; return { branches, nextCursor, totalCount, }; } function sanitizeRemoteName(value: string): string { const sanitized = value .trim() .replace(/[^A-Za-z0-9._-]+/g, "-") .replace(/^-+|-+$/g, ""); return sanitized.length > 0 ? sanitized : "fork"; } function normalizeRemoteUrl(value: string): string { return value .trim() .replace(/\/+$/g, "") .replace(/\.git$/i, "") .toLowerCase(); } 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; upstreamBranch: string } | null { const parsed = parseRemoteRefWithRemoteNames(upstreamRef, remoteNames); if (!parsed) { return null; } return { upstreamRef, remoteName: parsed.remoteName, upstreamBranch: parsed.branchName, }; } function parseUpstreamRefByFirstSeparator( upstreamRef: string, ): { upstreamRef: string; remoteName: string; upstreamBranch: string } | null { const separatorIndex = upstreamRef.indexOf("/"); if (separatorIndex <= 0 || separatorIndex === upstreamRef.length - 1) { return null; } const remoteName = upstreamRef.slice(0, separatorIndex).trim(); const upstreamBranch = upstreamRef.slice(separatorIndex + 1).trim(); if (remoteName.length === 0 || upstreamBranch.length === 0) { return null; } return { upstreamRef, remoteName, upstreamBranch, }; } 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 upstreamBranch = upstreamBranchRaw.trim(); if (branchName.length === 0 || upstreamBranch.length === 0) { continue; } if (upstreamBranch === 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 commandLabel(args: readonly string[]): string { return `git ${args.join(" ")}`; } function parseDefaultBranchFromRemoteHeadRef(value: string, remoteName: string): string | null { const trimmed = value.trim(); const prefix = `refs/remotes/${remoteName}/`; if (!trimmed.startsWith(prefix)) { return null; } const branch = trimmed.slice(prefix.length).trim(); return branch.length > 0 ? branch : null; } function createGitCommandError( operation: string, cwd: string, args: readonly string[], detail: string, cause?: unknown, ): GitCommandError { return new GitCommandError({ operation, command: commandLabel(args), cwd, detail, ...(cause !== undefined ? { cause } : {}), }); } function quoteGitCommand(args: ReadonlyArray<string>): string { return `git ${args.join(" ")}`; } function toGitCommandError( input: Pick<ExecuteGitInput, "operation" | "cwd" | "args">, detail: string, ) { return (cause: unknown) => Schema.is(GitCommandError)(cause) ? cause : new GitCommandError({ operation: input.operation, command: quoteGitCommand(input.args), cwd: input.cwd, detail: `${cause instanceof Error && cause.message.length > 0 ? cause.message : "Unknown error"} - ${detail}`, ...(cause !== undefined ? { cause } : {}), }); } interface Trace2Monitor { readonly env: NodeJS.ProcessEnv; readonly flush: Effect.Effect<void, never>; } const nowUnixNano = (): bigint => BigInt(Date.now()) * 1_000_000n; const addCurrentSpanEvent = (name: string, attributes: Record<string, unknown>) => Effect.currentSpan.pipe( Effect.tap((span) => Effect.sync(() => { span.event(name, nowUnixNano(), compactTraceAttributes(attributes)); }), ), Effect.catch(() => 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<ExecuteGitInput, "operation" | "cwd" | "args">, progress: 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( `GitCore.trace2: failed to parse trace line for ${quoteGitCommand(input.args)} in ${input.cwd}`, 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") { hookStartByChildKey.set(childKey, { hookName, startedAtMs: Date.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.code; const exitCode = typeof code === "number" && Number.isInteger(code) ? code : null; const durationMs = started ? Math.max(0, Date.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(