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.
§Why the LINK is the thing, never its target
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()fromsymlink_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::renameover a file another process has open fails on Windows where it would succeed on unix. That failure is REPORTED (SwapFailurenames 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.nlinkis 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.
- Swap
Failure - Everything that did and did not happen when a swap went wrong.
Enums§
- Foreign
Why - Why a file at a hook path is not ours to write.
- Hook
File - 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/hooksis one of ours. - is_
within - Whether
childisparentor 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
bodyto a sibling ofdest, ready to be renamed into place. - tracked
- Ask git whether it tracks
path.