mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(vcs): initialize through provider definitions
This commit is contained in:
@@ -2225,7 +2225,7 @@ export interface WorktreeApi<E = never> {
|
||||
|
||||
export type VcsInitInput = {
|
||||
readonly location?: { readonly directory?: string | undefined } | undefined
|
||||
readonly provider?: "git" | undefined
|
||||
readonly provider?: string | undefined
|
||||
}
|
||||
export type VcsInitOutput = void
|
||||
export type VcsInitOperation<E = never> = (input?: VcsInitInput) => Effect.Effect<VcsInitOutput, E>
|
||||
|
||||
@@ -6252,11 +6252,11 @@ export type WorktreeRefreshOutput = void
|
||||
export type VcsInitInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined } | undefined
|
||||
readonly provider?: "git" | undefined
|
||||
readonly provider?: string | undefined
|
||||
}["location"]
|
||||
readonly provider?: {
|
||||
readonly location?: { readonly directory?: string | undefined } | undefined
|
||||
readonly provider?: "git" | undefined
|
||||
readonly provider?: string | undefined
|
||||
}["provider"]
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ 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
|
||||
// Markerless locations need this definition to initialize their first repository.
|
||||
if (location.vcs && location.vcs.type !== "git") return
|
||||
|
||||
const processes = yield* AppProcess.Service
|
||||
const adapter = make(processes, {
|
||||
@@ -35,6 +36,13 @@ export const Plugin = define({
|
||||
editor.add({
|
||||
id: "git",
|
||||
name: "Git",
|
||||
init: (input) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* processes.run(
|
||||
ChildProcess.make(gitExecutable, ["init"], { cwd: input.worktree, stdin: "ignore" }),
|
||||
)
|
||||
if (result.exitCode !== 0) return yield* Effect.fail(new Error("Git initialization failed"))
|
||||
}),
|
||||
info: () => adapter.info(),
|
||||
base: () => adapter.base(),
|
||||
branches: (input) => adapter.branches({ search: input.search, limit: input.limit }),
|
||||
|
||||
@@ -26,7 +26,8 @@ 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
|
||||
// Markerless locations need this definition to initialize their first repository.
|
||||
if (location.vcs && location.vcs.type !== "hg") return
|
||||
|
||||
const processes = yield* AppProcess.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
@@ -39,6 +40,18 @@ export const Plugin = define({
|
||||
editor.add({
|
||||
id: "hg",
|
||||
name: "Mercurial",
|
||||
init: (input) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* processes.run(
|
||||
ChildProcess.make("hg", ["init"], {
|
||||
cwd: input.worktree,
|
||||
env: { HGPLAIN: "1" },
|
||||
extendEnv: true,
|
||||
stdin: "ignore",
|
||||
}),
|
||||
)
|
||||
if (result.exitCode !== 0) return yield* Effect.fail(new Error("Mercurial initialization failed"))
|
||||
}),
|
||||
info: () => adapter.info(),
|
||||
branches: (input) => adapter.branches({ search: input.search, limit: input.limit }),
|
||||
status: () => adapter.status(),
|
||||
|
||||
@@ -36,10 +36,6 @@ export class NotFoundError extends Schema.TaggedError<NotFoundError>()("Project.
|
||||
projectID: ID,
|
||||
}) {}
|
||||
|
||||
export class InitializeGitError extends Schema.TaggedError<InitializeGitError>()("Project.InitializeGitError", {
|
||||
kind: Schema.Literals(["missing", "conflict", "failed"]),
|
||||
}) {}
|
||||
|
||||
export interface Resolved {
|
||||
readonly previous?: ID
|
||||
readonly id: ID
|
||||
@@ -70,7 +66,6 @@ export interface Interface {
|
||||
readonly activate: (projectID: ID) => Effect.Effect<void>
|
||||
/** Resolves and persists the owning Project. */
|
||||
readonly resolve: (input: AbsolutePath, options?: { readonly discovery?: boolean }) => Effect.Effect<Resolved>
|
||||
readonly initializeGit: (directory: AbsolutePath) => Effect.Effect<Resolved, InitializeGitError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Project") {}
|
||||
@@ -387,17 +382,7 @@ const layer = Layer.effect(
|
||||
})
|
||||
})
|
||||
|
||||
const initializeGit = Effect.fn("Project.initializeGit")(function* (directory: AbsolutePath) {
|
||||
if (!(yield* fs.isDir(directory))) return yield* new InitializeGitError({ kind: "missing" })
|
||||
if ((yield* resolve(directory)).vcs) return yield* new InitializeGitError({ kind: "conflict" })
|
||||
const result = yield* proc
|
||||
.run(ChildProcess.make("git", ["init"], { cwd: directory, stdin: "ignore" }))
|
||||
.pipe(Effect.mapError(() => new InitializeGitError({ kind: "failed" })))
|
||||
if (result.exitCode !== 0) return yield* new InitializeGitError({ kind: "failed" })
|
||||
return yield* resolve(directory)
|
||||
})
|
||||
|
||||
return Service.of({ list, update, activate, resolve, initializeGit })
|
||||
return Service.of({ list, update, activate, resolve })
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { VcsEvent } from "@opencode/schema/vcs-event"
|
||||
import { makeLocationNode } from "@opencode/util/effect/app-node"
|
||||
import { FSUtil } from "@opencode/util/fs-util"
|
||||
import { Location } from "./location.js"
|
||||
import { Project } from "./project.js"
|
||||
import { Bus } from "./bus.js"
|
||||
import { State } from "./state.js"
|
||||
import { emptyPatch, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./vcs/patch.js"
|
||||
@@ -20,6 +21,10 @@ export class DiffError extends Schema.TaggedError<DiffError>()("Vcs.DiffError",
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class InitializeError extends Schema.TaggedError<InitializeError>()("Vcs.InitializeError", {
|
||||
kind: Schema.Literals(["missing", "conflict", "unsupported", "failed"]),
|
||||
}) {}
|
||||
|
||||
export interface DiffOptions {
|
||||
readonly context?: number
|
||||
readonly base?: string
|
||||
@@ -40,6 +45,7 @@ export interface Adapter {
|
||||
|
||||
export interface Interface extends Adapter, State.Transformable<VcsEditor> {
|
||||
readonly base: () => Effect.Effect<Base | null, DiffError>
|
||||
readonly initialize: (providerID: string) => Effect.Effect<void, InitializeError>
|
||||
}
|
||||
|
||||
interface Data {
|
||||
@@ -124,7 +130,9 @@ const layer = Layer.effect(
|
||||
const provider = selected()
|
||||
const next: Info = provider
|
||||
? {
|
||||
...(yield* protect(provider, "info", provider.info(scope).pipe(Effect.flatMap(decodeInfo)), { branch: {} })),
|
||||
...(yield* protect(provider, "info", provider.info(scope).pipe(Effect.flatMap(decodeInfo)), {
|
||||
branch: {},
|
||||
})),
|
||||
provider: provider.id,
|
||||
}
|
||||
: { branch: {} }
|
||||
@@ -166,6 +174,23 @@ const layer = Layer.effect(
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
reload: state.reload,
|
||||
initialize: Effect.fn("Vcs.initialize")(function* (providerID: string) {
|
||||
if (!(yield* fs.isDir(location.project.directory))) return yield* new InitializeError({ kind: "missing" })
|
||||
if (yield* Project.root(fs, location.project.directory)) return yield* new InitializeError({ kind: "conflict" })
|
||||
const provider = state.get().providers.get(providerID)
|
||||
if (!provider?.init) return yield* new InitializeError({ kind: "unsupported" })
|
||||
yield* provider
|
||||
.init(scope)
|
||||
.pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Cause.hasInterrupts(cause)
|
||||
? Effect.failCause(cause).pipe(Effect.orDie)
|
||||
: Effect.logWarning("vcs initialization failed", { provider: providerID, cause }).pipe(
|
||||
Effect.andThen(Effect.fail(new InitializeError({ kind: "failed" }))),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
info: Effect.fn("Vcs.info")(function* () {
|
||||
return current.info
|
||||
}),
|
||||
|
||||
@@ -101,7 +101,6 @@ describe("node build", () => {
|
||||
list: () => Effect.succeed([]),
|
||||
update: () => Effect.die("not implemented"),
|
||||
activate: () => Effect.void,
|
||||
initializeGit: () => Effect.die("not implemented"),
|
||||
resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }),
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -14,7 +14,6 @@ export const globalProjectNode = makeGlobalNode({
|
||||
list: () => Effect.succeed([]),
|
||||
update: () => Effect.die("not implemented"),
|
||||
activate: () => Effect.void,
|
||||
initializeGit: () => Effect.die("not implemented"),
|
||||
resolve: (directory) => {
|
||||
const project = { id: Project.ID.global, directory, canonical: directory }
|
||||
return upsertProject(database.db, project).pipe(Effect.orDie, Effect.as(project))
|
||||
|
||||
@@ -15,7 +15,6 @@ const projectLayer = Layer.succeed(
|
||||
list: () => Effect.succeed([]),
|
||||
update: () => Effect.die("not implemented"),
|
||||
activate: () => Effect.void,
|
||||
initializeGit: () => Effect.die("not implemented"),
|
||||
resolve: () =>
|
||||
Effect.succeed({
|
||||
id: Project.ID.make("project"),
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Plugin } from "@opencode/plugin"
|
||||
import type { VcsDefinition } from "@opencode/plugin/effect/vcs"
|
||||
import { PluginPromise } from "@opencode/core/plugin/promise"
|
||||
import { State } from "@opencode/core/state"
|
||||
import { Effect } from "effect"
|
||||
import { it } from "../lib/effect"
|
||||
import { host } from "./host"
|
||||
|
||||
it.live("Promise VCS initializer reaches the registered provider with its location scope", () =>
|
||||
Effect.gen(function* () {
|
||||
const calls: string[] = []
|
||||
const state = State.create({
|
||||
initial: () => new Map<string, VcsDefinition>(),
|
||||
editor: (providers) => ({
|
||||
add: (definition: VcsDefinition) => providers.set(definition.id, definition),
|
||||
default: { get: () => undefined, set: (_selection: string) => {} },
|
||||
}),
|
||||
})
|
||||
const context = host()
|
||||
const plugin = PluginPromise.fromPromise(
|
||||
Plugin.define({
|
||||
id: "init-vcs",
|
||||
async setup(ctx) {
|
||||
await ctx.vcs.transform((editor) =>
|
||||
editor.add({
|
||||
id: "custom",
|
||||
name: "Custom VCS",
|
||||
init: async (scope, { signal }) => {
|
||||
expect(signal.aborted).toBe(false)
|
||||
calls.push(scope.worktree)
|
||||
},
|
||||
info: async () => ({ branch: {} }),
|
||||
branches: async () => [],
|
||||
status: async () => [],
|
||||
diff: async () => [],
|
||||
}),
|
||||
)
|
||||
},
|
||||
}),
|
||||
)
|
||||
yield* plugin.effect(host({ vcs: { ...context.vcs, transform: state.transform, reload: state.reload } }))
|
||||
const init = state.get().get("custom")?.init
|
||||
if (!init) return yield* Effect.die("Initializer was not registered")
|
||||
yield* init({ directory: "/workspace", worktree: "/workspace", canonical: "/workspace" })
|
||||
expect(calls).toEqual(["/workspace"])
|
||||
}),
|
||||
)
|
||||
@@ -103,6 +103,29 @@ const provider = (input: Partial<VcsDefinition> = {}) =>
|
||||
}) satisfies VcsDefinition
|
||||
|
||||
describe("Vcs", () => {
|
||||
it.live("routes initialization through the requested provider before a repository exists", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const vcs = yield* Vcs.Service
|
||||
const calls: string[] = []
|
||||
yield* vcs.transform((editor) => {
|
||||
editor.add(provider({ id: "read-only" }))
|
||||
editor.add(
|
||||
provider({
|
||||
id: "custom",
|
||||
init: (input) => Effect.sync(() => void calls.push(input.worktree)),
|
||||
}),
|
||||
)
|
||||
})
|
||||
expect(yield* vcs.initialize("missing").pipe(Effect.flip)).toMatchObject({ kind: "unsupported" })
|
||||
expect(yield* vcs.initialize("read-only").pipe(Effect.flip)).toMatchObject({ kind: "unsupported" })
|
||||
expect(calls).toEqual([])
|
||||
yield* vcs.initialize("custom")
|
||||
expect(calls).toEqual([directory])
|
||||
}).pipe(provide(directory)),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("returns empty results outside version control", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface VcsDiffInput extends VcsScope {
|
||||
export interface VcsDefinition {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly init?: (input: VcsScope) => Effect.Effect<void, unknown>
|
||||
readonly info: (input: VcsScope) => Effect.Effect<Vcs.Info, unknown>
|
||||
readonly base?: (input: VcsScope) => Effect.Effect<Vcs.Base | null, unknown>
|
||||
readonly branches: (input: VcsBranchesInput) => Effect.Effect<Vcs.BranchList, unknown>
|
||||
|
||||
@@ -517,10 +517,12 @@ export function fromPromise(plugin: Plugin) {
|
||||
host.vcs.transform((editor) => {
|
||||
callback({
|
||||
add: (definition) => {
|
||||
const init = definition.init?.bind(definition)
|
||||
const base = definition.base?.bind(definition)
|
||||
editor.add({
|
||||
id: definition.id,
|
||||
name: definition.name,
|
||||
init: init ? (input) => attempt((signal) => init(input, { signal })) : undefined,
|
||||
info: (input) => attempt((signal) => definition.info(input, { signal })),
|
||||
base: base ? (input) => attempt((signal) => base(input, { signal })) : undefined,
|
||||
branches: (input) => attempt((signal) => definition.branches(input, { signal })),
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface VcsDiffInput extends VcsScope {
|
||||
export interface VcsDefinition {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly init?: (input: VcsScope, context: { readonly signal: AbortSignal }) => Promise<void>
|
||||
readonly info: (input: VcsScope, context: { readonly signal: AbortSignal }) => Promise<Vcs.Info>
|
||||
readonly base?: (input: VcsScope, context: { readonly signal: AbortSignal }) => Promise<Vcs.Base | null>
|
||||
readonly branches: (input: VcsBranchesInput, context: { readonly signal: AbortSignal }) => Promise<Vcs.BranchList>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ConflictError, InvalidRequestError, ServiceUnavailableError } from "../
|
||||
|
||||
const InitQuery = Schema.Struct({
|
||||
...LocationQuery.fields,
|
||||
provider: Schema.optional(Schema.Literal("git")),
|
||||
provider: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
const BranchesQuery = Schema.Struct({
|
||||
@@ -36,9 +36,9 @@ export const VcsGroup = HttpApiGroup.make("server.vcs")
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "vcs.init",
|
||||
summary: "Initialize Git repository",
|
||||
summary: "Initialize VCS repository",
|
||||
description:
|
||||
"Initialize the selected repository type in a markerless project's directory and refresh its location services. Only git is supported; omitting provider defaults to git.",
|
||||
"Initialize a repository using the selected VCS provider in a markerless project's directory and refresh its location services. Omitting provider defaults to git; built-in git and hg providers support initialization.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Vcs } from "@opencode/core/vcs"
|
||||
import { Project } from "@opencode/core/project"
|
||||
import { Plugin } from "@opencode/core/plugin"
|
||||
import { Location } from "@opencode/core/location"
|
||||
import { LocationServiceMap } from "@opencode/core/location-services"
|
||||
import { ConflictError, InvalidRequestError, ServiceUnavailableError } from "@opencode/protocol/errors"
|
||||
@@ -13,19 +14,29 @@ export const VcsHandler = HttpApiBuilder.group(Api, "server.vcs", (handlers) =>
|
||||
const project = yield* Project.Service
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
return handlers
|
||||
.handle("vcs.init", () =>
|
||||
.handle("vcs.init", (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
const directory = location.project.directory
|
||||
yield* project.initializeGit(directory).pipe(
|
||||
const providerID = ctx.query.provider ?? "git"
|
||||
yield* Plugin.awaitActivation
|
||||
const vcs = yield* Vcs.Service
|
||||
yield* vcs.initialize(providerID).pipe(
|
||||
Effect.mapError((error) => {
|
||||
if (error.kind === "missing")
|
||||
return new InvalidRequestError({ message: "Project directory does not exist", field: "location" })
|
||||
if (error.kind === "conflict")
|
||||
return new ConflictError({ message: "Project already has version control", resource: directory })
|
||||
return new ServiceUnavailableError({ service: "git", message: "Git initialization failed" })
|
||||
if (error.kind === "unsupported")
|
||||
return new InvalidRequestError({
|
||||
message: "VCS provider does not support initialization",
|
||||
field: "provider",
|
||||
})
|
||||
return new ServiceUnavailableError({ service: providerID, message: "VCS initialization failed" })
|
||||
}),
|
||||
)
|
||||
if (!(yield* project.resolve(directory)).vcs)
|
||||
return yield* new ServiceUnavailableError({ service: providerID, message: "VCS initialization failed" })
|
||||
yield* locations.invalidate(
|
||||
Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }),
|
||||
)
|
||||
|
||||
@@ -96,6 +96,7 @@ const vcsLayer = Layer.succeed(
|
||||
Vcs.Service,
|
||||
Vcs.Service.of({
|
||||
base: () => Effect.succeed(null),
|
||||
initialize: () => Effect.fail(new Vcs.InitializeError({ kind: "unsupported" })),
|
||||
transform: () => Effect.succeed({ dispose: Effect.void }),
|
||||
reload: () => Effect.void,
|
||||
info: () => Effect.succeed({ branch: {} }),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path"
|
||||
import { $ } from "bun"
|
||||
import { expect } from "bun:test"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { SdkPlugins } from "@opencode/core/plugin/sdk"
|
||||
import { Effect, Layer, Schedule } from "effect"
|
||||
import { tmpdir } from "../../core/test/fixture/tmpdir"
|
||||
@@ -88,10 +88,11 @@ it.live(
|
||||
const server = yield* startServer(path.join(tmp.path, "config"))
|
||||
const url = new URL("/api/vcs/init", server.base)
|
||||
url.searchParams.set("location[directory]", tmp.path)
|
||||
url.searchParams.set("provider", "hg")
|
||||
url.searchParams.set("provider", "unknown")
|
||||
const unsupported = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
|
||||
expect(unsupported.status).toBe(400)
|
||||
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".git", "HEAD")).exists())).toBe(false)
|
||||
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".hg", "requires")).exists())).toBe(false)
|
||||
url.searchParams.set("provider", "git")
|
||||
const initialized = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
|
||||
expect(initialized.status).toBe(204)
|
||||
@@ -100,6 +101,51 @@ it.live(
|
||||
15_000,
|
||||
)
|
||||
|
||||
const describeHg = Bun.which("hg") ? describe : describe.skip
|
||||
|
||||
describeHg("Mercurial initialization", () => {
|
||||
it.live(
|
||||
"initializes and serves an untracked-file diff through the Hg provider",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-vcs-hg-init-")))
|
||||
yield* Effect.promise(() => Bun.write(path.join(tmp.path, "hello.txt"), "hello\n"))
|
||||
const server = yield* startServer(path.join(tmp.path, "config"))
|
||||
const url = new URL("/api/vcs/init", server.base)
|
||||
url.searchParams.set("location[directory]", tmp.path)
|
||||
url.searchParams.set("provider", "hg")
|
||||
const initialized = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
|
||||
expect(initialized.status).toBe(204)
|
||||
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".hg", "requires")).exists())).toBe(true)
|
||||
url.pathname = "/api/vcs"
|
||||
const info = yield* Effect.tryPromise({
|
||||
try: async () => {
|
||||
const response = await fetch(url, { headers: server.headers })
|
||||
const body: unknown = await response.json()
|
||||
if (!isRecord(body) || !isRecord(body.data) || body.data.provider !== "hg")
|
||||
throw new Error("Mercurial provider not ready")
|
||||
return body
|
||||
},
|
||||
catch: (cause) => cause,
|
||||
}).pipe(Effect.retry(Schedule.spaced("10 millis")), Effect.timeout("2 seconds"))
|
||||
expect(info).toMatchObject({ data: { provider: "hg" } })
|
||||
url.pathname = "/api/vcs/diff"
|
||||
url.searchParams.set("mode", "working")
|
||||
const diff = yield* Effect.promise(() => fetch(url, { headers: server.headers }))
|
||||
expect(diff.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => diff.json())).toMatchObject({
|
||||
data: expect.arrayContaining([expect.objectContaining({ file: "hello.txt" })]),
|
||||
})
|
||||
url.pathname = "/api/vcs/init"
|
||||
url.searchParams.delete("mode")
|
||||
url.searchParams.set("provider", "git")
|
||||
const repeated = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
|
||||
expect(repeated.status).toBe(409)
|
||||
}),
|
||||
15_000,
|
||||
)
|
||||
})
|
||||
|
||||
it.live(
|
||||
"serves lazy review bases, committed diffs, and unavailable-base errors",
|
||||
() =>
|
||||
|
||||
@@ -1061,6 +1061,11 @@ Provider callbacks receive the current location, working-copy root, canonical pr
|
||||
store. Diff callbacks also receive the selected mode, requested context, and output byte budget. Repository discovery
|
||||
continues to use OpenCode's built-in Git and Mercurial detectors.
|
||||
|
||||
Providers can optionally implement `init(scope)` to create a repository in `scope.worktree`. Initialization selects
|
||||
the provider by ID (Git by default), not by the detected repository type. Register a provider with `init` even when
|
||||
the location has no repository yet; OpenCode checks that the directory exists and has no repository before calling
|
||||
it, then rediscovers the project. New repository formats still need discovery support beyond this callback.
|
||||
|
||||
Schemas: [`Vcs.Info`](/api#schema-Vcs.Info), [`Vcs.FileStatus`](/api#schema-Vcs.FileStatus),
|
||||
[`FileDiff.Info`](/api#schema-FileDiff.Info).
|
||||
|
||||
@@ -1073,10 +1078,15 @@ interface VcsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
interface VcsDomain extends VcsApi<unknown> {
|
||||
interface VcsDomain extends Omit<VcsApi<unknown>, "init"> {
|
||||
readonly transform: Transform<VcsEditor>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
interface VcsDefinition {
|
||||
readonly init?: (scope: VcsScope) => Effect.Effect<void, unknown>
|
||||
// Existing info, branches, status, and diff callbacks are required.
|
||||
}
|
||||
```
|
||||
|
||||
### Websearch
|
||||
|
||||
@@ -1029,6 +1029,11 @@ Provider callbacks receive the current location, working-copy root, canonical pr
|
||||
store. Diff callbacks also receive the selected mode, requested context, and output byte budget. Repository discovery
|
||||
continues to use OpenCode's built-in Git and Mercurial detectors.
|
||||
|
||||
Providers can optionally implement `init(scope, { signal })` to create a repository in `scope.worktree`. Initialization
|
||||
selects the provider by ID (Git by default), not by the detected repository type. Register a provider with `init` even
|
||||
when the location has no repository yet; OpenCode checks that the directory exists and has no repository before calling
|
||||
it, then rediscovers the project. New repository formats still need discovery support beyond this callback.
|
||||
|
||||
#### Reference
|
||||
|
||||
Schemas: [`Vcs.Info`](/api#schema-Vcs.Info), [`Vcs.FileStatus`](/api#schema-Vcs.FileStatus),
|
||||
@@ -1051,6 +1056,11 @@ interface VcsEditor {
|
||||
set(providerID: string): void
|
||||
}
|
||||
}
|
||||
|
||||
interface VcsDefinition {
|
||||
init?(scope: VcsScope, context: { signal: AbortSignal }): Promise<void>
|
||||
// Existing info, branches, status, and diff callbacks are required.
|
||||
}
|
||||
```
|
||||
|
||||
### Worktrees
|
||||
|
||||
Reference in New Issue
Block a user