Subject: [PATCH 7/8] Introduce default sparse patterns (core.defaultsparse)

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Sun, 30 Nov 2008 17:54:37 +0700

As noted in the previous commit message, new files will always get added into
working directory by unpack_trees(). This can be annoying so we keep
track of the sparse patterns people use to form their checkout area
and apply the patterns on new files. Those files that get filtered out
by this will be reported if verbose_update is TRUE.

Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
 Documentation/config.txt       |   10 +++++++
 Documentation/git-checkout.txt |   21 ++++++++++++++-
 Documentation/git-clone.txt    |    1 +
 builtin-checkout.c             |   54 ++++++++++++++++++++++++++++++++++++++++
 builtin-clone.c                |    2 +
 cache.h                        |    2 +
 config.c                       |    5 +++
 environment.c                  |    1 +
 t/t2011-checkout-sparse.sh     |   19 ++++++++++++++
 t/t5703-clone-narrow.sh        |    1 +
 unpack-trees.c                 |   52 ++++++++++++++++++++++++++++++++++++++
 unpack-trees.h                 |    1 +
 12 files changed, 167 insertions(+), 2 deletions(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index b233fe5..2b2dad1 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -422,6 +422,16 @@ relatively high IO latencies.  With this set to 'true', git will do the
 index comparison to the filesystem data in parallel, allowing
 overlapping IO's.
 
+core.defaultsparse::
+	The default sparse patterns that will be used to decide
+	whether a new file should be added into working
+	directory. If a new file matches the default sparse
+	patterns, then it will be checked out. Otherwise it will
+	be ignored and marked "no-checkout" in index.
+	Sparse update operations via "git clone" or "git checkout" will
+	automatically update this config. See linkgit:git-clone[1] and
+	linkgit:git-checkout[1] for more information.
+
 alias.*::
 	Command aliases for the linkgit:git[1] command wrapper - e.g.
 	after defining "alias.last = cat-file commit HEAD", the invocation
diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index b32043f..892a4bb 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -125,6 +125,7 @@ the conflicted merge in the specified paths.
 
 --full::
 	Quit sparse checkout mode. Return to full checkout.
+	`core.defaultsparse` will be removed.
 	This option cannot be used with either --sparse,
 	--include-sparse, --exclude-sparse or <paths>.
 
@@ -133,6 +134,7 @@ the conflicted merge in the specified paths.
 	Re-apply new sparse patterns on current working directory to
 	form new checkout area. All no-checkout bits will be wiped
 	out before applying the patterns.
+	`core.defaultsparse` will be set with the given patterns.
 	This option cannot be used with --full, --include-sparse,
 	--exclude-sparse or <paths>. Multiple --sparse is not allowed.
 
@@ -140,6 +142,11 @@ the conflicted merge in the specified paths.
 --include-sparse=<sparse patterns>::
 	Checkout more areas specified by sparse patterns to current
 	checkout area. Already checked out entries are not affected.
+	`core.defaultsparse` will be appended with the given patterns.
+	Repetitive use of --include-sparse and --exclude-sparse may
+	leave cruft in `core.defaultsparse`, and may slow down
+	checkout operations. You may want to clean up
+	`core.defaultsparse` with --sparse.
 	This option cannot be used with --full, --sparse,
 	--exclude-sparse or <paths>. Multiple --include-sparse is not allowed.
 
@@ -149,6 +156,11 @@ the conflicted merge in the specified paths.
 	from current checkout area. This operation will fail if there
 	are unmerged or modified files in the removing areas. No-checkout
 	entries are not affected.
+	`core.defaultsparse` will be appended with the negated version of the given patterns.
+	Repetitive use of --include-sparse and --exclude-sparse may
+	leave cruft in `core.defaultsparse`, and may slow down
+	checkout operations. You may want to clean up
+	`core.defaultsparse` with --sparse.
 	This option cannot be used with --full, --sparse,
 	--include-sparse or <paths>. Multiple --exclude-sparse is not allowed.
 
@@ -230,8 +242,11 @@ In sparse checkout mode, checkout status of every files in your
 working directory will be recorded in index. If a file is marked
 "no-checkout", it means that file is not needed to be present in
 working directory by user or any git command. When a new file is added
-to index, it will be marked "checkout" unless sparse patterns are
-applied.  Unmerged files are always "checkout". When you checkout new
+to index, it will be checked with default sparse pattern (see
+`core.defaultsparse` in linkgit:git-config.txt[1]). If the file
+matches the pattern, it will be marked "checkout" and added to
+working directory. Otherwise it will be marked "no-checkout" in
+index.  Unmerged files are always "checkout". When you checkout new
 files using "git checkout <file>" they will be automatically marked
 "checkout". Other commands such as "git apply" can also checkout new
 files if they are needed. linkgit:git-update-index[1] can be used to
@@ -311,6 +326,8 @@ the sparse patterns. Modified entries will refuse to become "no-checkout".
 
 You can form your checkout area in one go with --sparse option,
 or do it incrementally with --include-sparse and --exclude-sparse.
+Patterns used in those options will be automatically recorded in
+`core.defaultsparse`.
 
 EXAMPLES
 --------
diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt
index acdced2..f7ccf15 100644
--- a/Documentation/git-clone.txt
+++ b/Documentation/git-clone.txt
@@ -103,6 +103,7 @@ then the cloned repository will become corrupt.
 -S=<sparse patterns>::
 --sparse-checkout=<sparse patterns>::
 	Make a sparse checkout instead of a full one.
+	`core.defaultsparse` will be set with the given sparse patterns.
 	This option will not work with either --no-checkout or --bare.
 	Please refer to linkgit:git-checkout[1] for more detail on
 	sparse checkout and sparse patterns.
diff --git a/builtin-checkout.c b/builtin-checkout.c
index be4cd3a..8c6d5c5 100644
--- a/builtin-checkout.c
+++ b/builtin-checkout.c
@@ -592,6 +592,60 @@ static int switch_branches(struct checkout_opts *opts, struct branch_info *new)
 
 	update_refs_for_switch(opts, &old, new);
 
+	/* Update core.defaultsparse */
+	if (opts->all_path ||
+	    (opts->new_path && !opts->prefix)) /* optimization when prefix is NULL */
+		git_config_set("core.defaultsparse", opts->new_path);
+	else if (opts->new_path || opts->add_path || opts->remove_path) {
+		const char *narrow_spec;
+		struct narrow_spec *spec;
+		struct strbuf sb;
+		int i;
+
+		if (opts->new_path)
+			narrow_spec = opts->new_path;
+		else if (opts->add_path)
+			narrow_spec = opts->add_path;
+		else
+			narrow_spec = opts->remove_path;
+		spec = parse_narrow_spec(narrow_spec, NULL) ;
+		strbuf_init(&sb, 0);
+
+		/*
+		 * --sparse does not save old core.defaultsparse
+		 * --include-sparse and --exclude-sparse on the other hand
+		 * append to the old core.defaultsparse
+		 */
+		if (core_default_sparse && !opts->new_path) {
+			strbuf_addstr(&sb, core_default_sparse);
+			strbuf_addch(&sb, ':');
+		}
+
+		/*
+		 * This happens when we do full checkout,
+		 * then --exclude-sparse.
+		 * Add full pattern first so the next
+		 *  pattern has something to negate
+		 */
+		if (!core_default_sparse && opts->remove_path)
+			strbuf_addstr(&sb, "/:");
+
+		for (i = 0; i < spec->nr; i++) {
+			struct narrow_pattern *p = spec->patterns[i];
+
+			if (i)
+				strbuf_addch(&sb, ':');
+			if (!p->prefix && opts->prefix)
+				p->prefix = xstrdup(opts->prefix);
+			if (opts->remove_path)
+				p->negative = !p->negative;
+			narrow_spec_to_strbuf(p, &sb);
+		}
+		git_config_set("core.defaultsparse", sb.buf);
+		free_narrow_spec(spec);
+		strbuf_release(&sb);
+	}
+
 	ret = post_checkout_hook(old.commit, new->commit, 1);
 	return ret || opts->writeout_error;
 }
