Subject: [PATCH 10/10] clean: support cleaning sparse checkout with -S

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Mon, 15 Nov 2010 17:36:50 +0700

Those files that are managed by git, but out of working directory due
to sparse-checkout file are subjected to be cleaned by "-S". Files
that match the index exactly will be cleaned without "-f". Otherwise
'-f' is needed.

Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
 Documentation/git-clean.txt |    6 ++-
 builtin/clean.c             |   70 ++++++++++++++++++++++++++++++++
 t/t7301-clean-sparse.sh     |   92 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 167 insertions(+), 1 deletions(-)
 create mode 100755 t/t7301-clean-sparse.sh

diff --git a/Documentation/git-clean.txt b/Documentation/git-clean.txt
index 60e38e6..e0c95b1 100644
--- a/Documentation/git-clean.txt
+++ b/Documentation/git-clean.txt
@@ -8,7 +8,7 @@ git-clean - Remove untracked files from the working tree
 SYNOPSIS
 --------
 [verse]
-'git clean' [-d] [-f] [-n] [-q] [-e <pattern>] [-x | -X] [--] <path>...
+'git clean' [-d] [-f] [-n] [-q] [-e <pattern>] [-x | -X | -S] [--] <path>...
 
 DESCRIPTION
 -----------
@@ -61,6 +61,10 @@ OPTIONS
 	Remove only files ignored by git.  This may be useful to rebuild
 	everything from scratch, but keep manually created files.
 
+-S::
+	Remove files tracked by git but are outside of sparse checkout.
+	Files that match the index exactly will be removed even when
+	'-f' is not given and clean.requireForce is no.
 
 Author
 ------
diff --git a/builtin/clean.c b/builtin/clean.c
index c8798f5..5827993 100644
--- a/builtin/clean.c
+++ b/builtin/clean.c
@@ -34,11 +34,71 @@ static int exclude_cb(const struct option *opt, const char *arg, int unset)
 	return 0;
 }
 
+static int clean_sparse_checkout(const char *prefix, const char **pathspec,
+				 int show_only, int quiet, int force)
+{
+	struct stat st;
+	int i, errors = 0;
+	unsigned char sha1[20];
+	const char *qname;
+	struct strbuf buf = STRBUF_INIT;
+
+	if (read_cache() < 0)
+		die("index file corrupt");
+	for (i = 0; i < the_index.cache_nr; i++) {
+		struct cache_entry *ce = the_index.cache[i];
+
+		if (!ce_skip_worktree(ce))
+			continue;
+		if (pathspec && !match_pathspec(pathspec, ce->name, ce_namelen(ce), 0, NULL))
+			continue;
+
+		if (stat(ce->name, &st) < 0)
+			continue;
+		qname = quote_path_relative(ce->name, ce_namelen(ce), &buf, prefix);
+		if (index_path(sha1, ce->name, &st, 0) < 0) {
+			warning("failed to hash %s", qname);
+			errors++;
+			continue;
+		}
+		if (!hashcmp(sha1, ce->sha1)) {
+			if (show_only) {
+				printf("Would remove %s\n", qname);
+				continue;
+			}
+			if (!quiet)
+				printf("Removing %s\n", qname);
+			if (unlink(ce->name) != 0) {
+				warning("failed to remove %s", qname);
+				errors++;
+			}
+			continue;
+		}
+		if (force) {
+			if (show_only) {
+				printf("Would remove %s\n", qname);
+				continue;
+			}
+			if (!quiet)
+				printf("Removing %s\n", qname);
+			if (unlink(ce->name) != 0) {
+				warning("failed to remove %s", qname);
+				errors++;
+			}
+			continue;
+		}
+		if (show_only)
+			printf("Would not remove %s\n", qname);
+	}
+	return errors != 0;
+}
+
 int cmd_clean(int argc, const char **argv, const char *prefix)
 {
 	int i;
 	int show_only = 0, remove_directories = 0, quiet = 0, ignored = 0;
 	int ignored_only = 0, baselen = 0, config_set = 0, errors = 0;
+	int sparse = 0;
 	int rm_flags = REMOVE_DIR_KEEP_NESTED_GIT;
 	struct strbuf directory = STRBUF_INIT;
 	struct dir_struct dir;
@@ -55,6 +115,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
 				"remove whole directories"),
 		{ OPTION_CALLBACK, 'e', "exclude", &exclude_list, "pattern",
 		  "exclude <pattern>", PARSE_OPT_NONEG, exclude_cb },
