Skip to main content

Module hookfile

Module hookfile 

Source
Expand description

The single owner of every “is this file ours, and may we touch it?” answer.

Three separate places used to answer that question, each with its own one-liner, and every one of them failed OPEN — an error, an odd file type or a git that would not answer all collapsed into false, and false meant “not foreign”, which meant “go ahead and overwrite it”. The three:

install::foreign_hooks   read_to_string(..).map(|t| !is_our_shim(&t)).unwrap_or(false)
fleet::scan::is_ours     read_to_string(..).map(|s| is_our_shim(&s)).unwrap_or(false)
fleet::fix::plan         read_to_string(..).map(|t| !is_our_shim(&t)).unwrap_or(false)

read_to_string fails on any file that is not valid UTF-8. A compiled hook — somebody’s Go binary at .git/hooks/pre-commit, which is a perfectly ordinary thing to have — reads back Err(InvalidData), and unwrap_or(false) turned that into “not foreign”. amont install then wrote a shim straight over it, with no --force, no refusal, and no message. That is the same class of failure as the two incidents that overwrote tracked source files, and it had no guard at all.

So everything here fails CLOSED. When this module cannot establish that a path is ours and safe, it says so with a reason, and the caller refuses. Refusing is cheap; the alternative has destroyed somebody’s work three times.

std::fs::write and std::fs::OpenOptions FOLLOW symlinks: opening .git/hooks/pre-commit when that is a link to ../../devhooks/pre-commit truncates and rewrites devhooks/pre-commit, a tracked file in the working tree. That is the verified bug this module exists to end: the guard checked the link path — untracked, in .git, nothing alarming about it — and the write landed somewhere else entirely.

std::fs::rename does NOT follow symlinks; it replaces the link. So every write in this module is staged to a sibling temporary file and renamed into place. --force on a symlinked hook therefore means “replace the link”, and the file it pointed at is never opened at all. There is no code path here that writes through a link, which is a stronger statement than “we check first” — a check can be raced, and this cannot.

§Windows

  • A junction and a directory symlink both report is_symlink() from symlink_metadata, so the symlink refusal covers them. Ordinary users cannot create file symlinks without Developer Mode, which is why the symlink tests are #[cfg(unix)] — the CODE is not.
  • fs::rename over a file another process has open fails on Windows where it would succeed on unix. That failure is REPORTED (SwapFailure names the path and the io error) rather than swallowed, because a hook that was not written is exactly what the caller must be told about.
  • nlink is unix-only. The multiply-linked refusal below is #[cfg(unix)]; Windows hard links exist but std exposes no count, so that particular check simply is not made there. Every other guard applies on both.

Structs§

Staged
A body written to a sibling temporary file, not yet in place.
SwapFailure
Everything that did and did not happen when a swap went wrong.

Enums§

ForeignWhy
Why a file at a hook path is not ours to write.
HookFile
What is at a hook path, decided once, by classify.
Refuse
A refusal to touch a path, with the reason attached.
Tracked
Whether git tracks a path — with “could not ask” kept distinct from “no”.

Constants§

SHIM_MARKER
A line every shim carries and nothing else does.

Functions§

classify
What is at path. Never mutates, never follows a link, never guesses.
commit_all
Rename every staged file into place, stopping at the first failure.
guard_remove
May we remove the hook at path?
guard_write
May we write a hook at path, and what is there now?
is_our_shim
Whether a file in .git/hooks is one of ours.
is_within
Whether child is parent or lives under it, lexically.
remove_regular
Remove a file, treating “already gone” as success.
resolve_lexical
. and .. resolved textually, without asking the filesystem.
stage
Write body to a sibling of dest, ready to be renamed into place.
tracked
Ask git whether it tracks path.