Repository navigation
Support a cone-mode sparse index under a virtual file system - #1025
Draft
Tyrie Vella (tyrielv) wants to merge 3 commits into
Draft
Tyrie Vella (tyrielv) wants to merge 3 commits into
Tyrie Vella (tyrielv) wants to merge 3 commits into
Conversation
Tyrie Vella (tyrielv)
force-pushed
the
tyrielv/sparse-index/vfs-cone-aware
branch
from
November 14, 2026 05:18
fff2727 to
95d9b53
Compare
expand_index() builds a new cache array that reuses the cache entries of the sparse index. It then discards the name-hash of the index and leaves it uninitialized. The reused entries keep the CE_HASHED flag, so the next lazy_init_name_hash() skips them: hash_index_entry() returns early for an entry that has CE_HASHED. After the expansion, index_file_exists() and index_dir_find() cannot find these entries, and the dir_hash does not contain their directories. This happens when something initializes the name-hash before the index expands. With core.ignoreCase, the untracked-file walk of "git status" does this. For example, take a cone that contains "zin/", with sparse.expectFilesOutsideOfPatterns set and an untracked file in the out-of-cone directory "out/". The walk initializes the name-hash, then the lookup of "out/.gitignore" expands the index. Then the walk cannot find "zin/" in the dir_hash and reports this tracked directory as untracked: ?? out/new ?? zin/ Clear CE_HASHED on each entry that expand_index() reuses, so that the next lazy_init_name_hash() adds it to the new name-hash. The test for the "git status" case also exposes a leak in the same code. expand_index() discards the dir_hash with hashmap_clear(), which frees the table but not the dir_entry structs, so they leak whenever the name-hash was initialized with core.ignoreCase before the expansion. Use free_name_hash(), which also frees the dir_entry structs. Without this, the new test fails under the leak checker. Add a "--expand-and-find=<path>" option to "test-tool read-cache". It looks up a path in the name-hash, expands the index to a full index, and looks up the path again. Add a t1092 test that uses it, and a t1092 test for the "git status" case above. The order in which readdir() returns the directories can hide the "git status" case on some platforms, so the test-tool test is the deterministic check. Assisted-by: Claude Opus 5.5 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
apply_virtualfilesystem() runs on every index read. It sets CE_SKIP_WORKTREE on every cache entry, then clears the bit for each path that the virtual file system hook reports. To find these paths, clear_ce_flags_virtualfilesystem_1() uses index_name_pos(), or the name-hash helpers when core.ignoreCase is set. In a sparse index, a reported path can be inside a sparse-directory entry. Such a path has no cache entry of its own, and index_name_pos() then expands the whole index to a full index. Because this happens on every index read, a virtual file system repository can never keep a sparse index. A directory wildcard (for example "dir/") has a second problem: the loop that clears the bit on all entries below the directory also clears it on a nested sparse-directory entry. Use index_name_pos_sparse() instead. It does not expand the index, so a path inside a sparse-directory entry is not found, and the sparse-directory entry keeps its CE_SKIP_WORKTREE bit. This is correct: the contents of the sparse directory are outside of the sparse-checkout cone. With a full index, index_name_pos_sparse() and index_name_pos() are the same, so a full index behaves as before. The core.ignoreCase lookups do not change: the name-hash does not contain sparse-directory entries, so they do not expand the index. In the directory wildcard loop, skip sparse-directory entries. apply_virtualfilesystem() runs only when core.virtualFilesystem is set, so a repository without a virtual file system behaves as before. A virtual file system that uses a sparse index must keep modified paths inside the cone. Git does not look at the working tree for a path inside a sparse-directory entry. Add t1095. The tests use the trace2 "ensure_full_index" and "expand_index" regions and the "expansion-reason" data to assert that the index does not expand. They check the skip-worktree bits with "git ls-files -t --sparse" for reported paths inside sparse directories, for a directory wildcard that contains a nested sparse directory, and for case-insensitive matches of in-cone paths. They also check that "git status" reports an in-cone change, and that a full index keeps the original behavior. Assisted-by: Claude Opus 5.5 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…system With core.virtualFilesystem, path_in_sparse_checkout_1() reports every path as inside the sparse-checkout. convert_to_sparse() collapses a directory only when path_in_sparse_checkout() reports it as outside, so an out-of-cone directory never collapses into a sparse-directory entry, and a virtual file system repository cannot have a sparse index. When the repository uses cone-mode sparse-checkout and sets index.sparse to true in its configuration, let the cone patterns decide. The new helper vfs_sparse_index_configured() checks the configuration, and vfs_uses_cone_patterns() also checks the pattern file. The default value of index.sparse is true, but it does not count here: a virtual file system must turn this on explicitly, because a virtual file system that cannot read a sparse index must never get one. These helpers run for each path, so they do not use is_sparse_index_allowed(istate, 0), which reads the environment and can write index.sparse when GIT_TEST_SPARSE_INDEX is set. - path_in_sparse_checkout_1() no longer returns early under a virtual file system when vfs_uses_cone_patterns() is true. - path_matches_pattern_list() skips its virtual file system block for a cone-mode pattern list when vfs_sparse_index_configured() is true. The list is already in cone mode, so this checks only the configuration and does not load patterns into the temporary index of expand_index(). For a directory that the virtual file system does not report, that block returns 1. For the cone patterns, 1 means "in the cone", so such a directory would still never collapse. Pattern lists that do not use cone mode keep the virtual file system block. The path-walk API, for example in "git backfill --sparse", also uses a cone-mode pattern list. Under a virtual file system, it now also matches only the patterns, as it does without a virtual file system. This change also fixes "git sparse-checkout reapply" on a collapsed index, which died with "is_excluded_from_virtualfilesystem passed unhandled dtype" when expand_index() passed a sparse directory with DT_UNKNOWN to that block. - do_read_index() no longer collapses a full index under a virtual file system. It calls ensure_correct_sparsity() before post_read_index_from() calls apply_virtualfilesystem(), so convert_to_sparse() would use the skip-worktree bits that are stored in the index file. Under a virtual file system these bits are stale: the hook can now report a path that had the bit when the index was written. Then the directory collapses, and apply_virtualfilesystem() cannot clear the bit of a path inside a sparse directory. The index collapses when it is written, after the bits are correct. - clean_tracked_sparse_directories() returns early under a virtual file system. After "git sparse-checkout set", "add", or "reapply", it deletes each out-of-cone directory that has no untracked files. A virtual file system shows these directories in the working tree, and the virtual file system owns them. Before this change, nothing collapsed under a virtual file system, so this function deleted nothing. All new behavior requires core.virtualFilesystem. The cone patterns are used only when the repository also uses cone mode and sets index.sparse to true. Without a virtual file system, nothing changes. A virtual file system repository without these settings also behaves as before. Update the description of core.virtualFilesystem to match. Add t1096. The out-of-cone files are on disk, as a virtual file system shows them. The tests check that nothing collapses with index.sparse false or with the default value of index.sparse, and that the out-of-cone directory collapses with index.sparse true. They check that "git status" reports an in-cone change without an expansion, and that "git sparse-checkout add" and "reapply" keep all files on disk. They check that a directory with a reported path does not collapse, that non-cone mode under a virtual file system does not change, and that cone mode without a virtual file system does not change. Assisted-by: Claude Opus 5.5 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Tyrie Vella (tyrielv)
force-pushed
the
tyrielv/sparse-index/vfs-cone-aware
branch
from
November 14, 2026 06:12
95d9b53 to
4c6b9eb
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem and Context
A repository with
core.virtualFilesystemcannot use a sparse index today. There are three causes:path_in_sparse_checkout()reports every path as inside the sparse-checkout under a virtual file system. Soconvert_to_sparse()never collapses an out-of-cone directory into a sparse-directory entry.apply_virtualfilesystem()runs on every index read. When it looks up a reported path inside a sparse directory,index_name_pos()expands the whole index. On a repository with about 2.2 million index entries, this full expansion costs many seconds on every command.expand_index(), the name-hash cannot find the cache entries that the expansion reuses. Withcore.ignoreCase, lookups then miss tracked in-cone files. Under a virtual file system,git sparse-checkout addorreapplythen deletes in-cone files from the working tree. Without a virtual file system,git statuscan report a tracked in-cone directory as untracked (see the first commit).The changes to the virtual file system behavior apply only when
core.virtualFilesystemis set. The cone patterns are used only when the repository also uses cone-mode sparse-checkout and setsindex.sparsetotruein its configuration. The default value ofindex.sparse(true) does not count, so that a virtual file system that cannot read a sparse index never gets one. A virtual file system repository without these settings behaves as before.A virtual file system that uses a sparse index must keep modified files inside the cone. Git does not check the working tree for a path inside a sparse-directory entry, even when the hook reports the path.
Changes
The first commit fixes a sparse-index bug that does not depend on a virtual file system. In a plain cone-mode repository with
index.sparse,core.ignoreCase, andsparse.expectFilesOutsideOfPatterns,git statusreports a tracked in-cone directory as untracked when an untracked file sits in an out-of-cone directory that the walk visits first. The commit can move to a separate pull request if reviewers prefer.sparse-index.c:expand_index()clearsCE_HASHEDon each reused cache entry, so that the nextlazy_init_name_hash()adds it to the new name-hash. It also discards the old name-hash withfree_name_hash(), which frees thedir_hashentries thathashmap_clear()leaked. The newgit statustest exposes that leak. This change is not limited to a virtual file system.t/helper/test-read-cache.c: add a--expand-and-find=<path>option. It looks up a path in the name-hash, expands the index, and looks up the path again.git statuswithcore.ignoreCaseafter an expansion.virtualfilesystem.c:clear_ce_flags_virtualfilesystem_1()usesindex_name_pos_sparse()instead ofindex_name_pos(). A reported path inside a sparse directory is not found, so the sparse-directory entry keeps its skip-worktree bit. With a full index the two functions are the same. The directory-wildcard loop skips nested sparse-directory entries.expansion-reasondata to check that the index does not expand. They check the skip-worktree bits for paths inside sparse directories, for directory wildcards, and for case-insensitive matches.virtualfilesystem.c: addvfs_sparse_index_configured()andvfs_uses_cone_patterns(). The first returns true when the configuration sets cone mode and setsindex.sparsetotrueexplicitly. The second also checks the cone-mode pattern file. They run for each path, so they do not read the environment or write the configuration, unlikeis_sparse_index_allowed(istate, 0).dir.c: under a virtual file system,path_in_sparse_checkout_1()uses the cone patterns whenvfs_uses_cone_patterns()returns true.path_matches_pattern_list()gets a pattern list that is already in cone mode, so it checks onlyvfs_sparse_index_configured(). This keeps it from loading patterns into the temporary index ofexpand_index(). For a cone-mode pattern list,path_matches_pattern_list()skips its virtual file system block. That block returns 1 for a directory that the hook does not report, and for the cone list 1 means "in the cone". This also fixesgit sparse-checkout reapplyon a collapsed index, which died withis_excluded_from_virtualfilesystem passed unhandled dtype.read-cache.c: under a virtual file system,do_read_index()does not collapse a full index. The skip-worktree bits are not correct untilapply_virtualfilesystem()sets them, after the read. The index collapses when it is written.builtin/sparse-checkout.c:clean_tracked_sparse_directories()returns early under a virtual file system. Aftergit sparse-checkout set,add, orreapply, this function deletes each out-of-cone directory that has no untracked files. A virtual file system shows these directories in the working tree and owns them. Without this guard, a collapsed index under a virtual file system makes Git delete the projected out-of-cone files.Documentation/config/core.adoc: describe howcore.virtualFilesystemworks with cone mode and an explicitindex.sparse=true.index.sparsefalse or with its default value, and that the out-of-cone directory collapses withindex.sparsetrue. They check thatgit statusdoes not expand the index, and thatgit sparse-checkout addandreapplykeep all files on disk. They also check that a directory with a reported path does not collapse, and that the behavior without a virtual file system or without cone mode does not change.Known gaps and deliberate decisions
git statuswith untracked-file scanning can still expand the index when the walk enters an out-of-cone directory. The tests in this change use--untracked-files=no. A separate change handles the lookups in the working-tree walk.vfs_uses_cone_patterns()runs for each path under a virtual file system. It reads only the cached configuration and the loaded patterns. Its cost on a very large repository was not measured separately.git backfill --sparse) uses a cone-mode pattern list. With the cone patterns in use, it now matches only the patterns, as it does without a virtual file system.