mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-02 07:31:29 +00:00
114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
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)
|
|
}
|