threads / patch / 55302

v3, 7 partst3705: add tests for `git add` in sparse checkouts

Subject: [PATCH v3 2/7] t3705: add tests for `git add` in sparse checkouts

## tl;dr

32 messages between Mar 12, 2021 and Apr 16, 2021. Diffs are folded; open one to read it.

replies: 31people: 5as markdown or json

Matheus Tavares· Mar 12, 2021, 22:47 UTC · lore

[PATCH v3 0/7] add/rm: honor sparse checkout and warn on sparse paths

Make `rm` honor sparse checkouts, and make both `rm` and `add` warn when they are asked to update sparse entries.

Main changes since v2:
Patch 2:
- Removed unnecessary `rm` in t3705's auxiliary function and added
  comment about	why `update-index --force-remove` is used there.
Patch 4:
- Replaced the boolean function parameter with a more descriptive enum.
  This way, readers don't have to go to the function definition to
  understand what the caller wants.
Patches 5 and 6:
- Reworded both commit messages and the advice.updateSparsePath docs to
  avoid saying "pathspecs that *only* match sparse entries", as the
  situations to which they refer might also include matches among
  ignored paths.
- Rephrased the warning message about sparse paths so that it doesn't
  contradict the message on ignored paths when they are displayed
  together.
Patch 7:
- Adjusted the tests to check for the new sparse warning message.
Matheus Tavares (7):
  add: include magic part of pathspec on --refresh error
  t3705: add tests for `git add` in sparse checkouts
  add: make --chmod and --renormalize honor sparse checkouts
  pathspec: allow to ignore SKIP_WORKTREE entries on index matching
  refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag
  add: warn when asked to update SKIP_WORKTREE entries
  rm: honor sparse checkout patterns
 Documentation/config/advice.txt  |   4 +
 Documentation/git-rm.txt         |   4 +-
 advice.c                         |  20 ++++
 advice.h                         |   4 +
 builtin/add.c                    |  77 ++++++++++++---
 builtin/check-ignore.c           |   3 +-
 builtin/rm.c                     |  35 ++++---
 cache.h                          |  15 +--
 pathspec.c                       |  25 ++++-
 pathspec.h                       |  18 +++-
 read-cache.c                     |   5 +-
 t/t3602-rm-sparse-checkout.sh    |  78 ++++++++++++++++
 t/t3700-add.sh                   |   6 ++
 t/t3705-add-sparse-checkout.sh   | 155 +++++++++++++++++++++++++++++++
 t/t7011-skip-worktree-reading.sh |   5 -
 t/t7012-skip-worktree-writing.sh |  19 ----
 16 files changed, 409 insertions(+), 64 deletions(-)
 create mode 100755 t/t3602-rm-sparse-checkout.sh
 create mode 100755 t/t3705-add-sparse-checkout.sh
Range-diff against v2:
1:  46147a8637 = 1:  05b66a8c2b add: include magic part of pathspec on --refresh error
2:  2de55420fa ! 2:  d93c3f5146 t3705: add tests for `git add` in sparse checkouts
    @@ t/t3705-add-sparse-checkout.sh (new)
     +
     +# Optionally take a printf format string to write to the sparse_entry file
     +setup_sparse_entry () {
    -+	rm -f sparse_entry &&
    ++	# 'sparse_entry' might already be in the index with the skip-worktree
    ++	# bit set. Remove it so that the subsequent git add can update it.
     +	git update-index --force-remove sparse_entry &&
    -+
     +	if test $# -eq 1
     +	then
     +		printf "$1" >sparse_entry
3:  604480ade9 = 3:  c90d5e3c23 add: make --chmod and --renormalize honor sparse checkouts
4:  1494e65316 ! 4:  69d51579b3 pathspec: allow to ignore SKIP_WORKTREE entries on index matching
    @@ Metadata
      ## Commit message ##
         pathspec: allow to ignore SKIP_WORKTREE entries on index matching
     
    -    Add the 'ignore_skip_worktree' boolean parameter to both
    -    add_pathspec_matches_against_index() and
    -    find_pathspecs_matching_against_index(). When true, these functions will
    -    not try to match the given pathspec with SKIP_WORKTREE entries. This
    -    will be used in a future patch to make `git add` display a hint
    -    when the pathspec matches only sparse paths.
    +    Add a new enum parameter to `add_pathspec_matches_against_index()` and
    +    `find_pathspecs_matching_against_index()`, allowing callers to specify
    +    whether these function should attempt to match SKIP_WORKTREE entries or
    +    not. This will be used in a future patch to make `git add` display a
    +    hint when it is asked to update SKIP_WORKTREE entries.
     
         Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
     
    @@ builtin/add.c: static char *prune_directory(struct dir_struct *dir, struct paths
      	}
      	dir->nr = dst - dir->entries;
     -	add_pathspec_matches_against_index(pathspec, &the_index, seen);
    -+	add_pathspec_matches_against_index(pathspec, &the_index, seen, 0);
    ++	add_pathspec_matches_against_index(pathspec, &the_index, seen,
    ++					   PS_HEED_SKIP_WORKTREE);
      	return seen;
      }
      
    @@ builtin/add.c: int cmd_add(int argc, const char **argv, const char *prefix)
      
      		if (!seen)
     -			seen = find_pathspecs_matching_against_index(&pathspec, &the_index);
    -+			seen = find_pathspecs_matching_against_index(&pathspec, &the_index, 0);
    ++			seen = find_pathspecs_matching_against_index(&pathspec,
    ++					&the_index, PS_HEED_SKIP_WORKTREE);
      
      		/*
      		 * file_exists() assumes exact match
    @@ builtin/check-ignore.c: static int check_ignore(struct dir_struct *dir,
      	 * 'git status', 'git add' etc.
      	 */
     -	seen = find_pathspecs_matching_against_index(&pathspec, &the_index);
    -+	seen = find_pathspecs_matching_against_index(&pathspec, &the_index, 0);
    ++	seen = find_pathspecs_matching_against_index(&pathspec, &the_index,
    ++						     PS_HEED_SKIP_WORKTREE);
      	for (i = 0; i < pathspec.nr; i++) {
      		full_path = pathspec.items[i].match;
      		pattern = NULL;
    @@ pathspec.c
      void add_pathspec_matches_against_index(const struct pathspec *pathspec,
      					const struct index_state *istate,
     -					char *seen)
    -+					char *seen, int ignore_skip_worktree)
    ++					char *seen,
    ++					enum ps_skip_worktree_action sw_action)
      {
      	int num_unmatched = 0, i;
      
    @@ pathspec.c: void add_pathspec_matches_against_index(const struct pathspec *paths
      		return;
      	for (i = 0; i < istate->cache_nr; i++) {
      		const struct cache_entry *ce = istate->cache[i];
    -+		if (ignore_skip_worktree && ce_skip_worktree(ce))
    ++		if (sw_action == PS_IGNORE_SKIP_WORKTREE && ce_skip_worktree(ce))
     +			continue;
      		ce_path_match(istate, ce, pathspec, seen);
      	}
    @@ pathspec.c: void add_pathspec_matches_against_index(const struct pathspec *paths
      char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
     -					    const struct index_state *istate)
     +					    const struct index_state *istate,
    -+					    int ignore_skip_worktree)
    ++					    enum ps_skip_worktree_action sw_action)
      {
      	char *seen = xcalloc(pathspec->nr, 1);
     -	add_pathspec_matches_against_index(pathspec, istate, seen);
    -+	add_pathspec_matches_against_index(pathspec, istate, seen,
    -+					   ignore_skip_worktree);
    ++	add_pathspec_matches_against_index(pathspec, istate, seen, sw_action);
      	return seen;
      }
      
     
      ## pathspec.h ##
     @@ pathspec.h: static inline int ps_strcmp(const struct pathspec_item *item,
    + 		return strcmp(s1, s2);
    + }
      
    ++enum ps_skip_worktree_action {
    ++  PS_HEED_SKIP_WORKTREE = 0,
    ++  PS_IGNORE_SKIP_WORKTREE = 1
    ++};
      void add_pathspec_matches_against_index(const struct pathspec *pathspec,
      					const struct index_state *istate,
     -					char *seen);
    -+					char *seen, int ignore_skip_worktree);
    ++					char *seen,
    ++					enum ps_skip_worktree_action sw_action);
      char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
     -					    const struct index_state *istate);
     +					    const struct index_state *istate,
    -+					    int ignore_skip_worktree);
    ++					    enum ps_skip_worktree_action sw_action);
      int match_pathspec_attrs(const struct index_state *istate,
      			 const char *name, int namelen,
      			 const struct pathspec_item *item);
5:  584811e5c0 ! 5:  34a61a0d03 refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag
    @@ Commit message
     
         refresh_index() optionally takes a seen[] array to mark the pathspec
         items that had matches in the index. This is used by `git add --refresh`
    -    to find out if there were any pathspec without matches, and display an
    +    to find out if there was any pathspec without matches, and display an
         error accordingly.
     
         In the following patch, `git add` will also learn to warn about
    -    pathspecs that only match sparse entries (which are not updated). But
    -    for that, we will need a seen[] array marked exclusively with matches
    -    from dense entries. To avoid having to call ce_path_match() again for
    -    these entries after refresh_index() returns, add a flag that implements
    -    this restriction inside the function itself.
    +    pathspecs that match no eligible path for updating, but do match sparse
    +    entries. For that, we will need a seen[] array marked exclusively with
    +    matches from dense entries. To avoid having to call ce_path_match()
    +    again for these entries after refresh_index() returns, add a flag that
    +    implements this restriction inside the function itself.
     
         Note that refresh_index() does not update sparse entries, regardless of
         passing the flag or not. The flag only controls whether matches with
    @@ read-cache.c: int refresh_index(struct index_state *istate, unsigned int flags,
      	int in_porcelain = (flags & REFRESH_IN_PORCELAIN);
      	unsigned int options = (CE_MATCH_REFRESH |
     @@ read-cache.c: int refresh_index(struct index_state *istate, unsigned int flags,
    - 		int cache_errno = 0;
    - 		int changed = 0;
      		int filtered = 0;
    + 		int t2_did_lstat = 0;
    + 		int t2_did_scan = 0;
     +		char *cur_seen;
      
      		ce = istate->cache[i];
6:  15f399f4fa ! 6:  66d5c71182 add: warn when pathspec only matches SKIP_WORKTREE entries
    @@ Metadata
     Author: Matheus Tavares <matheus.bernardino@usp.br>
     
      ## Commit message ##
    -    add: warn when pathspec only matches SKIP_WORKTREE entries
    +    add: warn when asked to update SKIP_WORKTREE entries
     
         `git add` already refrains from updating SKIP_WORKTREE entries, but it
    -    silently exits with zero code when a pathspec only matches these
    -    entries. Instead, let's warn the user and display a hint on how to
    -    update these entries.
    +    silently exits with zero code when it is asked to do so. Instead, let's
    +    warn the user and display a hint on how to update these entries.
     
    -    Note that the warning is only shown if the pathspec matches no untracked
    -    paths in the working tree and only matches index entries with the
    -    SKIP_WORKTREE bit set. A warning message was chosen over erroring out
    -    right away to reproduce the same behavior `add` already exhibits with
    -    ignored files. This also allow users to continue their workflow without
    -    having to invoke `add` again with only the matching pathspecs, as the
    -    matched files will have already been added.
    +    Note that we only warn the user whey they give a pathspec item that
    +    matches no eligible path for updating, but it does match one or more
    +    SKIP_WORKTREE entries. A warning was chosen over erroring out right away
    +    to reproduce the same behavior `add` already exhibits with ignored
    +    files. This also allow users to continue their workflow without having
    +    to invoke `add` again with only the eligible paths (as those will have
    +    already been added).
     
         Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
     
    @@ Documentation/config/advice.txt: advice.*::
      		Advice shown if a user runs the add command without providing
      		the pathspec parameter.
     +	updateSparsePath::
    -+		Advice shown if the pathspec given to linkgit:git-add[1] only
    -+		matches index entries outside the current sparse-checkout.
    ++		Advice shown when linkgit:git-add[1] is asked to update index
    ++		entries outside the current sparse checkout.
      --
     
      ## advice.c ##
    @@ advice.c: void NORETURN die_conclude_merge(void)
     +	if (!pathspec_list->nr)
     +		return;
     +
    -+	fprintf(stderr, _("The following pathspecs only matched index entries outside the current\n"
    -+			  "sparse checkout:\n"));
    ++	fprintf(stderr, _("The following pathspecs didn't match any"
    ++			  " eligible path, but they do match index\n"
    ++			  "entries outside the current sparse checkout:\n"));
     +	for_each_string_list_item(item, pathspec_list)
     +		fprintf(stderr, "%s\n", item->string);
     +
     +	advise_if_enabled(ADVICE_UPDATE_SPARSE_PATH,
    -+			  _("Disable or modify the sparsity rules if you intend to update such entries."));
    -+
    ++			  _("Disable or modify the sparsity rules if you intend"
    ++			    " to update such entries."));
     +}
     +
      void detach_advice(const char *new_name)
    @@ advice.h: void advise_if_enabled(enum advice_type type, const char *advice, ...)
     
      ## builtin/add.c ##
     @@ builtin/add.c: static char *prune_directory(struct dir_struct *dir, struct pathspec *pathspec,
    - 			*dst++ = entry;
      	}
      	dir->nr = dst - dir->entries;
    --	add_pathspec_matches_against_index(pathspec, &the_index, seen, 0);
    -+	add_pathspec_matches_against_index(pathspec, &the_index, seen, 1);
    + 	add_pathspec_matches_against_index(pathspec, &the_index, seen,
    +-					   PS_HEED_SKIP_WORKTREE);
    ++					   PS_IGNORE_SKIP_WORKTREE);
      	return seen;
      }
      
    @@ builtin/add.c: int cmd_add(int argc, const char **argv, const char *prefix)
     +		struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
      
      		if (!seen)
    --			seen = find_pathspecs_matching_against_index(&pathspec, &the_index, 0);
    -+			seen = find_pathspecs_matching_against_index(&pathspec, &the_index, 1);
    + 			seen = find_pathspecs_matching_against_index(&pathspec,
    +-					&the_index, PS_HEED_SKIP_WORKTREE);
    ++					&the_index, PS_IGNORE_SKIP_WORKTREE);
      
      		/*
      		 * file_exists() assumes exact match
    @@ builtin/add.c: int cmd_add(int argc, const char **argv, const char *prefix)
     +				continue;
     +			}
     +
    -+			/* Don't complain at 'git add .' inside empty repo. */
    ++			/* Don't complain at 'git add .' on empty repo */
     +			if (!path[0])
     +				continue;
     +
    @@ pathspec.h
     @@ pathspec.h: void add_pathspec_matches_against_index(const struct pathspec *pathspec,
      char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
      					    const struct index_state *istate,
    - 					    int ignore_skip_worktree);
    + 					    enum ps_skip_worktree_action sw_action);
     +char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec);
     +static inline int matches_skip_worktree(const struct pathspec *pathspec,
     +					int item, char **seen_ptr)
    @@ t/t3705-add-sparse-checkout.sh: setup_gitignore () {
      	EOF
      }
      
    -+test_expect_success 'setup' '
    ++test_expect_success 'setup' "
     +	cat >sparse_error_header <<-EOF &&
    -+	The following pathspecs only matched index entries outside the current
    -+	sparse checkout:
    ++	The following pathspecs didn't match any eligible path, but they do match index
    ++	entries outside the current sparse checkout:
     +	EOF
     +
     +	cat >sparse_hint <<-EOF &&
     +	hint: Disable or modify the sparsity rules if you intend to update such entries.
    -+	hint: Disable this message with "git config advice.updateSparsePath false"
    ++	hint: Disable this message with \"git config advice.updateSparsePath false\"
     +	EOF
     +
     +	echo sparse_entry | cat sparse_error_header - >sparse_entry_error &&
     +	cat sparse_entry_error sparse_hint >error_and_hint
    -+'
    ++"
     +
      test_expect_success 'git add does not remove sparse entries' '
      	setup_sparse_entry &&
    @@ t/t3705-add-sparse-checkout.sh: test_expect_success 'git add --renormalize does
     -	git add --renormalize sparse_entry &&
     +	test_must_fail git add --renormalize sparse_entry 2>stderr &&
     +	test_i18ncmp error_and_hint stderr &&
    - 	test_sparse_entry_unchanged
    - '
    - 
    ++	test_sparse_entry_unchanged
    ++'
    ++
     +test_expect_success 'git add --dry-run --ignore-missing warn on sparse path' '
     +	setup_sparse_entry &&
     +	rm sparse_entry &&
     +	test_must_fail git add --dry-run --ignore-missing sparse_entry 2>stderr &&
     +	test_i18ncmp error_and_hint stderr &&
    -+	test_sparse_entry_unchanged
    -+'
    -+
    + 	test_sparse_entry_unchanged
    + '
    + 
     +test_expect_success 'do not advice about sparse entries when they do not match the pathspec' '
     +	setup_sparse_entry &&
     +	test_must_fail git add nonexistent 2>stderr &&
     +	test_i18ngrep "fatal: pathspec .nonexistent. did not match any files" stderr &&
    -+	test_i18ngrep ! "The following pathspecs only matched index entries" stderr
    ++	test_i18ngrep ! -F -f sparse_error_header stderr
     +'
     +
     +test_expect_success 'do not warn when pathspec matches dense entries' '
