diff --git a/packages/core/src/vcs.ts b/packages/core/src/vcs.ts index cb61e681f7..050b1de401 100644 --- a/packages/core/src/vcs.ts +++ b/packages/core/src/vcs.ts @@ -2,7 +2,7 @@ export * as Vcs from "./vcs" import { Context, Effect, Layer } from "effect" import { FileDiff } from "@opencode-ai/schema/file-diff" -import { FileStatus, Mode } from "@opencode-ai/schema/vcs" +import { FileStatus, Info, Mode } from "@opencode-ai/schema/vcs" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "./location" @@ -10,13 +10,14 @@ import { AppProcess } from "@opencode-ai/util/process" import { VcsGit } from "./vcs/git" import { VcsHg } from "./vcs/hg" -export { FileStatus, Mode } +export { FileStatus, Info, Mode } export interface DiffOptions { readonly context?: number } export interface Interface { + readonly info: () => Effect.Effect readonly status: () => Effect.Effect readonly diff: (mode: Mode, options?: DiffOptions) => Effect.Effect } @@ -40,6 +41,10 @@ const layer = Layer.effect( const location = yield* Location.Service const impl = adapter(proc, fs, location) return Service.of({ + info: Effect.fn("Vcs.info")(function* () { + if (!impl) return {} + return yield* impl.info() + }), status: Effect.fn("Vcs.status")(function* () { if (!impl) return [] return yield* impl.status() diff --git a/packages/core/src/vcs/git.ts b/packages/core/src/vcs/git.ts index 390348fc4f..8891898af9 100644 --- a/packages/core/src/vcs/git.ts +++ b/packages/core/src/vcs/git.ts @@ -20,6 +20,12 @@ export function make(proc: AppProcess.Interface, input: { directory: string; wor const ctx: Ctx = { git: makeGit(proc), directory: input.directory, worktree: input.worktree } return { + info: Effect.fn("VcsGit.info")(function* () { + const [branch, root] = yield* Effect.all([ctx.git.branch(ctx.directory), ctx.git.defaultBranch(ctx.directory)], { + concurrency: 2, + }) + return { branch, defaultBranch: root?.name } + }), status: Effect.fn("VcsGit.status")(function* () { const git = ctx.git const ref = (yield* git.hasHead(ctx.directory)) ? "HEAD" : undefined diff --git a/packages/core/src/vcs/hg.ts b/packages/core/src/vcs/hg.ts index 10ae0bcbfc..0d867c56c1 100644 --- a/packages/core/src/vcs/hg.ts +++ b/packages/core/src/vcs/hg.ts @@ -73,6 +73,9 @@ export function make( }) return { + info: Effect.fn("VcsHg.info")(function* () { + return { branch: yield* hg.branch(), defaultBranch: "default" } + }), status: Effect.fn("VcsHg.status")(function* () { const [items, batch] = yield* Effect.all( // Zero-context patches are enough to count changed lines. diff --git a/packages/core/test/vcs-hg.test.ts b/packages/core/test/vcs-hg.test.ts index f699eb6457..15f548b215 100644 --- a/packages/core/test/vcs-hg.test.ts +++ b/packages/core/test/vcs-hg.test.ts @@ -152,6 +152,7 @@ describeHg("Vcs mercurial", () => { await commitAll(directory, "initial") }) const vcs = yield* Vcs.Service + expect(yield* vcs.info()).toEqual({ branch: "default", defaultBranch: "default" }) expect(yield* vcs.diff("branch")).toEqual([]) yield* Effect.promise(async () => { diff --git a/packages/core/test/vcs.test.ts b/packages/core/test/vcs.test.ts index 822deff75b..815f01bc49 100644 --- a/packages/core/test/vcs.test.ts +++ b/packages/core/test/vcs.test.ts @@ -53,6 +53,7 @@ describe("Vcs", () => { withTmp((directory) => Effect.gen(function* () { const vcs = yield* Vcs.Service + expect(yield* vcs.info()).toEqual({}) expect(yield* vcs.status()).toEqual([]) expect(yield* vcs.diff("working")).toEqual([]) expect(yield* vcs.diff("branch")).toEqual([]) @@ -155,6 +156,7 @@ describe("Vcs", () => { await commitAll(directory, "initial") }) const vcs = yield* Vcs.Service + expect(yield* vcs.info()).toEqual({ branch: "main", defaultBranch: "main" }) expect(yield* vcs.diff("branch")).toEqual([]) yield* Effect.promise(async () => { diff --git a/packages/protocol/src/groups/vcs.ts b/packages/protocol/src/groups/vcs.ts index 5c2b85ceb2..2b160a9e18 100644 --- a/packages/protocol/src/groups/vcs.ts +++ b/packages/protocol/src/groups/vcs.ts @@ -13,6 +13,20 @@ const DiffQuery = Schema.Struct({ }) export const VcsGroup = HttpApiGroup.make("server.vcs") + .add( + HttpApiEndpoint.get("vcs.get", "/api/vcs", { + query: LocationQuery, + success: Location.response(Vcs.Info), + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.vcs.get", + summary: "VCS information", + description: "Get the current and default branches for the requested location.", + }), + ), + ) .add( HttpApiEndpoint.get("vcs.status", "/api/vcs/status", { query: LocationQuery, diff --git a/packages/schema/src/vcs.ts b/packages/schema/src/vcs.ts index 08127d8b51..b1122b9d1d 100644 --- a/packages/schema/src/vcs.ts +++ b/packages/schema/src/vcs.ts @@ -1,7 +1,7 @@ export * as Vcs from "./vcs.js" import { Schema } from "effect" -import { NonNegativeInt } from "./schema.js" +import { NonNegativeInt, optional } from "./schema.js" export const Mode = Schema.Literals(["working", "branch"]).annotate({ identifier: "Vcs.Mode" }) export type Mode = typeof Mode.Type @@ -13,3 +13,9 @@ export const FileStatus = Schema.Struct({ status: Schema.Literals(["added", "deleted", "modified"]), }).annotate({ identifier: "Vcs.FileStatus" }) export interface FileStatus extends Schema.Schema.Type {} + +export const Info = Schema.Struct({ + branch: optional(Schema.String), + defaultBranch: optional(Schema.String), +}).annotate({ identifier: "Vcs.Info" }) +export interface Info extends Schema.Schema.Type {} diff --git a/packages/server/src/handlers/vcs.ts b/packages/server/src/handlers/vcs.ts index 325eed474a..70a9aa6b44 100644 --- a/packages/server/src/handlers/vcs.ts +++ b/packages/server/src/handlers/vcs.ts @@ -7,6 +7,14 @@ import { response } from "../location" export const VcsHandler = HttpApiBuilder.group(Api, "server.vcs", (handlers) => Effect.gen(function* () { return handlers + .handle("vcs.get", () => + response( + Effect.gen(function* () { + const vcs = yield* Vcs.Service + return yield* vcs.info() + }), + ), + ) .handle("vcs.status", () => response( Effect.gen(function* () {