From 36fc2fb7a90b926e80c5aec6de497eb65437ed06 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] 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 }); }