7:  bb29aa4d8f ! 7:  d25ea10560 rm: honor sparse checkout patterns
    @@ Metadata
      ## Commit message ##
         rm: honor sparse checkout patterns
     
    -    `git add` refrains from adding or updating paths outside the sparsity
    -    rules, but `git rm` doesn't follow the same restrictions. This is
    -    somewhat counter-intuitive and inconsistent. So make `rm` honor the
    -    sparse checkout and advise on how to remove SKIP_WORKTREE entries, just
    -    like `add` does. Also add some tests for the new behavior.
    +    `git add` refrains from adding or updating index entries that are
    +    outside the current sparse checkout, but `git rm` doesn't follow the
    +    same restriction. This is somewhat counter-intuitive and inconsistent.
    +    So make `rm` honor the sparsity rules and advise on how to remove
    +    SKIP_WORKTREE entries, just like `add` does. Also add some tests for the
    +    new behavior.
     
         Suggested-by: Elijah Newren <newren@gmail.com>
         Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
    @@ Documentation/config/advice.txt: advice.*::
      		Advice shown if a user runs the add command without providing
      		the pathspec parameter.
      	updateSparsePath::
    --		Advice shown if the pathspec given to linkgit:git-add[1] only
    --		matches index entries outside the current sparse-checkout.
    -+		Advice shown if the pathspec given to linkgit:git-add[1] or
    -+		linkgit:git-rm[1] only matches index entries outside the
    -+		current sparse-checkout.
    +-		Advice shown when linkgit:git-add[1] is asked to update index
    +-		entries outside the current sparse checkout.
    ++		Advice shown when either linkgit:git-add[1] or linkgit:git-rm[1]
    ++		is asked to update index entries outside the current sparse
    ++		checkout.
      --
     
      ## Documentation/git-rm.txt ##
    @@ t/t3602-rm-sparse-checkout.sh (new)
     +
     +. ./test-lib.sh
     +
    -+test_expect_success 'setup' '
    ++test_expect_success 'setup' "
     +	mkdir -p sub/dir &&
     +	touch a b c sub/d sub/dir/e &&
     +	git add -A &&
     +	git commit -m files &&
     +
    -+	cat >sparse_entry_b_error <<-EOF &&
    -+	The following pathspecs only matched index entries outside the current
    -+	sparse checkout:
    -+	b
    ++	cat >sparse_error_header <<-EOF &&
    ++	The following pathspecs didn't match any eligible path, but they do match index
    ++	entries outside the current sparse checkout:
     +	EOF
     +
    -+	cat >b_error_and_hint sparse_entry_b_error - <<-EOF
    ++	cat >sparse_hint <<-EOF &&
     +	hint: Disable or modify the sparsity rules if you intend to update such entries.
    -+	hint: Disable this message with "git config advice.updateSparsePath false"
    ++	hint: Disable this message with \"git config advice.updateSparsePath false\"
     +	EOF
    -+'
    ++
    ++	echo b | cat sparse_error_header - >sparse_entry_b_error &&
    ++	cat sparse_entry_b_error sparse_hint >b_error_and_hint
    ++"
     +
     +for opt in "" -f --dry-run
     +do
    @@ t/t3602-rm-sparse-checkout.sh (new)
     +	git sparse-checkout set a &&
     +	test_must_fail git rm nonexistent 2>stderr &&
     +	test_i18ngrep "fatal: pathspec .nonexistent. did not match any files" stderr &&
    -+	test_i18ngrep ! "The following pathspecs only matched index entries" stderr
    ++	test_i18ngrep ! -F -f sparse_error_header stderr
     +'
     +
     +test_expect_success 'do not warn about sparse entries when pathspec matches dense entries' '
-- 
2.30.1
Matheus Tavares· Mar 12, 2021, 22:47 UTC · re: Matheus Tavares · lore

We already have a couple tests for `add` with SKIP_WORKTREE entries in t7012, but these only cover the most basic scenarios. As we will be changing how `add` deals with sparse paths in the subsequent commits, let's move these two tests to their own file and add more test cases for different `add` options and situations. This also demonstrates two options that don't currently respect SKIP_WORKTREE entries: `--chmod` and `--renormalize`.

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 t/t3705-add-sparse-checkout.sh   | 96 ++++++++++++++++++++++++++++++++
 t/t7012-skip-worktree-writing.sh | 19 -------
 2 files changed, 96 insertions(+), 19 deletions(-)
 create mode 100755 t/t3705-add-sparse-checkout.sh
Show changes to 2 files +96 −19

t/t3705-add-sparse-checkout.sh, t/t7012-skip-worktree-writing.sh

diff --git a/t/t3705-add-sparse-checkout.sh b/t/t3705-add-sparse-checkout.sh
new file mode 100755
index 0000000000..6c5b8be863
--- /dev/null
+++ b/t/t3705-add-sparse-checkout.sh
@@ -0,0 +1,96 @@
+#!/bin/sh
+
+test_description='git add in sparse checked out working trees'
+
+. ./test-lib.sh
+
+SPARSE_ENTRY_BLOB=""
+
+# Optionally take a printf format string to write to the sparse_entry file
+setup_sparse_entry () {
+	# 'sparse_entry' might already be in the index with the skip-worktree
+	# bit set. Remove it so that the subsequent git add can update it.
+	git update-index --force-remove sparse_entry &&
+	if test $# -eq 1
+	then
+		printf "$1" >sparse_entry
+	else
+		>sparse_entry
+	fi &&
+	git add sparse_entry &&
+	git update-index --skip-worktree sparse_entry &&
+	SPARSE_ENTRY_BLOB=$(git rev-parse :sparse_entry)
+}
+
+test_sparse_entry_unchanged () {
+	echo "100644 $SPARSE_ENTRY_BLOB 0	sparse_entry" >expected &&
+	git ls-files --stage sparse_entry >actual &&
+	test_cmp expected actual
+}
+
+setup_gitignore () {
+	test_when_finished rm -f .gitignore &&
+	cat >.gitignore <<-EOF
+	*
+	!/sparse_entry
+	EOF
+}
+
+test_expect_success 'git add does not remove sparse entries' '
+	setup_sparse_entry &&
+	rm sparse_entry &&
+	git add sparse_entry &&
+	test_sparse_entry_unchanged
+'
+
+test_expect_success 'git add -A does not remove sparse entries' '
+	setup_sparse_entry &&
+	rm sparse_entry &&
+	setup_gitignore &&
+	git add -A &&
+	test_sparse_entry_unchanged
+'
+
+test_expect_success 'git add . does not remove sparse entries' '
+	setup_sparse_entry &&
+	rm sparse_entry &&
+	setup_gitignore &&
+	git add . &&
+	test_sparse_entry_unchanged
+'
+
+for opt in "" -f -u --ignore-removal --dry-run
+do
+	test_expect_success "git add${opt:+ $opt} does not update sparse entries" '
+		setup_sparse_entry &&
+		echo modified >sparse_entry &&
+		git add $opt sparse_entry &&
+		test_sparse_entry_unchanged
+	'
+done
+
+test_expect_success 'git add --refresh does not update sparse entries' '
+	setup_sparse_entry &&
+	git ls-files --debug sparse_entry | grep mtime >before &&
+	test-tool chmtime -60 sparse_entry &&
+	git add --refresh sparse_entry &&
+	git ls-files --debug sparse_entry | grep mtime >after &&
+	test_cmp before after
+'
+
+test_expect_failure 'git add --chmod does not update sparse entries' '
+	setup_sparse_entry &&
+	git add --chmod=+x sparse_entry &&
+	test_sparse_entry_unchanged &&
+	! test -x sparse_entry
+'
+
+test_expect_failure 'git add --renormalize does not update sparse entries' '
+	test_config core.autocrlf false &&
+	setup_sparse_entry "LINEONE\r\nLINETWO\r\n" &&
+	echo "sparse_entry text=auto" >.gitattributes &&
+	git add --renormalize sparse_entry &&
+	test_sparse_entry_unchanged
+'
+
+test_done
diff --git a/t/t7012-skip-worktree-writing.sh b/t/t7012-skip-worktree-writing.sh
index f2a8e76511..a1080b94e3 100755
--- a/t/t7012-skip-worktree-writing.sh
+++ b/t/t7012-skip-worktree-writing.sh
@@ -60,13 +60,6 @@ setup_absent() {
 	git update-index --skip-worktree 1
 }
 
-test_absent() {
-	echo "100644 $EMPTY_BLOB 0	1" > expected &&
-	git ls-files --stage 1 > result &&
-	test_cmp expected result &&
-	test ! -f 1
-}
-
 setup_dirty() {
 	git update-index --force-remove 1 &&
 	echo dirty > 1 &&
@@ -100,18 +93,6 @@ test_expect_success 'index setup' '
 	test_cmp expected result
 '
 
-test_expect_success 'git-add ignores worktree content' '
-	setup_absent &&
-	git add 1 &&
-	test_absent
-'
-
-test_expect_success 'git-add ignores worktree content' '
-	setup_dirty &&
-	git add 1 &&
-	test_dirty
-'
-
 test_expect_success 'git-rm fails if worktree is dirty' '
 	setup_dirty &&
 	test_must_fail git rm 1 &&
-- 
2.30.1
Derrick Stolee· Mar 23, 2021, 20:00 UTC · re: Matheus Tavares · lore

Re: [PATCH v3 2/7] t3705: add tests for `git add` in sparse checkouts

On 3/12/21 5:47 PM, Matheus Tavares wrote:
Show 7 quoted lines
> We already have a couple tests for `add` with SKIP_WORKTREE entries in
> t7012, but these only cover the most basic scenarios. As we will be
> changing how `add` deals with sparse paths in the subsequent commits,
> let's move these two tests to their own file and add more test cases
> for different `add` options and situations. This also demonstrates two
> options that don't currently respect SKIP_WORKTREE entries: `--chmod`
> and `--renormalize`.

I'm super-late to taking a look at this series, but I wanted to point out how much I like this new test structure. In particular, I appreciate that you point out undesirable behavior with a failing test here that changes with your correction in the next patch.

Thanks! -Stolee

Matheus Tavares· Mar 12, 2021, 22:47 UTC · re: Matheus Tavares · lore

[PATCH v3 1/7] add: include magic part of pathspec on --refresh error

When `git add --refresh <pathspec>` doesn't find any matches for the given pathspec, it prints an error message using the `match` field of the `struct pathspec_item`. However, this field doesn't contain the magic part of the pathspec. Instead, let's use the `original` field.

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 builtin/add.c  | 2 +-
 t/t3700-add.sh | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)
Show changes to 2 files +7 −1

builtin/add.c, t/t3700-add.sh

diff --git a/builtin/add.c b/builtin/add.c
index ea762a41e3..24ed7e25f3 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -187,7 +187,7 @@ static void refresh(int verbose, const struct pathspec *pathspec)
 	for (i = 0; i < pathspec->nr; i++) {
 		if (!seen[i])
 			die(_("pathspec '%s' did not match any files"),
-			    pathspec->items[i].match);
+			    pathspec->items[i].original);
 	}
 	free(seen);
 }
diff --git a/t/t3700-add.sh b/t/t3700-add.sh
index b3b122ff97..cd78dd65e5 100755
--- a/t/t3700-add.sh
+++ b/t/t3700-add.sh
@@ -196,6 +196,12 @@ test_expect_success 'git add --refresh with pathspec' '
 	grep baz actual
 '
 
+test_expect_success 'git add --refresh correctly reports no match error' "
+	echo \"fatal: pathspec ':(icase)nonexistent' did not match any files\" >expect &&
+	test_must_fail git add --refresh ':(icase)nonexistent' 2>actual &&
+	test_i18ncmp expect actual
+"
+
 test_expect_success POSIXPERM,SANITY 'git add should fail atomically upon an unreadable file' '
 	git reset --hard &&
 	date >foo1 &&
-- 
2.30.1
Matheus Tavares· Mar 12, 2021, 22:47 UTC · re: Matheus Tavares · lore

[PATCH v3 4/7] pathspec: allow to ignore SKIP_WORKTREE entries on index matching

Add a new enum parameter to `add_pathspec_matches_against_index()` and `find_pathspecs_matching_against_index()`, allowing callers to specify whether these function should attempt to match SKIP_WORKTREE entries or not. This will be used in a future patch to make `git add` display a warning when it is asked to update SKIP_WORKTREE entries.

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 builtin/add.c          |  6 ++++--
 builtin/check-ignore.c |  3 ++-
 pathspec.c             | 10 +++++++---
 pathspec.h             | 10 ++++++++--
 4 files changed, 21 insertions(+), 8 deletions(-)
Show changes to 4 files +21 −8

builtin/add.c, builtin/check-ignore.c, pathspec.c, pathspec.h

diff --git a/builtin/add.c b/builtin/add.c
index 5fec21a792..050cb8af30 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -177,7 +177,8 @@ static char *prune_directory(struct dir_struct *dir, struct pathspec *pathspec,
 			*dst++ = entry;
 	}
 	dir->nr = dst - dir->entries;
-	add_pathspec_matches_against_index(pathspec, &the_index, seen);
+	add_pathspec_matches_against_index(pathspec, &the_index, seen,
+					   PS_HEED_SKIP_WORKTREE);
 	return seen;
 }
 
@@ -578,7 +579,8 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 		int i;
 
 		if (!seen)
-			seen = find_pathspecs_matching_against_index(&pathspec, &the_index);
+			seen = find_pathspecs_matching_against_index(&pathspec,
+					&the_index, PS_HEED_SKIP_WORKTREE);
 
 		/*
 		 * file_exists() assumes exact match
diff --git a/builtin/check-ignore.c b/builtin/check-ignore.c
index 3c652748d5..0f4480a11b 100644
--- a/builtin/check-ignore.c
+++ b/builtin/check-ignore.c
@@ -100,7 +100,8 @@ static int check_ignore(struct dir_struct *dir,
 	 * should not be ignored, in order to be consistent with
 	 * 'git status', 'git add' etc.
 	 */
-	seen = find_pathspecs_matching_against_index(&pathspec, &the_index);
+	seen = find_pathspecs_matching_against_index(&pathspec, &the_index,
+						     PS_HEED_SKIP_WORKTREE);
 	for (i = 0; i < pathspec.nr; i++) {
 		full_path = pathspec.items[i].match;
 		pattern = NULL;
diff --git a/pathspec.c b/pathspec.c
index 7a229d8d22..6d502e64e5 100644
--- a/pathspec.c
+++ b/pathspec.c
@@ -21,7 +21,8 @@
  */
 void add_pathspec_matches_against_index(const struct pathspec *pathspec,
 					const struct index_state *istate,
-					char *seen)
+					char *seen,
+					enum ps_skip_worktree_action sw_action)
 {
 	int num_unmatched = 0, i;
 
@@ -38,6 +39,8 @@ void add_pathspec_matches_against_index(const struct pathspec *pathspec,
 		return;
 	for (i = 0; i < istate->cache_nr; i++) {
 		const struct cache_entry *ce = istate->cache[i];
+		if (sw_action == PS_IGNORE_SKIP_WORKTREE && ce_skip_worktree(ce))
+			continue;
 		ce_path_match(istate, ce, pathspec, seen);
 	}
 }
@@ -51,10 +54,11 @@ void add_pathspec_matches_against_index(const struct pathspec *pathspec,
  * given pathspecs achieves against all items in the index.
  */
 char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
-					    const struct index_state *istate)
+					    const struct index_state *istate,
+					    enum ps_skip_worktree_action sw_action)
 {
 	char *seen = xcalloc(pathspec->nr, 1);
-	add_pathspec_matches_against_index(pathspec, istate, seen);
+	add_pathspec_matches_against_index(pathspec, istate, seen, sw_action);
 	return seen;
 }
 
diff --git a/pathspec.h b/pathspec.h
index 454ce364fa..0feb8e9f67 100644
--- a/pathspec.h
+++ b/pathspec.h
@@ -149,11 +149,17 @@ static inline int ps_strcmp(const struct pathspec_item *item,
 		return strcmp(s1, s2);
 }
 
+enum ps_skip_worktree_action {
+  PS_HEED_SKIP_WORKTREE = 0,
+  PS_IGNORE_SKIP_WORKTREE = 1
+};
 void add_pathspec_matches_against_index(const struct pathspec *pathspec,
 					const struct index_state *istate,
-					char *seen);
+					char *seen,
+					enum ps_skip_worktree_action sw_action);
 char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
-					    const struct index_state *istate);
+					    const struct index_state *istate,
+					    enum ps_skip_worktree_action sw_action);
 int match_pathspec_attrs(const struct index_state *istate,
 			 const char *name, int namelen,
 			 const struct pathspec_item *item);
-- 
2.30.1
Matheus Tavares· Mar 12, 2021, 22:48 UTC · re: Matheus Tavares · lore

[PATCH v3 5/7] refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag

refresh_index() optionally takes a seen[] array to mark the pathspec items that had matches in the index. This is used by `git add --refresh` to find out if there was any pathspec without matches, and display an error accordingly.

In the following patch, `git add` will also learn to warn about pathspecs that match no eligible path for updating, but do match sparse entries. For that, we will need a seen[] array marked exclusively with matches from dense entries. To avoid having to call ce_path_match() again for these entries after refresh_index() returns, add a flag that implements this restriction inside the function itself.

Note that refresh_index() does not update sparse entries, regardless of passing the flag or not. The flag only controls whether matches with these entries should appear in the seen[] array.

While we are here, also realign the REFRESH_* flags and convert the hex values to the more natural bit shift format, which makes it easier to spot holes.

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 cache.h      | 15 ++++++++-------
 read-cache.c |  5 ++++-
 2 files changed, 12 insertions(+), 8 deletions(-)
Show changes to 2 files +12 −8

cache.h, read-cache.c

