Skip to main content

Module index

Module index 

Source
Expand description

Reading git’s index, and making git look at a file again after it was rewritten in place.

Rewriting a working-tree file in place is not enough to leave git status clean, and the reason is a shortcut inside git. The index caches the stat of every file next to the object id of its cleaned content. When the cached size differs from the size on disk, git concludes the content changed and stops there — it never runs the clean filter to check. For an unfiltered file that shortcut is sound: a different size is a different file. For a filtered one it is not, and unlock hits it head on, because a clone checked out without a key recorded the size of the ciphertext, and the file is now its plaintext, 38 bytes shorter. lock hits the same wall going the other way.

The object ids the index stores are read here too, by staged_ids. That is what lets lock answer “is this content already a blob in this repository” without opening the object database: the index records the id of every tracked path’s cleaned content, and encryption is deterministic, so hashing what the clean path would produce and comparing is exact.

Paths are matched as raw bytes, and callers supply them from read_dir. On a case-insensitive filesystem (core.ignorecase, the default on macOS and Windows) and under core.precomposeunicode, the same file has two spellings: git keeps the one it was added under, the directory keeps the one on disk. Measured on git 2.55/APFS — a file added as secret.env and renamed to SECRET.env reads as untracked here, and an NFD name on disk does not match the NFC name in the index. Both callers notice when a name they know is tracked does not match. This gap survived open decision 13, settled on 2026-08-05: pattern matching now folds ASCII case, so a declaration reaches every spelling of a name — but the question here is a different one, whether an index entry and a directory entry are the same file, and neither the index nor read_dir folds anything. What changed is which refusal fires first: the walk below now recognises Secrets/db.env as declared, so lock stops on “declared and not tracked under this name” rather than on “the index names a path this walk never saw”. Same state, same exit code, and the message now names the spelling on disk, which is the one the user has to act on.

Correction, 2026-08-05: the consequences were not “on the safe side for lock, which then refuses rather than proceeds”, as this comment claimed until now. Measured on git 2.55 and APFS: after mv secrets Secrets the index still said secrets/db.env, git status was clean, the working-tree walk selected nothing, and lock --yes printed “no file here is declared for encryption”, exited 0 and deleted the key over a readable plaintext secret — the interactive path did the same after a typed yes. lock now proves the opposite by content rather than assuming it: see commands::lock::refuse_if_a_declared_file_is_still_open, which reads this module’s listing and refuses while any declared tracked path still holds plain text on disk.

Measured on git 2.55, in a clone unlocked with the right key:

git hash-object --path secrets/db.env -- secrets/db.env  → b51d5ac…  (matches the index)
git update-index --refresh                               → "needs update"
git status --porcelain                                   → " M secrets/db.env"

The content is right, the blob is right, and git still reports a change — permanently, since the refresh never succeeds and so never rewrites the entry. Zeroing the cached size flips it: git’s own comment in read-cache.c says that a zero length means “we have never even read the lstat information once”, so it has to go to the filesystem and compare content. Measured, same repository: after zeroing, refresh exits 0 and git status is clean.

So this module patches bytes rather than rebuilding the index: writing it out through a library would silently drop the extensions that library does not know how to write — the split-index link above all, whose loss is not a slow git status but a destroyed index. Patching in place preserves every byte we did not mean to change, and the trailing checksum is verified before the edit and recomputed after it, so a file that is not shaped the way we think is left alone rather than mangled.

forget_stat touches four bytes per affected entry. restage also replaces the object id, and therefore has to drop the TREE cache — see its own comment for the measured reason, which is a commit that quietly stored the plaintext again.

Structs§

Tracked
One tracked path, as the index records it.

Enums§

Listed
Every stage-0 entry in the index, or why it could not be read.
Outcome
What the index looked like, and what was done to it.
Restaged
What restage did.
Staged
What the index says about a set of paths.

Functions§

blob_id
The object id git stores for content as a blob.
forget_stat
Makes git re-read paths by forgetting the size it cached for them.
list
Lists what the index records, without being told the paths in advance.
object_hash
The hash a repository’s index is checksummed with.
restage
Points index entries at different blobs, and forgets their cached size.
staged_ids
The object ids the index records for paths.