Description
With a job that rewrites files — a formatter or linter run with --write / --fix — the hidden unstaged changes of a partially staged file can no longer be re-applied afterwards. When that happens, lefthook reverts the whole worktree with git checkout . — RevertAllUnstagedChanges() in guard.go.
Only partially staged files are covered by the patch that gets re-applied afterwards, so unstaged changes in every other file are silently discarded. The lefthook auto backup stash is dropped in the same run, so nothing is left to recover from.
Introduced in #1417. The abort-and-restore behaviour itself is a clear improvement — only the revert is wider than the backup. 2.1.6 and earlier don't touch the worktree here, but have #1369 instead.
lefthook.yml
pre-commit:
jobs:
- name: formatter
glob: "**/*.txt"
run: perl -pi -e 's/foo/formatted/' {staged_files}
stage_fixed: true
Commands to reproduce
Stage a hunk in a.txt containing foo, then edit that same line again without staging it. Leave an unstaged edit in an unrelated b.txt — already tracked, not a new file, since git checkout . leaves untracked files alone. Commit: the job rewrites foo, re-applying the saved patch fails, and the hook aborts as designed — a.txt is restored correctly, but b.txt is reverted too.
Lefthook version
2.1.10 (also reproduces on 2.1.7)
Possible solution
I assume the wide revert is deliberate: jobs can rewrite files outside the partially staged set, and those changes have to be undone too, so narrowing it to the hidden files alone would leave them behind. That's the tension — the revert has to cover everything the jobs touched, while the patch replayed right after only restores the partially staged ones.
Two things in scope at that point look relevant: withHiddenUnstagedChanges still holds the partiallyStagedFiles list it passed to RevertUnstagedChanges when hiding, and the lefthook auto backup stash created a few lines earlier is a full pre-hook snapshot that is currently only ever dropped, never read back. Happy to test a fix against the case above.
Description
With a job that rewrites files — a formatter or linter run with
--write/--fix— the hidden unstaged changes of a partially staged file can no longer be re-applied afterwards. When that happens, lefthook reverts the whole worktree withgit checkout .—RevertAllUnstagedChanges()inguard.go.Only partially staged files are covered by the patch that gets re-applied afterwards, so unstaged changes in every other file are silently discarded. The
lefthook auto backupstash is dropped in the same run, so nothing is left to recover from.Introduced in #1417. The abort-and-restore behaviour itself is a clear improvement — only the revert is wider than the backup. 2.1.6 and earlier don't touch the worktree here, but have #1369 instead.
lefthook.ymlCommands to reproduce
Stage a hunk in
a.txtcontainingfoo, then edit that same line again without staging it. Leave an unstaged edit in an unrelatedb.txt— already tracked, not a new file, sincegit checkout .leaves untracked files alone. Commit: the job rewritesfoo, re-applying the saved patch fails, and the hook aborts as designed —a.txtis restored correctly, butb.txtis reverted too.Lefthook version
2.1.10 (also reproduces on 2.1.7)
Possible solution
I assume the wide revert is deliberate: jobs can rewrite files outside the partially staged set, and those changes have to be undone too, so narrowing it to the hidden files alone would leave them behind. That's the tension — the revert has to cover everything the jobs touched, while the patch replayed right after only restores the partially staged ones.
Two things in scope at that point look relevant:
withHiddenUnstagedChangesstill holds thepartiallyStagedFileslist it passed toRevertUnstagedChangeswhen hiding, and thelefthook auto backupstash created a few lines earlier is a full pre-hook snapshot that is currently only ever dropped, never read back. Happy to test a fix against the case above.