diff --git a/cache.h b/cache.h
index 6fda8091f1..0b6074d49e 100644
--- a/cache.h
+++ b/cache.h
@@ -879,13 +879,14 @@ int match_stat_data_racy(const struct index_state *istate,
 
 void fill_stat_cache_info(struct index_state *istate, struct cache_entry *ce, struct stat *st);
 
-#define REFRESH_REALLY		0x0001	/* ignore_valid */
-#define REFRESH_UNMERGED	0x0002	/* allow unmerged */
-#define REFRESH_QUIET		0x0004	/* be quiet about it */
-#define REFRESH_IGNORE_MISSING	0x0008	/* ignore non-existent */
-#define REFRESH_IGNORE_SUBMODULES	0x0010	/* ignore submodules */
-#define REFRESH_IN_PORCELAIN	0x0020	/* user friendly output, not "needs update" */
-#define REFRESH_PROGRESS	0x0040  /* show progress bar if stderr is tty */
+#define REFRESH_REALLY                   (1 << 0) /* ignore_valid */
+#define REFRESH_UNMERGED                 (1 << 1) /* allow unmerged */
+#define REFRESH_QUIET                    (1 << 2) /* be quiet about it */
+#define REFRESH_IGNORE_MISSING           (1 << 3) /* ignore non-existent */
+#define REFRESH_IGNORE_SUBMODULES        (1 << 4) /* ignore submodules */
+#define REFRESH_IN_PORCELAIN             (1 << 5) /* user friendly output, not "needs update" */
+#define REFRESH_PROGRESS                 (1 << 6) /* show progress bar if stderr is tty */
+#define REFRESH_DONT_MARK_SPARSE_MATCHES (1 << 7) /* don't mark sparse entries' matches on seen[] */
 int refresh_index(struct index_state *, unsigned int flags, const struct pathspec *pathspec, char *seen, const char *header_msg);
 /*
  * Refresh the index and write it to disk.
diff --git a/read-cache.c b/read-cache.c
index 1e9a50c6c7..0bf7c64eff 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1514,6 +1514,7 @@ int refresh_index(struct index_state *istate, unsigned int flags,
 	int quiet = (flags & REFRESH_QUIET) != 0;
 	int not_new = (flags & REFRESH_IGNORE_MISSING) != 0;
 	int ignore_submodules = (flags & REFRESH_IGNORE_SUBMODULES) != 0;
+	int no_sparse_on_seen = (flags & REFRESH_DONT_MARK_SPARSE_MATCHES) != 0;
 	int first = 1;
 	int in_porcelain = (flags & REFRESH_IN_PORCELAIN);
 	unsigned int options = (CE_MATCH_REFRESH |
@@ -1552,12 +1553,14 @@ int refresh_index(struct index_state *istate, unsigned int flags,
 		int filtered = 0;
 		int t2_did_lstat = 0;
 		int t2_did_scan = 0;
+		char *cur_seen;
 
 		ce = istate->cache[i];
 		if (ignore_submodules && S_ISGITLINK(ce->ce_mode))
 			continue;
 
-		if (pathspec && !ce_path_match(istate, ce, pathspec, seen))
+		cur_seen = no_sparse_on_seen && ce_skip_worktree(ce) ? NULL : seen;
+		if (pathspec && !ce_path_match(istate, ce, pathspec, cur_seen))
 			filtered = 1;
 
 		if (ce_stage(ce)) {
-- 
2.30.1
Junio C Hamano· Mar 18, 2021, 23:45 UTC · re: Matheus Tavares · lore

Re: [PATCH v3 5/7] refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag

Matheus Tavares <matheus.bernardino@usp.br> writes:
> refresh_index() optionally takes a seen[] array to mark the pathspec
> items that had matches in the index. This is used by `git add --refresh`
> to find out if there was any pathspec without matches, and display an
> error accordingly.

It smells a bit iffy in that this allows a path to "match" a pattern but yet not result in seen[] to record that the pattern participated in the match. If the series is working towards certain operations not to touch paths that are outside the sparse checkout, shouldn't it be making these paths not to match the pattern, and by doing so it would automatically cause the pattern to be considered "not yet matching any path" when the matcher attempts to match the pattern to such a path?

Junio C Hamano· Mar 19, 2021, 00:00 UTC · re: Junio C Hamano · lore

Re: [PATCH v3 5/7] refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag

Junio C Hamano <gitster@pobox.com> writes:
Show 15 quoted lines
> Matheus Tavares <matheus.bernardino@usp.br> writes:
>
>> refresh_index() optionally takes a seen[] array to mark the pathspec
>> items that had matches in the index. This is used by `git add --refresh`
>> to find out if there was any pathspec without matches, and display an
>> error accordingly.
>
> It smells a bit iffy in that this allows a path to "match" a pattern
> but yet not result in seen[] to record that the pattern participated
> in the match.  If the series is working towards certain operations
> not to touch paths that are outside the sparse checkout, shouldn't
> it be making these paths not to match the pattern, and by doing so
> it would automatically cause the pattern to be considered "not yet
> matching any path" when the matcher attempts to match the pattern to
> such a path?

In other words, the change makes me wonder why we are not adding a flag that says "do we or do we not want to match paths outside the sparse checkout cone?", with which the seen[] would automatically record the right thing.

Matheus Tavares Bernardino· Mar 19, 2021, 12:23 UTC · re: Junio C Hamano · lore

Re: [PATCH v3 5/7] refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag

On Thu, Mar 18, 2021 at 9:00 PM Junio C Hamano <gitster@pobox.com> wrote:
Show 23 quoted lines
>
> Junio C Hamano <gitster@pobox.com> writes:
>
> > Matheus Tavares <matheus.bernardino@usp.br> writes:
> >
> >> refresh_index() optionally takes a seen[] array to mark the pathspec
> >> items that had matches in the index. This is used by `git add --refresh`
> >> to find out if there was any pathspec without matches, and display an
> >> error accordingly.
> >
> > It smells a bit iffy in that this allows a path to "match" a pattern
> > but yet not result in seen[] to record that the pattern participated
> > in the match.  If the series is working towards certain operations
> > not to touch paths that are outside the sparse checkout, shouldn't
> > it be making these paths not to match the pattern, and by doing so
> > it would automatically cause the pattern to be considered "not yet
> > matching any path" when the matcher attempts to match the pattern to
> > such a path?
>
> In other words, the change makes me wonder why we are not adding a
> flag that says "do we or do we not want to match paths outside the
> sparse checkout cone?", with which the seen[] would automatically
> record the right thing.

Yeah, makes sense. I didn't want to make the flag skip the sparse paths unconditionally (i.e. without matching them) because then we would also skip the ce_stage() checkings below and the later ce_mark_uptodate(). And I wasn't sure whether this could cause any unwanted side effects.

But thinking more carefully about this now, unmerged paths should never have the SKIP_WORKTREE bit set anyway, right? What about the CE_UPTODATE mark, would it be safe to skip it? I'm not very familiar with this code, but I'll try to investigate more later.

Junio C Hamano· Mar 19, 2021, 16:05 UTC · re: Matheus Tavares Bernardino · lore

Re: [PATCH v3 5/7] refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag

Matheus Tavares Bernardino <matheus.bernardino@usp.br> writes:
Show 15 quoted lines
>> In other words, the change makes me wonder why we are not adding a
>> flag that says "do we or do we not want to match paths outside the
>> sparse checkout cone?", with which the seen[] would automatically
>> record the right thing.
>
> Yeah, makes sense. I didn't want to make the flag skip the sparse
> paths unconditionally (i.e. without matching them) because then we
> would also skip the ce_stage() checkings below and the later
> ce_mark_uptodate(). And I wasn't sure whether this could cause any
> unwanted side effects.
>
> But thinking more carefully about this now, unmerged paths should
> never have the SKIP_WORKTREE bit set anyway, right? What about the
> CE_UPTODATE mark, would it be safe to skip it? I'm not very familiar
> with this code, but I'll try to investigate more later.

I do not offhand know the answers to these questions you ask in the last paragraph myself, and it could turn out to be that the "let processing go as before, matching what those excluded paths by the matcher, but making the caller responsible for filtering out the paths excluded by sparcity out of the result from the matcher, but tweak seen[]" approach, however ugly, is the most practical, after your investigation. But I'd prefer to see how bad the situation is first ;-).

Thanks.
Matheus Tavares Bernardino· Mar 30, 2021, 18:51 UTC · re: Junio C Hamano · lore

Re: [PATCH v3 5/7] refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag

On Fri, Mar 19, 2021 at 1:05 PM Junio C Hamano <gitster@pobox.com> wrote:
Show 18 quoted lines
>
> Matheus Tavares Bernardino <matheus.bernardino@usp.br> writes:
>
> >> In other words, the change makes me wonder why we are not adding a
> >> flag that says "do we or do we not want to match paths outside the
> >> sparse checkout cone?", with which the seen[] would automatically
> >> record the right thing.
> >
> > Yeah, makes sense. I didn't want to make the flag skip the sparse
> > paths unconditionally (i.e. without matching them) because then we
> > would also skip the ce_stage() checkings below and the later
> > ce_mark_uptodate(). And I wasn't sure whether this could cause any
> > unwanted side effects.
> >
> > But thinking more carefully about this now, unmerged paths should
> > never have the SKIP_WORKTREE bit set anyway, right? What about the
> > CE_UPTODATE mark, would it be safe to skip it? I'm not very familiar
> > with this code, but I'll try to investigate more later.

Sorry I haven't given any update on this yet. From what I could see so far, it seems OK to ignore the skip_worktree entries in refresh_index() when it is called from `git add --refresh`. But because we would no longer mark the skip_worktree entries that match the pathspec with CE_UPTODATE, do_write_index() would start checking if they are racy clean (this is only done when `!ce_uptodate(ce)`), which could add some lstat() overhead.

However, this made me think what happens today if we do have a racy clean entry with the skip_worktree bit set... `git add --refresh` will probably update the index without noticing that the entry is racy clean (because it won't check CE_UPTODATE entries, and skip_worktree entries will have this bit set in refresh_index()). Thus the entries' size won't be truncated to zero when writing the index, and the entry will appear unchanged even if we later unset the skip_worktree bit.

But can we have a "racy clean skip_worktree entry"? Yes, this seems possible e.g. if the following sequence happens fast enough for mtime to be the same before and after the update:

  echo x >file
  git update-index --refresh --skip-worktree file
  echo y>file

Here is a more complete example which artificially creates a "racy clean skip_worktree entry", runs `git add --refresh`, and shows that the racy clean entry was not detected:

# Setup echo sparse >sparse echo dense >dense git add . git commit -m files

# Emulate a racy clean situation touch -d yesterday date touch -r date sparse git update-index --refresh --skip-worktree sparse touch -r date .git/index echo xparse >sparse touch -r date sparse

# `git add` will now write a new index without checking if # `sparse` is racy clean nor truncating its size touch -r date dense git add --refresh .

git update-index --no-skip-worktree sparse git status <doesn't show that `sparse` was modified>

This situation feels rather uncommon, but perhaps the same could happen with `git sparse-checkout set` instead of `git update-index --refresh --skip-worktree`? IDK. This made me think whether refresh_index() should really mark skip_worktree entries with CE_UPTODATE, in the first place.

Any thoughts?
Elijah Newren· Mar 31, 2021, 09:14 UTC · re: Matheus Tavares Bernardino · lore

Re: [PATCH v3 5/7] refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag

Hi,

On Tue, Mar 30, 2021 at 11:51 AM Matheus Tavares Bernardino <matheus.bernardino@usp.br> wrote:

Show 79 quoted lines
>
> On Fri, Mar 19, 2021 at 1:05 PM Junio C Hamano <gitster@pobox.com> wrote:
> >
> > Matheus Tavares Bernardino <matheus.bernardino@usp.br> writes:
> >
> > >> In other words, the change makes me wonder why we are not adding a
> > >> flag that says "do we or do we not want to match paths outside the
> > >> sparse checkout cone?", with which the seen[] would automatically
> > >> record the right thing.
> > >
> > > Yeah, makes sense. I didn't want to make the flag skip the sparse
> > > paths unconditionally (i.e. without matching them) because then we
> > > would also skip the ce_stage() checkings below and the later
> > > ce_mark_uptodate(). And I wasn't sure whether this could cause any
> > > unwanted side effects.
> > >
> > > But thinking more carefully about this now, unmerged paths should
> > > never have the SKIP_WORKTREE bit set anyway, right? What about the
> > > CE_UPTODATE mark, would it be safe to skip it? I'm not very familiar
> > > with this code, but I'll try to investigate more later.
>
> Sorry I haven't given any update on this yet. From what I could see so
> far, it seems OK to ignore the skip_worktree entries in
> refresh_index() when it is called from `git add --refresh`. But
> because we would no longer mark the skip_worktree entries that match
> the pathspec with CE_UPTODATE, do_write_index() would start checking
> if they are racy clean (this is only done when `!ce_uptodate(ce)`),
> which could add some lstat() overhead.
>
> However, this made me think what happens today if we do have a racy
> clean entry with the skip_worktree bit set... `git add --refresh` will
> probably update the index without noticing that the entry is racy
> clean (because it won't check CE_UPTODATE entries, and skip_worktree
> entries will have this bit set in refresh_index()). Thus the entries'
> size won't be truncated to zero when writing the index, and the entry
> will appear unchanged even if we later unset the skip_worktree bit.
>
> But can we have a "racy clean skip_worktree entry"? Yes, this seems
> possible e.g. if the following sequence happens fast enough for mtime
> to be the same before and after the update:
>
>   echo x >file
>   git update-index --refresh --skip-worktree file
>   echo y>file
>
> Here is a more complete example which artificially creates a "racy
> clean skip_worktree entry", runs `git add --refresh`, and shows that
> the racy clean entry was not detected:
>
> # Setup
> echo sparse >sparse
> echo dense >dense
> git add .
> git commit -m files
>
> # Emulate a racy clean situation
> touch -d yesterday date
> touch -r date sparse
> git update-index --refresh --skip-worktree sparse
> touch -r date .git/index
> echo xparse >sparse
> touch -r date sparse
>
> # `git add` will now write a new index without checking if
> # `sparse` is racy clean nor truncating its size
> touch -r date dense
> git add --refresh .
>
> git update-index --no-skip-worktree sparse
> git status
> <doesn't show that `sparse` was modified>
>
> This situation feels rather uncommon, but perhaps the same could
> happen with `git sparse-checkout set` instead of `git update-index
> --refresh --skip-worktree`? IDK. This made me think whether
> refresh_index() should really mark skip_worktree entries with
> CE_UPTODATE, in the first place.
>
> Any thoughts?

Wow, that's a weird one. Nice digging. I don't understand the racily clean stuff that well, or the CE_UPTODATE handling...but based on my meager understanding of these things I'd say that marking skip_worktree entries with CE_UPTODATE seems wrong and I'd agree with your hunch that we shouldn't be updating it for those files. If an entry is skip_worktree, it's assumed to not be present in the working tree and that we'll treat it as "unchanged". So, when the filehappens to be present despite that bit being set, checking the files' stat information or pretending to have done so so we can claim it is up to date seems wrong to me. In fact, I'd say the mere recognition that the file is present in the working tree despite being SKIP_WORKTREE to me implies it's not "up-to-date" for at least one definition of that term.

I suspect that if someone flips the skip-worktree bit on and off for a file without removing it from the working tree, not marking skip_worktree entries with CE_UPTODATE as you mention above might force us to incur the cost of an extra stat on said file when we run "git status" later. If so, I think that's a totally reasonable cost, so if that was your worry, I'd say that this is shifting the cost where it belongs.

But, like I said, I'm not very familiar with the racily clean code or CE_UPTODATE, so it's possible I said something above that doesn't even make sense. Does my reasoning help at all?

Matheus Tavares· Mar 12, 2021, 22:47 UTC · re: Matheus Tavares · lore

[PATCH v3 3/7] add: make --chmod and --renormalize honor sparse checkouts

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 builtin/add.c                  | 5 +++++
 t/t3705-add-sparse-checkout.sh | 4 ++--
 2 files changed, 7 insertions(+), 2 deletions(-)
Show changes to 2 files +7 −2

builtin/add.c, t/t3705-add-sparse-checkout.sh

diff --git a/builtin/add.c b/builtin/add.c
index 24ed7e25f3..5fec21a792 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -46,6 +46,9 @@ static int chmod_pathspec(struct pathspec *pathspec, char flip, int show_only)
 		struct cache_entry *ce = active_cache[i];
 		int err;
 
+		if (ce_skip_worktree(ce))
+			continue;
+
 		if (pathspec && !ce_path_match(&the_index, ce, pathspec, NULL))
 			continue;
 
@@ -144,6 +147,8 @@ static int renormalize_tracked_files(const struct pathspec *pathspec, int flags)
 	for (i = 0; i < active_nr; i++) {
 		struct cache_entry *ce = active_cache[i];
 
+		if (ce_skip_worktree(ce))
+			continue;
 		if (ce_stage(ce))
 			continue; /* do not touch unmerged paths */
 		if (!S_ISREG(ce->ce_mode) && !S_ISLNK(ce->ce_mode))
diff --git a/t/t3705-add-sparse-checkout.sh b/t/t3705-add-sparse-checkout.sh
index 6c5b8be863..00b10ac877 100755
--- a/t/t3705-add-sparse-checkout.sh
+++ b/t/t3705-add-sparse-checkout.sh
@@ -78,14 +78,14 @@ test_expect_success 'git add --refresh does not update sparse entries' '
 	test_cmp before after
 '
 
-test_expect_failure 'git add --chmod does not update sparse entries' '
+test_expect_success 'git add --chmod does not update sparse entries' '
 	setup_sparse_entry &&
 	git add --chmod=+x sparse_entry &&
 	test_sparse_entry_unchanged &&
 	! test -x sparse_entry
 '
 
-test_expect_failure 'git add --renormalize does not update sparse entries' '
+test_expect_success 'git add --renormalize does not update sparse entries' '
 	test_config core.autocrlf false &&
 	setup_sparse_entry "LINEONE\r\nLINETWO\r\n" &&
 	echo "sparse_entry text=auto" >.gitattributes &&
-- 
2.30.1
Matheus Tavares· Mar 12, 2021, 22:48 UTC · re: Matheus Tavares · lore

[PATCH v3 6/7] add: warn when asked to update SKIP_WORKTREE entries

`git add` already refrains from updating SKIP_WORKTREE entries, but it silently exits with zero code when it is asked to do so. Instead, let's warn the user and display a hint on how to update these entries.

Note that we only warn the user whey they give a pathspec item that matches no eligible path for updating, but it does match one or more SKIP_WORKTREE entries. A warning was chosen over erroring out right away to reproduce the same behavior `add` already exhibits with ignored files. This also allow users to continue their workflow without having to invoke `add` again with only the eligible paths (as those will have already been added).

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 Documentation/config/advice.txt |  3 ++
 advice.c                        | 20 +++++++++
 advice.h                        |  4 ++
 builtin/add.c                   | 70 ++++++++++++++++++++++++-------
 pathspec.c                      | 15 +++++++
 pathspec.h                      |  8 ++++
 t/t3705-add-sparse-checkout.sh  | 73 +++++++++++++++++++++++++++++----
 7 files changed, 172 insertions(+), 21 deletions(-)
Show changes to 7 files +172 −21

Documentation/config/advice.txt, advice.c, advice.h, builtin/add.c, pathspec.c, pathspec.h, t/t3705-add-sparse-checkout.sh

diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
index acbd0c09aa..851b83cf30 100644
--- a/Documentation/config/advice.txt
+++ b/Documentation/config/advice.txt
@@ -119,4 +119,7 @@ advice.*::
 	addEmptyPathspec::
 		Advice shown if a user runs the add command without providing
 		the pathspec parameter.
+	updateSparsePath::
+		Advice shown when linkgit:git-add[1] is asked to update index
+		entries outside the current sparse checkout.
 --
diff --git a/advice.c b/advice.c
index 164742305f..0b9c89c48a 100644
--- a/advice.c
+++ b/advice.c
@@ -2,6 +2,7 @@
 #include "config.h"
 #include "color.h"
 #include "help.h"
+#include "string-list.h"
 
 int advice_fetch_show_forced_updates = 1;
 int advice_push_update_rejected = 1;
@@ -136,6 +137,7 @@ static struct {
 	[ADVICE_STATUS_HINTS]				= { "statusHints", 1 },
 	[ADVICE_STATUS_U_OPTION]			= { "statusUoption", 1 },
 	[ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE] = { "submoduleAlternateErrorStrategyDie", 1 },
+	[ADVICE_UPDATE_SPARSE_PATH]			= { "updateSparsePath", 1 },
 	[ADVICE_WAITING_FOR_EDITOR]			= { "waitingForEditor", 1 },
 };
 
@@ -284,6 +286,24 @@ void NORETURN die_conclude_merge(void)
 	die(_("Exiting because of unfinished merge."));
 }
 
+void advise_on_updating_sparse_paths(struct string_list *pathspec_list)
+{
+	struct string_list_item *item;
+
+	if (!pathspec_list->nr)
+		return;
+
+	fprintf(stderr, _("The following pathspecs didn't match any"
+			  " eligible path, but they do match index\n"
+			  "entries outside the current sparse checkout:\n"));
+	for_each_string_list_item(item, pathspec_list)
+		fprintf(stderr, "%s\n", item->string);
+
+	advise_if_enabled(ADVICE_UPDATE_SPARSE_PATH,
+			  _("Disable or modify the sparsity rules if you intend"
+			    " to update such entries."));
+}
+
 void detach_advice(const char *new_name)
 {
 	const char *fmt =
diff --git a/advice.h b/advice.h
index bc2432980a..bd26c385d0 100644
--- a/advice.h
+++ b/advice.h
@@ -3,6 +3,8 @@
 
 #include "git-compat-util.h"
 
+struct string_list;
+
 extern int advice_fetch_show_forced_updates;
 extern int advice_push_update_rejected;
 extern int advice_push_non_ff_current;
@@ -71,6 +73,7 @@ extern int advice_add_empty_pathspec;
 	ADVICE_STATUS_HINTS,
 	ADVICE_STATUS_U_OPTION,
 	ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE,
+	ADVICE_UPDATE_SPARSE_PATH,
 	ADVICE_WAITING_FOR_EDITOR,
 };
 
