From 2c2789e221fdcd84c5aa9c66e2f7b07482ec4907 Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Tue, 9 Jun 2026 18:55:38 +0200 Subject: [PATCH 1/2] sparse-index: require commands to declare compatibility 3c31623f6a (sparse-index: add guard to ensure full index, 2021-01-08) introduced command_requires_full_index as scaffolding: every command starts by expanding the sparse index, and commands opt out one by one by setting the field to 0. Because the default is 1, a command that never touches the field silently gets full expansion -- there is no signal that it was forgotten rather than intentionally left. Change the default to a sentinel (-1) and add an accessor that dies if a command reaches index reading without declaring a value. An environment variable GIT_ALLOW_SPARSE_INDEX_WITHOUT_DECLARATION=1 bypasses the check as a safety net. Both the safety net and the field itself are meant to be removed once no commands require full expansion from the start. Four commands that never declared this field are surfaced and explicitly set to 1 to preserve existing behavior: - git-am - git-mv - git-submodule--helper - grep's submodule path Behavior change: any undeclared code path will now die() instead of silently expanding. Signed-off-by: Kristofer Karlsson --- builtin/am.c | 4 ++++ builtin/grep.c | 4 ++++ builtin/mv.c | 4 ++++ builtin/submodule--helper.c | 5 +++++ read-cache.c | 4 ++-- repo-settings.c | 23 +++++++++++++++-------- repo-settings.h | 2 ++ repository.c | 2 +- unpack-trees.c | 4 ++-- 9 files changed, 39 insertions(+), 13 deletions(-) diff --git a/builtin/am.c b/builtin/am.c index e9623b8307793f..f71fa64e2eef02 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,9 @@ int cmd_am(int argc, /* Ensure a valid committer ident can be constructed */ git_committer_info(IDENT_STRICT); + 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..c33e414e78c5f6 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,9 @@ int cmd_mv(int argc, if (--argc < 1) usage_with_options(builtin_mv_usage, builtin_mv_options); + 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); } 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..8244e6b1a34688 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(_("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 04e22e28ed857cd73a12777bb9442640a131b505 Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Tue, 9 Jun 2026 18:56:19 +0200 Subject: [PATCH 2/2] sparse-index: mark mv as sparse-index compatible In-cone moves already work without expanding the index, since index_name_pos() auto-expands sparse directory entries on demand. For out-of-cone moves (--sparse), call ensure_full_index() up front because the bulk iteration assumes individual file entries. Without --sparse, moving a sparse directory should produce the helpful "Use the --sparse option" hint. The old code relied on full index expansion to make the individual skip-worktree entries visible to the error path. Now that the index stays sparse, teach empty_dir_has_sparse_contents() to recognize sparse directory entries directly and short-circuit to the hint. As a side effect the error now names the directory the user typed rather than listing each file inside it. Add tests verifying that in-cone renames do not expand the index, that out-of-cone moves without --sparse produce the hint, and that --sparse triggers expansion. Signed-off-by: Kristofer Karlsson --- builtin/mv.c | 16 ++++++++++++++-- t/t1092-sparse-checkout-compatibility.sh | 18 ++++++++++++++++++ t/t7002-mv-sparse-checkout.sh | 2 +- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/builtin/mv.c b/builtin/mv.c index c33e414e78c5f6..fbd1bd606019be 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] "), @@ -153,7 +154,11 @@ static int empty_dir_has_sparse_contents(const char *name) int pos = index_name_pos(the_repository->index, with_slash, length); const struct cache_entry *ce; - if (pos < 0) { + if (pos >= 0) { + ce = the_repository->index->cache[pos]; + if (S_ISSPARSEDIR(ce->ce_mode)) + ret = 1; + } else { pos = -pos - 1; if (pos >= the_repository->index->cache_nr) goto free_return; @@ -249,12 +254,15 @@ int cmd_mv(int argc, usage_with_options(builtin_mv_usage, builtin_mv_options); 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); @@ -317,6 +325,10 @@ int cmd_mv(int argc, if (!path_in_sparse_checkout(src_w_slash, the_repository->index) && empty_dir_has_sparse_contents(src)) { free(src_w_slash); + if (!ignore_sparse) { + string_list_append(&only_match_skip_worktree, src); + goto act_on_entry; + } modes[i] |= SKIP_WORKTREE_DIR; goto dir_check; } diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index 8186da5c887c56..67a8bac2c2470d 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -1558,6 +1558,24 @@ test_expect_success 'sparse-index is not expanded' ' ) ' +test_expect_success 'sparse-index is not expanded: git mv' ' + init_repos && + + ensure_not_expanded mv deep/a deep/renamed-a && + ensure_not_expanded mv deep/deeper2 deep/moved-deeper2 && + + ensure_not_expanded reset --hard && + + # mv of a sparse directory without --sparse should fail with + # a useful hint, without expanding the index. + run_sparse_index_trace2 ! mv folder1 deep && + test_region ! index ensure_full_index trace2.txt && + grep "Use the --sparse option" sparse-index-error && + + ensure_not_expanded reset --hard && + ensure_expanded mv --sparse folder1 deep +' + test_expect_success 'sparse-index is not expanded: merge conflict in cone' ' init_repos && diff --git a/t/t7002-mv-sparse-checkout.sh b/t/t7002-mv-sparse-checkout.sh index 4d3f221224fb39..65f1bf0f33bf80 100755 --- a/t/t7002-mv-sparse-checkout.sh +++ b/t/t7002-mv-sparse-checkout.sh @@ -238,7 +238,7 @@ test_expect_success 'refuse to move out-of-cone directory without --sparse' ' test_must_fail git mv folder1 sub 2>stderr && cat sparse_error_header >expect && - echo folder1/file1 >>expect && + echo folder1 >>expect && cat sparse_hint >>expect && test_cmp expect stderr '