+		OPT_BOOLEAN('S', NULL, &sparse, "remove tracked files outside sparse checkout"),
 		OPT_BOOLEAN('x', NULL, &ignored, "remove ignored files, too"),
 		OPT_BOOLEAN('X', NULL, &ignored_only,
 				"remove only ignored files"),
@@ -70,6 +131,15 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options, builtin_clean_usage,
 			     0);
 
+	if (sparse) {
+		if (ignored || ignored_only)
+			die("-S, -x and -X cannot be used together");
+		if (exclude_list.nr)
+			die("-S and -e cannot be used together (yet)");
+		pathspec = get_pathspec(prefix, argv);
+		return clean_sparse_checkout(prefix, pathspec, show_only, quiet, force);
+	}
+
 	memset(&dir, 0, sizeof(dir));
 	if (ignored_only)
 		dir.flags |= DIR_SHOW_IGNORED;
diff --git a/t/t7301-clean-sparse.sh b/t/t7301-clean-sparse.sh
new file mode 100755
index 0000000..3ac0b0a
--- /dev/null
+++ b/t/t7301-clean-sparse.sh
@@ -0,0 +1,92 @@
+#!/bin/sh
+
+test_description='git clean -S basic tests'
+
+. ./test-lib.sh
+
+git config clean.requireForce yes
+
+test_expect_success 'setup' '
+	mkdir src &&
+	touch file1 file2 &&
+	touch src/file1 src/file2 &&
+	git add . &&
+	git update-index --skip-worktree file1 src/file1
+'
+
+test_expect_success 'clean -x -S does not work' '
+	test_must_fail git clean -x -S &&
+	test_must_fail git clean -X -S
+'
+
+test_expect_success 'clean -n -S' '
+	cat >expected <<\EOF
+Would remove file1
+Would remove src/file1
+EOF
+	git clean -n -S >result &&
+	test_cmp expected result
+'
+
+test_expect_success 'clean -n -S src' '
+	cat >expected <<\EOF
+Would remove src/file1
+EOF
+	git clean -n -S src >result &&
+	test_cmp expected result
+'
+
+test_expect_success '[src] clean -n -S .' '
+	(
+	cd src
+	cat >expected <<\EOF
+Would remove file1
+EOF
+	git clean -n -S . >result &&
+	test_cmp expected result
+	)
+'
+
+test_expect_success '[src] clean -n -S ../file1' '
+	(
+	cd src
+	cat >expected <<\EOF
+Would remove ../file1
+EOF
+	git clean -n -S ../file1 >result &&
+	test_cmp expected result
+	)
+'
+
+test_expect_success 'clean -n -S with dirty worktree' '
+	echo dirty >file1 &&
+	cat >expected <<\EOF
+Would not remove file1
+Would remove src/file1
+EOF
+	git clean -n -S >result &&
+	test_cmp expected result
+'
+
+test_expect_success 'clean -f -n -S with dirty worktree' '
+	echo dirty >file1 &&
+	cat >expected <<\EOF
+Would remove file1
+Would remove src/file1
+EOF
+	git clean -f -n -S >result &&
+	test_cmp expected result
+'
+
+test_expect_success 'clean -S with dirty worktree' '
+	git clean -S &&
+	grep dirty file1 &&
+	test ! -f src/file1
+'
+
+test_expect_success 'clean -f -S with dirty worktree' '
+	git clean -f -S &&
+	test ! -f file1
+'
+
+test_done
-- 
1.7.3.2.210.g045198