refactor(core): move mercurial vcs into internal plugin (#44993)

This commit is contained in:
Shoubhit Dash 2026-08-25 20:35:21 +05:30 committed by GitHub
parent 0ab2d783e8
commit b71291c05a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 58 additions and 28 deletions

View file

@ -83,6 +83,7 @@ import { ProviderPlugins } from "./provider.js"
import { WebSearchPlugins } from "./websearch/index.js"
import { PluginRuntime } from "./runtime.js"
import { SkillPlugin } from "./skill.js"
import { VcsHgPlugin } from "./vcs/hg.js"
import { SystemPromptPlugin } from "./system-prompt.js"
import { VariantPlugin } from "./variant.js"
import { VcsGitPlugin } from "./vcs/git.js"
@ -241,6 +242,7 @@ const pre = [
PlanPlugin.Plugin,
CommandPlugin.Plugin,
SkillPlugin.Plugin,
VcsHgPlugin.Plugin,
...SystemPromptPlugin.Plugins,
ModelsDevPlugin,
...ProviderPlugins,

View file

@ -1,13 +1,15 @@
export * as VcsHg from "./hg.js"
export * as VcsHgPlugin from "./hg.js"
import path from "path"
import { Effect } from "effect"
import { ChildProcess } from "effect/unstable/process"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { FileStatus, Info, Mode } from "@opencode-ai/schema/vcs"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { AppProcess } from "@opencode-ai/util/process"
import type { Adapter, DiffOptions } from "../vcs.js"
import { Location } from "../../location.js"
import type { Adapter, DiffOptions } from "../../vcs.js"
import {
addPatch,
chunksByFile,
@ -17,14 +19,40 @@ import {
MAX_PATCH_BYTES,
MAX_TOTAL_PATCH_BYTES,
PATCH_CONTEXT_LINES,
} from "./patch.js"
} 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((draft) => {
draft.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 }),
})
})
}),
})
/**
* 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.
*/
export function make(
function make(
proc: AppProcess.Interface,
fs: FSUtil.Interface,
input: { directory: string; worktree: string },

View file

@ -10,10 +10,8 @@ import { VcsEvent } from "@opencode-ai/schema/vcs-event"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Location } from "./location.js"
import { AppProcess } from "@opencode-ai/util/process"
import { Bus } from "./bus.js"
import { State } from "./state.js"
import { VcsHg } from "./vcs/hg.js"
import { emptyPatch, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./vcs/patch.js"
export { BranchList, FileStatus, Info, Mode }
@ -43,24 +41,14 @@ interface Data {
export class Service extends Context.Service<Service, Interface>()("@opencode/Vcs") {}
// Adapter seam: one working-copy implementation per VCS type, selected by the
// resolved location. Locations without a supported VCS degrade to empty
// results so callers never need to special-case.
const adapter = (proc: AppProcess.Interface, fs: FSUtil.Interface, location: Location.Interface) => {
const scope = { directory: location.directory, worktree: location.project.directory }
if (location.vcs?.type === "hg") return VcsHg.make(proc, fs, scope)
}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const proc = yield* AppProcess.Service
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const bus = yield* Bus.Service
const native = adapter(proc, fs, location)
const vcs = location.vcs
const current = { info: native ? yield* native.info() : ({ branch: {} } satisfies Info) }
const current: { info: Info } = { info: { branch: {} } }
const scope = {
directory: location.directory,
worktree: location.project.directory,
@ -102,9 +90,7 @@ const layer = Layer.effect(
const provider = selected()
const next: Info = provider
? yield* protect(provider, "info", provider.info(scope).pipe(Effect.flatMap(decodeInfo)), { branch: {} })
: native
? yield* native.info()
: { branch: {} }
: { branch: {} }
const changed = current.info.branch.current !== next.branch.current
current.info = next
if (changed) yield* bus.publish(VcsEvent.BranchUpdated, { branch: next.branch.current })
@ -140,8 +126,7 @@ const layer = Layer.effect(
provider.branches({ ...scope, ...options }).pipe(Effect.flatMap(decodeBranches)),
[],
)
if (!native) return []
return yield* native.branches(options)
return []
}),
status: Effect.fn("Vcs.status")(function* () {
const provider = selected()
@ -155,12 +140,11 @@ const layer = Layer.effect(
),
[],
)
if (!native) return []
return yield* native.status()
return []
}),
diff: Effect.fn("Vcs.diff")(function* (mode: Mode, options?: DiffOptions) {
const provider = selected()
if (!provider) return native ? yield* native.diff(mode, options) : []
if (!provider) return []
const rows = yield* protect(
provider,
"diff",
@ -189,5 +173,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer: layer,
deps: [AppProcess.node, FSUtil.node, Location.node, Bus.node],
deps: [FSUtil.node, Location.node, Bus.node],
})

View file

@ -4,21 +4,25 @@ import fs from "fs/promises"
import path from "path"
import { Effect, Fiber, Layer, Stream } from "effect"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { AppProcess } from "@opencode-ai/util/process"
import { Bus } from "@opencode-ai/core/bus"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Vcs } from "@opencode-ai/core/vcs"
import { VcsHgPlugin } from "@opencode-ai/core/plugin/vcs/hg"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import { VcsEvent } from "@opencode-ai/schema/vcs-event"
import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
import { it } from "./lib/effect"
import { host } from "./plugin/host"
const describeHg = Bun.which("hg") ? describe : describe.skip
const provide = (directory: string) =>
Effect.provide(
LayerNode.compile(LayerNode.group([Vcs.node, Bus.node]), [
LayerNode.compile(LayerNode.group([Vcs.node, Bus.node, Location.node, AppProcess.node, FSUtil.node]), [
[
Location.node,
Layer.succeed(
@ -42,7 +46,19 @@ const withTmp = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
const withHg = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
withTmp((directory) =>
Effect.promise(() => hg(directory, "init")).pipe(Effect.andThen(f(directory).pipe(provide(directory)))),
Effect.promise(() => hg(directory, "init")).pipe(
Effect.andThen(
Effect.gen(function* () {
const vcs = yield* Vcs.Service
const context = host()
yield* VcsHgPlugin.Plugin.effect({
...context,
vcs: { ...context.vcs, transform: vcs.transform, reload: vcs.reload },
})
return yield* f(directory)
}).pipe(provide(directory)),
),
),
)
async function hg(directory: string, ...args: string[]) {