From 286a16c3e40491c59b333bde1b846b353185b353 Mon Sep 17 00:00:00 2001 From: David699 <33142294+David699@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:54:04 +0800 Subject: [PATCH 1/4] fix: review changes in repositories without HEAD Treat an unborn Git repository as reviewable by creating the initial DevSpace checkpoint from an empty index. This keeps the user's repository unborn while allowing show_changes to report files created after the workspace was opened. --- src/git.ts | 10 +++++----- src/review-checkpoints.test.ts | 22 ++++++++++------------ src/review-checkpoints.ts | 13 +++++++++---- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/git.ts b/src/git.ts index 8cb087521..e0e99011a 100644 --- a/src/git.ts +++ b/src/git.ts @@ -12,7 +12,8 @@ export interface GitCommandResult { export interface GitEligibility { ok: boolean; gitRoot?: string; - reason?: "not_git" | "no_head"; + hasHead?: boolean; + reason?: "not_git"; message?: string; } @@ -46,14 +47,13 @@ export async function getGitEligibility(cwd: string): Promise { await git(gitRoot, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]); } catch { return { - ok: false, + ok: true, gitRoot, - reason: "no_head", - message: "repository has no HEAD commit", + hasHead: false, }; } - return { ok: true, gitRoot }; + return { ok: true, gitRoot, hasHead: true }; } export function safeWorkspaceRefSegment(workspaceId: string): string { diff --git a/src/review-checkpoints.test.ts b/src/review-checkpoints.test.ts index 37ee2c558..1ed150d13 100644 --- a/src/review-checkpoints.test.ts +++ b/src/review-checkpoints.test.ts @@ -238,27 +238,25 @@ test("a concurrent review rejects a different root after initialization", async } }); -test("an unborn repository becomes reviewable after its first commit", async (t) => { +test("an unborn repository is reviewable without creating a HEAD commit", async (t) => { const root = await unbornRepository(t); + await writeFile(join(root, "existing.txt"), "present at open\n"); const manager = createReviewCheckpointManager(); - await manager.initializeWorkspace({ workspaceId: "ws_unborn", root }); - await assert.rejects( - () => manager.reviewChanges({ workspaceId: "ws_unborn", root }), - /repository has no HEAD commit/, - ); + const availability = await manager.initializeWorkspace({ workspaceId: "ws_unborn", root }); + assert.deepEqual(availability, { available: true }); + await assert.rejects(() => git(root, ["rev-parse", "--verify", "HEAD^{commit}"])); - await writeFile(join(root, "README.md"), "first commit\n"); - await git(root, ["add", "README.md"]); - await git(root, ["commit", "-m", "Initial commit"]); + await writeFile(join(root, "created-after-open.txt"), "new file\n"); - const afterFirstCommit = await manager.reviewChanges({ + const review = await manager.reviewChanges({ workspaceId: "ws_unborn", root, markReviewed: false, }); - assert.equal(afterFirstCommit.summary.files, 0); - assert.equal(afterFirstCommit.patch, ""); + assert.deepEqual(review.files.map((file) => file.path), ["created-after-open.txt"]); + assert.equal(review.files[0]?.type, "new"); + assert.match(review.patch, /new file/); }); async function committedRepository(t: TestContext): Promise { diff --git a/src/review-checkpoints.ts b/src/review-checkpoints.ts index 6f6872752..1c43778ca 100644 --- a/src/review-checkpoints.ts +++ b/src/review-checkpoints.ts @@ -230,7 +230,9 @@ async function initializeWorkspaceState( ]); if (!openCommit && !baselineCommit) { - const head = (await git(eligibility.gitRoot, ["rev-parse", "--verify", "HEAD^{commit}"])).stdout.trim(); + const head = eligibility.hasHead + ? (await git(eligibility.gitRoot, ["rev-parse", "--verify", "HEAD^{commit}"])).stdout.trim() + : undefined; const initialCommit = await createWorkingTreeSnapshot(eligibility.gitRoot, head); await git(eligibility.gitRoot, ["update-ref", state.openRef, initialCommit]); await git(eligibility.gitRoot, ["update-ref", state.baselineRef, initialCommit]); @@ -280,16 +282,19 @@ function reviewRefs( }; } -async function createWorkingTreeSnapshot(gitRoot: string, parent: string): Promise { +async function createWorkingTreeSnapshot(gitRoot: string, parent?: string): Promise { const tempDir = await mkdtemp(join(tmpdir(), "devspace-review-index-")); const indexPath = join(tempDir, "index"); const env = checkpointEnv(indexPath); try { - await git(gitRoot, ["read-tree", "HEAD"], { env }); + await git(gitRoot, parent ? ["read-tree", parent] : ["read-tree", "--empty"], { env }); await git(gitRoot, ["add", "-A", "--", "."], { env }); const tree = (await git(gitRoot, ["write-tree"], { env })).stdout.trim(); - return (await git(gitRoot, ["commit-tree", tree, "-p", parent, "-m", "DevSpace review snapshot"], { env })).stdout.trim(); + const commitArgs = ["commit-tree", tree]; + if (parent) commitArgs.push("-p", parent); + commitArgs.push("-m", "DevSpace review snapshot"); + return (await git(gitRoot, commitArgs, { env })).stdout.trim(); } finally { await rm(tempDir, { recursive: true, force: true }); } From c6330c463ed8a4da9248445fdb92cde06395ebd3 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:05:41 +0530 Subject: [PATCH 2/4] fix(review): distinguish unborn from broken HEAD --- src/git.ts | 17 ++++++++++++++++- src/review-checkpoints.test.ts | 11 +++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/git.ts b/src/git.ts index e0e99011a..a40a36ded 100644 --- a/src/git.ts +++ b/src/git.ts @@ -45,7 +45,22 @@ export async function getGitEligibility(cwd: string): Promise { const gitRoot = (await git(cwd, ["rev-parse", "--show-toplevel"])).stdout.trim(); try { await git(gitRoot, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]); - } catch { + } catch (error) { + let headRef: string; + try { + headRef = (await git(gitRoot, ["symbolic-ref", "--quiet", "HEAD"])).stdout.trim(); + } catch { + throw error; + } + + const existingHeadRef = (await git(gitRoot, [ + "for-each-ref", + "--format=%(refname)", + "--count=1", + headRef, + ])).stdout.trim(); + if (existingHeadRef) throw error; + return { ok: true, gitRoot, diff --git a/src/review-checkpoints.test.ts b/src/review-checkpoints.test.ts index 1ed150d13..8e9f097f4 100644 --- a/src/review-checkpoints.test.ts +++ b/src/review-checkpoints.test.ts @@ -259,6 +259,17 @@ test("an unborn repository is reviewable without creating a HEAD commit", async assert.match(review.patch, /new file/); }); +test("a broken HEAD is not treated as an unborn repository", async (t) => { + const root = await committedRepository(t); + const head = await gitOutput(root, ["rev-parse", "HEAD"]); + await rm(join(root, ".git", "objects", head.slice(0, 2), head.slice(2))); + const manager = createReviewCheckpointManager(); + + const availability = await manager.initializeWorkspace({ workspaceId: "ws_broken_head", root }); + + assert.equal(availability.available, false); +}); + async function committedRepository(t: TestContext): Promise { const root = await mkdtemp(join(tmpdir(), "devspace-review-checkpoints-test-")); t.after(() => rm(root, { recursive: true, force: true })); From f1560699d9f8aa0aa0bf4445ef134cb3c3110b21 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:06:09 +0530 Subject: [PATCH 3/4] test(review): cover first commit transition --- src/review-checkpoints.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/review-checkpoints.test.ts b/src/review-checkpoints.test.ts index 8e9f097f4..0a6dfce81 100644 --- a/src/review-checkpoints.test.ts +++ b/src/review-checkpoints.test.ts @@ -270,6 +270,29 @@ test("a broken HEAD is not treated as an unborn repository", async (t) => { assert.equal(availability.available, false); }); +test("an unborn review baseline survives the first user commit", async (t) => { + const root = await unbornRepository(t); + await writeFile(join(root, "existing.txt"), "present at open\n"); + const manager = createReviewCheckpointManager(); + + await manager.initializeWorkspace({ workspaceId: "ws_first_commit", root }); + await writeFile(join(root, "before-first-commit.txt"), "reviewed before commit\n"); + await manager.reviewChanges({ workspaceId: "ws_first_commit", root }); + + await git(root, ["add", "-A"]); + await git(root, ["commit", "-m", "Initial commit"]); + await writeFile(join(root, "after-first-commit.txt"), "created after commit\n"); + + const review = await manager.reviewChanges({ + workspaceId: "ws_first_commit", + root, + markReviewed: false, + }); + assert.deepEqual(review.files.map((file) => file.path), ["after-first-commit.txt"]); + assert.match(review.patch, /created after commit/); + assert.doesNotMatch(review.patch, /reviewed before commit/); +}); + async function committedRepository(t: TestContext): Promise { const root = await mkdtemp(join(tmpdir(), "devspace-review-checkpoints-test-")); t.after(() => rm(root, { recursive: true, force: true })); From b6958121e0df25a3759a669db516630c1acd3b92 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:07:27 +0530 Subject: [PATCH 4/4] test(review): cover unborn MCP workflow --- src/server.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/server.test.ts b/src/server.test.ts index 22f263235..59bcc5d37 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -186,6 +186,39 @@ test("open_workspace reports aggregate review availability", async (t) => { assert.deepEqual(gitReview, { available: true }); }); +test("show_changes reviews an unborn repository through the MCP tool surface", async (t) => { + const context = await fixture(t, { uiEnabled: false }); + await git(context.project, ["init"]); + + const opened = structuredContent(await callOpen(context.client, context.project, "unborn-review")); + const workspaceId = opened.workspace_id; + assert.equal(typeof workspaceId, "string"); + assert.deepEqual(opened.review, { available: true }); + + await writeFile(join(context.project, "created-after-open.txt"), "new file\n"); + const review = await context.client.callTool({ + name: "show_changes", + arguments: { workspace_id: workspaceId }, + }); + const card = responseCard(review); + + assert.deepEqual(card.files, [ + { + path: "created-after-open.txt", + type: "new", + additions: 1, + removals: 0, + }, + ]); + assert.match( + ((card.payload as { patch?: string } | undefined)?.patch) ?? "", + /new file/, + ); + await assert.rejects(() => execFileAsync("git", ["rev-parse", "--verify", "HEAD^{commit}"], { + cwd: context.project, + })); +}); + test("show_changes keeps model output compact and preserves the rich review card", async (t) => { const context = await fixture(t, { git: true, uiEnabled: false }); const opened = structuredContent(