wt-status: expand added sparse directory entries

It is difficult, but possible, to get into a state where we intend to add a directory that is outside of the sparse-checkout definition. Add a test to t1092-sparse-checkout-compatibility.sh that demonstrates this using a combination of 'git reset --mixed' and 'git checkout --orphan'. This test failed before because the output of 'git status --porcelain=v2' would not match on the lines for folder1/: * The sparse-checkout repo (with a full index) would output each path name that is intended to be added. * The sparse-index repo would only output that "folder1/" is staged for addition. The status should report the full list of files to be added, and so this sparse-directory entry should be expanded to a full list when reaching it inside the wt_status_collect_changes_initial() method. Use read_tree_at() to assist. Somehow, this loop over the cache entries was not guarded by ensure_full_index() as intended. Reviewed-by: Elijah Newren <newren@gmail.com> Signed-off-by: Derrick Stolee <dstolee@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Jul 14, 2021 at 13:12 UTC fe0d576153117953189e03d6c2c09445ccad4977
2 files changed +84
t/t1092-sparse-checkout-compatibility.sh
+33
@@ -524,4 +524,37 @@ test_expect_success 'sparse-index is not expanded' '
524 test_region ! index ensure_full_index trace2.txt
525 '
526
527 +test_expect_success 'reset mixed and checkout orphan' '
528 + init_repos &&
529 +
530 + test_all_match git checkout rename-out-to-in &&
531 +
532 + # Sparse checkouts do not agree with full checkouts about
533 + # how to report a directory/file conflict during a reset.
534 + # This command would fail with test_all_match because the
535 + # full checkout reports "T folder1/0/1" while a sparse
536 + # checkout reports "D folder1/0/1". This matches because
537 + # the sparse checkouts skip "adding" the other side of
538 + # the conflict.
539 + test_sparse_match git reset --mixed HEAD~1 &&
540 + test_sparse_match test-tool read-cache --table --expand &&
541 + test_sparse_match git status --porcelain=v2 &&
542 +
543 + # At this point, sparse-checkouts behave differently
544 + # from the full-checkout.
545 + test_sparse_match git checkout --orphan new-branch &&
546 + test_sparse_match test-tool read-cache --table --expand &&
547 + test_sparse_match git status --porcelain=v2
548 +'
549 +
550 +test_expect_success 'add everything with deep new file' '
551 + init_repos &&
552 +
553 + run_on_sparse git sparse-checkout set deep/deeper1/deepest &&
554 +
555 + run_on_all touch deep/deeper1/x &&
556 + test_all_match git add . &&
557 + test_all_match git status --porcelain=v2
558 +'
559 +
560 test_done
wt-status.c
+51
@@ -657,6 +657,36 @@ static void wt_status_collect_changes_index(struct wt_status *s)
657 clear_pathspec(&rev.prune_data);
658 }
659
660 +static int add_file_to_list(const struct object_id *oid,
661 + struct strbuf *base, const char *path,
662 + unsigned int mode, void *context)
663 +{
664 + struct string_list_item *it;
665 + struct wt_status_change_data *d;
666 + struct wt_status *s = context;
667 + struct strbuf full_name = STRBUF_INIT;
668 +
669 + if (S_ISDIR(mode))
670 + return READ_TREE_RECURSIVE;
671 +
672 + strbuf_add(&full_name, base->buf, base->len);
673 + strbuf_addstr(&full_name, path);
674 + it = string_list_insert(&s->change, full_name.buf);
675 + d = it->util;
676 + if (!d) {
677 + CALLOC_ARRAY(d, 1);
678 + it->util = d;
679 + }
680 +
681 + d->index_status = DIFF_STATUS_ADDED;
682 + /* Leave {mode,oid}_head zero for adds. */
683 + d->mode_index = mode;
684 + oidcpy(&d->oid_index, oid);
685 + s->committable = 1;
686 + strbuf_release(&full_name);
687 + return 0;
688 +}
689 +
690 static void wt_status_collect_changes_initial(struct wt_status *s)
691 {
692 struct index_state *istate = s->repo->index;
@@ -671,6 +701,27 @@ static void wt_status_collect_changes_initial(struct wt_status *s)
701 continue;
702 if (ce_intent_to_add(ce))
703 continue;
704 + if (S_ISSPARSEDIR(ce->ce_mode)) {
705 + /*
706 + * This is a sparse directory entry, so we want to collect all
707 + * of the added files within the tree. This requires recursively
708 + * expanding the trees to find the elements that are new in this
709 + * tree and marking them with DIFF_STATUS_ADDED.
710 + */
711 + struct strbuf base = STRBUF_INIT;
712 + struct pathspec ps = { 0 };
713 + struct tree *tree = lookup_tree(istate->repo, &ce->oid);
714 +
715 + ps.recursive = 1;
716 + ps.has_wildcard = 1;
717 + ps.max_depth = -1;
718 +
719 + strbuf_add(&base, ce->name, ce->ce_namelen);
720 + read_tree_at(istate->repo, tree, &base, &ps,
721 + add_file_to_list, s);
722 + continue;
723 + }
724 +
725 it = string_list_insert(&s->change, ce->name);
726 d = it->util;
727 if (!d) {