Expand description
Walking every reachable commit, looking for declared paths stored in the clear.
This is the answer to the product’s largest real risk: a secret committed
before the pattern that covers it existed. Nothing in the working tree
shows it, HEAD need not show it either — deleting the file does not delete
the blob — and it is still sitting at the hosting provider. A shallow check
would report such a repository as clean, which is worse than no check at all.
Three properties shape the implementation.
No decryption, and no key. The verdict per blob is the eleven bytes of
magic at its start, so status works in a locked repository and in a clone
that was never unlocked — which is exactly where a user most needs to ask.
Every object is looked at once. A tree shared by a thousand commits is walked once, a blob appearing under a thousand commits is read once. Without that the cost would be quadratic in the history rather than linear in the object count, and the founding document’s premise — “the cost depends on the number of objects, not their size” — would not hold. The premise holds only approximately, and the gap is worth naming: reading a blob through the object database decompresses all of it, not the first eleven bytes, because neither a loose object nor a packed delta can be truncated part way. The deduplication is what keeps that bounded.
Nothing here fails the scan over one bad object. A repository with a missing object is broken in a way this command did not cause and cannot fix, and refusing outright would withhold the findings from every object that did read. Such objects are counted and reported, so “nothing found” and “nothing found in what I could read” never look the same. A reference that will not resolve is counted separately and weighs more, because it is a whole branch unvisited rather than one file unjudged.
Known limits, recorded rather than hidden:
- The walk state is unbounded.
seen_treesholds one entry per(tree, path)pair over all reachable history, with the path cloned. It is comfortable for ordinary repositories and there is no cap, no progress output and no way to interrupt it part way. If that ever bites, interning the path prefixes and keying on(ObjectId, usize)cuts the dominant term. - A path mid-merge is invisible to the index half of
status, which reads stage 0 only. The history scan still sees the conflicting blobs, because they come from commits; what is missing is a statement about what the next commit would store, which is genuinely undecided until the merge is resolved. HeadLookupresolvesHEADonce per filter process. A long-running filter outlives agit rebasethat moves it, so the warning can be judged against the treeHEADhad at startup. It is advisory either way.- The reflog and the other pseudo-references are out of scope. Every
worktree’s
HEADandrefs/are walked, but notORIG_HEAD,MERGE_HEAD,FETCH_HEADorlogs/. So the canonical “oops”: commit a secret,git reset --hard HEAD~1, then declare the pattern — the blob stays in the object database untilgc.reflogExpire(90 days by default) and this scan reports nothing. The boundary is deliberate: those objects are local and no push carries them, so they are lost work rather than published exposure.git reflog expire --expire=now --allfollowed bygit gc --prune=nowclears them. The scan also says nothing about what a remote already holds, which no local command can. - A declared blob is decompressed whole to be judged. Only 11 bytes are needed, but the object database hands over the whole object, so one multi-gigabyte declared blob in history is a whole-file allocation.
Structs§
- Exposure
- One declared path that reachable history holds in the clear.
- Head
Lookup - One cheap question, asked on the check-in path: is this path already in
HEADin the clear? - Scan
- What one scan found.
- Sighting
- One plaintext blob, and a commit that contains it.
Functions§
- objects
- Opens the repository’s object database.
- scan
- Scans everything reachable in the repository at
git_dir/common_dir. - stored_
in_ the_ clear - Whether the blob
idis stored without our magic.