From 81c967c4e1f7957f30620d7f45f719758c3ccfb0 Mon Sep 17 00:00:00 2001 From: Brendonovich <14191578+Brendonovich@users.noreply.github.com> Date: Sat, 26 Sep 2026 06:28:01 +0000 Subject: [PATCH] fix(app): restore Git initialization for non-Git projects --- .../app/e2e/regression/review-no-git.spec.ts | 67 +++++++++++++++++ packages/app/src/session/review/model.ts | 31 ++++++++ packages/app/src/session/review/view.tsx | 24 ++----- packages/client/src/effect/api/api.ts | 5 ++ .../client/src/effect/generated/client.ts | 8 +++ .../client/src/promise/generated/client.ts | 14 ++++ .../client/src/promise/generated/types.ts | 6 ++ packages/core/src/project.ts | 17 ++++- .../test/effect/layer-node/node-build.test.ts | 1 + packages/core/test/lib/project.ts | 1 + packages/core/test/location.test.ts | 1 + packages/plugin/src/effect/vcs.ts | 2 +- packages/plugin/src/promise/vcs.ts | 2 +- packages/protocol/src/groups/vcs.ts | 19 ++++- packages/server/src/handlers/vcs.ts | 28 +++++++- packages/server/test/vcs.test.ts | 72 +++++++++++++++++++ 16 files changed, 274 insertions(+), 24 deletions(-) create mode 100644 packages/app/e2e/regression/review-no-git.spec.ts diff --git a/packages/app/e2e/regression/review-no-git.spec.ts b/packages/app/e2e/regression/review-no-git.spec.ts new file mode 100644 index 00000000000..42aac713ea0 --- /dev/null +++ b/packages/app/e2e/regression/review-no-git.spec.ts @@ -0,0 +1,67 @@ +import { base64Encode } from "@opencode/util/encode" +import { expect, test } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectSessionTitle } from "../utils/waits" + +const directory = "C:/OpenCode/ReviewWithoutGit" +const sessionID = "ses_review_without_git" +const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` + +for (const view of ["desktop", "mobile"] as const) { + test(`offers Git initialization instead of an empty changes selector (${view})`, async ({ page }) => { + test.setTimeout(180_000) + if (view === "mobile") await page.setViewportSize({ width: 390, height: 844 }) + const project = { + id: "proj_review_without_git", + worktree: directory, + vcs: undefined as string | undefined, + name: "review-without-git", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + } + const session = { + id: sessionID, + slug: sessionID, + projectID: project.id, + directory, + title: "Review without Git", + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + } + await mockOpenCodeServer(page, { + directory, + project, + provider: { all: [], connected: [], default: {} }, + sessions: [session], + pageMessages: () => ({ items: [] }), + }) + const requests: string[] = [] + await page.route("**/api/vcs/init*", (route) => { + requests.push(route.request().url()) + project.id = "proj_review_with_git" + project.vcs = "git" + session.projectID = project.id + return route.fulfill({ status: 204 }) + }) + await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`, { waitUntil: "domcontentloaded" }) + if (view === "desktop") { + await expectSessionTitle(page, "Review without Git") + await page.getByRole("button", { name: "Toggle review" }).click() + } else { + await page.getByRole("tablist", { name: "Session view" }).getByRole("tab", { name: "Changes" }).click() + } + + const panel = view === "desktop" ? page.locator("#review-panel") : page.locator("[data-component='session-review']") + await expect(panel.getByText("Track, review, and undo changes in this project")).toBeVisible() + await expect(panel.getByRole("button", { name: "Git changes" })).toHaveCount(0) + const init = panel.getByRole("button", { name: "Create Git repository" }) + await expect(init).toBeVisible() + await test.info().attach("review-without-git", { body: await panel.screenshot(), contentType: "image/png" }) + await init.click() + await expect(panel.getByRole("button", { name: "Git changes" })).toBeVisible() + await expect(init).toHaveCount(0) + 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) + }) +} diff --git a/packages/app/src/session/review/model.ts b/packages/app/src/session/review/model.ts index b771896868a..3ee7c6258ea 100644 --- a/packages/app/src/session/review/model.ts +++ b/packages/app/src/session/review/model.ts @@ -17,6 +17,8 @@ import { createOpenReviewFile } from "../helpers" import type { SessionModel } from "../model" import type { SessionScreenLayout } from "../screen-layout" import { createReviewPanelState } from "./panel-state" +import { showToast } from "@/shell/notifications/toast" +import { formatServerError } from "@/runtime/server/errors" import { reviewDiffDirectory, reviewDiffNeedsLoad, reviewRootDirectory } from "./review-diff-kinds" import type { DiffStyle } from "./review-tab" @@ -42,6 +44,7 @@ export function createSessionReview(input: { detailsOpen: false, scroll: undefined as HTMLDivElement | undefined, pendingFile: undefined as string | undefined, + initializingGit: false, }) const mode = () => input.session.layout.view().review.mode() ?? "git" const selectedFile = () => input.session.layout.view().review.file() @@ -161,6 +164,32 @@ export function createSessionReview(input: { if (mode() === "git" || mode() === "branch") return !vcsQuery.isPending return true } + const initializeGit = () => { + if (state.initializingGit) return + const directory = location().directory + const sessionID = input.session.identity.params.id + if (!sessionID) return + setState("initializingGit", true) + void server.api.vcs + .init({ location: { directory } }) + .then(async () => { + data.project.invalidate() + data.session.invalidate(sessionID) + data.location.invalidate({ directory }) + data.location.vcs.invalidate({ directory }) + await data.project.sync() + await data.session.sync(sessionID) + await Promise.all([data.location.sync({ directory }), data.location.vcs.sync({ directory })]) + }) + .catch((error) => + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: formatServerError(error, language.t), + }), + ) + .finally(() => setState("initializingGit", false)) + } const loadDiff = async (path: string, version?: number): Promise => { const value = vcsMode() if (!value) return undefined @@ -410,6 +439,8 @@ export function createSessionReview(input: { diffs, focusFile, hasChanges, + initializeGit, + initializingGit: () => state.initializingGit, loadDiff, mobile: { changes: mobileChanges, diff --git a/packages/app/src/session/review/view.tsx b/packages/app/src/session/review/view.tsx index e2e81013609..20bfd91b41d 100644 --- a/packages/app/src/session/review/view.tsx +++ b/packages/app/src/session/review/view.tsx @@ -1,4 +1,5 @@ import { SessionReviewEmptyChangesV2 } from "@opencode/session-ui/v2/session-review-empty-changes-v2" +import { SessionReviewEmptyNoGitV2 } from "@opencode/session-ui/v2/session-review-empty-no-git-v2" import { SessionReviewV2SidebarToggle } from "@opencode/session-ui/v2/session-review-v2" import { Select } from "@opencode/ui/select" import { Tabs } from "@opencode/ui/tabs" @@ -237,7 +238,7 @@ function ReviewTitle(props: { review: SessionReviewModel }) { return language.t("ui.sessionReview.title.lastTurn") } return ( - + 0}>