From cb7f6b7ba425f58c8eb0b14ec63a11dbb15d4112 Mon Sep 17 00:00:00 2001 From: Brendonovich <14191578+Brendonovich@users.noreply.github.com> Date: Tue, 29 Sep 2026 06:28:15 +0000 Subject: [PATCH] fix(vcs): accept explicit Git init provider --- .../app/e2e/regression/review-no-git.spec.ts | 1 + packages/app/src/session/review/model.ts | 2 +- packages/client/src/effect/api/api.ts | 5 ++++- .../client/src/effect/generated/client.ts | 4 +++- .../client/src/promise/generated/client.ts | 2 +- .../client/src/promise/generated/types.ts | 9 ++++++++- packages/protocol/src/groups/vcs.ts | 10 ++++++++-- packages/server/test/vcs.test.ts | 20 +++++++++++++++++++ 8 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/app/e2e/regression/review-no-git.spec.ts b/packages/app/e2e/regression/review-no-git.spec.ts index 42aac713ea0..b3ca0f0ae5a 100644 --- a/packages/app/e2e/regression/review-no-git.spec.ts +++ b/packages/app/e2e/regression/review-no-git.spec.ts @@ -63,5 +63,6 @@ for (const view of ["desktop", "mobile"] as const) { await test.info().attach("review-after-git", { body: await panel.screenshot(), contentType: "image/png" }) expect(requests).toHaveLength(1) expect(new URL(requests[0]!).searchParams.get("location[directory]")).toBe(directory) + expect(new URL(requests[0]!).searchParams.get("provider")).toBe("git") }) } diff --git a/packages/app/src/session/review/model.ts b/packages/app/src/session/review/model.ts index 3ee7c6258ea..1f1e648c07e 100644 --- a/packages/app/src/session/review/model.ts +++ b/packages/app/src/session/review/model.ts @@ -171,7 +171,7 @@ export function createSessionReview(input: { if (!sessionID) return setState("initializingGit", true) void server.api.vcs - .init({ location: { directory } }) + .init({ location: { directory }, provider: "git" }) .then(async () => { data.project.invalidate() data.session.invalidate(sessionID) diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 5b822882c24..63f91911d89 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -2223,7 +2223,10 @@ export interface WorktreeApi { readonly refresh: WorktreeRefreshOperation } -export type VcsInitInput = { readonly location?: { readonly directory?: string | undefined } | undefined } +export type VcsInitInput = { + readonly location?: { readonly directory?: string | undefined } | undefined + readonly provider?: "git" | undefined +} export type VcsInitOutput = void export type VcsInitOperation = (input?: VcsInitInput) => Effect.Effect diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index bef4f774aa5..63f80455f72 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -1454,7 +1454,9 @@ const adaptGroupWorktree = (raw: RawClient["server.worktree"]) => ({ const EndpointVcsInit = (raw: RawClient["server.vcs"]) => (input?: VcsInitInput) => preserveEffect()( - raw["vcs.init"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + raw["vcs.init"]({ query: { location: input?.["location"], provider: input?.["provider"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const EndpointVcsGet = (raw: RawClient["server.vcs"]) => (input?: VcsGetInput) => diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 5c8ad19b390..2a1991cb5d8 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -2019,7 +2019,7 @@ export function make(options: ClientOptions) { { method: "POST", path: `/api/vcs/init`, - query: { location: input?.["location"] }, + query: { location: input?.["location"], provider: input?.["provider"] }, successStatus: 204, declaredStatuses: [400, 401, 409, 503], empty: true, diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 1fdda33b188..7447d4a333a 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -6250,7 +6250,14 @@ export type WorktreeRefreshInput = { readonly projectID: { readonly projectID: s export type WorktreeRefreshOutput = void export type VcsInitInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined } | undefined + readonly provider?: "git" | undefined + }["location"] + readonly provider?: { + readonly location?: { readonly directory?: string | undefined } | undefined + readonly provider?: "git" | undefined + }["provider"] } export type VcsInitOutput = void diff --git a/packages/protocol/src/groups/vcs.ts b/packages/protocol/src/groups/vcs.ts index ec410f38e2d..113f559dd1b 100644 --- a/packages/protocol/src/groups/vcs.ts +++ b/packages/protocol/src/groups/vcs.ts @@ -7,6 +7,11 @@ import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/un import { LocationQuery, locationQueryOpenApi } from "./location.js" import { ConflictError, InvalidRequestError, ServiceUnavailableError } from "../errors.js" +const InitQuery = Schema.Struct({ + ...LocationQuery.fields, + provider: Schema.optional(Schema.Literal("git")), +}) + const BranchesQuery = Schema.Struct({ ...LocationQuery.fields, search: Schema.optional(Schema.String), @@ -23,7 +28,7 @@ const DiffQuery = Schema.Struct({ export const VcsGroup = HttpApiGroup.make("server.vcs") .add( HttpApiEndpoint.post("vcs.init", "/api/vcs/init", { - query: LocationQuery, + query: InitQuery, success: HttpApiSchema.NoContent, error: [ConflictError, InvalidRequestError, ServiceUnavailableError], }) @@ -32,7 +37,8 @@ export const VcsGroup = HttpApiGroup.make("server.vcs") OpenApi.annotations({ identifier: "vcs.init", summary: "Initialize Git repository", - description: "Initialize Git in a markerless project's directory and refresh its location services.", + 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.", }), ), ) diff --git a/packages/server/test/vcs.test.ts b/packages/server/test/vcs.test.ts index 2deee2ef787..476c4195181 100644 --- a/packages/server/test/vcs.test.ts +++ b/packages/server/test/vcs.test.ts @@ -80,6 +80,26 @@ it.live( 15_000, ) +it.live( + "accepts an explicit Git provider and rejects unsupported providers without initializing", + () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-vcs-provider-"))) + 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 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) + url.searchParams.set("provider", "git") + 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, ".git", "HEAD")).exists())).toBe(true) + }), + 15_000, +) + it.live( "serves lazy review bases, committed diffs, and unavailable-base errors", () =>