| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- 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<Repo | undefined>
- readonly remote: (repo: Repo, name?: string) => Effect.Effect<string | undefined>
- readonly roots: (repo: Repo) => Effect.Effect<string[]>
- }
- export class Service extends Context.Service<Service, Interface>()("@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)
- }
|