fix(protocol): discard also unstages the discarded blocks

Merged
TL
17h ago
+284-9

What

changes.discard on the filesystem/git storage (deco serve) restored the working tree to HEAD but left the git index untouched. A block staged with git add (or a git mv rename, a git rm deletion, a staged new file) stayed staged after the discard, so a later git commit would record the change that was just discarded.

PO: "deco serve should unstage git added blocks on discard".

How

Inside the existing single commit (same lock, after the expectedBase/version checks), a reason: "discard" commit of the fs storage now brings the index entries of exactly the files it restores or deletes back to HEAD, plus both sides of any staged rename that involves one of them (git diff-index --cached -M -z --name-status HEAD):

  • paths HEAD has: git reset -q HEAD -- <paths>
  • paths HEAD doesn't have (staged new files; everything on an unborn HEAD): git rm --cached -f --quiet --ignore-unmatch -- <paths> (-f because a staged new file that was edited again matches neither HEAD nor the disk)

It always uses execFile with argv arrays (no shell), literal pathspecs, chunked argv, and runs from the work tree's top level with repo-relative paths. The unstage runs before the file write. The previous entries (git ls-files -s -z) are kept, and if the write then fails they're put back (update-index --force-remove + update-index -z --index-info), so the commit stays all or nothing. No other path's staging is touched. After a discard, git status --porcelain -- .deco/blocks/<name>.json is empty for each discarded name.

Docs updated: storage/fs/git.ts and storage/fs/index.ts headers, CommitAttempt.reason in storage.ts, and the changes.discard notes in server/methods/changes.ts.

Known limit (unchanged): changes.list never consults the index. A change that is only staged, with a working file equal to HEAD, isn't listed, so nothing offers to discard it.

Tests

New describe("a discard unstages what it restores") in storage/fs/git.test.ts: staged modification, partially staged (staged + further edit), staged new file, staged new file edited again, staged deletion (git rm), git mv rename discarded by its new side / old side / both at once (both sides unstaged), unrelated staged files stay staged, a repo with no commits yet, and the index put back when the file write fails. The monorepo-folder test now stages its changes too.

Gates: bun run typecheck, biome check, knip, and vitest run packages/blocks/src/protocol (18 files, 557 passed / 85 skipped) all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig


Summary by cubic

Fixes changes.discard so discarding a block also unstages it from the git index, not just restoring the working tree to HEAD.

  • A block staged with git add, git mv, git rm, or as a new file stayed staged after a discard and a later git commit would record the discarded change.
  • The unstage happens inside the same commit and covers both sides of any staged rename; the previous index entries are restored if the file write fails.
  • No other path's staging is touched, and the behavior works in repositories with no commits yet.
  • Known limit: changes.list doesn't consult the index, so a solely staged change isn't listed and nothing offers to discard it.