@@ -92,6 +95,7 @@ void advise_if_enabled(enum advice_type type, const char *advice, ...);
 int error_resolve_conflict(const char *me);
 void NORETURN die_resolve_conflict(const char *me);
 void NORETURN die_conclude_merge(void);
+void advise_on_updating_sparse_paths(struct string_list *pathspec_list);
 void detach_advice(const char *new_name);
 
 #endif /* ADVICE_H */
diff --git a/builtin/add.c b/builtin/add.c
index 050cb8af30..d453313735 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -178,24 +178,43 @@ static char *prune_directory(struct dir_struct *dir, struct pathspec *pathspec,
 	}
 	dir->nr = dst - dir->entries;
 	add_pathspec_matches_against_index(pathspec, &the_index, seen,
-					   PS_HEED_SKIP_WORKTREE);
+					   PS_IGNORE_SKIP_WORKTREE);
 	return seen;
 }
 
-static void refresh(int verbose, const struct pathspec *pathspec)
+static int refresh(int verbose, const struct pathspec *pathspec)
 {
 	char *seen;
-	int i;
+	int i, ret = 0;
+	char *skip_worktree_seen = NULL;
+	struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
+	int flags = REFRESH_DONT_MARK_SPARSE_MATCHES |
+		    (verbose ? REFRESH_IN_PORCELAIN : REFRESH_QUIET);
 
 	seen = xcalloc(pathspec->nr, 1);
-	refresh_index(&the_index, verbose ? REFRESH_IN_PORCELAIN : REFRESH_QUIET,
-		      pathspec, seen, _("Unstaged changes after refreshing the index:"));
+	refresh_index(&the_index, flags, pathspec, seen,
+		      _("Unstaged changes after refreshing the index:"));
 	for (i = 0; i < pathspec->nr; i++) {
-		if (!seen[i])
-			die(_("pathspec '%s' did not match any files"),
-			    pathspec->items[i].original);
+		if (!seen[i]) {
+			if (matches_skip_worktree(pathspec, i, &skip_worktree_seen)) {
+				string_list_append(&only_match_skip_worktree,
+						   pathspec->items[i].original);
+			} else {
+				die(_("pathspec '%s' did not match any files"),
+				    pathspec->items[i].original);
+			}
+		}
+	}
+
+	if (only_match_skip_worktree.nr) {
+		advise_on_updating_sparse_paths(&only_match_skip_worktree);
+		ret = 1;
 	}
+
 	free(seen);
+	free(skip_worktree_seen);
+	string_list_clear(&only_match_skip_worktree, 0);
+	return ret;
 }
 
 int run_add_interactive(const char *revision, const char *patch_mode,
@@ -571,16 +590,18 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 	}
 
 	if (refresh_only) {
-		refresh(verbose, &pathspec);
+		exit_status |= refresh(verbose, &pathspec);
 		goto finish;
 	}
 
 	if (pathspec.nr) {
 		int i;
+		char *skip_worktree_seen = NULL;
+		struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
 
 		if (!seen)
 			seen = find_pathspecs_matching_against_index(&pathspec,
-					&the_index, PS_HEED_SKIP_WORKTREE);
+					&the_index, PS_IGNORE_SKIP_WORKTREE);
 
 		/*
 		 * file_exists() assumes exact match
@@ -594,12 +615,24 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 
 		for (i = 0; i < pathspec.nr; i++) {
 			const char *path = pathspec.items[i].match;
+
 			if (pathspec.items[i].magic & PATHSPEC_EXCLUDE)
 				continue;
-			if (!seen[i] && path[0] &&
-			    ((pathspec.items[i].magic &
-			      (PATHSPEC_GLOB | PATHSPEC_ICASE)) ||
-			     !file_exists(path))) {
+			if (seen[i])
+				continue;
+
+			if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) {
+				string_list_append(&only_match_skip_worktree,
+						   pathspec.items[i].original);
+				continue;
+			}
+
+			/* Don't complain at 'git add .' on empty repo */
+			if (!path[0])
+				continue;
+
+			if ((pathspec.items[i].magic & (PATHSPEC_GLOB | PATHSPEC_ICASE)) ||
+			    !file_exists(path)) {
 				if (ignore_missing) {
 					int dtype = DT_UNKNOWN;
 					if (is_excluded(&dir, &the_index, path, &dtype))
@@ -610,7 +643,16 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 					    pathspec.items[i].original);
 			}
 		}
+
+
+		if (only_match_skip_worktree.nr) {
+			advise_on_updating_sparse_paths(&only_match_skip_worktree);
+			exit_status = 1;
+		}
+
 		free(seen);
+		free(skip_worktree_seen);
+		string_list_clear(&only_match_skip_worktree, 0);
 	}
 
 	plug_bulk_checkin();
diff --git a/pathspec.c b/pathspec.c
index 6d502e64e5..8247a65ec8 100644
--- a/pathspec.c
+++ b/pathspec.c
@@ -62,6 +62,21 @@ char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
 	return seen;
 }
 
+char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec)
+{
+	struct index_state *istate = the_repository->index;
+	char *seen = xcalloc(pathspec->nr, 1);
+	int i;
+
+	for (i = 0; i < istate->cache_nr; i++) {
+		struct cache_entry *ce = istate->cache[i];
+		if (ce_skip_worktree(ce))
+		    ce_path_match(istate, ce, pathspec, seen);
+	}
+
+	return seen;
+}
+
 /*
  * Magic pathspec
  *
diff --git a/pathspec.h b/pathspec.h
index 0feb8e9f67..5b4c6614bf 100644
--- a/pathspec.h
+++ b/pathspec.h
@@ -160,6 +160,14 @@ void add_pathspec_matches_against_index(const struct pathspec *pathspec,
 char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
 					    const struct index_state *istate,
 					    enum ps_skip_worktree_action sw_action);
+char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec);
+static inline int matches_skip_worktree(const struct pathspec *pathspec,
+					int item, char **seen_ptr)
+{
+	if (!*seen_ptr)
+		*seen_ptr = find_pathspecs_matching_skip_worktree(pathspec);
+	return (*seen_ptr)[item];
+}
 int match_pathspec_attrs(const struct index_state *istate,
 			 const char *name, int namelen,
 			 const struct pathspec_item *item);
diff --git a/t/t3705-add-sparse-checkout.sh b/t/t3705-add-sparse-checkout.sh
index 00b10ac877..c8db764058 100755
--- a/t/t3705-add-sparse-checkout.sh
+++ b/t/t3705-add-sparse-checkout.sh
@@ -36,10 +36,26 @@ setup_gitignore () {
 	EOF
 }
 
+test_expect_success 'setup' "
+	cat >sparse_error_header <<-EOF &&
+	The following pathspecs didn't match any eligible path, but they do match index
+	entries outside the current sparse checkout:
+	EOF
+
+	cat >sparse_hint <<-EOF &&
+	hint: Disable or modify the sparsity rules if you intend to update such entries.
+	hint: Disable this message with \"git config advice.updateSparsePath false\"
+	EOF
+
+	echo sparse_entry | cat sparse_error_header - >sparse_entry_error &&
+	cat sparse_entry_error sparse_hint >error_and_hint
+"
+
 test_expect_success 'git add does not remove sparse entries' '
 	setup_sparse_entry &&
 	rm sparse_entry &&
-	git add sparse_entry &&
+	test_must_fail git add sparse_entry 2>stderr &&
+	test_i18ncmp error_and_hint stderr &&
 	test_sparse_entry_unchanged
 '
 
@@ -47,7 +63,8 @@ test_expect_success 'git add -A does not remove sparse entries' '
 	setup_sparse_entry &&
 	rm sparse_entry &&
 	setup_gitignore &&
-	git add -A &&
+	git add -A 2>stderr &&
+	test_must_be_empty stderr &&
 	test_sparse_entry_unchanged
 '
 
@@ -55,7 +72,13 @@ test_expect_success 'git add . does not remove sparse entries' '
 	setup_sparse_entry &&
 	rm sparse_entry &&
 	setup_gitignore &&
-	git add . &&
+	test_must_fail git add . 2>stderr &&
+
+	cat sparse_error_header >expect &&
+	echo . >>expect &&
+	cat sparse_hint >>expect &&
+
+	test_i18ncmp expect stderr &&
 	test_sparse_entry_unchanged
 '
 
@@ -64,7 +87,8 @@ do
 	test_expect_success "git add${opt:+ $opt} does not update sparse entries" '
 		setup_sparse_entry &&
 		echo modified >sparse_entry &&
-		git add $opt sparse_entry &&
+		test_must_fail git add $opt sparse_entry 2>stderr &&
+		test_i18ncmp error_and_hint stderr &&
 		test_sparse_entry_unchanged
 	'
 done
@@ -73,14 +97,16 @@ test_expect_success 'git add --refresh does not update sparse entries' '
 	setup_sparse_entry &&
 	git ls-files --debug sparse_entry | grep mtime >before &&
 	test-tool chmtime -60 sparse_entry &&
-	git add --refresh sparse_entry &&
+	test_must_fail git add --refresh sparse_entry 2>stderr &&
+	test_i18ncmp error_and_hint stderr &&
 	git ls-files --debug sparse_entry | grep mtime >after &&
 	test_cmp before after
 '
 
 test_expect_success 'git add --chmod does not update sparse entries' '
 	setup_sparse_entry &&
-	git add --chmod=+x sparse_entry &&
+	test_must_fail git add --chmod=+x sparse_entry 2>stderr &&
+	test_i18ncmp error_and_hint stderr &&
 	test_sparse_entry_unchanged &&
 	! test -x sparse_entry
 '
@@ -89,8 +115,41 @@ test_expect_success 'git add --renormalize does not update sparse entries' '
 	test_config core.autocrlf false &&
 	setup_sparse_entry "LINEONE\r\nLINETWO\r\n" &&
 	echo "sparse_entry text=auto" >.gitattributes &&
-	git add --renormalize sparse_entry &&
+	test_must_fail git add --renormalize sparse_entry 2>stderr &&
+	test_i18ncmp error_and_hint stderr &&
+	test_sparse_entry_unchanged
+'
+
+test_expect_success 'git add --dry-run --ignore-missing warn on sparse path' '
+	setup_sparse_entry &&
+	rm sparse_entry &&
+	test_must_fail git add --dry-run --ignore-missing sparse_entry 2>stderr &&
+	test_i18ncmp error_and_hint stderr &&
 	test_sparse_entry_unchanged
 '
 
+test_expect_success 'do not advice about sparse entries when they do not match the pathspec' '
+	setup_sparse_entry &&
+	test_must_fail git add nonexistent 2>stderr &&
+	test_i18ngrep "fatal: pathspec .nonexistent. did not match any files" stderr &&
+	test_i18ngrep ! -F -f sparse_error_header stderr
+'
+
+test_expect_success 'do not warn when pathspec matches dense entries' '
+	setup_sparse_entry &&
+	echo modified >sparse_entry &&
+	>dense_entry &&
+	git add "*_entry" 2>stderr &&
+	test_must_be_empty stderr &&
+	test_sparse_entry_unchanged &&
+	git ls-files --error-unmatch dense_entry
+'
+
+test_expect_success 'add obeys advice.updateSparsePath' '
+	setup_sparse_entry &&
+	test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr &&
+	test_i18ncmp sparse_entry_error stderr
+
+'
+
 test_done
-- 
2.30.1
Matheus Tavares· Mar 12, 2021, 22:48 UTC · re: Matheus Tavares · lore

[PATCH v3 7/7] rm: honor sparse checkout patterns

`git add` refrains from adding or updating index entries that are outside the current sparse checkout, but `git rm` doesn't follow the same restriction. This is somewhat counter-intuitive and inconsistent. So make `rm` honor the sparsity rules and advise on how to remove SKIP_WORKTREE entries just like `add` does. Also add some tests for the new behavior.

Suggested-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 Documentation/config/advice.txt  |  5 +-
 Documentation/git-rm.txt         |  4 +-
 builtin/rm.c                     | 35 +++++++++-----
 t/t3602-rm-sparse-checkout.sh    | 78 ++++++++++++++++++++++++++++++++
 t/t7011-skip-worktree-reading.sh |  5 --
 5 files changed, 108 insertions(+), 19 deletions(-)
 create mode 100755 t/t3602-rm-sparse-checkout.sh
Show changes to 5 files +108 −19

Documentation/config/advice.txt, Documentation/git-rm.txt, builtin/rm.c, t/t3602-rm-sparse-checkout.sh, t/t7011-skip-worktree-reading.sh

diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
index 851b83cf30..8b2849ff7b 100644
--- a/Documentation/config/advice.txt
+++ b/Documentation/config/advice.txt
@@ -120,6 +120,7 @@ advice.*::
 		Advice shown if a user runs the add command without providing
 		the pathspec parameter.
 	updateSparsePath::
-		Advice shown when linkgit:git-add[1] is asked to update index
-		entries outside the current sparse checkout.
+		Advice shown when either linkgit:git-add[1] or linkgit:git-rm[1]
+		is asked to update index entries outside the current sparse
+		checkout.
 --
diff --git a/Documentation/git-rm.txt b/Documentation/git-rm.txt
index ab750367fd..26e9b28470 100644
--- a/Documentation/git-rm.txt
+++ b/Documentation/git-rm.txt
@@ -23,7 +23,9 @@ branch, and no updates to their contents can be staged in the index,
 though that default behavior can be overridden with the `-f` option.
 When `--cached` is given, the staged content has to
 match either the tip of the branch or the file on disk,
-allowing the file to be removed from just the index.
+allowing the file to be removed from just the index. When
+sparse-checkouts are in use (see linkgit:git-sparse-checkout[1]),
+`git rm` will only remove paths within the sparse-checkout patterns.
 
 
 OPTIONS
diff --git a/builtin/rm.c b/builtin/rm.c
index 4858631e0f..d23a3b2164 100644
--- a/builtin/rm.c
+++ b/builtin/rm.c
@@ -5,6 +5,7 @@
  */
 #define USE_THE_INDEX_COMPATIBILITY_MACROS
 #include "builtin.h"
+#include "advice.h"
 #include "config.h"
 #include "lockfile.h"
 #include "dir.h"
@@ -254,7 +255,7 @@ static struct option builtin_rm_options[] = {
 int cmd_rm(int argc, const char **argv, const char *prefix)
 {
 	struct lock_file lock_file = LOCK_INIT;
-	int i;
+	int i, ret = 0;
 	struct pathspec pathspec;
 	char *seen;
 
@@ -295,6 +296,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
 
 	for (i = 0; i < active_nr; i++) {
 		const struct cache_entry *ce = active_cache[i];
+		if (ce_skip_worktree(ce))
+			continue;
 		if (!ce_path_match(&the_index, ce, &pathspec, seen))
 			continue;
 		ALLOC_GROW(list.entry, list.nr + 1, list.alloc);
@@ -308,24 +311,34 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
 	if (pathspec.nr) {
 		const char *original;
 		int seen_any = 0;
+		char *skip_worktree_seen = NULL;
+		struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
+
 		for (i = 0; i < pathspec.nr; i++) {
 			original = pathspec.items[i].original;
-			if (!seen[i]) {
-				if (!ignore_unmatch) {
-					die(_("pathspec '%s' did not match any files"),
-					    original);
-				}
-			}
-			else {
+			if (seen[i])
 				seen_any = 1;
-			}
+			else if (ignore_unmatch)
+				continue;
+			else if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen))
+				string_list_append(&only_match_skip_worktree, original);
+			else
+				die(_("pathspec '%s' did not match any files"), original);
+
 			if (!recursive && seen[i] == MATCHED_RECURSIVELY)
 				die(_("not removing '%s' recursively without -r"),
 				    *original ? original : ".");
 		}
 
+		if (only_match_skip_worktree.nr) {
+			advise_on_updating_sparse_paths(&only_match_skip_worktree);
+			ret = 1;
+		}
+		free(skip_worktree_seen);
+		string_list_clear(&only_match_skip_worktree, 0);
+
 		if (!seen_any)
-			exit(0);
+			exit(ret);
 	}
 
 	if (!index_only)
@@ -405,5 +418,5 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
 			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
 		die(_("Unable to write new index file"));
 
-	return 0;
+	return ret;
 }
diff --git a/t/t3602-rm-sparse-checkout.sh b/t/t3602-rm-sparse-checkout.sh
new file mode 100755
index 0000000000..c83b026ed8
--- /dev/null
+++ b/t/t3602-rm-sparse-checkout.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+test_description='git rm in sparse checked out working trees'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' "
+	mkdir -p sub/dir &&
+	touch a b c sub/d sub/dir/e &&
+	git add -A &&
+	git commit -m files &&
+
+	cat >sparse_error_header <<-EOF &&
+	The following pathspecs didn't match any eligible path, but they do match index
+	entries outside the current sparse checkout:
+	EOF
+
+	cat >sparse_hint <<-EOF &&
+	hint: Disable or modify the sparsity rules if you intend to update such entries.
+	hint: Disable this message with \"git config advice.updateSparsePath false\"
+	EOF
+
+	echo b | cat sparse_error_header - >sparse_entry_b_error &&
+	cat sparse_entry_b_error sparse_hint >b_error_and_hint
+"
+
+for opt in "" -f --dry-run
+do
+	test_expect_success "rm${opt:+ $opt} does not remove sparse entries" '
+		git sparse-checkout set a &&
+		test_must_fail git rm $opt b 2>stderr &&
+		test_i18ncmp b_error_and_hint stderr &&
+		git ls-files --error-unmatch b
+	'
+done
+
+test_expect_success 'recursive rm does not remove sparse entries' '
+	git reset --hard &&
+	git sparse-checkout set sub/dir &&
+	git rm -r sub &&
+	git status --porcelain -uno >actual &&
+	echo "D  sub/dir/e" >expected &&
+	test_cmp expected actual
+'
+
+test_expect_success 'rm obeys advice.updateSparsePath' '
+	git reset --hard &&
+	git sparse-checkout set a &&
+	test_must_fail git -c advice.updateSparsePath=false rm b 2>stderr &&
+	test_i18ncmp sparse_entry_b_error stderr
+'
+
+test_expect_success 'do not advice about sparse entries when they do not match the pathspec' '
+	git reset --hard &&
+	git sparse-checkout set a &&
+	test_must_fail git rm nonexistent 2>stderr &&
+	test_i18ngrep "fatal: pathspec .nonexistent. did not match any files" stderr &&
+	test_i18ngrep ! -F -f sparse_error_header stderr
+'
+
+test_expect_success 'do not warn about sparse entries when pathspec matches dense entries' '
+	git reset --hard &&
+	git sparse-checkout set a &&
+	git rm "[ba]" 2>stderr &&
+	test_must_be_empty stderr &&
+	git ls-files --error-unmatch b &&
+	test_must_fail git ls-files --error-unmatch a
+'
+
+test_expect_success 'do not warn about sparse entries with --ignore-unmatch' '
+	git reset --hard &&
+	git sparse-checkout set a &&
+	git rm --ignore-unmatch b 2>stderr &&
+	test_must_be_empty stderr &&
+	git ls-files --error-unmatch b
+'
+
+test_done
diff --git a/t/t7011-skip-worktree-reading.sh b/t/t7011-skip-worktree-reading.sh
index 26852586ac..1761a2b1b9 100755
--- a/t/t7011-skip-worktree-reading.sh
+++ b/t/t7011-skip-worktree-reading.sh
@@ -132,11 +132,6 @@ test_expect_success 'diff-files does not examine skip-worktree dirty entries' '
 	test -z "$(git diff-files -- one)"
 '
 
