ls-files: conditionally leave index sparse

When running 'git ls-files' with a pathspec, the index entries get filtered according to that pathspec before iterating over them in show_files(). In 78087097b8 (ls-files: add --sparse option, 2021-12-22), this iteration was prefixed with a check for the '--sparse' option which allows the command to output directory entries; this created a pre-loop call to ensure_full_index(). However, when a user runs 'git ls-files' where the pathspec matches directories that are recursively matched in the sparse-checkout, there are not any sparse directories that match the pathspec so they would not be written to the output. The expansion in this case is just a performance drop for no behavior difference. Replace this global check to expand the index with a check inside the loop for a matched sparse directory. If we see one, then expand the index and continue from the current location. This is safe since the previous entries in the index did not have any sparse directories and thus would remain stable in this expansion. A test in t1092 confirms that this changes the behavior. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Aug 15, 2025 at 16:12 UTC 681f26bccc017371ae6ee20db55e3edb52420a25
2 files changed +23 -3
builtin/ls-files.c
+10 -3
@@ -413,14 +413,21 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
413 if (!(show_cached || show_stage || show_deleted || show_modified))
414 return;
415
416 - if (!show_sparse_dirs)
417 - ensure_full_index(repo->index);
418 -
416 for (i = 0; i < repo->index->cache_nr; i++) {
417 const struct cache_entry *ce = repo->index->cache[i];
418 struct stat st;
419 int stat_err;
420
421 + if (S_ISSPARSEDIR(ce->ce_mode) && !show_sparse_dirs) {
422 + /*
423 + * This is the first time we've hit a sparse dir,
424 + * so expansion will leave the first 'i' entries
425 + * alone.
426 + */
427 + ensure_full_index(repo->index);
428 + ce = repo->index->cache[i];
429 + }
430 +
431 construct_fullname(&fullname, repo, ce);
432
433 if ((dir->flags & DIR_SHOW_IGNORED) &&
t/t1092-sparse-checkout-compatibility.sh
+13
@@ -1506,6 +1506,8 @@ test_expect_success 'sparse-index is not expanded' '
1506 ensure_not_expanded reset --hard &&
1507 ensure_not_expanded restore -s rename-out-to-out -- deep/deeper1 &&
1508
1509 + ensure_not_expanded ls-files deep/deeper1 &&
1510 +
1511 echo >>sparse-index/README.md &&
1512 ensure_not_expanded add -A &&
1513 echo >>sparse-index/extra.txt &&
@@ -1607,6 +1609,17 @@ test_expect_success 'describe tested on all' '
1609 test_all_match git describe --dirty
1610 '
1611
1612 +test_expect_success 'ls-files filtering and expansion' '
1613 + init_repos &&
1614 +
1615 + # This filtering will hit a sparse directory midway
1616 + # through the iteration.
1617 + test_all_match git ls-files deep &&
1618 +
1619 + # This pathspec will filter the index to only a sparse
1620 + # directory.
1621 + test_all_match git ls-files folder1
1622 +'
1623
1624 test_expect_success 'sparse-index is not expanded: describe' '
1625 init_repos &&