export * as Git from "./git" import path from "path" import { Context, Effect, Layer } from "effect" import { ChildProcess } from "effect/unstable/process" import { AbsolutePath } from "./schema" import { AppFileSystem } from "./filesystem" import { AppProcess } from "./process" export interface Repo { /** * The root directory of the working tree that contains the input path. * * For `/home/me/app/src/file.ts` in a normal clone, this is `/home/me/app`. * For `/home/me/app-feature/src/file.ts` in a linked worktree, this is * `/home/me/app-feature`. */ readonly directory: AbsolutePath /** * The shared Git storage directory used by this repo and any linked worktrees. * * For a normal clone at `/home/me/app`, this is usually `/home/me/app/.git`. * For a linked worktree at `/home/me/app-feature` whose main checkout is * `/home/me/app`, this is usually `/home/me/app/.git`. */ readonly store: AbsolutePath } export interface Interface { readonly find: (input: AbsolutePath) => Effect.Effect readonly remote: (repo: Repo, name?: string) => Effect.Effect readonly roots: (repo: Repo) => Effect.Effect } export class Service extends Context.Service()("@opencode/GitV2") {} export const layer = Layer.effect( Service, Effect.gen(function* () { const fs = yield* AppFileSystem.Service const proc = yield* AppProcess.Service const find = Effect.fn("Git.find")(function* (input: AbsolutePath) { const dotgit = yield* fs.up({ targets: [".git"], start: input }).pipe( Effect.map((matches) => matches[0]), Effect.catch(() => Effect.succeed(undefined)), ) if (!dotgit) return undefined const cwd = path.dirname(dotgit) const git = run(cwd, proc) const topLevel = yield* git(["rev-parse", "--show-toplevel"]) const commonDir = yield* git(["rev-parse", "--git-common-dir"]) if (commonDir.exitCode !== 0) return undefined return { directory: AbsolutePath.make(topLevel.exitCode === 0 ? resolvePath(cwd, topLevel.text) : cwd), store: AbsolutePath.make(resolvePath(cwd, commonDir.text)), } satisfies Repo }) const remote = Effect.fn("Git.remote")(function* (repo: Repo, name = "origin") { const result = yield* run(repo.directory, proc)(["remote", "get-url", name]) if (result.exitCode !== 0) return undefined return result.text.trim() || undefined }) const roots = Effect.fn("Git.roots")(function* (repo: Repo) { const result = yield* run(repo.directory, proc)(["rev-list", "--max-parents=0", "HEAD"]) if (result.exitCode !== 0) return [] return result.text .split("\n") .map((item) => item.trim()) .filter(Boolean) .toSorted() }) return Service.of({ find, remote, roots }) }), ) export const defaultLayer = layer.pipe( Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppProcess.defaultLayer), ) interface Result { readonly exitCode: number readonly text: string } function run(cwd: string, proc: AppProcess.Interface) { return (args: string[]) => proc .run( ChildProcess.make("git", args, { cwd, extendEnv: true, stdin: "ignore", }), ) .pipe( Effect.map((result) => ({ exitCode: result.exitCode, text: result.stdout.toString("utf8") }) satisfies Result), Effect.catch(() => Effect.succeed({ exitCode: 1, text: "" } satisfies Result)), ) } function resolvePath(cwd: string, value: string) { const trimmed = value.replace(/[\r\n]+$/, "") if (!trimmed) return cwd const normalized = AppFileSystem.windowsPath(trimmed) if (path.isAbsolute(normalized)) return path.normalize(normalized) return path.resolve(cwd, normalized) }