Skip to content
Closed
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
23 changes: 23 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,29 @@ sessions.
| `changes` | Enables the aggregate `show_changes` tool and attaches widget UI to `open_workspace` and `show_changes`. |
| `off` | Disables widget UI. |

## Change review

`show_changes` (enabled by `DEVSPACE_WIDGETS=changes`) reviews the changes
made in the current work session. By default it diffs a change journal that
records the original state of every file the write, edit, and apply_patch
tools first touch, so review works in any workspace, git or not, and never
scans the repository.

Set `DEVSPACE_REVIEW_MODE=git` to use the git-backed review instead. It
compares the working tree against persisted review checkpoints:

- Checkpoints are commits created from a temporary index inside the git
common directory and stored under the
`refs/devspace/review/<workspace>/open` and
`refs/devspace/review/<workspace>/baseline` refs.
- The baseline advances whenever changes are shown.
- If the repository's HEAD moves between reviews, checkpoints are re-anchored
to the latest commit rather than diffed across different histories.
- Diffs ignore whitespace-only changes and degrade to a file list when the
patch would exceed 10 MB.

The git-backed review is the fallback path; the journal is the default.

## Skills

| Variable | Purpose |
Expand Down
134 changes: 125 additions & 9 deletions src/review-checkpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,27 +173,143 @@ 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 and re-anchors after its first commit", async (t) => {
const root = await unbornRepository(t);
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 initial = await manager.reviewChanges({ workspaceId: "ws_unborn", root, markReviewed: false });
assert.equal(initial.summary.files, 0);

await writeFile(join(root, "README.md"), "working tree only\n");
const beforeCommit = await manager.reviewChanges({
workspaceId: "ws_unborn",
root,
markReviewed: false,
});
assert.equal(beforeCommit.files.length, 1);
assert.equal(beforeCommit.files[0]?.type, "new");

await writeFile(join(root, "README.md"), "first commit\n");
await git(root, ["add", "README.md"]);
await git(root, ["commit", "-m", "Initial commit"]);

const afterFirstCommit = await manager.reviewChanges({
const reanchored = await manager.reviewChanges({
workspaceId: "ws_unborn",
root,
markReviewed: false,
});
assert.equal(reanchored.summary.files, 0);
assert.match(reanchored.result, /re-anchored/);

await writeFile(join(root, "notes.txt"), "after commit\n");
const afterCommit = await manager.reviewChanges({
workspaceId: "ws_unborn",
root,
markReviewed: false,
});
assert.equal(afterFirstCommit.summary.files, 0);
assert.equal(afterFirstCommit.patch, "");
assert.deepEqual(afterCommit.files.map((file) => file.path), ["notes.txt"]);
});

test("whitespace-only changes are ignored by default and visible when requested", async (t) => {
const root = await committedRepository(t);
const manager = createReviewCheckpointManager();
await manager.initializeWorkspace({ workspaceId: "ws_whitespace", root });

await writeFile(join(root, "README.md"), "hello \n");

const ignored = await manager.reviewChanges({
workspaceId: "ws_whitespace",
root,
markReviewed: false,
});
assert.equal(ignored.summary.files, 0);
assert.equal(ignored.patch, "");

const visible = await manager.reviewChanges({
workspaceId: "ws_whitespace",
root,
markReviewed: false,
ignoreWhitespace: false,
});
assert.equal(visible.summary.files, 1);
assert.equal(visible.summary.additions, 1);
});

test("an oversized diff degrades to a file list instead of failing", async (t) => {
const lineCount = 1_500_000;
const root = await committedRepository(t);
await writeFile(join(root, "big.txt"), `${"aaa\n".repeat(lineCount)}`);
await git(root, ["add", "big.txt"]);
await git(root, ["commit", "-m", "Add big file"]);
const manager = createReviewCheckpointManager();
await manager.initializeWorkspace({ workspaceId: "ws_oversized", root });

await writeFile(join(root, "big.txt"), `${"bbb\n".repeat(lineCount)}`);

const review = await manager.reviewChanges({
workspaceId: "ws_oversized",
root,
markReviewed: true,
});
assert.equal(review.files.length, 1);
assert.equal(review.patch, "");
assert.match(review.result, /file list/);

const after = await manager.reviewChanges({
workspaceId: "ws_oversized",
root,
markReviewed: false,
});
assert.equal(after.summary.files, 0);
});

test("re-anchoring after a HEAD move keeps unreviewed pending edits visible", async (t) => {
const root = await committedRepository(t);
const manager = createReviewCheckpointManager();
await manager.initializeWorkspace({ workspaceId: "ws_reanchor_edits", root });

await writeFile(join(root, "pending.txt"), "uncommitted work\n");
await writeFile(join(root, "committed.txt"), "committed later\n");
await git(root, ["add", "committed.txt"]);
await git(root, ["commit", "-m", "Move HEAD"]);

const reanchored = await manager.reviewChanges({
workspaceId: "ws_reanchor_edits",
root,
markReviewed: false,
});
assert.equal(reanchored.summary.files, 0);
assert.match(reanchored.result, /re-anchored/);

const after = await manager.reviewChanges({
workspaceId: "ws_reanchor_edits",
root,
markReviewed: false,
});
assert.deepEqual(after.files.map((file) => file.path), ["pending.txt"]);
assert.match(after.patch, /uncommitted work/);
});

test("HEAD moves after a restart are absorbed by re-anchoring", async (t) => {
const root = await committedRepository(t);
const manager = createReviewCheckpointManager();
await manager.initializeWorkspace({ workspaceId: "ws_restart_track", root });
await manager.reviewChanges({ workspaceId: "ws_restart_track", root, markReviewed: true });

const restartedManager = createReviewCheckpointManager();
await restartedManager.initializeWorkspace({ workspaceId: "ws_restart_track", root });

await writeFile(join(root, "committed.txt"), "new\n");
await git(root, ["add", "committed.txt"]);
await git(root, ["commit", "-m", "Move HEAD after restart"]);

const review = await restartedManager.reviewChanges({
workspaceId: "ws_restart_track",
root,
markReviewed: false,
});
assert.equal(review.summary.files, 0);
assert.match(review.result, /re-anchored/);
});

async function committedRepository(t: TestContext): Promise<string> {
Expand Down
Loading
Loading