fix(uncheck): swap in the pre-commit hook whole so a running commit finishes the old one - #11
Merged
Merged
Conversation
…inishes the old one `sh` reads a script while running it, so `prepare` rewriting the hook in place (for example `pnpm install` in another worktree sharing `.git/hooks`) made a commit already running the hook carry on in the new content and die with a syntax error after its checks had passed. The hook is now written to a temporary file next to it and renamed over it, so a running commit keeps reading the old one. A regular hook still ends up executable whatever mode it had, a hook run by the husky 9 or Vite+ dispatcher keeps its mode, a symlinked hook keeps its link and updates the script it points to, and an unwritable hook is still reported without leaving the temporary file behind.
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
ℹ️ No critical issues — one minor edge case worth a look.
Reviewed changes
- Atomic hook swap:
preparenow writes the pre-commit hook to a sibling temp file andrenames it over the target, so a commit already running the old hook keeps reading the old inode instead of seeing a half-rewritten script.replaceFile(packages/uncheck/src/commands/prepare.ts:59) writes withwx, chmods the temp before the rename, and removes it on any failure. - Symlink and mode preservation: the target is resolved with
realPathso a symlinked hook keeps its link; dispatched hooks (husky 9 / Vite+) keep their existing mode viastat(target).mode & 0o7777, while plain hooks are forced to0o755. - Tests: a regression test spawns a real
shrunning the hook, blocks it mid-run, rewrites the hook, and asserts the running commit still exits 0 having run the old lines; plus a symlinked-hook test, mode assertions, and a no-temp-left check. I confirmed the regression test fails (exit 2) against the baseprepare.tsand passes on head, and the fullcommand.test.tspasses (51).
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏
…ist yet `realPath` fails on a dangling link, so the rename replaced the link with a regular file. The link is now read and its script created, as the old in-place write did.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This increment covers 81dcf0a on top of the previously reviewed 2085989.
- Dangling symlink hooks now followed, not replaced: when
fs.realPath(file)fails,preparefalls back tofs.readLink(file)resolved against the hook's directory, then to the hook path itself, so a link to a script that does not exist yet keeps its link and the target script is created. - Regression test extended: the symlinked-hook test now removes the hook, points it at a not-yet-existing
scripts/later, and asserts the link survives, the target receives the header + hook line, and the mode is0755. I confirmed this case fails against2085989'sprepare.ts(the link is replaced by a regular file) and passes on head, so it is real coverage rather than theatre. - Behavior preserved elsewhere: resolvable links still resolve via
realPath(thepath.resolveon an absolute path is a no-op), non-links still fall back to the hook path, and the mode handling is unchanged.
The prior review's one thread (dangling symlink replaced instead of followed) is resolved by this commit.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

uncheck prepare --pre-commitnow puts a new hook in place with a rename instead of rewriting the file in place, so a commit that is already running the hook finishes the hook it started with. Before, apreparefrom another checkout or worktree sharing.git/hooks(for example duringpnpm install) could make a running commit die withSyntax error: "||" unexpectedafter all its checks had passed.Fixes
prepareupdates the hook, becauseshkeeps reading the old file.Testing
preparemakes the hook longer while a commit is running it, and the commit still exits 0 after running the old hook's lines. On the old code, the commit dies with a dash syntax error (exit 2).vitest run: 66 passed.uncheck: sherif, oxlint, oxfmt and tsc pass.