-test_expect_success 'git-rm succeeds on skip-worktree absent entries' '
-	setup_absent &&
-	git rm 1
-'
-
 test_expect_success 'commit on skip-worktree absent entries' '
 	git reset &&
 	setup_absent &&
-- 
2.30.1
Ævar Arnfjörð Bjarmason· Mar 21, 2021, 23:03 UTC · re: Matheus Tavares · lore

Re: [PATCH v3 7/7] rm: honor sparse checkout patterns

On Fri, Mar 12 2021, Matheus Tavares wrote:
Show 22 quoted lines
> +	test_expect_success "rm${opt:+ $opt} does not remove sparse entries" '
> +		git sparse-checkout set a &&
> +		test_must_fail git rm $opt b 2>stderr &&
> +		test_i18ncmp b_error_and_hint stderr &&
> +		git ls-files --error-unmatch b
> +	'
> +done
> +
> +test_expect_success 'recursive rm does not remove sparse entries' '
> +	git reset --hard &&
> +	git sparse-checkout set sub/dir &&
> +	git rm -r sub &&
> +	git status --porcelain -uno >actual &&
> +	echo "D  sub/dir/e" >expected &&
> +	test_cmp expected actual
> +'
> +
> +test_expect_success 'rm obeys advice.updateSparsePath' '
> +	git reset --hard &&
> +	git sparse-checkout set a &&
> +	test_must_fail git -c advice.updateSparsePath=false rm b 2>stderr &&
> +	test_i18ncmp sparse_entry_b_error stderr

Calls to the test_i18ncmp function should just be test_cmp now. See 1108cea7f8e (tests: remove most uses of test_i18ncmp, 2021-02-11).

Matheus Tavares Bernardino· Mar 22, 2021, 01:08 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v3 7/7] rm: honor sparse checkout patterns

On Sun, Mar 21, 2021 at 8:03 PM Ævar Arnfjörð Bjarmason <avarab@gmail.com> wrote:

Show 29 quoted lines
>
>
> On Fri, Mar 12 2021, Matheus Tavares wrote:
>
> > +     test_expect_success "rm${opt:+ $opt} does not remove sparse entries" '
> > +             git sparse-checkout set a &&
> > +             test_must_fail git rm $opt b 2>stderr &&
> > +             test_i18ncmp b_error_and_hint stderr &&
> > +             git ls-files --error-unmatch b
> > +     '
> > +done
> > +
> > +test_expect_success 'recursive rm does not remove sparse entries' '
> > +     git reset --hard &&
> > +     git sparse-checkout set sub/dir &&
> > +     git rm -r sub &&
> > +     git status --porcelain -uno >actual &&
> > +     echo "D  sub/dir/e" >expected &&
> > +     test_cmp expected actual
> > +'
> > +
> > +test_expect_success 'rm obeys advice.updateSparsePath' '
> > +     git reset --hard &&
> > +     git sparse-checkout set a &&
> > +     test_must_fail git -c advice.updateSparsePath=false rm b 2>stderr &&
> > +     test_i18ncmp sparse_entry_b_error stderr
>
> Calls to the test_i18ncmp function should just be test_cmp now. See
> 1108cea7f8e (tests: remove most uses of test_i18ncmp, 2021-02-11).
Thanks for letting me know, I'll fix it for the next version.
Derrick Stolee· Mar 23, 2021, 20:47 UTC · re: Matheus Tavares · lore

Re: [PATCH v3 7/7] rm: honor sparse checkout patterns

On 3/12/2021 5:48 PM, Matheus Tavares wrote:
Show 31 quoted lines
> `git add` refrains from adding or updating index entries that are
> outside the current sparse checkout, but `git rm` doesn't follow the
> same restriction. This is somewhat counter-intuitive and inconsistent.
> So make `rm` honor the sparsity rules and advise on how to remove
> SKIP_WORKTREE entries just like `add` does. Also add some tests for the
> new behavior.
> 
> Suggested-by: Elijah Newren <newren@gmail.com>
> Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
> ---
>  Documentation/config/advice.txt  |  5 +-
>  Documentation/git-rm.txt         |  4 +-
>  builtin/rm.c                     | 35 +++++++++-----
>  t/t3602-rm-sparse-checkout.sh    | 78 ++++++++++++++++++++++++++++++++
>  t/t7011-skip-worktree-reading.sh |  5 --
>  5 files changed, 108 insertions(+), 19 deletions(-)
>  create mode 100755 t/t3602-rm-sparse-checkout.sh
> 
> diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
> index 851b83cf30..8b2849ff7b 100644
> --- a/Documentation/config/advice.txt
> +++ b/Documentation/config/advice.txt
> @@ -120,6 +120,7 @@ advice.*::
>  		Advice shown if a user runs the add command without providing
>  		the pathspec parameter.
>  	updateSparsePath::
> -		Advice shown when linkgit:git-add[1] is asked to update index
> -		entries outside the current sparse checkout.
> +		Advice shown when either linkgit:git-add[1] or linkgit:git-rm[1]
> +		is asked to update index entries outside the current sparse
> +		checkout.

I wonder if this is going to need to be updated forever, or if in the _next_ iteration we just say "Advice shown when a command is asked to update..." (using the "three is many" rule).

Show 12 quoted lines
> diff --git a/Documentation/git-rm.txt b/Documentation/git-rm.txt
> index ab750367fd..26e9b28470 100644
> --- a/Documentation/git-rm.txt
> +++ b/Documentation/git-rm.txt
> @@ -23,7 +23,9 @@ branch, and no updates to their contents can be staged in the index,
>  though that default behavior can be overridden with the `-f` option.
>  When `--cached` is given, the staged content has to
>  match either the tip of the branch or the file on disk,
> -allowing the file to be removed from just the index.
> +allowing the file to be removed from just the index. When
> +sparse-checkouts are in use (see linkgit:git-sparse-checkout[1]),
> +`git rm` will only remove paths within the sparse-checkout patterns.
Good to update in the correct spot.
Implementation and tests look good, too.

I just wanted to chime in and voice my support of this general direction. Thank you!

-Stolee
Elijah Newren· Mar 13, 2021, 07:07 UTC · re: Matheus Tavares · lore

Re: [PATCH v3 0/7] add/rm: honor sparse checkout and warn on sparse paths

On Fri, Mar 12, 2021 at 2:48 PM Matheus Tavares <matheus.bernardino@usp.br> wrote:

Show 26 quoted lines
>
> Make `rm` honor sparse checkouts, and make both `rm` and `add` warn
> when they are asked to update sparse entries.
>
> Main changes since v2:
>
> Patch 2:
> - Removed unnecessary `rm` in t3705's auxiliary function and added
>   comment about why `update-index --force-remove` is used there.
>
> Patch 4:
> - Replaced the boolean function parameter with a more descriptive enum.
>   This way, readers don't have to go to the function definition to
>   understand what the caller wants.
>
> Patches 5 and 6:
> - Reworded both commit messages and the advice.updateSparsePath docs to
>   avoid saying "pathspecs that *only* match sparse entries", as the
>   situations to which they refer might also include matches among
>   ignored paths.
> - Rephrased the warning message about sparse paths so that it doesn't
>   contradict the message on ignored paths when they are displayed
>   together.
>
> Patch 7:
> - Adjusted the tests to check for the new sparse warning message.

v2 already looked pretty good to me; even if I still had lots of little comments on that series. This version addresses all my remaining concerns with v2. So this gets my:

Reviewed-by: Elijah Newren <newren@gmail.com>
Thanks for all your work on this!
Matheus Tavares· Apr 8, 2021, 20:41 UTC · re: Matheus Tavares · lore

[PATCH v4 0/7] add/rm: honor sparse checkout and warn on sparse paths

Make `rm` honor sparse checkouts, and make both `rm` and `add` warn when asked to update sparse entries.

There are two changes since v3:
- `test_i18ncmp` and `test_i18ngrep` were replaced by `test_cmp` and
  `grep`
- The flag added in patch 5 now makes refresh_index() completely ignore
  skip_worktree entries, instead of just suppressing their matches on
  the seen[] array. The previous implementation was not necessarily
  wrong but, as Junio pointed out, it was rather odd to keep matching
  the entries if we no longer want to use the matches.
  As "side effects", the new version of the flag also makes
  refresh_index() refrain from both:
  (1) checking and warning if skip_worktree entries matching the given
  pathspec are unmerged.
  (2) marking skip_worktree entries matching the given pathspec with
  CE_UPTODATE.
  The change (1) is actually interesting because `git add` doesn't
  update skip_worktree entries, and thus, it doesn't make much sense to
  warn if they are unmerged. Besides, we will already warn if the user
  requests to update such entries, anyway. And finally, unmerged
  entries should not have the skip_worktree bit set in the first place.
  (`git merge` should clean this bit when writing the new index, and
  neither `git sparse-checkout` nor `git update-index` allow to set the
  bit on an unmerged entry.)
  Change (2) is perhaps not very beneficial, but it is also not harmful.
  The only practical difference we get by not setting the CE_UPTODATE
  flag in the skip_worktree entries is that, when writing a new index at
  the end of `git add --refresh`, do_write_index() will start checking
  if these entries are racy clean. Note that it already does that for
  all the skip_worktree entries that do not match the user-given
  pathspecs. And, in fact, this behavior distinction based on the
  pathspec only happens with `--refresh`. Plain `git add` and other
  options don't mark any skip_worktree entry with CE_UPTODATE
  (regardless of the pathspecs) and thus, all these entries are checked
  when writing the index. So `git add --refresh` will only do what the
  other options already do. 
  (Additionally, as I mentioned in [1], there might actually be at least
  one advantage in checking if the skip_worktree entries are racy clean.
  But this is a very specific case, and it's probably a topic for a
  another thread :)
[1]: https://lore.kernel.org/git/CAHd-oW4kRLjV9Sq3CFt-V1Ot9pYFzJggU1zPp3Hcuw=qWfq7Mg@mail.gmail.com/
Matheus Tavares (7):
  add: include magic part of pathspec on --refresh error
  t3705: add tests for `git add` in sparse checkouts
  add: make --chmod and --renormalize honor sparse checkouts
  pathspec: allow to ignore SKIP_WORKTREE entries on index matching
  refresh_index(): add flag to ignore SKIP_WORKTREE entries
  add: warn when asked to update SKIP_WORKTREE entries
  rm: honor sparse checkout patterns
 Documentation/config/advice.txt  |   4 +
 Documentation/git-rm.txt         |   4 +-
 advice.c                         |  20 ++++
 advice.h                         |   4 +
 builtin/add.c                    |  77 ++++++++++++---
 builtin/check-ignore.c           |   3 +-
 builtin/rm.c                     |  35 ++++---
 cache.h                          |  15 +--
 pathspec.c                       |  25 ++++-
 pathspec.h                       |  18 +++-
 read-cache.c                     |   3 +
 t/t3602-rm-sparse-checkout.sh    |  78 ++++++++++++++++
 t/t3700-add.sh                   |   6 ++
 t/t3705-add-sparse-checkout.sh   | 155 +++++++++++++++++++++++++++++++
 t/t7011-skip-worktree-reading.sh |   5 -
 t/t7012-skip-worktree-writing.sh |  19 ----
 16 files changed, 408 insertions(+), 63 deletions(-)
 create mode 100755 t/t3602-rm-sparse-checkout.sh
 create mode 100755 t/t3705-add-sparse-checkout.sh
Range-diff against v3:
1:  216e3e1af0 ! 1:  65f241b5c7 add: include magic part of pathspec on --refresh error
    @@ t/t3700-add.sh: test_expect_success 'git add --refresh with pathspec' '
     +test_expect_success 'git add --refresh correctly reports no match error' "
     +	echo \"fatal: pathspec ':(icase)nonexistent' did not match any files\" >expect &&
     +	test_must_fail git add --refresh ':(icase)nonexistent' 2>actual &&
    -+	test_i18ncmp expect actual
    ++	test_cmp expect actual
     +"
     +
      test_expect_success POSIXPERM,SANITY 'git add should fail atomically upon an unreadable file' '
