Merge branch 'ds/sparse-checkout-clean'

"git sparse-checkout" subcommand learned a new "clean" action to
prune otherwise unused working-tree files that are outside the
areas of interest.

* ds/sparse-checkout-clean:
  sparse-index: improve advice message instructions
  t: expand tests around sparse merges and clean
  sparse-index: point users to new 'clean' action
  sparse-checkout: add --verbose option to 'clean'
  dir: add generic "walk all files" helper
  sparse-checkout: match some 'clean' behavior
  sparse-checkout: add basics of 'clean' command
  sparse-checkout: remove use of the_repository
This commit is contained in:
Junio C Hamano
2025-10-28 10:29:08 -07:00
6 changed files with 412 additions and 58 deletions

View File

@@ -9,7 +9,7 @@ git-sparse-checkout - Reduce your working tree to a subset of tracked files
SYNOPSIS
--------
[verse]
'git sparse-checkout' (init | list | set | add | reapply | disable | check-rules) [<options>]
'git sparse-checkout' (init | list | set | add | reapply | disable | check-rules | clean) [<options>]
DESCRIPTION
@@ -111,6 +111,37 @@ flags, with the same meaning as the flags from the `set` command, in order
to change which sparsity mode you are using without needing to also respecify
all sparsity paths.
'clean'::
Opportunistically remove files outside of the sparse-checkout
definition. This command requires cone mode to use recursive
directory matches to determine which files should be removed. A
file is considered for removal if it is contained within a tracked
directory that is outside of the sparse-checkout definition.
+
Some special cases, such as merge conflicts or modified files outside of
the sparse-checkout definition could lead to keeping files that would
otherwise be removed. Resolve conflicts, stage modifications, and use
`git sparse-checkout reapply` in conjunction with `git sparse-checkout
clean` to resolve these cases.
+
This command can be used to be sure the sparse index works efficiently,
though it does not require enabling the sparse index feature via the
`index.sparse=true` configuration.
+
To prevent accidental deletion of worktree files, the `clean` subcommand
will not delete any files without the `-f` or `--force` option, unless
the `clean.requireForce` config option is set to `false`.
+
The `--dry-run` option will list the directories that would be removed
without deleting them. Running in this mode can be helpful to predict the
behavior of the clean comand or to determine which kinds of files are left
in the sparse directories.
+
The `--verbose` option will list every file within the directories that
are considered for removal. This option is helpful to determine if those
files are actually important or perhaps to explain why the directory is
still present despite the current sparse-checkout.
'disable'::
Disable the `core.sparseCheckout` config setting, and restore the
working directory to include all files.

View File

