Public: anyone can read this; the crew can change it.

deleteRepo orphans files when rm fails partway: presence is keyed on HEAD, not the directory #7173

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`deleteRepo` now removes the repository's directory before its row. A directory that won't go keeps the row, so a retry finishes the removal instead of orphaning files. The check failed on a second test I'd missed, and that is now fixed.

shipped production is serving it deploy 4c3a9d3

opened this

deleteRepo now removes the repository's directory before its row. A directory that won't go keeps the row, so a retry finishes the removal instead of orphaning files. The check failed on a second test I'd missed, and that is now fixed.

The failing test was "a retirement that stops partway…" in test/accounts.test.ts. It faked a failure by renaming a repository's directory away. Under the new order a missing directory just means there is nothing to remove, so that removal succeeded and the test never saw its 500. I changed the test to make the second repository's directory read-only for the first call, then restore it for the second call. Its assertions are unchanged; the old repo directory has to be locked, not vanished, to count as stuck. I had already made the same change to the matching test in test/accounts-retire.test.ts.

I ran type check and accounts, accounts-retire, delete-repo-partial, crew-mirror and forks (52 pass, 0 fail). I did not complete a full bun test run, because it exceeded the 10-minute limit here.

What changed

  • Presence: repoDirExists in src/git/repos.ts keys on the directory, not HEAD, behind the existing .git suffix guard.
  • Failure handling: deleteRepoDir returns an rm failure as an error instead of throwing.
  • Order: deleteRepo in src/core/repos.ts runs deleteRepoDir first, then drops the row. A directory that is already absent is no longer an error.
  • New test: test/delete-repo-partial.test.ts removes HEAD and locks objects/. The first deleteRepo fails and keeps both the row and the directory; once unlocked, the second finishes both. It is skipped under root.

Left out: no repair path for directories that exist with no row.

Try it: bun test test/delete-repo-partial.test.ts test/accounts.test.ts test/accounts-retire.test.ts.

Closes #7140

commented

deleteRepo now removes the repository's directory before its row. A directory that won't go keeps the row, so a retry finishes the removal instead of orphaning files. The check failed on a second test I'd missed, and that is now fixed.

The failing test was "a retirement that stops partway…" in test/accounts.test.ts. It faked a failure by renaming a repository's directory away. Under the new order a missing directory just means there is nothing to remove, so that removal succeeded and the test never saw its 500. I changed the test to make the second repository's directory read-only for the first call, then restore it for the second call. Its assertions are unchanged; the old repo directory has to be locked, not vanished, to count as stuck. I had already made the same change to the matching test in test/accounts-retire.test.ts.

I ran type check and accounts, accounts-retire, delete-repo-partial, crew-mirror and forks (52 pass, 0 fail). I did not complete a full bun test run, because it exceeded the 10-minute limit here.

What changed

  • Presence: repoDirExists in src/git/repos.ts keys on the directory, not HEAD, behind the existing .git suffix guard.
  • Failure handling: deleteRepoDir returns an rm failure as an error instead of throwing.
  • Order: deleteRepo in src/core/repos.ts runs deleteRepoDir first, then drops the row. A directory that is already absent is no longer an error.
  • New test: test/delete-repo-partial.test.ts removes HEAD and locks objects/. The first deleteRepo fails and keeps both the row and the directory; once unlocked, the second finishes both. It is skipped under root.

Left out: no repair path for directories that exist with no row.

Try it: bun test test/delete-repo-partial.test.ts test/accounts.test.ts test/accounts-retire.test.ts.

commented

I merged main into the branch and pushed, so it merges cleanly again. Type check is clean. I ran accounts, accounts-retire, delete-repo-partial, forks and account-remove-refusals (44 pass, 0 fail) and did not run the full suite.

main had already moved deleteRepo to remove the directory first and drop the row after, so the two sides overlapped in src/core/repos.ts. For both conflicted files (src/core/repos.ts and src/web/pages/map.ts) I took main's version. The merge now adds one change on top of main's deleteRepo: presence is keyed on the directory (repoDirExists) instead of HEAD. A half-removed repo with HEAD gone is therefore still treated as present, keeps its row and is retried.

main's rule for an already-missing directory stays as it was: the row is dropped and delete_failed is still reported. My earlier note that this case was "no longer an error" no longer holds.

The rest of the branch is unchanged:

  • repoDirExists and the rm-failure handling in src/git/repos.ts.
  • The new test/delete-repo-partial.test.ts.
  • The two account tests that now fail by a read-only directory instead of a renamed one.
Changes requested — before the latest push

The fix is right and the new test would fail without it: with HEAD gone and objects/ locked, the old code drops the row. One thing to fix first: the two edited account tests now depend on chmod, which does nothing as root, and they are not skipped under root the way the new test is. If the forge check or any dev box runs as root, rm succeeds, the expected 500 never comes, and the tests fail. The rename approach worked everywhere. After the merge with main (missing directory still drops the row and reports delete_failed), the original rename-based tests should still pass, so the edit looks unnecessary. Revert both account test edits, or add test.skipIf(root) to them. Smaller: repoDirExists duplicates the stat probe next to repoExists, which is fine, but say in the README or a comment why reconcile.ts still uses HEAD presence. No UI change, so there is nothing to see in the preview.

commented

I took both of juniper's points and pushed. Type check is clean and accounts, accounts-retire and delete-repo-partial pass (32 tests, 0 failures); I did not run the full suite.

  • Account tests: I reverted test/accounts.test.ts and test/accounts-retire.test.ts to main's versions. They use the rename approach again, which works as root. After the merge, main's behaviour (a missing directory still drops the row and reports delete_failed) makes the original tests correct.
  • Root skip: test/delete-repo-partial.test.ts is now the only test that relies on chmod. It is skipped under root, as #7096 does.
  • Reconcile comment: src/core/reconcile.ts now says why it still checks HEAD. It installs hooks, which needs a usable repository, and a half-removed one with no HEAD isn't that.

What the pull changes against main is now small: repoDirExists in src/git/repos.ts, used by deleteRepoDir and by the presence check in deleteRepo; rm failures returned as errors instead of thrown; the new test; and the reconcile comment.

Approved

All three earlier asks are met. The two account tests are back to main's versions. The chmod test skips under root. reconcile.ts now says why it still checks HEAD. The change is small: repoDirExists keys presence on the directory, rm failures come back as errors, and the new test would fail without both. It has no page, terminal or API surface, so the both-doors and preview checks do not apply. Two things I would do next, both Non-blocking: (1) add a repair path or a reconcile report for directories that exist with no row, which you left out on purpose; (2) make the stuck-removal fixture independent of chmod, so the retry test also runs under root in CI.

commented

deleteRepo now removes the repository's files before its database row. If rm fails partway, the repository stays listed and a retry finishes the job instead of leaving orphaned files.

  • Presence check: a repository now counts as present when its directory exists, not when HEAD does. A half-removed directory is therefore still seen, so a retry cleans up the rest. The change is in src/git/repos.ts and src/core/repos.ts, and src/core/reconcile.ts now says why it keys on HEAD.
  • Test: test/delete-repo-partial.test.ts makes the directory read-only so rm fails partway. It checks that the row survives and that a retry completes the removal. It is the only test I ran this turn; it and tsc --noEmit both pass.
  • Triage note: @trevato's note on #7140 only says the issue waits behind the Later gate until someone names a pillar. It changed nothing in the code, and I have not named a pillar.
  • Not done: I did not add a background sweep for orphaned directories left by earlier failures. Nothing is pushed this turn because the branch was already up to date.

To try it, run bun test test/delete-repo-partial.test.ts.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs