Sparse-checkout: modify 'git add', 'git rm', and 'git add' behavior
cover Sparse-checkout: modify 'git add', 'git rm', and 'git add' behavior
As requested, this series looks to update the behavior of git add, git rm, and git mv when they attempt to modify paths outside of the sparse-checkout cone. In particular, this care is expanded to not just cache entries with the SKIP_WORKTREE bit, but also paths that do not match the sparse-checkout definition.
This means that commands that worked before this series can now fail. In particular, if 'git merge' results in a conflict outside of the sparse-checkout cone, then 'git add ' will now fail.
In order to allow users to circumvent these protections, a new '--sparse' option is added that ignores the sparse-checkout patterns and the SKIP_WORKTREE bit. The message for advice.updateSparsePath is adjusted to assist with discovery of this option.
There is a subtle issue with git mv in that it does not check the index until it discovers a directory and then uses the index to find the contained entries. This means that in non-cone-mode patterns, a pattern such as "sub/dir" will not match the path "sub" and this can cause an issue.
In order to allow for checking arbitrary paths against the sparse-checkout patterns, some changes to the underlying pattern matching code is required. It turns out that there are some bugs in the methods as advertised, but these bugs were never discovered because of the way methods like unpack_trees() will check a directory for a pattern match before checking its contained paths. Our new "check patterns on-demand" approach pokes holes in that approach, specifically with patterns that match entire directories.
I'm open to alternative designs here, too.
The patches are based on v2 of the integration with merge, cherry-pick, and rebase. I'm sending the RFC really early in case it needs to be significantly reworked.
Thanks, -Stolee
Derrick Stolee (13):
t1092: behavior for adding sparse files
dir: extract directory-matching logic
dir: select directories correctly
dir: fix pattern matching on dirs
add: fail when adding an untracked sparse file
add: skip paths that are outside sparse-checkout cone
add: implement the --sparse option
add: prevent adding sparse conflict files
rm: add --sparse option
rm: skip sparse paths with missing SKIP_WORKTREE
mv: refuse to move sparse paths
mv: add '--sparse' option to ignore sparse-checkout
advice: update message to suggest '--sparse'
Documentation/git-add.txt | 9 +- Documentation/git-rm.txt | 6 ++ advice.c | 3 +- builtin/add.c | 22 ++++- builtin/mv.c | 39 ++++++-- builtin/rm.c | 10 +- dir.c | 54 ++++++++-- pathspec.c | 5 +- t/t1091-sparse-checkout-builtin.sh | 4 +- t/t1092-sparse-checkout-compatibility.sh | 47 ++++++--- t/t3602-rm-sparse-checkout.sh | 27 ++++- t/t3705-add-sparse-checkout.sh | 10 +- t/t7002-mv-sparse-checkout.sh | 121 +++++++++++++++++++++++ 13 files changed, 316 insertions(+), 41 deletions(-) create mode 100755 t/t7002-mv-sparse-checkout.sh
base-commit: df4bbec744f5cd4a060082212d95a36b812fa50b
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1018%2Fderrickstolee%2Fsparse-index%2Fadd-rm-mv-behavior-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1018/derrickstolee/sparse-index/add-rm-mv-behavior-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1018
Signature · 1 line
gitgitgadget
PATCH 1/13 t1092: behavior for adding sparse files
From: Derrick Stolee <[email protected]>
Add some tests to demonstrate the current behavior around adding files outside of the sparse-checkout cone. Currently, untracked files are handled differently from tracked files. A futuer change will make these cases be handled the same way.
--- t/t1092-sparse-checkout-compatibility.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+)
t/t1092-sparse-checkout-compatibility.sh +12 -0
@@ -291,6 +291,18 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' +# NEEDSWORK: This documents current behavior, but is not a desirable +# behavior (untracked files are handled differently than tracked). +test_expect_success 'add outside sparse cone' ' + init_repos && + + run_on_sparse mkdir folder1 && + run_on_sparse ../edit-contents folder1/a && + run_on_sparse ../edit-contents folder1/newfile && + test_sparse_match test_must_fail git add folder1/a && + test_sparse_match git add folder1/newfile +' + test_expect_success 'commit including unstaged changes' ' init_repos &&
Signature · 1 line
gitgitgadget
PATCH 2/13 dir: extract directory-matching logic
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The last_matching_pattern_from_list() logic performs some checks on the filetype of a path within the index when the PATTERN_FLAG_MUSTBEDIR flag is set. This works great when setting SKIP_WORKTREE bits within unpack_trees(), but doesn't work well when passing an arbitrary path such as a file within a matching directory.
This change only rearranges the logic but does not change its functionality.
We will expand the path_matches_dir_pattern() method in a following change.
--- dir.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-)
dir.c +17 -5
@@ -1303,6 +1303,19 @@ int match_pathname(const char *pathname, int pathlen, WM_PATHNAME) == 0; } +static int path_matches_dir_pattern (const char *pathname, +int pathlen, +int *dtype, +struct path_pattern *pattern, +struct index_state *istate) +{ + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); +if (*dtype !=DT_DIR ) +return 0 ; + +return 1 ; +} +/* * Scan the given exclude list in reverse to see whether pathname * should be ignored. The first match (i.e. the last on the list), if
@@ -1327,11 +1340,10 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnameconst char *exclude = pattern->pattern;int prefix = pattern->nowildcardlen; -if (pattern->flags &PATTERN_FLAG_MUSTBEDIR ) { - *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); -if (*dtype !=DT_DIR ) -continue ; - } +if ((pattern->flags &PATTERN_FLAG_MUSTBEDIR ) && + !path_matches_dir_pattern (pathname, pathlen, + dtype, pattern, istate)) +continue ;if (pattern->flags &PATTERN_FLAG_NODIR ) { if (match_basename(basename,
Signature · 1 line
gitgitgadget
PATCH 3/13 dir: select directories correctly
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When matching a path against a list of patterns, the ones that require a directory match previously did not work when a filename is specified. This was fine when all pattern-matching was done within methods such as unpack_trees() that check a directory before recursing into the contained files. However, other commands will start matching individual files against pattern lists without that recursive approach.
We modify path_matches_dir_pattern() to take a strbuf 'path_parent' that is used to store the parent directory of 'pathname' between multiple pattern matching tests. This is loaded lazily, only on the first pattern it finds that has the PATTERN_FLAG_MUSTBEDIR flag.
If we find that a path has a parent directory, we start by checking to see if that parent directory matches the pattern. If so, then we do not need to query the index for the type (which can be expensive). If we find that the parent does not match, then we still must check the type from the index for the given pathname.
Note that this does not affect cone mode pattern matching, but instead the more general -- and slower -- full pattern set. Thus, this does not affect the sparse index.
--- dir.c | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-)
dir.c +32 -2
@@ -1305,10 +1305,38 @@ int match_pathname(const char *pathname, int pathlen,static int path_matches_dir_pattern (const char *pathname,int pathlen, +struct strbuf *path_parent,int *dtype,struct path_pattern *pattern,struct index_state *istate) { +/* +* Use 'alloc' as an indicator that the string has not been +* initialized, in case the parent is the root directory. +*/ +if (!path_parent->alloc) { +char *slash; +strbuf_addstr (path_parent, pathname); + slash =find_last_dir_sep (path_parent->buf); + +if (slash) + *slash ='\0' ; +else +strbuf_setlen (path_parent,0 ); + } + +/* +* If the parent directory matches the pattern, then we do not +* need to check for dtype. +*/ +if (path_parent->len && +match_pathname (path_parent->buf, path_parent->len, + pattern->base, + pattern->baselen ? pattern->baselen -1 :0 , + pattern->pattern, pattern->nowildcardlen, + pattern->patternlen, pattern->flags)) +return 1 ; + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen);if (*dtype !=DT_DIR )return 0 ;
@@ -1331,6 +1359,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname {struct path_pattern *res =NULL ;/* undecided */ int i; +struct strbuf path_parent =STRBUF_INIT ;if (!pl->nr)return NULL ;/* undefined */
@@ -1340,8 +1369,8 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnameconst char *exclude = pattern->pattern;int prefix = pattern->nowildcardlen; -if ((pattern->flags &PATTERN_FLAG_MUSTBEDIR ) && - !path_matches_dir_pattern (pathname, pathlen, +if (pattern->flags &PATTERN_FLAG_MUSTBEDIR && + !path_matches_dir_pattern (pathname, pathlen, &path_parent, dtype, pattern, istate))continue ;
@@ -1367,6 +1396,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnamebreak ; } } +strbuf_release (&path_parent);return res; }
Signature · 1 line
gitgitgadget
PATCH 4/13 dir: fix pattern matching on dirs
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Within match_pathname(), one successful matching category happens when the pattern is equal to its non-wildcard prefix. At this point, we have checked that the input 'pathname' matches the pattern up to the prefix length, and then we subtraced that length from both 'patternlen' and 'namelen'.
In the case of a directory match, this prefix match should be sufficient. However, the success condition only cared about _exact_ equality here. Instead, we should allow any path that agrees on this prefix in the case of PATTERN_FLAG_MUSTBEDIR.
This case was not tested before because of the way unpack_trees() would match a parent directory before visiting the contained paths. This approach is changing, so we must change this comparison.
--- dir.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
dir.c +1 -1
@@ -1294,7 +1294,7 @@ int match_pathname(const char *pathname, int pathlen,* then our prefix match is all we need; we * do not need to call fnmatch at all. */ -if (!patternlen && !namelen) +if (!patternlen && (!namelen || (flags &PATTERN_FLAG_MUSTBEDIR )))return 1 ; }
Signature · 1 line
gitgitgadget
PATCH 5/13 add: fail when adding an untracked sparse file
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The add_files() method in builtin/add.c takes a set of untracked files that are being added by the input pathspec and inserts them into the index. If these files are outside of the sparse-checkout cone, then they gain the SKIP_WORKTREE bit at some point. However, this was not checked before inserting into the index, so these files are added even though we want to avoid modifying the index outside of the sparse-checkout cone.
Add a check within add_files() for these files and write the advice about files outside of the sprase-checkout cone.
This behavior change modifies some existing tests within t1092. These tests intended to document how a user could interact with the existing behavior in place. Many of these tests need to be marked as expecting failure. A future change will allow these tests to pass by adding a flag to 'git add' that allows users to modify index entries outside of the sparse-checkout cone.
The 'submodule handling' test is intended to document what happens to directories that contain a submodule when the sparse index is enabled. It is not trying to say that users should be able to add submodules outside of the sparse-checkout cone, so that test can be modified to avoid that operation.
--- builtin/add.c | 14 ++++++++++ t/t1092-sparse-checkout-compatibility.sh | 33 +++++++++++++++++------- 2 files changed, 38 insertions(+), 9 deletions(-)
builtin/add.c +14 -0
@@ -443,6 +443,7 @@ static void check_embedded_repo(const char *path)static int add_files (struct dir_struct *dir,int flags) {int i, exit_status =0 ; +struct string_list only_match_skip_worktree =STRING_LIST_INIT_NODUP ;if (dir->ignored_nr) {fprintf (stderr,_ (ignore_error));
@@ -456,6 +457,11 @@ static int add_files(struct dir_struct *dir, int flags) }for (i =0 ; i < dir->nr; i++) { +if (!path_in_sparse_checkout (dir->entries[i]->name, &the_index)) { +string_list_append (&only_match_skip_worktree, + dir->entries[i]->name); +continue ; + }if (add_file_to_index (&the_index, dir->entries[i]->name, flags)) {if (!ignore_add_errors)die (_ ("adding files failed" ));
@@ -464,6 +470,14 @@ static int add_files(struct dir_struct *dir, int flags)check_embedded_repo (dir->entries[i]->name); } } + +if (only_match_skip_worktree.nr) { +advise_on_updating_sparse_paths (&only_match_skip_worktree); + exit_status =1 ; + } + +string_list_clear (&only_match_skip_worktree,0 ); +return exit_status; }
t/t1092-sparse-checkout-compatibility.sh +24 -9
@@ -291,8 +291,6 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' -# NEEDSWORK: This documents current behavior, but is not a desirable -# behavior (untracked files are handled differently than tracked). test_expect_success 'add outside sparse cone' ' init_repos &&
@@ -300,7 +298,7 @@ test_expect_success 'add outside sparse cone' ' run_on_sparse ../edit-contents folder1/a && run_on_sparse ../edit-contents folder1/newfile && test_sparse_match test_must_fail git add folder1/a && - test_sparse_match git add folder1/newfile + test_sparse_match test_must_fail git add folder1/newfile ' test_expect_success 'commit including unstaged changes' '
@@ -331,7 +329,11 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -test_expect_success 'status/add: outside sparse cone' ' +# NEEDSWORK: Now that 'git add folder1/new' fails, the changes being +# attempted here fail for the sparse-checkout and sparse-index repos. +# We must enable a way for adding files outside the sparse-checkout +# done, even if it is by an optional flag. +test_expect_failure 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -352,10 +354,9 @@ test_expect_success 'status/add: outside sparse cone' ' # Adding the path outside of the sparse-checkout cone should fail. test_sparse_match test_must_fail git add folder1/a && test_sparse_match test_must_fail git add --refresh folder1/a && + test_sparse_match test_must_fail git add folder1/new && - # NEEDSWORK: Adding a newly-tracked file outside the cone succeeds - test_sparse_match git add folder1/new && - + # NEEDSWORK: behavior begins to deviate here. test_all_match git add . && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/new &&
@@ -511,7 +512,7 @@ test_expect_success 'merge, cherry-pick, and rebase' ' # Right now, users might be using this flow to work through conflicts, # so any solution should present advice to users who try this sequence # of commands to follow whatever new method we create. -test_expect_success 'merge with conflict outside cone' ' +test_expect_failure 'merge with conflict outside cone' ' init_repos && test_all_match git checkout -b merge-tip merge-left &&
@@ -525,12 +526,18 @@ test_expect_success 'merge with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers + # NEEDSWORK: Even though the merge conflict removed the + # SKIP_WORKTREE bit from the index entry for folder1/a, we should + # warn that this is a problematic add. test_all_match git add folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. run_on_all mv folder2/a folder2/z && + # NEEDSWORK: This mode now fails, because folder2/z is + # outside of the sparse-checkout cone and does not match an + # existing index entry with the SKIP_WORKTREE bit cleared. test_all_match git add folder2 && test_all_match git status --porcelain=v2 &&
@@ -539,7 +546,7 @@ test_expect_success 'merge with conflict outside cone' ' test_all_match git rev-parse HEAD^{tree} ' -test_expect_success 'cherry-pick/rebase with conflict outside cone' ' +test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' init_repos && for OPERATION in cherry-pick rebase
@@ -556,11 +563,17 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers + # NEEDSWORK: Even though the merge conflict removed the + # SKIP_WORKTREE bit from the index entry for folder1/a, we should + # warn that this is a problematic add. test_all_match git add folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. + # NEEDSWORK: This mode now fails, because folder2/z is + # outside of the sparse-checkout cone and does not match an + # existing index entry with the SKIP_WORKTREE bit cleared. run_on_all mv folder2/a folder2/z && test_all_match git add folder2 && test_all_match git status --porcelain=v2 &&
@@ -638,6 +651,7 @@ test_expect_success 'clean' ' test_expect_success 'submodule handling' ' init_repos && + test_sparse_match git sparse-checkout add modules && test_all_match mkdir modules && test_all_match touch modules/a && test_all_match git add modules &&
@@ -647,6 +661,7 @@ test_expect_success 'submodule handling' ' test_all_match git commit -m "add submodule" && # having a submodule prevents "modules" from collapse + test_sparse_match git sparse-checkout set deep/deeper1 && test-tool -C sparse-index read-cache --table >cache && grep "100644 blob .* modules/a" cache && grep "160000 commit $(git -C initial-repo rev-parse HEAD) modules/sub" cache
Signature · 1 line
gitgitgadget
PATCH 6/13 add: skip paths that are outside sparse-checkout cone
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When 'git add' adds a tracked file that is outside of the sparse-checkout cone, it checks the SKIP_WORKTREE bit to see if the file exists outside of the sparse-checkout cone. This is usually correct, except in the case of a merge conflict outside of the cone.
Modify add_pathspec_matched_against_index() to be more careful about pathes by checking the sparse-checkout patterns in addition to the SKIP_WORKTREE bit. This causes 'git add' to no longer allow files outside of the cone that removed the SKIP_WORKTREE bit due to a merge conflict.
With only this change, users will only be able to add the file after adding the file to the sparse-checkout cone. A later change will allow users to force adding even though the file is outside of the sparse-checkout cone.
--- pathspec.c | 3 ++- t/t1092-sparse-checkout-compatibility.sh | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-)
pathspec.c +2 -1
@@ -39,7 +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)) +if (sw_action ==PS_IGNORE_SKIP_WORKTREE && + (ce_skip_worktree (ce) || !path_in_sparse_checkout (ce->name, istate)))continue ;ce_path_match (istate, ce, pathspec, seen); }
t/t1092-sparse-checkout-compatibility.sh +4 -4
@@ -529,7 +529,7 @@ test_expect_failure 'merge with conflict outside cone' ' # NEEDSWORK: Even though the merge conflict removed the # SKIP_WORKTREE bit from the index entry for folder1/a, we should # warn that this is a problematic add. - test_all_match git add folder1/a && + test_sparse_match test_must_fail git add folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -538,7 +538,7 @@ test_expect_failure 'merge with conflict outside cone' ' # NEEDSWORK: This mode now fails, because folder2/z is # outside of the sparse-checkout cone and does not match an # existing index entry with the SKIP_WORKTREE bit cleared. - test_all_match git add folder2 && + test_sparse_match test_must_fail git add folder2 && test_all_match git status --porcelain=v2 && test_all_match git merge --continue &&
@@ -566,7 +566,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' # NEEDSWORK: Even though the merge conflict removed the # SKIP_WORKTREE bit from the index entry for folder1/a, we should # warn that this is a problematic add. - test_all_match git add folder1/a && + test_sparse_match test_must_fail git add folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -575,7 +575,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' # outside of the sparse-checkout cone and does not match an # existing index entry with the SKIP_WORKTREE bit cleared. run_on_all mv folder2/a folder2/z && - test_all_match git add folder2 && + test_sparse_match test_must_fail git add folder2 && test_all_match git status --porcelain=v2 && test_all_match git $OPERATION --continue &&
Signature · 1 line
gitgitgadget
PATCH 7/13 add: implement the --sparse option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
We previously modified 'git add' to refuse updating index entries outside of the sparse-checkout cone. This is justified to prevent users from accidentally getting into a confusing state when Git removes those files from the working tree at some later point.
Unfortunately, this caused some workflows that were previously possible to become impossible, especially around merge conflicts outside of the sparse-checkout cone. These were documented in tests within t1092.
We now re-enable these workflows using a new '--sparse' option to 'git add'. This allows users to signal "Yes, I do know what I'm doing with these files," and accept the consequences of the files leaving the worktree later.
We delay updating the advice message until implementing a similar option in 'git rm' and 'git mv'.
--- Documentation/git-add.txt | 9 +++++++- builtin/add.c | 10 ++++++--- t/t1092-sparse-checkout-compatibility.sh | 28 ++++++++---------------- t/t3705-add-sparse-checkout.sh | 8 +++++++ 4 files changed, 32 insertions(+), 23 deletions(-)
Documentation/git-add.txt +8 -1
@@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git add' [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p] - [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] + [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] [--sparse] [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize] [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]
@@ -79,6 +79,13 @@ in linkgit:gitglossary[7]. --force:: Allow adding otherwise ignored files. +--sparse:: + Allow updating index entries outside of the sparse-checkout cone. + Normally, `git add` refuses to update index entries whose paths do + not fit within the sparse-checkout cone, since those files might + be removed from the working tree without warning. See + linkgit:git-sparse-checkout[1] for more. + -i:: --interactive:: Add modified contents in the working tree interactively to
builtin/add.c +7 -3
@@ -30,6 +30,7 @@ static int patch_interactive, add_interactive, edit_interactive;static int take_worktree_changes;static int add_renormalize;static int pathspec_file_nul; +static int include_sparse;static const char *pathspec_from_file;static int legacy_stash_p;/* support for the scripted `git stash` */
@@ -46,7 +47,7 @@ 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)) +if (!include_sparse &&ce_skip_worktree (ce))continue ;if (pathspec && !ce_path_match (&the_index, ce, pathspec,NULL ))
@@ -379,6 +380,7 @@ static struct option builtin_add_options[] = {OPT_BOOL (0 ,"refresh" , &refresh_only,N_ ("don't add, only refresh the index" )),OPT_BOOL (0 ,"ignore-errors" , &ignore_add_errors,N_ ("just skip files which cannot be added because of errors" )),OPT_BOOL (0 ,"ignore-missing" , &ignore_missing,N_ ("check if - even missing - files are ignored in dry run" )), +OPT_BOOL (0 ,"sparse" , &include_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_STRING (0 ,"chmod" , &chmod_arg,"(+|-)x" ,N_ ("override the executable bit of the listed files" )), OPT_HIDDEN_BOOL(0, "warn-embedded-repo", &warn_on_embedded_repo,
@@ -457,7 +459,8 @@ static int add_files(struct dir_struct *dir, int flags) }for (i =0 ; i < dir->nr; i++) { -if (!path_in_sparse_checkout (dir->entries[i]->name, &the_index)) { +if (!include_sparse && + !path_in_sparse_checkout (dir->entries[i]->name, &the_index)) {string_list_append (&only_match_skip_worktree, dir->entries[i]->name);continue ;
@@ -642,7 +645,8 @@ int cmd_add(int argc, const char **argv, const char *prefix)if (seen[i])continue ; -if (matches_skip_worktree (&pathspec, i, &skip_worktree_seen)) { +if (!include_sparse && +matches_skip_worktree (&pathspec, i, &skip_worktree_seen)) {string_list_append (&only_match_skip_worktree, pathspec.items[i].original);continue ;
t/t1092-sparse-checkout-compatibility.sh +9 -19
@@ -329,11 +329,7 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -# NEEDSWORK: Now that 'git add folder1/new' fails, the changes being -# attempted here fail for the sparse-checkout and sparse-index repos. -# We must enable a way for adding files outside the sparse-checkout -# done, even if it is by an optional flag. -test_expect_failure 'status/add: outside sparse cone' ' +test_expect_success 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -355,15 +351,16 @@ test_expect_failure 'status/add: outside sparse cone' ' test_sparse_match test_must_fail git add folder1/a && test_sparse_match test_must_fail git add --refresh folder1/a && test_sparse_match test_must_fail git add folder1/new && + test_sparse_match git add --sparse folder1/a && + test_sparse_match git add --sparse folder1/new && - # NEEDSWORK: behavior begins to deviate here. - test_all_match git add . && + test_all_match git add --sparse . && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/new && test_all_match git rev-parse HEAD^{tree} && run_on_all ../edit-contents folder1/newer && - test_all_match git add folder1/ && + test_all_match git add --sparse folder1/ && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/newer && test_all_match git rev-parse HEAD^{tree}
@@ -507,12 +504,7 @@ test_expect_success 'merge, cherry-pick, and rebase' ' done ' -# NEEDSWORK: This test is documenting current behavior, but that -# behavior can be confusing to users so there is desire to change it. -# Right now, users might be using this flow to work through conflicts, -# so any solution should present advice to users who try this sequence -# of commands to follow whatever new method we create. -test_expect_failure 'merge with conflict outside cone' ' +test_expect_success 'merge with conflict outside cone' ' init_repos && test_all_match git checkout -b merge-tip merge-left &&
@@ -528,17 +520,15 @@ test_expect_failure 'merge with conflict outside cone' ' # 2. Add the file with conflict markers # NEEDSWORK: Even though the merge conflict removed the # SKIP_WORKTREE bit from the index entry for folder1/a, we should - # warn that this is a problematic add. - test_sparse_match test_must_fail git add folder1/a && + # warn that this is a problematic add when --sparse is not set. + test_all_match git add --sparse folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. run_on_all mv folder2/a folder2/z && - # NEEDSWORK: This mode now fails, because folder2/z is - # outside of the sparse-checkout cone and does not match an - # existing index entry with the SKIP_WORKTREE bit cleared. test_sparse_match test_must_fail git add folder2 && + test_all_match git add --sparse folder2 && test_all_match git status --porcelain=v2 && test_all_match git merge --continue &&
t/t3705-add-sparse-checkout.sh +8 -0
@@ -152,4 +152,12 @@ test_expect_success 'add obeys advice.updateSparsePath' ' ' +test_expect_success 'add allows sparse entries with --sparse' ' + git sparse-checkout set a && + echo modified >sparse_entry && + test_must_fail git add sparse_entry && + git add --sparse sparse_entry 2>stderr && + test_must_be_empty stderr +' + test_done
Signature · 1 line
gitgitgadget
PATCH 8/13 add: prevent adding sparse conflict files
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When a merge results in a conflict outside of the sparse-checkout cone, the conflicted file is written to the working tree and the index entry loses the SKIP_WORKTREE bit. This allows users to add the file to the index without realizing that the file might leave the working tree in a later Git command.
Block this behavior, but keep in mind that the user can override the failure using the '--sparse' option.
--- pathspec.c | 2 +- t/t1091-sparse-checkout-builtin.sh | 4 +++- t/t1092-sparse-checkout-compatibility.sh | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-)
pathspec.c +1 -1
@@ -71,7 +71,7 @@ char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec)for (i =0 ; i < istate->cache_nr; i++) {struct cache_entry *ce = istate->cache[i]; -if (ce_skip_worktree (ce)) +if (ce_skip_worktree (ce) || !path_in_sparse_checkout (ce->name, istate))ce_path_match (istate, ce, pathspec, seen); }
t/t1091-sparse-checkout-builtin.sh +3 -1
@@ -406,7 +406,7 @@ test_expect_success 'sparse-checkout (init|set|disable) warns with unmerged stat git -C unmerged sparse-checkout disable ' -test_expect_success 'sparse-checkout reapply' ' +test_expect_failure 'sparse-checkout reapply' ' git clone repo tweak && echo dirty >tweak/deep/deeper2/a &&
@@ -438,6 +438,8 @@ test_expect_success 'sparse-checkout reapply' ' test_i18ngrep "warning.*The following paths are unmerged" err && test_path_is_file tweak/folder1/a && + # NEEDSWORK: We are asking to update a file outside of the + # sparse-checkout cone, but this is no longer allowed. git -C tweak add folder1/a && git -C tweak sparse-checkout reapply 2>err && test_must_be_empty err &&
t/t1092-sparse-checkout-compatibility.sh +3 -3
@@ -518,9 +518,8 @@ test_expect_success 'merge with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers - # NEEDSWORK: Even though the merge conflict removed the - # SKIP_WORKTREE bit from the index entry for folder1/a, we should - # warn that this is a problematic add when --sparse is not set. + test_sparse_match test_must_fail git add folder1/a && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_all_match git add --sparse folder1/a && test_all_match git status --porcelain=v2 &&
@@ -528,6 +527,7 @@ test_expect_success 'merge with conflict outside cone' ' # accept conflict markers as resolved content. run_on_all mv folder2/a folder2/z && test_sparse_match test_must_fail git add folder2 && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_all_match git add --sparse folder2 && test_all_match git status --porcelain=v2 &&
Signature · 1 line
gitgitgadget
PATCH 9/13 rm: add --sparse option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
--- Documentation/git-rm.txt | 6 ++++++ builtin/rm.c | 8 ++++++-- t/t3602-rm-sparse-checkout.sh | 14 +++++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-)
Documentation/git-rm.txt +6 -0
@@ -72,6 +72,12 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7]. --ignore-unmatch:: Exit with a zero status even if no files matched. +--sparse:: + Allow updating index entries outside of the sparse-checkout cone. + Normally, `git rm` refuses to update index entries whose paths do + not fit within the sparse-checkout cone. See + linkgit:git-sparse-checkout[1] for more. + -q:: --quiet:: `git rm` normally outputs one line (in the form of an `rm` command)
builtin/rm.c +6 -2
@@ -237,6 +237,7 @@ static int check_local_mod(struct object_id *head, int index_only)static int show_only =0 , force =0 , index_only =0 , recursive =0 , quiet =0 ;static int ignore_unmatch =0 , pathspec_file_nul; +static int include_sparse;static char *pathspec_from_file;static struct option builtin_rm_options[] = {
@@ -247,6 +248,7 @@ static struct option builtin_rm_options[] = {OPT_BOOL ('r' ,NULL , &recursive,N_ ("allow recursive removal" )),OPT_BOOL (0 ,"ignore-unmatch" , &ignore_unmatch,N_ ("exit with a zero status even if nothing matched" )), +OPT_BOOL (0 ,"sparse" , &include_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_PATHSPEC_FROM_FILE (&pathspec_from_file),OPT_PATHSPEC_FILE_NUL (&pathspec_file_nul), OPT_END(),
@@ -298,7 +300,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix)ensure_full_index (&the_index);for (i =0 ; i < active_nr; i++) {const struct cache_entry *ce = active_cache[i]; -if (ce_skip_worktree (ce)) + +if (!include_sparse &&ce_skip_worktree (ce))continue ;if (!ce_path_match (&the_index, ce, &pathspec, seen))continue ;
@@ -322,7 +325,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix) seen_any =1 ; else if (ignore_unmatch) continue; - else if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) + else if (!include_sparse && + 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);
t/t3602-rm-sparse-checkout.sh +13 -1
@@ -36,13 +36,25 @@ done test_expect_success 'recursive rm does not remove sparse entries' ' git reset --hard && - git sparse-checkout set sub/dir && + 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 'recursive rm --sparse removes sparse entries' ' + git reset --hard && + git sparse-checkout set "sub/dir" && + git rm --sparse -r sub && + git status --porcelain -uno >actual && + cat >expected <<-\EOF && + D sub/d + D sub/dir/e + EOF + test_cmp expected actual +' + test_expect_success 'rm obeys advice.updateSparsePath' ' git reset --hard && git sparse-checkout set a &&
Signature · 1 line
gitgitgadget
PATCH 10/13 rm: skip sparse paths with missing SKIP_WORKTREE
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
If a path does not match the sparse-checkout cone but is somehow missing the SKIP_WORKTREE bit, then 'git rm' currently succeeds in removing the file. One reason a user might be in this situation is a merge conflict outside of the sparse-checkout cone. Removing such a file might be problematic for users who are not sure what they are doing.
Add a check to path_in_sparse_checkout() when 'git rm' is checking if a path should be considered for deletion. Of course, this check is ignored if the '--sparse' option is specified, allowing users who accept the risks to continue with the removal.
--- builtin/rm.c | 4 +++- t/t3602-rm-sparse-checkout.sh | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-)
builtin/rm.c +3 -1
@@ -301,7 +301,9 @@ 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 (!include_sparse &&ce_skip_worktree (ce)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;if (!ce_path_match (&the_index, ce, &pathspec, seen))continue ;
t/t3602-rm-sparse-checkout.sh +11 -0
@@ -87,4 +87,15 @@ test_expect_success 'do not warn about sparse entries with --ignore-unmatch' ' git ls-files --error-unmatch b ' +test_expect_success 'refuse to rm a non-skip-worktree path outside sparse cone' ' + git reset --hard && + git sparse-checkout set a && + git update-index --no-skip-worktree b && + test_must_fail git rm b 2>stderr && + test_cmp b_error_and_hint stderr && + git rm --sparse b 2>stderr && + test_must_be_empty stderr && + test_path_is_missing b +' + test_done
Signature · 1 line
gitgitgadget
PATCH 11/13 mv: refuse to move sparse paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Since cmd_mv() does not operate on cache entries and instead directly checks the filesystem, we can only use path_in_sparse_checkout() as a mechanism for seeing if a path is sparse or not. Be sure to skip returning a failure if '-k' is specified.
--- builtin/mv.c | 19 +++++++ t/t7002-mv-sparse-checkout.sh | 99 +++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100755 t/t7002-mv-sparse-checkout.sh
builtin/mv.c +19 -0
@@ -133,6 +133,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)struct string_list src_for_dst =STRING_LIST_INIT_NODUP ;struct lock_file lock_file =LOCK_INIT ;struct cache_entry *ce; +struct string_list only_match_skip_worktree =STRING_LIST_INIT_NODUP ;git_config (git_default_config,NULL );
@@ -176,10 +177,22 @@ int cmd_mv(int argc, const char **argv, const char *prefix)const char *src = source[i], *dst = destination[i];int length, src_is_dir;const char *bad =NULL ; +int skip_sparse =0 ;if (show_only)printf (_ ("Checking rename of '%s' to '%s'\n" ), src, dst); +if (!path_in_sparse_checkout (src, &the_index)) { +string_list_append (&only_match_skip_worktree, src); + skip_sparse =1 ; + } +if (!path_in_sparse_checkout (dst, &the_index)) { +string_list_append (&only_match_skip_worktree, dst); + skip_sparse =1 ; + } +if (skip_sparse) +continue ; + length =strlen (src);if (lstat (src, &st) <0 ) bad =_ ("bad source" );
@@ -266,6 +279,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix) } } +if (only_match_skip_worktree.nr) { +advise_on_updating_sparse_paths (&only_match_skip_worktree); +if (!ignore_errors) +return 1 ; + } +for (i =0 ; i < argc; i++) {const char *src = source[i], *dst = destination[i];enum update_mode mode = modes[i];
t/t7002-mv-sparse-checkout.sh new file +99 -0
99 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH 12/13 mv: add '--sparse' option to ignore sparse-checkout
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Users can get into strange situations if 'git mv' allows moving files into, out of, or around the sparse-checkout cone. However, some users may still want to do it. Allow knowledgeable users to do so via a new '--sparse' option.
There are some special cases that occur in this change, such as the case of a directory that doesn't match the sparse-checkout cone, but exists in the working tree because a subset of its contents do match. We need to communicate that index entries with the SKIP_WORKTREE bit are not expected to be in the working directory and hence are not needed when moving the contents. This is only a check for the existence of the source file. The call to rename_cache_entry_at() still changes the index appropriately in these cases.
--- builtin/mv.c | 24 ++++++++++++++---------- t/t7002-mv-sparse-checkout.sh | 34 ++++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 16 deletions(-)
builtin/mv.c +14 -10
@@ -118,17 +118,18 @@ static int index_range_of_same_dir(const char *src, int length,int cmd_mv (int argc,const char **argv,const char *prefix) {int i, flags, gitmodules_modified =0 ; -int verbose =0 , show_only =0 , force =0 , ignore_errors =0 ; +int verbose =0 , show_only =0 , force =0 , ignore_errors =0 , ignore_sparse =0 ;struct option builtin_mv_options[] = {OPT__VERBOSE (&verbose,N_ ("be verbose" )),OPT__DRY_RUN (&show_only,N_ ("dry run" )),OPT__FORCE (&force,N_ ("force move/rename even if target exists" ),PARSE_OPT_NOCOMPLETE ),OPT_BOOL ('k' ,NULL , &ignore_errors,N_ ("skip move/rename errors" )), +OPT_BOOL (0 ,"sparse" , &ignore_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_END (), };const char **source, **destination, **dest_path, **submodule_gitfile; -enum update_mode {BOTH =0 ,WORKING_DIRECTORY ,INDEX } *modes; +enum update_mode {BOTH =0 ,WORKING_DIRECTORY ,INDEX ,SPARSE } *modes;struct stat st;struct string_list src_for_dst =STRING_LIST_INIT_NODUP ;struct lock_file lock_file =LOCK_INIT ;
@@ -182,11 +183,11 @@ int cmd_mv(int argc, const char **argv, const char *prefix)if (show_only)printf (_ ("Checking rename of '%s' to '%s'\n" ), src, dst); -if (!path_in_sparse_checkout (src, &the_index)) { +if (!ignore_sparse && !path_in_sparse_checkout (src, &the_index)) {string_list_append (&only_match_skip_worktree, src); skip_sparse =1 ; } -if (!path_in_sparse_checkout (dst, &the_index)) { +if (!ignore_sparse && !path_in_sparse_checkout (dst, &the_index)) {string_list_append (&only_match_skip_worktree, dst); skip_sparse =1 ; }
@@ -194,9 +195,11 @@ int cmd_mv(int argc, const char **argv, const char *prefix)continue ; length =strlen (src); -if (lstat (src, &st) <0 ) - bad =_ ("bad source" ); -else if (!strncmp (src, dst, length) && +if (lstat (src, &st) <0 ) { +/* only error if existence is expected. */ +if (modes[i] !=SPARSE ) + bad =_ ("bad source" ); + }else if (!strncmp (src, dst, length) && (dst[length] ==0 || dst[length] =='/' )) { bad =_ ("can not move directory into itself" ); } else if ((src_is_dir = S_ISDIR(st.st_mode))
@@ -225,11 +228,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix) dst_len =strlen (dst);for (j =0 ; j < last - first; j++) { -const char *path = active_cache[first + j]->name; +const struct cache_entry *ce = active_cache[first + j]; +const char *path = ce->name; source[argc + j] = path; destination[argc + j] =prefix_path (dst, dst_len, path + length +1 ); - modes[argc + j] =INDEX ; + modes[argc + j] =ce_skip_worktree (ce) ?SPARSE :INDEX ; submodule_gitfile[argc + j] =NULL ; } argc += last - first;
@@ -293,7 +297,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)printf (_ ("Renaming %s to %s\n" ), src, dst);if (show_only)continue ; -if (mode !=INDEX &&rename (src, dst) <0 ) { +if (mode !=INDEX && mode !=SPARSE &&rename (src, dst) <0 ) {if (ignore_errors)continue ;die_errno (_ ("renaming '%s' failed" ), src);
t/t7002-mv-sparse-checkout.sh +28 -6
@@ -31,7 +31,9 @@ test_expect_success 'mv refuses to move sparse-to-sparse' ' echo b >>expect && echo e >>expect && cat sparse_hint >>expect && - test_cmp expect stderr + test_cmp expect stderr && + git mv --sparse b e 2>stderr && + test_must_be_empty stderr ' test_expect_success 'mv refuses to move sparse-to-sparse, ignores failure' '
@@ -44,7 +46,9 @@ test_expect_success 'mv refuses to move sparse-to-sparse, ignores failure' ' echo b >>expect && echo e >>expect && cat sparse_hint >>expect && - test_cmp expect stderr + test_cmp expect stderr && + git mv --sparse -k b e 2>stderr && + test_must_be_empty stderr ' test_expect_success 'mv refuses to move non-sparse-to-sparse' '
@@ -55,7 +59,9 @@ test_expect_success 'mv refuses to move non-sparse-to-sparse' ' cat sparse_error_header >expect && echo e >>expect && cat sparse_hint >>expect && - test_cmp expect stderr + test_cmp expect stderr && + git mv --sparse a e 2>stderr && + test_must_be_empty stderr ' test_expect_success 'mv refuses to move sparse-to-non-sparse' '
@@ -67,7 +73,9 @@ test_expect_success 'mv refuses to move sparse-to-non-sparse' ' cat sparse_error_header >expect && echo b >>expect && cat sparse_hint >>expect && - test_cmp expect stderr + test_cmp expect stderr && + git mv --sparse b e 2>stderr && + test_must_be_empty stderr ' test_expect_success 'recursive mv refuses to move (possible) sparse' '
@@ -80,7 +88,14 @@ test_expect_success 'recursive mv refuses to move (possible) sparse' ' echo sub >>expect && echo sub2 >>expect && cat sparse_hint >>expect && - test_cmp expect stderr + test_cmp expect stderr && + git mv --sparse sub sub2 2>stderr && + test_must_be_empty stderr && + git commit -m "moved sub to sub2" && + git rev-parse HEAD~1:sub >expect && + git rev-parse HEAD:sub2 >actual && + test_cmp expect actual && + git reset --hard HEAD~1 ' test_expect_success 'recursive mv refuses to move sparse' '
@@ -93,7 +108,14 @@ test_expect_success 'recursive mv refuses to move sparse' ' echo sub/dir2/e >>expect && echo sub2/dir2/e >>expect && cat sparse_hint >>expect && - test_cmp expect stderr + test_cmp expect stderr && + git mv --sparse sub sub2 2>stderr && + test_must_be_empty stderr && + git commit -m "moved sub to sub2" && + git rev-parse HEAD~1:sub >expect && + git rev-parse HEAD:sub2 >actual && + test_cmp expect actual && + git reset --hard HEAD~1 ' test_done
Signature · 1 line
gitgitgadget
PATCH 13/13 advice: update message to suggest '--sparse'
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The previous changes modified the behavior of 'git add', 'git rm', and 'git mv' to not adjust paths outside the sparse-checkout cone, even if they exist in the working tree and their cache entries lack the SKIP_WORKTREE bit. The intention is to warn users that they are doing something potentially dangerous. The '--sparse' option was added to each command to allow careful users the same ability they had before.
To improve the discoverability of this new functionality, add a message to advice.updateSparsePath that mentions the existence of the option.
--- advice.c | 3 ++- t/t3602-rm-sparse-checkout.sh | 2 +- t/t3705-add-sparse-checkout.sh | 2 +- t/t7002-mv-sparse-checkout.sh | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-)
advice.c +2 -1
@@ -300,7 +300,8 @@ void advise_on_updating_sparse_paths(struct string_list *pathspec_list)fprintf (stderr,"%s\n" , item->string);advise_if_enabled (ADVICE_UPDATE_SPARSE_PATH , -_ ("Disable or modify the sparsity rules if you intend" +_ ("Disable or modify the sparsity rules or" +" use the --sparse option if you intend" " to update such entries." )); }
t/t3602-rm-sparse-checkout.sh +1 -1
@@ -16,7 +16,7 @@ test_expect_success 'setup' " EOF cat >sparse_hint <<-EOF && - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
t/t3705-add-sparse-checkout.sh +1 -1
@@ -43,7 +43,7 @@ test_expect_success 'setup' " EOF cat >sparse_hint <<-EOF && - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
t/t7002-mv-sparse-checkout.sh +1 -1
@@ -16,7 +16,7 @@ test_expect_success 'setup' " EOF cat >sparse_hint <<-EOF - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF "
Signature · 1 line
gitgitgadget
Re: [PATCH 05/13] add: fail when adding an untracked sparse file
In reply to Derrick Stolee via GitGitGadgetQuoted 10 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
The
add_files()method inbuiltin/add.ctakes a set of untracked files that are being added by the input pathspec and inserts them into the index. If these files are outside of the sparse-checkout cone, then they gain theSKIP_WORKTREEbit at some point. However, this was not checked before inserting into the index, so these files are added even though we want to avoid modifying the index outside of the sparse-checkout cone.
Quoted 2 lines
Add a check within
add_files()for these files and write the advice about files outside of the sprase-checkout cone.
s/sprase/sparse/
Quoted 7 lines
This behavior change modifies some existing tests within t1092. These tests intended to document how a user could interact with the existing behavior in place. Many of these tests need to be marked as expecting failure. A future change will allow these tests to pass by adding a flag to 'git add' that allows users to modify index entries outside of the sparse-checkout cone.
Quoted 5 lines
The 'submodule handling' test is intended to document what happens to directories that contain a submodule when the sparse index is enabled. It is not trying to say that users should be able to add submodules outside of the sparse-checkout cone, so that test can be modified to avoid that operation.
While I was playing with this patch, I did the following:
echo a >a
echo b >b
git add .
git commit -m files
git sparse-checkout set a
echo c >c
git add c
And the last git add was successful in adding the untracked c file which is outside the sparse checkout. I'm not sure if I'm doing something wrong, but it seems that path_in_sparse_checkout() returns UNDECIDED for c. Is it because there was no pattern in the list explicitly excluding it? And if so, should we consider UNDECIDED as NOT_MATCHED for path_in_sparse_checkout()?
Quoted 11 lines
Signed-off-by Derrick Stolee <[email protected]>--- builtin/add.c | 14 ++++++++++ t/t1092-sparse-checkout-compatibility.sh | 33 +++++++++++++++++------- 2 files changed, 38 insertions(+), 9 deletions(-)builtin/add.c +0
-0
builtin/add.c static void check_embedded_repo(const char *path) · line 446
Quoted 4 lines
builtin/add.c +1
-0@@ -443,6 +443,7 @@ static void check_embedded_repo(const char *path)static int add_files (struct dir_struct *dir,int flags) {int i, exit_status =0 ; +struct string_list only_match_skip_worktree =STRING_LIST_INIT_NODUP ;
I see this reuses the logic from cmd_add() and refresh(). But since we are operating on untracked files here, perhaps we could replace "skip_worktree" by "sparse_paths" or something similar?
Quoted 15 lines
builtin/add.c +5
-0if (dir->ignored_nr) {fprintf (stderr,_ (ignore_error));@@ -456,6 +457,11 @@ static int add_files(struct dir_struct *dir, int flags) }for (i =0 ; i < dir->nr; i++) { +if (!path_in_sparse_checkout (dir->entries[i]->name, &the_index)) { +string_list_append (&only_match_skip_worktree, + dir->entries[i]->name); +continue ; + }if (add_file_to_index (&the_index, dir->entries[i]->name, flags)) {if (!ignore_add_errors)die (_ ("adding files failed" ));
builtin/add.c static int add_files(struct dir_struct *dir, int flags) · line 475
Quoted 6 lines
builtin/add.c +3
-0@@ -464,6 +470,14 @@ static int add_files(struct dir_struct *dir, int flags)check_embedded_repo (dir->entries[i]->name); } } + +if (only_match_skip_worktree.nr) { +advise_on_updating_sparse_paths (&only_match_skip_worktree);
Hmm, advise_on_updating_sparse_paths() takes a list of pathspecs that only matched sparse paths, but here we are passing a list of actual pathnames... Well, these are technically pathspecs too, but the advice message may be confusing.
For example, if we ran git add *.c on a repo with the untracked files d1/file.c and d2/file.c, we will get:
The following pathspecs didn't match any eligible path, but they do match index entries outside the current sparse checkout:d1/file.cd2/file.c
However, d1/file.c and d2/file.c are neither index entries nor the pathspecs that the user has given to git add. So perhaps we need to change the error/advice message?
Quoted 6 lines
builtin/add.c +4
-0+ exit_status =1 ; + } +string_list_clear (&only_match_skip_worktree,0 ); +return exit_status; }
Re: [PATCH 06/13] add: skip paths that are outside sparse-checkout cone
In reply to Derrick Stolee via GitGitGadgetQuoted 2 lines · Derrick Stolee via GitGitGadget
Subject: [PATCH 06/13] add: skip paths that are outside sparse-checkout cone
Perhaps this could be "skip _tracked_ paths that ..." (to help differentiate the end goal of this patch from the previous one).
pathspec.c void add_pathspec_matches_against_index(const struct pathspec *pathspec, · line 43
Quoted 11 lines
pathspec.c +2
-1@@ -39,7 +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)) +if (sw_action ==PS_IGNORE_SKIP_WORKTREE && + (ce_skip_worktree (ce) || !path_in_sparse_checkout (ce->name, istate)))
Hmm, even though we skip the sparse paths here, cmd_add() will call add_files_to_cache() at the end and still update these paths in the index. I think there are two ways to fix this. We could either change run_diff_files() to skip these paths (but I don't know how other callers of this functions want to handle this, so maybe this needs to hide behind an option flag):
diff-lib.c +2 -1
@@ -198,7 +198,8 @@ int run_diff_files(struct rev_info *revs, unsigned int option)continue ; } -if (ce_uptodate (ce) ||ce_skip_worktree (ce)) +if (ce_uptodate (ce) ||ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, istate))continue ;/*
Or we could change the callback in add itself:
builtin/add.c +4 -0
@@ -94,6 +94,10 @@ static void update_callback(struct diff_queue_struct *q,for (i =0 ; i < q->nr; i++) {struct diff_filepair *p = q->queue[i];const char *path = p->one->path; + +if (!path_in_sparse_checkout (path, &the_index)) +continue ; +switch (fix_unmerged_status (p, data)) {default :die (_ ("unexpected diff status %c" ), p->status);
I believe we also need to update a few other places to use the (ce_skip_worktree(ce) || !path_in_sparse_checkout()) logic in order to avoid updating tracked sparse paths: chmod_pathspec() for add's --chmod option, renormalize_tracked_files() for --renormalize, and read-cache.c:refresh_index() for --refresh.
pathspec.c void add_pathspec_matches_against_index(const struct pathspec *pathspec, · line 46
Quoted 3 lines
pathspec.c +0
-0continue ;ce_path_match (istate, ce, pathspec, seen); }
Hmm, don't we also want to update find_pathspecs_matching_skip_worktree() in this patch to use path_in_sparse_checkout()? I see you did that in patch 8, but I think this should be together with this current patch as, without it, we stop adding tracked sparse paths but we print no error/advice message about it.
Re: [PATCH 07/13] add: implement the --sparse option
In reply to Derrick Stolee via GitGitGadgetbuiltin/add.c static int chmod_pathspec(struct pathspec *pathspec, char flip, int show_only) · line 51
Quoted 13 lines · Derrick Stolee via GitGitGadget
builtin/add.c +1
-1@@ -46,7 +47,7 @@ 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)) +if (!include_sparse &&ce_skip_worktree (ce))continue ;
Not related to this change but just as a reminder: if you agree with my suggestion from the previous patch to use the skipping logic at run_diff_files() or update_callback(), we also need to override it in this patch when include_sparse is true.
Re: [PATCH 08/13] add: prevent adding sparse conflict files
In reply to Derrick Stolee via GitGitGadgetQuoted 11 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
When a merge results in a conflict outside of the sparse-checkout cone, the conflicted file is written to the working tree and the index entry loses the
SKIP_WORKTREEbit. This allows users to add the file to the index without realizing that the file might leave the working tree in a later Git command.Block this behavior, but keep in mind that the user can override the failure using the '--sparse' option.
Hmm, didn't we already block this behavior at patch 6?
Nevertheless, as I mentioned there, I think the change to find_pathspecs_matching_skip_worktree() from this patch should be together with the other changes from 6.
Re: [PATCH 09/13] rm: add --sparse option
In reply to Derrick Stolee via GitGitGadgetQuoted 2 lines · Derrick Stolee via GitGitGadget
Subject: [PATCH 09/12] rm: add --sparse option
Maybe mention in the commit message that, for now, rm's --sparse only affects entries with the skip_worktree bit set? (Which will be changed in the following patch.)
builtin/rm.c int cmd_rm(int argc, const char **argv, const char *prefix) · line 305
Quoted 10 lines
builtin/rm.c +2
-1@@ -298,7 +300,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix)ensure_full_index (&the_index);for (i =0 ; i < active_nr; i++) {const struct cache_entry *ce = active_cache[i]; -if (ce_skip_worktree (ce)) + +if (!include_sparse &&ce_skip_worktree (ce))continue ;
OK, we no longer ignore the skip_worktree entry if --sparse is given ...
builtin/rm.c int cmd_rm(int argc, const char **argv, const char *prefix) · line 330
Quoted 10 lines
builtin/rm.c +2
-1if (!ce_path_match (&the_index, ce, &pathspec, seen))continue ;@@ -322,7 +325,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix) seen_any =1 ; else if (ignore_unmatch) continue; - else if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) + else if (!include_sparse && + matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) string_list_append(&only_match_skip_worktree, original);
... and we also don't need to look for matches here as we won't display the error/advice match.
t/t3602-rm-sparse-checkout.sh done · line 39
Quoted 12 lines
builtin/rm.c +0
-0else die (_ ("pathspec '%s' did not match any files" ), original);t/t3602-rm-sparse-checkout.sh +1
-1@@ -36,13 +36,25 @@ done test_expect_success 'recursive rm does not remove sparse entries' ' git reset --hard && - git sparse-checkout set sub/dir && + git sparse-checkout set sub/dir/ &&
Is this change necessary?
Quoted 7 lines
t/t3602-rm-sparse-checkout.sh +1
-0git rm -r sub && git status --porcelain -uno >actual && echo "D sub/dir/e" >expected && test_cmp expected actual ' +test_expect_success 'recursive rm --sparse removes sparse entries' '
t/t3602-rm-sparse-checkout.sh done · line 56
Quoted 10 lines
t/t3602-rm-sparse-checkout.sh +10
-0+ git reset --hard && + git sparse-checkout set "sub/dir" && + git rm --sparse -r sub && + git status --porcelain -uno >actual && + cat >expected <<-\EOF && + D sub/d + D sub/dir/e + EOF + test_cmp expected actual +'
Nice!
Re: [PATCH 10/13] rm: skip sparse paths with missing SKIP_WORKTREE
In reply to Derrick Stolee via GitGitGadgetQuoted 6 lines · Derrick Stolee via GitGitGadget
builtin/rm.c +0
-0
builtin/rm.c int cmd_rm(int argc, const char **argv, const char *prefix) · line 307
Quoted 8 lines
builtin/rm.c +3
-1@@ -301,7 +301,9 @@ 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 (!include_sparse &&ce_skip_worktree (ce)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;
OK. And we don't need to update the advice reporting code below, because matches_skip_worktree() already uses the (ce_skip_worktree(ce) || !path_in_sparse_checkout()) logic.
Quoted 12 lines
builtin/rm.c +1
-0+ !path_in_sparse_checkout" if (!ce_path_match (&the_index, ce, &pathspec, seen)) continue;t/t3602-rm-sparse-checkout.sh +0
-0@@ -87,4 +87,15 @@ test_expect_success 'do not warn about sparse entries with --ignore-unmatch' ' git ls-files --error-unmatch b '
t/t3602-rm-sparse-checkout.sh test_expect_success 'do not warn about sparse entries with --ignore-unmatch' ' · line 99
Quoted 10 lines
t/t3602-rm-sparse-checkout.sh +10
-0+test_expect_success 'refuse to rm a non-skip-worktree path outside sparse cone' ' + git reset --hard && + git sparse-checkout set a && + git update-index --no-skip-worktree b && + test_must_fail git rm b 2>stderr && + test_cmp b_error_and_hint stderr && + git rm --sparse b 2>stderr && + test_must_be_empty stderr && + test_path_is_missing b +'
OK.
Re: [PATCH 11/13] mv: refuse to move sparse paths
In reply to Derrick Stolee via GitGitGadgetQuoted 6 lines · Derrick Stolee via GitGitGadget
builtin/mv.c +0
-0
builtin/mv.c int cmd_mv(int argc, const char **argv, const char *prefix) · line 185
Quoted 9 lines
builtin/mv.c +2
-0@@ -176,10 +177,22 @@ int cmd_mv(int argc, const char **argv, const char *prefix)const char *src = source[i], *dst = destination[i];int length, src_is_dir;const char *bad =NULL ; +int skip_sparse =0 ;if (show_only)printf (_ ("Checking rename of '%s' to '%s'\n" ), src, dst); +if (!path_in_sparse_checkout (src, &the_index)) {
git mv can only move/rename tracked paths, but since we check whether src is sparse before checking if it is in the index, the user will get the sparse error message instead. This is OK, but the advice might be misleading, as it says they can use --sparse if they really want to move the file, but repeating the command with --sparse will now fail for another reason. I wonder if we should check whether src is tracked before checking if it is sparse, or if that is not really an issue we should bother with.
Quoted 35 lines
builtin/mv.c +10
-0+string_list_append (&only_match_skip_worktree, src); + skip_sparse =1 ; + } +if (!path_in_sparse_checkout (dst, &the_index)) { +string_list_append (&only_match_skip_worktree, dst); + skip_sparse =1 ; + } +if (skip_sparse) +continue ; + length =strlen (src);if (lstat (src, &st) <0 ) bad =_ ("bad source" );t/t7002-mv-sparse-checkout.sh new file +15
-0@@ -0,0 +1,99 @@ +#!/bin/sh + +test_description='git mv in sparse working trees' + +. ./test-lib.sh + +test_expect_success 'setup' " + mkdir -p sub/dir sub/dir2 && + touch a b c sub/d sub/dir/e sub/dir2/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:
t/t7002-mv-sparse-checkout.sh line 25
Quoted 10 lines
t/t7002-mv-sparse-checkout.sh new file +10
-0+ 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 +" + +test_expect_success 'mv refuses to move sparse-to-sparse' ' + rm -f e &&
At first glance, it confused me a bit that we are removing e when the setup didn't create it. But then I realized the test itself might create e if git mv succeeds in moving the b file. Could perhaps this and the others rm -f e be a test_when_finished, to make it clearer that it is a cleanup?
t/t7002-mv-sparse-checkout.sh line 29
Quoted 4 lines
t/t7002-mv-sparse-checkout.sh new file +4
-0+ git reset --hard && + git sparse-checkout set a && + touch b && + test_must_fail git mv b e 2>stderr &&
Here we try to move a "tracked sparse path" to an "untracked sparse path". Do we also want to test with a tracked to tracked operation? (Although the code path will be the same, of course.)
t/t7002-mv-sparse-checkout.sh line 42
Quoted 13 lines
t/t7002-mv-sparse-checkout.sh new file +13
-0+ cat sparse_error_header >expect && + echo b >>expect && + echo e >>expect && + cat sparse_hint >>expect && + test_cmp expect stderr +' + +test_expect_success 'mv refuses to move sparse-to-sparse, ignores failure' ' + rm -f e && + git reset --hard && + git sparse-checkout set a && + touch b && + git mv -k b e 2>stderr &&
Maybe also check that b is still there, and e is missing?
Quoted 7 lines
t/t7002-mv-sparse-checkout.sh new file +7
-0+ cat sparse_error_header >expect && + echo b >>expect && + echo e >>expect && + cat sparse_hint >>expect && + test_cmp expect stderr +' +
t/t7002-mv-sparse-checkout.sh line 59
Quoted 10 lines
t/t7002-mv-sparse-checkout.sh new file +10
-0+test_expect_success 'mv refuses to move non-sparse-to-sparse' ' + rm -f e && + git reset --hard && + git sparse-checkout set a && + test_must_fail git mv a e 2>stderr && + cat sparse_error_header >expect && + echo e >>expect && + cat sparse_hint >>expect && + test_cmp expect stderr +'
OK.
t/t7002-mv-sparse-checkout.sh line 71
Quoted 11 lines
t/t7002-mv-sparse-checkout.sh new file +11
-0+test_expect_success 'mv refuses to move sparse-to-non-sparse' ' + rm -f e && + git reset --hard && + git sparse-checkout set a e && + touch b && + test_must_fail git mv b e 2>stderr && + cat sparse_error_header >expect && + echo b >>expect && + cat sparse_hint >>expect && + test_cmp expect stderr +'
OK.
Quoted 16 lines
t/t7002-mv-sparse-checkout.sh new file +16
-0+test_expect_success 'recursive mv refuses to move (possible) sparse' ' + rm -f e && + git reset --hard && + # Without cone mode, "sub" and "sub2" do not match + git sparse-checkout set sub/dir sub2/dir && + test_must_fail git mv sub sub2 2>stderr && + cat sparse_error_header >expect && + echo sub >>expect && + echo sub2 >>expect && + cat sparse_hint >>expect && + test_cmp expect stderr +' + +test_expect_success 'recursive mv refuses to move sparse' ' + git reset --hard && + # Use cone mode so "sub/" matches the sparse-checkout patterns
t/t7002-mv-sparse-checkout.sh line 98
Quoted 10 lines
t/t7002-mv-sparse-checkout.sh new file +10
-0+ git sparse-checkout init --cone && + git sparse-checkout set sub/dir sub2/dir && + test_must_fail git mv sub sub2 2>stderr && + cat sparse_error_header >expect && + echo sub/dir2/e >>expect && + echo sub2/dir2/e >>expect && + cat sparse_hint >>expect && + test_cmp expect stderr +' +
Ah, these last two are very interesting cases!
Re: [PATCH 05/13] add: fail when adding an untracked sparse file
In reply to Matheus Tavares BernardinoQuoted 11 lines · Matheus Tavares Bernardino
While I was playing with this patch, I did the following:
echo a >a
echo b >b
git add .
git commit -m files
git sparse-checkout set a
echo c >c
git add c
Quoted 6 lines
And the last
git addwas successful in adding the untrackedcfile which is outside the sparse checkout. I'm not sure if I'm doing something wrong, but it seems thatpath_in_sparse_checkout()returns UNDECIDED forc. Is it because there was no pattern in the list explicitly excluding it? And if so, should we consider UNDECIDED asNOT_MATCHEDforpath_in_sparse_checkout()?
Please disconsider this, It was my fault indeed. I had applied the patches onto the wrong base. Now I fetched them again but from the GGG tag, and my manual test worked as expected.
Re: [PATCH 11/13] mv: refuse to move sparse paths
In reply to Matheus Tavares BernardinoQuoted 17 lines · Matheus Tavares Bernardino
Quoted 14 lines · Derrick Stolee via GitGitGadget
builtin/mv.c +1
-0@@ -176,10 +177,22 @@ int cmd_mv(int argc, const char **argv, const char *prefix)const char *src = source[i], *dst = destination[i];int length, src_is_dir;const char *bad =NULL ; +int skip_sparse =0 ;if (show_only)printf (_ ("Checking rename of '%s' to '%s'\n" ), src, dst);
Quoted 10 lines
builtin/mv.c int cmd_mv(int argc, const char **argv, const char *prefix) · line 185
Quoted 1 line
builtin/mv.c +1
-0+if (!path_in_sparse_checkout (src, &the_index)) {
git mvcan only move/rename tracked paths, but since we check whethersrcis sparse before checking if it is in the index, the user will get the sparse error message instead. This is OK, but the advice might be misleading, as it says they can use--sparseif they really want to move the file, but repeating the command with--sparsewill now fail for another reason. I wonder if we should check whethersrcis tracked before checking if it is sparse, or if that is not really an issue we should bother with.
Another problem is that the displayed message will say that the pathspecs "match index entries outside sparse checkout" even when the path given to mv doesn't really exist:
git sparse-checkout set some/dir/
git mv nonexistent-file foo
The following pathspecs didn't match any eligible path, but they do match index entries outside the current sparse checkout:
nonexistent-file
hint: Disable or modify the sparsity rules if you intend to update such entries.
hint: Disable this message with "git config advice.updateSparsePath false"
Re: [PATCH 12/13] mv: add '--sparse' option to ignore sparse-checkout
In reply to Derrick Stolee via GitGitGadgetQuoted 8 lines · Derrick Stolee via GitGitGadget
builtin/mv.c +0
-0@@ -118,17 +118,18 @@ static int index_range_of_same_dir(const char *src, int length,int cmd_mv (int argc,const char **argv,const char *prefix) {
builtin/mv.c static int index_range_of_same_dir(const char *src, int length, · line 128
Quoted 10 lines
builtin/mv.c +2
-1int i, flags, gitmodules_modified =0 ; -int verbose =0 , show_only =0 , force =0 , ignore_errors =0 ; +int verbose =0 , show_only =0 , force =0 , ignore_errors =0 , ignore_sparse =0 ;struct option builtin_mv_options[] = {OPT__VERBOSE (&verbose,N_ ("be verbose" )),OPT__DRY_RUN (&show_only,N_ ("dry run" )),OPT__FORCE (&force,N_ ("force move/rename even if target exists" ),PARSE_OPT_NOCOMPLETE ),OPT_BOOL ('k' ,NULL , &ignore_errors,N_ ("skip move/rename errors" )), +OPT_BOOL (0 ,"sparse" , &ignore_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),
Should this include a doc update too?
Quoted 23 lines
builtin/mv.c +3
-3OPT_END(), };const char **source, **destination, **dest_path, **submodule_gitfile; -enum update_mode {BOTH =0 ,WORKING_DIRECTORY ,INDEX } *modes; +enum update_mode {BOTH =0 ,WORKING_DIRECTORY ,INDEX ,SPARSE } *modes;struct stat st;struct string_list src_for_dst =STRING_LIST_INIT_NODUP ;struct lock_file lock_file =LOCK_INIT ;@@ -182,11 +183,11 @@ int cmd_mv(int argc, const char **argv, const char *prefix)if (show_only)printf (_ ("Checking rename of '%s' to '%s'\n" ), src, dst); -if (!path_in_sparse_checkout (src, &the_index)) { +if (!ignore_sparse && !path_in_sparse_checkout (src, &the_index)) {string_list_append (&only_match_skip_worktree, src); skip_sparse =1 ; } -if (!path_in_sparse_checkout (dst, &the_index)) { +if (!ignore_sparse && !path_in_sparse_checkout (dst, &the_index)) {string_list_append (&only_match_skip_worktree, dst); skip_sparse =1 ; }
builtin/mv.c int cmd_mv(int argc, const char **argv, const char *prefix) · line 201
Quoted 10 lines
builtin/mv.c +4
-3@@ -194,9 +195,11 @@ int cmd_mv(int argc, const char **argv, const char *prefix)continue ; length =strlen (src); -if (lstat (src, &st) <0 ) - bad =_ ("bad source" ); - else if (!strncmp(src, dst, length) && +if (lstat (src, &st) <0 ) { +/* only error if existence is expected. */ +if (modes[i] !=SPARSE ) + bad =_ ("bad source" );
OK, so this is about the directories which contain sparse entries in it, right? In this case, we don't expect such entries to be in the working tree, so we don't error out if they are missing and still let the parent directory be moved.
This made me wonder about a slightly different case: would it be interesting to also allow git mv --sparse to rename a single sparse entry even if it's not in the working tree? I mean, something like:
echo a >a
echo b >b
git add a b
git commit -m files
git sparse-checkout set a
git mv --sparse b c
This currently wouldn't be allowed because "b" is not in the working tree ("fatal: bad source" error). But, perhaps, would it be interesting to allow the index to be updated anyway?
Quoted 6 lines
builtin/mv.c +1
-0+ }else if (!strncmp (src, dst, length) && (dst[length] ==0 || dst[length] =='/' )) { bad =_ ("can not move directory into itself" ); } else if ((src_is_dir = S_ISDIR(st.st_mode))@@ -225,11 +228,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix) dst_len =strlen (dst);
builtin/mv.c int cmd_mv(int argc, const char **argv, const char *prefix) · line 236
Quoted 10 lines
builtin/mv.c +3
-2for (j =0 ; j < last - first; j++) { -const char *path = active_cache[first + j]->name; +const struct cache_entry *ce = active_cache[first + j]; +const char *path = ce->name; source[argc + j] = path; destination[argc + j] =prefix_path (dst, dst_len, path + length +1 ); - modes[argc + j] =INDEX ; + modes[argc + j] =ce_skip_worktree (ce) ?SPARSE :INDEX ;
OK, here we are assigning the SPARSE mode to sparse index entries that are inside a directory we want to move. Later iterations of the loop will then process these entries, see this mode, and not error out if the files are missing.
builtin/mv.c int cmd_mv(int argc, const char **argv, const char *prefix) · line 300
Quoted 9 lines
builtin/mv.c +1
-1submodule_gitfile[argc + j] =NULL ; } argc += last - first;@@ -293,7 +297,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)printf (_ ("Renaming %s to %s\n" ), src, dst);if (show_only)continue ; -if (mode !=INDEX &&rename (src, dst) <0 ) { +if (mode !=INDEX && mode !=SPARSE &&rename (src, dst) <0 ) {
And finally, we don't want to rename the SPARSE working tree file (if it exists) because its parent directory was already moved.
Quoted 19 lines
builtin/mv.c +0
-0if (ignore_errors)continue ;die_errno (_ ("renaming '%s' failed" ), src);t/t7002-mv-sparse-checkout.sh +3
-1@@ -31,7 +31,9 @@ test_expect_success 'mv refuses to move sparse-to-sparse' ' echo b >>expect && echo e >>expect && cat sparse_hint >>expect && - test_cmp expect stderr + test_cmp expect stderr && + git mv --sparse b e 2>stderr && + test_must_be_empty stderr ' test_expect_success 'mv refuses to move sparse-to-sparse, ignores failure' '
t/t7002-mv-sparse-checkout.sh test_expect_success 'mv refuses to move sparse-to-sparse, ignores failure' ' · line 51
Quoted 7 lines
t/t7002-mv-sparse-checkout.sh +3
-1@@ -44,7 +46,9 @@ test_expect_success 'mv refuses to move sparse-to-sparse, ignores failure' ' echo b >>expect && echo e >>expect && cat sparse_hint >>expect && - test_cmp expect stderr + test_cmp expect stderr && + git mv --sparse -k b e 2>stderr && + test_must_be_empty stderr
Nit: Isn't this case a bit redundant considering the test before this one? That is, with --sparse there should be no error for -k to ignore, and in the test above it we already checked that this command indeed succeeds with --sparse.
Quoted 5 lines
t/t7002-mv-sparse-checkout.sh +0
-0test_expect_success 'recursive mv refuses to move (possible) sparse' '@@ -80,7 +88,14 @@ test_expect_success 'recursive mv refuses to move (possible) sparse' ' echo sub >>expect && echo sub2 >>expect &&
t/t7002-mv-sparse-checkout.sh test_expect_success 'recursive mv refuses to move (possible) sparse' ' · line 98
Quoted 10 lines
t/t7002-mv-sparse-checkout.sh +8
-1cat sparse_hint >>expect && - test_cmp expect stderr + test_cmp expect stderr && + git mv --sparse sub sub2 2>stderr && + test_must_be_empty stderr && + git commit -m "moved sub to sub2" && + git rev-parse HEAD~1:sub >expect && + git rev-parse HEAD:sub2 >actual && + test_cmp expect actual && + git reset --hard HEAD~1
Perhaps this could be a test_when_finished (maybe right after the git commit invocation), so that we can restore the original state for the next tests even if this one fails?
Re: [PATCH 05/13] add: fail when adding an untracked sparse file
In reply to Matheus Tavares BernardinoQuoted 2 lines · Matheus Tavares Bernardino
On Tue, Aug 24, 2021 at 6:54 PM Derrick Stolee via GitGitGadget <[email protected]> wrote:
Thanks for adding your review. I'm sorry I'm so late getting back to it.
Quoted 9 lines
Quoted 9 lines
From: Derrick Stolee <[email protected]>
The
add_files()method inbuiltin/add.ctakes a set of untracked files that are being added by the input pathspec and inserts them into the index. If these files are outside of the sparse-checkout cone, then they gain theSKIP_WORKTREEbit at some point. However, this was not checked before inserting into the index, so these files are added even though we want to avoid modifying the index outside of the sparse-checkout cone.
Quoted 4 lines
Quoted 2 lines
Add a check within
add_files()for these files and write the advice about files outside of the sprase-checkout cone.s/sprase/sparse/
Thanks.
Quoted 5 lines
Quoted 5 lines
builtin/add.c +0
-0@@ -443,6 +443,7 @@ static void check_embedded_repo(const char *path)
Quoted 8 lines
builtin/add.c static void check_embedded_repo(const char *path) · line 446
Quoted 4 lines
builtin/add.c +1
-0static int add_files (struct dir_struct *dir,int flags) {int i, exit_status =0 ; +struct string_list only_match_skip_worktree =STRING_LIST_INIT_NODUP ;I see this reuses the logic from
cmd_add()andrefresh(). But since we are operating on untracked files here, perhaps we could replace "skip_worktree" by "sparse_paths" or something similar?
How about "matched_sparse_paths" as a whole name swap? The earlier uses cared only if every match was sparse, but here we are actually looking at cases that are untracked, and the pathspec could also match other non-sparse cases.
Quoted 13 lines
builtin/add.c static int add_files(struct dir_struct *dir, int flags) · line 475
Quoted 3 lines
builtin/add.c +3
-0+ +if (only_match_skip_worktree.nr) { +advise_on_updating_sparse_paths (&only_match_skip_worktree);Hmm,
advise_on_updating_sparse_paths()takes a list of pathspecs that only matched sparse paths, but here we are passing a list of actual pathnames... Well, these are technically pathspecs too, but the advice message may be confusing.For example, if we ran
git add *.con a repo with the untracked filesd1/file.candd2/file.c, we will get:
Quoted 8 lines
The following pathspecs didn't match any eligible path, but they do match index entries outside the current sparse checkout:
d1/file.cd2/file.cHowever,
d1/file.candd2/file.care neither index entries nor the pathspecs that the user has given togit add. So perhaps we need to change the error/advice message?
I think the advice should be modified to refer to paths and/or pathspecs, and also it is not completely correct anymore.
Instead of
The following pathspecs didn't match any eligible path, but they do match index entries outside the current sparse checkout:
perhaps
The following paths and/or pathspecs matched paths that exist outside of your sparse-checkout definition, so will not be updated in the index:
I'm going to save this one for a new patch at the end to make sure it handles all of the cases involved in this series.
Thanks,
-Stolee
Re: [PATCH 09/13] rm: add --sparse option
In reply to Matheus Tavares BernardinoQuoted 8 lines · Matheus Tavares Bernardino
Quoted 2 lines · Derrick Stolee via GitGitGadget
Subject: [PATCH 09/12] rm: add --sparse option
Maybe mention in the commit message that, for now, rm's --sparse only affects entries with the
skip_worktreebit set? (Which will be changed in the following patch.)
I will expand with these details.
Quoted 6 lines
t/t3602-rm-sparse-checkout.sh done · line 39
Quoted 4 lines
test_expect_success'recursive rm does not remove sparse entries' 'git reset --hard &&t/t3602-rm-sparse-checkout.sh +1
-1- git sparse-checkout set sub/dir && + git sparse-checkout set sub/dir/ &&Is this change necessary?
No, it is not. Thanks.
-Stolee
Re: [PATCH 11/13] mv: refuse to move sparse paths
In reply to Matheus Tavares BernardinoQuoted 16 lines · Matheus Tavares Bernardino
Quoted 14 lines · Derrick Stolee via GitGitGadget
builtin/mv.c +1
-0@@ -176,10 +177,22 @@ int cmd_mv(int argc, const char **argv, const char *prefix)const char *src = source[i], *dst = destination[i];int length, src_is_dir;const char *bad =NULL ; +int skip_sparse =0 ;if (show_only)printf (_ ("Checking rename of '%s' to '%s'\n" ), src, dst);
Quoted 10 lines
builtin/mv.c int cmd_mv(int argc, const char **argv, const char *prefix) · line 185
Quoted 1 line
builtin/mv.c +1
-0+if (!path_in_sparse_checkout (src, &the_index)) {
git mvcan only move/rename tracked paths, but since we check whethersrcis sparse before checking if it is in the index, the user will get the sparse error message instead. This is OK, but the advice might be misleading, as it says they can use--sparseif they really want to move the file, but repeating the command with--sparsewill now fail for another reason. I wonder if we should check whethersrcis tracked before checking if it is sparse, or if that is not really an issue we should bother with.
I will move the logic to the last possible place before "accepting" the move, then add a comment detailing why it should be there.
Quoted 10 lines
Quoted 10 lines
builtin/mv.c +10
-0+string_list_append (&only_match_skip_worktree, src); + skip_sparse =1 ; + } +if (!path_in_sparse_checkout (dst, &the_index)) { +string_list_append (&only_match_skip_worktree, dst); + skip_sparse =1 ; + } +if (skip_sparse) +continue ; +
...
Quoted 8 lines
builtin/mv.c
Quoted 2 lines
builtin/mv.c +2
-0+test_expect_success 'mv refuses to move sparse-to-sparse' ' + rm -f e &&At first glance, it confused me a bit that we are removing
ewhen the setup didn't create it. But then I realized the test itself might createeifgit mvsucceeds in moving thebfile. Could perhaps this and the othersrm -f ebe atest_when_finished, to make it clearer that it is a cleanup?
test_when_finished is cleaner.
Quoted 9 lines
builtin/mv.c
Quoted 5 lines
builtin/mv.c +4
-0+ git reset --hard && + git sparse-checkout set a && + touch b && + test_must_fail git mv b e 2>stderr &&Here we try to move a "tracked sparse path" to an "untracked sparse path". Do we also want to test with a tracked to tracked operation? (Although the code path will be the same, of course.)
I can expand these tests to include tracked and untracked targets.
Quoted 7 lines
Quoted 7 lines
builtin/mv.c +7
-0+ cat sparse_error_header >expect && + echo b >>expect && + echo e >>expect && + cat sparse_hint >>expect && + test_cmp expect stderr +' +
Quoted 8 lines
builtin/mv.c
Quoted 6 lines
builtin/mv.c +6
-0+test_expect_success 'mv refuses to move sparse-to-sparse, ignores failure' ' + rm -f e && + git reset --hard && + git sparse-checkout set a && + touch b && + git mv -k b e 2>stderr &&Maybe also check that
bis still there, andeis missing?
Good idea.
In fact, there is a problem that the '-k' gets around the protections because it doesn't return 1 early. I'll fix this by jumping to the end of the loop which removes the entries from the arrays.
Thanks,
-Stolee
Re: [PATCH 06/13] add: skip paths that are outside sparse-checkout cone
In reply to Matheus TavaresQuoted 17 lines · Matheus Tavares
Quoted 2 lines · Derrick Stolee via GitGitGadget
Subject: [PATCH 06/13] add: skip paths that are outside sparse-checkout cone
Perhaps this could be "skip _tracked_ paths that ..." (to help differentiate the end goal of this patch from the previous one).
Quoted 10 lines
pathspec.c +1
-1@@ -39,7 +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)) + if (sw_action == PS_IGNORE_SKIP_WORKTREE &&
Quoted 8 lines
pathspec.c void add_pathspec_matches_against_index(const struct pathspec *pathspec, · line 43
Quoted 1 line
pathspec.c +1
-0+ (ce_skip_worktree(ce) || !path_in_sparse_checkout(ce->name, istate)))Hmm, even though we skip the sparse paths here,
cmd_add()will calladd_files_to_cache()at the end and still update these paths in the index. I think there are two ways to fix this. We could either changerun_diff_files()to skip these paths (but I don't know how other callers of this functions want to handle this, so maybe this needs to hide behind an option flag):
You are absolutely right to point this out. I had missed this interaction. But, this is also already broken. The patch below adds a check to show that 'git add' does not add the sparse_entry, but it does (even when applied before any patch in this series). That is: all the modified tests fail after this change. I'll work to fix this issue before the next version of this series.
Thanks,
-Stolee
--- >8 ---
From 21dab466d221e8632d98553f5f1fa900a2d47c7f Mon Sep 17 00:00:00 2001
From: Derrick Stolee <[email protected]>
Date: Wed, 8 Sep 2021 15:40:32 -0400
Subject: [PATCH] t3705: test that 'sparse_entry' is unstaged
The tests in t3705-add-sparse-checkout.sh check to see how 'git add' behaves with paths outside the sparse-checkout definition. These currently check to see if a given warning is present but not that the index is not updated with the sparse entries. Add a new 'test_sparse_entry_unstaged' helper to be sure 'git add' is behaving correctly.
--- t/t3705-add-sparse-checkout.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+)
t/t3705-add-sparse-checkout.sh +13 -0
@@ -36,6 +36,11 @@ setup_gitignore () { EOF } +test_sparse_entry_unstaged () { + git status --porcelain >actual && + ! grep "A sparse_entry" actual +} + test_expect_success 'setup' " cat >sparse_error_header <<-EOF && The following pathspecs didn't match any eligible path, but they do match index
@@ -55,6 +60,7 @@ test_expect_success 'git add does not remove sparse entries' ' setup_sparse_entry && rm sparse_entry && test_must_fail git add sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -73,6 +79,7 @@ test_expect_success 'git add . does not remove sparse entries' ' rm sparse_entry && setup_gitignore && test_must_fail git add . 2>stderr && + test_sparse_entry_unstaged && cat sparse_error_header >expect && echo . >>expect &&
@@ -88,6 +95,7 @@ do setup_sparse_entry && echo modified >sparse_entry && test_must_fail git add $opt sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -98,6 +106,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' git ls-files --debug sparse_entry | grep mtime >before && test-tool chmtime -60 sparse_entry && test_must_fail git add --refresh sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && git ls-files --debug sparse_entry | grep mtime >after && test_cmp before after
@@ -106,6 +115,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' test_expect_success 'git add --chmod does not update sparse entries' ' setup_sparse_entry && test_must_fail git add --chmod=+x sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged && ! test -x sparse_entry
@@ -116,6 +126,7 @@ test_expect_success 'git add --renormalize does not update sparse entries' ' setup_sparse_entry "LINEONE\r\nLINETWO\r\n" && echo "sparse_entry text=auto" >.gitattributes && test_must_fail git add --renormalize sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -124,6 +135,7 @@ 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_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -148,6 +160,7 @@ test_expect_success 'do not warn when pathspec matches dense entries' ' test_expect_success 'add obeys advice.updateSparsePath' ' setup_sparse_entry && test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp sparse_entry_error stderr '
Signature · 1 line
2.33.0.vfs.signtest.55.g904c8365e91
Re: [PATCH 06/13] add: skip paths that are outside sparse-checkout cone
Quoted 27 lines · Derrick Stolee
Quoted 17 lines · Matheus Tavares
Quoted 2 lines · Derrick Stolee via GitGitGadget
Subject: [PATCH 06/13] add: skip paths that are outside sparse-checkout cone
Perhaps this could be "skip _tracked_ paths that ..." (to help differentiate the end goal of this patch from the previous one).
Quoted 10 lines
pathspec.c +1
-1@@ -39,7 +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)) + if (sw_action == PS_IGNORE_SKIP_WORKTREE &&Quoted 8 lines
pathspec.c void add_pathspec_matches_against_index(const struct pathspec *pathspec, · line 43
Quoted 1 line
pathspec.c +1
-0+ (ce_skip_worktree(ce) || !path_in_sparse_checkout(ce->name, istate)))Hmm, even though we skip the sparse paths here,
cmd_add()will calladd_files_to_cache()at the end and still update these paths in the index. I think there are two ways to fix this. We could either changerun_diff_files()to skip these paths (but I don't know how other callers of this functions want to handle this, so maybe this needs to hide behind an option flag):
Quoted 6 lines
You are absolutely right to point this out. I had missed this interaction. But, this is also already broken. The patch below adds a check to show that 'git add' does not add the
sparse_entry, but it does (even when applied before any patch in this series). That is: all the modified tests fail after this change. I'll work to fix this issue before the next version of this series.
Of course, the reason for the failures is because the 'sparse_entry' is staged as part of setup_sparse_entry. Not a bug.
This makes things more difficult to test, so I'll look around for an alternative way to test that 'git add' is behaving correctly.
Thanks,
-Stolee
Re: [PATCH 06/13] add: skip paths that are outside sparse-checkout cone
In reply to Matheus TavaresQuoted 17 lines · Matheus Tavares
Quoted 2 lines · Derrick Stolee via GitGitGadget
Subject: [PATCH 06/13] add: skip paths that are outside sparse-checkout cone
Perhaps this could be "skip _tracked_ paths that ..." (to help differentiate the end goal of this patch from the previous one).
Quoted 10 lines
pathspec.c +1
-1@@ -39,7 +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)) + if (sw_action == PS_IGNORE_SKIP_WORKTREE &&
Quoted 8 lines
pathspec.c void add_pathspec_matches_against_index(const struct pathspec *pathspec, · line 43
Quoted 1 line
pathspec.c +1
-0+ (ce_skip_worktree(ce) || !path_in_sparse_checkout(ce->name, istate)))Hmm, even though we skip the sparse paths here,
cmd_add()will calladd_files_to_cache()at the end and still update these paths in the index. I think there are two ways to fix this. We could either changerun_diff_files()to skip these paths (but I don't know how other callers of this functions want to handle this, so maybe this needs to hide behind an option flag):
Ok, this sent me off on a tangent (see other replies) trying to show that 'git add <sparse-path>' is still modifying index entries. I finally added enough checks that this fails in my merge/cherry-pick/rebase tests for conflicts outside of the sparse-checkout cone. Here is a test that I can add to t3705 to get a failure:
test_expect_success 'git add fails outside of sparse-checkout definition' '
test_when_finished git sparse-checkout disable && test_commit a && git sparse-checkout init && git sparse-checkout set a && git update-index --no-skip-worktree sparse_entry && test_must_fail git add sparse_entry && test_sparse_entry_unstaged
'
Quoted 22 lines
diff-lib.c +2
-1@@ -198,7 +198,8 @@ int run_diff_files(struct rev_info *revs, unsigned int option)continue ; } -if (ce_uptodate (ce) ||ce_skip_worktree (ce)) +if (ce_uptodate (ce) ||ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, istate))continue ;/* Or we could change the callback in add itself:
builtin/add.c +0
-0
builtin/add.c static void update_callback(struct diff_queue_struct *q, · line 103
Quoted 10 lines
builtin/add.c +4
-0@@ -94,6 +94,10 @@ static void update_callback(struct diff_queue_struct *q,for (i =0 ; i < q->nr; i++) {struct diff_filepair *p = q->queue[i];const char *path = p->one->path; + +if (!path_in_sparse_checkout (path, &the_index)) +continue ; +switch (fix_unmerged_status (p, data)) {default :die (_ ("unexpected diff status %c" ), p->status);
If I use this second callback, then I have access to 'include_sparse' in the later change that adds the --sparse option.
Quoted 5 lines
I believe we also need to update a few other places to use the
(ce_skip_worktree(ce) || !path_in_sparse_checkout())logic in order to avoid updating tracked sparse paths:chmod_pathspec()for add's --chmod option,renormalize_tracked_files()for --renormalize, and read-cache.c:refresh_index() for --refresh.
OK, I'll make sure to include and test these cases.
Quoted 10 lines
pathspec.c void add_pathspec_matches_against_index(const struct pathspec *pathspec, · line 46
Quoted 3 lines
pathspec.c +0
-0continue ;ce_path_match (istate, ce, pathspec, seen); }Hmm, don't we also want to update
find_pathspecs_matching_skip_worktree()in this patch to usepath_in_sparse_checkout()? I see you did that in patch 8, but I think this should be together with this current patch as, without it, we stop adding tracked sparse paths but we print no error/advice message about it.
I'll merge the patches to avoid confusion.
Thanks,
-Stolee
v2 cover Sparse-checkout: modify 'git add', 'git rm', and 'git add' behavior
In reply to Derrick Stolee via GitGitGadgetThis series is based on ds/mergies-with-sparse-index.
As requested, this series looks to update the behavior of git add, git rm, and git mv when they attempt to modify paths outside of the sparse-checkout cone. In particular, this care is expanded to not just cache entries with the SKIP_WORKTREE bit, but also paths that do not match the sparse-checkout definition.
This means that commands that worked before this series can now fail. In particular, if 'git merge' results in a conflict outside of the sparse-checkout cone, then 'git add ' will now fail.
In order to allow users to circumvent these protections, a new '--sparse' option is added that ignores the sparse-checkout patterns and the SKIP_WORKTREE bit. The message for advice.updateSparsePath is adjusted to assist with discovery of this option.
There is a subtle issue with git mv in that it does not check the index until it discovers a directory and then uses the index to find the contained entries. This means that in non-cone-mode patterns, a pattern such as "sub/dir" will not match the path "sub" and this can cause an issue.
In order to allow for checking arbitrary paths against the sparse-checkout patterns, some changes to the underlying pattern matching code is required. It turns out that there are some bugs in the methods as advertised, but these bugs were never discovered because of the way methods like unpack_trees() will check a directory for a pattern match before checking its contained paths. Our new "check patterns on-demand" approach pokes holes in that approach, specifically with patterns that match entire directories.
Updates in v2
=============
* I got no complaints about these restrictions, so this is now a full series, not RFC.
* Thanks to Matheus, several holes are filled with extra testing and bugfixes.
* New patches add --chmod and --renormalize improvements. These are added after the --sparse option to make them be one change each.
Thanks, -Stolee
Derrick Stolee (14):
t3705: test that 'sparse_entry' is unstaged
t1092: behavior for adding sparse files
dir: extract directory-matching logic
dir: select directories correctly
dir: fix pattern matching on dirs
add: fail when adding an untracked sparse file
add: skip tracked paths outside sparse-checkout cone
add: implement the --sparse option
add: update --chmod to skip sparse paths
add: update --renormalize to skip sparse paths
rm: add --sparse option
rm: skip sparse paths with missing SKIP_WORKTREE
mv: refuse to move sparse paths
advice: update message to suggest '--sparse'
Documentation/git-add.txt | 9 +- Documentation/git-rm.txt | 6 + advice.c | 10 +- builtin/add.c | 32 +++- builtin/mv.c | 52 +++++-- builtin/rm.c | 10 +- dir.c | 54 ++++++- pathspec.c | 5 +- t/t1091-sparse-checkout-builtin.sh | 4 +- t/t1092-sparse-checkout-compatibility.sh | 75 +++++++-- t/t3602-rm-sparse-checkout.sh | 37 ++++- t/t3705-add-sparse-checkout.sh | 65 +++++++- t/t7002-mv-sparse-checkout.sh | 186 +++++++++++++++++++++++ 13 files changed, 494 insertions(+), 51 deletions(-) create mode 100755 t/t7002-mv-sparse-checkout.sh
base-commit: 516680ba7704c473bb21628aa19cabbd787df4db
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1018%2Fderrickstolee%2Fsparse-index%2Fadd-rm-mv-behavior-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1018/derrickstolee/sparse-index/add-rm-mv-behavior-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/1018
Range-diff vs v1:
-: ----------- > 1: 8aefce6254c t3705: test that 'sparse_entry' is unstaged
1: 69ca54877b4 ! 2: 61c23dc59a6 t1092: behavior for adding sparse files
@@ Commit message
Add some tests to demonstrate the current behavior around adding files
outside of the sparse-checkout cone. Currently, untracked files are
- handled differently from tracked files. A futuer change will make these
+ handled differently from tracked files. A future change will make these
cases be handled the same way.
+ Further expand checking that a failed 'git add' does not stage changes
+ to the index.
+
Signed-off-by: Derrick Stolee <[email protected]>
## t/t1092-sparse-checkout-compatibility.sh ##
+@@ t/t1092-sparse-checkout-compatibility.sh: test_sparse_match () {
+ test_cmp sparse-checkout-err sparse-index-err
+ }
+
++test_sparse_unstaged () {
++ file=$1 &&
++ for repo in sparse-checkout sparse-index
++ do
++ git -C $repo status --porcelain >$repo-out &&
++ ! grep "^A $file\$" $repo-out &&
++ ! grep "^M $file\$" $repo-out || return 1
++ done
++}
++
+ test_expect_success 'sparse-index contents' '
+ init_repos &&
+
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'add, commit, checkout' '
test_all_match git checkout -
'
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'add, commit, chec
+ run_on_sparse ../edit-contents folder1/a &&
+ run_on_sparse ../edit-contents folder1/newfile &&
+ test_sparse_match test_must_fail git add folder1/a &&
++ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ test_sparse_unstaged folder1/a &&
+ test_sparse_match git add folder1/newfile
+'
+
test_expect_success 'commit including unstaged changes' '
init_repos &&
+@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'status/add: outside sparse cone' '
+
+ # Adding the path outside of the sparse-checkout cone should fail.
+ test_sparse_match test_must_fail git add folder1/a &&
++ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ test_sparse_unstaged folder1/a &&
+ test_sparse_match test_must_fail git add --refresh folder1/a &&
++ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ test_sparse_unstaged folder1/a &&
+
+ # NEEDSWORK: Adding a newly-tracked file outside the cone succeeds
+ test_sparse_match git add folder1/new &&
2: 8cca5bcf405 = 3: 747b52e7bde dir: extract directory-matching logic
3: d47c7a1cf2a = 4: 723de4e4258 dir: select directories correctly
4: 58e7df44982 = 5: ed2472d33f1 dir: fix pattern matching on dirs
5: e80fcfa932c ! 6: bac6cedfc8d add: fail when adding an untracked sparse file
@@ Commit message
want to avoid modifying the index outside of the sparse-checkout cone.
Add a check within add_files() for these files and write the advice
- about files outside of the sprase-checkout cone.
+ about files outside of the sparse-checkout cone.
This behavior change modifies some existing tests within t1092. These
tests intended to document how a user could interact with the existing
@@ builtin/add.c: static void check_embedded_repo(const char *path)
static int add_files(struct dir_struct *dir, int flags)
{
int i, exit_status = 0;
-+ struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
++ struct string_list matched_sparse_paths = STRING_LIST_INIT_NODUP;
if (dir->ignored_nr) {
fprintf(stderr, _(ignore_error));
@@ builtin/add.c: static int add_files(struct dir_struct *dir, int flags)
for (i = 0; i < dir->nr; i++) {
+ if (!path_in_sparse_checkout(dir->entries[i]->name, &the_index)) {
-+ string_list_append(&only_match_skip_worktree,
++ string_list_append(&matched_sparse_paths,
+ dir->entries[i]->name);
+ continue;
+ }
@@ builtin/add.c: static int add_files(struct dir_struct *dir, int flags)
}
}
+
-+ if (only_match_skip_worktree.nr) {
-+ advise_on_updating_sparse_paths(&only_match_skip_worktree);
++ if (matched_sparse_paths.nr) {
++ advise_on_updating_sparse_paths(&matched_sparse_paths);
+ exit_status = 1;
+ }
+
-+ string_list_clear(&only_match_skip_worktree, 0);
++ string_list_clear(&matched_sparse_paths, 0);
+
return exit_status;
}
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'add, commit, chec
init_repos &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'add outside sparse cone' '
- run_on_sparse ../edit-contents folder1/a &&
- run_on_sparse ../edit-contents folder1/newfile &&
test_sparse_match test_must_fail git add folder1/a &&
+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/a &&
- test_sparse_match git add folder1/newfile
-+ test_sparse_match test_must_fail git add folder1/newfile
++ test_sparse_match test_must_fail git add folder1/newfile &&
++ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ test_sparse_unstaged folder1/newfile
'
test_expect_success 'commit including unstaged changes' '
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'commit including
# folder1 is at HEAD, but outside the sparse cone
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'status/add: outside sparse cone' '
- # Adding the path outside of the sparse-checkout cone should fail.
- test_sparse_match test_must_fail git add folder1/a &&
test_sparse_match test_must_fail git add --refresh folder1/a &&
+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/a &&
+ test_sparse_match test_must_fail git add folder1/new &&
++ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ test_sparse_unstaged folder1/new &&
- # NEEDSWORK: Adding a newly-tracked file outside the cone succeeds
- test_sparse_match git add folder1/new &&
6: eeba97ad492 < -: ----------- add: skip paths that are outside sparse-checkout cone
8: f1764f9ed18 ! 7: d4706e17cae add: prevent adding sparse conflict files @@ Metadata
Author: Derrick Stolee <[email protected]>
## Commit message ##
- add: prevent adding sparse conflict files
+ add: skip tracked paths outside sparse-checkout cone
- When a merge results in a conflict outside of the sparse-checkout cone,
- the conflicted file is written to the working tree and the index entry
- loses the SKIP_WORKTREE bit. This allows users to add the file to the
- index without realizing that the file might leave the working tree in a
- later Git command.
+ When 'git add' adds a tracked file that is outside of the
+ sparse-checkout cone, it checks the SKIP_WORKTREE bit to see if the file
+ exists outside of the sparse-checkout cone. This is usually correct,
+ except in the case of a merge conflict outside of the cone.
- Block this behavior, but keep in mind that the user can override the
- failure using the '--sparse' option.
+ Modify add_pathspec_matched_against_index() to be more careful about
+ paths by checking the sparse-checkout patterns in addition to the
+ SKIP_WORKTREE bit. This causes 'git add' to no longer allow files
+ outside of the cone that removed the SKIP_WORKTREE bit due to a merge
+ conflict.
+
+ With only this change, users will only be able to add the file after
+ adding the file to the sparse-checkout cone. A later change will allow
+ users to force adding even though the file is outside of the
+ sparse-checkout cone.
+ ## builtin/add.c ##
+@@ builtin/add.c: static void update_callback(struct diff_queue_struct *q,
+ for (i = 0; i < q->nr; i++) {
+ struct diff_filepair *p = q->queue[i];
+ const char *path = p->one->path;
++
++ if (!path_in_sparse_checkout(path, &the_index))
++ continue;
++
+ switch (fix_unmerged_status(p, data)) {
+ default:
+ die(_("unexpected diff status %c"), p->status);
+
## pathspec.c ##
+@@ pathspec.c: 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))
++ if (sw_action == PS_IGNORE_SKIP_WORKTREE &&
++ (ce_skip_worktree(ce) || !path_in_sparse_checkout(ce->name, istate)))
+ continue;
+ ce_path_match(istate, ce, pathspec, seen);
+ }
@@ pathspec.c: char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec)
for (i = 0; i < istate->cache_nr; i++) {
@@ t/t1091-sparse-checkout-builtin.sh: test_expect_success 'sparse-checkout reapply
test_must_be_empty err &&
## t/t1092-sparse-checkout-compatibility.sh ##
-@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'merge with conflict outside cone' ' +@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with conflict outside cone' '
test_all_match git status --porcelain=v2 &&
# 2. Add the file with conflict markers
- # NEEDSWORK: Even though the merge conflict removed the
- # SKIP_WORKTREE bit from the index entry for folder1/a, we should
-- # warn that this is a problematic add when --sparse is not set.
+- # warn that this is a problematic add.
+- test_all_match git add folder1/a &&
+ test_sparse_match test_must_fail git add folder1/a &&
+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
- test_all_match git add --sparse folder1/a &&
++ test_sparse_unstaged folder1/a &&
++ test_all_match git add --sparse folder1/a &&
test_all_match git status --porcelain=v2 &&
-@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'merge with conflict outside cone' '
- # accept conflict markers as resolved content.
- run_on_all mv folder2/a folder2/z &&
- test_sparse_match test_must_fail git add folder2 &&
+ # 3. Rename the file to another sparse filename and
+@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with conflict outside cone' '
+ # NEEDSWORK: This mode now fails, because folder2/z is
+ # outside of the sparse-checkout cone and does not match an
+ # existing index entry with the SKIP_WORKTREE bit cleared.
+- test_all_match git add folder2 &&
++ test_sparse_match test_must_fail git add folder2 &&
+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
- test_all_match git add --sparse folder2 &&
++ test_sparse_unstaged folder2/z &&
++ test_all_match git add --sparse folder2 &&
test_all_match git status --porcelain=v2 &&
+ test_all_match git merge --continue &&
+@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'cherry-pick/rebase with conflict outside cone' '
+ # NEEDSWORK: Even though the merge conflict removed the
+ # SKIP_WORKTREE bit from the index entry for folder1/a, we should
+ # warn that this is a problematic add.
+- test_all_match git add folder1/a &&
++ test_sparse_match test_must_fail git add folder1/a &&
++ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ test_sparse_unstaged folder1/a &&
+ test_all_match git status --porcelain=v2 &&
+
+ # 3. Rename the file to another sparse filename and
+@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'cherry-pick/rebase with conflict outside cone' '
+ # outside of the sparse-checkout cone and does not match an
+ # existing index entry with the SKIP_WORKTREE bit cleared.
+ run_on_all mv folder2/a folder2/z &&
+- test_all_match git add folder2 &&
++ test_sparse_match test_must_fail git add folder2 &&
++ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ test_sparse_unstaged folder2/z &&
+ test_all_match git status --porcelain=v2 &&
+
+ test_all_match git $OPERATION --continue &&
+
+ ## t/t3705-add-sparse-checkout.sh ##
+@@ t/t3705-add-sparse-checkout.sh: test_expect_success 'do not warn when pathspec matches dense entries' '
+ git ls-files --error-unmatch dense_entry
+ '
+
++test_expect_success 'git add fails outside of sparse-checkout definition' '
++ test_when_finished git sparse-checkout disable &&
++ test_commit a &&
++ git sparse-checkout init &&
++ git sparse-checkout set a &&
++ echo >>sparse_entry &&
++
++ git update-index --no-skip-worktree sparse_entry &&
++ test_must_fail git add sparse_entry &&
++ test_sparse_entry_unstaged
++'
++
+ test_expect_success 'add obeys advice.updateSparsePath' '
+ setup_sparse_entry &&
+ test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr &&
7: 6edadbc2000 ! 8: 944ae2cffa8 add: implement the --sparse option
@@ builtin/add.c: static int chmod_pathspec(struct pathspec *pathspec, char flip, i
continue;
if (pathspec && !ce_path_match(&the_index, ce, pathspec, NULL))
+@@ builtin/add.c: static void update_callback(struct diff_queue_struct *q,
+ struct diff_filepair *p = q->queue[i];
+ const char *path = p->one->path;
+
+- if (!path_in_sparse_checkout(path, &the_index))
++ if (!include_sparse && !path_in_sparse_checkout(path, &the_index))
+ continue;
+
+ switch (fix_unmerged_status(p, data)) {
@@ builtin/add.c: static struct option builtin_add_options[] = {
OPT_BOOL( 0 , "refresh", &refresh_only, N_("don't add, only refresh the index")),
OPT_BOOL( 0 , "ignore-errors", &ignore_add_errors, N_("just skip files which cannot be added because of errors")),
@@ builtin/add.c: static int add_files(struct dir_struct *dir, int flags)
- if (!path_in_sparse_checkout(dir->entries[i]->name, &the_index)) {
+ if (!include_sparse &&
+ !path_in_sparse_checkout(dir->entries[i]->name, &the_index)) {
- string_list_append(&only_match_skip_worktree,
+ string_list_append(&matched_sparse_paths,
dir->entries[i]->name);
continue;
@@ builtin/add.c: int cmd_add(int argc, const char **argv, const char *prefix)
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'commit including
# folder1 is at HEAD, but outside the sparse cone
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'status/add: outside sparse cone' '
- test_sparse_match test_must_fail git add folder1/a &&
- test_sparse_match test_must_fail git add --refresh folder1/a &&
test_sparse_match test_must_fail git add folder1/new &&
+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/new &&
+ test_sparse_match git add --sparse folder1/a &&
+ test_sparse_match git add --sparse folder1/new &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'merge, cherry-pic
test_all_match git checkout -b merge-tip merge-left &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with conflict outside cone' '
- # 2. Add the file with conflict markers
- # NEEDSWORK: Even though the merge conflict removed the
- # SKIP_WORKTREE bit from the index entry for folder1/a, we should
-- # warn that this is a problematic add.
-- test_sparse_match test_must_fail git add folder1/a &&
-+ # warn that this is a problematic add when --sparse is not set.
-+ test_all_match git add --sparse folder1/a &&
- test_all_match git status --porcelain=v2 &&
-
# 3. Rename the file to another sparse filename and
# accept conflict markers as resolved content.
run_on_all mv folder2/a folder2/z &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with confli
- # outside of the sparse-checkout cone and does not match an
- # existing index entry with the SKIP_WORKTREE bit cleared.
test_sparse_match test_must_fail git add folder2 &&
-+ test_all_match git add --sparse folder2 &&
- test_all_match git status --porcelain=v2 &&
+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder2/z &&
+@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with conflict outside cone' '
+ test_all_match git rev-parse HEAD^{tree}
+ '
+
+-test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' ++test_expect_success 'cherry-pick/rebase with conflict outside cone' '
+ init_repos &&
- test_all_match git merge --continue &&
+ for OPERATION in cherry-pick rebase
+@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'cherry-pick/rebase with conflict outside cone' '
+ test_sparse_match test_must_fail git add folder1/a &&
+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/a &&
++ test_all_match git add --sparse folder1/a &&
+ test_all_match git status --porcelain=v2 &&
+
+ # 3. Rename the file to another sparse filename and
+@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'cherry-pick/rebase with conflict outside cone' '
+ test_sparse_match test_must_fail git add folder2 &&
+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder2/z &&
++ test_all_match git add --sparse folder2 &&
+ test_all_match git status --porcelain=v2 &&
+
+ test_all_match git $OPERATION --continue &&
## t/t3705-add-sparse-checkout.sh ##
+@@ t/t3705-add-sparse-checkout.sh: test_expect_success 'git add fails outside of sparse-checkout definition' ' +
+ git update-index --no-skip-worktree sparse_entry &&
+ test_must_fail git add sparse_entry &&
+- test_sparse_entry_unstaged
++ test_sparse_entry_unstaged &&
++
++ # Avoid munging CRLFs to avoid an error message
++ git -c core.autocrlf=input add --sparse sparse_entry 2>stderr &&
++ test_must_be_empty stderr &&
++ test-tool read-cache --table >actual &&
++ grep "^100644 blob.*sparse_entry\$" actual
+ '
+
+ test_expect_success 'add obeys advice.updateSparsePath' '
@@ t/t3705-add-sparse-checkout.sh: test_expect_success 'add obeys advice.updateSparsePath' '
'
@@ t/t3705-add-sparse-checkout.sh: test_expect_success 'add obeys advice.updateSpar
+ git sparse-checkout set a &&
+ echo modified >sparse_entry &&
+ test_must_fail git add sparse_entry &&
++ test_sparse_entry_unchanged &&
+ git add --sparse sparse_entry 2>stderr &&
+ test_must_be_empty stderr
+'
-: ----------- > 9: 7ace030c709 add: update --chmod to skip sparse paths -: ----------- > 10: f34be166097 add: update --renormalize to skip sparse paths 9: 6c9c986ff43 ! 11: fb3f86efa9b rm: add --sparse option
@@ Metadata
## Commit message ##
rm: add --sparse option
+ As we did previously in 'git add', add a '--sparse' option to 'git rm'
+ that allows modifying paths outside of the sparse-checkout definition.
+ The existing checks in 'git rm' are restricted to tracked files that
+ have the SKIP_WORKTREE bit in the current index. Future changes will
+ cause 'git rm' to reject removing paths outside of the sparse-checkout
+ definition, even if they are untracked or do not have the SKIP_WORKTREE
+ bit.
+
## Documentation/git-rm.txt ##
@@ builtin/rm.c: int cmd_rm(int argc, const char **argv, const char *prefix)
die(_("pathspec '%s' did not match any files"), original);
## t/t3602-rm-sparse-checkout.sh ##
-@@ t/t3602-rm-sparse-checkout.sh: done
-
- test_expect_success 'recursive rm does not remove sparse entries' '
- git reset --hard &&
-- git sparse-checkout set sub/dir &&
-+ git sparse-checkout set sub/dir/ &&
- git rm -r sub &&
- git status --porcelain -uno >actual &&
- echo "D sub/dir/e" >expected &&
+@@ t/t3602-rm-sparse-checkout.sh: test_expect_success 'recursive rm does not remove sparse entries' '
test_cmp expected actual
'
10: 5153accded4 ! 12: de0c9d09ef7 rm: skip sparse paths with missing SKIP_WORKTREE
@@ Commit message
if the '--sparse' option is specified, allowing users who accept the
risks to continue with the removal.
+ This also removes a confusing behavior where a user asks for a directory
+ to be removed, but only the entries that are within the sparse-checkout
+ definition are removed. Now, 'git rm <dir>' will fail without '--sparse'
+ and will succeed in removing all contained paths with '--sparse'.
+
Signed-off-by: Derrick Stolee <[email protected]>
## builtin/rm.c ##
@@ builtin/rm.c: int cmd_rm(int argc, const char **argv, const char *prefix)
continue;
## t/t3602-rm-sparse-checkout.sh ##
+@@ t/t3602-rm-sparse-checkout.sh: done
+ test_expect_success 'recursive rm does not remove sparse entries' '
+ git reset --hard &&
+ git sparse-checkout set sub/dir &&
+- git rm -r sub &&
++ test_must_fail git rm -r sub &&
++ git rm --sparse -r sub &&
+ git status --porcelain -uno >actual &&
+- echo "D sub/dir/e" >expected &&
++ cat >expected <<-\EOF &&
++ D sub/d
++ D sub/dir/e
++ EOF
+ test_cmp expected actual
+ '
+
@@ t/t3602-rm-sparse-checkout.sh: test_expect_success 'do not warn about sparse entries with --ignore-unmatch' '
git ls-files --error-unmatch b
'
11: d31c6411806 ! 13: e17a529332d mv: refuse to move sparse paths
@@ Commit message
mechanism for seeing if a path is sparse or not. Be sure to skip
returning a failure if '-k' is specified.
+ To ensure that the advice around sparse paths is the only reason a move
+ failed, be sure to check this as the very last thing before inserting
+ into the src_for_dst list.
+
+ The tests cover a variety of cases such as whether the target is tracked
+ or untracked, and whether the source or destination are in or outside of
+ the sparse-checkout definition.
+
+ Helped-by: Matheus Tavares Bernardino <[email protected]>
Signed-off-by: Derrick Stolee <[email protected]>
## builtin/mv.c ##
-@@ builtin/mv.c: int cmd_mv(int argc, const char **argv, const char *prefix)
+@@ builtin/mv.c: static int index_range_of_same_dir(const char *src, int length,
+ int cmd_mv(int argc, const char **argv, const char *prefix)
+ {
+ int i, flags, gitmodules_modified = 0;
+- int verbose = 0, show_only = 0, force = 0, ignore_errors = 0;
++ int verbose = 0, show_only = 0, force = 0, ignore_errors = 0, ignore_sparse = 0;
+ struct option builtin_mv_options[] = {
+ OPT__VERBOSE(&verbose, N_("be verbose")),
+ OPT__DRY_RUN(&show_only, N_("dry run")),
+ OPT__FORCE(&force, N_("force move/rename even if target exists"),
+ PARSE_OPT_NOCOMPLETE),
+ OPT_BOOL('k', NULL, &ignore_errors, N_("skip move/rename errors")),
++ OPT_BOOL(0, "sparse", &ignore_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
+ OPT_END(),
+ };
+ const char **source, **destination, **dest_path, **submodule_gitfile;
+- enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes;
++ enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX, SPARSE } *modes;
+ struct stat st;
struct string_list src_for_dst = STRING_LIST_INIT_NODUP;
struct lock_file lock_file = LOCK_INIT;
struct cache_entry *ce;
@@ builtin/mv.c: int cmd_mv(int argc, const char **argv, const char *prefix)
if (show_only)
printf(_("Checking rename of '%s' to '%s'\n"), src, dst);
-+ if (!path_in_sparse_checkout(src, &the_index)) {
-+ string_list_append(&only_match_skip_worktree, src);
-+ skip_sparse = 1;
-+ }
-+ if (!path_in_sparse_checkout(dst, &the_index)) {
-+ string_list_append(&only_match_skip_worktree, dst);
-+ skip_sparse = 1;
-+ }
-+ if (skip_sparse)
-+ continue;
-+
length = strlen(src);
- if (lstat(src, &st) < 0)
- bad = _("bad source");
+- if (lstat(src, &st) < 0)
+- bad = _("bad source");
+- else if (!strncmp(src, dst, length) &&
++ if (lstat(src, &st) < 0) {
++ /* only error if existence is expected. */
++ if (modes[i] != SPARSE)
++ bad = _("bad source");
++ } else if (!strncmp(src, dst, length) &&
+ (dst[length] == 0 || dst[length] == '/')) {
+ bad = _("can not move directory into itself");
+ } else if ((src_is_dir = S_ISDIR(st.st_mode))
+@@ builtin/mv.c: int cmd_mv(int argc, const char **argv, const char *prefix)
+ dst_len = strlen(dst);
+
+ for (j = 0; j < last - first; j++) {
+- const char *path = active_cache[first + j]->name;
++ const struct cache_entry *ce = active_cache[first + j];
++ const char *path = ce->name;
+ source[argc + j] = path;
+ destination[argc + j] =
+ prefix_path(dst, dst_len, path + length + 1);
+- modes[argc + j] = INDEX;
++ modes[argc + j] = ce_skip_worktree(ce) ? SPARSE : INDEX;
+ submodule_gitfile[argc + j] = NULL;
+ }
+ argc += last - first;
+@@ builtin/mv.c: int cmd_mv(int argc, const char **argv, const char *prefix)
+ bad = _("multiple sources for the same target");
+ else if (is_dir_sep(dst[strlen(dst) - 1]))
+ bad = _("destination directory does not exist");
+- else
++ else {
++ /*
++ * We check if the paths are in the sparse-checkout
++ * definition as a very final check, since that
++ * allows us to point the user to the --sparse
++ * option as a way to have a successful run.
++ */
++ if (!ignore_sparse &&
++ !path_in_sparse_checkout(src, &the_index)) {
++ string_list_append(&only_match_skip_worktree, src);
++ skip_sparse = 1;
++ }
++ if (!ignore_sparse &&
++ !path_in_sparse_checkout(dst, &the_index)) {
++ string_list_append(&only_match_skip_worktree, dst);
++ skip_sparse = 1;
++ }
++
++ if (skip_sparse)
++ goto remove_entry;
++
+ string_list_insert(&src_for_dst, dst);
++ }
+
+ if (!bad)
+ continue;
+ if (!ignore_errors)
+ die(_("%s, source=%s, destination=%s"),
+ bad, src, dst);
++remove_entry:
+ if (--argc > 0) {
+ int n = argc - i;
+ memmove(source + i, source + i + 1,
@@ builtin/mv.c: int cmd_mv(int argc, const char **argv, const char *prefix)
}
}
@@ builtin/mv.c: int cmd_mv(int argc, const char **argv, const char *prefix)
for (i = 0; i < argc; i++) {
const char *src = source[i], *dst = destination[i];
enum update_mode mode = modes[i];
+@@ builtin/mv.c: int cmd_mv(int argc, const char **argv, const char *prefix)
+ printf(_("Renaming %s to %s\n"), src, dst);
+ if (show_only)
+ continue;
+- if (mode != INDEX && rename(src, dst) < 0) {
++ if (mode != INDEX && mode != SPARSE && rename(src, dst) < 0) {
+ if (ignore_errors)
+ continue;
+ die_errno(_("renaming '%s' failed"), src);
## t/t7002-mv-sparse-checkout.sh (new) ##
@@
@@ t/t7002-mv-sparse-checkout.sh (new)
+"
+
+test_expect_success 'mv refuses to move sparse-to-sparse' '
-+ rm -f e &&
++ test_when_finished rm -f e &&
+ git reset --hard &&
+ git sparse-checkout set a &&
+ touch b &&
@@ t/t7002-mv-sparse-checkout.sh (new)
+ echo b >>expect &&
+ echo e >>expect &&
+ cat sparse_hint >>expect &&
-+ test_cmp expect stderr
++ test_cmp expect stderr &&
++ git mv --sparse b e 2>stderr &&
++ test_must_be_empty stderr
+'
+
+test_expect_success 'mv refuses to move sparse-to-sparse, ignores failure' '
-+ rm -f e &&
++ test_when_finished rm -f b c e &&
+ git reset --hard &&
+ git sparse-checkout set a &&
++
++ # tracked-to-untracked
+ touch b &&
+ git mv -k b e 2>stderr &&
++ test_path_exists b &&
++ test_path_is_missing e &&
+ cat sparse_error_header >expect &&
+ echo b >>expect &&
+ echo e >>expect &&
+ cat sparse_hint >>expect &&
-+ test_cmp expect stderr
++ test_cmp expect stderr &&
++
++ git mv --sparse b e 2>stderr &&
++ test_must_be_empty stderr &&
++ test_path_is_missing b &&
++ test_path_exists e &&
++
++ # tracked-to-tracked
++ git reset --hard &&
++ touch b &&
++ git mv -k b c 2>stderr &&
++ test_path_exists b &&
++ test_path_is_missing c &&
++ cat sparse_error_header >expect &&
++ echo b >>expect &&
++ echo c >>expect &&
++ cat sparse_hint >>expect &&
++ test_cmp expect stderr &&
++
++ git mv --sparse b c 2>stderr &&
++ test_must_be_empty stderr &&
++ test_path_is_missing b &&
++ test_path_exists c
+'
+
+test_expect_success 'mv refuses to move non-sparse-to-sparse' '
-+ rm -f e &&
++ test_when_finished rm -f b c e &&
+ git reset --hard &&
+ git sparse-checkout set a &&
++
++ # tracked-to-untracked
+ test_must_fail git mv a e 2>stderr &&
++ test_path_exists a &&
++ test_path_is_missing e &&
+ cat sparse_error_header >expect &&
+ echo e >>expect &&
+ cat sparse_hint >>expect &&
-+ test_cmp expect stderr
++ test_cmp expect stderr &&
++ git mv --sparse a e 2>stderr &&
++ test_must_be_empty stderr &&
++ test_path_is_missing a &&
++ test_path_exists e &&
++
++ # tracked-to-tracked
++ rm e &&
++ git reset --hard &&
++ test_must_fail git mv a c 2>stderr &&
++ test_path_exists a &&
++ test_path_is_missing c &&
++ cat sparse_error_header >expect &&
++ echo c >>expect &&
++ cat sparse_hint >>expect &&
++ test_cmp expect stderr &&
++ git mv --sparse a c 2>stderr &&
++ test_must_be_empty stderr &&
++ test_path_is_missing a &&
++ test_path_exists c
+'
+
+test_expect_success 'mv refuses to move sparse-to-non-sparse' '
-+ rm -f e &&
++ test_when_finished rm -f b c e &&
+ git reset --hard &&
+ git sparse-checkout set a e &&
++
++ # tracked-to-untracked
+ touch b &&
+ test_must_fail git mv b e 2>stderr &&
+ cat sparse_error_header >expect &&
+ echo b >>expect &&
+ cat sparse_hint >>expect &&
-+ test_cmp expect stderr
++ test_cmp expect stderr &&
++ git mv --sparse b e 2>stderr &&
++ test_must_be_empty stderr
+'
+
+test_expect_success 'recursive mv refuses to move (possible) sparse' '
-+ rm -f e &&
++ test_when_finished rm -rf b c e sub2 &&
+ git reset --hard &&
+ # Without cone mode, "sub" and "sub2" do not match
+ git sparse-checkout set sub/dir sub2/dir &&
++
++ # Add contained contents to ensure we avoid non-existence errors
++ mkdir sub/dir2 &&
++ touch sub/d sub/dir2/e &&
++
+ test_must_fail git mv sub sub2 2>stderr &&
+ cat sparse_error_header >expect &&
-+ echo sub >>expect &&
-+ echo sub2 >>expect &&
++ cat >>expect <<-\EOF &&
++ sub/d
++ sub2/d
++ sub/dir/e
++ sub2/dir/e
++ sub/dir2/e
++ sub2/dir2/e
++ EOF
+ cat sparse_hint >>expect &&
-+ test_cmp expect stderr
++ test_cmp expect stderr &&
++ git mv --sparse sub sub2 2>stderr &&
++ test_must_be_empty stderr &&
++ git commit -m "moved sub to sub2" &&
++ git rev-parse HEAD~1:sub >expect &&
++ git rev-parse HEAD:sub2 >actual &&
++ test_cmp expect actual &&
++ git reset --hard HEAD~1
+'
+
+test_expect_success 'recursive mv refuses to move sparse' '
@@ t/t7002-mv-sparse-checkout.sh (new)
+ # Use cone mode so "sub/" matches the sparse-checkout patterns
+ git sparse-checkout init --cone &&
+ git sparse-checkout set sub/dir sub2/dir &&
++
++ # Add contained contents to ensure we avoid non-existence errors
++ mkdir sub/dir2 &&
++ touch sub/dir2/e &&
++
+ test_must_fail git mv sub sub2 2>stderr &&
+ cat sparse_error_header >expect &&
-+ echo sub/dir2/e >>expect &&
-+ echo sub2/dir2/e >>expect &&
++ cat >>expect <<-\EOF &&
++ sub/dir2/e
++ sub2/dir2/e
++ EOF
+ cat sparse_hint >>expect &&
-+ test_cmp expect stderr
++ test_cmp expect stderr &&
++ git mv --sparse sub sub2 2>stderr &&
++ test_must_be_empty stderr &&
++ git commit -m "moved sub to sub2" &&
++ git rev-parse HEAD~1:sub >expect &&
++ git rev-parse HEAD:sub2 >actual &&
++ test_cmp expect actual &&
++ git reset --hard HEAD~1
+'
+
+test_done
12: f6c0d4e3a06 < -: ----------- mv: add '--sparse' option to ignore sparse-checkout
13: 7749a69a38f ! 14: f2abc387822 advice: update message to suggest '--sparse' @@ Commit message
To improve the discoverability of this new functionality, add a message to advice.updateSparsePath that mentions the existence of the option.
+ The previous set of changes also modified the purpose of this message to
+ include possibly a list of paths instead of only a list of pathspecs.
+ Make the warning message more clear about this new behavior.
+
## advice.c ##
@@ advice.c: void advise_on_updating_sparse_paths(struct string_list *pathspec_list)
+ 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"));
++ fprintf(stderr, _("The following paths and/or pathspecs matched "
++ "paths that exist outside of your\n"
++ "sparse-checkout definition, so will not be "
++ "updated in the index:\n"));
+ for_each_string_list_item(item, pathspec_list)
fprintf(stderr, "%s\n", item->string);
advise_if_enabled(ADVICE_UPDATE_SPARSE_PATH,
@@ advice.c: void advise_on_updating_sparse_paths(struct string_list *pathspec_list
## t/t3602-rm-sparse-checkout.sh ##
@@ t/t3602-rm-sparse-checkout.sh: test_expect_success 'setup' "
+ 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:
++ The following paths and/or pathspecs matched paths that exist outside of your
++ sparse-checkout definition, so will not be updated in the index:
EOF
cat >sparse_hint <<-EOF &&
@@ t/t3602-rm-sparse-checkout.sh: test_expect_success 'setup' "
## t/t3705-add-sparse-checkout.sh ##
-@@ t/t3705-add-sparse-checkout.sh: test_expect_success 'setup' " +@@ t/t3705-add-sparse-checkout.sh: test_sparse_entry_unstaged () { +
+ 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:
++ The following paths and/or pathspecs matched paths that exist outside of your
++ sparse-checkout definition, so will not be updated in the index:
EOF
cat >sparse_hint <<-EOF &&
@@ t/t3705-add-sparse-checkout.sh: test_expect_success 'setup' "
## t/t7002-mv-sparse-checkout.sh ##
@@ t/t7002-mv-sparse-checkout.sh: test_expect_success 'setup' "
+ 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:
++ The following paths and/or pathspecs matched paths that exist outside of your
++ sparse-checkout definition, so will not be updated in the index:
EOF
cat >sparse_hint <<-EOF
Signature · 1 line
gitgitgadget
PATCH v2 1/14 t3705: test that 'sparse_entry' is unstaged
From: Derrick Stolee <[email protected]>
The tests in t3705-add-sparse-checkout.sh check to see how 'git add' behaves with paths outside the sparse-checkout definition. These currently check to see if a given warning is present but not that the index is not updated with the sparse entries. Add a new 'test_sparse_entry_unstaged' helper to be sure 'git add' is behaving correctly.
We need to modify setup_sparse_entry to actually commit the sparse_entry file so it exists at HEAD but is not already staged in the index.
--- t/t3705-add-sparse-checkout.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+)
t/t3705-add-sparse-checkout.sh +14 -0
@@ -19,6 +19,7 @@ setup_sparse_entry () { fi && git add sparse_entry && git update-index --skip-worktree sparse_entry && + git commit --allow-empty -m "ensure sparse_entry exists at HEAD" && SPARSE_ENTRY_BLOB=$(git rev-parse :sparse_entry) }
@@ -36,6 +37,11 @@ setup_gitignore () { EOF } +test_sparse_entry_unstaged () { + git status --porcelain >actual && + ! grep "^M sparse_entry\$" actual +} + test_expect_success 'setup' " cat >sparse_error_header <<-EOF && The following pathspecs didn't match any eligible path, but they do match index
@@ -55,6 +61,7 @@ test_expect_success 'git add does not remove sparse entries' ' setup_sparse_entry && rm sparse_entry && test_must_fail git add sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -73,6 +80,7 @@ test_expect_success 'git add . does not remove sparse entries' ' rm sparse_entry && setup_gitignore && test_must_fail git add . 2>stderr && + test_sparse_entry_unstaged && cat sparse_error_header >expect && echo . >>expect &&
@@ -88,6 +96,7 @@ do setup_sparse_entry && echo modified >sparse_entry && test_must_fail git add $opt sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -98,6 +107,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' git ls-files --debug sparse_entry | grep mtime >before && test-tool chmtime -60 sparse_entry && test_must_fail git add --refresh sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && git ls-files --debug sparse_entry | grep mtime >after && test_cmp before after
@@ -106,6 +116,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' test_expect_success 'git add --chmod does not update sparse entries' ' setup_sparse_entry && test_must_fail git add --chmod=+x sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged && ! test -x sparse_entry
@@ -116,6 +127,7 @@ test_expect_success 'git add --renormalize does not update sparse entries' ' setup_sparse_entry "LINEONE\r\nLINETWO\r\n" && echo "sparse_entry text=auto" >.gitattributes && test_must_fail git add --renormalize sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -124,6 +136,7 @@ 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_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -148,6 +161,7 @@ test_expect_success 'do not warn when pathspec matches dense entries' ' test_expect_success 'add obeys advice.updateSparsePath' ' setup_sparse_entry && test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp sparse_entry_error stderr '
Signature · 1 line
gitgitgadget
PATCH v2 2/14 t1092: behavior for adding sparse files
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Add some tests to demonstrate the current behavior around adding files outside of the sparse-checkout cone. Currently, untracked files are handled differently from tracked files. A future change will make these cases be handled the same way.
Further expand checking that a failed 'git add' does not stage changes to the index.
--- t/t1092-sparse-checkout-compatibility.sh | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+)
t/t1092-sparse-checkout-compatibility.sh +28 -0
@@ -187,6 +187,16 @@ test_sparse_match () { test_cmp sparse-checkout-err sparse-index-err } +test_sparse_unstaged () { + file=$1 && + for repo in sparse-checkout sparse-index + do + git -C $repo status --porcelain >$repo-out && + ! grep "^A $file\$" $repo-out && + ! grep "^M $file\$" $repo-out || return 1 + done +} + test_expect_success 'sparse-index contents' ' init_repos &&
@@ -291,6 +301,20 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' +# NEEDSWORK: This documents current behavior, but is not a desirable +# behavior (untracked files are handled differently than tracked). +test_expect_success 'add outside sparse cone' ' + init_repos && + + run_on_sparse mkdir folder1 && + run_on_sparse ../edit-contents folder1/a && + run_on_sparse ../edit-contents folder1/newfile && + test_sparse_match test_must_fail git add folder1/a && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && + test_sparse_match git add folder1/newfile +' + test_expect_success 'commit including unstaged changes' ' init_repos &&
@@ -339,7 +363,11 @@ test_expect_success 'status/add: outside sparse cone' ' # Adding the path outside of the sparse-checkout cone should fail. test_sparse_match test_must_fail git add folder1/a && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && test_sparse_match test_must_fail git add --refresh folder1/a && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && # NEEDSWORK: Adding a newly-tracked file outside the cone succeeds test_sparse_match git add folder1/new &&
Signature · 1 line
gitgitgadget
PATCH v2 3/14 dir: extract directory-matching logic
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The last_matching_pattern_from_list() logic performs some checks on the filetype of a path within the index when the PATTERN_FLAG_MUSTBEDIR flag is set. This works great when setting SKIP_WORKTREE bits within unpack_trees(), but doesn't work well when passing an arbitrary path such as a file within a matching directory.
This change only rearranges the logic but does not change its functionality.
We will expand the path_matches_dir_pattern() method in a following change.
--- dir.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-)
dir.c +17 -5
@@ -1303,6 +1303,19 @@ int match_pathname(const char *pathname, int pathlen, WM_PATHNAME) == 0; } +static int path_matches_dir_pattern (const char *pathname, +int pathlen, +int *dtype, +struct path_pattern *pattern, +struct index_state *istate) +{ + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); +if (*dtype !=DT_DIR ) +return 0 ; + +return 1 ; +} +/* * Scan the given exclude list in reverse to see whether pathname * should be ignored. The first match (i.e. the last on the list), if
@@ -1327,11 +1340,10 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnameconst char *exclude = pattern->pattern;int prefix = pattern->nowildcardlen; -if (pattern->flags &PATTERN_FLAG_MUSTBEDIR ) { - *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); -if (*dtype !=DT_DIR ) -continue ; - } +if ((pattern->flags &PATTERN_FLAG_MUSTBEDIR ) && + !path_matches_dir_pattern (pathname, pathlen, + dtype, pattern, istate)) +continue ;if (pattern->flags &PATTERN_FLAG_NODIR ) { if (match_basename(basename,
Signature · 1 line
gitgitgadget
PATCH v2 4/14 dir: select directories correctly
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When matching a path against a list of patterns, the ones that require a directory match previously did not work when a filename is specified. This was fine when all pattern-matching was done within methods such as unpack_trees() that check a directory before recursing into the contained files. However, other commands will start matching individual files against pattern lists without that recursive approach.
We modify path_matches_dir_pattern() to take a strbuf 'path_parent' that is used to store the parent directory of 'pathname' between multiple pattern matching tests. This is loaded lazily, only on the first pattern it finds that has the PATTERN_FLAG_MUSTBEDIR flag.
If we find that a path has a parent directory, we start by checking to see if that parent directory matches the pattern. If so, then we do not need to query the index for the type (which can be expensive). If we find that the parent does not match, then we still must check the type from the index for the given pathname.
Note that this does not affect cone mode pattern matching, but instead the more general -- and slower -- full pattern set. Thus, this does not affect the sparse index.
--- dir.c | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-)
dir.c +32 -2
@@ -1305,10 +1305,38 @@ int match_pathname(const char *pathname, int pathlen,static int path_matches_dir_pattern (const char *pathname,int pathlen, +struct strbuf *path_parent,int *dtype,struct path_pattern *pattern,struct index_state *istate) { +/* +* Use 'alloc' as an indicator that the string has not been +* initialized, in case the parent is the root directory. +*/ +if (!path_parent->alloc) { +char *slash; +strbuf_addstr (path_parent, pathname); + slash =find_last_dir_sep (path_parent->buf); + +if (slash) + *slash ='\0' ; +else +strbuf_setlen (path_parent,0 ); + } + +/* +* If the parent directory matches the pattern, then we do not +* need to check for dtype. +*/ +if (path_parent->len && +match_pathname (path_parent->buf, path_parent->len, + pattern->base, + pattern->baselen ? pattern->baselen -1 :0 , + pattern->pattern, pattern->nowildcardlen, + pattern->patternlen, pattern->flags)) +return 1 ; + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen);if (*dtype !=DT_DIR )return 0 ;
@@ -1331,6 +1359,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname {struct path_pattern *res =NULL ;/* undecided */ int i; +struct strbuf path_parent =STRBUF_INIT ;if (!pl->nr)return NULL ;/* undefined */
@@ -1340,8 +1369,8 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnameconst char *exclude = pattern->pattern;int prefix = pattern->nowildcardlen; -if ((pattern->flags &PATTERN_FLAG_MUSTBEDIR ) && - !path_matches_dir_pattern (pathname, pathlen, +if (pattern->flags &PATTERN_FLAG_MUSTBEDIR && + !path_matches_dir_pattern (pathname, pathlen, &path_parent, dtype, pattern, istate))continue ;
@@ -1367,6 +1396,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnamebreak ; } } +strbuf_release (&path_parent);return res; }
Signature · 1 line
gitgitgadget
PATCH v2 5/14 dir: fix pattern matching on dirs
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Within match_pathname(), one successful matching category happens when the pattern is equal to its non-wildcard prefix. At this point, we have checked that the input 'pathname' matches the pattern up to the prefix length, and then we subtraced that length from both 'patternlen' and 'namelen'.
In the case of a directory match, this prefix match should be sufficient. However, the success condition only cared about _exact_ equality here. Instead, we should allow any path that agrees on this prefix in the case of PATTERN_FLAG_MUSTBEDIR.
This case was not tested before because of the way unpack_trees() would match a parent directory before visiting the contained paths. This approach is changing, so we must change this comparison.
--- dir.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
dir.c +1 -1
@@ -1294,7 +1294,7 @@ int match_pathname(const char *pathname, int pathlen,* then our prefix match is all we need; we * do not need to call fnmatch at all. */ -if (!patternlen && !namelen) +if (!patternlen && (!namelen || (flags &PATTERN_FLAG_MUSTBEDIR )))return 1 ; }
Signature · 1 line
gitgitgadget
PATCH v2 6/14 add: fail when adding an untracked sparse file
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The add_files() method in builtin/add.c takes a set of untracked files that are being added by the input pathspec and inserts them into the index. If these files are outside of the sparse-checkout cone, then they gain the SKIP_WORKTREE bit at some point. However, this was not checked before inserting into the index, so these files are added even though we want to avoid modifying the index outside of the sparse-checkout cone.
Add a check within add_files() for these files and write the advice about files outside of the sparse-checkout cone.
This behavior change modifies some existing tests within t1092. These tests intended to document how a user could interact with the existing behavior in place. Many of these tests need to be marked as expecting failure. A future change will allow these tests to pass by adding a flag to 'git add' that allows users to modify index entries outside of the sparse-checkout cone.
The 'submodule handling' test is intended to document what happens to directories that contain a submodule when the sparse index is enabled. It is not trying to say that users should be able to add submodules outside of the sparse-checkout cone, so that test can be modified to avoid that operation.
--- builtin/add.c | 14 +++++++++ t/t1092-sparse-checkout-compatibility.sh | 37 ++++++++++++++++++------ 2 files changed, 42 insertions(+), 9 deletions(-)
builtin/add.c +14 -0
@@ -443,6 +443,7 @@ static void check_embedded_repo(const char *path)static int add_files (struct dir_struct *dir,int flags) {int i, exit_status =0 ; +struct string_list matched_sparse_paths =STRING_LIST_INIT_NODUP ;if (dir->ignored_nr) {fprintf (stderr,_ (ignore_error));
@@ -456,6 +457,11 @@ static int add_files(struct dir_struct *dir, int flags) }for (i =0 ; i < dir->nr; i++) { +if (!path_in_sparse_checkout (dir->entries[i]->name, &the_index)) { +string_list_append (&matched_sparse_paths, + dir->entries[i]->name); +continue ; + }if (add_file_to_index (&the_index, dir->entries[i]->name, flags)) {if (!ignore_add_errors)die (_ ("adding files failed" ));
@@ -464,6 +470,14 @@ static int add_files(struct dir_struct *dir, int flags)check_embedded_repo (dir->entries[i]->name); } } + +if (matched_sparse_paths.nr) { +advise_on_updating_sparse_paths (&matched_sparse_paths); + exit_status =1 ; + } + +string_list_clear (&matched_sparse_paths,0 ); +return exit_status; }
t/t1092-sparse-checkout-compatibility.sh +28 -9
@@ -301,8 +301,6 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' -# NEEDSWORK: This documents current behavior, but is not a desirable -# behavior (untracked files are handled differently than tracked). test_expect_success 'add outside sparse cone' ' init_repos &&
@@ -312,7 +310,9 @@ test_expect_success 'add outside sparse cone' ' test_sparse_match test_must_fail git add folder1/a && test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && - test_sparse_match git add folder1/newfile + test_sparse_match test_must_fail git add folder1/newfile && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/newfile ' test_expect_success 'commit including unstaged changes' '
@@ -343,7 +343,11 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -test_expect_success 'status/add: outside sparse cone' ' +# NEEDSWORK: Now that 'git add folder1/new' fails, the changes being +# attempted here fail for the sparse-checkout and sparse-index repos. +# We must enable a way for adding files outside the sparse-checkout +# done, even if it is by an optional flag. +test_expect_failure 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -368,10 +372,11 @@ test_expect_success 'status/add: outside sparse cone' ' test_sparse_match test_must_fail git add --refresh folder1/a && test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && + test_sparse_match test_must_fail git add folder1/new && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/new && - # NEEDSWORK: Adding a newly-tracked file outside the cone succeeds - test_sparse_match git add folder1/new && - + # NEEDSWORK: behavior begins to deviate here. test_all_match git add . && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/new &&
@@ -527,7 +532,7 @@ test_expect_success 'merge, cherry-pick, and rebase' ' # Right now, users might be using this flow to work through conflicts, # so any solution should present advice to users who try this sequence # of commands to follow whatever new method we create. -test_expect_success 'merge with conflict outside cone' ' +test_expect_failure 'merge with conflict outside cone' ' init_repos && test_all_match git checkout -b merge-tip merge-left &&
@@ -541,12 +546,18 @@ test_expect_success 'merge with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers + # NEEDSWORK: Even though the merge conflict removed the + # SKIP_WORKTREE bit from the index entry for folder1/a, we should + # warn that this is a problematic add. test_all_match git add folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. run_on_all mv folder2/a folder2/z && + # NEEDSWORK: This mode now fails, because folder2/z is + # outside of the sparse-checkout cone and does not match an + # existing index entry with the SKIP_WORKTREE bit cleared. test_all_match git add folder2 && test_all_match git status --porcelain=v2 &&
@@ -555,7 +566,7 @@ test_expect_success 'merge with conflict outside cone' ' test_all_match git rev-parse HEAD^{tree} ' -test_expect_success 'cherry-pick/rebase with conflict outside cone' ' +test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' init_repos && for OPERATION in cherry-pick rebase
@@ -572,11 +583,17 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers + # NEEDSWORK: Even though the merge conflict removed the + # SKIP_WORKTREE bit from the index entry for folder1/a, we should + # warn that this is a problematic add. test_all_match git add folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. + # NEEDSWORK: This mode now fails, because folder2/z is + # outside of the sparse-checkout cone and does not match an + # existing index entry with the SKIP_WORKTREE bit cleared. run_on_all mv folder2/a folder2/z && test_all_match git add folder2 && test_all_match git status --porcelain=v2 &&
@@ -654,6 +671,7 @@ test_expect_success 'clean' ' test_expect_success 'submodule handling' ' init_repos && + test_sparse_match git sparse-checkout add modules && test_all_match mkdir modules && test_all_match touch modules/a && test_all_match git add modules &&
@@ -663,6 +681,7 @@ test_expect_success 'submodule handling' ' test_all_match git commit -m "add submodule" && # having a submodule prevents "modules" from collapse + test_sparse_match git sparse-checkout set deep/deeper1 && test-tool -C sparse-index read-cache --table >cache && grep "100644 blob .* modules/a" cache && grep "160000 commit $(git -C initial-repo rev-parse HEAD) modules/sub" cache
Signature · 1 line
gitgitgadget
PATCH v2 7/14 add: skip tracked paths outside sparse-checkout cone
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When 'git add' adds a tracked file that is outside of the sparse-checkout cone, it checks the SKIP_WORKTREE bit to see if the file exists outside of the sparse-checkout cone. This is usually correct, except in the case of a merge conflict outside of the cone.
Modify add_pathspec_matched_against_index() to be more careful about paths by checking the sparse-checkout patterns in addition to the SKIP_WORKTREE bit. This causes 'git add' to no longer allow files outside of the cone that removed the SKIP_WORKTREE bit due to a merge conflict.
With only this change, users will only be able to add the file after adding the file to the sparse-checkout cone. A later change will allow users to force adding even though the file is outside of the sparse-checkout cone.
--- builtin/add.c | 4 ++++ pathspec.c | 5 +++-- t/t1091-sparse-checkout-builtin.sh | 4 +++- t/t1092-sparse-checkout-compatibility.sh | 21 ++++++++++++++------- t/t3705-add-sparse-checkout.sh | 12 ++++++++++++ 5 files changed, 36 insertions(+), 10 deletions(-)
builtin/add.c +4 -0
@@ -94,6 +94,10 @@ static void update_callback(struct diff_queue_struct *q,for (i =0 ; i < q->nr; i++) {struct diff_filepair *p = q->queue[i];const char *path = p->one->path; + +if (!path_in_sparse_checkout (path, &the_index)) +continue ; +switch (fix_unmerged_status (p, data)) {default :die (_ ("unexpected diff status %c" ), p->status);
pathspec.c +3 -2
@@ -39,7 +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)) +if (sw_action ==PS_IGNORE_SKIP_WORKTREE && + (ce_skip_worktree (ce) || !path_in_sparse_checkout (ce->name, istate)))continue ;ce_path_match (istate, ce, pathspec, seen); }
@@ -70,7 +71,7 @@ char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec)for (i =0 ; i < istate->cache_nr; i++) {struct cache_entry *ce = istate->cache[i]; -if (ce_skip_worktree (ce)) +if (ce_skip_worktree (ce) || !path_in_sparse_checkout (ce->name, istate))ce_path_match (istate, ce, pathspec, seen); }
t/t1091-sparse-checkout-builtin.sh +3 -1
@@ -406,7 +406,7 @@ test_expect_success 'sparse-checkout (init|set|disable) warns with unmerged stat git -C unmerged sparse-checkout disable ' -test_expect_success 'sparse-checkout reapply' ' +test_expect_failure 'sparse-checkout reapply' ' git clone repo tweak && echo dirty >tweak/deep/deeper2/a &&
@@ -438,6 +438,8 @@ test_expect_success 'sparse-checkout reapply' ' test_i18ngrep "warning.*The following paths are unmerged" err && test_path_is_file tweak/folder1/a && + # NEEDSWORK: We are asking to update a file outside of the + # sparse-checkout cone, but this is no longer allowed. git -C tweak add folder1/a && git -C tweak sparse-checkout reapply 2>err && test_must_be_empty err &&
t/t1092-sparse-checkout-compatibility.sh +14 -7
@@ -546,10 +546,10 @@ test_expect_failure 'merge with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers - # NEEDSWORK: Even though the merge conflict removed the - # SKIP_WORKTREE bit from the index entry for folder1/a, we should - # warn that this is a problematic add. - test_all_match git add folder1/a && + test_sparse_match test_must_fail git add folder1/a && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && + test_all_match git add --sparse folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -558,7 +558,10 @@ test_expect_failure 'merge with conflict outside cone' ' # NEEDSWORK: This mode now fails, because folder2/z is # outside of the sparse-checkout cone and does not match an # existing index entry with the SKIP_WORKTREE bit cleared. - test_all_match git add folder2 && + test_sparse_match test_must_fail git add folder2 && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder2/z && + test_all_match git add --sparse folder2 && test_all_match git status --porcelain=v2 && test_all_match git merge --continue &&
@@ -586,7 +589,9 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' # NEEDSWORK: Even though the merge conflict removed the # SKIP_WORKTREE bit from the index entry for folder1/a, we should # warn that this is a problematic add. - test_all_match git add folder1/a && + test_sparse_match test_must_fail git add folder1/a && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -595,7 +600,9 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' # outside of the sparse-checkout cone and does not match an # existing index entry with the SKIP_WORKTREE bit cleared. run_on_all mv folder2/a folder2/z && - test_all_match git add folder2 && + test_sparse_match test_must_fail git add folder2 && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder2/z && test_all_match git status --porcelain=v2 && test_all_match git $OPERATION --continue &&
t/t3705-add-sparse-checkout.sh +12 -0
@@ -158,6 +158,18 @@ test_expect_success 'do not warn when pathspec matches dense entries' ' git ls-files --error-unmatch dense_entry ' +test_expect_success 'git add fails outside of sparse-checkout definition' ' + test_when_finished git sparse-checkout disable && + test_commit a && + git sparse-checkout init && + git sparse-checkout set a && + echo >>sparse_entry && + + git update-index --no-skip-worktree sparse_entry && + test_must_fail git add sparse_entry && + test_sparse_entry_unstaged +' + test_expect_success 'add obeys advice.updateSparsePath' ' setup_sparse_entry && test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr &&
Signature · 1 line
gitgitgadget
PATCH v2 9/14 add: update --chmod to skip sparse paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
We added checks for path_in_sparse_checkout() to portions of 'git add' that add warnings and prevent staging a modification, but we skipped the --chmod mode. Update chmod_pathspec() to ignore cache entries whose path is outside of the sparse-checkout cone (unless --sparse is provided). Add a test in t3705.
--- builtin/add.c | 4 +++- t/t3705-add-sparse-checkout.sh | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-)
builtin/add.c +3 -1
@@ -47,7 +47,9 @@ static int chmod_pathspec(struct pathspec *pathspec, char flip, int show_only)struct cache_entry *ce = active_cache[i];int err; -if (!include_sparse &&ce_skip_worktree (ce)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;if (pathspec && !ce_path_match (&the_index, ce, pathspec,NULL ))
t/t3705-add-sparse-checkout.sh +10 -2
@@ -39,7 +39,7 @@ setup_gitignore () { test_sparse_entry_unstaged () { git status --porcelain >actual && - ! grep "^M sparse_entry\$" actual + ! grep "^M[ M] sparse_entry\$" actual } test_expect_success 'setup' "
@@ -169,11 +169,19 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' test_must_fail git add sparse_entry && test_sparse_entry_unstaged && + test_must_fail git add --chmod=+x sparse_entry && + test_sparse_entry_unstaged && + # Avoid munging CRLFs to avoid an error message git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && test_must_be_empty stderr && test-tool read-cache --table >actual && - grep "^100644 blob.*sparse_entry\$" actual + grep "^100644 blob.*sparse_entry\$" actual && + + git add --sparse --chmod=+x sparse_entry 2>stderr && + test_must_be_empty stderr && + test-tool read-cache --table >actual && + grep "^100755 blob.*sparse_entry\$" actual ' test_expect_success 'add obeys advice.updateSparsePath' '
Signature · 1 line
gitgitgadget
PATCH v2 8/14 add: implement the --sparse option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
We previously modified 'git add' to refuse updating index entries outside of the sparse-checkout cone. This is justified to prevent users from accidentally getting into a confusing state when Git removes those files from the working tree at some later point.
Unfortunately, this caused some workflows that were previously possible to become impossible, especially around merge conflicts outside of the sparse-checkout cone. These were documented in tests within t1092.
We now re-enable these workflows using a new '--sparse' option to 'git add'. This allows users to signal "Yes, I do know what I'm doing with these files," and accept the consequences of the files leaving the worktree later.
We delay updating the advice message until implementing a similar option in 'git rm' and 'git mv'.
--- Documentation/git-add.txt | 9 +++++++- builtin/add.c | 12 +++++++---- t/t1092-sparse-checkout-compatibility.sh | 27 ++++++++---------------- t/t3705-add-sparse-checkout.sh | 17 ++++++++++++++- 4 files changed, 41 insertions(+), 24 deletions(-)
Documentation/git-add.txt +8 -1
@@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git add' [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p] - [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] + [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] [--sparse] [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize] [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]
@@ -79,6 +79,13 @@ in linkgit:gitglossary[7]. --force:: Allow adding otherwise ignored files. +--sparse:: + Allow updating index entries outside of the sparse-checkout cone. + Normally, `git add` refuses to update index entries whose paths do + not fit within the sparse-checkout cone, since those files might + be removed from the working tree without warning. See + linkgit:git-sparse-checkout[1] for more. + -i:: --interactive:: Add modified contents in the working tree interactively to
builtin/add.c +8 -4
@@ -30,6 +30,7 @@ static int patch_interactive, add_interactive, edit_interactive;static int take_worktree_changes;static int add_renormalize;static int pathspec_file_nul; +static int include_sparse;static const char *pathspec_from_file;static int legacy_stash_p;/* support for the scripted `git stash` */
@@ -46,7 +47,7 @@ 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)) +if (!include_sparse &&ce_skip_worktree (ce))continue ;if (pathspec && !ce_path_match (&the_index, ce, pathspec,NULL ))
@@ -95,7 +96,7 @@ static void update_callback(struct diff_queue_struct *q,struct diff_filepair *p = q->queue[i];const char *path = p->one->path; -if (!path_in_sparse_checkout (path, &the_index)) +if (!include_sparse && !path_in_sparse_checkout (path, &the_index))continue ;switch (fix_unmerged_status (p, data)) {
@@ -383,6 +384,7 @@ static struct option builtin_add_options[] = {OPT_BOOL (0 ,"refresh" , &refresh_only,N_ ("don't add, only refresh the index" )),OPT_BOOL (0 ,"ignore-errors" , &ignore_add_errors,N_ ("just skip files which cannot be added because of errors" )),OPT_BOOL (0 ,"ignore-missing" , &ignore_missing,N_ ("check if - even missing - files are ignored in dry run" )), +OPT_BOOL (0 ,"sparse" , &include_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_STRING (0 ,"chmod" , &chmod_arg,"(+|-)x" ,N_ ("override the executable bit of the listed files" )), OPT_HIDDEN_BOOL(0, "warn-embedded-repo", &warn_on_embedded_repo,
@@ -461,7 +463,8 @@ static int add_files(struct dir_struct *dir, int flags) }for (i =0 ; i < dir->nr; i++) { -if (!path_in_sparse_checkout (dir->entries[i]->name, &the_index)) { +if (!include_sparse && + !path_in_sparse_checkout (dir->entries[i]->name, &the_index)) {string_list_append (&matched_sparse_paths, dir->entries[i]->name);continue ;
@@ -646,7 +649,8 @@ int cmd_add(int argc, const char **argv, const char *prefix)if (seen[i])continue ; -if (matches_skip_worktree (&pathspec, i, &skip_worktree_seen)) { +if (!include_sparse && +matches_skip_worktree (&pathspec, i, &skip_worktree_seen)) {string_list_append (&only_match_skip_worktree, pathspec.items[i].original);continue ;
t/t1092-sparse-checkout-compatibility.sh +9 -18
@@ -343,11 +343,7 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -# NEEDSWORK: Now that 'git add folder1/new' fails, the changes being -# attempted here fail for the sparse-checkout and sparse-index repos. -# We must enable a way for adding files outside the sparse-checkout -# done, even if it is by an optional flag. -test_expect_failure 'status/add: outside sparse cone' ' +test_expect_success 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -375,15 +371,16 @@ test_expect_failure 'status/add: outside sparse cone' ' test_sparse_match test_must_fail git add folder1/new && test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/new && + test_sparse_match git add --sparse folder1/a && + test_sparse_match git add --sparse folder1/new && - # NEEDSWORK: behavior begins to deviate here. - test_all_match git add . && + test_all_match git add --sparse . && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/new && test_all_match git rev-parse HEAD^{tree} && run_on_all ../edit-contents folder1/newer && - test_all_match git add folder1/ && + test_all_match git add --sparse folder1/ && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/newer && test_all_match git rev-parse HEAD^{tree}
@@ -527,12 +524,7 @@ test_expect_success 'merge, cherry-pick, and rebase' ' done ' -# NEEDSWORK: This test is documenting current behavior, but that -# behavior can be confusing to users so there is desire to change it. -# Right now, users might be using this flow to work through conflicts, -# so any solution should present advice to users who try this sequence -# of commands to follow whatever new method we create. -test_expect_failure 'merge with conflict outside cone' ' +test_expect_success 'merge with conflict outside cone' ' init_repos && test_all_match git checkout -b merge-tip merge-left &&
@@ -555,9 +547,6 @@ test_expect_failure 'merge with conflict outside cone' ' # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. run_on_all mv folder2/a folder2/z && - # NEEDSWORK: This mode now fails, because folder2/z is - # outside of the sparse-checkout cone and does not match an - # existing index entry with the SKIP_WORKTREE bit cleared. test_sparse_match test_must_fail git add folder2 && test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder2/z &&
@@ -569,7 +558,7 @@ test_expect_failure 'merge with conflict outside cone' ' test_all_match git rev-parse HEAD^{tree} ' -test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' +test_expect_success 'cherry-pick/rebase with conflict outside cone' ' init_repos && for OPERATION in cherry-pick rebase
@@ -592,6 +581,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' test_sparse_match test_must_fail git add folder1/a && test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && + test_all_match git add --sparse folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -603,6 +593,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' test_sparse_match test_must_fail git add folder2 && test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder2/z && + test_all_match git add --sparse folder2 && test_all_match git status --porcelain=v2 && test_all_match git $OPERATION --continue &&
t/t3705-add-sparse-checkout.sh +16 -1
@@ -167,7 +167,13 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' git update-index --no-skip-worktree sparse_entry && test_must_fail git add sparse_entry && - test_sparse_entry_unstaged + test_sparse_entry_unstaged && + + # Avoid munging CRLFs to avoid an error message + git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && + test_must_be_empty stderr && + test-tool read-cache --table >actual && + grep "^100644 blob.*sparse_entry\$" actual ' test_expect_success 'add obeys advice.updateSparsePath' '
@@ -178,4 +184,13 @@ test_expect_success 'add obeys advice.updateSparsePath' ' ' +test_expect_success 'add allows sparse entries with --sparse' ' + git sparse-checkout set a && + echo modified >sparse_entry && + test_must_fail git add sparse_entry && + test_sparse_entry_unchanged && + git add --sparse sparse_entry 2>stderr && + test_must_be_empty stderr +' + test_done
Signature · 1 line
gitgitgadget
PATCH v2 10/14 add: update --renormalize to skip sparse paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
We added checks for path_in_sparse_checkout() to portions of 'git add' that add warnings and prevent stagins a modification, but we skipped the --renormalize mode. Update renormalize_tracked_files() to ignore cache entries whose path is outside of the sparse-checkout cone (unless --sparse is provided). Add a test in t3705.
--- builtin/add.c | 4 +++- t/t3705-add-sparse-checkout.sh | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-)
builtin/add.c +3 -1
@@ -154,7 +154,9 @@ 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)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;if (ce_stage (ce))continue ;/* do not touch unmerged paths */
t/t3705-add-sparse-checkout.sh +11 -1
@@ -172,6 +172,9 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' test_must_fail git add --chmod=+x sparse_entry && test_sparse_entry_unstaged && + test_must_fail git add --renormalize sparse_entry && + test_sparse_entry_unstaged && + # Avoid munging CRLFs to avoid an error message git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && test_must_be_empty stderr &&
@@ -181,7 +184,14 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' git add --sparse --chmod=+x sparse_entry 2>stderr && test_must_be_empty stderr && test-tool read-cache --table >actual && - grep "^100755 blob.*sparse_entry\$" actual + grep "^100755 blob.*sparse_entry\$" actual && + + git reset && + + # This will print a message over stderr on Windows. + git add --sparse --renormalize sparse_entry && + git status --porcelain >actual && + grep "^M sparse_entry\$" actual ' test_expect_success 'add obeys advice.updateSparsePath' '
Signature · 1 line
gitgitgadget
PATCH v2 11/14 rm: add --sparse option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
As we did previously in 'git add', add a '--sparse' option to 'git rm' that allows modifying paths outside of the sparse-checkout definition. The existing checks in 'git rm' are restricted to tracked files that have the SKIP_WORKTREE bit in the current index. Future changes will cause 'git rm' to reject removing paths outside of the sparse-checkout definition, even if they are untracked or do not have the SKIP_WORKTREE bit.
--- Documentation/git-rm.txt | 6 ++++++ builtin/rm.c | 8 ++++++-- t/t3602-rm-sparse-checkout.sh | 12 ++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-)
Documentation/git-rm.txt +6 -0
@@ -72,6 +72,12 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7]. --ignore-unmatch:: Exit with a zero status even if no files matched. +--sparse:: + Allow updating index entries outside of the sparse-checkout cone. + Normally, `git rm` refuses to update index entries whose paths do + not fit within the sparse-checkout cone. See + linkgit:git-sparse-checkout[1] for more. + -q:: --quiet:: `git rm` normally outputs one line (in the form of an `rm` command)
builtin/rm.c +6 -2
@@ -237,6 +237,7 @@ static int check_local_mod(struct object_id *head, int index_only)static int show_only =0 , force =0 , index_only =0 , recursive =0 , quiet =0 ;static int ignore_unmatch =0 , pathspec_file_nul; +static int include_sparse;static char *pathspec_from_file;static struct option builtin_rm_options[] = {
@@ -247,6 +248,7 @@ static struct option builtin_rm_options[] = {OPT_BOOL ('r' ,NULL , &recursive,N_ ("allow recursive removal" )),OPT_BOOL (0 ,"ignore-unmatch" , &ignore_unmatch,N_ ("exit with a zero status even if nothing matched" )), +OPT_BOOL (0 ,"sparse" , &include_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_PATHSPEC_FROM_FILE (&pathspec_from_file),OPT_PATHSPEC_FILE_NUL (&pathspec_file_nul), OPT_END(),
@@ -298,7 +300,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix)ensure_full_index (&the_index);for (i =0 ; i < active_nr; i++) {const struct cache_entry *ce = active_cache[i]; -if (ce_skip_worktree (ce)) + +if (!include_sparse &&ce_skip_worktree (ce))continue ;if (!ce_path_match (&the_index, ce, &pathspec, seen))continue ;
@@ -322,7 +325,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix) seen_any =1 ; else if (ignore_unmatch) continue; - else if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) + else if (!include_sparse && + 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);
t/t3602-rm-sparse-checkout.sh +12 -0
@@ -43,6 +43,18 @@ test_expect_success 'recursive rm does not remove sparse entries' ' test_cmp expected actual ' +test_expect_success 'recursive rm --sparse removes sparse entries' ' + git reset --hard && + git sparse-checkout set "sub/dir" && + git rm --sparse -r sub && + git status --porcelain -uno >actual && + cat >expected <<-\EOF && + D sub/d + D sub/dir/e + EOF + test_cmp expected actual +' + test_expect_success 'rm obeys advice.updateSparsePath' ' git reset --hard && git sparse-checkout set a &&
Signature · 1 line
gitgitgadget
PATCH v2 14/14 advice: update message to suggest '--sparse'
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The previous changes modified the behavior of 'git add', 'git rm', and 'git mv' to not adjust paths outside the sparse-checkout cone, even if they exist in the working tree and their cache entries lack the SKIP_WORKTREE bit. The intention is to warn users that they are doing something potentially dangerous. The '--sparse' option was added to each command to allow careful users the same ability they had before.
To improve the discoverability of this new functionality, add a message to advice.updateSparsePath that mentions the existence of the option.
The previous set of changes also modified the purpose of this message to include possibly a list of paths instead of only a list of pathspecs. Make the warning message more clear about this new behavior.
--- advice.c | 10 ++++++---- t/t3602-rm-sparse-checkout.sh | 6 +++--- t/t3705-add-sparse-checkout.sh | 6 +++--- t/t7002-mv-sparse-checkout.sh | 6 +++--- 4 files changed, 15 insertions(+), 13 deletions(-)
advice.c +6 -4
@@ -293,14 +293,16 @@ void advise_on_updating_sparse_paths(struct string_list *pathspec_list)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" )); +fprintf (stderr,_ ("The following paths and/or pathspecs matched " +"paths that exist outside of your\n" +"sparse-checkout definition, so will not be " +"updated in the index:\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" +_ ("Disable or modify the sparsity rules or" +" use the --sparse option if you intend" " to update such entries." )); }
t/t3602-rm-sparse-checkout.sh +3 -3
@@ -11,12 +11,12 @@ test_expect_success 'setup' " 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: + The following paths and/or pathspecs matched paths that exist outside of your + sparse-checkout definition, so will not be updated in the index: EOF cat >sparse_hint <<-EOF && - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
t/t3705-add-sparse-checkout.sh +3 -3
@@ -44,12 +44,12 @@ test_sparse_entry_unstaged () { 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: + The following paths and/or pathspecs matched paths that exist outside of your + sparse-checkout definition, so will not be updated in the index: EOF cat >sparse_hint <<-EOF && - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
t/t7002-mv-sparse-checkout.sh +3 -3
@@ -11,12 +11,12 @@ test_expect_success 'setup' " 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: + The following paths and/or pathspecs matched paths that exist outside of your + sparse-checkout definition, so will not be updated in the index: EOF cat >sparse_hint <<-EOF - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF "
Signature · 1 line
gitgitgadget
PATCH v2 12/14 rm: skip sparse paths with missing SKIP_WORKTREE
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
If a path does not match the sparse-checkout cone but is somehow missing the SKIP_WORKTREE bit, then 'git rm' currently succeeds in removing the file. One reason a user might be in this situation is a merge conflict outside of the sparse-checkout cone. Removing such a file might be problematic for users who are not sure what they are doing.
Add a check to path_in_sparse_checkout() when 'git rm' is checking if a path should be considered for deletion. Of course, this check is ignored if the '--sparse' option is specified, allowing users who accept the risks to continue with the removal.
This also removes a confusing behavior where a user asks for a directory to be removed, but only the entries that are within the sparse-checkout definition are removed. Now, 'git rm <dir>' will fail without '--sparse' and will succeed in removing all contained paths with '--sparse'.
--- builtin/rm.c | 4 +++- t/t3602-rm-sparse-checkout.sh | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-)
builtin/rm.c +3 -1
@@ -301,7 +301,9 @@ 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 (!include_sparse &&ce_skip_worktree (ce)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;if (!ce_path_match (&the_index, ce, &pathspec, seen))continue ;
t/t3602-rm-sparse-checkout.sh +17 -2
@@ -37,9 +37,13 @@ done test_expect_success 'recursive rm does not remove sparse entries' ' git reset --hard && git sparse-checkout set sub/dir && - git rm -r sub && + test_must_fail git rm -r sub && + git rm --sparse -r sub && git status --porcelain -uno >actual && - echo "D sub/dir/e" >expected && + cat >expected <<-\EOF && + D sub/d + D sub/dir/e + EOF test_cmp expected actual '
@@ -87,4 +91,15 @@ test_expect_success 'do not warn about sparse entries with --ignore-unmatch' ' git ls-files --error-unmatch b ' +test_expect_success 'refuse to rm a non-skip-worktree path outside sparse cone' ' + git reset --hard && + git sparse-checkout set a && + git update-index --no-skip-worktree b && + test_must_fail git rm b 2>stderr && + test_cmp b_error_and_hint stderr && + git rm --sparse b 2>stderr && + test_must_be_empty stderr && + test_path_is_missing b +' + test_done
Signature · 1 line
gitgitgadget
PATCH v2 13/14 mv: refuse to move sparse paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Since cmd_mv() does not operate on cache entries and instead directly checks the filesystem, we can only use path_in_sparse_checkout() as a mechanism for seeing if a path is sparse or not. Be sure to skip returning a failure if '-k' is specified.
To ensure that the advice around sparse paths is the only reason a move failed, be sure to check this as the very last thing before inserting into the src_for_dst list.
The tests cover a variety of cases such as whether the target is tracked or untracked, and whether the source or destination are in or outside of the sparse-checkout definition.
--- builtin/mv.c | 52 ++++++++-- t/t7002-mv-sparse-checkout.sh | 186 ++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 9 deletions(-) create mode 100755 t/t7002-mv-sparse-checkout.sh
builtin/mv.c +43 -9
@@ -118,21 +118,23 @@ static int index_range_of_same_dir(const char *src, int length,int cmd_mv (int argc,const char **argv,const char *prefix) {int i, flags, gitmodules_modified =0 ; -int verbose =0 , show_only =0 , force =0 , ignore_errors =0 ; +int verbose =0 , show_only =0 , force =0 , ignore_errors =0 , ignore_sparse =0 ;struct option builtin_mv_options[] = {OPT__VERBOSE (&verbose,N_ ("be verbose" )),OPT__DRY_RUN (&show_only,N_ ("dry run" )),OPT__FORCE (&force,N_ ("force move/rename even if target exists" ),PARSE_OPT_NOCOMPLETE ),OPT_BOOL ('k' ,NULL , &ignore_errors,N_ ("skip move/rename errors" )), +OPT_BOOL (0 ,"sparse" , &ignore_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_END (), };const char **source, **destination, **dest_path, **submodule_gitfile; -enum update_mode {BOTH =0 ,WORKING_DIRECTORY ,INDEX } *modes; +enum update_mode {BOTH =0 ,WORKING_DIRECTORY ,INDEX ,SPARSE } *modes;struct stat st;struct string_list src_for_dst =STRING_LIST_INIT_NODUP ;struct lock_file lock_file =LOCK_INIT ;struct cache_entry *ce; +struct string_list only_match_skip_worktree =STRING_LIST_INIT_NODUP ;git_config (git_default_config,NULL );
@@ -176,14 +178,17 @@ int cmd_mv(int argc, const char **argv, const char *prefix)const char *src = source[i], *dst = destination[i];int length, src_is_dir;const char *bad =NULL ; +int skip_sparse =0 ;if (show_only)printf (_ ("Checking rename of '%s' to '%s'\n" ), src, dst); length =strlen (src); -if (lstat (src, &st) <0 ) - bad =_ ("bad source" ); -else if (!strncmp (src, dst, length) && +if (lstat (src, &st) <0 ) { +/* only error if existence is expected. */ +if (modes[i] !=SPARSE ) + bad =_ ("bad source" ); + }else if (!strncmp (src, dst, length) && (dst[length] ==0 || dst[length] =='/' )) { bad =_ ("can not move directory into itself" ); } else if ((src_is_dir = S_ISDIR(st.st_mode))
@@ -212,11 +217,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix) dst_len =strlen (dst);for (j =0 ; j < last - first; j++) { -const char *path = active_cache[first + j]->name; +const struct cache_entry *ce = active_cache[first + j]; +const char *path = ce->name; source[argc + j] = path; destination[argc + j] =prefix_path (dst, dst_len, path + length +1 ); - modes[argc + j] =INDEX ; + modes[argc + j] =ce_skip_worktree (ce) ?SPARSE :INDEX ; submodule_gitfile[argc + j] =NULL ; } argc += last - first;
@@ -244,14 +250,36 @@ int cmd_mv(int argc, const char **argv, const char *prefix) bad =_ ("multiple sources for the same target" ); else if (is_dir_sep(dst[strlen(dst) - 1])) bad =_ ("destination directory does not exist" ); - else + else { +/* +* We check if the paths are in the sparse-checkout +* definition as a very final check, since that +* allows us to point the user to the --sparse +* option as a way to have a successful run. +*/ +if (!ignore_sparse && + !path_in_sparse_checkout (src, &the_index)) { +string_list_append (&only_match_skip_worktree, src); + skip_sparse =1 ; + } +if (!ignore_sparse && + !path_in_sparse_checkout (dst, &the_index)) { +string_list_append (&only_match_skip_worktree, dst); + skip_sparse =1 ; + } + +if (skip_sparse) + goto remove_entry; +string_list_insert (&src_for_dst, dst); + }if (!bad)continue ;if (!ignore_errors)die (_ ("%s, source=%s, destination=%s" ), bad, src, dst); +remove_entry:if (--argc >0 ) {int n = argc - i; memmove(source + i, source + i + 1,
@@ -266,6 +294,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix) } } +if (only_match_skip_worktree.nr) { +advise_on_updating_sparse_paths (&only_match_skip_worktree); +if (!ignore_errors) +return 1 ; + } +for (i =0 ; i < argc; i++) {const char *src = source[i], *dst = destination[i];enum update_mode mode = modes[i];
@@ -274,7 +308,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)printf (_ ("Renaming %s to %s\n" ), src, dst);if (show_only)continue ; -if (mode !=INDEX &&rename (src, dst) <0 ) { +if (mode !=INDEX && mode !=SPARSE &&rename (src, dst) <0 ) {if (ignore_errors)continue ;die_errno (_ ("renaming '%s' failed" ), src);
t/t7002-mv-sparse-checkout.sh new file +186 -0
186 lines not on this page · raw message
Signature · 1 line
gitgitgadget
Re: [PATCH v2 14/14] advice: update message to suggest '--sparse'
In reply to Derrick Stolee via GitGitGadgetadvice.c void advise_on_updating_sparse_paths(struct string_list *pathspec_list) · line 301
Quoted 9 lines · Derrick Stolee via GitGitGadget
advice.c +4
-3-fprintf (stderr,_ ("The following pathspecs didn't match any" -" eligible path, but they do match index\n" -"entries outside the current sparse checkout:\n" )); +fprintf (stderr,_ ("The following paths and/or pathspecs matched " +"paths that exist outside of your\n" +"sparse-checkout definition, so will not be " +"updated in the index:\n" ));for_each_string_list_item (item, pathspec_list)fprintf (stderr,"%s\n" , item->string);
This before and after looks about as well line-wrapped...
advice.c void advise_on_updating_sparse_paths(struct string_list *pathspec_list) · line 307
Quoted 6 lines
advice.c +2
-1advise_if_enabled (ADVICE_UPDATE_SPARSE_PATH , -_ ("Disable or modify the sparsity rules if you intend" +_ ("Disable or modify the sparsity rules or" +" use the --sparse option if you intend" " to update such entries." )); }
...but here..
t/t3602-rm-sparse-checkout.sh test_expect_success 'setup' " · line 21
Quoted 12 lines
t/t3602-rm-sparse-checkout.sh +3
-3cat >sparse_error_header <<-EOF && - The following pathspecs didn't match any eligible path, but they do match index - entries outside the current sparse checkout: + The following paths and/or pathspecs matched paths that exist outside of your + sparse-checkout definition, so will not be updated in the index: EOF cat >sparse_hint <<-EOF && - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
...this used to line-wrap at 80 characters, but is now a bit beyond that.
Maybe instead make these two into bullet-points?
Also the third "Disable" looks a bit jarring at first, it seems like a continuation of the first message, but it's just the standard "disable this message" we tend to print out.
This commentary pre-dates this commit, but just in general:
I think the advice system is best used where there's an initial non-optional message, and then the advice elaborates on what happened, how to fix it. A good example is the "short object ID %s is ambiguous" in object-name.c.
But in this case both messages are rather long. I'd think better would be something like (and I didn't look very deeply at the involved code):
error("pathspec '%s' matched only outside sparse checkout")
I.e. in e.g. cmd_rm() we loop through the pathspecs, and we error on the first one, so to a first approximation why do we need to for sparse emit ALL the pathspecs we didn't match? if we're going to error out anyway shouldn'w we just error out on the first one?
But going on, I'd think this would be better overall (pseudocode):
error("pathspec '%s' matched only outside sparse checkout")
if (advice_enabled(ADVICE_UPDATE_SPARSE_PATH)) {
char *list_str;
list_of_bad_pathspecs = make_that_list(my_pathspec_string_list, &list_str);
if (list_of_bad_pathspecs.nr > 1)
/* Emit a message that details what's wrong, but also has a
* list of all the other pathspecs we'd also die on if the user */
else
/* Ditto, but no list *?Maybe I'm missing something with the sparse implemention, but I'd think going above & beyond and listing all failures is a bit much in either case, i.e. for non-sparse we have:
$ git rm 'file-i-do-not-have' 'directory-i-do-not-have/'
fatal: pathspec 'file-i-do-not-have' did not match any files
$I'd think in general a user who's screwed up and typo'd both isn't going to be much harmed by us noting the first, maybe they'll get another error then.
But usually it's obvious (e.g. you just ran the command in the wrong directory), so if you have a large list of pathspecs getting a firehose of all the things that didn't match can be less helpful due to being overrly verbose.
Re: [PATCH v2 02/14] t1092: behavior for adding sparse files
In reply to Derrick Stolee via GitGitGadgett/t1092-sparse-checkout-compatibility.sh test_expect_success 'add, commit, checkout' ' · line 305
Quoted 2 lines · Derrick Stolee via GitGitGadget
t/t1092-sparse-checkout-compatibility.sh +2
-0+# NEEDSWORK: This documents current behavior, but is not a desirable +# behavior (untracked files are handled differently than tracked).
I wonder if a test_expect_failure test would be better for the thing that is the desired behavior, but maybe we don't know what the CLI UI for that would look like yet.
t/t1092-sparse-checkout-compatibility.sh test_expect_success 'add, commit, checkout' ' · line 313
Quoted 8 lines
t/t1092-sparse-checkout-compatibility.sh +8
-0+test_expect_success 'add outside sparse cone' ' + init_repos && + + run_on_sparse mkdir folder1 && + run_on_sparse ../edit-contents folder1/a && + run_on_sparse ../edit-contents folder1/newfile && + test_sparse_match test_must_fail git add folder1/a && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
Just "grep" is preferred over "test_i18ngrep" now, the GETTEXT_POISON went away.
Re: [PATCH v2 04/14] dir: select directories correctly
In reply to Derrick Stolee via GitGitGadgetdir.c int match_pathname(const char *pathname, int pathlen, · line 1317
Quoted 5 lines · Derrick Stolee via GitGitGadget
dir.c +5
-0+/* +* Use 'alloc' as an indicator that the string has not been +* initialized, in case the parent is the root directory. +*/ +if (!path_parent->alloc) {
This isn't wrong, but seems to be way too cozy with the internal implementation details of strbuf. For what it's worth I renamed it to "alloc2" and found that this would be only the 3rd bit of code out of strbuf.[ch] that cares about that member.
dir.c int match_pathname(const char *pathname, int pathlen, · line 1319
Quoted 2 lines
dir.c +2
-0+char *slash; +strbuf_addstr (path_parent, pathname);
So is "pathname" ever the empty string? If not we could check the length?
Or probably better: ...
dir.c static struct path_pattern *last_matching_pattern_from_list(const char *pathname · line 1362
Quoted 5 lines
dir.c +1
-0@@ -1331,6 +1359,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname {struct path_pattern *res =NULL ;/* undecided */ int i; +struct strbuf path_parent =STRBUF_INIT ;
Just malloc + strbuf_init() this in the above function and have a "struct strbuf *" initialized to NULL here? Then we can use a much more idiomatic "is it NULL?" to check if it's initialized.
Re: [PATCH v2 02/14] t1092: behavior for adding sparse files
In reply to Ævar Arnfjörð BjarmasonQuoted 9 lines · Ævar Arnfjörð Bjarmason
t/t1092-sparse-checkout-compatibility.sh test_expect_success 'add, commit, checkout' ' · line 305
Quoted 2 lines · Derrick Stolee via GitGitGadget
t/t1092-sparse-checkout-compatibility.sh +2
-0+# NEEDSWORK: This documents current behavior, but is not a desirable +# behavior (untracked files are handled differently than tracked).I wonder if a
test_expect_failuretest would be better for the thing that is the desired behavior, but maybe we don't know what the CLI UI for that would look like yet.
The problem with test_expect_failure is that you don't know which line of the test is the problem. That's probably all fine and good when we completely understand the situation we want to solve but don't have a good approach to fixing it, but here I want to document a change in behavior.
Using test_expect_success allows me to demonstrate "it works this way now" and then "this is how behavior changes".
Quoted 11 lines
t/t1092-sparse-checkout-compatibility.sh test_expect_success 'add, commit, checkout' ' · line 313
Quoted 8 lines
t/t1092-sparse-checkout-compatibility.sh +8
-0+test_expect_success 'add outside sparse cone' ' + init_repos && + + run_on_sparse mkdir folder1 && + run_on_sparse ../edit-contents folder1/a && + run_on_sparse ../edit-contents folder1/newfile && + test_sparse_match test_must_fail git add folder1/a && + test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&Just "grep" is preferred over "
test_i18ngrep" now, theGETTEXT_POISONwent away.
Right. Force of habit.
Thanks,
-Stolee
Re: [PATCH v2 01/14] t3705: test that 'sparse_entry' is unstaged
In reply to Derrick Stolee via GitGitGadgetQuoted 31 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
The tests in
t3705-add-sparse-checkout.shcheck to see how 'git add' behaves with paths outside the sparse-checkout definition. These currently check to see if a given warning is present but not that the index is not updated with the sparse entries. Add a new 'test_sparse_entry_unstaged' helper to be sure 'git add' is behaving correctly.We need to modify
setup_sparse_entryto actually commit thesparse_entryfile so it exists at HEAD but is not already staged in the index.Signed-off-by Derrick Stolee <[email protected]>--- t/t3705-add-sparse-checkout.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+)t/t3705-add-sparse-checkout.sh +1
-0@@ -19,6 +19,7 @@ setup_sparse_entry () { fi && git add sparse_entry && git update-index --skip-worktree sparse_entry && + git commit --allow-empty -m "ensure sparse_entry exists at HEAD" && SPARSE_ENTRY_BLOB=$(git rev-parse :sparse_entry) }
t/t3705-add-sparse-checkout.sh setup_gitignore () { · line 42
Quoted 6 lines
t/t3705-add-sparse-checkout.sh +3
-0@@ -36,6 +37,11 @@ setup_gitignore () { EOF } +test_sparse_entry_unstaged () { + git status --porcelain >actual && + ! grep "^M sparse_entry\$" actual
Is there a reason this is ^M rather than ^D? Granted, both would be bugs so I wouldn't expect either to appear, but the point of the check is looking for likely errors. Wouldn't the more likely error case for a file not present in the working tree be that we stage the deletion of the file?
Quoted 70 lines
t/t3705-add-sparse-checkout.sh +10
-1+} + test_expect_success 'setup' " cat >sparse_error_header <<-EOF && The following pathspecs didn't match any eligible path, but they do match index@@ -55,6 +61,7 @@ test_expect_success 'git add does not remove sparse entries' ' setup_sparse_entry && rm sparse_entry && test_must_fail git add sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '@@ -73,6 +80,7 @@ test_expect_success 'git add . does not remove sparse entries' ' rm sparse_entry && setup_gitignore && test_must_fail git add . 2>stderr && + test_sparse_entry_unstaged && cat sparse_error_header >expect && echo . >>expect &&@@ -88,6 +96,7 @@ do setup_sparse_entry && echo modified >sparse_entry && test_must_fail git add $opt sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '@@ -98,6 +107,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' git ls-files --debug sparse_entry | grep mtime >before && test-tool chmtime -60 sparse_entry && test_must_fail git add --refresh sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && git ls-files --debug sparse_entry | grep mtime >after && test_cmp before after@@ -106,6 +116,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' test_expect_success 'git add --chmod does not update sparse entries' ' setup_sparse_entry && test_must_fail git add --chmod=+x sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged && ! test -x sparse_entry@@ -116,6 +127,7 @@ test_expect_success 'git add --renormalize does not update sparse entries' ' setup_sparse_entry "LINEONE\r\nLINETWO\r\n" && echo "sparse_entry text=auto" >.gitattributes && test_must_fail git add --renormalize sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '@@ -124,6 +136,7 @@ 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_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '@@ -148,6 +161,7 @@ test_expect_success 'do not warn when pathspec matches dense entries' ' test_expect_success 'add obeys advice.updateSparsePath' ' setup_sparse_entry && test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp sparse_entry_error stderr ' --
Quoted 1 line
gitgitgadget
Looks fine otherwise.
Re: [PATCH v2 04/14] dir: select directories correctly
In reply to Ævar Arnfjörð BjarmasonQuoted 13 lines · Ævar Arnfjörð Bjarmason
dir.c int match_pathname(const char *pathname, int pathlen, · line 1317
Quoted 5 lines · Derrick Stolee via GitGitGadget
dir.c +5
-0+/* +* Use 'alloc' as an indicator that the string has not been +* initialized, in case the parent is the root directory. +*/ +if (!path_parent->alloc) {This isn't wrong, but seems to be way too cozy with the internal implementation details of strbuf. For what it's worth I renamed it to "alloc2" and found that this would be only the 3rd bit of code out of strbuf.[ch] that cares about that member.
I can understand not wanting to poke into the internals.
Quoted 5 lines
dir.c int match_pathname(const char *pathname, int pathlen, · line 1319
Quoted 2 lines
dir.c +2
-0+char *slash; +strbuf_addstr (path_parent, pathname);So is "pathname" ever the empty string? If not we could check the length?
We are given 'pathlen' as a parameter, so this should just use strbuf_add() instead.
Quoted 11 lines
Or probably better: ...
dir.c static struct path_pattern *last_matching_pattern_from_list(const char *pathname · line 1362
Quoted 5 lines
dir.c +1
-0@@ -1331,6 +1359,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname {struct path_pattern *res =NULL ;/* undecided */ int i; +struct strbuf path_parent =STRBUF_INIT ;Just malloc +
strbuf_init()this in the above function and have a "struct strbuf *" initialized to NULL here? Then we can use a much more idiomatic "is it NULL?" to check if it's initialized.
That makes sense. Can do.
Thanks,
-Stolee
Re: [PATCH v2 04/14] dir: select directories correctly
In reply to Derrick Stolee via GitGitGadgetQuoted 44 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
When matching a path against a list of patterns, the ones that require a directory match previously did not work when a filename is specified. This was fine when all pattern-matching was done within methods such as
unpack_trees()that check a directory before recursing into the contained files. However, other commands will start matching individual files against pattern lists without that recursive approach.We modify
path_matches_dir_pattern()to take a strbuf 'path_parent' that is used to store the parent directory of 'pathname' between multiple pattern matching tests. This is loaded lazily, only on the first pattern it finds that has thePATTERN_FLAG_MUSTBEDIRflag.If we find that a path has a parent directory, we start by checking to see if that parent directory matches the pattern. If so, then we do not need to query the index for the type (which can be expensive). If we find that the parent does not match, then we still must check the type from the index for the given pathname.
Note that this does not affect cone mode pattern matching, but instead the more general -- and slower -- full pattern set. Thus, this does not affect the sparse index.
Signed-off-by Derrick Stolee <[email protected]>--- dir.c | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-)dir.c +2
-0@@ -1305,10 +1305,38 @@ int match_pathname(const char *pathname, int pathlen,static int path_matches_dir_pattern (const char *pathname,int pathlen, +struct strbuf *path_parent,int *dtype,struct path_pattern *pattern,struct index_state *istate) { +/*
dir.c int match_pathname(const char *pathname, int pathlen, · line 1323
Quoted 10 lines
dir.c +10
-0+* Use 'alloc' as an indicator that the string has not been +* initialized, in case the parent is the root directory. +*/ +if (!path_parent->alloc) { +char *slash; +strbuf_addstr (path_parent, pathname); + slash =find_last_dir_sep (path_parent->buf); + +if (slash) + *slash ='\0' ;
Are you breaking strbuf invariants here? path_parent->len will not be corrected by this string manipulation. Perhaps replace this if-else block with
strbuf_setlen(path_parent, slash ? slash - path_parent->buf : 0)
Quoted 48 lines
dir.c +20
-3+else +strbuf_setlen (path_parent,0 ); + } + +/* +* If the parent directory matches the pattern, then we do not +* need to check for dtype. +*/ +if (path_parent->len && +match_pathname (path_parent->buf, path_parent->len, + pattern->base, + pattern->baselen ? pattern->baselen -1 :0 , + pattern->pattern, pattern->nowildcardlen, + pattern->patternlen, pattern->flags)) +return 1 ; + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen);if (*dtype !=DT_DIR )return 0 ;@@ -1331,6 +1359,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname {struct path_pattern *res =NULL ;/* undecided */ int i; +struct strbuf path_parent =STRBUF_INIT ;if (!pl->nr)return NULL ;/* undefined */ @@ -1340,8 +1369,8 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnameconst char *exclude = pattern->pattern;int prefix = pattern->nowildcardlen; -if ((pattern->flags &PATTERN_FLAG_MUSTBEDIR ) && - !path_matches_dir_pattern (pathname, pathlen, +if (pattern->flags &PATTERN_FLAG_MUSTBEDIR && + !path_matches_dir_pattern (pathname, pathlen, &path_parent, dtype, pattern, istate))continue ;@@ -1367,6 +1396,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnamebreak ; } } +strbuf_release (&path_parent);return res; } --gitgitgadget
Re: [PATCH v2 01/14] t3705: test that 'sparse_entry' is unstaged
In reply to Elijah NewrenQuoted 36 lines · Elijah Newren
Quoted 34 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
The tests in
t3705-add-sparse-checkout.shcheck to see how 'git add' behaves with paths outside the sparse-checkout definition. These currently check to see if a given warning is present but not that the index is not updated with the sparse entries. Add a new 'test_sparse_entry_unstaged' helper to be sure 'git add' is behaving correctly.We need to modify
setup_sparse_entryto actually commit thesparse_entryfile so it exists at HEAD but is not already staged in the index.Signed-off-by Derrick Stolee <[email protected]>--- t/t3705-add-sparse-checkout.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+)t/t3705-add-sparse-checkout.sh +1
-0@@ -19,6 +19,7 @@ setup_sparse_entry () { fi && git add sparse_entry && git update-index --skip-worktree sparse_entry && + git commit --allow-empty -m "ensure sparse_entry exists at HEAD" && SPARSE_ENTRY_BLOB=$(git rev-parse :sparse_entry) }@@ -36,6 +37,11 @@ setup_gitignore () { EOF }
Quoted 9 lines
t/t3705-add-sparse-checkout.sh setup_gitignore () { · line 42
Quoted 3 lines
t/t3705-add-sparse-checkout.sh +3
-0+test_sparse_entry_unstaged () { + git status --porcelain >actual && + ! grep "^M sparse_entry\$" actualIs there a reason this is ^M rather than ^D? Granted, both would be bugs so I wouldn't expect either to appear, but the point of the check is looking for likely errors. Wouldn't the more likely error case for a file not present in the working tree be that we stage the deletion of the file?
You are right that we should be checking for deletions or adds _as well_ as modifications. The test_sparse_entry_unstaged helper is used in a variety of situations that typically would trigger a modification, but at least one instance in this test is a possible deletion.
Quoted 11 lines
t/t3705-add-sparse-checkout.sh test_expect_success 'git add does not remove sparse entries' ' · line 64
Quoted 11 lines
t/t3705-add-sparse-checkout.sh +3
-0+} + test_expect_success 'setup' " cat >sparse_error_header <<-EOF && The following pathspecs didn't match any eligible path, but they do match index@@ -55,6 +61,7 @@ test_expect_success 'git add does not remove sparse entries' ' setup_sparse_entry && rm sparse_entry && test_must_fail git add sparse_entry 2>stderr && + test_sparse_entry_unstaged &&
Here, sparse_entry could be staged as a deletion.
Quoted 8 lines
t/t3705-add-sparse-checkout.sh test_expect_success 'git add . does not remove sparse entries' ' · line 83
Quoted 8 lines
t/t3705-add-sparse-checkout.sh +1
-0test_cmp error_and_hint stderr && test_sparse_entry_unchanged '@@ -73,6 +80,7 @@ test_expect_success 'git add . does not remove sparse entries' ' rm sparse_entry && setup_gitignore && test_must_fail git add . 2>stderr && + test_sparse_entry_unstaged &&
Deletion here.
Quoted 7 lines
t/t3705-add-sparse-checkout.sh do · line 99
Quoted 7 lines
t/t3705-add-sparse-checkout.sh +1
-0cat sparse_error_header >expect && echo . >>expect &&@@ -88,6 +96,7 @@ do setup_sparse_entry && echo modified >sparse_entry && test_must_fail git add $opt sparse_entry 2>stderr && + test_sparse_entry_unstaged &&
But here would be modified.
Quoted 8 lines
t/t3705-add-sparse-checkout.sh test_expect_success 'git add --refresh does not update sparse entries' ' · line 110
Quoted 8 lines
t/t3705-add-sparse-checkout.sh +1
-0test_cmp error_and_hint stderr && test_sparse_entry_unchanged '@@ -98,6 +107,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' git ls-files --debug sparse_entry | grep mtime >before && test-tool chmtime -60 sparse_entry && test_must_fail git add --refresh sparse_entry 2>stderr && + test_sparse_entry_unstaged &&
Same here.
Quoted 8 lines
t/t3705-add-sparse-checkout.sh test_expect_success 'git add --refresh does not update sparse entries' ' · line 119
Quoted 8 lines
t/t3705-add-sparse-checkout.sh +1
-0test_cmp error_and_hint stderr && git ls-files --debug sparse_entry | grep mtime >after && test_cmp before after@@ -106,6 +116,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' test_expect_success 'git add --chmod does not update sparse entries' ' setup_sparse_entry && test_must_fail git add --chmod=+x sparse_entry 2>stderr && + test_sparse_entry_unstaged &&
Here it would be modified with a mode change.
Using the pattern "^[MDARCU][M ] sparse_entry\$" seems to work while also covering these other cases.
Thanks,
-Stolee
Re: [PATCH v2 01/14] t3705: test that 'sparse_entry' is unstaged
In reply to Derrick Stolee via GitGitGadgetQuoted 7 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
The tests in
t3705-add-sparse-checkout.shcheck to see how 'git add' behaves with paths outside the sparse-checkout definition. These currently check to see if a given warning is present but not that the index is not updated with the sparse entries.
Hmm, I probably missed something, but don't we already check that with the test_sparse_entry_unchanged helper? The only test case that we don't call it is 'git add --refresh does not update sparse entries', but we explicitly compare the cached 'mtime' from before and after git add there.
Re: [PATCH v2 01/14] t3705: test that 'sparse_entry' is unstaged
Quoted 10 lines · Matheus Tavares
Quoted 7 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
The tests in
t3705-add-sparse-checkout.shcheck to see how 'git add' behaves with paths outside the sparse-checkout definition. These currently check to see if a given warning is present but not that the index is not updated with the sparse entries.
Quoted 5 lines
Hmm, I probably missed something, but don't we already check that with the
test_sparse_entry_unchangedhelper? The only test case that we don't call it is 'git add --refresh does not update sparse entries', but we explicitly compare the cached 'mtime' from before and aftergit addthere.
test_sparse_entry_unchanged does a bit more by actually requiring that we fully know the mode and OID of the object in the index.
Since some of the tests modify sparse_entry and then update the index using the --sparse option (including a mode change with --chmod=x), it seems more robust to avoid an exact match from ls-files.
Thanks,
-Stolee
Re: [PATCH v2 04/14] dir: select directories correctly
In reply to Elijah NewrenQuoted 12 lines · Elijah Newren
dir.c int match_pathname(const char *pathname, int pathlen, · line 1323
Quoted 11 lines
dir.c +11
-0+/* +* Use 'alloc' as an indicator that the string has not been +* initialized, in case the parent is the root directory. +*/ +if (!path_parent->alloc) { +char *slash; +strbuf_addstr (path_parent, pathname); + slash =find_last_dir_sep (path_parent->buf); + +if (slash) + *slash ='\0' ;
Quoted 5 lines
Are you breaking strbuf invariants here?
path_parent->lenwill not be corrected by this string manipulation. Perhaps replace this if-else block withstrbuf_setlen(path_parent, slash ? slash - path_parent->buf : 0)
Yes, I am. I noticed and fixed this when I was rewriting this patch for Ævar's feedback. Thanks for pointing it out.
Thanks,
-Stolee
Re: [PATCH v2 14/14] advice: update message to suggest '--sparse'
In reply to Ævar Arnfjörð BjarmasonOn 9/12/2021 5:58 PM, Ævar Arnfjörð Bjarmason wrote:
...
Quoted 16 lines
t/t3602-rm-sparse-checkout.sh test_expect_success 'setup' " · line 21
Quoted 12 lines
t/t3602-rm-sparse-checkout.sh +3
-3cat >sparse_error_header <<-EOF && - The following pathspecs didn't match any eligible path, but they do match index - entries outside the current sparse checkout: + The following paths and/or pathspecs matched paths that exist outside of your + sparse-checkout definition, so will not be updated in the index: EOF cat >sparse_hint <<-EOF && - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF...this used to line-wrap at 80 characters, but is now a bit beyond that.
Quoted 1 line
Maybe instead make these two into bullet-points?
Do you mean something like this?
hint: If you intend to update such entries, try one of the following:
hint: * Use the --sparse option.
hint: * Disable or modify the sparsity rules.
Quoted 3 lines
Also the third "Disable" looks a bit jarring at first, it seems like a continuation of the first message, but it's just the standard "disable this message" we tend to print out.
With the bullet points, this is no longer a concern.
Quoted 10 lines
This commentary pre-dates this commit, but just in general:
I think the advice system is best used where there's an initial non-optional message, and then the advice elaborates on what happened, how to fix it. A good example is the "short object ID %s is ambiguous" in
object-name.c.But in this case both messages are rather long. I'd think better would be something like (and I didn't look very deeply at the involved code):
Quoted 6 lines
error("pathspec '%s' matched only outside sparse checkout")I.e. in e.g.
cmd_rm()we loop through the pathspecs, and we error on the first one, so to a first approximation why do we need to for sparse emit ALL the pathspecs we didn't match? if we're going to error out anyway shouldn'w we just error out on the first one?
If we don't list the entire set, then users will need to use trial and error to discover how to get out of a bad state.
Quoted 12 lines
But going on, I'd think this would be better overall (pseudocode):
error("pathspec '%s' matched only outside sparse checkout") if (advice_enabled(ADVICE_UPDATE_SPARSE_PATH)) { char *list_str; list_of_bad_pathspecs = make_that_list(my_pathspec_string_list, &list_str); if (list_of_bad_pathspecs.nr > 1) /* Emit a message that details what's wrong, but also has a * list of all the other pathspecs we'd also die on if the user */ else /* Ditto, but no list *?
I'm not a fan of this "here's an error message for the first thing, but the advice gives all the details" approach.
Quoted 8 lines
Maybe I'm missing something with the sparse implemention, but I'd think going above & beyond and listing all failures is a bit much in either case, i.e. for non-sparse we have:
$ git rm 'file-i-do-not-have' 'directory-i-do-not-have/' fatal: pathspec 'file-i-do-not-have' did not match any files $
Quoted 8 lines
I'd think in general a user who's screwed up and typo'd both isn't going to be much harmed by us noting the first, maybe they'll get another error then.
But usually it's obvious (e.g. you just ran the command in the wrong directory), so if you have a large list of pathspecs getting a firehose of all the things that didn't match can be less helpful due to being overrly verbose.
The difference here is a die() versus an error(). It would probably be better to convert the die() into an error() and report all failures rather than have the sparse-checkout changes start short-circuiting and providing less data.
Thanks,
-Stolee
Re: [PATCH v2 08/14] add: implement the --sparse option
In reply to Derrick Stolee via GitGitGadgetQuoted 42 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
We previously modified 'git add' to refuse updating index entries outside of the sparse-checkout cone. This is justified to prevent users from accidentally getting into a confusing state when Git removes those files from the working tree at some later point.
Unfortunately, this caused some workflows that were previously possible to become impossible, especially around merge conflicts outside of the sparse-checkout cone. These were documented in tests within t1092.
We now re-enable these workflows using a new '--sparse' option to 'git add'. This allows users to signal "Yes, I do know what I'm doing with these files," and accept the consequences of the files leaving the worktree later.
We delay updating the advice message until implementing a similar option in 'git rm' and 'git mv'.
Signed-off-by Derrick Stolee <[email protected]>--- Documentation/git-add.txt | 9 +++++++- builtin/add.c | 12 +++++++---- t/t1092-sparse-checkout-compatibility.sh | 27 ++++++++---------------- t/t3705-add-sparse-checkout.sh | 17 ++++++++++++++- 4 files changed, 41 insertions(+), 24 deletions(-)Documentation/git-add.txt +1
-1@@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git add' [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p] - [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] + [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] [--sparse] [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize] [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]
Documentation/git-add.txt in linkgit:gitglossary[7]. · line 87
Quoted 9 lines
Documentation/git-add.txt +6
-0@@ -79,6 +79,13 @@ in linkgit:gitglossary[7]. --force:: Allow adding otherwise ignored files. +--sparse:: + Allow updating index entries outside of the sparse-checkout cone. + Normally, `git add` refuses to update index entries whose paths do + not fit within the sparse-checkout cone, since those files might + be removed from the working tree without warning. See + linkgit:git-sparse-checkout[1] for more.
for more ...? details? I find the last sentence incomplete.
Following that track for a moment and thinking out loud, I wonder if we want more details somewhere in the sparse-checkout docs about this issue and if so, if we should point to that specific part of that page. The 'reapply' section of the sparse-checkout page kind of touches on the topic of the worktree not exactly matching sparsity patterns (due to other commands), but focuses on unsparsifying files due to conflicts and kind of ignores the re-sparsification that happens in other commands after the working copy matches the index. (Such a documentation improvement could come after your series, as I said, I'm just thinking out loud.)
Quoted 100 lines
Documentation/git-add.txt +1
-0+ -i:: --interactive:: Add modified contents in the working tree interactively tobuiltin/add.c +8
-4@@ -30,6 +30,7 @@ static int patch_interactive, add_interactive, edit_interactive;static int take_worktree_changes;static int add_renormalize;static int pathspec_file_nul; +static int include_sparse;static const char *pathspec_from_file;static int legacy_stash_p;/* support for the scripted `git stash` */ @@ -46,7 +47,7 @@ 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)) +if (!include_sparse &&ce_skip_worktree (ce))continue ;if (pathspec && !ce_path_match (&the_index, ce, pathspec,NULL ))@@ -95,7 +96,7 @@ static void update_callback(struct diff_queue_struct *q,struct diff_filepair *p = q->queue[i];const char *path = p->one->path; -if (!path_in_sparse_checkout (path, &the_index)) +if (!include_sparse && !path_in_sparse_checkout (path, &the_index))continue ;switch (fix_unmerged_status (p, data)) {@@ -383,6 +384,7 @@ static struct option builtin_add_options[] = {OPT_BOOL (0 ,"refresh" , &refresh_only,N_ ("don't add, only refresh the index" )),OPT_BOOL (0 ,"ignore-errors" , &ignore_add_errors,N_ ("just skip files which cannot be added because of errors" )),OPT_BOOL (0 ,"ignore-missing" , &ignore_missing,N_ ("check if - even missing - files are ignored in dry run" )), +OPT_BOOL (0 ,"sparse" , &include_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_STRING (0 ,"chmod" , &chmod_arg,"(+|-)x" ,N_ ("override the executable bit of the listed files" )), OPT_HIDDEN_BOOL(0, "warn-embedded-repo", &warn_on_embedded_repo,@@ -461,7 +463,8 @@ static int add_files(struct dir_struct *dir, int flags) }for (i =0 ; i < dir->nr; i++) { -if (!path_in_sparse_checkout (dir->entries[i]->name, &the_index)) { +if (!include_sparse && + !path_in_sparse_checkout (dir->entries[i]->name, &the_index)) {string_list_append (&matched_sparse_paths, dir->entries[i]->name);continue ;@@ -646,7 +649,8 @@ int cmd_add(int argc, const char **argv, const char *prefix)if (seen[i])continue ; -if (matches_skip_worktree (&pathspec, i, &skip_worktree_seen)) { +if (!include_sparse && +matches_skip_worktree (&pathspec, i, &skip_worktree_seen)) {string_list_append (&only_match_skip_worktree, pathspec.items[i].original);continue ;t/t1092-sparse-checkout-compatibility.sh +5
-8@@ -343,11 +343,7 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -# NEEDSWORK: Now that 'git add folder1/new' fails, the changes being -# attempted here fail for the sparse-checkout and sparse-index repos. -# We must enable a way for adding files outside the sparse-checkout -# done, even if it is by an optional flag. -test_expect_failure 'status/add: outside sparse cone' ' +test_expect_success 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone@@ -375,15 +371,16 @@ test_expect_failure 'status/add: outside sparse cone' ' test_sparse_match test_must_fail git add folder1/new && test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/new && + test_sparse_match git add --sparse folder1/a && + test_sparse_match git add --sparse folder1/new && - # NEEDSWORK: behavior begins to deviate here. - test_all_match git add . && + test_all_match git add --sparse . && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/new && test_all_match git rev-parse HEAD^{tree} && run_on_all ../edit-contents folder1/newer && - test_all_match git add folder1/ && + test_all_match git add --sparse folder1/ && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/newer && test_all_match git rev-parse HEAD^{tree}
t/t1092-sparse-checkout-compatibility.sh test_expect_success 'merge, cherry-pick, and rebase' ' · line 527
Quoted 10 lines
t/t1092-sparse-checkout-compatibility.sh +1
-6@@ -527,12 +524,7 @@ test_expect_success 'merge, cherry-pick, and rebase' ' done ' -# NEEDSWORK: This test is documenting current behavior, but that -# behavior can be confusing to users so there is desire to change it. -# Right now, users might be using this flow to work through conflicts, -# so any solution should present advice to users who try this sequence -# of commands to follow whatever new method we create. -test_expect_failure 'merge with conflict outside cone' ' +test_expect_success 'merge with conflict outside cone' '
Based on the comments on the next hunk, I also wonder if this hunk doesn't belong in the previous commit...
t/t1092-sparse-checkout-compatibility.sh test_expect_failure 'merge with conflict outside cone' ' · line 552
Quoted 13 lines
t/t1092-sparse-checkout-compatibility.sh +0
-3init_repos && test_all_match git checkout -b merge-tip merge-left &&@@ -555,9 +547,6 @@ test_expect_failure 'merge with conflict outside cone' ' # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. run_on_all mv folder2/a folder2/z && - # NEEDSWORK: This mode now fails, because folder2/z is - # outside of the sparse-checkout cone and does not match an - # existing index entry with the SKIP_WORKTREE bit cleared. test_sparse_match test_must_fail git add folder2 && test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder2/z &&
Is this hunk in the wrong commit? You added a --sparse flag to the git add a few lines below in the previous commit, so it seems the NEEDSWORK comment should have been removed at the same time.
Quoted 31 lines
t/t1092-sparse-checkout-compatibility.sh +3
-1@@ -569,7 +558,7 @@ test_expect_failure 'merge with conflict outside cone' ' test_all_match git rev-parse HEAD^{tree} ' -test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' +test_expect_success 'cherry-pick/rebase with conflict outside cone' ' init_repos && for OPERATION in cherry-pick rebase@@ -592,6 +581,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' test_sparse_match test_must_fail git add folder1/a && test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && + test_all_match git add --sparse folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and@@ -603,6 +593,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' test_sparse_match test_must_fail git add folder2 && test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder2/z && + test_all_match git add --sparse folder2 && test_all_match git status --porcelain=v2 && test_all_match git $OPERATION --continue &&t/t3705-add-sparse-checkout.sh +0
-0@@ -167,7 +167,13 @@ test_expect_success 'git add fails outside of sparse-checkout definition' '
t/t3705-add-sparse-checkout.sh test_expect_success 'git add fails outside of sparse-checkout definition' ' · line 176
Quoted 10 lines
t/t3705-add-sparse-checkout.sh +7
-1git update-index --no-skip-worktree sparse_entry && test_must_fail git add sparse_entry && - test_sparse_entry_unstaged + test_sparse_entry_unstaged && + + # Avoid munging CRLFs to avoid an error message + git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && + test_must_be_empty stderr && + test-tool read-cache --table >actual && + grep "^100644 blob.*sparse_entry\$" actual
Does this CRLF anti-munging belong in a separate commit somewhere? I was surprised to see it, and it's not clear to me how it relates to the other changes. Am I missing something?
Quoted 19 lines
t/t3705-add-sparse-checkout.sh +9
-1' test_expect_success 'add obeys advice.updateSparsePath' '@@ -178,4 +184,13 @@ test_expect_success 'add obeys advice.updateSparsePath' ' ' +test_expect_success 'add allows sparse entries with --sparse' ' + git sparse-checkout set a && + echo modified >sparse_entry && + test_must_fail git add sparse_entry && + test_sparse_entry_unchanged && + git add --sparse sparse_entry 2>stderr && + test_must_be_empty stderr +' + test_done --gitgitgadget
Quoted 35 lines · Derrick Stolee via GitGitGadget
This series is based on ds/mergies-with-sparse-index.
As requested, this series looks to update the behavior of git add, git rm, and git mv when they attempt to modify paths outside of the sparse-checkout cone. In particular, this care is expanded to not just cache entries with the
SKIP_WORKTREEbit, but also paths that do not match the sparse-checkout definition.This means that commands that worked before this series can now fail. In particular, if 'git merge' results in a conflict outside of the sparse-checkout cone, then 'git add ' will now fail.
In order to allow users to circumvent these protections, a new '--sparse' option is added that ignores the sparse-checkout patterns and the
SKIP_WORKTREEbit. The message for advice.updateSparsePath is adjusted to assist with discovery of this option.There is a subtle issue with git mv in that it does not check the index until it discovers a directory and then uses the index to find the contained entries. This means that in non-cone-mode patterns, a pattern such as "sub/dir" will not match the path "sub" and this can cause an issue.
In order to allow for checking arbitrary paths against the sparse-checkout patterns, some changes to the underlying pattern matching code is required. It turns out that there are some bugs in the methods as advertised, but these bugs were never discovered because of the way methods like
unpack_trees()will check a directory for a pattern match before checking its contained paths. Our new "check patterns on-demand" approach pokes holes in that approach, specifically with patterns that match entire directories.Updates in v2
=============
Quoted 8 lines
* I got no complaints about these restrictions, so this is now a full series, not RFC.
* Thanks to Matheus, several holes are filled with extra testing and bugfixes.
* New patches add --chmod and --renormalize improvements. These are added after the --sparse option to make them be one change each.
Sorry for taking so long, but I finally read through the series. Only had a few small comments here and there; the high level direction looks good.
Re: [PATCH v2 08/14] add: implement the --sparse option
In reply to Elijah NewrenQuoted 1 line · Elijah Newren
On Sun, Sep 12, 2021 at 6:23 AM Derrick Stolee via GitGitGadget
...
Quoted 8 lines
Documentation/git-add.txt in linkgit:gitglossary[7]. · line 87
Quoted 6 lines
+--sparse::
Documentation/git-add.txt +5
-0+ Allow updating index entries outside of the sparse-checkout cone. + Normally, `git add` refuses to update index entries whose paths do + not fit within the sparse-checkout cone, since those files might + be removed from the working tree without warning. See + linkgit:git-sparse-checkout[1] for more.for more ...? details? I find the last sentence incomplete.
I'll add "details"
Quoted 10 lines
Following that track for a moment and thinking out loud, I wonder if we want more details somewhere in the sparse-checkout docs about this issue and if so, if we should point to that specific part of that page. The 'reapply' section of the sparse-checkout page kind of touches on the topic of the worktree not exactly matching sparsity patterns (due to other commands), but focuses on unsparsifying files due to conflicts and kind of ignores the re-sparsification that happens in other commands after the working copy matches the index. (Such a documentation improvement could come after your series, as I said, I'm just thinking out loud.)
This is a good idea to include in a new "Troubleshooting" section.
Quoted 10 lines
t/t1092-sparse-checkout-compatibility.sh test_expect_success 'merge, cherry-pick, and rebase' ' · line 527
Quoted 7 lines
t/t1092-sparse-checkout-compatibility.sh +1
-6-# NEEDSWORK: This test is documenting current behavior, but that -# behavior can be confusing to users so there is desire to change it. -# Right now, users might be using this flow to work through conflicts, -# so any solution should present advice to users who try this sequence -# of commands to follow whatever new method we create. -test_expect_failure 'merge with conflict outside cone' ' +test_expect_success 'merge with conflict outside cone' 'Based on the comments on the next hunk, I also wonder if this hunk doesn't belong in the previous commit...
You are absolutely right, I squashed the wrong commits. Thanks.
-Stolee
v3 cover Sparse-checkout: modify 'git add', 'git rm', and 'git add' behavior
In reply to Derrick Stolee via GitGitGadgetThis series is based on ds/mergies-with-sparse-index.
As requested, this series looks to update the behavior of git add, git rm, and git mv when they attempt to modify paths outside of the sparse-checkout cone. In particular, this care is expanded to not just cache entries with the SKIP_WORKTREE bit, but also paths that do not match the sparse-checkout definition.
This means that commands that worked before this series can now fail. In particular, if 'git merge' results in a conflict outside of the sparse-checkout cone, then 'git add ' will now fail.
In order to allow users to circumvent these protections, a new '--sparse' option is added that ignores the sparse-checkout patterns and the SKIP_WORKTREE bit. The message for advice.updateSparsePath is adjusted to assist with discovery of this option.
There is a subtle issue with git mv in that it does not check the index until it discovers a directory and then uses the index to find the contained entries. This means that in non-cone-mode patterns, a pattern such as "sub/dir" will not match the path "sub" and this can cause an issue.
In order to allow for checking arbitrary paths against the sparse-checkout patterns, some changes to the underlying pattern matching code is required. It turns out that there are some bugs in the methods as advertised, but these bugs were never discovered because of the way methods like unpack_trees() will check a directory for a pattern match before checking its contained paths. Our new "check patterns on-demand" approach pokes holes in that approach, specifically with patterns that match entire directories.
Updates in v3
=============
* Fixed an incorrectly-squashed commit. Spread out some changes in a better way. For example, I don't add --sparse to tests before introducing the option.
* Use a NULL struct strbuf pointer to indicate an uninitialized value instead of relying on an internal member.
* Use grep over test_i18ngrep.
* Fixed line wrapping for error messages.
* Use strbuf_setlen() over modifying the len member manually.
Updates in v2
=============
* I got no complaints about these restrictions, so this is now a full series, not RFC.
* Thanks to Matheus, several holes are filled with extra testing and bugfixes.
* New patches add --chmod and --renormalize improvements. These are added after the --sparse option to make them be one change each.
Thanks, -Stolee
Derrick Stolee (14):
t3705: test that 'sparse_entry' is unstaged
t1092: behavior for adding sparse files
dir: extract directory-matching logic
dir: select directories correctly
dir: fix pattern matching on dirs
add: fail when adding an untracked sparse file
add: skip tracked paths outside sparse-checkout cone
add: implement the --sparse option
add: update --chmod to skip sparse paths
add: update --renormalize to skip sparse paths
rm: add --sparse option
rm: skip sparse paths with missing SKIP_WORKTREE
mv: refuse to move sparse paths
advice: update message to suggest '--sparse'
Documentation/git-add.txt | 9 +- Documentation/git-rm.txt | 6 + advice.c | 11 +- builtin/add.c | 32 +++- builtin/mv.c | 52 +++++-- builtin/rm.c | 10 +- dir.c | 56 ++++++- pathspec.c | 5 +- t/t1091-sparse-checkout-builtin.sh | 4 +- t/t1092-sparse-checkout-compatibility.sh | 75 +++++++-- t/t3602-rm-sparse-checkout.sh | 40 ++++- t/t3705-add-sparse-checkout.sh | 68 +++++++- t/t7002-mv-sparse-checkout.sh | 189 +++++++++++++++++++++++ 13 files changed, 505 insertions(+), 52 deletions(-) create mode 100755 t/t7002-mv-sparse-checkout.sh
base-commit: 516680ba7704c473bb21628aa19cabbd787df4db
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1018%2Fderrickstolee%2Fsparse-index%2Fadd-rm-mv-behavior-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1018/derrickstolee/sparse-index/add-rm-mv-behavior-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/1018
Range-diff vs v2:
1: 8aefce6254c ! 1: ea940f10a7c t3705: test that 'sparse_entry' is unstaged
@@ Commit message
correctly.
We need to modify setup_sparse_entry to actually commit the sparse_entry
- file so it exists at HEAD but is not already staged in the index.
+ file so it exists at HEAD and as an entry in the index, but its exact
+ contents are not staged in the index.
@@ t/t3705-add-sparse-checkout.sh: setup_gitignore () {
+test_sparse_entry_unstaged () {
+ git status --porcelain >actual &&
-+ ! grep "^M sparse_entry\$" actual
++ ! grep "^[MDARCU][M ] sparse_entry\$" actual
+}
+
test_expect_success 'setup' "
2: 61c23dc59a6 ! 2: c7dedb41291 t1092: behavior for adding sparse files
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'add, commit, chec
+ run_on_sparse ../edit-contents folder1/a &&
+ run_on_sparse ../edit-contents folder1/newfile &&
+ test_sparse_match test_must_fail git add folder1/a &&
-+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/a &&
+ test_sparse_match git add folder1/newfile
+'
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'status/add: outsi
# Adding the path outside of the sparse-checkout cone should fail.
test_sparse_match test_must_fail git add folder1/a &&
-+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/a &&
test_sparse_match test_must_fail git add --refresh folder1/a &&
-+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/a &&
# NEEDSWORK: Adding a newly-tracked file outside the cone succeeds
3: 747b52e7bde = 3: b1f6468f9cd dir: extract directory-matching logic
4: 723de4e4258 ! 4: 0252c7ee15c dir: select directories correctly
@@ Commit message
contained files. However, other commands will start matching individual
files against pattern lists without that recursive approach.
- We modify path_matches_dir_pattern() to take a strbuf 'path_parent' that
- is used to store the parent directory of 'pathname' between multiple
- pattern matching tests. This is loaded lazily, only on the first pattern
- it finds that has the PATTERN_FLAG_MUSTBEDIR flag.
+ We modify path_matches_dir_pattern() to take a strbuf pointer
+ 'path_parent' that is used to store the parent directory of 'pathname'
+ between multiple pattern matching tests. This is loaded lazily, only on
+ the first pattern it finds that has the PATTERN_FLAG_MUSTBEDIR flag.
If we find that a path has a parent directory, we start by checking to
see if that parent directory matches the pattern. If so, then we do not
@@ Commit message
the more general -- and slower -- full pattern set. Thus, this does not
affect the sparse index.
+ Helped-by: Ævar Arnfjörð Bjarmason <[email protected]>
Signed-off-by: Derrick Stolee <[email protected]>
## dir.c ##
@@ dir.c: int match_pathname(const char *pathname, int pathlen,
static int path_matches_dir_pattern(const char *pathname,
int pathlen,
-+ struct strbuf *path_parent,
++ struct strbuf **path_parent,
int *dtype,
struct path_pattern *pattern,
struct index_state *istate)
{
-+ /*
-+ * Use 'alloc' as an indicator that the string has not been
-+ * initialized, in case the parent is the root directory.
-+ */
-+ if (!path_parent->alloc) {
++ if (!*path_parent) {
+ char *slash;
-+ strbuf_addstr(path_parent, pathname);
-+ slash = find_last_dir_sep(path_parent->buf);
++ CALLOC_ARRAY(*path_parent, 1);
++ strbuf_add(*path_parent, pathname, pathlen);
++ slash = find_last_dir_sep((*path_parent)->buf);
+
+ if (slash)
-+ *slash = '\0';
++ strbuf_setlen(*path_parent, slash - (*path_parent)->buf);
+ else
-+ strbuf_setlen(path_parent, 0);
++ strbuf_setlen(*path_parent, 0);
+ }
+
+ /*
+ * If the parent directory matches the pattern, then we do not
+ * need to check for dtype.
+ */
-+ if (path_parent->len &&
-+ match_pathname(path_parent->buf, path_parent->len,
++ if ((*path_parent)->len &&
++ match_pathname((*path_parent)->buf, (*path_parent)->len,
+ pattern->base,
+ pattern->baselen ? pattern->baselen - 1 : 0,
+ pattern->pattern, pattern->nowildcardlen,
@@ dir.c: static struct path_pattern *last_matching_pattern_from_list(const char *p
{
struct path_pattern *res = NULL; /* undecided */
int i;
-+ struct strbuf path_parent = STRBUF_INIT;
++ struct strbuf *path_parent = NULL;
if (!pl->nr)
return NULL; /* undefined */
@@ dir.c: static struct path_pattern *last_matching_pattern_from_list(const char *p
break;
}
}
-+ strbuf_release(&path_parent);
++
++ if (path_parent) {
++ strbuf_release(path_parent);
++ free(path_parent);
++ }
++
return res;
}5: ed2472d33f1 = 5: c6d17df5e5d dir: fix pattern matching on dirs 6: bac6cedfc8d ! 6: 3dd1d6c228c add: fail when adding an untracked sparse file
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'add, commit, chec
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'add outside sparse cone' '
test_sparse_match test_must_fail git add folder1/a &&
- test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/a &&
- test_sparse_match git add folder1/newfile
+ test_sparse_match test_must_fail git add folder1/newfile &&
-+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/newfile
'
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'commit including
# folder1 is at HEAD, but outside the sparse cone
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'status/add: outside sparse cone' '
test_sparse_match test_must_fail git add --refresh folder1/a &&
- test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/a &&
+ test_sparse_match test_must_fail git add folder1/new &&
-+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/new &&
- # NEEDSWORK: Adding a newly-tracked file outside the cone succeeds
7: d4706e17cae ! 7: 15039e031e5 add: skip tracked paths outside sparse-checkout cone
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with confli
- # warn that this is a problematic add.
- test_all_match git add folder1/a &&
+ test_sparse_match test_must_fail git add folder1/a &&
-+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/a &&
-+ test_all_match git add --sparse folder1/a &&
test_all_match git status --porcelain=v2 &&
# 3. Rename the file to another sparse filename and
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with confli
# existing index entry with the SKIP_WORKTREE bit cleared.
- test_all_match git add folder2 &&
+ test_sparse_match test_must_fail git add folder2 &&
-+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder2/z &&
-+ test_all_match git add --sparse folder2 &&
test_all_match git status --porcelain=v2 &&
test_all_match git merge --continue &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'cherry-pick/rebas
# warn that this is a problematic add.
- test_all_match git add folder1/a &&
+ test_sparse_match test_must_fail git add folder1/a &&
-+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/a &&
test_all_match git status --porcelain=v2 &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'cherry-pick/rebas
run_on_all mv folder2/a folder2/z &&
- test_all_match git add folder2 &&
+ test_sparse_match test_must_fail git add folder2 &&
-+ test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
++ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder2/z &&
test_all_match git status --porcelain=v2 && 8: 944ae2cffa8 ! 8: 6014ac8ab9e add: implement the --sparse option
@@ Documentation/git-add.txt: in linkgit:gitglossary[7].
+ Normally, git add refuses to update index entries whose paths do
+ not fit within the sparse-checkout cone, since those files might
+ be removed from the working tree without warning. See
-+ linkgit:git-sparse-checkout[1] for more.
++ linkgit:git-sparse-checkout[1] for more details.
+
-i::
--interactive::
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'commit including
# folder1 is at HEAD, but outside the sparse cone
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'status/add: outside sparse cone' '
test_sparse_match test_must_fail git add folder1/new &&
- test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/new &&
+ test_sparse_match git add --sparse folder1/a &&
+ test_sparse_match git add --sparse folder1/new &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'merge, cherry-pic
test_all_match git checkout -b merge-tip merge-left &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with conflict outside cone' '
+ test_sparse_match test_must_fail git add folder1/a &&
+ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ test_sparse_unstaged folder1/a &&
++ test_all_match git add --sparse folder1/a &&
+ test_all_match git status --porcelain=v2 &&
+
# 3. Rename the file to another sparse filename and
# accept conflict markers as resolved content.
run_on_all mv folder2/a folder2/z &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with confli
- # outside of the sparse-checkout cone and does not match an
- # existing index entry with the SKIP_WORKTREE bit cleared.
test_sparse_match test_must_fail git add folder2 &&
- test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder2/z &&
++ test_all_match git add --sparse folder2 &&
+ test_all_match git status --porcelain=v2 &&
+
+ test_all_match git merge --continue &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with conflict outside cone' '
test_all_match git rev-parse HEAD^{tree}
'
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'merge with confli
for OPERATION in cherry-pick rebase
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'cherry-pick/rebase with conflict outside cone' '
test_sparse_match test_must_fail git add folder1/a &&
- test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder1/a &&
+ test_all_match git add --sparse folder1/a &&
test_all_match git status --porcelain=v2 &&
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'cherry-pick/rebas
# 3. Rename the file to another sparse filename and
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_failure 'cherry-pick/rebase with conflict outside cone' '
test_sparse_match test_must_fail git add folder2 &&
- test_i18ngrep "Disable or modify the sparsity rules" sparse-checkout-err &&
+ grep "Disable or modify the sparsity rules" sparse-checkout-err &&
test_sparse_unstaged folder2/z &&
+ test_all_match git add --sparse folder2 &&
test_all_match git status --porcelain=v2 &&
9: 7ace030c709 ! 9: 2bd3448be5f add: update --chmod to skip sparse paths
@@ builtin/add.c: static int chmod_pathspec(struct pathspec *pathspec, char flip, i
if (pathspec && !ce_path_match(&the_index, ce, pathspec, NULL))
## t/t3705-add-sparse-checkout.sh ##
-@@ t/t3705-add-sparse-checkout.sh: setup_gitignore () {
-
- test_sparse_entry_unstaged () {
- git status --porcelain >actual &&
-- ! grep "^M sparse_entry\$" actual
-+ ! grep "^M[ M] sparse_entry\$" actual
- }
-
- test_expect_success 'setup' "
@@ t/t3705-add-sparse-checkout.sh: test_expect_success 'git add fails outside of sparse-checkout definition' '
test_must_fail git add sparse_entry &&
test_sparse_entry_unstaged &&
10: f34be166097 = 10: 131beda1bc3 add: update --renormalize to skip sparse paths
11: fb3f86efa9b = 11: 837a9314893 rm: add --sparse option
12: de0c9d09ef7 = 12: cc25ce17162 rm: skip sparse paths with missing SKIP_WORKTREE
13: e17a529332d = 13: 63a9cd80ade mv: refuse to move sparse paths
14: f2abc387822 ! 14: 79a3518dc15 advice: update message to suggest '--sparse' @@ advice.c: void advise_on_updating_sparse_paths(struct string_list *pathspec_list
- fprintf(stderr, _("The following pathspecs didn't match any"
- " eligible path, but they do match index\n"
- "entries outside the current sparse checkout:\n"));
-+ fprintf(stderr, _("The following paths and/or pathspecs matched "
-+ "paths that exist outside of your\n"
-+ "sparse-checkout definition, so will not be "
++ fprintf(stderr, _("The following paths and/or pathspecs matched paths that exist\n"
++ "outside of your sparse-checkout definition, so will not be\n"
+ "updated in the index:\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"
-+ _("Disable or modify the sparsity rules or"
-+ " use the --sparse option if you intend"
- " to update such entries."));
+- " to update such entries."));
++ _("If you intend to update such entries, try one of the following:\n"
++ "* Use the --sparse option.\n"
++ "* Disable or modify the sparsity rules."));
}
+ void detach_advice(const char *new_name)
## t/t3602-rm-sparse-checkout.sh ##
@@ t/t3602-rm-sparse-checkout.sh: test_expect_success 'setup' "
@@ t/t3602-rm-sparse-checkout.sh: 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:
-+ The following paths and/or pathspecs matched paths that exist outside of your
-+ sparse-checkout definition, so will not be updated in the index:
++ The following paths and/or pathspecs matched paths that exist
++ outside of your sparse-checkout definition, so will not be
++ updated in the index:
EOF
cat >sparse_hint <<-EOF &&
- hint: Disable or modify the sparsity rules if you intend to update such entries.
-+ hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries.
++ hint: If you intend to update such entries, try one of the following:
++ hint: * Use the --sparse option.
++ hint: * Disable or modify the sparsity rules.
hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
@@ t/t3705-add-sparse-checkout.sh: test_sparse_entry_unstaged () {
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:
-+ The following paths and/or pathspecs matched paths that exist outside of your
-+ sparse-checkout definition, so will not be updated in the index:
++ The following paths and/or pathspecs matched paths that exist
++ outside of your sparse-checkout definition, so will not be
++ updated in the index:
EOF
cat >sparse_hint <<-EOF &&
- hint: Disable or modify the sparsity rules if you intend to update such entries.
-+ hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries.
++ hint: If you intend to update such entries, try one of the following:
++ hint: * Use the --sparse option.
++ hint: * Disable or modify the sparsity rules.
hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
@@ t/t7002-mv-sparse-checkout.sh: 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:
-+ The following paths and/or pathspecs matched paths that exist outside of your
-+ sparse-checkout definition, so will not be updated in the index:
++ The following paths and/or pathspecs matched paths that exist
++ outside of your sparse-checkout definition, so will not be
++ updated in the index:
EOF
cat >sparse_hint <<-EOF
- hint: Disable or modify the sparsity rules if you intend to update such entries.
-+ hint: Disable or modify the sparsity rules or use the --sparse option if you intend to update such entries.
++ hint: If you intend to update such entries, try one of the following:
++ hint: * Use the --sparse option.
++ hint: * Disable or modify the sparsity rules.
hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
"
Signature · 1 line
gitgitgadget
PATCH v3 2/14 t1092: behavior for adding sparse files
From: Derrick Stolee <[email protected]>
Add some tests to demonstrate the current behavior around adding files outside of the sparse-checkout cone. Currently, untracked files are handled differently from tracked files. A future change will make these cases be handled the same way.
Further expand checking that a failed 'git add' does not stage changes to the index.
--- t/t1092-sparse-checkout-compatibility.sh | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+)
t/t1092-sparse-checkout-compatibility.sh +28 -0
@@ -187,6 +187,16 @@ test_sparse_match () { test_cmp sparse-checkout-err sparse-index-err } +test_sparse_unstaged () { + file=$1 && + for repo in sparse-checkout sparse-index + do + git -C $repo status --porcelain >$repo-out && + ! grep "^A $file\$" $repo-out && + ! grep "^M $file\$" $repo-out || return 1 + done +} + test_expect_success 'sparse-index contents' ' init_repos &&
@@ -291,6 +301,20 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' +# NEEDSWORK: This documents current behavior, but is not a desirable +# behavior (untracked files are handled differently than tracked). +test_expect_success 'add outside sparse cone' ' + init_repos && + + run_on_sparse mkdir folder1 && + run_on_sparse ../edit-contents folder1/a && + run_on_sparse ../edit-contents folder1/newfile && + test_sparse_match test_must_fail git add folder1/a && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && + test_sparse_match git add folder1/newfile +' + test_expect_success 'commit including unstaged changes' ' init_repos &&
@@ -339,7 +363,11 @@ test_expect_success 'status/add: outside sparse cone' ' # Adding the path outside of the sparse-checkout cone should fail. test_sparse_match test_must_fail git add folder1/a && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && test_sparse_match test_must_fail git add --refresh folder1/a && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && # NEEDSWORK: Adding a newly-tracked file outside the cone succeeds test_sparse_match git add folder1/new &&
Signature · 1 line
gitgitgadget
PATCH v3 1/14 t3705: test that 'sparse_entry' is unstaged
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The tests in t3705-add-sparse-checkout.sh check to see how 'git add' behaves with paths outside the sparse-checkout definition. These currently check to see if a given warning is present but not that the index is not updated with the sparse entries. Add a new 'test_sparse_entry_unstaged' helper to be sure 'git add' is behaving correctly.
We need to modify setup_sparse_entry to actually commit the sparse_entry file so it exists at HEAD and as an entry in the index, but its exact contents are not staged in the index.
--- t/t3705-add-sparse-checkout.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+)
t/t3705-add-sparse-checkout.sh +14 -0
@@ -19,6 +19,7 @@ setup_sparse_entry () { fi && git add sparse_entry && git update-index --skip-worktree sparse_entry && + git commit --allow-empty -m "ensure sparse_entry exists at HEAD" && SPARSE_ENTRY_BLOB=$(git rev-parse :sparse_entry) }
@@ -36,6 +37,11 @@ setup_gitignore () { EOF } +test_sparse_entry_unstaged () { + git status --porcelain >actual && + ! grep "^[MDARCU][M ] sparse_entry\$" actual +} + test_expect_success 'setup' " cat >sparse_error_header <<-EOF && The following pathspecs didn't match any eligible path, but they do match index
@@ -55,6 +61,7 @@ test_expect_success 'git add does not remove sparse entries' ' setup_sparse_entry && rm sparse_entry && test_must_fail git add sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -73,6 +80,7 @@ test_expect_success 'git add . does not remove sparse entries' ' rm sparse_entry && setup_gitignore && test_must_fail git add . 2>stderr && + test_sparse_entry_unstaged && cat sparse_error_header >expect && echo . >>expect &&
@@ -88,6 +96,7 @@ do setup_sparse_entry && echo modified >sparse_entry && test_must_fail git add $opt sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -98,6 +107,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' git ls-files --debug sparse_entry | grep mtime >before && test-tool chmtime -60 sparse_entry && test_must_fail git add --refresh sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && git ls-files --debug sparse_entry | grep mtime >after && test_cmp before after
@@ -106,6 +116,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' test_expect_success 'git add --chmod does not update sparse entries' ' setup_sparse_entry && test_must_fail git add --chmod=+x sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged && ! test -x sparse_entry
@@ -116,6 +127,7 @@ test_expect_success 'git add --renormalize does not update sparse entries' ' setup_sparse_entry "LINEONE\r\nLINETWO\r\n" && echo "sparse_entry text=auto" >.gitattributes && test_must_fail git add --renormalize sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -124,6 +136,7 @@ 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_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -148,6 +161,7 @@ test_expect_success 'do not warn when pathspec matches dense entries' ' test_expect_success 'add obeys advice.updateSparsePath' ' setup_sparse_entry && test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp sparse_entry_error stderr '
Signature · 1 line
gitgitgadget
PATCH v3 3/14 dir: extract directory-matching logic
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The last_matching_pattern_from_list() logic performs some checks on the filetype of a path within the index when the PATTERN_FLAG_MUSTBEDIR flag is set. This works great when setting SKIP_WORKTREE bits within unpack_trees(), but doesn't work well when passing an arbitrary path such as a file within a matching directory.
This change only rearranges the logic but does not change its functionality.
We will expand the path_matches_dir_pattern() method in a following change.
--- dir.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-)
dir.c +17 -5
@@ -1303,6 +1303,19 @@ int match_pathname(const char *pathname, int pathlen, WM_PATHNAME) == 0; } +static int path_matches_dir_pattern (const char *pathname, +int pathlen, +int *dtype, +struct path_pattern *pattern, +struct index_state *istate) +{ + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); +if (*dtype !=DT_DIR ) +return 0 ; + +return 1 ; +} +/* * Scan the given exclude list in reverse to see whether pathname * should be ignored. The first match (i.e. the last on the list), if
@@ -1327,11 +1340,10 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnameconst char *exclude = pattern->pattern;int prefix = pattern->nowildcardlen; -if (pattern->flags &PATTERN_FLAG_MUSTBEDIR ) { - *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); -if (*dtype !=DT_DIR ) -continue ; - } +if ((pattern->flags &PATTERN_FLAG_MUSTBEDIR ) && + !path_matches_dir_pattern (pathname, pathlen, + dtype, pattern, istate)) +continue ;if (pattern->flags &PATTERN_FLAG_NODIR ) { if (match_basename(basename,
Signature · 1 line
gitgitgadget
PATCH v3 4/14 dir: select directories correctly
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When matching a path against a list of patterns, the ones that require a directory match previously did not work when a filename is specified. This was fine when all pattern-matching was done within methods such as unpack_trees() that check a directory before recursing into the contained files. However, other commands will start matching individual files against pattern lists without that recursive approach.
We modify path_matches_dir_pattern() to take a strbuf pointer 'path_parent' that is used to store the parent directory of 'pathname' between multiple pattern matching tests. This is loaded lazily, only on the first pattern it finds that has the PATTERN_FLAG_MUSTBEDIR flag.
If we find that a path has a parent directory, we start by checking to see if that parent directory matches the pattern. If so, then we do not need to query the index for the type (which can be expensive). If we find that the parent does not match, then we still must check the type from the index for the given pathname.
Note that this does not affect cone mode pattern matching, but instead the more general -- and slower -- full pattern set. Thus, this does not affect the sparse index.
--- dir.c | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-)
dir.c +34 -2
@@ -1305,10 +1305,35 @@ int match_pathname(const char *pathname, int pathlen,static int path_matches_dir_pattern (const char *pathname,int pathlen, +struct strbuf **path_parent,int *dtype,struct path_pattern *pattern,struct index_state *istate) { +if (!*path_parent) { +char *slash; +CALLOC_ARRAY (*path_parent,1 ); +strbuf_add (*path_parent, pathname, pathlen); + slash =find_last_dir_sep ((*path_parent)->buf); + +if (slash) +strbuf_setlen (*path_parent, slash - (*path_parent)->buf); +else +strbuf_setlen (*path_parent,0 ); + } + +/* +* If the parent directory matches the pattern, then we do not +* need to check for dtype. +*/ +if ((*path_parent)->len && +match_pathname ((*path_parent)->buf, (*path_parent)->len, + pattern->base, + pattern->baselen ? pattern->baselen -1 :0 , + pattern->pattern, pattern->nowildcardlen, + pattern->patternlen, pattern->flags)) +return 1 ; + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen);if (*dtype !=DT_DIR )return 0 ;
@@ -1331,6 +1356,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname {struct path_pattern *res =NULL ;/* undecided */ int i; +struct strbuf *path_parent =NULL ;if (!pl->nr)return NULL ;/* undefined */
@@ -1340,8 +1366,8 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnameconst char *exclude = pattern->pattern;int prefix = pattern->nowildcardlen; -if ((pattern->flags &PATTERN_FLAG_MUSTBEDIR ) && - !path_matches_dir_pattern (pathname, pathlen, +if (pattern->flags &PATTERN_FLAG_MUSTBEDIR && + !path_matches_dir_pattern (pathname, pathlen, &path_parent, dtype, pattern, istate))continue ;
@@ -1367,6 +1393,12 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnamebreak ; } } + +if (path_parent) { +strbuf_release (path_parent); +free (path_parent); + } +return res; }
Signature · 1 line
gitgitgadget
PATCH v3 5/14 dir: fix pattern matching on dirs
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Within match_pathname(), one successful matching category happens when the pattern is equal to its non-wildcard prefix. At this point, we have checked that the input 'pathname' matches the pattern up to the prefix length, and then we subtraced that length from both 'patternlen' and 'namelen'.
In the case of a directory match, this prefix match should be sufficient. However, the success condition only cared about _exact_ equality here. Instead, we should allow any path that agrees on this prefix in the case of PATTERN_FLAG_MUSTBEDIR.
This case was not tested before because of the way unpack_trees() would match a parent directory before visiting the contained paths. This approach is changing, so we must change this comparison.
--- dir.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
dir.c +1 -1
@@ -1294,7 +1294,7 @@ int match_pathname(const char *pathname, int pathlen,* then our prefix match is all we need; we * do not need to call fnmatch at all. */ -if (!patternlen && !namelen) +if (!patternlen && (!namelen || (flags &PATTERN_FLAG_MUSTBEDIR )))return 1 ; }
Signature · 1 line
gitgitgadget
PATCH v3 6/14 add: fail when adding an untracked sparse file
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The add_files() method in builtin/add.c takes a set of untracked files that are being added by the input pathspec and inserts them into the index. If these files are outside of the sparse-checkout cone, then they gain the SKIP_WORKTREE bit at some point. However, this was not checked before inserting into the index, so these files are added even though we want to avoid modifying the index outside of the sparse-checkout cone.
Add a check within add_files() for these files and write the advice about files outside of the sparse-checkout cone.
This behavior change modifies some existing tests within t1092. These tests intended to document how a user could interact with the existing behavior in place. Many of these tests need to be marked as expecting failure. A future change will allow these tests to pass by adding a flag to 'git add' that allows users to modify index entries outside of the sparse-checkout cone.
The 'submodule handling' test is intended to document what happens to directories that contain a submodule when the sparse index is enabled. It is not trying to say that users should be able to add submodules outside of the sparse-checkout cone, so that test can be modified to avoid that operation.
--- builtin/add.c | 14 +++++++++ t/t1092-sparse-checkout-compatibility.sh | 37 ++++++++++++++++++------ 2 files changed, 42 insertions(+), 9 deletions(-)
builtin/add.c +14 -0
@@ -443,6 +443,7 @@ static void check_embedded_repo(const char *path)static int add_files (struct dir_struct *dir,int flags) {int i, exit_status =0 ; +struct string_list matched_sparse_paths =STRING_LIST_INIT_NODUP ;if (dir->ignored_nr) {fprintf (stderr,_ (ignore_error));
@@ -456,6 +457,11 @@ static int add_files(struct dir_struct *dir, int flags) }for (i =0 ; i < dir->nr; i++) { +if (!path_in_sparse_checkout (dir->entries[i]->name, &the_index)) { +string_list_append (&matched_sparse_paths, + dir->entries[i]->name); +continue ; + }if (add_file_to_index (&the_index, dir->entries[i]->name, flags)) {if (!ignore_add_errors)die (_ ("adding files failed" ));
@@ -464,6 +470,14 @@ static int add_files(struct dir_struct *dir, int flags)check_embedded_repo (dir->entries[i]->name); } } + +if (matched_sparse_paths.nr) { +advise_on_updating_sparse_paths (&matched_sparse_paths); + exit_status =1 ; + } + +string_list_clear (&matched_sparse_paths,0 ); +return exit_status; }
t/t1092-sparse-checkout-compatibility.sh +28 -9
@@ -301,8 +301,6 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' -# NEEDSWORK: This documents current behavior, but is not a desirable -# behavior (untracked files are handled differently than tracked). test_expect_success 'add outside sparse cone' ' init_repos &&
@@ -312,7 +310,9 @@ test_expect_success 'add outside sparse cone' ' test_sparse_match test_must_fail git add folder1/a && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && - test_sparse_match git add folder1/newfile + test_sparse_match test_must_fail git add folder1/newfile && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/newfile ' test_expect_success 'commit including unstaged changes' '
@@ -343,7 +343,11 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -test_expect_success 'status/add: outside sparse cone' ' +# NEEDSWORK: Now that 'git add folder1/new' fails, the changes being +# attempted here fail for the sparse-checkout and sparse-index repos. +# We must enable a way for adding files outside the sparse-checkout +# done, even if it is by an optional flag. +test_expect_failure 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -368,10 +372,11 @@ test_expect_success 'status/add: outside sparse cone' ' test_sparse_match test_must_fail git add --refresh folder1/a && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && + test_sparse_match test_must_fail git add folder1/new && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/new && - # NEEDSWORK: Adding a newly-tracked file outside the cone succeeds - test_sparse_match git add folder1/new && - + # NEEDSWORK: behavior begins to deviate here. test_all_match git add . && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/new &&
@@ -527,7 +532,7 @@ test_expect_success 'merge, cherry-pick, and rebase' ' # Right now, users might be using this flow to work through conflicts, # so any solution should present advice to users who try this sequence # of commands to follow whatever new method we create. -test_expect_success 'merge with conflict outside cone' ' +test_expect_failure 'merge with conflict outside cone' ' init_repos && test_all_match git checkout -b merge-tip merge-left &&
@@ -541,12 +546,18 @@ test_expect_success 'merge with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers + # NEEDSWORK: Even though the merge conflict removed the + # SKIP_WORKTREE bit from the index entry for folder1/a, we should + # warn that this is a problematic add. test_all_match git add folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. run_on_all mv folder2/a folder2/z && + # NEEDSWORK: This mode now fails, because folder2/z is + # outside of the sparse-checkout cone and does not match an + # existing index entry with the SKIP_WORKTREE bit cleared. test_all_match git add folder2 && test_all_match git status --porcelain=v2 &&
@@ -555,7 +566,7 @@ test_expect_success 'merge with conflict outside cone' ' test_all_match git rev-parse HEAD^{tree} ' -test_expect_success 'cherry-pick/rebase with conflict outside cone' ' +test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' init_repos && for OPERATION in cherry-pick rebase
@@ -572,11 +583,17 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers + # NEEDSWORK: Even though the merge conflict removed the + # SKIP_WORKTREE bit from the index entry for folder1/a, we should + # warn that this is a problematic add. test_all_match git add folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. + # NEEDSWORK: This mode now fails, because folder2/z is + # outside of the sparse-checkout cone and does not match an + # existing index entry with the SKIP_WORKTREE bit cleared. run_on_all mv folder2/a folder2/z && test_all_match git add folder2 && test_all_match git status --porcelain=v2 &&
@@ -654,6 +671,7 @@ test_expect_success 'clean' ' test_expect_success 'submodule handling' ' init_repos && + test_sparse_match git sparse-checkout add modules && test_all_match mkdir modules && test_all_match touch modules/a && test_all_match git add modules &&
@@ -663,6 +681,7 @@ test_expect_success 'submodule handling' ' test_all_match git commit -m "add submodule" && # having a submodule prevents "modules" from collapse + test_sparse_match git sparse-checkout set deep/deeper1 && test-tool -C sparse-index read-cache --table >cache && grep "100644 blob .* modules/a" cache && grep "160000 commit $(git -C initial-repo rev-parse HEAD) modules/sub" cache
Signature · 1 line
gitgitgadget
PATCH v3 7/14 add: skip tracked paths outside sparse-checkout cone
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When 'git add' adds a tracked file that is outside of the sparse-checkout cone, it checks the SKIP_WORKTREE bit to see if the file exists outside of the sparse-checkout cone. This is usually correct, except in the case of a merge conflict outside of the cone.
Modify add_pathspec_matched_against_index() to be more careful about paths by checking the sparse-checkout patterns in addition to the SKIP_WORKTREE bit. This causes 'git add' to no longer allow files outside of the cone that removed the SKIP_WORKTREE bit due to a merge conflict.
With only this change, users will only be able to add the file after adding the file to the sparse-checkout cone. A later change will allow users to force adding even though the file is outside of the sparse-checkout cone.
--- builtin/add.c | 4 ++++ pathspec.c | 5 +++-- t/t1091-sparse-checkout-builtin.sh | 4 +++- t/t1092-sparse-checkout-compatibility.sh | 19 ++++++++++++------- t/t3705-add-sparse-checkout.sh | 12 ++++++++++++ 5 files changed, 34 insertions(+), 10 deletions(-)
builtin/add.c +4 -0
@@ -94,6 +94,10 @@ static void update_callback(struct diff_queue_struct *q,for (i =0 ; i < q->nr; i++) {struct diff_filepair *p = q->queue[i];const char *path = p->one->path; + +if (!path_in_sparse_checkout (path, &the_index)) +continue ; +switch (fix_unmerged_status (p, data)) {default :die (_ ("unexpected diff status %c" ), p->status);
pathspec.c +3 -2
@@ -39,7 +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)) +if (sw_action ==PS_IGNORE_SKIP_WORKTREE && + (ce_skip_worktree (ce) || !path_in_sparse_checkout (ce->name, istate)))continue ;ce_path_match (istate, ce, pathspec, seen); }
@@ -70,7 +71,7 @@ char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec)for (i =0 ; i < istate->cache_nr; i++) {struct cache_entry *ce = istate->cache[i]; -if (ce_skip_worktree (ce)) +if (ce_skip_worktree (ce) || !path_in_sparse_checkout (ce->name, istate))ce_path_match (istate, ce, pathspec, seen); }
t/t1091-sparse-checkout-builtin.sh +3 -1
@@ -406,7 +406,7 @@ test_expect_success 'sparse-checkout (init|set|disable) warns with unmerged stat git -C unmerged sparse-checkout disable ' -test_expect_success 'sparse-checkout reapply' ' +test_expect_failure 'sparse-checkout reapply' ' git clone repo tweak && echo dirty >tweak/deep/deeper2/a &&
@@ -438,6 +438,8 @@ test_expect_success 'sparse-checkout reapply' ' test_i18ngrep "warning.*The following paths are unmerged" err && test_path_is_file tweak/folder1/a && + # NEEDSWORK: We are asking to update a file outside of the + # sparse-checkout cone, but this is no longer allowed. git -C tweak add folder1/a && git -C tweak sparse-checkout reapply 2>err && test_must_be_empty err &&
t/t1092-sparse-checkout-compatibility.sh +12 -7
@@ -546,10 +546,9 @@ test_expect_failure 'merge with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers - # NEEDSWORK: Even though the merge conflict removed the - # SKIP_WORKTREE bit from the index entry for folder1/a, we should - # warn that this is a problematic add. - test_all_match git add folder1/a && + test_sparse_match test_must_fail git add folder1/a && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -558,7 +557,9 @@ test_expect_failure 'merge with conflict outside cone' ' # NEEDSWORK: This mode now fails, because folder2/z is # outside of the sparse-checkout cone and does not match an # existing index entry with the SKIP_WORKTREE bit cleared. - test_all_match git add folder2 && + test_sparse_match test_must_fail git add folder2 && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder2/z && test_all_match git status --porcelain=v2 && test_all_match git merge --continue &&
@@ -586,7 +587,9 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' # NEEDSWORK: Even though the merge conflict removed the # SKIP_WORKTREE bit from the index entry for folder1/a, we should # warn that this is a problematic add. - test_all_match git add folder1/a && + test_sparse_match test_must_fail git add folder1/a && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -595,7 +598,9 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' # outside of the sparse-checkout cone and does not match an # existing index entry with the SKIP_WORKTREE bit cleared. run_on_all mv folder2/a folder2/z && - test_all_match git add folder2 && + test_sparse_match test_must_fail git add folder2 && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder2/z && test_all_match git status --porcelain=v2 && test_all_match git $OPERATION --continue &&
t/t3705-add-sparse-checkout.sh +12 -0
@@ -158,6 +158,18 @@ test_expect_success 'do not warn when pathspec matches dense entries' ' git ls-files --error-unmatch dense_entry ' +test_expect_success 'git add fails outside of sparse-checkout definition' ' + test_when_finished git sparse-checkout disable && + test_commit a && + git sparse-checkout init && + git sparse-checkout set a && + echo >>sparse_entry && + + git update-index --no-skip-worktree sparse_entry && + test_must_fail git add sparse_entry && + test_sparse_entry_unstaged +' + test_expect_success 'add obeys advice.updateSparsePath' ' setup_sparse_entry && test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr &&
Signature · 1 line
gitgitgadget
PATCH v3 8/14 add: implement the --sparse option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
We previously modified 'git add' to refuse updating index entries outside of the sparse-checkout cone. This is justified to prevent users from accidentally getting into a confusing state when Git removes those files from the working tree at some later point.
Unfortunately, this caused some workflows that were previously possible to become impossible, especially around merge conflicts outside of the sparse-checkout cone. These were documented in tests within t1092.
We now re-enable these workflows using a new '--sparse' option to 'git add'. This allows users to signal "Yes, I do know what I'm doing with these files," and accept the consequences of the files leaving the worktree later.
We delay updating the advice message until implementing a similar option in 'git rm' and 'git mv'.
--- Documentation/git-add.txt | 9 +++++++- builtin/add.c | 12 ++++++---- t/t1092-sparse-checkout-compatibility.sh | 29 +++++++++--------------- t/t3705-add-sparse-checkout.sh | 17 +++++++++++++- 4 files changed, 43 insertions(+), 24 deletions(-)
Documentation/git-add.txt +8 -1
@@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git add' [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p] - [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] + [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] [--sparse] [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize] [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]
@@ -79,6 +79,13 @@ in linkgit:gitglossary[7]. --force:: Allow adding otherwise ignored files. +--sparse:: + Allow updating index entries outside of the sparse-checkout cone. + Normally, `git add` refuses to update index entries whose paths do + not fit within the sparse-checkout cone, since those files might + be removed from the working tree without warning. See + linkgit:git-sparse-checkout[1] for more details. + -i:: --interactive:: Add modified contents in the working tree interactively to
builtin/add.c +8 -4
@@ -30,6 +30,7 @@ static int patch_interactive, add_interactive, edit_interactive;static int take_worktree_changes;static int add_renormalize;static int pathspec_file_nul; +static int include_sparse;static const char *pathspec_from_file;static int legacy_stash_p;/* support for the scripted `git stash` */
@@ -46,7 +47,7 @@ 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)) +if (!include_sparse &&ce_skip_worktree (ce))continue ;if (pathspec && !ce_path_match (&the_index, ce, pathspec,NULL ))
@@ -95,7 +96,7 @@ static void update_callback(struct diff_queue_struct *q,struct diff_filepair *p = q->queue[i];const char *path = p->one->path; -if (!path_in_sparse_checkout (path, &the_index)) +if (!include_sparse && !path_in_sparse_checkout (path, &the_index))continue ;switch (fix_unmerged_status (p, data)) {
@@ -383,6 +384,7 @@ static struct option builtin_add_options[] = {OPT_BOOL (0 ,"refresh" , &refresh_only,N_ ("don't add, only refresh the index" )),OPT_BOOL (0 ,"ignore-errors" , &ignore_add_errors,N_ ("just skip files which cannot be added because of errors" )),OPT_BOOL (0 ,"ignore-missing" , &ignore_missing,N_ ("check if - even missing - files are ignored in dry run" )), +OPT_BOOL (0 ,"sparse" , &include_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_STRING (0 ,"chmod" , &chmod_arg,"(+|-)x" ,N_ ("override the executable bit of the listed files" )), OPT_HIDDEN_BOOL(0, "warn-embedded-repo", &warn_on_embedded_repo,
@@ -461,7 +463,8 @@ static int add_files(struct dir_struct *dir, int flags) }for (i =0 ; i < dir->nr; i++) { -if (!path_in_sparse_checkout (dir->entries[i]->name, &the_index)) { +if (!include_sparse && + !path_in_sparse_checkout (dir->entries[i]->name, &the_index)) {string_list_append (&matched_sparse_paths, dir->entries[i]->name);continue ;
@@ -646,7 +649,8 @@ int cmd_add(int argc, const char **argv, const char *prefix)if (seen[i])continue ; -if (matches_skip_worktree (&pathspec, i, &skip_worktree_seen)) { +if (!include_sparse && +matches_skip_worktree (&pathspec, i, &skip_worktree_seen)) {string_list_append (&only_match_skip_worktree, pathspec.items[i].original);continue ;
t/t1092-sparse-checkout-compatibility.sh +11 -18
@@ -343,11 +343,7 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -# NEEDSWORK: Now that 'git add folder1/new' fails, the changes being -# attempted here fail for the sparse-checkout and sparse-index repos. -# We must enable a way for adding files outside the sparse-checkout -# done, even if it is by an optional flag. -test_expect_failure 'status/add: outside sparse cone' ' +test_expect_success 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -375,15 +371,16 @@ test_expect_failure 'status/add: outside sparse cone' ' test_sparse_match test_must_fail git add folder1/new && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/new && + test_sparse_match git add --sparse folder1/a && + test_sparse_match git add --sparse folder1/new && - # NEEDSWORK: behavior begins to deviate here. - test_all_match git add . && + test_all_match git add --sparse . && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/new && test_all_match git rev-parse HEAD^{tree} && run_on_all ../edit-contents folder1/newer && - test_all_match git add folder1/ && + test_all_match git add --sparse folder1/ && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/newer && test_all_match git rev-parse HEAD^{tree}
@@ -527,12 +524,7 @@ test_expect_success 'merge, cherry-pick, and rebase' ' done ' -# NEEDSWORK: This test is documenting current behavior, but that -# behavior can be confusing to users so there is desire to change it. -# Right now, users might be using this flow to work through conflicts, -# so any solution should present advice to users who try this sequence -# of commands to follow whatever new method we create. -test_expect_failure 'merge with conflict outside cone' ' +test_expect_success 'merge with conflict outside cone' ' init_repos && test_all_match git checkout -b merge-tip merge-left &&
@@ -549,17 +541,16 @@ test_expect_failure 'merge with conflict outside cone' ' test_sparse_match test_must_fail git add folder1/a && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && + test_all_match git add --sparse folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. run_on_all mv folder2/a folder2/z && - # NEEDSWORK: This mode now fails, because folder2/z is - # outside of the sparse-checkout cone and does not match an - # existing index entry with the SKIP_WORKTREE bit cleared. test_sparse_match test_must_fail git add folder2 && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder2/z && + test_all_match git add --sparse folder2 && test_all_match git status --porcelain=v2 && test_all_match git merge --continue &&
@@ -567,7 +558,7 @@ test_expect_failure 'merge with conflict outside cone' ' test_all_match git rev-parse HEAD^{tree} ' -test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' +test_expect_success 'cherry-pick/rebase with conflict outside cone' ' init_repos && for OPERATION in cherry-pick rebase
@@ -590,6 +581,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' test_sparse_match test_must_fail git add folder1/a && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && + test_all_match git add --sparse folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -601,6 +593,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' test_sparse_match test_must_fail git add folder2 && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder2/z && + test_all_match git add --sparse folder2 && test_all_match git status --porcelain=v2 && test_all_match git $OPERATION --continue &&
t/t3705-add-sparse-checkout.sh +16 -1
@@ -167,7 +167,13 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' git update-index --no-skip-worktree sparse_entry && test_must_fail git add sparse_entry && - test_sparse_entry_unstaged + test_sparse_entry_unstaged && + + # Avoid munging CRLFs to avoid an error message + git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && + test_must_be_empty stderr && + test-tool read-cache --table >actual && + grep "^100644 blob.*sparse_entry\$" actual ' test_expect_success 'add obeys advice.updateSparsePath' '
@@ -178,4 +184,13 @@ test_expect_success 'add obeys advice.updateSparsePath' ' ' +test_expect_success 'add allows sparse entries with --sparse' ' + git sparse-checkout set a && + echo modified >sparse_entry && + test_must_fail git add sparse_entry && + test_sparse_entry_unchanged && + git add --sparse sparse_entry 2>stderr && + test_must_be_empty stderr +' + test_done
Signature · 1 line
gitgitgadget
PATCH v3 9/14 add: update --chmod to skip sparse paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
We added checks for path_in_sparse_checkout() to portions of 'git add' that add warnings and prevent staging a modification, but we skipped the --chmod mode. Update chmod_pathspec() to ignore cache entries whose path is outside of the sparse-checkout cone (unless --sparse is provided). Add a test in t3705.
--- builtin/add.c | 4 +++- t/t3705-add-sparse-checkout.sh | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-)
builtin/add.c +3 -1
@@ -47,7 +47,9 @@ static int chmod_pathspec(struct pathspec *pathspec, char flip, int show_only)struct cache_entry *ce = active_cache[i];int err; -if (!include_sparse &&ce_skip_worktree (ce)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;if (pathspec && !ce_path_match (&the_index, ce, pathspec,NULL ))
t/t3705-add-sparse-checkout.sh +9 -1
@@ -169,11 +169,19 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' test_must_fail git add sparse_entry && test_sparse_entry_unstaged && + test_must_fail git add --chmod=+x sparse_entry && + test_sparse_entry_unstaged && + # Avoid munging CRLFs to avoid an error message git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && test_must_be_empty stderr && test-tool read-cache --table >actual && - grep "^100644 blob.*sparse_entry\$" actual + grep "^100644 blob.*sparse_entry\$" actual && + + git add --sparse --chmod=+x sparse_entry 2>stderr && + test_must_be_empty stderr && + test-tool read-cache --table >actual && + grep "^100755 blob.*sparse_entry\$" actual ' test_expect_success 'add obeys advice.updateSparsePath' '
Signature · 1 line
gitgitgadget
PATCH v3 10/14 add: update --renormalize to skip sparse paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
We added checks for path_in_sparse_checkout() to portions of 'git add' that add warnings and prevent stagins a modification, but we skipped the --renormalize mode. Update renormalize_tracked_files() to ignore cache entries whose path is outside of the sparse-checkout cone (unless --sparse is provided). Add a test in t3705.
--- builtin/add.c | 4 +++- t/t3705-add-sparse-checkout.sh | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-)
builtin/add.c +3 -1
@@ -154,7 +154,9 @@ 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)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;if (ce_stage (ce))continue ;/* do not touch unmerged paths */
t/t3705-add-sparse-checkout.sh +11 -1
@@ -172,6 +172,9 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' test_must_fail git add --chmod=+x sparse_entry && test_sparse_entry_unstaged && + test_must_fail git add --renormalize sparse_entry && + test_sparse_entry_unstaged && + # Avoid munging CRLFs to avoid an error message git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && test_must_be_empty stderr &&
@@ -181,7 +184,14 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' git add --sparse --chmod=+x sparse_entry 2>stderr && test_must_be_empty stderr && test-tool read-cache --table >actual && - grep "^100755 blob.*sparse_entry\$" actual + grep "^100755 blob.*sparse_entry\$" actual && + + git reset && + + # This will print a message over stderr on Windows. + git add --sparse --renormalize sparse_entry && + git status --porcelain >actual && + grep "^M sparse_entry\$" actual ' test_expect_success 'add obeys advice.updateSparsePath' '
Signature · 1 line
gitgitgadget
PATCH v3 11/14 rm: add --sparse option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
As we did previously in 'git add', add a '--sparse' option to 'git rm' that allows modifying paths outside of the sparse-checkout definition. The existing checks in 'git rm' are restricted to tracked files that have the SKIP_WORKTREE bit in the current index. Future changes will cause 'git rm' to reject removing paths outside of the sparse-checkout definition, even if they are untracked or do not have the SKIP_WORKTREE bit.
--- Documentation/git-rm.txt | 6 ++++++ builtin/rm.c | 8 ++++++-- t/t3602-rm-sparse-checkout.sh | 12 ++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-)
Documentation/git-rm.txt +6 -0
@@ -72,6 +72,12 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7]. --ignore-unmatch:: Exit with a zero status even if no files matched. +--sparse:: + Allow updating index entries outside of the sparse-checkout cone. + Normally, `git rm` refuses to update index entries whose paths do + not fit within the sparse-checkout cone. See + linkgit:git-sparse-checkout[1] for more. + -q:: --quiet:: `git rm` normally outputs one line (in the form of an `rm` command)
builtin/rm.c +6 -2
@@ -237,6 +237,7 @@ static int check_local_mod(struct object_id *head, int index_only)static int show_only =0 , force =0 , index_only =0 , recursive =0 , quiet =0 ;static int ignore_unmatch =0 , pathspec_file_nul; +static int include_sparse;static char *pathspec_from_file;static struct option builtin_rm_options[] = {
@@ -247,6 +248,7 @@ static struct option builtin_rm_options[] = {OPT_BOOL ('r' ,NULL , &recursive,N_ ("allow recursive removal" )),OPT_BOOL (0 ,"ignore-unmatch" , &ignore_unmatch,N_ ("exit with a zero status even if nothing matched" )), +OPT_BOOL (0 ,"sparse" , &include_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_PATHSPEC_FROM_FILE (&pathspec_from_file),OPT_PATHSPEC_FILE_NUL (&pathspec_file_nul), OPT_END(),
@@ -298,7 +300,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix)ensure_full_index (&the_index);for (i =0 ; i < active_nr; i++) {const struct cache_entry *ce = active_cache[i]; -if (ce_skip_worktree (ce)) + +if (!include_sparse &&ce_skip_worktree (ce))continue ;if (!ce_path_match (&the_index, ce, &pathspec, seen))continue ;
@@ -322,7 +325,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix) seen_any =1 ; else if (ignore_unmatch) continue; - else if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) + else if (!include_sparse && + 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);
t/t3602-rm-sparse-checkout.sh +12 -0
@@ -43,6 +43,18 @@ test_expect_success 'recursive rm does not remove sparse entries' ' test_cmp expected actual ' +test_expect_success 'recursive rm --sparse removes sparse entries' ' + git reset --hard && + git sparse-checkout set "sub/dir" && + git rm --sparse -r sub && + git status --porcelain -uno >actual && + cat >expected <<-\EOF && + D sub/d + D sub/dir/e + EOF + test_cmp expected actual +' + test_expect_success 'rm obeys advice.updateSparsePath' ' git reset --hard && git sparse-checkout set a &&
Signature · 1 line
gitgitgadget
PATCH v3 12/14 rm: skip sparse paths with missing SKIP_WORKTREE
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
If a path does not match the sparse-checkout cone but is somehow missing the SKIP_WORKTREE bit, then 'git rm' currently succeeds in removing the file. One reason a user might be in this situation is a merge conflict outside of the sparse-checkout cone. Removing such a file might be problematic for users who are not sure what they are doing.
Add a check to path_in_sparse_checkout() when 'git rm' is checking if a path should be considered for deletion. Of course, this check is ignored if the '--sparse' option is specified, allowing users who accept the risks to continue with the removal.
This also removes a confusing behavior where a user asks for a directory to be removed, but only the entries that are within the sparse-checkout definition are removed. Now, 'git rm <dir>' will fail without '--sparse' and will succeed in removing all contained paths with '--sparse'.
--- builtin/rm.c | 4 +++- t/t3602-rm-sparse-checkout.sh | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-)
builtin/rm.c +3 -1
@@ -301,7 +301,9 @@ 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 (!include_sparse &&ce_skip_worktree (ce)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;if (!ce_path_match (&the_index, ce, &pathspec, seen))continue ;
t/t3602-rm-sparse-checkout.sh +17 -2
@@ -37,9 +37,13 @@ done test_expect_success 'recursive rm does not remove sparse entries' ' git reset --hard && git sparse-checkout set sub/dir && - git rm -r sub && + test_must_fail git rm -r sub && + git rm --sparse -r sub && git status --porcelain -uno >actual && - echo "D sub/dir/e" >expected && + cat >expected <<-\EOF && + D sub/d + D sub/dir/e + EOF test_cmp expected actual '
@@ -87,4 +91,15 @@ test_expect_success 'do not warn about sparse entries with --ignore-unmatch' ' git ls-files --error-unmatch b ' +test_expect_success 'refuse to rm a non-skip-worktree path outside sparse cone' ' + git reset --hard && + git sparse-checkout set a && + git update-index --no-skip-worktree b && + test_must_fail git rm b 2>stderr && + test_cmp b_error_and_hint stderr && + git rm --sparse b 2>stderr && + test_must_be_empty stderr && + test_path_is_missing b +' + test_done
Signature · 1 line
gitgitgadget
PATCH v3 13/14 mv: refuse to move sparse paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Since cmd_mv() does not operate on cache entries and instead directly checks the filesystem, we can only use path_in_sparse_checkout() as a mechanism for seeing if a path is sparse or not. Be sure to skip returning a failure if '-k' is specified.
To ensure that the advice around sparse paths is the only reason a move failed, be sure to check this as the very last thing before inserting into the src_for_dst list.
The tests cover a variety of cases such as whether the target is tracked or untracked, and whether the source or destination are in or outside of the sparse-checkout definition.
--- builtin/mv.c | 52 ++++++++-- t/t7002-mv-sparse-checkout.sh | 186 ++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 9 deletions(-) create mode 100755 t/t7002-mv-sparse-checkout.sh
builtin/mv.c +43 -9
@@ -118,21 +118,23 @@ static int index_range_of_same_dir(const char *src, int length,int cmd_mv (int argc,const char **argv,const char *prefix) {int i, flags, gitmodules_modified =0 ; -int verbose =0 , show_only =0 , force =0 , ignore_errors =0 ; +int verbose =0 , show_only =0 , force =0 , ignore_errors =0 , ignore_sparse =0 ;struct option builtin_mv_options[] = {OPT__VERBOSE (&verbose,N_ ("be verbose" )),OPT__DRY_RUN (&show_only,N_ ("dry run" )),OPT__FORCE (&force,N_ ("force move/rename even if target exists" ),PARSE_OPT_NOCOMPLETE ),OPT_BOOL ('k' ,NULL , &ignore_errors,N_ ("skip move/rename errors" )), +OPT_BOOL (0 ,"sparse" , &ignore_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_END (), };const char **source, **destination, **dest_path, **submodule_gitfile; -enum update_mode {BOTH =0 ,WORKING_DIRECTORY ,INDEX } *modes; +enum update_mode {BOTH =0 ,WORKING_DIRECTORY ,INDEX ,SPARSE } *modes;struct stat st;struct string_list src_for_dst =STRING_LIST_INIT_NODUP ;struct lock_file lock_file =LOCK_INIT ;struct cache_entry *ce; +struct string_list only_match_skip_worktree =STRING_LIST_INIT_NODUP ;git_config (git_default_config,NULL );
@@ -176,14 +178,17 @@ int cmd_mv(int argc, const char **argv, const char *prefix)const char *src = source[i], *dst = destination[i];int length, src_is_dir;const char *bad =NULL ; +int skip_sparse =0 ;if (show_only)printf (_ ("Checking rename of '%s' to '%s'\n" ), src, dst); length =strlen (src); -if (lstat (src, &st) <0 ) - bad =_ ("bad source" ); -else if (!strncmp (src, dst, length) && +if (lstat (src, &st) <0 ) { +/* only error if existence is expected. */ +if (modes[i] !=SPARSE ) + bad =_ ("bad source" ); + }else if (!strncmp (src, dst, length) && (dst[length] ==0 || dst[length] =='/' )) { bad =_ ("can not move directory into itself" ); } else if ((src_is_dir = S_ISDIR(st.st_mode))
@@ -212,11 +217,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix) dst_len =strlen (dst);for (j =0 ; j < last - first; j++) { -const char *path = active_cache[first + j]->name; +const struct cache_entry *ce = active_cache[first + j]; +const char *path = ce->name; source[argc + j] = path; destination[argc + j] =prefix_path (dst, dst_len, path + length +1 ); - modes[argc + j] =INDEX ; + modes[argc + j] =ce_skip_worktree (ce) ?SPARSE :INDEX ; submodule_gitfile[argc + j] =NULL ; } argc += last - first;
@@ -244,14 +250,36 @@ int cmd_mv(int argc, const char **argv, const char *prefix) bad =_ ("multiple sources for the same target" ); else if (is_dir_sep(dst[strlen(dst) - 1])) bad =_ ("destination directory does not exist" ); - else + else { +/* +* We check if the paths are in the sparse-checkout +* definition as a very final check, since that +* allows us to point the user to the --sparse +* option as a way to have a successful run. +*/ +if (!ignore_sparse && + !path_in_sparse_checkout (src, &the_index)) { +string_list_append (&only_match_skip_worktree, src); + skip_sparse =1 ; + } +if (!ignore_sparse && + !path_in_sparse_checkout (dst, &the_index)) { +string_list_append (&only_match_skip_worktree, dst); + skip_sparse =1 ; + } + +if (skip_sparse) + goto remove_entry; +string_list_insert (&src_for_dst, dst); + }if (!bad)continue ;if (!ignore_errors)die (_ ("%s, source=%s, destination=%s" ), bad, src, dst); +remove_entry:if (--argc >0 ) {int n = argc - i; memmove(source + i, source + i + 1,
@@ -266,6 +294,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix) } } +if (only_match_skip_worktree.nr) { +advise_on_updating_sparse_paths (&only_match_skip_worktree); +if (!ignore_errors) +return 1 ; + } +for (i =0 ; i < argc; i++) {const char *src = source[i], *dst = destination[i];enum update_mode mode = modes[i];
@@ -274,7 +308,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)printf (_ ("Renaming %s to %s\n" ), src, dst);if (show_only)continue ; -if (mode !=INDEX &&rename (src, dst) <0 ) { +if (mode !=INDEX && mode !=SPARSE &&rename (src, dst) <0 ) {if (ignore_errors)continue ;die_errno (_ ("renaming '%s' failed" ), src);
t/t7002-mv-sparse-checkout.sh new file +186 -0
186 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v3 14/14 advice: update message to suggest '--sparse'
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The previous changes modified the behavior of 'git add', 'git rm', and 'git mv' to not adjust paths outside the sparse-checkout cone, even if they exist in the working tree and their cache entries lack the SKIP_WORKTREE bit. The intention is to warn users that they are doing something potentially dangerous. The '--sparse' option was added to each command to allow careful users the same ability they had before.
To improve the discoverability of this new functionality, add a message to advice.updateSparsePath that mentions the existence of the option.
The previous set of changes also modified the purpose of this message to include possibly a list of paths instead of only a list of pathspecs. Make the warning message more clear about this new behavior.
--- advice.c | 11 ++++++----- t/t3602-rm-sparse-checkout.sh | 9 ++++++--- t/t3705-add-sparse-checkout.sh | 9 ++++++--- t/t7002-mv-sparse-checkout.sh | 9 ++++++--- 4 files changed, 24 insertions(+), 14 deletions(-)
advice.c +6 -5
@@ -293,15 +293,16 @@ void advise_on_updating_sparse_paths(struct string_list *pathspec_list)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" )); +fprintf (stderr,_ ("The following paths and/or pathspecs matched paths that exist\n" +"outside of your sparse-checkout definition, so will not be\n" +"updated in the index:\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." )); +_ ("If you intend to update such entries, try one of the following:\n" +"* Use the --sparse option.\n" +"* Disable or modify the sparsity rules." )); }void detach_advice (const char *new_name)
t/t3602-rm-sparse-checkout.sh +6 -3
@@ -11,12 +11,15 @@ test_expect_success 'setup' " 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: + The following paths and/or pathspecs matched paths that exist + outside of your sparse-checkout definition, so will not be + updated in the index: EOF cat >sparse_hint <<-EOF && - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: If you intend to update such entries, try one of the following: + hint: * Use the --sparse option. + hint: * Disable or modify the sparsity rules. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
t/t3705-add-sparse-checkout.sh +6 -3
@@ -44,12 +44,15 @@ test_sparse_entry_unstaged () { 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: + The following paths and/or pathspecs matched paths that exist + outside of your sparse-checkout definition, so will not be + updated in the index: EOF cat >sparse_hint <<-EOF && - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: If you intend to update such entries, try one of the following: + hint: * Use the --sparse option. + hint: * Disable or modify the sparsity rules. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
t/t7002-mv-sparse-checkout.sh +6 -3
@@ -11,12 +11,15 @@ test_expect_success 'setup' " 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: + The following paths and/or pathspecs matched paths that exist + outside of your sparse-checkout definition, so will not be + updated in the index: EOF cat >sparse_hint <<-EOF - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: If you intend to update such entries, try one of the following: + hint: * Use the --sparse option. + hint: * Disable or modify the sparsity rules. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF "
Signature · 1 line
gitgitgadget
Re: [PATCH v3 01/14] t3705: test that 'sparse_entry' is unstaged
In reply to Derrick Stolee via GitGitGadgetQuoted 31 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
The tests in
t3705-add-sparse-checkout.shcheck to see how 'git add' behaves with paths outside the sparse-checkout definition. These currently check to see if a given warning is present but not that the index is not updated with the sparse entries. Add a new 'test_sparse_entry_unstaged' helper to be sure 'git add' is behaving correctly.We need to modify
setup_sparse_entryto actually commit thesparse_entryfile so it exists at HEAD and as an entry in the index, but its exact contents are not staged in the index.Signed-off-by Derrick Stolee <[email protected]>--- t/t3705-add-sparse-checkout.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+)t/t3705-add-sparse-checkout.sh +1
-0@@ -19,6 +19,7 @@ setup_sparse_entry () { fi && git add sparse_entry && git update-index --skip-worktree sparse_entry && + git commit --allow-empty -m "ensure sparse_entry exists at HEAD" && SPARSE_ENTRY_BLOB=$(git rev-parse :sparse_entry) }
t/t3705-add-sparse-checkout.sh setup_gitignore () { · line 42
Quoted 6 lines
t/t3705-add-sparse-checkout.sh +3
-0@@ -36,6 +37,11 @@ setup_gitignore () { EOF } +test_sparse_entry_unstaged () { + git status --porcelain >actual && + ! grep "^[MDARCU][M ] sparse_entry\$" actual
Does this say "we do not want any difference from the index, be it modification, deletion, addtion, etc."?
Just wondering if there were a reason why the pattern is more complex than "^[^ ][M ]" (i.e. anything but "unmodified since the index"), not necessarily suggesting to spell the test differently.
Thanks.
Re: [PATCH v3 02/14] t1092: behavior for adding sparse files
In reply to Derrick Stolee via GitGitGadgetQuoted 20 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
Add some tests to demonstrate the current behavior around adding files outside of the sparse-checkout cone. Currently, untracked files are handled differently from tracked files. A future change will make these cases be handled the same way.
Further expand checking that a failed 'git add' does not stage changes to the index.
Signed-off-by Derrick Stolee <[email protected]>--- t/t1092-sparse-checkout-compatibility.sh | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+)t/t1092-sparse-checkout-compatibility.sh +0
-0
t/t1092-sparse-checkout-compatibility.sh test_sparse_match () { · line 196
Quoted 10 lines
t/t1092-sparse-checkout-compatibility.sh +7
-0@@ -187,6 +187,16 @@ test_sparse_match () { test_cmp sparse-checkout-err sparse-index-err } +test_sparse_unstaged () { + file=$1 && + for repo in sparse-checkout sparse-index + do + git -C $repo status --porcelain >$repo-out && + ! grep "^A $file\$" $repo-out && + ! grep "^M $file\$" $repo-out || return 1
Is addition and modification very special, as opposed to other kinds of changes? Is the reason why we say "we do not want to see addition nor modification" here because there is no concrete X that we can say "we do want to see X" in various callers of this helper?
I am also wondering, if this is asserting that $file is not added to the index, why we are using "git status" and not "ls-files", for example. Wouldn't
test_must_fail git ls-files --error-unmatch "$file"
be a more direct way to do so?
Re: [PATCH v3 03/14] dir: extract directory-matching logic
In reply to Derrick Stolee via GitGitGadgetdir.c int match_pathname(const char *pathname, int pathlen, · line 1317
Quoted 12 lines · Derrick Stolee via GitGitGadget
dir.c +12
-0+static int path_matches_dir_pattern (const char *pathname, +int pathlen, +int *dtype, +struct path_pattern *pattern, +struct index_state *istate) +{ + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); +if (*dtype !=DT_DIR ) +return 0 ; + +return 1 ; +}
The function name and parameter list have "pattern" but as far as I can see any "matches" or "pattern" comes into the picture. The code in the caller after calling this function may be doing pattern matching, but not this one.
What this helper is doing is "signal if the pathname in the working tree is supposed to be a directory with the return value, while filling *dtype with what kind of thing it is."
path_must_be_dir_in_working_tree() or something, perhaps?
Quoted 16 lines
dir.c +4
-5@@ -1327,11 +1340,10 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnameconst char *exclude = pattern->pattern;int prefix = pattern->nowildcardlen; -if (pattern->flags &PATTERN_FLAG_MUSTBEDIR ) { - *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); -if (*dtype !=DT_DIR ) -continue ; - } +if ((pattern->flags &PATTERN_FLAG_MUSTBEDIR ) && + !path_matches_dir_pattern (pathname, pathlen, + dtype, pattern, istate)) +continue ;if (pattern->flags &PATTERN_FLAG_NODIR ) { if (match_basename(basename,
Re: [PATCH v3 02/14] t1092: behavior for adding sparse files
In reply to Junio C HamanoQuoted 38 lines · Junio C Hamano
Quoted 20 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
Add some tests to demonstrate the current behavior around adding files outside of the sparse-checkout cone. Currently, untracked files are handled differently from tracked files. A future change will make these cases be handled the same way.
Further expand checking that a failed 'git add' does not stage changes to the index.
Signed-off-by Derrick Stolee <[email protected]>--- t/t1092-sparse-checkout-compatibility.sh | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+)t/t1092-sparse-checkout-compatibility.sh +0
-0t/t1092-sparse-checkout-compatibility.sh test_sparse_match () { · line 196
Quoted 10 lines
t/t1092-sparse-checkout-compatibility.sh +7
-0@@ -187,6 +187,16 @@ test_sparse_match () { test_cmp sparse-checkout-err sparse-index-err } +test_sparse_unstaged () { + file=$1 && + for repo in sparse-checkout sparse-index + do + git -C $repo status --porcelain >$repo-out && + ! grep "^A $file\$" $repo-out && + ! grep "^M $file\$" $repo-out || return 1Is addition and modification very special, as opposed to other kinds of changes? Is the reason why we say "we do not want to see addition nor modification" here because there is no concrete X that we can say "we do want to see X" in various callers of this helper?
Quoted 7 lines
I am also wondering, if this is asserting that $file is not added to the index, why we are using "git status" and not "ls-files", for example. Wouldn't
test_must_fail git ls-files --error-unmatch "$file"be a more direct way to do so?
The intention is to avoid a staged change, even if that is a modification or addition. The --error-unmatch approach only works if we are trying to avoid an add, and we need to know that the path does not exist in the index already if we want to take that approach.
Upon reflection, perhaps what we really want is this:
git diff --staged -- $path >diff && test_must_be_empty diff
This applies to the previous patch, too.
Thanks,
-Stolee
Re: [PATCH v3 03/14] dir: extract directory-matching logic
In reply to Junio C HamanoQuoted 20 lines · Junio C Hamano
dir.c int match_pathname(const char *pathname, int pathlen, · line 1317
Quoted 12 lines · Derrick Stolee via GitGitGadget
dir.c +12
-0+static int path_matches_dir_pattern (const char *pathname, +int pathlen, +int *dtype, +struct path_pattern *pattern, +struct index_state *istate) +{ + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); +if (*dtype !=DT_DIR ) +return 0 ; + +return 1 ; +}The function name and parameter list have "pattern" but as far as I can see any "matches" or "pattern" comes into the picture. The code in the caller after calling this function may be doing pattern matching, but not this one.
Quoted 5 lines
What this helper is doing is "signal if the pathname in the working tree is supposed to be a directory with the return value, while filling *dtype with what kind of thing it is."
path_must_be_dir_in_working_tree()or something, perhaps?
Yes, a rename would be prudent here. Thanks.
-Stolee
Re: [PATCH v3 03/14] dir: extract directory-matching logic
Quoted 21 lines · Derrick Stolee
Quoted 20 lines · Junio C Hamano
Review
Quoted 12 lines · Derrick Stolee via GitGitGadget
(quoted diff) +12
-0+static int path_matches_dir_pattern(const char *pathname, + int pathlen, + int *dtype, + struct path_pattern *pattern, + struct index_state *istate) +{ + *dtype = resolve_dtype(*dtype, istate, pathname, pathlen); + if (*dtype != DT_DIR) + return 0; + + return 1; +}The function name and parameter list have "pattern" but as far as I can see any "matches" or "pattern" comes into the picture. The code in the caller after calling this function may be doing pattern matching, but not this one.
Quoted 7 lines
Quoted 5 lines
What this helper is doing is "signal if the pathname in the working tree is supposed to be a directory with the return value, while filling *dtype with what kind of thing it is."
path_must_be_dir_in_working_tree()or something, perhaps?Yes, a rename would be prudent here. Thanks.
Of course, when I go to amend the commit, the commit message says
We will expand the path_matches_dir_pattern() method in a following change.
which means that more will follow that will actually care about the pattern and matching as a directory.
After looking at the extension in the next patch, do you still think a rename is necessary? Specifically, this diff hunk:
dir.c +25 -0
@@ -1305,10 +1305,35 @@ int match_pathname(const char *pathname, int pathlen,static int path_matches_dir_pattern (const char *pathname,int pathlen, +struct strbuf **path_parent,int *dtype,struct path_pattern *pattern,struct index_state *istate) { +if (!*path_parent) { +char *slash; +CALLOC_ARRAY (*path_parent,1 ); +strbuf_add (*path_parent, pathname, pathlen); + slash =find_last_dir_sep ((*path_parent)->buf); + +if (slash) +strbuf_setlen (*path_parent, slash - (*path_parent)->buf); +else +strbuf_setlen (*path_parent,0 ); + } + +/* +* If the parent directory matches the pattern, then we do not +* need to check for dtype. +*/ +if ((*path_parent)->len && +match_pathname ((*path_parent)->buf, (*path_parent)->len, + pattern->base, + pattern->baselen ? pattern->baselen -1 :0 , + pattern->pattern, pattern->nowildcardlen, + pattern->patternlen, pattern->flags)) +return 1 ; + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen);if (*dtype !=DT_DIR )return 0 ;
Thanks,
-Stolee
Re: [PATCH v3 03/14] dir: extract directory-matching logic
Quoted 32 lines · Derrick Stolee
Quoted 21 lines · Derrick Stolee
Quoted 20 lines · Junio C Hamano
Review
Quoted 12 lines · Derrick Stolee via GitGitGadget
(quoted diff) +12
-0+static int path_matches_dir_pattern(const char *pathname, + int pathlen, + int *dtype, + struct path_pattern *pattern, + struct index_state *istate) +{ + *dtype = resolve_dtype(*dtype, istate, pathname, pathlen); + if (*dtype != DT_DIR) + return 0; + + return 1; +}The function name and parameter list have "pattern" but as far as I can see any "matches" or "pattern" comes into the picture. The code in the caller after calling this function may be doing pattern matching, but not this one.
Quoted 7 lines
Quoted 5 lines
What this helper is doing is "signal if the pathname in the working tree is supposed to be a directory with the return value, while filling *dtype with what kind of thing it is."
path_must_be_dir_in_working_tree()or something, perhaps?Yes, a rename would be prudent here. Thanks.
Of course, when I go to amend the commit, the commit message says
Quoted 8 lines
We will expand the
path_matches_dir_pattern()method in a following change.which means that more will follow that will actually care about the pattern and matching as a directory.
After looking at the extension in the next patch, do you still think a rename is necessary?
When the focus and purpose of the function changes, it may warrant a rename to include "matching" or "pattern", but not before.
Or we might be seeing a premature refactoring with these two steps. Are we gaining multiple callers of this function before it gets extended to care about pattern and matching? If not, perhaps teaching the inlined codepath about the pattern and matching in place first before extracting the code to a helper function for readability and reusability may help make the resulting series easier to follow, and we do not have to see a function with a misleading name.
Quoted 39 lines · Derrick Stolee via GitGitGadget
This series is based on ds/mergies-with-sparse-index.
As requested, this series looks to update the behavior of git add, git rm, and git mv when they attempt to modify paths outside of the sparse-checkout cone. In particular, this care is expanded to not just cache entries with the
SKIP_WORKTREEbit, but also paths that do not match the sparse-checkout definition.This means that commands that worked before this series can now fail. In particular, if 'git merge' results in a conflict outside of the sparse-checkout cone, then 'git add ' will now fail.
In order to allow users to circumvent these protections, a new '--sparse' option is added that ignores the sparse-checkout patterns and the
SKIP_WORKTREEbit. The message for advice.updateSparsePath is adjusted to assist with discovery of this option.There is a subtle issue with git mv in that it does not check the index until it discovers a directory and then uses the index to find the contained entries. This means that in non-cone-mode patterns, a pattern such as "sub/dir" will not match the path "sub" and this can cause an issue.
In order to allow for checking arbitrary paths against the sparse-checkout patterns, some changes to the underlying pattern matching code is required. It turns out that there are some bugs in the methods as advertised, but these bugs were never discovered because of the way methods like
unpack_trees()will check a directory for a pattern match before checking its contained paths. Our new "check patterns on-demand" approach pokes holes in that approach, specifically with patterns that match entire directories.Updates in v3
=============* Fixed an incorrectly-squashed commit. Spread out some changes in a better way. For example, I don't add --sparse to tests before introducing the option.
Quoted 8 lines
* Use a NULL struct strbuf pointer to indicate an uninitialized value instead of relying on an internal member.
* Use grep over
test_i18ngrep.* Fixed line wrapping for error messages.
* Use
strbuf_setlen()over modifying the len member manually.
I see that you and Junio had some interesting comments on the first 3 patches, so I look forward to seeing how those play out, but you addressed all my feedback from the previous rounds here.
Re: [PATCH 03/13] dir: select directories correctly
In reply to Derrick Stolee via GitGitGadgetAm 24.08.21 um 23:54 schrieb Derrick Stolee via GitGitGadget:
Quoted 36 lines
From: Derrick Stolee <[email protected]>
When matching a path against a list of patterns, the ones that require a directory match previously did not work when a filename is specified. This was fine when all pattern-matching was done within methods such as
unpack_trees()that check a directory before recursing into the contained files. However, other commands will start matching individual files against pattern lists without that recursive approach.We modify
path_matches_dir_pattern()to take a strbuf 'path_parent' that is used to store the parent directory of 'pathname' between multiple pattern matching tests. This is loaded lazily, only on the first pattern it finds that has thePATTERN_FLAG_MUSTBEDIRflag.If we find that a path has a parent directory, we start by checking to see if that parent directory matches the pattern. If so, then we do not need to query the index for the type (which can be expensive). If we find that the parent does not match, then we still must check the type from the index for the given pathname.
Note that this does not affect cone mode pattern matching, but instead the more general -- and slower -- full pattern set. Thus, this does not affect the sparse index.
Signed-off-by Derrick Stolee <[email protected]>--- dir.c | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-)dir.c +0
-0@@ -1305,10 +1305,38 @@ int match_pathname(const char *pathname, int pathlen, static int path_matches_dir_pattern(const char *pathname,
dir.c int match_pathname(const char *pathname, int pathlen, · line 1316
Quoted 10 lines
dir.c +5
-0int pathlen, + struct strbuf *path_parent, int *dtype, struct path_pattern *pattern, struct index_state *istate) { +/* +* Use 'alloc' as an indicator that the string has not been +* initialized, in case the parent is the root directory. +*/
This means the caller needs to take care to release the strbuf between calls for files from different directories. Seems a bit fragile. The current caller is only ever passing in the same pathname before throwing away the strbuf, so it's doing the right thing.
dir.c int match_pathname(const char *pathname, int pathlen, · line 1320
Quoted 4 lines
dir.c +4
-0+if (!path_parent->alloc) { +char *slash; +strbuf_addstr (path_parent, pathname); + slash =find_last_dir_sep (path_parent->buf);
The caller has pathname, pathlen and basename. If basename is guaranteed to be a substring of pathname then the parent directory name length could be calculated without requiring a string copy or scan.
IIUC if pathname and basename can be pointers to different objects then just checking if basename is between pathname and pathname + pathlen would already be undefined behavior.
Using pathname, pathlen and dirlen instead would be safer for such calculations, as it enforces basename to be a substring. Seems like this would require a lot of function signature changes, though, as the call tree is quite high. :|
dir.c int match_pathname(const char *pathname, int pathlen, · line 1323
Quoted 3 lines
dir.c +3
-0+ +if (slash) + *slash ='\0' ;
This doesn't update path_parent->len...
dir.c int match_pathname(const char *pathname, int pathlen, · line 1333
Quoted 10 lines
dir.c +10
-0+ else + strbuf_setlen(path_parent, 0); + } + +/* +* If the parent directory matches the pattern, then we do not +* need to check for dtype. +*/ +if (path_parent->len && + match_pathname(path_parent->buf, path_parent->len,
... so this checks if "<dirname>\0<basename>" matches. Intended?
Quoted 18 lines
dir.c +7
-0+ pattern->base, + pattern->baselen ? pattern->baselen -1 :0 , + pattern->pattern, pattern->nowildcardlen, + pattern->patternlen, pattern->flags)) + return 1; + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen);if (*dtype !=DT_DIR )return 0 ;@@ -1331,6 +1359,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname {struct path_pattern *res =NULL ;/* undecided */ int i; +struct strbuf path_parent =STRBUF_INIT ;if (!pl->nr)return NULL ;/* undefined */
dir.c static struct path_pattern *last_matching_pattern_from_list(const char *pathname · line 1373
Quoted 7 lines
dir.c +2
-2@@ -1340,8 +1369,8 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnameconst char *exclude = pattern->pattern;int prefix = pattern->nowildcardlen; -if ((pattern->flags &PATTERN_FLAG_MUSTBEDIR ) && - !path_matches_dir_pattern(pathname, pathlen, +if (pattern->flags &PATTERN_FLAG_MUSTBEDIR && + !path_matches_dir_pattern(pathname, pathlen, &path_parent,
"a & b && c" is equivalent to "(a & b) && c", but removing the parentheses here serves no apparent purpose and distracts a bit from the actual change, i.e. adding a parameter.
Quoted 12 lines
dir.c +1
-0dtype, pattern, istate))continue ;@@ -1367,6 +1396,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnamebreak ; } } +strbuf_release (&path_parent);return res; }
Re: [PATCH v3 03/14] dir: extract directory-matching logic
In reply to Junio C HamanoQuoted 46 lines · Junio C Hamano
Quoted 32 lines · Derrick Stolee
Quoted 21 lines · Derrick Stolee
Quoted 20 lines · Junio C Hamano
Review
Quoted 12 lines · Derrick Stolee via GitGitGadget
(quoted diff) +12
-0+static int path_matches_dir_pattern(const char *pathname, + int pathlen, + int *dtype, + struct path_pattern *pattern, + struct index_state *istate) +{ + *dtype = resolve_dtype(*dtype, istate, pathname, pathlen); + if (*dtype != DT_DIR) + return 0; + + return 1; +}The function name and parameter list have "pattern" but as far as I can see any "matches" or "pattern" comes into the picture. The code in the caller after calling this function may be doing pattern matching, but not this one.
Quoted 7 lines
Quoted 5 lines
What this helper is doing is "signal if the pathname in the working tree is supposed to be a directory with the return value, while filling *dtype with what kind of thing it is."
path_must_be_dir_in_working_tree()or something, perhaps?Yes, a rename would be prudent here. Thanks.
Of course, when I go to amend the commit, the commit message says
Quoted 8 lines
We will expand the
path_matches_dir_pattern()method in a following change.which means that more will follow that will actually care about the pattern and matching as a directory.
After looking at the extension in the next patch, do you still think a rename is necessary?
When the focus and purpose of the function changes, it may warrant a rename to include "matching" or "pattern", but not before.
Quoted 8 lines
Or we might be seeing a premature refactoring with these two steps. Are we gaining multiple callers of this function before it gets extended to care about pattern and matching? If not, perhaps teaching the inlined codepath about the pattern and matching in place first before extracting the code to a helper function for readability and reusability may help make the resulting series easier to follow, and we do not have to see a function with a misleading name.
Squashing these two patches together has the same effect, but takes a little bit extra work to see that the re-used code is the same. It's small enough that I don't see that as a huge hurdle.
Thanks,
-Stolee
v4 cover Sparse-checkout: modify 'git add', 'git rm', and 'git mv' behavior
In reply to Derrick Stolee via GitGitGadgetThis series is based on ds/mergies-with-sparse-index.
As requested, this series looks to update the behavior of git add, git rm, and git mv when they attempt to modify paths outside of the sparse-checkout cone. In particular, this care is expanded to not just cache entries with the SKIP_WORKTREE bit, but also paths that do not match the sparse-checkout definition.
This means that commands that worked before this series can now fail. In particular, if 'git merge' results in a conflict outside of the sparse-checkout cone, then 'git add ' will now fail.
In order to allow users to circumvent these protections, a new '--sparse' option is added that ignores the sparse-checkout patterns and the SKIP_WORKTREE bit. The message for advice.updateSparsePath is adjusted to assist with discovery of this option.
There is a subtle issue with git mv in that it does not check the index until it discovers a directory and then uses the index to find the contained entries. This means that in non-cone-mode patterns, a pattern such as "sub/dir" will not match the path "sub" and this can cause an issue.
In order to allow for checking arbitrary paths against the sparse-checkout patterns, some changes to the underlying pattern matching code is required. It turns out that there are some bugs in the methods as advertised, but these bugs were never discovered because of the way methods like unpack_trees() will check a directory for a pattern match before checking its contained paths. Our new "check patterns on-demand" approach pokes holes in that approach, specifically with patterns that match entire directories.
Updates in v4
=============
* Instead of using 'git status' and 'grep' to detect staged changes, we use 'git diff --staged'. t1092 uses an additional --diff-filter because it tests with merge conflicts, so it needs this extra flag.
* Patches 3 and 4 are merged into the new patch 3 to avoid temporarily having a poorly named method.
Updates in v3
=============
* Fixed an incorrectly-squashed commit. Spread out some changes in a better way. For example, I don't add --sparse to tests before introducing the option.
* Use a NULL struct strbuf pointer to indicate an uninitialized value instead of relying on an internal member.
* Use grep over test_i18ngrep.
* Fixed line wrapping for error messages.
* Use strbuf_setlen() over modifying the len member manually.
Updates in v2
=============
* I got no complaints about these restrictions, so this is now a full series, not RFC.
* Thanks to Matheus, several holes are filled with extra testing and bugfixes.
* New patches add --chmod and --renormalize improvements. These are added after the --sparse option to make them be one change each.
Thanks, -Stolee
Derrick Stolee (13):
t3705: test that 'sparse_entry' is unstaged
t1092: behavior for adding sparse files
dir: select directories correctly
dir: fix pattern matching on dirs
add: fail when adding an untracked sparse file
add: skip tracked paths outside sparse-checkout cone
add: implement the --sparse option
add: update --chmod to skip sparse paths
add: update --renormalize to skip sparse paths
rm: add --sparse option
rm: skip sparse paths with missing SKIP_WORKTREE
mv: refuse to move sparse paths
advice: update message to suggest '--sparse'
Documentation/git-add.txt | 9 +- Documentation/git-rm.txt | 6 + advice.c | 11 +- builtin/add.c | 32 +++- builtin/mv.c | 52 +++++-- builtin/rm.c | 10 +- dir.c | 56 ++++++- pathspec.c | 5 +- t/t1091-sparse-checkout-builtin.sh | 4 +- t/t1092-sparse-checkout-compatibility.sh | 75 +++++++-- t/t3602-rm-sparse-checkout.sh | 40 ++++- t/t3705-add-sparse-checkout.sh | 68 +++++++- t/t7002-mv-sparse-checkout.sh | 189 +++++++++++++++++++++++ 13 files changed, 505 insertions(+), 52 deletions(-) create mode 100755 t/t7002-mv-sparse-checkout.sh
base-commit: 516680ba7704c473bb21628aa19cabbd787df4db
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1018%2Fderrickstolee%2Fsparse-index%2Fadd-rm-mv-behavior-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1018/derrickstolee/sparse-index/add-rm-mv-behavior-v4
Pull-Request: https://github.com/gitgitgadget/git/pull/1018
Range-diff vs v3:
1: ea940f10a7c ! 1: 642b05fc020 t3705: test that 'sparse_entry' is unstaged
@@ t/t3705-add-sparse-checkout.sh: setup_gitignore () {
} +test_sparse_entry_unstaged () {
-+ git status --porcelain >actual &&
-+ ! grep "^[MDARCU][M ] sparse_entry\$" actual
++ git diff --staged -- sparse_entry >diff &&
++ test_must_be_empty diff
+}
+
test_expect_success 'setup' "
2: c7dedb41291 ! 2: 58389edc76c t1092: behavior for adding sparse files
@@ t/t1092-sparse-checkout-compatibility.sh: test_sparse_match () {
+ file=$1 &&
+ for repo in sparse-checkout sparse-index
+ do
-+ git -C $repo status --porcelain >$repo-out &&
-+ ! grep "^A $file\$" $repo-out &&
-+ ! grep "^M $file\$" $repo-out || return 1
++ # Skip "unmerged" paths
++ git -C $repo diff --staged --diff-filter=ACDMRTXB -- "$file" >diff &&
++ test_must_be_empty diff || return 1
+ done
+}
+
3: b1f6468f9cd < -: ----------- dir: extract directory-matching logic
4: 0252c7ee15c ! 3: 2ebaf8e68c2 dir: select directories correctly @@ Commit message
contained files. However, other commands will start matching individual files against pattern lists without that recursive approach.
- We modify path_matches_dir_pattern() to take a strbuf pointer
- 'path_parent' that is used to store the parent directory of 'pathname'
- between multiple pattern matching tests. This is loaded lazily, only on
- the first pattern it finds that has the PATTERN_FLAG_MUSTBEDIR flag.
+ The last_matching_pattern_from_list() logic performs some checks on the
+ filetype of a path within the index when the PATTERN_FLAG_MUSTBEDIR flag
+ is set. This works great when setting SKIP_WORKTREE bits within
+ unpack_trees(), but doesn't work well when passing an arbitrary path
+ such as a file within a matching directory.
+
+ We extract the logic around determining the file type, but attempt to
+ avoid checking the filesystem if the parent directory already matches
+ the sparse-checkout patterns. The new path_matches_dir_pattern() method
+ includes a 'path_parent' parameter that is used to store the parent
+ directory of 'pathname' between multiple pattern matching tests. This is
+ loaded lazily, only on the first pattern it finds that has the
+ PATTERN_FLAG_MUSTBEDIR flag.
If we find that a path has a parent directory, we start by checking to see if that parent directory matches the pattern. If so, then we do not
@@ Commit message
## dir.c ##
@@ dir.c: int match_pathname(const char *pathname, int pathlen,
+ WM_PATHNAME) == 0;
+ }
- static int path_matches_dir_pattern(const char *pathname,
- int pathlen,
++static int path_matches_dir_pattern(const char *pathname,
++ int pathlen,
+ struct strbuf **path_parent,
- int *dtype,
- struct path_pattern *pattern,
- struct index_state *istate)
- {
++ int *dtype,
++ struct path_pattern *pattern,
++ struct index_state *istate)
++{
+ if (!*path_parent) {
+ char *slash;
+ CALLOC_ARRAY(*path_parent, 1);
@@ dir.c: int match_pathname(const char *pathname, int pathlen,
+ pattern->patternlen, pattern->flags))
+ return 1;
+
- *dtype = resolve_dtype(*dtype, istate, pathname, pathlen);
- if (*dtype != DT_DIR)
- return 0;
++ *dtype = resolve_dtype(*dtype, istate, pathname, pathlen);
++ if (*dtype != DT_DIR)
++ return 0;
++
++ return 1;
++}
++
+ /*
+ * Scan the given exclude list in reverse to see whether pathname
+ * should be ignored. The first match (i.e. the last on the list), if
@@ dir.c: static struct path_pattern *last_matching_pattern_from_list(const char *pathname
{
struct path_pattern *res = NULL; /* undecided */
@@ dir.c: static struct path_pattern *last_matching_pattern_from_list(const char *p
const char *exclude = pattern->pattern;
int prefix = pattern->nowildcardlen;
-- if ((pattern->flags & PATTERN_FLAG_MUSTBEDIR) &&
-- !path_matches_dir_pattern(pathname, pathlen,
+- if (pattern->flags & PATTERN_FLAG_MUSTBEDIR) {
+- *dtype = resolve_dtype(*dtype, istate, pathname, pathlen);
+- if (*dtype != DT_DIR)
+- continue;
+- }
+ if (pattern->flags & PATTERN_FLAG_MUSTBEDIR &&
+ !path_matches_dir_pattern(pathname, pathlen, &path_parent,
- dtype, pattern, istate))
- continue;
++ dtype, pattern, istate))
++ continue;
+ if (pattern->flags & PATTERN_FLAG_NODIR) {
+ if (match_basename(basename,
@@ dir.c: static struct path_pattern *last_matching_pattern_from_list(const char *pathname
break;
}
5: c6d17df5e5d = 4: 24bffdab139 dir: fix pattern matching on dirs 6: 3dd1d6c228c = 5: e3a749e3182 add: fail when adding an untracked sparse file 7: 15039e031e5 = 6: 2c5c834bc9f add: skip tracked paths outside sparse-checkout cone 8: 6014ac8ab9e = 7: 430ab44e4f1 add: implement the --sparse option 9: 2bd3448be5f = 8: 4f7b5cdfa36 add: update --chmod to skip sparse paths 10: 131beda1bc3 = 9: 30ec6096939 add: update --renormalize to skip sparse paths 11: 837a9314893 = 10: 99d50921ef4 rm: add --sparse option 12: cc25ce17162 = 11: 47a1444115b rm: skip sparse paths with missing SKIP_WORKTREE 13: 63a9cd80ade = 12: 28e703d80d3 mv: refuse to move sparse paths 14: 79a3518dc15 = 13: 9fbc88ee0da advice: update message to suggest '--sparse'
Signature · 1 line
gitgitgadget
PATCH v4 1/13 t3705: test that 'sparse_entry' is unstaged
From: Derrick Stolee <[email protected]>
The tests in t3705-add-sparse-checkout.sh check to see how 'git add' behaves with paths outside the sparse-checkout definition. These currently check to see if a given warning is present but not that the index is not updated with the sparse entries. Add a new 'test_sparse_entry_unstaged' helper to be sure 'git add' is behaving correctly.
We need to modify setup_sparse_entry to actually commit the sparse_entry file so it exists at HEAD and as an entry in the index, but its exact contents are not staged in the index.
--- t/t3705-add-sparse-checkout.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+)
t/t3705-add-sparse-checkout.sh +14 -0
@@ -19,6 +19,7 @@ setup_sparse_entry () { fi && git add sparse_entry && git update-index --skip-worktree sparse_entry && + git commit --allow-empty -m "ensure sparse_entry exists at HEAD" && SPARSE_ENTRY_BLOB=$(git rev-parse :sparse_entry) }
@@ -36,6 +37,11 @@ setup_gitignore () { EOF } +test_sparse_entry_unstaged () { + git diff --staged -- sparse_entry >diff && + test_must_be_empty diff +} + test_expect_success 'setup' " cat >sparse_error_header <<-EOF && The following pathspecs didn't match any eligible path, but they do match index
@@ -55,6 +61,7 @@ test_expect_success 'git add does not remove sparse entries' ' setup_sparse_entry && rm sparse_entry && test_must_fail git add sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -73,6 +80,7 @@ test_expect_success 'git add . does not remove sparse entries' ' rm sparse_entry && setup_gitignore && test_must_fail git add . 2>stderr && + test_sparse_entry_unstaged && cat sparse_error_header >expect && echo . >>expect &&
@@ -88,6 +96,7 @@ do setup_sparse_entry && echo modified >sparse_entry && test_must_fail git add $opt sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -98,6 +107,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' git ls-files --debug sparse_entry | grep mtime >before && test-tool chmtime -60 sparse_entry && test_must_fail git add --refresh sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && git ls-files --debug sparse_entry | grep mtime >after && test_cmp before after
@@ -106,6 +116,7 @@ test_expect_success 'git add --refresh does not update sparse entries' ' test_expect_success 'git add --chmod does not update sparse entries' ' setup_sparse_entry && test_must_fail git add --chmod=+x sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged && ! test -x sparse_entry
@@ -116,6 +127,7 @@ test_expect_success 'git add --renormalize does not update sparse entries' ' setup_sparse_entry "LINEONE\r\nLINETWO\r\n" && echo "sparse_entry text=auto" >.gitattributes && test_must_fail git add --renormalize sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -124,6 +136,7 @@ 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_sparse_entry_unstaged && test_cmp error_and_hint stderr && test_sparse_entry_unchanged '
@@ -148,6 +161,7 @@ test_expect_success 'do not warn when pathspec matches dense entries' ' test_expect_success 'add obeys advice.updateSparsePath' ' setup_sparse_entry && test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr && + test_sparse_entry_unstaged && test_cmp sparse_entry_error stderr '
Signature · 1 line
gitgitgadget
PATCH v4 2/13 t1092: behavior for adding sparse files
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Add some tests to demonstrate the current behavior around adding files outside of the sparse-checkout cone. Currently, untracked files are handled differently from tracked files. A future change will make these cases be handled the same way.
Further expand checking that a failed 'git add' does not stage changes to the index.
--- t/t1092-sparse-checkout-compatibility.sh | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+)
t/t1092-sparse-checkout-compatibility.sh +28 -0
@@ -187,6 +187,16 @@ test_sparse_match () { test_cmp sparse-checkout-err sparse-index-err } +test_sparse_unstaged () { + file=$1 && + for repo in sparse-checkout sparse-index + do + # Skip "unmerged" paths + git -C $repo diff --staged --diff-filter=ACDMRTXB -- "$file" >diff && + test_must_be_empty diff || return 1 + done +} + test_expect_success 'sparse-index contents' ' init_repos &&
@@ -291,6 +301,20 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' +# NEEDSWORK: This documents current behavior, but is not a desirable +# behavior (untracked files are handled differently than tracked). +test_expect_success 'add outside sparse cone' ' + init_repos && + + run_on_sparse mkdir folder1 && + run_on_sparse ../edit-contents folder1/a && + run_on_sparse ../edit-contents folder1/newfile && + test_sparse_match test_must_fail git add folder1/a && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && + test_sparse_match git add folder1/newfile +' + test_expect_success 'commit including unstaged changes' ' init_repos &&
@@ -339,7 +363,11 @@ test_expect_success 'status/add: outside sparse cone' ' # Adding the path outside of the sparse-checkout cone should fail. test_sparse_match test_must_fail git add folder1/a && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && test_sparse_match test_must_fail git add --refresh folder1/a && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && # NEEDSWORK: Adding a newly-tracked file outside the cone succeeds test_sparse_match git add folder1/new &&
Signature · 1 line
gitgitgadget
PATCH v4 3/13 dir: select directories correctly
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When matching a path against a list of patterns, the ones that require a directory match previously did not work when a filename is specified. This was fine when all pattern-matching was done within methods such as unpack_trees() that check a directory before recursing into the contained files. However, other commands will start matching individual files against pattern lists without that recursive approach.
The last_matching_pattern_from_list() logic performs some checks on the filetype of a path within the index when the PATTERN_FLAG_MUSTBEDIR flag is set. This works great when setting SKIP_WORKTREE bits within unpack_trees(), but doesn't work well when passing an arbitrary path such as a file within a matching directory.
We extract the logic around determining the file type, but attempt to avoid checking the filesystem if the parent directory already matches the sparse-checkout patterns. The new path_matches_dir_pattern() method includes a 'path_parent' parameter that is used to store the parent directory of 'pathname' between multiple pattern matching tests. This is loaded lazily, only on the first pattern it finds that has the PATTERN_FLAG_MUSTBEDIR flag.
If we find that a path has a parent directory, we start by checking to see if that parent directory matches the pattern. If so, then we do not need to query the index for the type (which can be expensive). If we find that the parent does not match, then we still must check the type from the index for the given pathname.
Note that this does not affect cone mode pattern matching, but instead the more general -- and slower -- full pattern set. Thus, this does not affect the sparse index.
--- dir.c | 54 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-)
dir.c +49 -5
@@ -1303,6 +1303,44 @@ int match_pathname(const char *pathname, int pathlen, WM_PATHNAME) == 0; } +static int path_matches_dir_pattern (const char *pathname, +int pathlen, +struct strbuf **path_parent, +int *dtype, +struct path_pattern *pattern, +struct index_state *istate) +{ +if (!*path_parent) { +char *slash; +CALLOC_ARRAY (*path_parent,1 ); +strbuf_add (*path_parent, pathname, pathlen); + slash =find_last_dir_sep ((*path_parent)->buf); + +if (slash) +strbuf_setlen (*path_parent, slash - (*path_parent)->buf); +else +strbuf_setlen (*path_parent,0 ); + } + +/* +* If the parent directory matches the pattern, then we do not +* need to check for dtype. +*/ +if ((*path_parent)->len && +match_pathname ((*path_parent)->buf, (*path_parent)->len, + pattern->base, + pattern->baselen ? pattern->baselen -1 :0 , + pattern->pattern, pattern->nowildcardlen, + pattern->patternlen, pattern->flags)) +return 1 ; + + *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); +if (*dtype !=DT_DIR ) +return 0 ; + +return 1 ; +} +/* * Scan the given exclude list in reverse to see whether pathname * should be ignored. The first match (i.e. the last on the list), if
@@ -1318,6 +1356,7 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathname {struct path_pattern *res =NULL ;/* undecided */ int i; +struct strbuf *path_parent =NULL ;if (!pl->nr)return NULL ;/* undefined */
@@ -1327,11 +1366,10 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnameconst char *exclude = pattern->pattern;int prefix = pattern->nowildcardlen; -if (pattern->flags &PATTERN_FLAG_MUSTBEDIR ) { - *dtype =resolve_dtype (*dtype, istate, pathname, pathlen); -if (*dtype !=DT_DIR ) -continue ; - } +if (pattern->flags &PATTERN_FLAG_MUSTBEDIR && + !path_matches_dir_pattern (pathname, pathlen, &path_parent, + dtype, pattern, istate)) +continue ;if (pattern->flags &PATTERN_FLAG_NODIR ) { if (match_basename(basename,
@@ -1355,6 +1393,12 @@ static struct path_pattern *last_matching_pattern_from_list(const char *pathnamebreak ; } } + +if (path_parent) { +strbuf_release (path_parent); +free (path_parent); + } +return res; }
Signature · 1 line
gitgitgadget
PATCH v4 4/13 dir: fix pattern matching on dirs
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Within match_pathname(), one successful matching category happens when the pattern is equal to its non-wildcard prefix. At this point, we have checked that the input 'pathname' matches the pattern up to the prefix length, and then we subtraced that length from both 'patternlen' and 'namelen'.
In the case of a directory match, this prefix match should be sufficient. However, the success condition only cared about _exact_ equality here. Instead, we should allow any path that agrees on this prefix in the case of PATTERN_FLAG_MUSTBEDIR.
This case was not tested before because of the way unpack_trees() would match a parent directory before visiting the contained paths. This approach is changing, so we must change this comparison.
--- dir.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
dir.c +1 -1
@@ -1294,7 +1294,7 @@ int match_pathname(const char *pathname, int pathlen,* then our prefix match is all we need; we * do not need to call fnmatch at all. */ -if (!patternlen && !namelen) +if (!patternlen && (!namelen || (flags &PATTERN_FLAG_MUSTBEDIR )))return 1 ; }
Signature · 1 line
gitgitgadget
PATCH v4 5/13 add: fail when adding an untracked sparse file
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The add_files() method in builtin/add.c takes a set of untracked files that are being added by the input pathspec and inserts them into the index. If these files are outside of the sparse-checkout cone, then they gain the SKIP_WORKTREE bit at some point. However, this was not checked before inserting into the index, so these files are added even though we want to avoid modifying the index outside of the sparse-checkout cone.
Add a check within add_files() for these files and write the advice about files outside of the sparse-checkout cone.
This behavior change modifies some existing tests within t1092. These tests intended to document how a user could interact with the existing behavior in place. Many of these tests need to be marked as expecting failure. A future change will allow these tests to pass by adding a flag to 'git add' that allows users to modify index entries outside of the sparse-checkout cone.
The 'submodule handling' test is intended to document what happens to directories that contain a submodule when the sparse index is enabled. It is not trying to say that users should be able to add submodules outside of the sparse-checkout cone, so that test can be modified to avoid that operation.
--- builtin/add.c | 14 +++++++++ t/t1092-sparse-checkout-compatibility.sh | 37 ++++++++++++++++++------ 2 files changed, 42 insertions(+), 9 deletions(-)
builtin/add.c +14 -0
@@ -443,6 +443,7 @@ static void check_embedded_repo(const char *path)static int add_files (struct dir_struct *dir,int flags) {int i, exit_status =0 ; +struct string_list matched_sparse_paths =STRING_LIST_INIT_NODUP ;if (dir->ignored_nr) {fprintf (stderr,_ (ignore_error));
@@ -456,6 +457,11 @@ static int add_files(struct dir_struct *dir, int flags) }for (i =0 ; i < dir->nr; i++) { +if (!path_in_sparse_checkout (dir->entries[i]->name, &the_index)) { +string_list_append (&matched_sparse_paths, + dir->entries[i]->name); +continue ; + }if (add_file_to_index (&the_index, dir->entries[i]->name, flags)) {if (!ignore_add_errors)die (_ ("adding files failed" ));
@@ -464,6 +470,14 @@ static int add_files(struct dir_struct *dir, int flags)check_embedded_repo (dir->entries[i]->name); } } + +if (matched_sparse_paths.nr) { +advise_on_updating_sparse_paths (&matched_sparse_paths); + exit_status =1 ; + } + +string_list_clear (&matched_sparse_paths,0 ); +return exit_status; }
t/t1092-sparse-checkout-compatibility.sh +28 -9
@@ -301,8 +301,6 @@ test_expect_success 'add, commit, checkout' ' test_all_match git checkout - ' -# NEEDSWORK: This documents current behavior, but is not a desirable -# behavior (untracked files are handled differently than tracked). test_expect_success 'add outside sparse cone' ' init_repos &&
@@ -312,7 +310,9 @@ test_expect_success 'add outside sparse cone' ' test_sparse_match test_must_fail git add folder1/a && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && - test_sparse_match git add folder1/newfile + test_sparse_match test_must_fail git add folder1/newfile && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/newfile ' test_expect_success 'commit including unstaged changes' '
@@ -343,7 +343,11 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -test_expect_success 'status/add: outside sparse cone' ' +# NEEDSWORK: Now that 'git add folder1/new' fails, the changes being +# attempted here fail for the sparse-checkout and sparse-index repos. +# We must enable a way for adding files outside the sparse-checkout +# done, even if it is by an optional flag. +test_expect_failure 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -368,10 +372,11 @@ test_expect_success 'status/add: outside sparse cone' ' test_sparse_match test_must_fail git add --refresh folder1/a && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && + test_sparse_match test_must_fail git add folder1/new && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/new && - # NEEDSWORK: Adding a newly-tracked file outside the cone succeeds - test_sparse_match git add folder1/new && - + # NEEDSWORK: behavior begins to deviate here. test_all_match git add . && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/new &&
@@ -527,7 +532,7 @@ test_expect_success 'merge, cherry-pick, and rebase' ' # Right now, users might be using this flow to work through conflicts, # so any solution should present advice to users who try this sequence # of commands to follow whatever new method we create. -test_expect_success 'merge with conflict outside cone' ' +test_expect_failure 'merge with conflict outside cone' ' init_repos && test_all_match git checkout -b merge-tip merge-left &&
@@ -541,12 +546,18 @@ test_expect_success 'merge with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers + # NEEDSWORK: Even though the merge conflict removed the + # SKIP_WORKTREE bit from the index entry for folder1/a, we should + # warn that this is a problematic add. test_all_match git add folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. run_on_all mv folder2/a folder2/z && + # NEEDSWORK: This mode now fails, because folder2/z is + # outside of the sparse-checkout cone and does not match an + # existing index entry with the SKIP_WORKTREE bit cleared. test_all_match git add folder2 && test_all_match git status --porcelain=v2 &&
@@ -555,7 +566,7 @@ test_expect_success 'merge with conflict outside cone' ' test_all_match git rev-parse HEAD^{tree} ' -test_expect_success 'cherry-pick/rebase with conflict outside cone' ' +test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' init_repos && for OPERATION in cherry-pick rebase
@@ -572,11 +583,17 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers + # NEEDSWORK: Even though the merge conflict removed the + # SKIP_WORKTREE bit from the index entry for folder1/a, we should + # warn that this is a problematic add. test_all_match git add folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. + # NEEDSWORK: This mode now fails, because folder2/z is + # outside of the sparse-checkout cone and does not match an + # existing index entry with the SKIP_WORKTREE bit cleared. run_on_all mv folder2/a folder2/z && test_all_match git add folder2 && test_all_match git status --porcelain=v2 &&
@@ -654,6 +671,7 @@ test_expect_success 'clean' ' test_expect_success 'submodule handling' ' init_repos && + test_sparse_match git sparse-checkout add modules && test_all_match mkdir modules && test_all_match touch modules/a && test_all_match git add modules &&
@@ -663,6 +681,7 @@ test_expect_success 'submodule handling' ' test_all_match git commit -m "add submodule" && # having a submodule prevents "modules" from collapse + test_sparse_match git sparse-checkout set deep/deeper1 && test-tool -C sparse-index read-cache --table >cache && grep "100644 blob .* modules/a" cache && grep "160000 commit $(git -C initial-repo rev-parse HEAD) modules/sub" cache
Signature · 1 line
gitgitgadget
PATCH v4 6/13 add: skip tracked paths outside sparse-checkout cone
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When 'git add' adds a tracked file that is outside of the sparse-checkout cone, it checks the SKIP_WORKTREE bit to see if the file exists outside of the sparse-checkout cone. This is usually correct, except in the case of a merge conflict outside of the cone.
Modify add_pathspec_matched_against_index() to be more careful about paths by checking the sparse-checkout patterns in addition to the SKIP_WORKTREE bit. This causes 'git add' to no longer allow files outside of the cone that removed the SKIP_WORKTREE bit due to a merge conflict.
With only this change, users will only be able to add the file after adding the file to the sparse-checkout cone. A later change will allow users to force adding even though the file is outside of the sparse-checkout cone.
--- builtin/add.c | 4 ++++ pathspec.c | 5 +++-- t/t1091-sparse-checkout-builtin.sh | 4 +++- t/t1092-sparse-checkout-compatibility.sh | 19 ++++++++++++------- t/t3705-add-sparse-checkout.sh | 12 ++++++++++++ 5 files changed, 34 insertions(+), 10 deletions(-)
builtin/add.c +4 -0
@@ -94,6 +94,10 @@ static void update_callback(struct diff_queue_struct *q,for (i =0 ; i < q->nr; i++) {struct diff_filepair *p = q->queue[i];const char *path = p->one->path; + +if (!path_in_sparse_checkout (path, &the_index)) +continue ; +switch (fix_unmerged_status (p, data)) {default :die (_ ("unexpected diff status %c" ), p->status);
pathspec.c +3 -2
@@ -39,7 +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)) +if (sw_action ==PS_IGNORE_SKIP_WORKTREE && + (ce_skip_worktree (ce) || !path_in_sparse_checkout (ce->name, istate)))continue ;ce_path_match (istate, ce, pathspec, seen); }
@@ -70,7 +71,7 @@ char *find_pathspecs_matching_skip_worktree(const struct pathspec *pathspec)for (i =0 ; i < istate->cache_nr; i++) {struct cache_entry *ce = istate->cache[i]; -if (ce_skip_worktree (ce)) +if (ce_skip_worktree (ce) || !path_in_sparse_checkout (ce->name, istate))ce_path_match (istate, ce, pathspec, seen); }
t/t1091-sparse-checkout-builtin.sh +3 -1
@@ -406,7 +406,7 @@ test_expect_success 'sparse-checkout (init|set|disable) warns with unmerged stat git -C unmerged sparse-checkout disable ' -test_expect_success 'sparse-checkout reapply' ' +test_expect_failure 'sparse-checkout reapply' ' git clone repo tweak && echo dirty >tweak/deep/deeper2/a &&
@@ -438,6 +438,8 @@ test_expect_success 'sparse-checkout reapply' ' test_i18ngrep "warning.*The following paths are unmerged" err && test_path_is_file tweak/folder1/a && + # NEEDSWORK: We are asking to update a file outside of the + # sparse-checkout cone, but this is no longer allowed. git -C tweak add folder1/a && git -C tweak sparse-checkout reapply 2>err && test_must_be_empty err &&
t/t1092-sparse-checkout-compatibility.sh +12 -7
@@ -546,10 +546,9 @@ test_expect_failure 'merge with conflict outside cone' ' test_all_match git status --porcelain=v2 && # 2. Add the file with conflict markers - # NEEDSWORK: Even though the merge conflict removed the - # SKIP_WORKTREE bit from the index entry for folder1/a, we should - # warn that this is a problematic add. - test_all_match git add folder1/a && + test_sparse_match test_must_fail git add folder1/a && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -558,7 +557,9 @@ test_expect_failure 'merge with conflict outside cone' ' # NEEDSWORK: This mode now fails, because folder2/z is # outside of the sparse-checkout cone and does not match an # existing index entry with the SKIP_WORKTREE bit cleared. - test_all_match git add folder2 && + test_sparse_match test_must_fail git add folder2 && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder2/z && test_all_match git status --porcelain=v2 && test_all_match git merge --continue &&
@@ -586,7 +587,9 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' # NEEDSWORK: Even though the merge conflict removed the # SKIP_WORKTREE bit from the index entry for folder1/a, we should # warn that this is a problematic add. - test_all_match git add folder1/a && + test_sparse_match test_must_fail git add folder1/a && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -595,7 +598,9 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' # outside of the sparse-checkout cone and does not match an # existing index entry with the SKIP_WORKTREE bit cleared. run_on_all mv folder2/a folder2/z && - test_all_match git add folder2 && + test_sparse_match test_must_fail git add folder2 && + grep "Disable or modify the sparsity rules" sparse-checkout-err && + test_sparse_unstaged folder2/z && test_all_match git status --porcelain=v2 && test_all_match git $OPERATION --continue &&
t/t3705-add-sparse-checkout.sh +12 -0
@@ -158,6 +158,18 @@ test_expect_success 'do not warn when pathspec matches dense entries' ' git ls-files --error-unmatch dense_entry ' +test_expect_success 'git add fails outside of sparse-checkout definition' ' + test_when_finished git sparse-checkout disable && + test_commit a && + git sparse-checkout init && + git sparse-checkout set a && + echo >>sparse_entry && + + git update-index --no-skip-worktree sparse_entry && + test_must_fail git add sparse_entry && + test_sparse_entry_unstaged +' + test_expect_success 'add obeys advice.updateSparsePath' ' setup_sparse_entry && test_must_fail git -c advice.updateSparsePath=false add sparse_entry 2>stderr &&
Signature · 1 line
gitgitgadget
PATCH v4 7/13 add: implement the --sparse option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
We previously modified 'git add' to refuse updating index entries outside of the sparse-checkout cone. This is justified to prevent users from accidentally getting into a confusing state when Git removes those files from the working tree at some later point.
Unfortunately, this caused some workflows that were previously possible to become impossible, especially around merge conflicts outside of the sparse-checkout cone. These were documented in tests within t1092.
We now re-enable these workflows using a new '--sparse' option to 'git add'. This allows users to signal "Yes, I do know what I'm doing with these files," and accept the consequences of the files leaving the worktree later.
We delay updating the advice message until implementing a similar option in 'git rm' and 'git mv'.
--- Documentation/git-add.txt | 9 +++++++- builtin/add.c | 12 ++++++---- t/t1092-sparse-checkout-compatibility.sh | 29 +++++++++--------------- t/t3705-add-sparse-checkout.sh | 17 +++++++++++++- 4 files changed, 43 insertions(+), 24 deletions(-)
Documentation/git-add.txt +8 -1
@@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git add' [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p] - [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] + [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] [--sparse] [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize] [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]
@@ -79,6 +79,13 @@ in linkgit:gitglossary[7]. --force:: Allow adding otherwise ignored files. +--sparse:: + Allow updating index entries outside of the sparse-checkout cone. + Normally, `git add` refuses to update index entries whose paths do + not fit within the sparse-checkout cone, since those files might + be removed from the working tree without warning. See + linkgit:git-sparse-checkout[1] for more details. + -i:: --interactive:: Add modified contents in the working tree interactively to
builtin/add.c +8 -4
@@ -30,6 +30,7 @@ static int patch_interactive, add_interactive, edit_interactive;static int take_worktree_changes;static int add_renormalize;static int pathspec_file_nul; +static int include_sparse;static const char *pathspec_from_file;static int legacy_stash_p;/* support for the scripted `git stash` */
@@ -46,7 +47,7 @@ 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)) +if (!include_sparse &&ce_skip_worktree (ce))continue ;if (pathspec && !ce_path_match (&the_index, ce, pathspec,NULL ))
@@ -95,7 +96,7 @@ static void update_callback(struct diff_queue_struct *q,struct diff_filepair *p = q->queue[i];const char *path = p->one->path; -if (!path_in_sparse_checkout (path, &the_index)) +if (!include_sparse && !path_in_sparse_checkout (path, &the_index))continue ;switch (fix_unmerged_status (p, data)) {
@@ -383,6 +384,7 @@ static struct option builtin_add_options[] = {OPT_BOOL (0 ,"refresh" , &refresh_only,N_ ("don't add, only refresh the index" )),OPT_BOOL (0 ,"ignore-errors" , &ignore_add_errors,N_ ("just skip files which cannot be added because of errors" )),OPT_BOOL (0 ,"ignore-missing" , &ignore_missing,N_ ("check if - even missing - files are ignored in dry run" )), +OPT_BOOL (0 ,"sparse" , &include_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_STRING (0 ,"chmod" , &chmod_arg,"(+|-)x" ,N_ ("override the executable bit of the listed files" )), OPT_HIDDEN_BOOL(0, "warn-embedded-repo", &warn_on_embedded_repo,
@@ -461,7 +463,8 @@ static int add_files(struct dir_struct *dir, int flags) }for (i =0 ; i < dir->nr; i++) { -if (!path_in_sparse_checkout (dir->entries[i]->name, &the_index)) { +if (!include_sparse && + !path_in_sparse_checkout (dir->entries[i]->name, &the_index)) {string_list_append (&matched_sparse_paths, dir->entries[i]->name);continue ;
@@ -646,7 +649,8 @@ int cmd_add(int argc, const char **argv, const char *prefix)if (seen[i])continue ; -if (matches_skip_worktree (&pathspec, i, &skip_worktree_seen)) { +if (!include_sparse && +matches_skip_worktree (&pathspec, i, &skip_worktree_seen)) {string_list_append (&only_match_skip_worktree, pathspec.items[i].original);continue ;
t/t1092-sparse-checkout-compatibility.sh +11 -18
@@ -343,11 +343,7 @@ test_expect_success 'commit including unstaged changes' ' test_all_match git status --porcelain=v2 ' -# NEEDSWORK: Now that 'git add folder1/new' fails, the changes being -# attempted here fail for the sparse-checkout and sparse-index repos. -# We must enable a way for adding files outside the sparse-checkout -# done, even if it is by an optional flag. -test_expect_failure 'status/add: outside sparse cone' ' +test_expect_success 'status/add: outside sparse cone' ' init_repos && # folder1 is at HEAD, but outside the sparse cone
@@ -375,15 +371,16 @@ test_expect_failure 'status/add: outside sparse cone' ' test_sparse_match test_must_fail git add folder1/new && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/new && + test_sparse_match git add --sparse folder1/a && + test_sparse_match git add --sparse folder1/new && - # NEEDSWORK: behavior begins to deviate here. - test_all_match git add . && + test_all_match git add --sparse . && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/new && test_all_match git rev-parse HEAD^{tree} && run_on_all ../edit-contents folder1/newer && - test_all_match git add folder1/ && + test_all_match git add --sparse folder1/ && test_all_match git status --porcelain=v2 && test_all_match git commit -m folder1/newer && test_all_match git rev-parse HEAD^{tree}
@@ -527,12 +524,7 @@ test_expect_success 'merge, cherry-pick, and rebase' ' done ' -# NEEDSWORK: This test is documenting current behavior, but that -# behavior can be confusing to users so there is desire to change it. -# Right now, users might be using this flow to work through conflicts, -# so any solution should present advice to users who try this sequence -# of commands to follow whatever new method we create. -test_expect_failure 'merge with conflict outside cone' ' +test_expect_success 'merge with conflict outside cone' ' init_repos && test_all_match git checkout -b merge-tip merge-left &&
@@ -549,17 +541,16 @@ test_expect_failure 'merge with conflict outside cone' ' test_sparse_match test_must_fail git add folder1/a && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && + test_all_match git add --sparse folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and # accept conflict markers as resolved content. run_on_all mv folder2/a folder2/z && - # NEEDSWORK: This mode now fails, because folder2/z is - # outside of the sparse-checkout cone and does not match an - # existing index entry with the SKIP_WORKTREE bit cleared. test_sparse_match test_must_fail git add folder2 && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder2/z && + test_all_match git add --sparse folder2 && test_all_match git status --porcelain=v2 && test_all_match git merge --continue &&
@@ -567,7 +558,7 @@ test_expect_failure 'merge with conflict outside cone' ' test_all_match git rev-parse HEAD^{tree} ' -test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' +test_expect_success 'cherry-pick/rebase with conflict outside cone' ' init_repos && for OPERATION in cherry-pick rebase
@@ -590,6 +581,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' test_sparse_match test_must_fail git add folder1/a && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder1/a && + test_all_match git add --sparse folder1/a && test_all_match git status --porcelain=v2 && # 3. Rename the file to another sparse filename and
@@ -601,6 +593,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' ' test_sparse_match test_must_fail git add folder2 && grep "Disable or modify the sparsity rules" sparse-checkout-err && test_sparse_unstaged folder2/z && + test_all_match git add --sparse folder2 && test_all_match git status --porcelain=v2 && test_all_match git $OPERATION --continue &&
t/t3705-add-sparse-checkout.sh +16 -1
@@ -167,7 +167,13 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' git update-index --no-skip-worktree sparse_entry && test_must_fail git add sparse_entry && - test_sparse_entry_unstaged + test_sparse_entry_unstaged && + + # Avoid munging CRLFs to avoid an error message + git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && + test_must_be_empty stderr && + test-tool read-cache --table >actual && + grep "^100644 blob.*sparse_entry\$" actual ' test_expect_success 'add obeys advice.updateSparsePath' '
@@ -178,4 +184,13 @@ test_expect_success 'add obeys advice.updateSparsePath' ' ' +test_expect_success 'add allows sparse entries with --sparse' ' + git sparse-checkout set a && + echo modified >sparse_entry && + test_must_fail git add sparse_entry && + test_sparse_entry_unchanged && + git add --sparse sparse_entry 2>stderr && + test_must_be_empty stderr +' + test_done
Signature · 1 line
gitgitgadget
PATCH v4 8/13 add: update --chmod to skip sparse paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
We added checks for path_in_sparse_checkout() to portions of 'git add' that add warnings and prevent staging a modification, but we skipped the --chmod mode. Update chmod_pathspec() to ignore cache entries whose path is outside of the sparse-checkout cone (unless --sparse is provided). Add a test in t3705.
--- builtin/add.c | 4 +++- t/t3705-add-sparse-checkout.sh | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-)
builtin/add.c +3 -1
@@ -47,7 +47,9 @@ static int chmod_pathspec(struct pathspec *pathspec, char flip, int show_only)struct cache_entry *ce = active_cache[i];int err; -if (!include_sparse &&ce_skip_worktree (ce)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;if (pathspec && !ce_path_match (&the_index, ce, pathspec,NULL ))
t/t3705-add-sparse-checkout.sh +9 -1
@@ -169,11 +169,19 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' test_must_fail git add sparse_entry && test_sparse_entry_unstaged && + test_must_fail git add --chmod=+x sparse_entry && + test_sparse_entry_unstaged && + # Avoid munging CRLFs to avoid an error message git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && test_must_be_empty stderr && test-tool read-cache --table >actual && - grep "^100644 blob.*sparse_entry\$" actual + grep "^100644 blob.*sparse_entry\$" actual && + + git add --sparse --chmod=+x sparse_entry 2>stderr && + test_must_be_empty stderr && + test-tool read-cache --table >actual && + grep "^100755 blob.*sparse_entry\$" actual ' test_expect_success 'add obeys advice.updateSparsePath' '
Signature · 1 line
gitgitgadget
PATCH v4 9/13 add: update --renormalize to skip sparse paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
We added checks for path_in_sparse_checkout() to portions of 'git add' that add warnings and prevent stagins a modification, but we skipped the --renormalize mode. Update renormalize_tracked_files() to ignore cache entries whose path is outside of the sparse-checkout cone (unless --sparse is provided). Add a test in t3705.
--- builtin/add.c | 4 +++- t/t3705-add-sparse-checkout.sh | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-)
builtin/add.c +3 -1
@@ -154,7 +154,9 @@ 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)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;if (ce_stage (ce))continue ;/* do not touch unmerged paths */
t/t3705-add-sparse-checkout.sh +11 -1
@@ -172,6 +172,9 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' test_must_fail git add --chmod=+x sparse_entry && test_sparse_entry_unstaged && + test_must_fail git add --renormalize sparse_entry && + test_sparse_entry_unstaged && + # Avoid munging CRLFs to avoid an error message git -c core.autocrlf=input add --sparse sparse_entry 2>stderr && test_must_be_empty stderr &&
@@ -181,7 +184,14 @@ test_expect_success 'git add fails outside of sparse-checkout definition' ' git add --sparse --chmod=+x sparse_entry 2>stderr && test_must_be_empty stderr && test-tool read-cache --table >actual && - grep "^100755 blob.*sparse_entry\$" actual + grep "^100755 blob.*sparse_entry\$" actual && + + git reset && + + # This will print a message over stderr on Windows. + git add --sparse --renormalize sparse_entry && + git status --porcelain >actual && + grep "^M sparse_entry\$" actual ' test_expect_success 'add obeys advice.updateSparsePath' '
Signature · 1 line
gitgitgadget
PATCH v4 10/13 rm: add --sparse option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
As we did previously in 'git add', add a '--sparse' option to 'git rm' that allows modifying paths outside of the sparse-checkout definition. The existing checks in 'git rm' are restricted to tracked files that have the SKIP_WORKTREE bit in the current index. Future changes will cause 'git rm' to reject removing paths outside of the sparse-checkout definition, even if they are untracked or do not have the SKIP_WORKTREE bit.
--- Documentation/git-rm.txt | 6 ++++++ builtin/rm.c | 8 ++++++-- t/t3602-rm-sparse-checkout.sh | 12 ++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-)
Documentation/git-rm.txt +6 -0
@@ -72,6 +72,12 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7]. --ignore-unmatch:: Exit with a zero status even if no files matched. +--sparse:: + Allow updating index entries outside of the sparse-checkout cone. + Normally, `git rm` refuses to update index entries whose paths do + not fit within the sparse-checkout cone. See + linkgit:git-sparse-checkout[1] for more. + -q:: --quiet:: `git rm` normally outputs one line (in the form of an `rm` command)
builtin/rm.c +6 -2
@@ -237,6 +237,7 @@ static int check_local_mod(struct object_id *head, int index_only)static int show_only =0 , force =0 , index_only =0 , recursive =0 , quiet =0 ;static int ignore_unmatch =0 , pathspec_file_nul; +static int include_sparse;static char *pathspec_from_file;static struct option builtin_rm_options[] = {
@@ -247,6 +248,7 @@ static struct option builtin_rm_options[] = {OPT_BOOL ('r' ,NULL , &recursive,N_ ("allow recursive removal" )),OPT_BOOL (0 ,"ignore-unmatch" , &ignore_unmatch,N_ ("exit with a zero status even if nothing matched" )), +OPT_BOOL (0 ,"sparse" , &include_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_PATHSPEC_FROM_FILE (&pathspec_from_file),OPT_PATHSPEC_FILE_NUL (&pathspec_file_nul), OPT_END(),
@@ -298,7 +300,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix)ensure_full_index (&the_index);for (i =0 ; i < active_nr; i++) {const struct cache_entry *ce = active_cache[i]; -if (ce_skip_worktree (ce)) + +if (!include_sparse &&ce_skip_worktree (ce))continue ;if (!ce_path_match (&the_index, ce, &pathspec, seen))continue ;
@@ -322,7 +325,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix) seen_any =1 ; else if (ignore_unmatch) continue; - else if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) + else if (!include_sparse && + 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);
t/t3602-rm-sparse-checkout.sh +12 -0
@@ -43,6 +43,18 @@ test_expect_success 'recursive rm does not remove sparse entries' ' test_cmp expected actual ' +test_expect_success 'recursive rm --sparse removes sparse entries' ' + git reset --hard && + git sparse-checkout set "sub/dir" && + git rm --sparse -r sub && + git status --porcelain -uno >actual && + cat >expected <<-\EOF && + D sub/d + D sub/dir/e + EOF + test_cmp expected actual +' + test_expect_success 'rm obeys advice.updateSparsePath' ' git reset --hard && git sparse-checkout set a &&
Signature · 1 line
gitgitgadget
PATCH v4 11/13 rm: skip sparse paths with missing SKIP_WORKTREE
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
If a path does not match the sparse-checkout cone but is somehow missing the SKIP_WORKTREE bit, then 'git rm' currently succeeds in removing the file. One reason a user might be in this situation is a merge conflict outside of the sparse-checkout cone. Removing such a file might be problematic for users who are not sure what they are doing.
Add a check to path_in_sparse_checkout() when 'git rm' is checking if a path should be considered for deletion. Of course, this check is ignored if the '--sparse' option is specified, allowing users who accept the risks to continue with the removal.
This also removes a confusing behavior where a user asks for a directory to be removed, but only the entries that are within the sparse-checkout definition are removed. Now, 'git rm <dir>' will fail without '--sparse' and will succeed in removing all contained paths with '--sparse'.
--- builtin/rm.c | 4 +++- t/t3602-rm-sparse-checkout.sh | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-)
builtin/rm.c +3 -1
@@ -301,7 +301,9 @@ 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 (!include_sparse &&ce_skip_worktree (ce)) +if (!include_sparse && + (ce_skip_worktree (ce) || + !path_in_sparse_checkout (ce->name, &the_index)))continue ;if (!ce_path_match (&the_index, ce, &pathspec, seen))continue ;
t/t3602-rm-sparse-checkout.sh +17 -2
@@ -37,9 +37,13 @@ done test_expect_success 'recursive rm does not remove sparse entries' ' git reset --hard && git sparse-checkout set sub/dir && - git rm -r sub && + test_must_fail git rm -r sub && + git rm --sparse -r sub && git status --porcelain -uno >actual && - echo "D sub/dir/e" >expected && + cat >expected <<-\EOF && + D sub/d + D sub/dir/e + EOF test_cmp expected actual '
@@ -87,4 +91,15 @@ test_expect_success 'do not warn about sparse entries with --ignore-unmatch' ' git ls-files --error-unmatch b ' +test_expect_success 'refuse to rm a non-skip-worktree path outside sparse cone' ' + git reset --hard && + git sparse-checkout set a && + git update-index --no-skip-worktree b && + test_must_fail git rm b 2>stderr && + test_cmp b_error_and_hint stderr && + git rm --sparse b 2>stderr && + test_must_be_empty stderr && + test_path_is_missing b +' + test_done
Signature · 1 line
gitgitgadget
PATCH v4 12/13 mv: refuse to move sparse paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Since cmd_mv() does not operate on cache entries and instead directly checks the filesystem, we can only use path_in_sparse_checkout() as a mechanism for seeing if a path is sparse or not. Be sure to skip returning a failure if '-k' is specified.
To ensure that the advice around sparse paths is the only reason a move failed, be sure to check this as the very last thing before inserting into the src_for_dst list.
The tests cover a variety of cases such as whether the target is tracked or untracked, and whether the source or destination are in or outside of the sparse-checkout definition.
--- builtin/mv.c | 52 ++++++++-- t/t7002-mv-sparse-checkout.sh | 186 ++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 9 deletions(-) create mode 100755 t/t7002-mv-sparse-checkout.sh
builtin/mv.c +43 -9
@@ -118,21 +118,23 @@ static int index_range_of_same_dir(const char *src, int length,int cmd_mv (int argc,const char **argv,const char *prefix) {int i, flags, gitmodules_modified =0 ; -int verbose =0 , show_only =0 , force =0 , ignore_errors =0 ; +int verbose =0 , show_only =0 , force =0 , ignore_errors =0 , ignore_sparse =0 ;struct option builtin_mv_options[] = {OPT__VERBOSE (&verbose,N_ ("be verbose" )),OPT__DRY_RUN (&show_only,N_ ("dry run" )),OPT__FORCE (&force,N_ ("force move/rename even if target exists" ),PARSE_OPT_NOCOMPLETE ),OPT_BOOL ('k' ,NULL , &ignore_errors,N_ ("skip move/rename errors" )), +OPT_BOOL (0 ,"sparse" , &ignore_sparse,N_ ("allow updating entries outside of the sparse-checkout cone" )),OPT_END (), };const char **source, **destination, **dest_path, **submodule_gitfile; -enum update_mode {BOTH =0 ,WORKING_DIRECTORY ,INDEX } *modes; +enum update_mode {BOTH =0 ,WORKING_DIRECTORY ,INDEX ,SPARSE } *modes;struct stat st;struct string_list src_for_dst =STRING_LIST_INIT_NODUP ;struct lock_file lock_file =LOCK_INIT ;struct cache_entry *ce; +struct string_list only_match_skip_worktree =STRING_LIST_INIT_NODUP ;git_config (git_default_config,NULL );
@@ -176,14 +178,17 @@ int cmd_mv(int argc, const char **argv, const char *prefix)const char *src = source[i], *dst = destination[i];int length, src_is_dir;const char *bad =NULL ; +int skip_sparse =0 ;if (show_only)printf (_ ("Checking rename of '%s' to '%s'\n" ), src, dst); length =strlen (src); -if (lstat (src, &st) <0 ) - bad =_ ("bad source" ); -else if (!strncmp (src, dst, length) && +if (lstat (src, &st) <0 ) { +/* only error if existence is expected. */ +if (modes[i] !=SPARSE ) + bad =_ ("bad source" ); + }else if (!strncmp (src, dst, length) && (dst[length] ==0 || dst[length] =='/' )) { bad =_ ("can not move directory into itself" ); } else if ((src_is_dir = S_ISDIR(st.st_mode))
@@ -212,11 +217,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix) dst_len =strlen (dst);for (j =0 ; j < last - first; j++) { -const char *path = active_cache[first + j]->name; +const struct cache_entry *ce = active_cache[first + j]; +const char *path = ce->name; source[argc + j] = path; destination[argc + j] =prefix_path (dst, dst_len, path + length +1 ); - modes[argc + j] =INDEX ; + modes[argc + j] =ce_skip_worktree (ce) ?SPARSE :INDEX ; submodule_gitfile[argc + j] =NULL ; } argc += last - first;
@@ -244,14 +250,36 @@ int cmd_mv(int argc, const char **argv, const char *prefix) bad =_ ("multiple sources for the same target" ); else if (is_dir_sep(dst[strlen(dst) - 1])) bad =_ ("destination directory does not exist" ); - else + else { +/* +* We check if the paths are in the sparse-checkout +* definition as a very final check, since that +* allows us to point the user to the --sparse +* option as a way to have a successful run. +*/ +if (!ignore_sparse && + !path_in_sparse_checkout (src, &the_index)) { +string_list_append (&only_match_skip_worktree, src); + skip_sparse =1 ; + } +if (!ignore_sparse && + !path_in_sparse_checkout (dst, &the_index)) { +string_list_append (&only_match_skip_worktree, dst); + skip_sparse =1 ; + } + +if (skip_sparse) + goto remove_entry; +string_list_insert (&src_for_dst, dst); + }if (!bad)continue ;if (!ignore_errors)die (_ ("%s, source=%s, destination=%s" ), bad, src, dst); +remove_entry:if (--argc >0 ) {int n = argc - i; memmove(source + i, source + i + 1,
@@ -266,6 +294,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix) } } +if (only_match_skip_worktree.nr) { +advise_on_updating_sparse_paths (&only_match_skip_worktree); +if (!ignore_errors) +return 1 ; + } +for (i =0 ; i < argc; i++) {const char *src = source[i], *dst = destination[i];enum update_mode mode = modes[i];
@@ -274,7 +308,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)printf (_ ("Renaming %s to %s\n" ), src, dst);if (show_only)continue ; -if (mode !=INDEX &&rename (src, dst) <0 ) { +if (mode !=INDEX && mode !=SPARSE &&rename (src, dst) <0 ) {if (ignore_errors)continue ;die_errno (_ ("renaming '%s' failed" ), src);
t/t7002-mv-sparse-checkout.sh new file +186 -0
186 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v4 13/13 advice: update message to suggest '--sparse'
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The previous changes modified the behavior of 'git add', 'git rm', and 'git mv' to not adjust paths outside the sparse-checkout cone, even if they exist in the working tree and their cache entries lack the SKIP_WORKTREE bit. The intention is to warn users that they are doing something potentially dangerous. The '--sparse' option was added to each command to allow careful users the same ability they had before.
To improve the discoverability of this new functionality, add a message to advice.updateSparsePath that mentions the existence of the option.
The previous set of changes also modified the purpose of this message to include possibly a list of paths instead of only a list of pathspecs. Make the warning message more clear about this new behavior.
--- advice.c | 11 ++++++----- t/t3602-rm-sparse-checkout.sh | 9 ++++++--- t/t3705-add-sparse-checkout.sh | 9 ++++++--- t/t7002-mv-sparse-checkout.sh | 9 ++++++--- 4 files changed, 24 insertions(+), 14 deletions(-)
advice.c +6 -5
@@ -293,15 +293,16 @@ void advise_on_updating_sparse_paths(struct string_list *pathspec_list)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" )); +fprintf (stderr,_ ("The following paths and/or pathspecs matched paths that exist\n" +"outside of your sparse-checkout definition, so will not be\n" +"updated in the index:\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." )); +_ ("If you intend to update such entries, try one of the following:\n" +"* Use the --sparse option.\n" +"* Disable or modify the sparsity rules." )); }void detach_advice (const char *new_name)
t/t3602-rm-sparse-checkout.sh +6 -3
@@ -11,12 +11,15 @@ test_expect_success 'setup' " 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: + The following paths and/or pathspecs matched paths that exist + outside of your sparse-checkout definition, so will not be + updated in the index: EOF cat >sparse_hint <<-EOF && - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: If you intend to update such entries, try one of the following: + hint: * Use the --sparse option. + hint: * Disable or modify the sparsity rules. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
t/t3705-add-sparse-checkout.sh +6 -3
@@ -44,12 +44,15 @@ test_sparse_entry_unstaged () { 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: + The following paths and/or pathspecs matched paths that exist + outside of your sparse-checkout definition, so will not be + updated in the index: EOF cat >sparse_hint <<-EOF && - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: If you intend to update such entries, try one of the following: + hint: * Use the --sparse option. + hint: * Disable or modify the sparsity rules. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF
t/t7002-mv-sparse-checkout.sh +6 -3
@@ -11,12 +11,15 @@ test_expect_success 'setup' " 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: + The following paths and/or pathspecs matched paths that exist + outside of your sparse-checkout definition, so will not be + updated in the index: EOF cat >sparse_hint <<-EOF - hint: Disable or modify the sparsity rules if you intend to update such entries. + hint: If you intend to update such entries, try one of the following: + hint: * Use the --sparse option. + hint: * Disable or modify the sparsity rules. hint: Disable this message with \"git config advice.updateSparsePath false\" EOF "
Signature · 1 line
gitgitgadget
Re: [PATCH v4 00/13] Sparse-checkout: modify 'git add', 'git rm', and 'git mv' behavior
In reply to Derrick Stolee via GitGitGadgetOn Fri, Sep 24, 2021 at 8:39 AM Derrick Stolee via GitGitGadget <[email protected]> wrote:
...
Quoted 11 lines
Updates in v4
=============* Instead of using 'git status' and 'grep' to detect staged changes, we use 'git diff --staged'. t1092 uses an additional --diff-filter because it tests with merge conflicts, so it needs this extra flag.
* Patches 3 and 4 are merged into the new patch 3 to avoid temporarily having a poorly named method.
...
Quoted 17 lines
Range-diff vs v3:
1: ea940f10a7c ! 1: 642b05fc020 t3705: test that 'sparse_entry' is unstaged @@ t/t3705-add-sparse-checkout.sh: setup_gitignore () { }+test_sparse_entry_unstaged () { -+ git status --porcelain >actual && -+ ! grep "^[MDARCU][M ] sparse_entry\$" actual ++ git diff --staged -- sparse_entry >diff && ++ test_must_be_empty diff +} + test_expect_success 'setup' " 2: c7dedb41291 ! 2: 58389edc76c t1092: behavior for adding sparse files @@ t/t1092-sparse-checkout-compatibility.sh: test_sparse_match () {
Quoted 8 lines
+ file=$1 &&
+ for repo in sparse-checkout sparse-index
+ do
-+ git -C $repo status --porcelain >$repo-out &&
-+ ! grep "^A $file\$" $repo-out &&
-+ ! grep "^M $file\$" $repo-out || return 1
++ # Skip "unmerged" paths
++ git -C $repo diff --staged --diff-filter=ACDMRTXB -- "$file" >diff &&Wouldn't this be more naturally spelled as --diff-filter=u ? (Note:
lowercase 'u', not uppercase.) Then you could drop the comment too.
Other than that nit, this round looks good to me. Feel free to add a
Re: [PATCH v4 00/13] Sparse-checkout: modify 'git add', 'git rm', and 'git mv' behavior
Quoted 11 lines · Elijah Newren
Quoted 10 lines
2: c7dedb41291 ! 2: 58389edc76c t1092: behavior for adding sparse files @@ t/t1092-sparse-checkout-compatibility.sh: test_sparse_match () { + file=$1 && + for repo in sparse-checkout sparse-index + do -+ git -C $repo status --porcelain >$repo-out && -+ ! grep "^A $file\$" $repo-out && -+ ! grep "^M $file\$" $repo-out || return 1 ++ # Skip "unmerged" paths ++ git -C $repo diff --staged --diff-filter=ACDMRTXB -- "$file" >diff &&
Quoted 2 lines
Wouldn't this be more naturally spelled as --diff-filter=u ? (Note:
lowercase 'u', not uppercase.) Then you could drop the comment too.
Excellent.
Quoted 4 lines
Other than that nit, this round looks good to me. Feel free to add a
Reviewed-by Elijah Newren <[email protected]>
Quoted 7 lines · Derrick Stolee via GitGitGadget
This series is based on ds/mergies-with-sparse-index.
As requested, this series looks to update the behavior of git add, git rm, and git mv when they attempt to modify paths outside of the sparse-checkout cone. In particular, this care is expanded to not just cache entries with the
SKIP_WORKTREEbit, but also paths that do not match the sparse-checkout definition.
I suspect something in this series broke 'git add' and friends with "odd" sparse definitions (I haven't actually bisected). git 2.33.0 rejects attempts to add files with the below sparse-checkout and modified files. There appears to be a discrepancy in the query vs. checkout logic as the rejected files are checked out in the working tree, e.g. git sees that the local file was deleted, yet will not stage the deletion.
There's also arguably a flaw in the "advise" trigger. AFAICT, the help message is displayed if and only if the entire path is excluded from the working tree. In my perfect world, git would complain and advise if there are unstaged changes for tracked files covered by the specified path.
Note, my sparse-checkout is very much the result of trial and error to get the exact files I care about. It's entirely possible I'm doing something weird, but at the same time git itself is obviously confused.
Thanks!
$ cat .git/info/sparse-checkout !arch/* !tools/arch/* !virt/kvm/arm/* /* arch/.gitignore arch/Kconfig arch/x86 tools/arch/x86 tools/include/uapi/linux/kvm.h !Documentation !drivers
$ git read-tree -mu HEAD
$ rm arch/x86/kvm/x86.c
$ git commit -a On branch x86/kvm_find_cpuid_entry_index Your branch is up to date with 'kvm/queue'.
You are in a sparse checkout with 40% of tracked files present.
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory)
deleted: arch/x86/kvm/x86.c
no changes added to commit (use "git add" and/or "git commit -a")
$ git add arch
$ git add .
$ git add arch/x86 The following paths and/or pathspecs matched paths that exist outside of your sparse-checkout definition, so will not be updated in the index: arch/x86 hint: If you intend to update such entries, try one of the following: hint: * Use the --sparse option. hint: * Disable or modify the sparsity rules. hint: Disable this message with "git config advice.updateSparsePath false"
Quoted 9 lines · Sean Christopherson
Quoted 7 lines · Derrick Stolee via GitGitGadget
This series is based on ds/mergies-with-sparse-index.
As requested, this series looks to update the behavior of git add, git rm, and git mv when they attempt to modify paths outside of the sparse-checkout cone. In particular, this care is expanded to not just cache entries with the
SKIP_WORKTREEbit, but also paths that do not match the sparse-checkout definition.
Quoted 6 lines
I suspect something in this series broke 'git add' and friends with "odd" sparse definitions (I haven't actually bisected). git 2.33.0 rejects attempts to add files with the below sparse-checkout and modified files. There appears to be a discrepancy in the query vs. checkout logic as the rejected files are checked out in the working tree, e.g. git sees that the local file was deleted, yet will not stage the deletion.
Are you using v2.33.0? This change is not in that version.
However, mt/add-rm-in-sparse-checkout [1] was introduced in v2.33.0 and introduced these advice suggestions.
The series you are commenting on goes even farther in restricting adds to be within the sparse-checkout definitions, even for unstaged files or files that removed the skip-worktree bit due to a merge conflict. It also creates an override '--sparse' option that allows you to ignore these protections.
Quoted 10 lines
There's also arguably a flaw in the "advise" trigger. AFAICT, the help message is displayed if and only if the entire path is excluded from the working tree. In my perfect world, git would complain and advise if there are unstaged changes for tracked files covered by the specified path.
Quoted 1 line
Note, my sparse-checkout is very much the result of trial and error to get the
exact files I care about. It's entirely possible I'm doing something weird, but at the same time git itself is obviously confused.
Thanks!
Quoted 12 lines
$ cat .git/info/sparse-checkout !arch/* !tools/arch/* !virt/kvm/arm/* /* arch/.gitignore arch/Kconfig arch/x86 tools/arch/x86 tools/include/uapi/linux/kvm.h !Documentation !drivers
Have you tried using 'arch/x86/' and 'tools/arch/x86/' to specify that these are directories? Just a thought.
Quoted 5 lines
$ git read-tree -mu HEAD$ rm arch/x86/kvm/x86.c$ git commit -a
...
Quoted 1 line
deleted: arch/x86/kvm/x86.c
This is certainly odd. Worth more investigation that I don't have time for at this moment.
Thanks,
-Stolee
Quoted 10 lines · Derrick Stolee
Quoted 9 lines · Sean Christopherson
Quoted 7 lines · Derrick Stolee via GitGitGadget
This series is based on ds/mergies-with-sparse-index.
As requested, this series looks to update the behavior of git add, git rm, and git mv when they attempt to modify paths outside of the sparse-checkout cone. In particular, this care is expanded to not just cache entries with the
SKIP_WORKTREEbit, but also paths that do not match the sparse-checkout definition.
Quoted 8 lines
Quoted 6 lines
I suspect something in this series broke 'git add' and friends with "odd" sparse definitions (I haven't actually bisected). git 2.33.0 rejects attempts to add files with the below sparse-checkout and modified files. There appears to be a discrepancy in the query vs. checkout logic as the rejected files are checked out in the working tree, e.g. git sees that the local file was deleted, yet will not stage the deletion.
Are you using v2.33.0? This change is not in that version.
Hrm, it's an internal build that says v2.33.0 is the bsae, but the --sparse option is available so who knows what's actually underneath the hood. I can try vanilla upstream builds if that would help narrow down the issue.
Quoted 33 lines
However, mt/add-rm-in-sparse-checkout [1] was introduced in v2.33.0 and introduced these advice suggestions.
The series you are commenting on goes even farther in restricting adds to be within the sparse-checkout definitions, even for unstaged files or files that removed the skip-worktree bit due to a merge conflict. It also creates an override '--sparse' option that allows you to ignore these protections.
Quoted 10 lines
There's also arguably a flaw in the "advise" trigger. AFAICT, the help message is displayed if and only if the entire path is excluded from the working tree. In my perfect world, git would complain and advise if there are unstaged changes for tracked files covered by the specified path.
Quoted 1 line
Note, my sparse-checkout is very much the result of trial and error to get the
exact files I care about. It's entirely possible I'm doing something weird, but at the same time git itself is obviously confused.
Thanks!
Quoted 12 lines
$ cat .git/info/sparse-checkout !arch/* !tools/arch/* !virt/kvm/arm/* /* arch/.gitignore arch/Kconfig arch/x86 tools/arch/x86 tools/include/uapi/linux/kvm.h !Documentation !drivers
Quoted 2 lines
Have you tried using '
arch/x86/' and 'tools/arch/x86/' to specify that these are directories? Just a thought.
Nice! That workaround resolves the issue. I vaguely recall intentionally omitting the trailing slash, but adding it back doesn't seem to have any unwanted side effects on the current git versions I'm using.
Quoted 10 lines
Quoted 5 lines
$ git read-tree -mu HEAD$ rm arch/x86/kvm/x86.c$ git commit -a...
Quoted 1 line
deleted: arch/x86/kvm/x86.cThis is certainly odd. Worth more investigation that I don't have time for at this moment.
I've no objection to punting on this now that I have a workaround. The man pages are quite clear that sparse checkouts are still experimental and it's no trouble for me to whine again if something breaks in the future :-)
Thanks again!
Quoted 5 lines · Sean Christopherson
Quoted 1 line
Are you using v2.33.0? This change is not in that version.
Hrm, it's an internal build that says v2.33.0 is the bsae, but the --sparse option is available so who knows what's actually underneath the hood. I can try vanilla upstream builds if that would help narrow down the issue.
$ git version
Guessing from the e-mail address, perhaps you are using something derived from the next branch of the day, maintained by Jonathan Nieder's group, for internal consumption at Google.
Quoted 8 lines · Junio C Hamano
Quoted 5 lines · Sean Christopherson
Quoted 1 line
Are you using v2.33.0? This change is not in that version.
Hrm, it's an internal build that says v2.33.0 is the bsae, but the --sparse option is available so who knows what's actually underneath the hood. I can try vanilla upstream builds if that would help narrow down the issue.
Quoted 5 lines
$ git versionGuessing from the e-mail address, perhaps you are using something derived from the next branch of the day, maintained by Jonathan Nieder's group, for internal consumption at Google.
That's more than likely the case. 2.33.0.1079.g6e70778dc9-goog
RFC PATCH 1/1 add|rm|mv: fix bug that prevent the update of non-sparse
In reply to Sean ChristophersonQuoted 17 lines · Sean Christopherson
$ cat .git/info/sparse-checkout !arch/* !tools/arch/* !virt/kvm/arm/* /* arch/.gitignore arch/Kconfig arch/x86 tools/arch/x86 tools/include/uapi/linux/kvm.h !Documentation !drivers$ git read-tree -mu HEAD$ rm arch/x86/kvm/x86.c
[...]
Quoted 5 lines
$ git add arch/x86 The following paths and/or pathspecs matched paths that exist outside of your sparse-checkout definition, so will not be updated in the index: arch/x86
I think the problem may be that we are performing pattern matching slightly different in add, mv, and rm, in comparison to "git sparse-checkout". On "git sparse-checkout init" (or reapply), we call clear_ce_flags() which calls path_matches_pattern_list() for each component of the working tree paths. If the full path gives a match result of UNDECIDED, we recursively try to use the match result from the parent dir (or NOT_MATCHED if we reach the top with UNDECIDED).
In Sean's example, we get UNDECIDED for "arch/x86/kvm/x86.c", but "arch/x86" gives MATCHED, so we end up using that for the full path.
However, in add|mv|rm we only call path_matches_pattern_list() for the full path and get UNDECIDED, which we consider the same as NOT_MATCHED, and end up disallowing the path update operation with a warning message.
The commands do work if we replace the sparsity pattern "arch/x86" with "arch/x86/" (with a trailing slash), but note that it only works because the pattern is relative to the root (see dir.c:1297). If we change it to "x86/", it would no longer work.
So far, the only way I could think of to fix this would be to perform pattern matching for the leading components of the paths too. That doesn't seem very nice, though, as it can probably be quite expensive... But here is a patch for discussion:
-- >8 --
Subject: [RFC PATCH] add|rm|mv: fix bug that prevent the update of non-sparse dirs
These three commands recently learned to avoid updating paths that do not match the sparse-checkout patterns even if they are missing the SKIP_WORKTREE bit. This is done using path_in_sparse_checkout(), which tries to match the path with the current set of sparsity rules using path_matches_pattern_list(). This is similar to what clear_ce_flags() does when we run "git sparse-checkout init" or "git sparse-checkout reapply". But note that clear_ce_flags() has a recursive behavior, calling path_matches_pattern_list() for each component in a path, whereas path_in_sparse_checkout() only calls it for the full path. This makes the function miss matches such as the one between path "a/b/c" and the pattern "b/". So if the user has the sparsity rules "!/a" and "b/", for example, add, rm, and mv will fail to update the path "a/b/c" and end up displaying a warning about "a/b/c" being outside the sparse checkout even though it isn't. Note that this problem only occurs with non-cone mode.
Fix this by making path_in_sparse_checkout() perform pattern matching for every component in the given path when cone mode is disabled. (This can be expensive, and we might want to do some form of caching for the match results of the leading components. However, this is not implemented in this patch.) Also add two tests for each command (add, rm, and mv) to check that they behave correctly with the said pattern matching. The first test would previously fail without this patch, while the second already succeeded. It is added mostly to make sure that we are not breaking the existing pattern matching for directories that are really sparse, and also as a protection against any future regressions.
Note that two other existing tests had to be changed: one test in t3602 checks that "git rm -r <dir>" won't remove sparse entries, but it didn't allow the non-sparse entries inside <dir> to be removed. The other one, in t7002, tested that "git mv" would correctly display a warning message for sparse paths, but it accidentally expected the message to include two non-sparse paths as well.
--- dir.c | 33 ++++++++++++++++++++++++------ t/t3602-rm-sparse-checkout.sh | 37 +++++++++++++++++++++++++++++++--- t/t3705-add-sparse-checkout.sh | 18 +++++++++++++++++ t/t7002-mv-sparse-checkout.sh | 28 +++++++++++++++++++++++-- 4 files changed, 105 insertions(+), 11 deletions(-)
dir.c +27 -6
@@ -1504,8 +1504,9 @@ static int path_in_sparse_checkout_1(const char *path,struct index_state *istate, int require_cone_mode) { -const char *base; int dtype = DT_REG; +enum pattern_match_result ret =NOT_MATCHED ; +const char *p, *last_slash =NULL ;/* * We default to accepting a path if there are no patterns or
@@ -1516,11 +1517,31 @@ static int path_in_sparse_checkout_1(const char *path, !istate->sparse_checkout_patterns->use_cone_patterns))return 1 ; - base =strrchr (path,'/' ); -return path_matches_pattern_list (path,strlen (path), base ? base +1 : path, - &dtype, - istate->sparse_checkout_patterns, - istate) >0 ; +if (istate->sparse_checkout_patterns->use_cone_patterns) { +const char *base =strrchr (path,'/' ); +return path_matches_pattern_list (path,strlen (path), + base ? base +1 : path, &dtype, + istate->sparse_checkout_patterns, istate) >0 ; + } + +for (p = path; ; p++) { +enum pattern_match_result match; + +if (*p && *p !='/' ) +continue ; + + match =path_matches_pattern_list (path, p - path, + last_slash ? last_slash +1 : path, &dtype, + istate->sparse_checkout_patterns, istate); + +if (match !=UNDECIDED ) + ret = match; +if (!*p) +break ; + last_slash = p; + } + +return ret; } int path_in_sparse_checkout(const char *path,
t/t3602-rm-sparse-checkout.sh +34 -3
@@ -40,14 +40,20 @@ done test_expect_success 'recursive rm does not remove sparse entries' ' git reset --hard && git sparse-checkout set sub/dir && - test_must_fail git rm -r sub && - git rm --sparse -r sub && + git rm -r sub && git status --porcelain -uno >actual && cat >expected <<-\EOF && + D sub/dir/e + EOF + test_cmp expected actual && + + git rm --sparse -r sub && + git status --porcelain -uno >actual2 && + cat >expected2 <<-\EOF && D sub/d D sub/dir/e EOF - test_cmp expected actual + test_cmp expected2 actual2 ' test_expect_success 'recursive rm --sparse removes sparse entries' '
@@ -105,4 +111,29 @@ test_expect_success 'refuse to rm a non-skip-worktree path outside sparse cone' test_path_is_missing b ' +test_expect_success 'can remove files from non-sparse dir' ' + git reset --hard && + git sparse-checkout disable && + mkdir -p w x/y && + test_commit w/f && + test_commit x/y/f && + + git sparse-checkout set w !/x y && + git rm w/f.t x/y/f.t 2>stderr && + test_must_be_empty stderr +' + +test_expect_success 'refuse to remove non-skip-worktree file from sparse dir' ' + git reset --hard && + git sparse-checkout disable && + mkdir -p x/y/z && + test_commit x/y/z/f && + git sparse-checkout set !/x y !x/y/z && + + git update-index --no-skip-worktree x/y/z/f.t && + test_must_fail git rm x/y/z/f.t 2>stderr && + echo x/y/z/f.t | cat sparse_error_header - sparse_hint >expect && + test_cmp expect stderr +' + test_done
t/t3705-add-sparse-checkout.sh +18 -0
@@ -214,4 +214,22 @@ test_expect_success 'add allows sparse entries with --sparse' ' test_must_be_empty stderr ' +test_expect_success 'can add files from non-sparse dir' ' + git sparse-checkout set w !/x y && + mkdir -p w x/y && + touch w/f x/y/f && + git add w/f x/y/f 2>stderr && + test_must_be_empty stderr +' + +test_expect_success 'refuse to add non-skip-worktree file from sparse dir' ' + git sparse-checkout set !/x y !x/y/z && + mkdir -p x/y/z && + touch x/y/z/f && + test_must_fail git add x/y/z/f 2>stderr && + echo x/y/z/f | cat sparse_error_header - sparse_hint >expect && + test_cmp expect stderr + +' + test_done
t/t7002-mv-sparse-checkout.sh +26 -2
@@ -143,8 +143,6 @@ test_expect_success 'recursive mv refuses to move (possible) sparse' ' cat >>expect <<-\EOF && sub/d sub2/d - sub/dir/e - sub2/dir/e sub/dir2/e sub2/dir2/e EOF
@@ -186,4 +184,30 @@ test_expect_success 'recursive mv refuses to move sparse' ' git reset --hard HEAD~1 ' +test_expect_success 'can move files to non-sparse dir' ' + git reset --hard && + git sparse-checkout init --no-cone && + git sparse-checkout set a b c w !/x y && + mkdir -p w x/y && + + git mv a w/new-a 2>stderr && + git mv b x/y/new-b 2>stderr && + test_must_be_empty stderr +' + +test_expect_success 'refuse to move file to non-skip-worktree sparse path' ' + git reset --hard && + git sparse-checkout init --no-cone && + git sparse-checkout set a !/x y !x/y/z && + mkdir -p x/y/z && + + test_must_fail git mv a x/y/z/new-a 2>stderr && + cat sparse_error_header >expect && + cat >>expect <<-\EOF && + x/y/z/new-a + EOF + cat sparse_hint >>expect && + test_cmp expect stderr +' + test_done
Signature · 1 line
2.33.0
Re: [RFC PATCH] add|rm|mv: fix bug that prevent the update of non-sparse
Quoted 30 lines · Matheus Tavares
dir.c +16
-5@@ -1516,11 +1517,31 @@ static int path_in_sparse_checkout_1(const char *path, !istate->sparse_checkout_patterns->use_cone_patterns))return 1 ; - base =strrchr (path,'/' ); -return path_matches_pattern_list (path,strlen (path), base ? base +1 : path, - &dtype, - istate->sparse_checkout_patterns, - istate) >0 ; +if (istate->sparse_checkout_patterns->use_cone_patterns) { +const char *base =strrchr (path,'/' ); +return path_matches_pattern_list (path,strlen (path), + base ? base +1 : path, &dtype, + istate->sparse_checkout_patterns, istate) >0 ; + } + +for (p = path; ; p++) { +enum pattern_match_result match; + +if (*p && *p !='/' ) +continue ; + + match =path_matches_pattern_list (path, p - path, + last_slash ? last_slash +1 : path, &dtype, + istate->sparse_checkout_patterns, istate);
dir.c static int path_in_sparse_checkout_1(const char *path, · line 1545
Quoted 10 lines
dir.c +9
-0+ +if (match !=UNDECIDED ) + ret = match; +if (!*p) +break ; + last_slash = p; + } + +return ret; }
Of course, after hitting send I realized it would make a lot more sense to start the pattern matching from the full path and only go backwards through the parent dirs until we find the first non-UNDECIDED result. I.e. something like this:
static int path_in_sparse_checkout_1(const char *path,
struct index_state *istate, int require_cone_mode)
{
int dtype = DT_REG;
enum pattern_match_result ret;
const char *p, *base;
/*
* We default to accepting a path if there are no patterns or
* they are of the wrong type.
*/
if (init_sparse_checkout_patterns(istate) ||
(require_cone_mode &&
!istate->sparse_checkout_patterns->use_cone_patterns))
return 1;
if (istate->sparse_checkout_patterns->use_cone_patterns) {
base = strrchr(path, '/');
return path_matches_pattern_list(path, strlen(path),
base ? base + 1 : path, &dtype,
istate->sparse_checkout_patterns, istate) > 0;
}
/*
* If the match for the path is UNDECIDED, try to match the parent dir
* recursively.
*/
for (p = path + strlen(path); p && p > path; p = base) {
base = memrchr(path, '/', p - path);
ret = path_matches_pattern_list(path, p - path,
base ? base + 1 : path, &dtype,
istate->sparse_checkout_patterns, istate);
if (ret != UNDECIDED)
break;
}
return ret == UNDECIDED ? NOT_MATCHED : ret;
}But I will let others comment on the overall idea and/or other alternatives before sending a possible v2.
Re: [RFC PATCH] add|rm|mv: fix bug that prevent the update of non-sparse
In reply to Matheus TavaresQuoted 25 lines · Matheus Tavares
Quoted 17 lines · Sean Christopherson
$ cat .git/info/sparse-checkout !arch/* !tools/arch/* !virt/kvm/arm/* /* arch/.gitignore arch/Kconfig arch/x86 tools/arch/x86 tools/include/uapi/linux/kvm.h !Documentation !drivers$ git read-tree -mu HEAD$ rm arch/x86/kvm/x86.c[...]
Quoted 5 lines
$ git add arch/x86 The following paths and/or pathspecs matched paths that exist outside of your sparse-checkout definition, so will not be updated in the index: arch/x86
Quoted 7 lines
I think the problem may be that we are performing pattern matching slightly different in add, mv, and rm, in comparison to "git sparse-checkout". On "git sparse-checkout init" (or reapply), we call
clear_ce_flags()which callspath_matches_pattern_list()for each component of the working tree paths. If the full path gives a match result of UNDECIDED, we recursively try to use the match result from the parent dir (orNOT_MATCHEDif we reach the top with UNDECIDED).
Yes! I think this is absolutely the problem. Thanks for pointing this out!
Quoted 12 lines
In Sean's example, we get UNDECIDED for "
arch/x86/kvm/x86.c", but "arch/x86" gives MATCHED, so we end up using that for the full path.However, in add|mv|rm we only call
path_matches_pattern_list()for the full path and get UNDECIDED, which we consider the same asNOT_MATCHED, and end up disallowing the path update operation with a warning message.The commands do work if we replace the sparsity pattern "
arch/x86" with "arch/x86/" (with a trailing slash), but note that it only works because the pattern is relative to the root (see dir.c:1297). If we change it to "x86/", it would no longer work.
Quoted 4 lines
So far, the only way I could think of to fix this would be to perform pattern matching for the leading components of the paths too. That doesn't seem very nice, though, as it can probably be quite expensive... But here is a patch for discussion:
I agree that it is expensive, but that's already the case for the non-cone sparse-checkout patterns. Hopefully it is sufficient that these cases are restricted to modified files (in the case of git add .) or specific pathspecs (in the case of git mv and git rm).
Quoted 74 lines
-- >8 --
Subject: [RFC PATCH] add|rm|mv: fix bug that prevent the update of non-sparse dirsThese three commands recently learned to avoid updating paths that do not match the sparse-checkout patterns even if they are missing the
SKIP_WORKTREEbit. This is done usingpath_in_sparse_checkout(), which tries to match the path with the current set of sparsity rules usingpath_matches_pattern_list(). This is similar to whatclear_ce_flags()does when we run "git sparse-checkout init" or "git sparse-checkout reapply". But note thatclear_ce_flags()has a recursive behavior, callingpath_matches_pattern_list()for each component in a path, whereaspath_in_sparse_checkout()only calls it for the full path. This makes the function miss matches such as the one between path "a/b/c" and the pattern "b/". So if the user has the sparsity rules "!/a" and "b/", for example, add, rm, and mv will fail to update the path "a/b/c" and end up displaying a warning about "a/b/c" being outside the sparse checkout even though it isn't. Note that this problem only occurs with non-cone mode.Fix this by making
path_in_sparse_checkout()perform pattern matching for every component in the given path when cone mode is disabled. (This can be expensive, and we might want to do some form of caching for the match results of the leading components. However, this is not implemented in this patch.) Also add two tests for each command (add, rm, and mv) to check that they behave correctly with the said pattern matching. The first test would previously fail without this patch, while the second already succeeded. It is added mostly to make sure that we are not breaking the existing pattern matching for directories that are really sparse, and also as a protection against any future regressions.Note that two other existing tests had to be changed: one test in t3602 checks that "git rm -r <dir>" won't remove sparse entries, but it didn't allow the non-sparse entries inside <dir> to be removed. The other one, in t7002, tested that "git mv" would correctly display a warning message for sparse paths, but it accidentally expected the message to include two non-sparse paths as well.
dir.c +17
-6@@ -1504,8 +1504,9 @@ static int path_in_sparse_checkout_1(const char *path,struct index_state *istate, int require_cone_mode) { -const char *base; int dtype = DT_REG; +enum pattern_match_result ret =NOT_MATCHED ; +const char *p, *last_slash =NULL ;/* * We default to accepting a path if there are no patterns or @@ -1516,11 +1517,31 @@ static int path_in_sparse_checkout_1(const char *path, !istate->sparse_checkout_patterns->use_cone_patterns))return 1 ; - base =strrchr (path,'/' ); -return path_matches_pattern_list (path,strlen (path), base ? base +1 : path, - &dtype, - istate->sparse_checkout_patterns, - istate) >0 ; +if (istate->sparse_checkout_patterns->use_cone_patterns) { +const char *base =strrchr (path,'/' ); +return path_matches_pattern_list (path,strlen (path), + base ? base +1 : path, &dtype, + istate->sparse_checkout_patterns, istate) >0 ; + } + +for (p = path; ; p++) { +enum pattern_match_result match; + +if (*p && *p !='/' ) +continue ; + + match = path_matches_pattern_list(path, p - path, + last_slash ? last_slash + 1 : path, &dtype,
dir.c static int path_in_sparse_checkout_1(const char *path, · line 1544
Quoted 10 lines
dir.c +10
-0+ istate->sparse_checkout_patterns, istate); + +if (match !=UNDECIDED ) + ret = match; +if (!*p) +break ; + last_slash = p; + } + +return ret;
This implementation makes sense to me.
t/t3602-rm-sparse-checkout.sh done · line 43
Quoted 6 lines
t/t3602-rm-sparse-checkout.sh +1
-2test_expect_success 'recursive rm does not remove sparse entries' ' git reset --hard && git sparse-checkout set sub/dir && - test_must_fail git rm -r sub && - git rm --sparse -r sub && + git rm -r sub &&
Interesting that the new pattern-matching already presents a change of behavior in this test case.
t/t3602-rm-sparse-checkout.sh done · line 48
Quoted 5 lines
t/t3602-rm-sparse-checkout.sh +3
-0git status --porcelain -uno >actual && cat >expected <<-\EOF && + D sub/dir/e + EOF + test_cmp expected actual &&
And here is why. Excellent. I suppose that setting the pattern to be "sub/dir/" would have shown this behavior before.
t/t3602-rm-sparse-checkout.sh done · line 57
Quoted 10 lines
t/t3602-rm-sparse-checkout.sh +5
-1+ + git rm --sparse -r sub && + git status --porcelain -uno >actual2 && + cat >expected2 <<-\EOF && D sub/d D sub/dir/e EOF - test_cmp expected actual + test_cmp expected2 actual2 '
The rest of the test cases add new checks that are very valuable.
I love this idea and I agree that it would be better to change the loop direction to match the full path first (as you mention in your response).
Thanks,
-Stolee
Re: [PATCH v4 04/13] dir: fix pattern matching on dirs
In reply to Derrick Stolee via GitGitGadgetThis patch changes the behavior of .gitignore such that directories are now matched by prefix instead of matching exactly.
The failure that we observed is something like the following:
In "a/.gitignore", we have the pattern "git/". We should expect that "a/git/foo" to be ignored because "git/" should be matched exactly. However, "a/git-foo/bar" is also ignored because "git-foo" matches the prefix.
I'll prepare a test case for this as soon as I figure out how to write it..
Re: [PATCH v4 04/13] dir: fix pattern matching on dirs
Quoted 5 lines · Glen Choo
This patch changes the behavior of .gitignore such that directories are now matched by prefix instead of matching exactly.
The failure that we observed is something like the following:
Quoted 7 lines
In "a/.gitignore", we have the pattern "git/". We should expect that "a/git/foo" to be ignored because "git/" should be matched exactly. However, "a/git-foo/bar" is also ignored because "git-foo" matches the prefix.
I'll prepare a test case for this as soon as I figure out how to write it..
FWIW, reverting this commit (and nothing else) from 'master' does not seem to break any test. That does not necessarily mean much (it may be indicating a gap in the test coverage).
Re: [PATCH v4 04/13] dir: fix pattern matching on dirs
Quoted 4 lines · Junio C Hamano
Quoted 2 lines · Glen Choo
This patch changes the behavior of .gitignore such that directories are now matched by prefix instead of matching exactly.
Thank you for pointing out an unintended consequence.
Quoted 7 lines
Quoted 7 lines
The failure that we observed is something like the following:
In "a/.gitignore", we have the pattern "git/". We should expect that "a/git/foo" to be ignored because "git/" should be matched exactly. However, "a/git-foo/bar" is also ignored because "git-foo" matches the prefix.
Quoted 6 lines
Quoted 2 lines
I'll prepare a test case for this as soon as I figure out how to write it..
FWIW, reverting this commit (and nothing else) from 'master' does not seem to break any test. That does not necessarily mean much (it may be indicating a gap in the test coverage).
Hm. I definitely seemed confident in my commit message that this change was important for a test I was introducing at some point. Let me revert it in microsoft/git and see if our Scalar functional tests find an issue.
In the meantime, I'll try to create a Git test that demonstrates a problem one way or another.
Thanks,
-Stolee
Re: [PATCH v4 04/13] dir: fix pattern matching on dirs
Quoted 17 lines · Derrick Stolee
Quoted 4 lines · Junio C Hamano
Quoted 2 lines · Glen Choo
This patch changes the behavior of .gitignore such that directories are now matched by prefix instead of matching exactly.
Thank you for pointing out an unintended consequence.
Quoted 9 lines
Quoted 9 lines
The failure that we observed is something like the following:
In "a/.gitignore", we have the pattern "git/". We should expect that "a/git/foo" to be ignored because "git/" should be matched exactly. However, "a/git-foo/bar" is also ignored because "git-foo" matches the prefix.
I'll prepare a test case for this as soon as I figure out how to write it..
...
Quoted 2 lines
In the meantime, I'll try to create a Git test that demonstrates a problem one way or another.
I created a test, but had some trouble reproducing it due to some subtleties higher in the call stack. Here is a patch that reverts the change and adds some tests.
The Scalar functional tests passed with the revert, so the original patch was worthless to begin with. I don't recall what motivated the change, but clearly it was a mistake. Sorry.
---- >8 ----
From d1cfc8efeab015273bfebd6cd93465e6f38dc40f Mon Sep 17 00:00:00 2001
From: Derrick Stolee <[email protected]>
Date: Tue, 2 Nov 2021 10:40:06 -0400
Subject: [PATCH] dir: fix directory-matching bug
This reverts the change from ed49584 (dir: fix pattern matching on dirs, 2021-09-24), which claimed to fix a directory-matching problem without a test case. It turns out to _create_ a bug, but it is a bit subtle.
The bug would have been revealed by the first of two tests being added to t0008-ignores.sh. The first uses a pattern "/git/" inside the a/.gitignores file, which matches against 'a/git/foo' but not 'a/git-foo/bar'. This test would fail before the revert.
The second test shows what happens if the test instead uses a pattern "git/" and this test passes both before and after the revert.
The difference in these two cases are due to how last_matching_pattern_from_list() checks patterns both if they have the PATTERN_FLAG_MUSTBEDIR and PATTERN_FLAG_NODIR flags. In the case of "git/", the PATTERN_FLAG_NODIR is also provided, making the change in behavior in match_pathname() not affect the end result of last_matching_pattern_from_list().
--- dir.c | 2 +- t/t0008-ignores.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-)
dir.c +1 -1
@@ -1294,7 +1294,7 @@ int match_pathname(const char *pathname, int pathlen,* then our prefix match is all we need; we * do not need to call fnmatch at all. */ -if (!patternlen && (!namelen || (flags &PATTERN_FLAG_MUSTBEDIR ))) +if (!patternlen && !namelen)return 1 ; }
t/t0008-ignores.sh +26 -0
@@ -803,6 +803,32 @@ test_expect_success 'existing directory and file' ' grep top-level-dir actual ' +test_expect_success 'exact prefix matching (with root)' ' + test_when_finished rm -r a && + mkdir -p a/git a/git-foo && + touch a/git/foo a/git-foo/bar && + echo /git/ >a/.gitignore && + git check-ignore a/git a/git/foo a/git-foo a/git-foo/bar >actual && + cat >expect <<-\EOF && + a/git + a/git/foo + EOF + test_cmp expect actual +' + +test_expect_success 'exact prefix matching (without root)' ' + test_when_finished rm -r a && + mkdir -p a/git a/git-foo && + touch a/git/foo a/git-foo/bar && + echo git/ >a/.gitignore && + git check-ignore a/git a/git/foo a/git-foo a/git-foo/bar >actual && + cat >expect <<-\EOF && + a/git + a/git/foo + EOF + test_cmp expect actual +' + ############################################################################ # # test whitespace handling
Signature · 1 line
2.34.0.vfs.0.0.rc0.dirty
Re: [PATCH v4 04/13] dir: fix pattern matching on dirs
Quoted 103 lines · Derrick Stolee
Quoted 17 lines · Derrick Stolee
Quoted 4 lines · Junio C Hamano
Quoted 2 lines · Glen Choo
This patch changes the behavior of .gitignore such that directories are now matched by prefix instead of matching exactly.
Thank you for pointing out an unintended consequence.
Quoted 9 lines
Quoted 9 lines
The failure that we observed is something like the following:
In "a/.gitignore", we have the pattern "git/". We should expect that "a/git/foo" to be ignored because "git/" should be matched exactly. However, "a/git-foo/bar" is also ignored because "git-foo" matches the prefix.
I'll prepare a test case for this as soon as I figure out how to write it..
...
Quoted 2 lines
In the meantime, I'll try to create a Git test that demonstrates a problem one way or another.
I created a test, but had some trouble reproducing it due to some subtleties higher in the call stack. Here is a patch that reverts the change and adds some tests.
The Scalar functional tests passed with the revert, so the original patch was worthless to begin with. I don't recall what motivated the change, but clearly it was a mistake. Sorry.
---- >8 ----
From
d1cfc8efeab015273bfebd6cd93465e6f38dc40fMon Sep 17 00:00:00 2001
From: Derrick Stolee <[email protected]>
Date: Tue, 2 Nov 2021 10:40:06 -0400
Subject: [PATCH] dir: fix directory-matching bugThis reverts the change from ed49584 (dir: fix pattern matching on dirs, 2021-09-24), which claimed to fix a directory-matching problem without a test case. It turns out to _create_ a bug, but it is a bit subtle.
The bug would have been revealed by the first of two tests being added to
t0008-ignores.sh. The first uses a pattern "/git/" inside the a/.gitignores file, which matches against 'a/git/foo' but not 'a/git-foo/bar'. This test would fail before the revert.The second test shows what happens if the test instead uses a pattern "git/" and this test passes both before and after the revert.
The difference in these two cases are due to how
last_matching_pattern_from_list()checks patterns both if they have thePATTERN_FLAG_MUSTBEDIRandPATTERN_FLAG_NODIRflags. In the case of "git/", thePATTERN_FLAG_NODIRis also provided, making the change in behavior inmatch_pathname()not affect the end result oflast_matching_pattern_from_list().Reported-by Glen Choo <[email protected]>Signed-off-by Derrick Stolee <[email protected]>--- dir.c | 2 +- t/t0008-ignores.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-)dir.c +1
-1@@ -1294,7 +1294,7 @@ int match_pathname(const char *pathname, int pathlen,* then our prefix match is all we need; we * do not need to call fnmatch at all. */ -if (!patternlen && (!namelen || (flags &PATTERN_FLAG_MUSTBEDIR ))) +if (!patternlen && !namelen)return 1 ; }t/t0008-ignores.sh +19
-0@@ -803,6 +803,32 @@ test_expect_success 'existing directory and file' ' grep top-level-dir actual ' +test_expect_success 'exact prefix matching (with root)' ' + test_when_finished rm -r a && + mkdir -p a/git a/git-foo && + touch a/git/foo a/git-foo/bar && + echo /git/ >a/.gitignore && + git check-ignore a/git a/git/foo a/git-foo a/git-foo/bar >actual && + cat >expect <<-\EOF && + a/git + a/git/foo + EOF + test_cmp expect actual +' + +test_expect_success 'exact prefix matching (without root)' ' + test_when_finished rm -r a && + mkdir -p a/git a/git-foo && + touch a/git/foo a/git-foo/bar && + echo git/ >a/.gitignore && + git check-ignore a/git a/git/foo a/git-foo a/git-foo/bar >actual &&
t/t0008-ignores.sh test_expect_success 'existing directory and file' ' · line 834
Quoted 10 lines
t/t0008-ignores.sh +7
-0+ cat >expect <<-\EOF && + a/git + a/git/foo + EOF + test_cmp expect actual +' + ############################################################################ # # test whitespace handling
We have t3070-wildmatch.sh testing various combinations of these, and indeed this code resolves back to wildmatch().
But I think in this case this is due to dir.c's particular behavior of splitting paths before feeding them to wildmatch, as it needs to match things relative to the subdirectory.
Still, we've got a matrix of these in t3070-wildmatch.sh, which already tests some (but apparently not all) cases where we need to create an actual file on disk. These sorts of test blindspots are why I added that in de8bada2bf6 (wildmatch test: create & test files on disk in addition to in-memory, 2018-01-30).
Wouldn't it be better & more exhaustive here to change its test lines like:
match 0 0 1 1 foo/bar/baz 'bar'
To say:
match 0 0 1 1 ? ? foo/bar/baz 'bar'
And just add to its match() function so that if we have a subject with a slash, we mkdir up to that first slash (here: "mkdir foo"), and create a .gitignore file therein with the "foo" directory with the "bar" content, perhaps adding "/bar", "bar/" and "/bar/" variants implicitly.
Then create a "bar.txt" in the directory as well, and a bar-otherdir/somefile.txt or whatever.
And fill in the "? ?" depending on whether those variants matched or not...
Anyway, just an idea, but if you pursue that you should be able to get much more exhaustive testing in this area that we've apparently had a blindspot in.
Re: [PATCH v4 04/13] dir: fix pattern matching on dirs
Quoted 32 lines · Ævar Arnfjörð Bjarmason
We have
t3070-wildmatch.shtesting various combinations of these, and indeed this code resolves back towildmatch().But I think in this case this is due to dir.c's particular behavior of splitting paths before feeding them to wildmatch, as it needs to match things relative to the subdirectory.
Still, we've got a matrix of these in
t3070-wildmatch.sh, which already tests some (but apparently not all) cases where we need to create an actual file on disk. These sorts of test blindspots are why I added that inde8bada2bf6(wildmatch test: create & test files on disk in addition to in-memory, 2018-01-30).Wouldn't it be better & more exhaustive here to change its test lines like:
match 0 0 1 1 foo/bar/baz 'bar'To say:
match 0 0 1 1 ? ? foo/bar/baz 'bar'And just add to its
match()function so that if we have a subject with a slash, we mkdir up to that first slash (here: "mkdir foo"), and create a .gitignore file therein with the "foo" directory with the "bar" content, perhaps adding "/bar", "bar/" and "/bar/" variants implicitly.Then create a "bar.txt" in the directory as well, and a
bar-otherdir/somefile.txtor whatever.
Quoted 6 lines
And fill in the "? ?" depending on whether those variants matched or not...
Anyway, just an idea, but if you pursue that you should be able to get much more exhaustive testing in this area that we've apparently had a blindspot in.
Those tests are quite exhaustive, but the test script is pretty inscrutable and the refactor you suggest is pretty major. I'd prefer to keep to the focused test for the sake of fixing this in time for the release.
Thanks,
-Stolee
Re: [PATCH v4 04/13] dir: fix pattern matching on dirs
Quoted 4 lines · Derrick Stolee
Those tests are quite exhaustive, but the test script is pretty inscrutable and the refactor you suggest is pretty major. I'd prefer to keep to the focused test for the sake of fixing this in time for the release.
Thanks, both.