@@ -2,6 +2,7 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "builtin.h"
#include "abspath.h"
#include "config.h"
#include "dir.h"
#include "environment.h"
@@ -23,7 +24,7 @@
static const char *empty_base = "";
static char const * const builtin_sparse_checkout_usage[] = {
N_("git sparse-checkout (init | list | set | add | reapply | disable | check-rules) [<options>]"),
N_("git sparse-checkout (init | list | set | add | reapply | disable | check-rules | clean) [<options>]"),
NULL
};
@@ -204,12 +205,12 @@ static void clean_tracked_sparse_directories(struct repository *r)
ensure_full_index(r->index);
}
static int update_working_directory(struct pattern_list *pl)
static int update_working_directory(struct repository *r,
struct pattern_list *pl)
{
enum update_sparsity_result result;
struct unpack_trees_options o;
struct lock_file lock_file = LOCK_INIT;
struct repository *r = the_repository;
struct pattern_list *old_pl;
/* If no branch has been checked out, there are no updates to make. */
@@ -327,7 +328,8 @@ static void write_cone_to_file(FILE *fp, struct pattern_list *pl)
string_list_clear(&sl, 0);
}
static int write_patterns_and_update(struct pattern_list *pl)
static int write_patterns_and_update(struct repository *repo,
struct pattern_list *pl)
{
char *sparse_filename;
FILE *fp;
@@ -336,15 +338,15 @@ static int write_patterns_and_update(struct pattern_list *pl)
sparse_filename = get_sparse_checkout_filename();
if (safe_create_leading_directories(the_repository, sparse_filename))
if (safe_create_leading_directories(repo, sparse_filename))
die(_("failed to create directory for sparse-checkout file"));
hold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR);
result = update_working_directory(pl);
result = update_working_directory(repo, pl);
if (result) {
rollback_lock_file(&lk);
update_working_directory(NULL);
update_working_directory(repo, NULL);
goto out;
}
@@ -372,25 +374,26 @@ enum sparse_checkout_mode {
MODE_CONE_PATTERNS = 2,
};
static int set_config(enum sparse_checkout_mode mode)
static int set_config(struct repository *repo,
enum sparse_checkout_mode mode)
{
/* Update to use worktree config, if not already. */
if (init_worktree_config(the_repository)) {
if (init_worktree_config(repo)) {
error(_("failed to initialize worktree config"));
return 1;
}
if (repo_config_set_worktree_gently(the_repository,
if (repo_config_set_worktree_gently(repo,
"core.sparseCheckout",
mode ? "true" : "false") ||
repo_config_set_worktree_gently(the_repository,
repo_config_set_worktree_gently(repo,
"core.sparseCheckoutCone",
mode == MODE_CONE_PATTERNS ?
"true" : "false"))
return 1;
if (mode == MODE_NO_PATTERNS)
return set_sparse_index_config(the_repository, 0);
return set_sparse_index_config(repo, 0);
return 0;
}
@@ -410,7 +413,7 @@ static enum sparse_checkout_mode update_cone_mode(int *cone_mode) {
return MODE_ALL_PATTERNS;
}
static int update_modes(int *cone_mode, int *sparse_index)
static int update_modes(struct repository *repo, int *cone_mode, int *sparse_index)
{
int mode, record_mode;
@@ -418,20 +421,20 @@ static int update_modes(int *cone_mode, int *sparse_index)
record_mode = (*cone_mode != -1) || !core_apply_sparse_checkout;
mode = update_cone_mode(cone_mode);
if (record_mode && set_config(mode))
if (record_mode && set_config(repo, mode))
return 1;
/* Set sparse-index/non-sparse-index mode if specified */
if (*sparse_index >= 0) {
if (set_sparse_index_config(the_repository, *sparse_index) < 0)
if (set_sparse_index_config(repo, *sparse_index) < 0)
die(_("failed to modify sparse-index config"));
/* force an index rewrite */
repo_read_index(the_repository);
the_repository->index->updated_workdir = 1;
repo_read_index(repo);
repo->index->updated_workdir = 1;
if (!*sparse_index)
ensure_full_index(the_repository->index);
ensure_full_index(repo->index);
}
return 0;
@@ -448,7 +451,7 @@ static struct sparse_checkout_init_opts {
} init_opts;
static int sparse_checkout_init(int argc, const char **argv, const char *prefix,
struct repository *repo UNUSED)
struct repository *repo)
{
struct pattern_list pl;
char *sparse_filename;
@@ -464,7 +467,7 @@ static int sparse_checkout_init(int argc, const char **argv, const char *prefix,
};
setup_work_tree();
repo_read_index(the_repository);
repo_read_index(repo);
init_opts.cone_mode = -1;
init_opts.sparse_index = -1;
@@ -473,7 +476,7 @@ static int sparse_checkout_init(int argc, const char **argv, const char *prefix,
builtin_sparse_checkout_init_options,
builtin_sparse_checkout_init_usage, 0);
if (update_modes(&init_opts.cone_mode, &init_opts.sparse_index))
if (update_modes(repo, &init_opts.cone_mode, &init_opts.sparse_index))
return 1;
memset(&pl, 0, sizeof(pl));
@@ -485,14 +488,14 @@ static int sparse_checkout_init(int argc, const char **argv, const char *prefix,
if (res >= 0) {
free(sparse_filename);
clear_pattern_list(&pl);
return update_working_directory(NULL);
return update_working_directory(repo, NULL);
}
if (repo_get_oid(the_repository, "HEAD", &oid)) {
if (repo_get_oid(repo, "HEAD", &oid)) {
FILE *fp;
/* assume we are in a fresh repo, but update the sparse-checkout file */
if (safe_create_leading_directories(the_repository, sparse_filename))
if (safe_create_leading_directories(repo, sparse_filename))
die(_("unable to create leading directories of %s"),
sparse_filename);
fp = xfopen(sparse_filename, "w");
@@ -511,7 +514,7 @@ static int sparse_checkout_init(int argc, const char **argv, const char *prefix,
add_pattern("!/*/", empty_base, 0, &pl, 0);
pl.use_cone_patterns = init_opts.cone_mode;
return write_patterns_and_update(&pl);
return write_patterns_and_update(repo, &pl);
}
static void insert_recursive_pattern(struct pattern_list *pl, struct strbuf *path)
@@ -674,7 +677,8 @@ static void add_patterns_literal(int argc, const char **argv,
add_patterns_from_input(pl, argc, argv, use_stdin ? stdin : NULL);
}
static int modify_pattern_list(struct strvec *args, int use_stdin,
static int modify_pattern_list(struct repository *repo,
struct strvec *args, int use_stdin,
enum modify_type m)
{
int result;
@@ -696,22 +700,23 @@ static int modify_pattern_list(struct strvec *args, int use_stdin,
}
if (!core_apply_sparse_checkout) {
set_config(MODE_ALL_PATTERNS);
set_config(repo, MODE_ALL_PATTERNS);
core_apply_sparse_checkout = 1;
changed_config = 1;
}
result = write_patterns_and_update(pl);
result = write_patterns_and_update(repo, pl);
if (result && changed_config)
set_config(MODE_NO_PATTERNS);
set_config(repo, MODE_NO_PATTERNS);
clear_pattern_list(pl);
free(pl);
return result;
}
static void sanitize_paths(struct strvec *args,
static void sanitize_paths(struct repository *repo,
struct strvec *args,
const char *prefix, int skip_checks)
{
int i;
@@ -752,7 +757,7 @@ static void sanitize_paths(struct strvec *args,
for (i = 0; i < args->nr; i++) {
struct cache_entry *ce;
struct index_state *index = the_repository->index;
struct index_state *index = repo->index;
int pos = index_name_pos(index, args->v[i], strlen(args->v[i]));
if (pos < 0)
@@ -779,7 +784,7 @@ static struct sparse_checkout_add_opts {
} add_opts;
static int sparse_checkout_add(int argc, const char **argv, const char *prefix,
struct repository *repo UNUSED)
struct repository *repo)
{
static struct option builtin_sparse_checkout_add_options[] = {
OPT_BOOL_F(0, "skip-checks", &add_opts.skip_checks,
@@ -796,7 +801,7 @@ static int sparse_checkout_add(int argc, const char **argv, const char *prefix,
if (!core_apply_sparse_checkout)
die(_("no sparse-checkout to add to"));
repo_read_index(the_repository);
repo_read_index(repo);
argc = parse_options(argc, argv, prefix,
builtin_sparse_checkout_add_options,
@@ -804,9 +809,9 @@ static int sparse_checkout_add(int argc, const char **argv, const char *prefix,
for (int i = 0; i < argc; i++)
strvec_push(&patterns, argv[i]);
sanitize_paths(&patterns, prefix, add_opts.skip_checks);
sanitize_paths(repo, &patterns, prefix, add_opts.skip_checks);
ret = modify_pattern_list(&patterns, add_opts.use_stdin, ADD);
ret = modify_pattern_list(repo, &patterns, add_opts.use_stdin, ADD);
strvec_clear(&patterns);
return ret;
@@ -825,7 +830,7 @@ static struct sparse_checkout_set_opts {
} set_opts;
static int sparse_checkout_set(int argc, const char **argv, const char *prefix,
struct repository *repo UNUSED)
struct repository *repo)
{
int default_patterns_nr = 2;
const char *default_patterns[] = {"/*", "!/*/", NULL};
@@ -847,7 +852,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix,
int ret;
setup_work_tree();
repo_read_index(the_repository);
repo_read_index(repo);
set_opts.cone_mode = -1;
set_opts.sparse_index = -1;
@@ -856,7 +861,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix,
builtin_sparse_checkout_set_options,
builtin_sparse_checkout_set_usage, 0);
if (update_modes(&set_opts.cone_mode, &set_opts.sparse_index))
if (update_modes(repo, &set_opts.cone_mode, &set_opts.sparse_index))
return 1;
/*
@@ -870,10 +875,10 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix,
} else {
for (int i = 0; i < argc; i++)
strvec_push(&patterns, argv[i]);
sanitize_paths(&patterns, prefix, set_opts.skip_checks);
sanitize_paths(repo, &patterns, prefix, set_opts.skip_checks);
}
ret = modify_pattern_list(&patterns, set_opts.use_stdin, REPLACE);
ret = modify_pattern_list(repo, &patterns, set_opts.use_stdin, REPLACE);
strvec_clear(&patterns);
return ret;
@@ -891,7 +896,7 @@ static struct sparse_checkout_reapply_opts {
static int sparse_checkout_reapply(int argc, const char **argv,
const char *prefix,
struct repository *repo UNUSED)
struct repository *repo)
{
static struct option builtin_sparse_checkout_reapply_options[] = {
OPT_BOOL(0, "cone", &reapply_opts.cone_mode,
@@ -912,12 +917,107 @@ static int sparse_checkout_reapply(int argc, const char **argv,
builtin_sparse_checkout_reapply_options,
builtin_sparse_checkout_reapply_usage, 0);
repo_read_index(the_repository);
repo_read_index(repo);
if (update_modes(&reapply_opts.cone_mode, &reapply_opts.sparse_index))
if (update_modes(repo, &reapply_opts.cone_mode, &reapply_opts.sparse_index))
return 1;
return update_working_directory(NULL);
return update_working_directory(repo, NULL);
}
static char const * const builtin_sparse_checkout_clean_usage[] = {
"git sparse-checkout clean [-n|--dry-run]",
NULL
};
static int list_file_iterator(const char *path, const void *data)
{
const char *msg = data;
printf(msg, path);
return 0;
}
static void list_every_file_in_dir(const char *msg,
const char *directory)
{
struct strbuf path = STRBUF_INIT;
strbuf_addstr(&path, directory);
for_each_file_in_dir(&path, list_file_iterator, msg);
strbuf_release(&path);
}
static const char *msg_remove = N_("Removing %s\n");
static const char *msg_would_remove = N_("Would remove %s\n");
static int sparse_checkout_clean(int argc, const char **argv,
const char *prefix,
struct repository *repo)
{
struct strbuf full_path = STRBUF_INIT;
const char *msg = msg_remove;
size_t worktree_len;
int force = 0, dry_run = 0, verbose = 0;
int require_force = 1;
struct option builtin_sparse_checkout_clean_options[] = {
OPT__DRY_RUN(&dry_run, N_("dry run")),
OPT__FORCE(&force, N_("force"), PARSE_OPT_NOCOMPLETE),
OPT__VERBOSE(&verbose, N_("report each affected file, not just directories")),
OPT_END(),
};
setup_work_tree();
if (!core_apply_sparse_checkout)
die(_("must be in a sparse-checkout to clean directories"));
if (!core_sparse_checkout_cone)
die(_("must be in a cone-mode sparse-checkout to clean directories"));
argc = parse_options(argc, argv, prefix,
builtin_sparse_checkout_clean_options,
builtin_sparse_checkout_clean_usage, 0);
repo_config_get_bool(repo, "clean.requireforce", &require_force);
if (require_force && !force && !dry_run)
die(_("for safety, refusing to clean without one of --force or --dry-run"));
if (dry_run)
msg = msg_would_remove;
if (repo_read_index(repo) < 0)
die(_("failed to read index"));
if (convert_to_sparse(repo->index, SPARSE_INDEX_MEMORY_ONLY) ||
repo->index->sparse_index == INDEX_EXPANDED)
die(_("failed to convert index to a sparse index; resolve merge conflicts and try again"));
strbuf_addstr(&full_path, repo->worktree);
strbuf_addch(&full_path, '/');
worktree_len = full_path.len;
for (size_t i = 0; i < repo->index->cache_nr; i++) {
struct cache_entry *ce = repo->index->cache[i];
if (!S_ISSPARSEDIR(ce->ce_mode))
continue;
strbuf_setlen(&full_path, worktree_len);
strbuf_add(&full_path, ce->name, ce->ce_namelen);
if (!is_directory(full_path.buf))
continue;
if (verbose)
list_every_file_in_dir(msg, ce->name);
else
printf(msg, ce->name);
if (dry_run <= 0 &&
remove_dir_recursively(&full_path, 0))
warning_errno(_("failed to remove '%s'"), ce->name);
}
strbuf_release(&full_path);
return 0;
}
static char const * const builtin_sparse_checkout_disable_usage[] = {
@@ -927,7 +1027,7 @@ static char const * const builtin_sparse_checkout_disable_usage[] = {
static int sparse_checkout_disable(int argc, const char **argv,
const char *prefix,
struct repository *repo UNUSED)
struct repository *repo)
{
static struct option builtin_sparse_checkout_disable_options[] = {
OPT_END(),
@@ -955,7 +1055,7 @@ static int sparse_checkout_disable(int argc, const char **argv,
* are expecting to do that when disabling sparse-checkout.
*/
give_advice_on_expansion = 0;
repo_read_index(the_repository);
repo_read_index(repo);
memset(&pl, 0, sizeof(pl));
hashmap_init(&pl.recursive_hashmap, pl_hashmap_cmp, NULL, 0);
@@ -966,13 +1066,13 @@ static int sparse_checkout_disable(int argc, const char **argv,
add_pattern("/*", empty_base, 0, &pl, 0);
prepare_repo_settings(the_repository);
the_repository->settings.sparse_index = 0;
repo->settings.sparse_index = 0;
if (update_working_directory(&pl))
if (update_working_directory(repo, &pl))
die(_("error while refreshing working directory"));
clear_pattern_list(&pl);
return set_config(MODE_NO_PATTERNS);
return set_config(repo, MODE_NO_PATTERNS);
}
static char const * const builtin_sparse_checkout_check_rules_usage[] = {
@@ -987,14 +1087,17 @@ static struct sparse_checkout_check_rules_opts {
char *rules_file;
} check_rules_opts;
static int check_rules(struct pattern_list *pl, int null_terminated) {
static int check_rules(struct repository *repo,
struct pattern_list *pl,
int null_terminated)
{
struct strbuf line = STRBUF_INIT;
struct strbuf unquoted = STRBUF_INIT;
char *path;
int line_terminator = null_terminated ? 0 : '\n';
strbuf_getline_fn getline_fn = null_terminated ? strbuf_getline_nul
: strbuf_getline;
the_repository->index->sparse_checkout_patterns = pl;
repo->index->sparse_checkout_patterns = pl;
while (!getline_fn(&line, stdin)) {
path = line.buf;
if (!null_terminated && line.buf[0] == '"') {
@@ -1006,7 +1109,7 @@ static int check_rules(struct pattern_list *pl, int null_terminated) {
path = unquoted.buf;
}
if (path_in_sparse_checkout(path, the_repository->index))
if (path_in_sparse_checkout(path, repo->index))
write_name_quoted(path, stdout, line_terminator);
}
strbuf_release(&line);
@@ -1016,7 +1119,7 @@ static int check_rules(struct pattern_list *pl, int null_terminated) {
}
static int sparse_checkout_check_rules(int argc, const char **argv, const char *prefix,
struct repository *repo UNUSED)
struct repository *repo)
{
static struct option builtin_sparse_checkout_check_rules_options[] = {
OPT_BOOL('z', NULL, &check_rules_opts.null_termination,
@@ -1055,7 +1158,7 @@ static int sparse_checkout_check_rules(int argc, const char **argv, const char *
free(sparse_filename);
}
ret = check_rules(&pl, check_rules_opts.null_termination);
ret = check_rules(repo, &pl, check_rules_opts.null_termination);
clear_pattern_list(&pl);
free(check_rules_opts.rules_file);
return ret;
@@ -1073,6 +1176,7 @@ int cmd_sparse_checkout(int argc,
OPT_SUBCOMMAND("set", &fn, sparse_checkout_set),
OPT_SUBCOMMAND("add", &fn, sparse_checkout_add),
OPT_SUBCOMMAND("reapply", &fn, sparse_checkout_reapply),
OPT_SUBCOMMAND("clean", &fn, sparse_checkout_clean),
OPT_SUBCOMMAND("disable", &fn, sparse_checkout_disable),
OPT_SUBCOMMAND("check-rules", &fn, sparse_checkout_check_rules),
OPT_END(),
@@ -1084,8 +1188,8 @@ int cmd_sparse_checkout(int argc,
repo_config(the_repository, git_default_config, NULL);
prepare_repo_settings(the_repository);
the_repository->settings.command_requires_full_index = 0;
prepare_repo_settings(repo);
repo->settings.command_requires_full_index = 0;
return fn(argc, argv, prefix, repo);
}

28
dir.c
View File

@@ -30,6 +30,7 @@
#include "read-cache-ll.h"
#include "setup.h"
#include "sparse-index.h"
#include "strbuf.h"
#include "submodule-config.h"
#include "symlinks.h"
#include "trace2.h"
@@ -87,6 +88,33 @@ struct dirent *readdir_skip_dot_and_dotdot(DIR *dirp)
return e;
}
int for_each_file_in_dir(struct strbuf *path, file_iterator fn, const void *data)
{
struct dirent *e;
int res = 0;
size_t baselen = path->len;
DIR *dir = opendir(path->buf);
if (!dir)
return 0;
while (!res && (e = readdir_skip_dot_and_dotdot(dir)) != NULL) {
unsigned char dtype = get_dtype(e, path, 0);
strbuf_setlen(path, baselen);
strbuf_addstr(path, e->d_name);
if (dtype == DT_REG) {
res = fn(path->buf, data);
} else if (dtype == DT_DIR) {
strbuf_addch(path, '/');
res = for_each_file_in_dir(path, fn, data);
}
}
closedir(dir);
return res;
}
int count_slashes(const char *s)
{
int cnt = 0;

14
dir.h
View File

@@ -536,6 +536,20 @@ int get_sparse_checkout_patterns(struct pattern_list *pl);
*/
int remove_dir_recursively(struct strbuf *path, int flag);
/*
* This function pointer type is called on each file discovered in
* for_each_file_in_dir. The iteration stops if this method returns
* non-zero.
*/
typedef int (*file_iterator)(const char *path, const void *data);
struct strbuf;
/*
* Given a directory path, recursively visit each file within, including
* within subdirectories.
*/
int for_each_file_in_dir(struct strbuf *path, file_iterator fn, const void *data);
/*
* Tries to remove the path, along with leading empty directories so long as
* those empty directories are not startup_info->original_cwd. Ignores

View File

@@ -32,7 +32,9 @@ int give_advice_on_expansion = 1;
"Your working directory likely has contents that are outside of\n" \
"your sparse-checkout patterns. Use 'git sparse-checkout list' to\n" \
"see your sparse-checkout definition and compare it to your working\n" \
"directory contents. Running 'git clean' may assist in this cleanup."
"directory contents. Cleaning up any merge conflicts or staged\n" \
"changes before running 'git sparse-checkout clean' or 'git\n" \
"sparse-checkout reapply' may assist in this cleanup."
struct modify_index_context {
struct index_state *write;

View File

@@ -1050,5 +1050,180 @@ test_expect_success 'check-rules null termination' '
test_cmp expect actual
'
test_expect_success 'clean' '
git -C repo sparse-checkout set --cone deep/deeper1 &&
git -C repo sparse-checkout reapply &&
mkdir -p repo/deep/deeper2 repo/folder1/extra/inside &&
# Add untracked files
touch repo/deep/deeper2/file &&
touch repo/folder1/extra/inside/file &&
test_must_fail git -C repo sparse-checkout clean 2>err &&
grep "refusing to clean" err &&
git -C repo config clean.requireForce true &&
test_must_fail git -C repo sparse-checkout clean 2>err &&
grep "refusing to clean" err &&
cat >expect <<-\EOF &&
Would remove deep/deeper2/
Would remove folder1/
EOF
git -C repo sparse-checkout clean --dry-run >out &&
test_cmp expect out &&
test_path_exists repo/deep/deeper2 &&
test_path_exists repo/folder1/extra/inside/file &&
cat >expect <<-\EOF &&
Would remove deep/deeper2/file
Would remove folder1/extra/inside/file
EOF
git -C repo sparse-checkout clean --dry-run --verbose >out &&
test_cmp expect out &&
cat >expect <<-\EOF &&
Removing deep/deeper2/
Removing folder1/
EOF
git -C repo sparse-checkout clean -f >out &&
test_cmp expect out &&
test_path_is_missing repo/deep/deeper2 &&
test_path_is_missing repo/folder1
'
test_expect_success 'clean with sparse file states' '
test_when_finished git reset --hard &&
git -C repo sparse-checkout set --cone deep/deeper1 &&
mkdir repo/folder2 &&
# The previous test case checked the -f option, so
# test the config option in this one.
git -C repo config clean.requireForce false &&
# create an untracked file and a modified file
touch repo/folder2/file &&
echo dirty >repo/folder2/a &&
# First clean/reapply pass will do nothing.
git -C repo sparse-checkout clean >out &&
test_must_be_empty out &&
test_path_exists repo/folder2/a &&
test_path_exists repo/folder2/file &&
git -C repo sparse-checkout reapply 2>err &&
test_grep folder2 err &&
test_path_exists repo/folder2/a &&
test_path_exists repo/folder2/file &&
# Now, stage the change to the tracked file.
git -C repo add --sparse folder2/a &&
# Clean will continue not doing anything.
git -C repo sparse-checkout clean >out &&
test_line_count = 0 out &&
test_path_exists repo/folder2/a &&
test_path_exists repo/folder2/file &&
# But we can reapply to remove the staged change.
git -C repo sparse-checkout reapply 2>err &&
test_grep folder2 err &&
test_path_is_missing repo/folder2/a &&
test_path_exists repo/folder2/file &&
# We can clean now.
cat >expect <<-\EOF &&
Removing folder2/
EOF
git -C repo sparse-checkout clean >out &&
test_cmp expect out &&
test_path_is_missing repo/folder2 &&
# At the moment, the file is staged.
cat >expect <<-\EOF &&
M folder2/a
EOF
git -C repo status -s >out &&
test_cmp expect out &&
# Reapply persists the modified state.
git -C repo sparse-checkout reapply &&
cat >expect <<-\EOF &&
M folder2/a
EOF
git -C repo status -s >out &&
test_cmp expect out &&
# Committing the change leads to resolved status.
git -C repo commit -m "modified" &&
git -C repo status -s >out &&
test_must_be_empty out &&
# Repeat, but this time commit before reapplying.
mkdir repo/folder2/ &&
echo dirtier >repo/folder2/a &&
git -C repo add --sparse folder2/a &&
git -C repo sparse-checkout clean >out &&
test_must_be_empty out &&
test_path_exists repo/folder2/a &&
# Committing without reapplying makes it look like a deletion
# due to no skip-worktree bit.
git -C repo commit -m "dirtier" &&
git -C repo status -s >out &&
test_must_be_empty out &&
git -C repo sparse-checkout reapply &&
git -C repo status -s >out &&
test_must_be_empty out
'
test_expect_success 'sparse-checkout operations with merge conflicts' '
git clone repo merge &&
(
cd merge &&
mkdir -p folder1/even/more/dirs &&
echo base >folder1/even/more/dirs/file &&
git add folder1 &&
git commit -m "base" &&
git checkout -b right&&
echo right >folder1/even/more/dirs/file &&
git commit -a -m "right" &&
git checkout -b left HEAD~1 &&
echo left >folder1/even/more/dirs/file &&
git commit -a -m "left" &&
git checkout -b merge &&
git sparse-checkout set deep/deeper1 &&
test_must_fail git merge -m "will-conflict" right &&
test_must_fail git sparse-checkout clean -f 2>err &&
grep "failed to convert index to a sparse index" err &&
echo merged >folder1/even/more/dirs/file &&
git add --sparse folder1 &&
git merge --continue &&
test_path_exists folder1/even/more/dirs/file &&
# clean does not remove the file, because the
# SKIP_WORKTREE bit was not cleared by the merge command.
git sparse-checkout clean -f >out &&
test_line_count = 0 out &&
test_path_exists folder1/even/more/dirs/file &&
git sparse-checkout reapply &&
test_path_is_missing folder1
)
'
test_done