2:  fd250e29b3 = 2:  5d0cdb3718 t3705: add tests for `git add` in sparse checkouts
3:  3ab45d387a = 3:  565c7d3460 add: make --chmod and --renormalize honor sparse checkouts
4:  df4509268a = 4:  a6e98b17eb pathspec: allow to ignore SKIP_WORKTREE entries on index matching
5:  ceb6b508a2 ! 5:  4330f50930 refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag
    @@ Metadata
     Author: Matheus Tavares <matheus.bernardino@usp.br>
     
      ## Commit message ##
    -    refresh_index(): add REFRESH_DONT_MARK_SPARSE_MATCHES flag
    +    refresh_index(): add flag to ignore SKIP_WORKTREE entries
     
    -    refresh_index() optionally takes a seen[] array to mark the pathspec
    -    items that had matches in the index. This is used by `git add --refresh`
    -    to find out if there was any pathspec without matches, and display an
    -    error accordingly.
    -
    -    In the following patch, `git add` will also learn to warn about
    -    pathspecs that match no eligible path for updating, but do match sparse
    -    entries. For that, we will need a seen[] array marked exclusively with
    -    matches from dense entries. To avoid having to call ce_path_match()
    -    again for these entries after refresh_index() returns, add a flag that
    -    implements this restriction inside the function itself.
    -
    -    Note that refresh_index() does not update sparse entries, regardless of
    -    passing the flag or not. The flag only controls whether matches with
    -    these entries should appear in the seen[] array.
    +    refresh_index() doesn't update SKIP_WORKTREE entries, but it still
    +    matches them against the given pathspecs, marks the matches on the
    +    seen[] array, check if unmerged, etc. In the following patch, one caller
    +    will need refresh_index() to ignore SKIP_WORKTREE entries entirely, so
    +    add a flag that implements this behavior.
     
         While we are here, also realign the REFRESH_* flags and convert the hex
         values to the more natural bit shift format, which makes it easier to
    @@ cache.h: int match_stat_data_racy(const struct index_state *istate,
     +#define REFRESH_IGNORE_SUBMODULES        (1 << 4) /* ignore submodules */
     +#define REFRESH_IN_PORCELAIN             (1 << 5) /* user friendly output, not "needs update" */
     +#define REFRESH_PROGRESS                 (1 << 6) /* show progress bar if stderr is tty */
    -+#define REFRESH_DONT_MARK_SPARSE_MATCHES (1 << 7) /* don't mark sparse entries' matches on seen[] */
    ++#define REFRESH_IGNORE_SKIP_WORKTREE     (1 << 7) /* ignore skip_worktree entries */
      int refresh_index(struct index_state *, unsigned int flags, const struct pathspec *pathspec, char *seen, const char *header_msg);
      /*
       * Refresh the index and write it to disk.
    @@ read-cache.c: int refresh_index(struct index_state *istate, unsigned int flags,
      	int quiet = (flags & REFRESH_QUIET) != 0;
      	int not_new = (flags & REFRESH_IGNORE_MISSING) != 0;
      	int ignore_submodules = (flags & REFRESH_IGNORE_SUBMODULES) != 0;
    -+	int no_sparse_on_seen = (flags & REFRESH_DONT_MARK_SPARSE_MATCHES) != 0;
    ++	int ignore_skip_worktree = (flags & REFRESH_IGNORE_SKIP_WORKTREE) != 0;
      	int first = 1;
      	int in_porcelain = (flags & REFRESH_IN_PORCELAIN);
      	unsigned int options = (CE_MATCH_REFRESH |
     @@ read-cache.c: int refresh_index(struct index_state *istate, unsigned int flags,
    - 		int filtered = 0;
    - 		int t2_did_lstat = 0;
    - 		int t2_did_scan = 0;
    -+		char *cur_seen;
    - 
      		ce = istate->cache[i];
      		if (ignore_submodules && S_ISGITLINK(ce->ce_mode))
      			continue;
    ++		if (ignore_skip_worktree && ce_skip_worktree(ce))
    ++			continue;
      
    --		if (pathspec && !ce_path_match(istate, ce, pathspec, seen))
    -+		cur_seen = no_sparse_on_seen && ce_skip_worktree(ce) ? NULL : seen;
    -+		if (pathspec && !ce_path_match(istate, ce, pathspec, cur_seen))
    + 		if (pathspec && !ce_path_match(istate, ce, pathspec, seen))
      			filtered = 1;
    - 
    - 		if (ce_stage(ce)) {
6:  048b406fa3 ! 6:  da9cd3e9f6 add: warn when asked to update SKIP_WORKTREE entries
    @@ builtin/add.c: static char *prune_directory(struct dir_struct *dir, struct paths
     +	int i, ret = 0;
     +	char *skip_worktree_seen = NULL;
     +	struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
    -+	int flags = REFRESH_DONT_MARK_SPARSE_MATCHES |
    ++	int flags = REFRESH_IGNORE_SKIP_WORKTREE |
     +		    (verbose ? REFRESH_IN_PORCELAIN : REFRESH_QUIET);
      
      	seen = xcalloc(pathspec->nr, 1);
    @@ t/t3705-add-sparse-checkout.sh: setup_gitignore () {
      	rm sparse_entry &&
     -	git add sparse_entry &&
     +	test_must_fail git add sparse_entry 2>stderr &&
    -+	test_i18ncmp error_and_hint stderr &&
    ++	test_cmp error_and_hint stderr &&
      	test_sparse_entry_unchanged
      '
      
    @@ t/t3705-add-sparse-checkout.sh: test_expect_success 'git add . does not remove s
     +	echo . >>expect &&
     +	cat sparse_hint >>expect &&
     +
    -+	test_i18ncmp expect stderr &&
    ++	test_cmp expect stderr &&
      	test_sparse_entry_unchanged
      '
      
    @@ t/t3705-add-sparse-checkout.sh: do
      		echo modified >sparse_entry &&
     -		git add $opt sparse_entry &&
     +		test_must_fail git add $opt sparse_entry 2>stderr &&
    -+		test_i18ncmp error_and_hint stderr &&
    ++		test_cmp error_and_hint stderr &&
      		test_sparse_entry_unchanged
      	'
      done
    @@ t/t3705-add-sparse-checkout.sh: test_expect_success 'git add --refresh does not
      	test-tool chmtime -60 sparse_entry &&
     -	git add --refresh sparse_entry &&
     +	test_must_fail git add --refresh sparse_entry 2>stderr &&
    -+	test_i18ncmp error_and_hint stderr &&
    ++	test_cmp error_and_hint stderr &&
      	git ls-files --debug sparse_entry | grep mtime >after &&
      	test_cmp before after
      '
    @@ t/t3705-add-sparse-checkout.sh: test_expect_success 'git add --refresh does not
      	setup_sparse_entry &&
     -	git add --chmod=+x sparse_entry &&
     +	test_must_fail git add --chmod=+x sparse_entry 2>stderr &&
    -+	test_i18ncmp error_and_hint stderr &&
    ++	test_cmp error_and_hint stderr &&
      	test_sparse_entry_unchanged &&
      	! test -x sparse_entry
      '
    @@ t/t3705-add-sparse-checkout.sh: test_expect_success 'git add --renormalize does
      	echo "sparse_entry text=auto" >.gitattributes &&
     -	git add --renormalize sparse_entry &&
     +	test_must_fail git add --renormalize sparse_entry 2>stderr &&
    -+	test_i18ncmp error_and_hint stderr &&
    ++	test_cmp error_and_hint stderr &&
     +	test_sparse_entry_unchanged
     +'
     +
    @@ t/t3705-add-sparse-checkout.sh: test_expect_success 'git add --renormalize does
     +	setup_sparse_entry &&
     +	rm sparse_entry &&
     +	test_must_fail git add --dry-run --ignore-missing sparse_entry 2>stderr &&
    -+	test_i18ncmp error_and_hint stderr &&
    ++	test_cmp error_and_hint stderr &&
      	test_sparse_entry_unchanged
      '
      
     +test_expect_success 'do not advice about sparse entries when they do not match the pathspec' '
     +	setup_sparse_entry &&
     +	test_must_fail git add nonexistent 2>stderr &&
    -+	test_i18ngrep "fatal: pathspec .nonexistent. did not match any files" stderr &&
    -+	test_i18ngrep ! -F -f sparse_error_header stderr
    ++	grep "fatal: pathspec .nonexistent. did not match any files" stderr &&
    ++	! grep -F -f sparse_error_header stderr
     +'
     +
     +test_expect_success 'do not warn when pathspec matches dense entries' '
    @@ t/t3705-add-sparse-checkout.sh: test_expect_success 'git add --renormalize does
     +test_expect_success 'add obeys advice.updateSparsePath' '
     +	setup_sparse_entry &&
     +	test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr &&
    -+	test_i18ncmp sparse_entry_error stderr
    ++	test_cmp sparse_entry_error stderr
     +
     +'
     +
7:  dd0fe93eb7 ! 7:  4459976ae9 rm: honor sparse checkout patterns
    @@ t/t3602-rm-sparse-checkout.sh (new)
     +	test_expect_success "rm${opt:+ $opt} does not remove sparse entries" '
     +		git sparse-checkout set a &&
     +		test_must_fail git rm $opt b 2>stderr &&
    -+		test_i18ncmp b_error_and_hint stderr &&
    ++		test_cmp b_error_and_hint stderr &&
     +		git ls-files --error-unmatch b
     +	'
     +done
    @@ t/t3602-rm-sparse-checkout.sh (new)
     +	git reset --hard &&
     +	git sparse-checkout set a &&
     +	test_must_fail git -c advice.updateSparsePath=false rm b 2>stderr &&
    -+	test_i18ncmp sparse_entry_b_error stderr
    ++	test_cmp sparse_entry_b_error stderr
     +'
     +
     +test_expect_success 'do not advice about sparse entries when they do not match the pathspec' '
     +	git reset --hard &&
     +	git sparse-checkout set a &&
     +	test_must_fail git rm nonexistent 2>stderr &&
    -+	test_i18ngrep "fatal: pathspec .nonexistent. did not match any files" stderr &&
    -+	test_i18ngrep ! -F -f sparse_error_header stderr
    ++	grep "fatal: pathspec .nonexistent. did not match any files" stderr &&
    ++	! grep -F -f sparse_error_header stderr
     +'
     +
     +test_expect_success 'do not warn about sparse entries when pathspec matches dense entries' '
-- 
2.30.1
Matheus Tavares· Apr 8, 2021, 20:41 UTC · re: Matheus Tavares · lore

[PATCH v4 1/7] add: include magic part of pathspec on --refresh error

When `git add --refresh <pathspec>` doesn't find any matches for the given pathspec, it prints an error message using the `match` field of the `struct pathspec_item`. However, this field doesn't contain the magic part of the pathspec. Instead, let's use the `original` field.

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 builtin/add.c  | 2 +-
 t/t3700-add.sh | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)
Show changes to 2 files +7 −1

builtin/add.c, t/t3700-add.sh

diff --git a/builtin/add.c b/builtin/add.c
index ea762a41e3..24ed7e25f3 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -187,7 +187,7 @@ static void refresh(int verbose, const struct pathspec *pathspec)
 	for (i = 0; i < pathspec->nr; i++) {
 		if (!seen[i])
 			die(_("pathspec '%s' did not match any files"),
-			    pathspec->items[i].match);
+			    pathspec->items[i].original);
 	}
 	free(seen);
 }
diff --git a/t/t3700-add.sh b/t/t3700-add.sh
index b3b122ff97..dd3011430d 100755
--- a/t/t3700-add.sh
+++ b/t/t3700-add.sh
@@ -196,6 +196,12 @@ test_expect_success 'git add --refresh with pathspec' '
 	grep baz actual
 '
 
+test_expect_success 'git add --refresh correctly reports no match error' "
+	echo \"fatal: pathspec ':(icase)nonexistent' did not match any files\" >expect &&
+	test_must_fail git add --refresh ':(icase)nonexistent' 2>actual &&
+	test_cmp expect actual
+"
+
 test_expect_success POSIXPERM,SANITY 'git add should fail atomically upon an unreadable file' '
 	git reset --hard &&
 	date >foo1 &&
-- 
2.30.1
Matheus Tavares· Apr 8, 2021, 20:41 UTC · re: Matheus Tavares · lore

[PATCH v4 2/7] t3705: add tests for `git add` in sparse checkouts

We already have a couple tests for `add` with SKIP_WORKTREE entries in t7012, but these only cover the most basic scenarios. As we will be changing how `add` deals with sparse paths in the subsequent commits, let's move these two tests to their own file and add more test cases for different `add` options and situations. This also demonstrates two options that don't currently respect SKIP_WORKTREE entries: `--chmod` and `--renormalize`.

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 t/t3705-add-sparse-checkout.sh   | 96 ++++++++++++++++++++++++++++++++
 t/t7012-skip-worktree-writing.sh | 19 -------
 2 files changed, 96 insertions(+), 19 deletions(-)
 create mode 100755 t/t3705-add-sparse-checkout.sh
Show changes to 2 files +96 −19

t/t3705-add-sparse-checkout.sh, t/t7012-skip-worktree-writing.sh

diff --git a/t/t3705-add-sparse-checkout.sh b/t/t3705-add-sparse-checkout.sh
new file mode 100755
index 0000000000..6c5b8be863
--- /dev/null
+++ b/t/t3705-add-sparse-checkout.sh
@@ -0,0 +1,96 @@
+#!/bin/sh
+
+test_description='git add in sparse checked out working trees'
+
+. ./test-lib.sh
+
+SPARSE_ENTRY_BLOB=""
+
+# Optionally take a printf format string to write to the sparse_entry file
+setup_sparse_entry () {
+	# 'sparse_entry' might already be in the index with the skip-worktree
+	# bit set. Remove it so that the subsequent git add can update it.
+	git update-index --force-remove sparse_entry &&
+	if test $# -eq 1
+	then
+		printf "$1" >sparse_entry
+	else
+		>sparse_entry
+	fi &&
+	git add sparse_entry &&
+	git update-index --skip-worktree sparse_entry &&
+	SPARSE_ENTRY_BLOB=$(git rev-parse :sparse_entry)
+}
+
+test_sparse_entry_unchanged () {
+	echo "100644 $SPARSE_ENTRY_BLOB 0	sparse_entry" >expected &&
+	git ls-files --stage sparse_entry >actual &&
+	test_cmp expected actual
+}
+
+setup_gitignore () {
+	test_when_finished rm -f .gitignore &&
+	cat >.gitignore <<-EOF
+	*
+	!/sparse_entry
+	EOF
+}
+
+test_expect_success 'git add does not remove sparse entries' '
+	setup_sparse_entry &&
+	rm sparse_entry &&
+	git add sparse_entry &&
+	test_sparse_entry_unchanged
+'
+
+test_expect_success 'git add -A does not remove sparse entries' '
+	setup_sparse_entry &&
+	rm sparse_entry &&
+	setup_gitignore &&
+	git add -A &&
+	test_sparse_entry_unchanged
+'
+
+test_expect_success 'git add . does not remove sparse entries' '
+	setup_sparse_entry &&
+	rm sparse_entry &&
+	setup_gitignore &&
+	git add . &&
+	test_sparse_entry_unchanged
+'
+
+for opt in "" -f -u --ignore-removal --dry-run
+do
+	test_expect_success "git add${opt:+ $opt} does not update sparse entries" '
+		setup_sparse_entry &&
+		echo modified >sparse_entry &&
+		git add $opt sparse_entry &&
+		test_sparse_entry_unchanged
+	'
+done
+
+test_expect_success 'git add --refresh does not update sparse entries' '
+	setup_sparse_entry &&
+	git ls-files --debug sparse_entry | grep mtime >before &&
+	test-tool chmtime -60 sparse_entry &&
+	git add --refresh sparse_entry &&
+	git ls-files --debug sparse_entry | grep mtime >after &&
+	test_cmp before after
+'
+
+test_expect_failure 'git add --chmod does not update sparse entries' '
+	setup_sparse_entry &&
+	git add --chmod=+x sparse_entry &&
+	test_sparse_entry_unchanged &&
+	! test -x sparse_entry
+'
+
+test_expect_failure 'git add --renormalize does not update sparse entries' '
+	test_config core.autocrlf false &&
+	setup_sparse_entry "LINEONE\r\nLINETWO\r\n" &&
+	echo "sparse_entry text=auto" >.gitattributes &&
+	git add --renormalize sparse_entry &&
+	test_sparse_entry_unchanged
+'
+
+test_done
diff --git a/t/t7012-skip-worktree-writing.sh b/t/t7012-skip-worktree-writing.sh
index f2a8e76511..a1080b94e3 100755
--- a/t/t7012-skip-worktree-writing.sh
+++ b/t/t7012-skip-worktree-writing.sh
@@ -60,13 +60,6 @@ setup_absent() {
 	git update-index --skip-worktree 1
 }
 
-test_absent() {
-	echo "100644 $EMPTY_BLOB 0	1" > expected &&
-	git ls-files --stage 1 > result &&
-	test_cmp expected result &&
-	test ! -f 1
-}
-
 setup_dirty() {
 	git update-index --force-remove 1 &&
 	echo dirty > 1 &&
@@ -100,18 +93,6 @@ test_expect_success 'index setup' '
 	test_cmp expected result
 '
 
-test_expect_success 'git-add ignores worktree content' '
-	setup_absent &&
-	git add 1 &&
-	test_absent
-'
-
-test_expect_success 'git-add ignores worktree content' '
-	setup_dirty &&
-	git add 1 &&
-	test_dirty
-'
-
 test_expect_success 'git-rm fails if worktree is dirty' '
 	setup_dirty &&
 	test_must_fail git rm 1 &&
-- 
2.30.1
Derrick Stolee· Apr 14, 2021, 16:39 UTC · re: Matheus Tavares · lore

Re: [PATCH v4 2/7] t3705: add tests for `git add` in sparse checkouts

On 4/8/2021 4:41 PM, Matheus Tavares wrote:
Show 7 quoted lines
> We already have a couple tests for `add` with SKIP_WORKTREE entries in
> t7012, but these only cover the most basic scenarios. As we will be
> changing how `add` deals with sparse paths in the subsequent commits,
> let's move these two tests to their own file and add more test cases
> for different `add` options and situations. This also demonstrates two
> options that don't currently respect SKIP_WORKTREE entries: `--chmod`
> and `--renormalize`.

These are good tests. I will adapt these for my sparse-index series to ensure 'git add' works correctly in that environment.

Thanks, -Stolee

Matheus Tavares· Apr 8, 2021, 20:41 UTC · re: Matheus Tavares · lore

[PATCH v4 3/7] add: make --chmod and --renormalize honor sparse checkouts

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 builtin/add.c                  | 5 +++++
 t/t3705-add-sparse-checkout.sh | 4 ++--
 2 files changed, 7 insertions(+), 2 deletions(-)
Show changes to 2 files +7 −2

builtin/add.c, t/t3705-add-sparse-checkout.sh

diff --git a/builtin/add.c b/builtin/add.c
index 24ed7e25f3..5fec21a792 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -46,6 +46,9 @@ static int chmod_pathspec(struct pathspec *pathspec, char flip, int show_only)
 		struct cache_entry *ce = active_cache[i];
 		int err;
 
+		if (ce_skip_worktree(ce))
+			continue;
+
 		if (pathspec && !ce_path_match(&the_index, ce, pathspec, NULL))
 			continue;
 
@@ -144,6 +147,8 @@ static int renormalize_tracked_files(const struct pathspec *pathspec, int flags)
 	for (i = 0; i < active_nr; i++) {
 		struct cache_entry *ce = active_cache[i];
 
+		if (ce_skip_worktree(ce))
+			continue;
 		if (ce_stage(ce))
 			continue; /* do not touch unmerged paths */
 		if (!S_ISREG(ce->ce_mode) && !S_ISLNK(ce->ce_mode))
diff --git a/t/t3705-add-sparse-checkout.sh b/t/t3705-add-sparse-checkout.sh
index 6c5b8be863..00b10ac877 100755
--- a/t/t3705-add-sparse-checkout.sh
+++ b/t/t3705-add-sparse-checkout.sh
@@ -78,14 +78,14 @@ test_expect_success 'git add --refresh does not update sparse entries' '
 	test_cmp before after
 '
 
-test_expect_failure 'git add --chmod does not update sparse entries' '
+test_expect_success 'git add --chmod does not update sparse entries' '
 	setup_sparse_entry &&
 	git add --chmod=+x sparse_entry &&
 	test_sparse_entry_unchanged &&
 	! test -x sparse_entry
 '
 
-test_expect_failure 'git add --renormalize does not update sparse entries' '
+test_expect_success 'git add --renormalize does not update sparse entries' '
 	test_config core.autocrlf false &&
 	setup_sparse_entry "LINEONE\r\nLINETWO\r\n" &&
 	echo "sparse_entry text=auto" >.gitattributes &&
-- 
2.30.1
Matheus Tavares· Apr 8, 2021, 20:41 UTC · re: Matheus Tavares · lore

[PATCH v4 4/7] pathspec: allow to ignore SKIP_WORKTREE entries on index matching

Add a new enum parameter to `add_pathspec_matches_against_index()` and `find_pathspecs_matching_against_index()`, allowing callers to specify whether these function should attempt to match SKIP_WORKTREE entries or not. This will be used in a future patch to make `git add` display a warning when it is asked to update SKIP_WORKTREE entries.

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 builtin/add.c          |  6 ++++--
 builtin/check-ignore.c |  3 ++-
 pathspec.c             | 10 +++++++---
 pathspec.h             | 10 ++++++++--
 4 files changed, 21 insertions(+), 8 deletions(-)
Show changes to 4 files +21 −8

builtin/add.c, builtin/check-ignore.c, pathspec.c, pathspec.h

diff --git a/builtin/add.c b/builtin/add.c
index 5fec21a792..050cb8af30 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -177,7 +177,8 @@ static char *prune_directory(struct dir_struct *dir, struct pathspec *pathspec,
 			*dst++ = entry;
 	}
 	dir->nr = dst - dir->entries;
-	add_pathspec_matches_against_index(pathspec, &the_index, seen);
+	add_pathspec_matches_against_index(pathspec, &the_index, seen,
+					   PS_HEED_SKIP_WORKTREE);
 	return seen;
 }
 
@@ -578,7 +579,8 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 		int i;
 
 		if (!seen)
-			seen = find_pathspecs_matching_against_index(&pathspec, &the_index);
+			seen = find_pathspecs_matching_against_index(&pathspec,
+					&the_index, PS_HEED_SKIP_WORKTREE);
 
 		/*
 		 * file_exists() assumes exact match
diff --git a/builtin/check-ignore.c b/builtin/check-ignore.c
index 3c652748d5..0f4480a11b 100644
--- a/builtin/check-ignore.c
+++ b/builtin/check-ignore.c
@@ -100,7 +100,8 @@ static int check_ignore(struct dir_struct *dir,
 	 * should not be ignored, in order to be consistent with
 	 * 'git status', 'git add' etc.
 	 */
-	seen = find_pathspecs_matching_against_index(&pathspec, &the_index);
+	seen = find_pathspecs_matching_against_index(&pathspec, &the_index,
+						     PS_HEED_SKIP_WORKTREE);
 	for (i = 0; i < pathspec.nr; i++) {
 		full_path = pathspec.items[i].match;
 		pattern = NULL;
diff --git a/pathspec.c b/pathspec.c
index 18b3be362a..8b05575110 100644
--- a/pathspec.c
+++ b/pathspec.c
@@ -21,7 +21,8 @@
  */
 void add_pathspec_matches_against_index(const struct pathspec *pathspec,
 					const struct index_state *istate,
-					char *seen)
+					char *seen,
+					enum ps_skip_worktree_action sw_action)
 {
 	int num_unmatched = 0, i;
 
@@ -38,6 +39,8 @@ void add_pathspec_matches_against_index(const struct pathspec *pathspec,
 		return;
 	for (i = 0; i < istate->cache_nr; i++) {
 		const struct cache_entry *ce = istate->cache[i];
+		if (sw_action == PS_IGNORE_SKIP_WORKTREE && ce_skip_worktree(ce))
+			continue;
 		ce_path_match(istate, ce, pathspec, seen);
 	}
 }
@@ -51,10 +54,11 @@ void add_pathspec_matches_against_index(const struct pathspec *pathspec,
  * given pathspecs achieves against all items in the index.
  */
 char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
-					    const struct index_state *istate)
+					    const struct index_state *istate,
+					    enum ps_skip_worktree_action sw_action)
 {
 	char *seen = xcalloc(pathspec->nr, 1);
-	add_pathspec_matches_against_index(pathspec, istate, seen);
+	add_pathspec_matches_against_index(pathspec, istate, seen, sw_action);
 	return seen;
 }
 
