Skip to content

fix(uncheck): swap in the pre-commit hook whole so a running commit finishes the old one - #11

Merged
dinwwwh merged 3 commits into
mainfrom
claude/youthful-lamarr-207e9d
Sep 24, 2026
Merged

dinwwwh merged 3 commits into
mainfrom
claude/youthful-lamarr-207e9d

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 24, 2026

Copy link
Copy Markdown
Member

uncheck prepare --pre-commit now 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, a prepare from another checkout or worktree sharing .git/hooks (for example during pnpm install) could make a running commit die with Syntax error: "||" unexpected after all its checks had passed.

Fixes

  • A running commit is no longer broken when prepare updates the hook, because sh keeps reading the old file.
  • A regular hook still ends up executable (0755) whatever mode it had, and a hook run by the husky 9 or Vite+ dispatcher keeps its mode, so there is no mode change to commit.
  • A symlinked hook keeps its link, and the script it points to is updated.
  • An unwritable hook is still reported without failing the install, and no temporary file is left behind.
  • A hook in a read-only hooks directory is now reported as not written, since the rename needs to create a file next to the hook.

Testing

  • New test: prepare makes 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).
  • New test for a symlinked hook. Existing tests now check the hook's mode after an update, including a husky hook committed as 0755, and check that a hook that can't be written leaves no temporary file.
  • vitest run: 66 passed. uncheck: sherif, oxlint, oxfmt and tsc pass.

…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.
@pkg-pr-new

pkg-pr-new Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/uncheck@11

commit: 81dcf0a

@codecov

codecov Bot commented Sep 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ No critical issues — one minor edge case worth a look.

Reviewed changes

  • Atomic hook swap: prepare now writes the pre-commit hook to a sibling temp file and renames 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 with wx, chmods the temp before the rename, and removes it on any failure.
  • Symlink and mode preservation: the target is resolved with realPath so a symlinked hook keeps its link; dispatched hooks (husky 9 / Vite+) keep their existing mode via stat(target).mode & 0o7777, while plain hooks are forced to 0o755.
  • Tests: a regression test spawns a real sh running 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 base prepare.ts and passes on head, and the full command.test.ts passes (51).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Comment thread packages/uncheck/src/commands/prepare.ts Outdated
…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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ 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, prepare falls back to fs.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 is 0755. I confirmed this case fails against 2085989's prepare.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 (the path.resolve on 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.

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@dinwwwh
dinwwwh merged commit 2c035ee into main Sep 24, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant