From 3585a318ba7ce8e8144c126b927b9e5027959612 Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Tue, 9 Jun 2026 15:38:08 +0200 Subject: [PATCH 1/3] sparse-index: require commands to declare sparse-index compatibility Change the default value of command_requires_full_index from 1 to -1 (unset) and add an accessor that dies when the value has not been explicitly set by the command. This catches commands that read the index without first declaring whether they need the full index expanded. Each command that has already been audited for sparse-index compatibility sets command_requires_full_index = 0. Commands that have not been audited will now fail loudly instead of silently expanding the index, making it clear which commands still need attention. Set GIT_ALLOW_SPARSE_INDEX_WITHOUT_DECLARATION=1 to fall back to the old behavior (expand full index) while investigating failures. --- read-cache.c | 4 ++-- repo-settings.c | 23 +++++++++++++++-------- repo-settings.h | 2 ++ repository.c | 2 +- unpack-trees.c | 4 ++-- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/read-cache.c b/read-cache.c index 21829102ae275e..999ea5defad3e5 100644 --- a/read-cache.c +++ b/read-cache.c @@ -2189,7 +2189,7 @@ static void set_new_index_sparsity(struct index_state *istate) * repo settings. */ prepare_repo_settings(istate->repo); - if (!istate->repo->settings.command_requires_full_index && + if (!repo_settings_get_command_requires_full_index(istate->repo) && is_sparse_index_allowed(istate, 0)) istate->sparse_index = 1; } @@ -2321,7 +2321,7 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist) * settings and other properties of the index (if necessary). */ prepare_repo_settings(istate->repo); - if (istate->repo->settings.command_requires_full_index) + if (repo_settings_get_command_requires_full_index(istate->repo)) ensure_full_index(istate); else ensure_correct_sparsity(istate); diff --git a/repo-settings.c b/repo-settings.c index 208e09ff17fcee..62122969990bd5 100644 --- a/repo-settings.c +++ b/repo-settings.c @@ -1,5 +1,6 @@ #include "git-compat-util.h" #include "config.h" +#include "gettext.h" #include "repo-settings.h" #include "repository.h" #include "midx.h" @@ -131,14 +132,6 @@ void prepare_repo_settings(struct repository *r) die("unknown fetch negotiation algorithm '%s'", strval); } - /* - * This setting guards all index reads to require a full index - * over a sparse index. After suitable guards are placed in the - * codebase around uses of the index, this setting will be - * removed. - */ - r->settings.command_requires_full_index = 1; - if (!repo_config_get_ulong(r, "core.deltabasecachelimit", &ulongval)) r->settings.delta_base_cache_limit = ulongval; @@ -156,6 +149,20 @@ void prepare_repo_settings(struct repository *r) r->settings.packed_git_limit = ulongval; } +int repo_settings_get_command_requires_full_index(struct repository *r) +{ + if (r->settings.command_requires_full_index >= 0) + return r->settings.command_requires_full_index; + + if (git_env_bool("GIT_ALLOW_SPARSE_INDEX_WITHOUT_DECLARATION", 0)) { + r->settings.command_requires_full_index = 1; + return 1; + } + + die(_("BUG: command has not declared sparse-index compatibility\n" + "set GIT_ALLOW_SPARSE_INDEX_WITHOUT_DECLARATION=1 to bypass")); +} + void repo_settings_clear(struct repository *r) { struct repo_settings empty = REPO_SETTINGS_INIT; diff --git a/repo-settings.h b/repo-settings.h index cad9c3f0cc15f3..cc9316b779d3a1 100644 --- a/repo-settings.h +++ b/repo-settings.h @@ -72,6 +72,7 @@ struct repo_settings { char *hooks_path; }; #define REPO_SETTINGS_INIT { \ + .command_requires_full_index = -1, \ .shared_repository = -1, \ .index_version = -1, \ .core_untracked_cache = UNTRACKED_CACHE_KEEP, \ @@ -85,6 +86,7 @@ struct repo_settings { void prepare_repo_settings(struct repository *r); void repo_settings_clear(struct repository *r); +int repo_settings_get_command_requires_full_index(struct repository *r); /* Read the value for "core.logAllRefUpdates". */ enum log_refs_config repo_settings_get_log_all_ref_updates(struct repository *repo); diff --git a/repository.c b/repository.c index db57b8308b94e7..6c7f86d1b739c3 100644 --- a/repository.c +++ b/repository.c @@ -465,7 +465,7 @@ int repo_read_index(struct repository *repo) res = read_index_from(repo->index, repo->index_file, repo->gitdir); prepare_repo_settings(repo); - if (repo->settings.command_requires_full_index) + if (repo_settings_get_command_requires_full_index(repo)) ensure_full_index(repo->index); /* diff --git a/unpack-trees.c b/unpack-trees.c index 998a1e6dc70cae..33b927747039df 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -1906,7 +1906,7 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options trace2_region_enter("unpack_trees", "unpack_trees", the_repository); prepare_repo_settings(repo); - if (repo->settings.command_requires_full_index) { + if (repo_settings_get_command_requires_full_index(repo)) { ensure_full_index(o->src_index); if (o->dst_index) ensure_full_index(o->dst_index); @@ -1964,7 +1964,7 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options o->internal.result.fsmonitor_has_run_once = o->src_index->fsmonitor_has_run_once; if (!o->src_index->initialized && - !repo->settings.command_requires_full_index && + !repo_settings_get_command_requires_full_index(repo) && is_sparse_index_allowed(&o->internal.result, 0)) o->internal.result.sparse_index = 1; From b6b4a96845e8270224630e6ac02896e4e26aeffb Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Tue, 9 Jun 2026 15:58:39 +0200 Subject: [PATCH 2/3] sparse-index: declare command_requires_full_index for undeclared commands These four code paths were found to never declare their command_requires_full_index value. Set them to 1 (require full index expansion) as a safe default, with comments noting they have not yet been verified for sparse-index compatibility. Found by changing the default to -1 (unset) and adding an accessor that dies when a command reads the index without first declaring its sparse-index requirement. --- builtin/am.c | 5 +++++ builtin/grep.c | 4 ++++ builtin/mv.c | 5 +++++ builtin/submodule--helper.c | 5 +++++ 4 files changed, 19 insertions(+) diff --git a/builtin/am.c b/builtin/am.c index e9623b8307793f..8ed97683ae793c 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -23,6 +23,7 @@ #include "lockfile.h" #include "cache-tree.h" #include "refs.h" +#include "repo-settings.h" #include "commit.h" #include "diff.h" #include "unpack-trees.h" @@ -2464,6 +2465,10 @@ int cmd_am(int argc, /* Ensure a valid committer ident can be constructed */ git_committer_info(IDENT_STRICT); + /* not yet verified whether this can use the sparse index */ + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 1; + if (repo_read_index_preload(the_repository, NULL, 0) < 0) die(_("failed to read the index")); diff --git a/builtin/grep.c b/builtin/grep.c index 6a09571903cd26..7f67839d86714c 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -500,6 +500,10 @@ static int grep_submodule(struct grep_opt *opt, * * Note that this list is not exhaustive. */ + /* not yet verified whether subrepo can use the sparse index */ + prepare_repo_settings(subrepo); + subrepo->settings.command_requires_full_index = 1; + repo_read_gitmodules(subrepo, 0); /* diff --git a/builtin/mv.c b/builtin/mv.c index 948b3306390337..f9b61aeadd1d13 100644 --- a/builtin/mv.c +++ b/builtin/mv.c @@ -22,6 +22,7 @@ #include "string-list.h" #include "parse-options.h" #include "read-cache-ll.h" +#include "repo-settings.h" #include "setup.h" #include "strvec.h" @@ -247,6 +248,10 @@ int cmd_mv(int argc, if (--argc < 1) usage_with_options(builtin_mv_usage, builtin_mv_options); + /* not yet verified whether this can use the sparse index */ + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 1; + repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); if (repo_read_index(the_repository) < 0) die(_("index file corrupt")); diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 1cc82a134db22e..c61e2ba5ce6622 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -16,6 +16,7 @@ #include "read-cache.h" #include "setup.h" #include "sparse-index.h" +#include "repo-settings.h" #include "submodule.h" #include "submodule-config.h" #include "string-list.h" @@ -3832,5 +3833,9 @@ int cmd_submodule__helper(int argc, }; argc = parse_options(argc, argv, prefix, options, usage, 0); + /* not yet verified whether this can use the sparse index */ + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 1; + return fn(argc, argv, prefix, repo); } From 017928956309964c5894ae3421130c586d3451e7 Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Tue, 9 Jun 2026 16:10:00 +0200 Subject: [PATCH 3/3] sparse-index: mark am, mv, submodule, grep as sparse-index compatible These commands were found to have never declared command_requires_full_index. After analysis, all four are safe with sparse index (= 0). git-am: The patch application path uses index_name_pos() which auto-expands sparse directory entries on demand when a specific file path is looked up. The three-way merge fallback uses merge-ort, which works from tree OIDs and handles sparse directories natively. refresh_index() explicitly skips S_ISSPARSEDIR entries. git-rebase --apply invokes git-am as a child process; rebase itself already declares command_requires_full_index = 0. Tested by t1092.42 (merge, cherry-pick, and rebase) which exercises rebase --apply through the sparse-index repo. git-mv: For in-cone moves, all index lookups use index_name_pos() on paths that cannot be ancestors of sparse directory entries (by definition of the sparse cone), so no expansion is triggered. index_range_of_same_dir() iterates only in-cone entries in this case. For out-of-cone moves (--sparse flag), a targeted ensure_full_index() is needed because the bulk cache[] iteration and rename_index_entry_at() assume individual file entries, not collapsed directory stubs. Add tests in t1092.59 (sparse-index is not expanded) to verify that in-cone file and directory renames do not trigger index expansion. git-submodule--helper: Submodule entries (S_ISGITLINK) are never collapsed into sparse directory entries -- convert_to_sparse_rec() in sparse-index.c explicitly blocks directories containing gitlinks from being collapsed. module_list_compute() filters on S_ISGITLINK, skipping any sparse directory entries. module_add() already calls ensure_full_index() unconditionally before its index iteration. Tested by t1092.55 (submodule handling). grep (submodule path): grep_cache() explicitly handles S_ISSPARSEDIR entries by reading the tree object and recursing via grep_tree(), so sparse directory contents are grepped correctly. The subrepo index is independent of the superproject. Tested by t1092.85 (grep sparse directory within submodules). --- builtin/am.c | 3 +-- builtin/grep.c | 3 +-- builtin/mv.c | 7 +++++-- builtin/submodule--helper.c | 3 +-- t/t1092-sparse-checkout-compatibility.sh | 6 +++++- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/builtin/am.c b/builtin/am.c index 8ed97683ae793c..fbb442ae799d8a 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -2465,9 +2465,8 @@ int cmd_am(int argc, /* Ensure a valid committer ident can be constructed */ git_committer_info(IDENT_STRICT); - /* not yet verified whether this can use the sparse index */ prepare_repo_settings(the_repository); - the_repository->settings.command_requires_full_index = 1; + the_repository->settings.command_requires_full_index = 0; if (repo_read_index_preload(the_repository, NULL, 0) < 0) die(_("failed to read the index")); diff --git a/builtin/grep.c b/builtin/grep.c index 7f67839d86714c..d3a6404cc85e39 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -500,9 +500,8 @@ static int grep_submodule(struct grep_opt *opt, * * Note that this list is not exhaustive. */ - /* not yet verified whether subrepo can use the sparse index */ prepare_repo_settings(subrepo); - subrepo->settings.command_requires_full_index = 1; + subrepo->settings.command_requires_full_index = 0; repo_read_gitmodules(subrepo, 0); diff --git a/builtin/mv.c b/builtin/mv.c index f9b61aeadd1d13..860baa5e2c2260 100644 --- a/builtin/mv.c +++ b/builtin/mv.c @@ -28,6 +28,7 @@ #include "strvec.h" #include "submodule.h" #include "entry.h" +#include "sparse-index.h" static const char * const builtin_mv_usage[] = { N_("git mv [-v] [-f] [-n] [-k] "), @@ -248,14 +249,16 @@ int cmd_mv(int argc, if (--argc < 1) usage_with_options(builtin_mv_usage, builtin_mv_options); - /* not yet verified whether this can use the sparse index */ prepare_repo_settings(the_repository); - the_repository->settings.command_requires_full_index = 1; + the_repository->settings.command_requires_full_index = 0; repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); if (repo_read_index(the_repository) < 0) die(_("index file corrupt")); + if (ignore_sparse) + ensure_full_index(the_repository->index); + internal_prefix_pathspec(&sources, prefix, argv, argc, 0); CALLOC_ARRAY(modes, argc); diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index c61e2ba5ce6622..a40cf7d66bd9b6 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -3833,9 +3833,8 @@ int cmd_submodule__helper(int argc, }; argc = parse_options(argc, argv, prefix, options, usage, 0); - /* not yet verified whether this can use the sparse index */ prepare_repo_settings(the_repository); - the_repository->settings.command_requires_full_index = 1; + the_repository->settings.command_requires_full_index = 0; return fn(argc, argv, prefix, repo); } diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index 8186da5c887c56..bd664628296e9a 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -1555,7 +1555,11 @@ test_expect_success 'sparse-index is not expanded' ' ensure_not_expanded merge -m merge update-folder1 && ensure_not_expanded merge -m merge update-folder2 || return 1 done - ) + ) && + + ensure_not_expanded reset --hard && + ensure_not_expanded mv deep/a deep/renamed-a && + ensure_not_expanded mv deep/deeper2 deep/moved-deeper2 ' test_expect_success 'sparse-index is not expanded: merge conflict in cone' '