Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -44,16 +45,30 @@ export async function getGitEligibility(cwd: string): Promise<GitEligibility> {
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: false,
ok: true,
gitRoot,
reason: "no_head",
message: "repository has no HEAD commit",
hasHead: false,
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

return { ok: true, gitRoot };
return { ok: true, gitRoot, hasHead: true };
}

export function safeWorkspaceRefSegment(workspaceId: string): string {
Expand Down
56 changes: 44 additions & 12 deletions src/review-checkpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,27 +238,59 @@ 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/);
Comment on lines 251 to +259

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 First-commit transition remains untested

The regression test performs only one unmarked review while the repository is unborn. Add coverage that creates the first user commit and then reviews or advances the checkpoint again, so regressions in the new parentless checkpoint lifecycle are detected.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

});

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);
});

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<string> {
Expand Down
13 changes: 9 additions & 4 deletions src/review-checkpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -280,16 +282,19 @@ function reviewRefs(
};
}

async function createWorkingTreeSnapshot(gitRoot: string, parent: string): Promise<string> {
async function createWorkingTreeSnapshot(gitRoot: string, parent?: string): Promise<string> {
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 });
}
Expand Down
33 changes: 33 additions & 0 deletions src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down