sparse-checkout: add --verbose option to 'clean'

The 'git sparse-checkout clean' subcommand is focused on directories, deleting any tracked sparse directories to clean up the worktree and make the sparse index feature work optimally. However, this directory-focused approach can leave users wondering why those directories exist at all. In my experience, these files are left over due to ignore or exclude patterns, Windows file handles, or possibly merge conflict resolutions. Add a new '--verbose' option for users to see all the files that are being deleted (with '--force') or would be deleted (with '--dry-run'). Based on usage, users may request further context on this list of files for states such as tracked/untracked, unstaged/staged/conflicted, etc. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Sep 12, 2025 at 10:30 UTC 5b5a7f5ebd2e2701ac6fa522866f22b885147c01
3 files changed +40 -5
Documentation/git-sparse-checkout.adoc
+5
@@ -136,6 +136,11 @@ The `--dry-run` option will list the directories that would be removed
136 without deleting them. Running in this mode can be helpful to predict the
137 behavior of the clean comand or to determine which kinds of files are left
138 in the sparse directories.
139 ++
140 +The `--verbose` option will list every file within the directories that
141 +are considered for removal. This option is helpful to determine if those
142 +files are actually important or perhaps to explain why the directory is
143 +still present despite the current sparse-checkout.
144
145 'disable'::
146 Disable the `core.sparseCheckout` config setting, and restore the
builtin/sparse-checkout.c
+24 -2
@@ -930,6 +930,24 @@ static char const * const builtin_sparse_checkout_clean_usage[] = {
930 NULL
931 };
932
933 +static int list_file_iterator(const char *path, const void *data)
934 +{
935 + const char *msg = data;
936 +
937 + printf(msg, path);
938 + return 0;
939 +}
940 +
941 +static void list_every_file_in_dir(const char *msg,
942 + const char *directory)
943 +{
944 + struct strbuf path = STRBUF_INIT;
945 +
946 + strbuf_addstr(&path, directory);
947 + for_each_file_in_dir(&path, list_file_iterator, msg);
948 + strbuf_release(&path);
949 +}
950 +
951 static const char *msg_remove = N_("Removing %s\n");
952 static const char *msg_would_remove = N_("Would remove %s\n");
953
@@ -940,12 +958,13 @@ static int sparse_checkout_clean(int argc, const char **argv,
958 struct strbuf full_path = STRBUF_INIT;
959 const char *msg = msg_remove;
960 size_t worktree_len;
943 - int force = 0, dry_run = 0;
961 + int force = 0, dry_run = 0, verbose = 0;
962 int require_force = 1;
963
964 struct option builtin_sparse_checkout_clean_options[] = {
965 OPT__DRY_RUN(&dry_run, N_("dry run")),
966 OPT__FORCE(&force, N_("force"), PARSE_OPT_NOCOMPLETE),
967 + OPT__VERBOSE(&verbose, N_("report each affected file, not just directories")),
968 OPT_END(),
969 };
970
@@ -987,7 +1006,10 @@ static int sparse_checkout_clean(int argc, const char **argv,
1006 if (!is_directory(full_path.buf))
1007 continue;
1008
990 - printf(msg, ce->name);
1009 + if (verbose)
1010 + list_every_file_in_dir(msg, ce->name);
1011 + else
1012 + printf(msg, ce->name);
1013
1014 if (dry_run <= 0 &&
1015 remove_dir_recursively(&full_path, 0))
t/t1091-sparse-checkout-builtin.sh
+11 -3
@@ -1053,11 +1053,11 @@ test_expect_success 'check-rules null termination' '
1053 test_expect_success 'clean' '
1054 git -C repo sparse-checkout set --cone deep/deeper1 &&
1055 git -C repo sparse-checkout reapply &&
1056 - mkdir repo/deep/deeper2 repo/folder1 &&
1056 + mkdir -p repo/deep/deeper2 repo/folder1/extra/inside &&
1057
1058 # Add untracked files
1059 touch repo/deep/deeper2/file &&
1060 - touch repo/folder1/file &&
1060 + touch repo/folder1/extra/inside/file &&
1061
1062 test_must_fail git -C repo sparse-checkout clean 2>err &&
1063 grep "refusing to clean" err &&
@@ -1074,7 +1074,15 @@ test_expect_success 'clean' '
1074 git -C repo sparse-checkout clean --dry-run >out &&
1075 test_cmp expect out &&
1076 test_path_exists repo/deep/deeper2 &&
1077 - test_path_exists repo/folder1 &&
1077 + test_path_exists repo/folder1/extra/inside/file &&
1078 +
1079 + cat >expect <<-\EOF &&
1080 + Would remove deep/deeper2/file
1081 + Would remove folder1/extra/inside/file
1082 + EOF
1083 +
1084 + git -C repo sparse-checkout clean --dry-run --verbose >out &&
1085 + test_cmp expect out &&
1086
1087 cat >expect <<-\EOF &&
1088 Removing deep/deeper2/