diff --git a/builtin-clone.c b/builtin-clone.c
index ea341a1..209030a 100644
--- a/builtin-clone.c
+++ b/builtin-clone.c
@@ -619,6 +619,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 		if (write_cache(fd, active_cache, active_nr) ||
 		    commit_locked_index(lock_file))
 			die("unable to write new index file");
+
+		git_config_set("core.defaultsparse", option_narrow_path);
 	}
 
 	strbuf_release(&reflog_msg);
diff --git a/cache.h b/cache.h
index b13df06..a3366da 100644
--- a/cache.h
+++ b/cache.h
@@ -175,6 +175,7 @@ struct cache_entry {
 
 /* Only remove in work directory, not index */
 #define CE_WT_REMOVE (0x400000)
+#define CE_NEW       (0x800000)
 
 /*
  * Extended on-disk flags
@@ -522,6 +523,7 @@ extern size_t delta_base_cache_limit;
 extern int auto_crlf;
 extern int fsync_object_files;
 extern int core_preload_index;
+extern const char *core_default_sparse;
 
 enum safe_crlf {
 	SAFE_CRLF_FALSE = 0,
diff --git a/config.c b/config.c
index d2fc8f5..ca2cf03 100644
--- a/config.c
+++ b/config.c
@@ -495,6 +495,11 @@ static int git_default_core_config(const char *var, const char *value)
 		return 0;
 	}
 
+	if (!strcmp(var, "core.defaultsparse")) {
+		core_default_sparse = xstrdup(value);
+		return 0;
+	}
+
 	/* Add other config variables here and to Documentation/config.txt. */
 	return 0;
 }
