Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222export * as VcsHgPlugin from "./hg.js"
import path from "path"import { Effect } from "effect"import { ChildProcess } from "effect/unstable/process"import { define } from "@opencode/plugin/effect/plugin"import { FileDiff } from "@opencode/schema/file-diff"import { FileStatus, Info, Mode } from "@opencode/schema/vcs"import { FSUtil } from "@opencode/util/fs-util"import { AppProcess } from "@opencode/util/process"import { Location } from "../../location.js"import type { Adapter, DiffOptions } from "../../vcs.js"import { DiffError } from "../../vcs.js"import { addPatch, chunksByFile, countPatch, deletePatch, emptyPatch, MAX_PATCH_BYTES, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES,} from "../../vcs/patch.js"
export const Plugin = define({ id: "opencode.vcs.hg", effect: Effect.fn("VcsHgPlugin")(function* (ctx) { const location = yield* Location.Service if (location.vcs?.type !== "hg") return
const processes = yield* AppProcess.Service const fs = yield* FSUtil.Service const adapter = make(processes, fs, { directory: location.directory, worktree: location.project.directory, })
yield* ctx.vcs.transform((editor) => { editor.add({ id: "hg", name: "Mercurial", info: () => adapter.info(), branches: (input) => adapter.branches({ search: input.search, limit: input.limit }), status: () => adapter.status(), diff: (input) => adapter.diff(input.mode, { context: input.context, base: input.base }), }) }) }),})
/** * Mercurial adapter for the Vcs service. `hg diff --git` emits git-format * patches for tracked changes; untracked (`?`) and missing (`!`) files never * appear in `hg diff`, so their patches are synthesized from file contents. */function make( proc: AppProcess.Interface, fs: FSUtil.Interface, input: { directory: string; worktree: string },): Adapter { const hg = makeHg(proc, input.worktree) // All commands run from the worktree root (hg prints root-relative paths); // this pathspec scopes them to the requested directory. const scope = path.relative(input.worktree, input.directory) || "."
const patchFor = Effect.fnUntraced(function* (item: Item, rev: string | undefined, chunk: string | undefined) { if (chunk !== undefined) return chunk if (item.code === "?") { const content = yield* fs .readFileString(path.join(input.worktree, item.file)) .pipe(Effect.orElseSucceed(() => undefined)) if (content === undefined || Buffer.byteLength(content) > MAX_PATCH_BYTES) return emptyPatch(item.file) return addPatch(item.file, content) } if (item.code === "!") { const content = yield* hg.cat(rev ?? ".", item.file) if (content === undefined || Buffer.byteLength(content) > MAX_PATCH_BYTES) return emptyPatch(item.file) return deletePatch(item.file, content) } return emptyPatch(item.file) })
const diffAgainst = Effect.fnUntraced(function* (rev: string | undefined, options?: DiffOptions) { const [items, batch] = yield* Effect.all( [hg.status(rev, scope), hg.diff(rev, scope, { context: options?.context ?? PATCH_CONTEXT_LINES })], { concurrency: 2 }, ) const chunks = chunksByFile(batch, () => undefined) const list: FileDiff.Info[] = [] for (const item of items.toSorted((a, b) => a.file.localeCompare(b.file))) { const patch = yield* patchFor(item, rev, chunks.get(item.file)) const counts = countPatch(patch) list.push({ file: item.file, patch, additions: counts.additions, deletions: counts.deletions, status: item.status, }) } return list })
return { info: Effect.fn("VcsHg.info")(function* () { return { branch: { current: yield* hg.branch(), default: "default" } } satisfies Info }), branches: Effect.fn("VcsHg.branches")(function* () { return [] }), status: Effect.fn("VcsHg.status")(function* () { return (yield* diffAgainst(undefined, { context: 0 })).map( (item) => ({ file: item.file, additions: item.additions, deletions: item.deletions, status: item.status, }) satisfies FileStatus, ) }), diff: Effect.fn("VcsHg.diff")(function* (mode: Mode, options?: DiffOptions) { if (mode === "working") return yield* diffAgainst(undefined, options) if (mode === "committed" || options?.base !== undefined) { return yield* new DiffError({ message: "The Mercurial provider does not support committed reviews or explicit bases", }) }
const branch = yield* hg.branch() if (!branch || branch === "default") return [] const ancestor = yield* hg.ancestor() if (!ancestor) return [] return yield* diffAgainst(ancestor, options) }), }}
type Kind = FileStatus["status"]
interface Item { readonly file: string readonly code: string readonly status: Kind}
const kind = (code: string): Kind => { if (code === "A" || code === "?") return "added" if (code === "R" || code === "!") return "deleted" return "modified"}
function makeHg(proc: AppProcess.Interface, worktree: string) { const run = Effect.fnUntraced( function* (args: string[], opts?: { maxOutputBytes?: number }) { const result = yield* proc.run( ChildProcess.make("hg", args, { cwd: worktree, env: { HGPLAIN: "1" }, extendEnv: true, stdin: "ignore", }), { maxOutputBytes: opts?.maxOutputBytes }, ) return { exitCode: result.exitCode, text: () => result.stdout.toString("utf8"), truncated: result.stdoutTruncated || result.stderrTruncated, } }, Effect.orElseSucceed(() => ({ exitCode: 1, text: () => "", truncated: false })), )
const status = Effect.fn("VcsHg.statusNames")(function* (rev: string | undefined, scope: string) { const result = yield* run(["status", "-0", ...(rev ? ["--rev", rev] : []), scope]) if (result.exitCode !== 0) return [] return result .text() .split("\0") .filter(Boolean) .flatMap((entry) => { const code = entry.slice(0, 1) const file = entry.slice(2) if (!file) return [] return [{ file, code, status: kind(code) } satisfies Item] }) })
const diff = Effect.fn("VcsHg.diffPatch")(function* ( rev: string | undefined, scope: string, options: { context: number }, ) { const result = yield* run( ["diff", "--git", "--unified", String(options.context), ...(rev ? ["-r", rev] : []), scope], { maxOutputBytes: MAX_TOTAL_PATCH_BYTES }, ) if (result.exitCode !== 0) return { text: "", truncated: false } return { text: result.text(), truncated: result.truncated } })
const branch = Effect.fn("VcsHg.branch")(function* () { const result = yield* run(["branch"]) if (result.exitCode !== 0) return undefined return result.text().trim() || undefined })
const ancestor = Effect.fn("VcsHg.ancestor")(function* () { const result = yield* run(["log", "-r", "ancestor(., default)", "-T", "{node}"]) if (result.exitCode !== 0) return undefined return result.text().trim() || undefined })
const cat = Effect.fn("VcsHg.cat")(function* (rev: string, file: string) { const result = yield* run(["cat", "-r", rev, "--", file], { maxOutputBytes: MAX_PATCH_BYTES }) if (result.exitCode !== 0 || result.truncated) return undefined return result.text() })
return { status, diff, branch, ancestor, cat }}