From 0ab2d783e86a4855b1d9ba4e107ad4267798edda Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Tue, 25 Aug 2026 20:26:43 +0530 Subject: [PATCH] refactor(core): move git vcs into internal plugin (#44992) --- packages/core/src/plugin/internal.ts | 2 ++ packages/core/src/{ => plugin}/vcs/git.ts | 37 ++++++++++++++++++++--- packages/core/src/vcs.ts | 6 ++-- packages/core/test/vcs.test.ts | 17 +++++++++-- 4 files changed, 51 insertions(+), 11 deletions(-) rename packages/core/src/{ => plugin}/vcs/git.ts (93%) diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 3205ac8c645..4219a85f452 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -85,6 +85,7 @@ import { PluginRuntime } from "./runtime.js" import { SkillPlugin } from "./skill.js" import { SystemPromptPlugin } from "./system-prompt.js" import { VariantPlugin } from "./variant.js" +import { VcsGitPlugin } from "./vcs/git.js" import { WarmingPlugin } from "./warming.js" import { WellKnownPlugin } from "../wellknown/plugin.js" @@ -235,6 +236,7 @@ const pre = [ ConfigMCPPlugin.Plugin, MCPCodeModeExclusionPlugin.Plugin, WellKnownPlugin.Plugin, + VcsGitPlugin.Plugin, AgentPlugin.Plugin, PlanPlugin.Plugin, CommandPlugin.Plugin, diff --git a/packages/core/src/vcs/git.ts b/packages/core/src/plugin/vcs/git.ts similarity index 93% rename from packages/core/src/vcs/git.ts rename to packages/core/src/plugin/vcs/git.ts index d6d0f51d09b..434974307fa 100644 --- a/packages/core/src/vcs/git.ts +++ b/packages/core/src/plugin/vcs/git.ts @@ -1,20 +1,47 @@ -export * as VcsGit from "./git.js" +export * as VcsGitPlugin from "./git.js" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect } from "effect" import { ChildProcess } from "effect/unstable/process" import { FileDiff } from "@opencode-ai/schema/file-diff" import { BranchList, FileStatus, Info, Mode } from "@opencode-ai/schema/vcs" import { AppProcess } from "@opencode-ai/util/process" -import type { Adapter, BranchOptions, DiffOptions } from "../vcs.js" -import { chunksByFile, emptyPatch, MAX_PATCH_BYTES, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./patch.js" -import type { Patch } from "./patch.js" +import { Location } from "../../location.js" +import type { Adapter, BranchOptions, DiffOptions } from "../../vcs.js" +import { chunksByFile, emptyPatch, MAX_PATCH_BYTES, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "../../vcs/patch.js" +import type { Patch } from "../../vcs/patch.js" + +export const Plugin = define({ + id: "opencode.vcs.git", + effect: Effect.fn("VcsGitPlugin")(function* (ctx) { + const location = yield* Location.Service + if (location.vcs?.type !== "git") return + + const processes = yield* AppProcess.Service + const adapter = make(processes, { + directory: location.directory, + worktree: location.project.directory, + }) + + yield* ctx.vcs.transform((draft) => { + draft.add({ + id: "git", + name: "Git", + 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 }), + }) + }) + }), +}) /** * Git adapter for the Vcs service. Ported from the V1 pipeline: patches are * batched through one `git diff` invocation where possible and capped by * per-file and total byte budgets, falling back to empty patches when capped. */ -export function make(proc: AppProcess.Interface, input: { directory: string; worktree: string }): Adapter { +function make(proc: AppProcess.Interface, input: { directory: string; worktree: string }): Adapter { // Listing commands scope pathspecs to the requested directory; per-file // commands run from the worktree root because git lists root-relative paths. const ctx: Ctx = { git: makeGit(proc), directory: input.directory, worktree: input.worktree } diff --git a/packages/core/src/vcs.ts b/packages/core/src/vcs.ts index 301bca79bd6..4fe85545719 100644 --- a/packages/core/src/vcs.ts +++ b/packages/core/src/vcs.ts @@ -13,7 +13,6 @@ import { Location } from "./location.js" import { AppProcess } from "@opencode-ai/util/process" import { Bus } from "./bus.js" import { State } from "./state.js" -import { VcsGit } from "./vcs/git.js" import { VcsHg } from "./vcs/hg.js" import { emptyPatch, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./vcs/patch.js" @@ -49,7 +48,6 @@ export class Service extends Context.Service()("@opencode/Vc // 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 === "git") return VcsGit.make(proc, scope) if (location.vcs?.type === "hg") return VcsHg.make(proc, fs, scope) } @@ -69,7 +67,7 @@ const layer = Layer.effect( canonical: location.project.canonical, ...(vcs ? { store: vcs.store } : {}), } - const decodeInfo = Schema.decodeUnknownEffect(Info) + const decodeInfo = Schema.decodeUnknownEffect(Schema.toType(Info)) const decodeBranches = Schema.decodeUnknownEffect(BranchList) const decodeStatus = Schema.decodeUnknownEffect(Schema.Array(FileStatus)) const decodeDiff = Schema.decodeUnknownEffect(Schema.Array(FileDiff.Info)) @@ -112,7 +110,7 @@ const layer = Layer.effect( if (changed) yield* bus.publish(VcsEvent.BranchUpdated, { branch: next.branch.current }) }) - if (vcs && native) { + if (vcs) { const store = yield* fs.realPath(vcs.store).pipe(Effect.orElseSucceed(() => vcs.store)) const isBranchMetadata = vcs.type === "git" diff --git a/packages/core/test/vcs.test.ts b/packages/core/test/vcs.test.ts index 0558af39d0c..b939e7fe1c9 100644 --- a/packages/core/test/vcs.test.ts +++ b/packages/core/test/vcs.test.ts @@ -4,20 +4,23 @@ import fs from "fs/promises" import path from "path" import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect" import { LayerNode } from "@opencode-ai/util/effect/layer-node" +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 { VcsGitPlugin } from "@opencode-ai/core/plugin/vcs/git" import type { VcsDefinition, VcsDiffInput } from "@opencode-ai/plugin/effect/vcs" 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 provide = (directory: string, input: { git?: boolean } = {}) => Effect.provide( - LayerNode.compile(LayerNode.group([Vcs.node, Bus.node]), [ + LayerNode.compile(LayerNode.group([Vcs.node, Bus.node, Location.node, AppProcess.node]), [ [ Location.node, Layer.succeed( @@ -42,7 +45,17 @@ const withTmp = (f: (directory: string) => Effect.Effect) => const withGit = (f: (directory: string) => Effect.Effect) => withTmp((directory) => Effect.promise(() => initRepo(directory)).pipe( - Effect.andThen(f(directory).pipe(provide(directory, { git: true }))), + Effect.andThen( + Effect.gen(function* () { + const vcs = yield* Vcs.Service + const context = host() + yield* VcsGitPlugin.Plugin.effect({ + ...context, + vcs: { ...context.vcs, transform: vcs.transform, reload: vcs.reload }, + }) + return yield* f(directory) + }).pipe(provide(directory, { git: true })), + ), ), )