diff --git a/pathspec.h b/pathspec.h
index 454ce364fa..0feb8e9f67 100644
--- a/pathspec.h
+++ b/pathspec.h
@@ -149,11 +149,17 @@ static inline int ps_strcmp(const struct pathspec_item *item,
 		return strcmp(s1, s2);
 }
 
+enum ps_skip_worktree_action {
+  PS_HEED_SKIP_WORKTREE = 0,
+  PS_IGNORE_SKIP_WORKTREE = 1
+};
 void add_pathspec_matches_against_index(const struct pathspec *pathspec,
 					const struct index_state *istate,
-					char *seen);
+					char *seen,
+					enum ps_skip_worktree_action sw_action);
 char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
-					    const struct index_state *istate);
+					    const struct index_state *istate,
+					    enum ps_skip_worktree_action sw_action);
 int match_pathspec_attrs(const struct index_state *istate,
 			 const char *name, int namelen,
 			 const struct pathspec_item *item);
-- 
2.30.1
Matheus Tavares· Apr 8, 2021, 20:41 UTC · re: Matheus Tavares · lore

[PATCH v4 5/7] refresh_index(): add flag to ignore SKIP_WORKTREE entries

refresh_index() doesn't update SKIP_WORKTREE entries, but it still matches them against the given pathspecs, marks the matches on the seen[] array, check if unmerged, etc. In the following patch, one caller will need refresh_index() to ignore SKIP_WORKTREE entries entirely, so add a flag that implements this behavior.

While we are here, also realign the REFRESH_* flags and convert the hex values to the more natural bit shift format, which makes it easier to spot holes.

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 cache.h      | 15 ++++++++-------
 read-cache.c |  3 +++
 2 files changed, 11 insertions(+), 7 deletions(-)
Show changes to 2 files +11 −7

cache.h, read-cache.c

diff --git a/cache.h b/cache.h
index 148d9ab5f1..19c03fb6e7 100644
--- a/cache.h
+++ b/cache.h
@@ -879,13 +879,14 @@ int match_stat_data_racy(const struct index_state *istate,
 
 void fill_stat_cache_info(struct index_state *istate, struct cache_entry *ce, struct stat *st);
 
-#define REFRESH_REALLY		0x0001	/* ignore_valid */
-#define REFRESH_UNMERGED	0x0002	/* allow unmerged */
-#define REFRESH_QUIET		0x0004	/* be quiet about it */
-#define REFRESH_IGNORE_MISSING	0x0008	/* ignore non-existent */
-#define REFRESH_IGNORE_SUBMODULES	0x0010	/* ignore submodules */
-#define REFRESH_IN_PORCELAIN	0x0020	/* user friendly output, not "needs update" */
-#define REFRESH_PROGRESS	0x0040  /* show progress bar if stderr is tty */
+#define REFRESH_REALLY                   (1 << 0) /* ignore_valid */
+#define REFRESH_UNMERGED                 (1 << 1) /* allow unmerged */
+#define REFRESH_QUIET                    (1 << 2) /* be quiet about it */
+#define REFRESH_IGNORE_MISSING           (1 << 3) /* ignore non-existent */
+#define REFRESH_IGNORE_SUBMODULES        (1 << 4) /* ignore submodules */
+#define REFRESH_IN_PORCELAIN             (1 << 5) /* user friendly output, not "needs update" */
+#define REFRESH_PROGRESS                 (1 << 6) /* show progress bar if stderr is tty */
+#define REFRESH_IGNORE_SKIP_WORKTREE     (1 << 7) /* ignore skip_worktree entries */
 int refresh_index(struct index_state *, unsigned int flags, const struct pathspec *pathspec, char *seen, const char *header_msg);
 /*
  * Refresh the index and write it to disk.
diff --git a/read-cache.c b/read-cache.c
index 5a907af2fb..8381935b84 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1514,6 +1514,7 @@ int refresh_index(struct index_state *istate, unsigned int flags,
 	int quiet = (flags & REFRESH_QUIET) != 0;
 	int not_new = (flags & REFRESH_IGNORE_MISSING) != 0;
 	int ignore_submodules = (flags & REFRESH_IGNORE_SUBMODULES) != 0;
+	int ignore_skip_worktree = (flags & REFRESH_IGNORE_SKIP_WORKTREE) != 0;
 	int first = 1;
 	int in_porcelain = (flags & REFRESH_IN_PORCELAIN);
 	unsigned int options = (CE_MATCH_REFRESH |
@@ -1556,6 +1557,8 @@ int refresh_index(struct index_state *istate, unsigned int flags,
 		ce = istate->cache[i];
 		if (ignore_submodules && S_ISGITLINK(ce->ce_mode))
 			continue;
+		if (ignore_skip_worktree && ce_skip_worktree(ce))
+			continue;
 
 		if (pathspec && !ce_path_match(istate, ce, pathspec, seen))
 			filtered = 1;
-- 
2.30.1
Matheus Tavares· Apr 8, 2021, 20:41 UTC · re: Matheus Tavares · lore

[PATCH v4 6/7] add: warn when asked to update SKIP_WORKTREE entries

`git add` already refrains from updating SKIP_WORKTREE entries, but it silently exits with zero code when it is asked to do so. Instead, let's warn the user and display a hint on how to update these entries.

Note that we only warn the user whey they give a pathspec item that matches no eligible path for updating, but it does match one or more SKIP_WORKTREE entries. A warning was chosen over erroring out right away to reproduce the same behavior `add` already exhibits with ignored files. This also allow users to continue their workflow without having to invoke `add` again with only the eligible paths (as those will have already been added).

Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 Documentation/config/advice.txt |  3 ++
 advice.c                        | 20 +++++++++
 advice.h                        |  4 ++
 builtin/add.c                   | 70 ++++++++++++++++++++++++-------
 pathspec.c                      | 15 +++++++
 pathspec.h                      |  8 ++++
 t/t3705-add-sparse-checkout.sh  | 73 +++++++++++++++++++++++++++++----
 7 files changed, 172 insertions(+), 21 deletions(-)
Show changes to 7 files +172 −21

Documentation/config/advice.txt, advice.c, advice.h, builtin/add.c, pathspec.c, pathspec.h, t/t3705-add-sparse-checkout.sh

diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
index acbd0c09aa..851b83cf30 100644
--- a/Documentation/config/advice.txt
+++ b/Documentation/config/advice.txt
@@ -119,4 +119,7 @@ advice.*::
 	addEmptyPathspec::
 		Advice shown if a user runs the add command without providing
 		the pathspec parameter.
+	updateSparsePath::
+		Advice shown when linkgit:git-add[1] is asked to update index
+		entries outside the current sparse checkout.
 --
diff --git a/advice.c b/advice.c
index 164742305f..0b9c89c48a 100644
--- a/advice.c
+++ b/advice.c
@@ -2,6 +2,7 @@
 #include "config.h"
 #include "color.h"
 #include "help.h"
+#include "string-list.h"
 
 int advice_fetch_show_forced_updates = 1;
 int advice_push_update_rejected = 1;
@@ -136,6 +137,7 @@ static struct {
 	[ADVICE_STATUS_HINTS]				= { "statusHints", 1 },
 	[ADVICE_STATUS_U_OPTION]			= { "statusUoption", 1 },
 	[ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE] = { "submoduleAlternateErrorStrategyDie", 1 },
+	[ADVICE_UPDATE_SPARSE_PATH]			= { "updateSparsePath", 1 },
 	[ADVICE_WAITING_FOR_EDITOR]			= { "waitingForEditor", 1 },
 };
 
@@ -284,6 +286,24 @@ void NORETURN die_conclude_merge(void)
 	die(_("Exiting because of unfinished merge."));
 }
 
+void advise_on_updating_sparse_paths(struct string_list *pathspec_list)
+{
+	struct string_list_item *item;
+
+	if (!pathspec_list->nr)
+		return;
+
+	fprintf(stderr, _("The following pathspecs didn't match any"
+			  " eligible path, but they do match index\n"
+			  "entries outside the current sparse checkout:\n"));
+	for_each_string_list_item(item, pathspec_list)
+		fprintf(stderr, "%s\n", item->string);
+
+	advise_if_enabled(ADVICE_UPDATE_SPARSE_PATH,
+			  _("Disable or modify the sparsity rules if you intend"
+			    " to update such entries."));
+}
+
 void detach_advice(const char *new_name)
 {
 	const char *fmt =
diff --git a/advice.h b/advice.h
index bc2432980a..bd26c385d0 100644
--- a/advice.h
+++ b/advice.h
@@ -3,6 +3,8 @@
 
 #include "git-compat-util.h"
 
+struct string_list;
+
 extern int advice_fetch_show_forced_updates;
 extern int advice_push_update_rejected;
 extern int advice_push_non_ff_current;
@@ -71,6 +73,7 @@ extern int advice_add_empty_pathspec;
 	ADVICE_STATUS_HINTS,
 	ADVICE_STATUS_U_OPTION,
 	ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE,
+	ADVICE_UPDATE_SPARSE_PATH,
 	ADVICE_WAITING_FOR_EDITOR,
 };
 
@@ -92,6 +95,7 @@ void advise_if_enabled(enum advice_type type, const char *advice, ...);
 int error_resolve_conflict(const char *me);
 void NORETURN die_resolve_conflict(const char *me);
 void NORETURN die_conclude_merge(void);
+void advise_on_updating_sparse_paths(struct string_list *pathspec_list);
 void detach_advice(const char *new_name);
 
 #endif /* ADVICE_H */
diff --git a/builtin/add.c b/builtin/add.c
index 050cb8af30..6980311aaa 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -178,24 +178,43 @@ static char *prune_directory(struct dir_struct *dir, struct pathspec *pathspec,
 	}
 	dir->nr = dst - dir->entries;
 	add_pathspec_matches_against_index(pathspec, &the_index, seen,
-					   PS_HEED_SKIP_WORKTREE);
+					   PS_IGNORE_SKIP_WORKTREE);
 	return seen;
 }
 
-static void refresh(int verbose, const struct pathspec *pathspec)
+static int refresh(int verbose, const struct pathspec *pathspec)
 {
 	char *seen;
-	int i;
+	int i, ret = 0;
+	char *skip_worktree_seen = NULL;
+	struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
+	int flags = REFRESH_IGNORE_SKIP_WORKTREE |
+		    (verbose ? REFRESH_IN_PORCELAIN : REFRESH_QUIET);
 
 	seen = xcalloc(pathspec->nr, 1);
-	refresh_index(&the_index, verbose ? REFRESH_IN_PORCELAIN : REFRESH_QUIET,
-		      pathspec, seen, _("Unstaged changes after refreshing the index:"));
+	refresh_index(&the_index, flags, pathspec, seen,
+		      _("Unstaged changes after refreshing the index:"));
 	for (i = 0; i < pathspec->nr; i++) {
-		if (!seen[i])
-			die(_("pathspec '%s' did not match any files"),
-			    pathspec->items[i].original);
+		if (!seen[i]) {
+			if (matches_skip_worktree(pathspec, i, &skip_worktree_seen)) {
+				string_list_append(&only_match_skip_worktree,
+						   pathspec->items[i].original);
+			} else {
+				die(_("pathspec '%s' did not match any files"),
+				    pathspec->items[i].original);
+			}
+		}
+	}
+
+	if (only_match_skip_worktree.nr) {
+		advise_on_updating_sparse_paths(&only_match_skip_worktree);
+		ret = 1;
 	}
+
 	free(seen);
+	free(skip_worktree_seen);
+	string_list_clear(&only_match_skip_worktree, 0);
+	return ret;
 }
 
 int run_add_interactive(const char *revision, const char *patch_mode,
@@ -571,16 +590,18 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 	}
 
 	if (refresh_only) {
-		refresh(verbose, &pathspec);
+		exit_status |= refresh(verbose, &pathspec);
 		goto finish;
 	}
 
 	if (pathspec.nr) {
 		int i;
+		char *skip_worktree_seen = NULL;
+		struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
 
 		if (!seen)
 			seen = find_pathspecs_matching_against_index(&pathspec,
-					&the_index, PS_HEED_SKIP_WORKTREE);
+					&the_index, PS_IGNORE_SKIP_WORKTREE);
 
 		/*
 		 * file_exists() assumes exact match
@@ -594,12 +615,24 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 
 		for (i = 0; i < pathspec.nr; i++) {
 			const char *path = pathspec.items[i].match;
+
 			if (pathspec.items[i].magic & PATHSPEC_EXCLUDE)
 				continue;
-			if (!seen[i] && path[0] &&
-			    ((pathspec.items[i].magic &
-			      (PATHSPEC_GLOB | PATHSPEC_ICASE)) ||
-			     !file_exists(path))) {
+			if (seen[i])
+				continue;
+
+			if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) {
+				string_list_append(&only_match_skip_worktree,
+						   pathspec.items[i].original);
+				continue;
+			}
+
+			/* Don't complain at 'git add .' on empty repo */
+			if (!path[0])
+				continue;
+
+			if ((pathspec.items[i].magic & (PATHSPEC_GLOB | PATHSPEC_ICASE)) ||
+			    !file_exists(path)) {
 				if (ignore_missing) {
 					int dtype = DT_UNKNOWN;
 					if (is_excluded(&dir, &the_index, path, &dtype))
@@ -610,7 +643,16 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 					    pathspec.items[i].original);
 			}
 		}
+
+
+		if (only_match_skip_worktree.nr) {
+			advise_on_updating_sparse_paths(&only_match_skip_worktree);
+			exit_status = 1;
+		}
+
 		free(seen);
+		free(skip_worktree_seen);
+		string_list_clear(&only_match_skip_worktree, 0);
 	}
 
 	plug_bulk_checkin();
diff --git a/pathspec.c b/pathspec.c
index 8b05575110..ab67ff7c5c 100644
--- a/pathspec.c
+++ b/pathspec.c
@@ -62,6 +62,21 @@ char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
 	return seen;
 }
 
+char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec)
+{
+	struct index_state *istate = the_repository->index;
+	char *seen = xcalloc(pathspec->nr, 1);
+	int i;
+
+	for (i = 0; i < istate->cache_nr; i++) {
+		struct cache_entry *ce = istate->cache[i];
+		if (ce_skip_worktree(ce))
+		    ce_path_match(istate, ce, pathspec, seen);
+	}
+
+	return seen;
+}
+
 /*
  * Magic pathspec
  *
diff --git a/pathspec.h b/pathspec.h
index 0feb8e9f67..5b4c6614bf 100644
--- a/pathspec.h
+++ b/pathspec.h
@@ -160,6 +160,14 @@ void add_pathspec_matches_against_index(const struct pathspec *pathspec,
 char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
 					    const struct index_state *istate,
 					    enum ps_skip_worktree_action sw_action);
+char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec);
+static inline int matches_skip_worktree(const struct pathspec *pathspec,
+					int item, char **seen_ptr)
+{
+	if (!*seen_ptr)
+		*seen_ptr = find_pathspecs_matching_skip_worktree(pathspec);
+	return (*seen_ptr)[item];
+}
 int match_pathspec_attrs(const struct index_state *istate,
 			 const char *name, int namelen,
 			 const struct pathspec_item *item);
diff --git a/t/t3705-add-sparse-checkout.sh b/t/t3705-add-sparse-checkout.sh
index 00b10ac877..2b1fd0d0ee 100755
--- a/t/t3705-add-sparse-checkout.sh
+++ b/t/t3705-add-sparse-checkout.sh
@@ -36,10 +36,26 @@ setup_gitignore () {
 	EOF
 }
 
+test_expect_success 'setup' "
+	cat >sparse_error_header <<-EOF &&
+	The following pathspecs didn't match any eligible path, but they do match index
+	entries outside the current sparse checkout:
+	EOF
+
+	cat >sparse_hint <<-EOF &&
+	hint: Disable or modify the sparsity rules if you intend to update such entries.
+	hint: Disable this message with \"git config advice.updateSparsePath false\"
+	EOF
+
+	echo sparse_entry | cat sparse_error_header - >sparse_entry_error &&
+	cat sparse_entry_error sparse_hint >error_and_hint
+"
+
 test_expect_success 'git add does not remove sparse entries' '
 	setup_sparse_entry &&
 	rm sparse_entry &&
-	git add sparse_entry &&
+	test_must_fail git add sparse_entry 2>stderr &&
+	test_cmp error_and_hint stderr &&
 	test_sparse_entry_unchanged
 '
 
@@ -47,7 +63,8 @@ test_expect_success 'git add -A does not remove sparse entries' '
 	setup_sparse_entry &&
 	rm sparse_entry &&
 	setup_gitignore &&
-	git add -A &&
+	git add -A 2>stderr &&
+	test_must_be_empty stderr &&
 	test_sparse_entry_unchanged
 '
 
@@ -55,7 +72,13 @@ test_expect_success 'git add . does not remove sparse entries' '
 	setup_sparse_entry &&
 	rm sparse_entry &&
 	setup_gitignore &&
-	git add . &&
+	test_must_fail git add . 2>stderr &&
+
+	cat sparse_error_header >expect &&
+	echo . >>expect &&
+	cat sparse_hint >>expect &&
+
+	test_cmp expect stderr &&
 	test_sparse_entry_unchanged
 '
 
@@ -64,7 +87,8 @@ do
 	test_expect_success "git add${opt:+ $opt} does not update sparse entries" '
 		setup_sparse_entry &&
 		echo modified >sparse_entry &&
-		git add $opt sparse_entry &&
+		test_must_fail git add $opt sparse_entry 2>stderr &&
+		test_cmp error_and_hint stderr &&
 		test_sparse_entry_unchanged
 	'
 done
@@ -73,14 +97,16 @@ test_expect_success 'git add --refresh does not update sparse entries' '
 	setup_sparse_entry &&
 	git ls-files --debug sparse_entry | grep mtime >before &&
 	test-tool chmtime -60 sparse_entry &&
-	git add --refresh sparse_entry &&
+	test_must_fail git add --refresh sparse_entry 2>stderr &&
+	test_cmp error_and_hint stderr &&
 	git ls-files --debug sparse_entry | grep mtime >after &&
 	test_cmp before after
 '
 
 test_expect_success 'git add --chmod does not update sparse entries' '
 	setup_sparse_entry &&
-	git add --chmod=+x sparse_entry &&
+	test_must_fail git add --chmod=+x sparse_entry 2>stderr &&
+	test_cmp error_and_hint stderr &&
 	test_sparse_entry_unchanged &&
 	! test -x sparse_entry
 '
@@ -89,8 +115,41 @@ test_expect_success 'git add --renormalize does not update sparse entries' '
 	test_config core.autocrlf false &&
 	setup_sparse_entry "LINEONE\r\nLINETWO\r\n" &&
 	echo "sparse_entry text=auto" >.gitattributes &&
-	git add --renormalize sparse_entry &&
+	test_must_fail git add --renormalize sparse_entry 2>stderr &&
+	test_cmp error_and_hint stderr &&
+	test_sparse_entry_unchanged
+'
+
+test_expect_success 'git add --dry-run --ignore-missing warn on sparse path' '
+	setup_sparse_entry &&
+	rm sparse_entry &&
+	test_must_fail git add --dry-run --ignore-missing sparse_entry 2>stderr &&
+	test_cmp error_and_hint stderr &&
 	test_sparse_entry_unchanged
 '
 
+test_expect_success 'do not advice about sparse entries when they do not match the pathspec' '
+	setup_sparse_entry &&
+	test_must_fail git add nonexistent 2>stderr &&
+	grep "fatal: pathspec .nonexistent. did not match any files" stderr &&
+	! grep -F -f sparse_error_header stderr
+'
+
+test_expect_success 'do not warn when pathspec matches dense entries' '
+	setup_sparse_entry &&
+	echo modified >sparse_entry &&
+	>dense_entry &&
+	git add "*_entry" 2>stderr &&
+	test_must_be_empty stderr &&
+	test_sparse_entry_unchanged &&
+	git ls-files --error-unmatch dense_entry
+'
+
+test_expect_success 'add obeys advice.updateSparsePath' '
+	setup_sparse_entry &&
+	test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr &&
+	test_cmp sparse_entry_error stderr
+
+'
+
 test_done
-- 
2.30.1
Matheus Tavares· Apr 8, 2021, 20:41 UTC · re: Matheus Tavares · lore

[PATCH v4 7/7] rm: honor sparse checkout patterns

`git add` refrains from adding or updating index entries that are outside the current sparse checkout, but `git rm` doesn't follow the same restriction. This is somewhat counter-intuitive and inconsistent. So make `rm` honor the sparsity rules and advise on how to remove SKIP_WORKTREE entries just like `add` does. Also add some tests for the new behavior.

Suggested-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Matheus Tavares <matheus.bernardino@usp.br>
---
 Documentation/config/advice.txt  |  5 +-
 Documentation/git-rm.txt         |  4 +-
 builtin/rm.c                     | 35 +++++++++-----
 t/t3602-rm-sparse-checkout.sh    | 78 ++++++++++++++++++++++++++++++++
 t/t7011-skip-worktree-reading.sh |  5 --
 5 files changed, 108 insertions(+), 19 deletions(-)
 create mode 100755 t/t3602-rm-sparse-checkout.sh
Show changes to 5 files +108 −19

Documentation/config/advice.txt, Documentation/git-rm.txt, builtin/rm.c, t/t3602-rm-sparse-checkout.sh, t/t7011-skip-worktree-reading.sh

diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
index 851b83cf30..8b2849ff7b 100644
--- a/Documentation/config/advice.txt
+++ b/Documentation/config/advice.txt
@@ -120,6 +120,7 @@ advice.*::
 		Advice shown if a user runs the add command without providing
 		the pathspec parameter.
 	updateSparsePath::
-		Advice shown when linkgit:git-add[1] is asked to update index
-		entries outside the current sparse checkout.
+		Advice shown when either linkgit:git-add[1] or linkgit:git-rm[1]
+		is asked to update index entries outside the current sparse
+		checkout.
 --
diff --git a/Documentation/git-rm.txt b/Documentation/git-rm.txt
index ab750367fd..26e9b28470 100644
--- a/Documentation/git-rm.txt
+++ b/Documentation/git-rm.txt
@@ -23,7 +23,9 @@ branch, and no updates to their contents can be staged in the index,
 though that default behavior can be overridden with the `-f` option.
 When `--cached` is given, the staged content has to
 match either the tip of the branch or the file on disk,
-allowing the file to be removed from just the index.
+allowing the file to be removed from just the index. When
+sparse-checkouts are in use (see linkgit:git-sparse-checkout[1]),
+`git rm` will only remove paths within the sparse-checkout patterns.
 
 
 OPTIONS
diff --git a/builtin/rm.c b/builtin/rm.c
index 4858631e0f..d23a3b2164 100644
--- a/builtin/rm.c
+++ b/builtin/rm.c
@@ -5,6 +5,7 @@
  */
 #define USE_THE_INDEX_COMPATIBILITY_MACROS
 #include "builtin.h"
