Răsfoiți Sursa

feat(vcs): expose repository branch metadata

Brendan Allan 4 săptămâni în urmă
părinte
comite
305466e5ef

+ 7 - 2
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<Info>
   readonly status: () => Effect.Effect<FileStatus[]>
   readonly diff: (mode: Mode, options?: DiffOptions) => Effect.Effect<FileDiff.Info[]>
 }
@@ -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()

+ 6 - 0
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

+ 3 - 0
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.

+ 1 - 0
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 () => {

+ 2 - 0
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 () => {

+ 14 - 0
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,

+ 7 - 1
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<typeof FileStatus> {}
+
+export const Info = Schema.Struct({
+  branch: optional(Schema.String),
+  defaultBranch: optional(Schema.String),
+}).annotate({ identifier: "Vcs.Info" })
+export interface Info extends Schema.Schema.Type<typeof Info> {}

+ 8 - 0
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* () {