diff --git a/environment.c b/environment.c
index e278bce..5a66e30 100644
--- a/environment.c
+++ b/environment.c
@@ -30,6 +30,7 @@ int zlib_compression_level = Z_BEST_SPEED;
 int core_compression_level;
 int core_compression_seen;
 int fsync_object_files;
+const char *core_default_sparse;
 size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE;
 size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT;
 size_t delta_base_cache_limit = 16 * 1024 * 1024;
diff --git a/t/t2011-checkout-sparse.sh b/t/t2011-checkout-sparse.sh
index 67aea96..15f3761 100755
--- a/t/t2011-checkout-sparse.sh
+++ b/t/t2011-checkout-sparse.sh
@@ -45,6 +45,7 @@ test_expect_success 'limit worktree to work1 and work2' '
 
 test_expect_success 'exit sparse checkout' '
 	git checkout --full &&
+	test -z "$(git config core.defaultsparse)" &&
 	test -f work1/one &&
 	test -f work2/two &&
 	test -f work3/three &&
@@ -125,4 +126,22 @@ test_expect_success 'removal outside checkout area' '
 	git checkout --sparse=work2/ HEAD^
 '
 
+test_expect_success 'core.defaultsparse, new files get filtered' '
+	git checkout --sparse=work2/ $WORK2 &&
+	git config core.defaultsparse "./*:work2/"
+	git checkout $WORK3 &&
+	test -f three &&
+	! test -f work3/three &&
+	git config --unset core.defaultsparse
+'
+
+test_expect_success 'save core.defaultsparse' '
+	git checkout --sparse="./*:work2/" $WORK2 &&
+	test "$(git config core.defaultsparse)" = "./*:work2/" &&
+	cd work3 && git checkout --include-sparse=1 $WORK2 && cd .. &&
+	test "$(git config core.defaultsparse)" = "./*:work2/:work3//1" &&
+	git checkout --exclude-sparse=work3/ $WORK2 &&
+	test "$(git config core.defaultsparse)" = "./*:work2/:work3//1:!work3/"
+'
+
 test_done
diff --git a/t/t5703-clone-narrow.sh b/t/t5703-clone-narrow.sh
index e34246d..691bb40 100755
--- a/t/t5703-clone-narrow.sh
+++ b/t/t5703-clone-narrow.sh
@@ -34,6 +34,7 @@ test_expect_success 'clone with --sparse-checkout' '
 	rm -fr dst &&
 	git clone --sparse-checkout=work/ src dst &&
 	cd dst &&