+#include "advice.h"
 #include "config.h"
 #include "lockfile.h"
 #include "dir.h"
@@ -254,7 +255,7 @@ static struct option builtin_rm_options[] = {
 int cmd_rm(int argc, const char **argv, const char *prefix)
 {
 	struct lock_file lock_file = LOCK_INIT;
-	int i;
+	int i, ret = 0;
 	struct pathspec pathspec;
 	char *seen;
 
@@ -295,6 +296,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
 
 	for (i = 0; i < active_nr; i++) {
 		const struct cache_entry *ce = active_cache[i];
+		if (ce_skip_worktree(ce))
+			continue;
 		if (!ce_path_match(&the_index, ce, &pathspec, seen))
 			continue;
 		ALLOC_GROW(list.entry, list.nr + 1, list.alloc);
@@ -308,24 +311,34 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
 	if (pathspec.nr) {
 		const char *original;
 		int seen_any = 0;
+		char *skip_worktree_seen = NULL;
+		struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
+
 		for (i = 0; i < pathspec.nr; i++) {
 			original = pathspec.items[i].original;
-			if (!seen[i]) {
-				if (!ignore_unmatch) {
-					die(_("pathspec '%s' did not match any files"),
-					    original);
-				}
-			}
-			else {
+			if (seen[i])
 				seen_any = 1;
-			}
+			else if (ignore_unmatch)
+				continue;
+			else if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen))
+				string_list_append(&only_match_skip_worktree, original);
+			else
+				die(_("pathspec '%s' did not match any files"), original);
+
 			if (!recursive && seen[i] == MATCHED_RECURSIVELY)
 				die(_("not removing '%s' recursively without -r"),
 				    *original ? original : ".");
 		}
 
+		if (only_match_skip_worktree.nr) {
+			advise_on_updating_sparse_paths(&only_match_skip_worktree);
+			ret = 1;
+		}
+		free(skip_worktree_seen);
+		string_list_clear(&only_match_skip_worktree, 0);
+
 		if (!seen_any)
-			exit(0);
+			exit(ret);
 	}
 
 	if (!index_only)
@@ -405,5 +418,5 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
 			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
 		die(_("Unable to write new index file"));
 
-	return 0;
+	return ret;
 }
diff --git a/t/t3602-rm-sparse-checkout.sh b/t/t3602-rm-sparse-checkout.sh
new file mode 100755
index 0000000000..e9e9a15c74
--- /dev/null
+++ b/t/t3602-rm-sparse-checkout.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+test_description='git rm in sparse checked out working trees'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' "
+	mkdir -p sub/dir &&
+	touch a b c sub/d sub/dir/e &&
+	git add -A &&
+	git commit -m files &&
+
+	cat >sparse_error_header <<-EOF &&
+	The following pathspecs didn't match any eligible path, but they do match index
+	entries outside the current sparse checkout:
+	EOF
+
+	cat >sparse_hint <<-EOF &&
+	hint: Disable or modify the sparsity rules if you intend to update such entries.
+	hint: Disable this message with \"git config advice.updateSparsePath false\"
+	EOF
+
+	echo b | cat sparse_error_header - >sparse_entry_b_error &&
+	cat sparse_entry_b_error sparse_hint >b_error_and_hint
+"
+
+for opt in "" -f --dry-run
+do
+	test_expect_success "rm${opt:+ $opt} does not remove sparse entries" '
+		git sparse-checkout set a &&
+		test_must_fail git rm $opt b 2>stderr &&
+		test_cmp b_error_and_hint stderr &&
+		git ls-files --error-unmatch b
+	'
+done
+
+test_expect_success 'recursive rm does not remove sparse entries' '
+	git reset --hard &&
+	git sparse-checkout set sub/dir &&
+	git rm -r sub &&
+	git status --porcelain -uno >actual &&
+	echo "D  sub/dir/e" >expected &&
+	test_cmp expected actual
+'
+
+test_expect_success 'rm obeys advice.updateSparsePath' '
+	git reset --hard &&
+	git sparse-checkout set a &&
+	test_must_fail git -c advice.updateSparsePath=false rm b 2>stderr &&
+	test_cmp sparse_entry_b_error stderr
+'
+
+test_expect_success 'do not advice about sparse entries when they do not match the pathspec' '
+	git reset --hard &&
+	git sparse-checkout set a &&
+	test_must_fail git rm nonexistent 2>stderr &&
+	grep "fatal: pathspec .nonexistent. did not match any files" stderr &&
+	! grep -F -f sparse_error_header stderr
+'
+
+test_expect_success 'do not warn about sparse entries when pathspec matches dense entries' '
+	git reset --hard &&
+	git sparse-checkout set a &&
+	git rm "[ba]" 2>stderr &&
+	test_must_be_empty stderr &&
+	git ls-files --error-unmatch b &&
+	test_must_fail git ls-files --error-unmatch a
+'
+
+test_expect_success 'do not warn about sparse entries with --ignore-unmatch' '
+	git reset --hard &&
+	git sparse-checkout set a &&
+	git rm --ignore-unmatch b 2>stderr &&
+	test_must_be_empty stderr &&
+	git ls-files --error-unmatch b
+'
+
+test_done
diff --git a/t/t7011-skip-worktree-reading.sh b/t/t7011-skip-worktree-reading.sh
index 26852586ac..1761a2b1b9 100755
--- a/t/t7011-skip-worktree-reading.sh
+++ b/t/t7011-skip-worktree-reading.sh
@@ -132,11 +132,6 @@ test_expect_success 'diff-files does not examine skip-worktree dirty entries' '
 	test -z "$(git diff-files -- one)"
 '
 
-test_expect_success 'git-rm succeeds on skip-worktree absent entries' '
-	setup_absent &&
-	git rm 1
-'
-
 test_expect_success 'commit on skip-worktree absent entries' '
 	git reset &&
 	setup_absent &&
-- 
2.30.1
Elijah Newren· Apr 14, 2021, 16:36 UTC · re: Matheus Tavares · lore

Re: [PATCH v4 0/7] add/rm: honor sparse checkout and warn on sparse paths

On Thu, Apr 8, 2021 at 1:41 PM Matheus Tavares <matheus.bernardino@usp.br> wrote:

Show 45 quoted lines
>
> Make `rm` honor sparse checkouts, and make both `rm` and `add` warn
> when asked to update sparse entries.
>
> There are two changes since v3:
>
> - `test_i18ncmp` and `test_i18ngrep` were replaced by `test_cmp` and
>   `grep`
>
> - The flag added in patch 5 now makes refresh_index() completely ignore
>   skip_worktree entries, instead of just suppressing their matches on
>   the seen[] array. The previous implementation was not necessarily
>   wrong but, as Junio pointed out, it was rather odd to keep matching
>   the entries if we no longer want to use the matches.
>
>   As "side effects", the new version of the flag also makes
>   refresh_index() refrain from both:
>
>   (1) checking and warning if skip_worktree entries matching the given
>   pathspec are unmerged.
>
>   (2) marking skip_worktree entries matching the given pathspec with
>   CE_UPTODATE.
>
>   The change (1) is actually interesting because `git add` doesn't
>   update skip_worktree entries, and thus, it doesn't make much sense to
>   warn if they are unmerged. Besides, we will already warn if the user
>   requests to update such entries, anyway. And finally, unmerged
>   entries should not have the skip_worktree bit set in the first place.
>   (`git merge` should clean this bit when writing the new index, and
>   neither `git sparse-checkout` nor `git update-index` allow to set the
>   bit on an unmerged entry.)
>
>   Change (2) is perhaps not very beneficial, but it is also not harmful.
>   The only practical difference we get by not setting the CE_UPTODATE
>   flag in the skip_worktree entries is that, when writing a new index at
>   the end of `git add --refresh`, do_write_index() will start checking
>   if these entries are racy clean. Note that it already does that for
>   all the skip_worktree entries that do not match the user-given
>   pathspecs. And, in fact, this behavior distinction based on the
>   pathspec only happens with `--refresh`. Plain `git add` and other
>   options don't mark any skip_worktree entry with CE_UPTODATE
>   (regardless of the pathspecs) and thus, all these entries are checked
>   when writing the index. So `git add --refresh` will only do what the
>   other options already do.

Sorry for the delay. These two changes sound good to me, and the range-diff looks reasonable.

Show 6 quoted lines
>   (Additionally, as I mentioned in [1], there might actually be at least
>   one advantage in checking if the skip_worktree entries are racy clean.
>   But this is a very specific case, and it's probably a topic for a
>   another thread :)
>
> [1]: https://lore.kernel.org/git/CAHd-oW4kRLjV9Sq3CFt-V1Ot9pYFzJggU1zPp3Hcuw=qWfq7Mg@mail.gmail.com/

This I'm a bit surprised by. I thought the outcome there was that you didn't want to mark skip_worktree entries as CE_UPTODATE in order to force them to be stat'd in the future when someone clears the skip_worktree bit. (And that we'd expect the sparse-checkout add/disable command to be the likely one to handle that, though if users use `git update-index --no-skip-worktree PATH` then they'd also be responsible for any refreshing as well.) Am I misunderstanding?

Matheus Tavares Bernardino· Apr 14, 2021, 18:04 UTC · re: Elijah Newren · lore

Re: [PATCH v4 0/7] add/rm: honor sparse checkout and warn on sparse paths

On Wed, Apr 14, 2021 at 1:36 PM Elijah Newren <newren@gmail.com> wrote:
Show 63 quoted lines
>
> On Thu, Apr 8, 2021 at 1:41 PM Matheus Tavares
> <matheus.bernardino@usp.br> wrote:
> >
> > Make `rm` honor sparse checkouts, and make both `rm` and `add` warn
> > when asked to update sparse entries.
> >
> > There are two changes since v3:
> >
> > - `test_i18ncmp` and `test_i18ngrep` were replaced by `test_cmp` and
> >   `grep`
> >
> > - The flag added in patch 5 now makes refresh_index() completely ignore
> >   skip_worktree entries, instead of just suppressing their matches on
> >   the seen[] array. The previous implementation was not necessarily
> >   wrong but, as Junio pointed out, it was rather odd to keep matching
> >   the entries if we no longer want to use the matches.
> >
> >   As "side effects", the new version of the flag also makes
> >   refresh_index() refrain from both:
> >
> >   (1) checking and warning if skip_worktree entries matching the given
> >   pathspec are unmerged.
> >
> >   (2) marking skip_worktree entries matching the given pathspec with
> >   CE_UPTODATE.
> >
> >   The change (1) is actually interesting because `git add` doesn't
> >   update skip_worktree entries, and thus, it doesn't make much sense to
> >   warn if they are unmerged. Besides, we will already warn if the user
> >   requests to update such entries, anyway. And finally, unmerged
> >   entries should not have the skip_worktree bit set in the first place.
> >   (`git merge` should clean this bit when writing the new index, and
> >   neither `git sparse-checkout` nor `git update-index` allow to set the
> >   bit on an unmerged entry.)
> >
> >   Change (2) is perhaps not very beneficial, but it is also not harmful.
> >   The only practical difference we get by not setting the CE_UPTODATE
> >   flag in the skip_worktree entries is that, when writing a new index at
> >   the end of `git add --refresh`, do_write_index() will start checking
> >   if these entries are racy clean. Note that it already does that for
> >   all the skip_worktree entries that do not match the user-given
> >   pathspecs. And, in fact, this behavior distinction based on the
> >   pathspec only happens with `--refresh`. Plain `git add` and other
> >   options don't mark any skip_worktree entry with CE_UPTODATE
> >   (regardless of the pathspecs) and thus, all these entries are checked
> >   when writing the index. So `git add --refresh` will only do what the
> >   other options already do.
>
> Sorry for the delay.  These two changes sound good to me, and the
> range-diff looks reasonable.
>
> >   (Additionally, as I mentioned in [1], there might actually be at least
> >   one advantage in checking if the skip_worktree entries are racy clean.
> >   But this is a very specific case, and it's probably a topic for a
> >   another thread :)
> >
> > [1]: https://lore.kernel.org/git/CAHd-oW4kRLjV9Sq3CFt-V1Ot9pYFzJggU1zPp3Hcuw=qWfq7Mg@mail.gmail.com/
>
> This I'm a bit surprised by.  I thought the outcome there was that you
> didn't want to mark skip_worktree entries as CE_UPTODATE in order to
> force them to be stat'd in the future when someone clears the
> skip_worktree bit.

Hmm, not exactly. This situation is a bit tricky and I probably got lost when trying to communicate my thoughts about it.

In short, the outcome of not marking skip_worktree entries as CE_UPTODATE (which is an in-memory-only flag) is that, when writing the updated index at the end of `git status --refresh` , we now properly detect and mark skip_worktree entries whose associated files are present in the working tree and are modified in relation to the respective blobs. (This whole process is skipped for CE_UPTODATE entries.)

This doesn't have any effect while the skip_worktree bit is set. But it makes it possible for a later `git status` to properly show the files as modified when the skip_worktree bit gets unset. If we don't do this, the later `git status` will wrongly think these entries are clean.

This is because of the way git detects racily clean entries. Paraphrasing `Documentation/technical/racy-git.txt`, we take two actions to diagnose these entries:

1) When we want to know if an entry is up-to-date: if the entry's
timestamp is equal to, or newer than, the index timestamp, we not only
compare the cached stat info with the filesystem stat info but we also
compare the actual contents.
2) When writing a new index: if the index contains racily clean
entries, their `st_size` is truncated to zero.

Item 2) is important because, otherwise, the subsequent operations wouldn't be able to detect the racily clean entries using 1) as the index timestamp would have been updated.

And that's what happens with skip_worktree entries on `git status --refresh`. We mark them as CE_UPTODATE even if the file exists in the working tree, so we don't check if the cached entry is racily clean, and thus we don't truncate `st_size` to 0, hiding the racily clean entry.

With all that said, I think this whole situation must be quite rare and not very important in practice...

Junio C Hamano· Apr 16, 2021, 21:33 UTC · re: Matheus Tavares · lore

Re: [PATCH v4 0/7] add/rm: honor sparse checkout and warn on sparse paths

Matheus Tavares <matheus.bernardino@usp.br> writes:
> Make `rm` honor sparse checkouts, and make both `rm` and `add` warn
> when asked to update sparse entries.

OK, has the back-and-forth concluded and this is now ready to be merged down to 'next'?

Elijah Newren· Apr 16, 2021, 23:17 UTC · re: Junio C Hamano · lore

Re: [PATCH v4 0/7] add/rm: honor sparse checkout and warn on sparse paths

On Fri, Apr 16, 2021 at 2:33 PM Junio C Hamano <gitster@pobox.com> wrote:
Show 8 quoted lines
>
> Matheus Tavares <matheus.bernardino@usp.br> writes:
>
> > Make `rm` honor sparse checkouts, and make both `rm` and `add` warn
> > when asked to update sparse entries.
>
> OK, has the back-and-forth concluded and this is now ready to be
> merged down to 'next'?
Yes, let's merge it.

← back to recent threads