+	test "$(git config core.defaultsparse)" = "work/" &&
 	test -z "$(git ls-files --sparse | grep -v ^work/)"
 	)
 '
diff --git a/unpack-trees.c b/unpack-trees.c
index a2794b8..ad26987 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -151,6 +151,7 @@ static int verify_absent_sparse(struct cache_entry *ce, const char *action, stru
 static int apply_narrow_spec(struct unpack_trees_options *o)
 {
 	struct index_state *index = &o->result;
+	static struct narrow_spec *default_spec = NULL;
 	int i, len, namelen;
 	int header = 0;
 	char *special_files[] = { ".gitignore", GITATTRIBUTES_FILE, NULL };
@@ -188,6 +189,25 @@ static int apply_narrow_spec(struct unpack_trees_options *o)
 			if (o->remove_narrow_path && match)
 				ce_mark_no_checkout(ce);
 		}
+		/*
+		 * Default spec only applies if no directive is given
+		 * and it only applies to new files
+		 */
+		else if (ce->ce_flags & CE_NEW) {
+			if (!default_spec && core_default_sparse)
+				default_spec = parse_narrow_spec(core_default_sparse, NULL);
+
+			if (default_spec && !match_narrow_spec(default_spec, ce->name)) {
+				if (o->update && o->verbose_update) {
+					if (!header) {
+						fprintf(stderr, "New files get filtered out by default sparse patterns:\n");
+						header = 1;
+					}
+					fprintf(stderr, " %s\n", ce->name);
+				}
+				ce_mark_no_checkout(ce);
+			}
+		}
 
 		/* Update worktree, add/remove entries if needed */
 
@@ -805,6 +825,8 @@ static int merged_entry(struct cache_entry *merge, struct cache_entry *old,
 	else {
 		if (verify_absent(merge, "overwritten", o))
 			return -1;
+		if (!o->has_unmerged)
+			update |= CE_NEW;
 		invalidate_ce_path(merge, o);
 	}
 
@@ -957,6 +979,36 @@ void free_narrow_spec(struct narrow_spec *spec)
 	free(spec);
 }
 
+void narrow_spec_to_strbuf(struct narrow_pattern *p, struct strbuf *sb)
+{
+	const char *s;
+	if (p->prefix && !p->has_root) {
+		s = p->prefix;
+		while (*s) {
+			if (*s == ':')
+				strbuf_addch(sb, '\\');
+			strbuf_addch(sb, *s);
+			s++;
+		}
+		strbuf_addch(sb, '/'); /* double slashes to end prefix */
+	}
+	if (p->negative)
+		strbuf_addch(sb, '!');
+	if (p->has_root)
+		strbuf_addch(sb, '/');
+	else if (p->has_slashes && strchr(p->pattern, '/') == NULL) {
+		strbuf_addch(sb, '.');
+		strbuf_addch(sb, '/');
+	}
+	s = p->pattern;
+	while (*s) {
+		if (*s == ':')
+			strbuf_addch(sb, '\\');
+		strbuf_addch(sb, *s);
+		s++;
+	}
+}
+
 int match_narrow_spec(struct narrow_spec *spec, const char *path)
 {
 	int i, match;
diff --git a/unpack-trees.h b/unpack-trees.h
index 7e9febd..c5b19e7 100644
--- a/unpack-trees.h
+++ b/unpack-trees.h
@@ -75,6 +75,7 @@ extern int unpack_trees(unsigned n, struct tree_desc *t,
 
 struct narrow_spec *parse_narrow_spec(const char *spec, const char *prefix);
 void free_narrow_spec(struct narrow_spec *spec);
+void narrow_spec_to_strbuf(struct narrow_pattern *p, struct strbuf *sb);
 int match_narrow_spec(struct narrow_spec *spec, const char *path);
 int threeway_merge(struct cache_entry **stages, struct unpack_trees_options *o);
 int twoway_merge(struct cache_entry **src, struct unpack_trees_options *o);
-- 
1.6.0.3.890.g95457