Sorry, one more thing...
On Wed, Oct 27, 2021 at 7:39 AM Victoria Dye via GitGitGadget
[off-list ref] wrote:
quoted hunk
From: Victoria Dye <redacted>
Remove the `ensure_full_index` guard on `read_from_tree` and update `git
reset --mixed` to ensure it can use sparse directory index entries wherever
possible. Sparse directory entries are reset use `diff_tree_oid`, which
requires `change` and `add_remove` functions to process the internal
contents of the sparse directory. The `recursive` diff option handles cases
in which `reset --mixed` must diff/merge files that are nested multiple
levels deep in a sparse directory.
The use of pathspecs with `git reset --mixed` introduces scenarios in which
internal contents of sparse directories may be matched by the pathspec. In
order to reset *all* files in the repo that may match the pathspec, the
following conditions on the pathspec require index expansion before
performing the reset:
* "magic" pathspecs
* wildcard pathspecs that do not match only in-cone files or entire sparse
directories
* literal pathspecs matching something outside the sparse checkout
definition
Helped-by: Elijah Newren [off-list ref]
Signed-off-by: Victoria Dye <redacted>
---
builtin/reset.c | 78 +++++++++++++++++++++++-
t/t1092-sparse-checkout-compatibility.sh | 17 ++++++
2 files changed, 93 insertions(+), 2 deletions(-)
@@ -781,11 +781,28 @@ test_expect_success 'sparse-index is not expanded' 'ensure_not_expandedreset--hard$ref||return1done&&+ensure_not_expandedreset--mixedbase&&ensure_not_expandedreset--hardupdate-deep&&ensure_not_expandedreset--keepbase&&ensure_not_expandedreset--mergeupdate-deep&&ensure_not_expandedreset--hard&&+ensure_not_expandedresetbase--deep/a&&+ensure_not_expandedresetbase--nonexistent-file&&+ensure_not_expandedresetdeepest--deep&&++# Although folder1 is outside the sparse definition, it exists as a+# directory entry in the index, so the pathspec will not force the+# index to be expanded.+ensure_not_expandedresetdeepest--folder1&&+ensure_not_expandedresetdeepest--folder1/&&++# Wildcard identifies only in-cone files, no index expansion+ensure_not_expandedresetdeepest--deep/\*&&++# Wildcard identifies only full sparse directories, no index expansion+ensure_not_expandedresetdeepest--folder\*&&+
You've added two testcases where a wildcard results in no index
expansion; should there also be a test where a wildcard results in
index expansion for completeness?
Hi!
On Wed, Oct 27, 2021 at 7:39 AM Victoria Dye via GitGitGadget
[off-list ref] wrote:
This series integrates the sparse index with git reset and provides
miscellaneous fixes and improvements to the command in sparse checkouts.
...
Changes since V3
================
* Replace git update-index --force-full-index with git reset update-folder1
-- folder1/a, remove introduction of new --force-full-index option
entirely, and add comment clarifying the intent of sparse-index is
expanded and converted back test
* Fix authorship on reset: preserve skip-worktree bit in mixed reset
(current patch fully replaces original patch, but metadata of the
original wasn't properly replaced)
Changes since V4
================
* Update t1092 test 'checkout and reset (mixed)' to explicitly verify
differences between sparse and full checkouts
I apologize for my tardiness in reviewing your updated series. You
have addressed all my feedback from v2 and things look really good. I
had a couple small questions on patch 7.
As with my previous review, I kinda skipped over the last patch
because I never figured out the cache_bottom stuff. I read it to see
if there were any obvious mistakes to someone unfamiliar with that
mechanism (i.e. me) but didn't see anything. I read over the earlier
patches much more carefully.
Anyway, other than patch 7 -- where I only had a minor nit on the
commit message plus two questions (which might result in no changes),
the series looks good.
From: Victoria Dye <hidden> Date: 2021-11-22 16:46:06
Elijah Newren wrote:
On Thu, Oct 7, 2021 at 2:15 PM Victoria Dye via GitGitGadget
[off-list ref] wrote:
quoted
From: Victoria Dye <redacted>
Remove the `ensure_full_index` guard on `read_from_tree` and update `git
reset --mixed` to ensure it can use sparse directory index entries wherever
possible. Sparse directory entries are reset use `diff_tree_oid`, which
I am having trouble parsing this second sentence. Was this meant to
be 'Sparse directory entries _which_ are reset use...'?
It should be "Sparse directory entries are reset _using_
`diff_tree_oid`...".
quoted
requires `change` and `add_remove` functions to process the internal
contents of the sparse directory. The `recursive` diff option handles cases
in which `reset --mixed` must diff/merge files that are nested multiple
levels deep in a sparse directory.
The use of pathspecs with `git reset --mixed` introduces scenarios in which
internal contents of sparse directories may be matched by the pathspec. In
order to reset *all* files in the repo that may match the pathspec, the
following conditions on the pathspec require index expansion before
performing the reset:
* "magic" pathspecs
* wildcard pathspecs that do not match only in-cone files or entire sparse
directories
* literal pathspecs matching something outside the sparse checkout
definition
Helped-by: Elijah Newren [off-list ref]
Signed-off-by: Victoria Dye <redacted>
---
builtin/reset.c | 78 +++++++++++++++++++++++-
t/t1092-sparse-checkout-compatibility.sh | 17 ++++++
2 files changed, 93 insertions(+), 2 deletions(-)
This double loop over all pathspecs and over all index entries reminds
me of the original non-cone mode sparsity patterns. Stolee introduced
cone mode patterns specifically to avoid the expensiveness of such
double loops (cf.
https://lore.kernel.org/git/19d664a5dada87a9a8dcf18d7548582275593f10.1566313865.git.gitgitgadget@gmail.com/).
Can one of the functions he added allow us to avoid this double loop,
or are there complications that don't allow this (e.g. the actually
SKIP_WORKTREE paths don't quite match the requested sparsity paths in
some cases, or here we are faced with just a leading path of multiple
index entries)?
quoted
+ /*
+ * If the pre-wildcard length is longer than the sparse
+ * directory name and the sparse directory is the first
+ * component of the pathspec, need to expand the index.
+ */
+ if (item.nowildcard_len > ce_namelen(ce) &&
+ !strncmp(item.original, ce->name, ce_namelen(ce))) {
+ res = 1;
+ break;
+ }
+
+ /*
+ * If the pre-wildcard length is shorter than the sparse
+ * directory and the pathspec does not match the whole
+ * directory, need to expand the index.
+ */
+ if (!strncmp(item.original, ce->name, item.nowildcard_len) &&
+ wildmatch(item.original, ce->name, 0)) {
+ res = 1;
+ break;
+ }
+ }
+ } else if (!path_in_cone_mode_sparse_checkout(item.original, &the_index) &&
+ !matches_skip_worktree(pathspec, i, &skip_worktree_seen))
Oh, so you can at least generally avoid the double loop. That's good.
So is this just a case of wildcards are special and there isn't a way,
even in cone-mode, to avoid the double loop?
The wildcard pathspecs are difficult to handle as "cleanly" as the
non-wildcard pathspecs, since the condition for expanding the index is
whether the pathspec has the potential to match some, but not all, contents
of any given sparse directory. Luckily, the double loop is constrained to
only sparse directories in the index and wildcard pathspecs, and exits as
soon as there is any indication that the index needs to be expanded.
(Given that I'm so tardy in reviewing this, even if the answer is that
the double loop is avoidable, or if we just don't know, I'd be totally
fine with a 'TODO: consider whether this double loop could be avoided
in cone mode using some kind of variant of
path_in_cone_mode_sparse_checkout()')
I had originally tried something like this, but even if a wildcard pathspec
is in-cone, it could match something outside of it. For example, '*.c' is
in-cone, but has the potential to match only some of the files in a sparse
directory containing both *.c and *.h files.
Looking at it again, though, if you constrain
`path_in_cone_mode_sparse_checkout()` pathspecs further and require that the
only post-`nowildcard_len` characters are '*', there's no risk of matching
partial subsets of files in sparse directories. I'll add that in a re-roll.
quoted
+ res = 1;
+
+ if (res > 0)
+ break;
+ }
+
+ free(skip_worktree_seen);
+ return res;
+}
+
static int read_from_tree(const struct pathspec *pathspec,
struct object_id *tree_oid,
int intent_to_add)
@@ -774,11 +774,28 @@ test_expect_success 'sparse-index is not expanded' 'ensure_not_expandedreset--hard$ref||return1done&&+ensure_not_expandedreset--mixedbase&&ensure_not_expandedreset--hardupdate-deep&&ensure_not_expandedreset--keepbase&&ensure_not_expandedreset--mergeupdate-deep&&ensure_not_expandedreset--hard&&+ensure_not_expandedresetbase--deep/a&&+ensure_not_expandedresetbase--nonexistent-file&&+ensure_not_expandedresetdeepest--deep&&++# Although folder1 is outside the sparse definition, it exists as a+# directory entry in the index, so the pathspec will not force the+# index to be expanded.+ensure_not_expandedresetdeepest--folder1&&+ensure_not_expandedresetdeepest--folder1/&&++# Wildcard identifies only in-cone files, no index expansion+ensure_not_expandedresetdeepest--deep/\*&&++# Wildcard identifies only full sparse directories, no index expansion+ensure_not_expandedresetdeepest--folder\*&&+ensure_not_expandedcheckout-fupdate-deep&&test_config-Csparse-indexpull.twoheadort&&(--
From: Victoria Dye <hidden> Date: 2021-11-22 16:54:09
Elijah Newren wrote:
Sorry, one more thing...
On Wed, Oct 27, 2021 at 7:39 AM Victoria Dye via GitGitGadget
[off-list ref] wrote:
quoted
From: Victoria Dye <redacted>
Remove the `ensure_full_index` guard on `read_from_tree` and update `git
reset --mixed` to ensure it can use sparse directory index entries wherever
possible. Sparse directory entries are reset use `diff_tree_oid`, which
requires `change` and `add_remove` functions to process the internal
contents of the sparse directory. The `recursive` diff option handles cases
in which `reset --mixed` must diff/merge files that are nested multiple
levels deep in a sparse directory.
The use of pathspecs with `git reset --mixed` introduces scenarios in which
internal contents of sparse directories may be matched by the pathspec. In
order to reset *all* files in the repo that may match the pathspec, the
following conditions on the pathspec require index expansion before
performing the reset:
* "magic" pathspecs
* wildcard pathspecs that do not match only in-cone files or entire sparse
directories
* literal pathspecs matching something outside the sparse checkout
definition
Helped-by: Elijah Newren [off-list ref]
Signed-off-by: Victoria Dye <redacted>
---
builtin/reset.c | 78 +++++++++++++++++++++++-
t/t1092-sparse-checkout-compatibility.sh | 17 ++++++
2 files changed, 93 insertions(+), 2 deletions(-)
@@ -781,11 +781,28 @@ test_expect_success 'sparse-index is not expanded' 'ensure_not_expandedreset--hard$ref||return1done&&+ensure_not_expandedreset--mixedbase&&ensure_not_expandedreset--hardupdate-deep&&ensure_not_expandedreset--keepbase&&ensure_not_expandedreset--mergeupdate-deep&&ensure_not_expandedreset--hard&&+ensure_not_expandedresetbase--deep/a&&+ensure_not_expandedresetbase--nonexistent-file&&+ensure_not_expandedresetdeepest--deep&&++# Although folder1 is outside the sparse definition, it exists as a+# directory entry in the index, so the pathspec will not force the+# index to be expanded.+ensure_not_expandedresetdeepest--folder1&&+ensure_not_expandedresetdeepest--folder1/&&++# Wildcard identifies only in-cone files, no index expansion+ensure_not_expandedresetdeepest--deep/\*&&++# Wildcard identifies only full sparse directories, no index expansion+ensure_not_expandedresetdeepest--folder\*&&+
You've added two testcases where a wildcard results in no index
expansion; should there also be a test where a wildcard results in
index expansion for completeness?
The tests haven't verified when the index *is* expanded for any of the
commands implemented so far (save for the one ensuring that, when the index
is expanded, the expansion is logged via trace2). I see the value in it
(e.g. using the tests to demonstrate what can trigger index expansion).
Conversely, if the tests are intended to confirm "successful" sparse index
support (where index expansion is equivalent to "the sparse index is not
supported"), then verifying index expansion doesn't necessarily fit that
purpose.
I'm not sure which interpretation is "correct", but if it does make sense to
test expansion cases I'm happy to add them here (or, if not in this series,
add a TODO to include them in the future).
For what it's worth, the test 'reset with wildcard pathspec' in [4/8] is
intended to cover a broader set of wildcard scenarios (verifying
correctness, rather than index expansion). Given the other updates I intend
to make to wildcard handling, I'm planning on adding cases to that test in
my next re-roll.
From: Victoria Dye via GitGitGadget <hidden> Date: 2021-11-29 19:35:05
This series integrates the sparse index with git reset and provides
miscellaneous fixes and improvements to the command in sparse checkouts.
This includes:
1. tests added to t1092 and p2000 to establish the baseline functionality
of the command
2. repository settings to enable the sparse index with ensure_full_index
guarding any code paths that break tests without other compatibility
updates.
3. modifications to remove or reduce the scope in which ensure_full_index
must be called.
The sparse index updates are predicated on a fix originating from the
microsoft/git fork [1], correcting how git reset --mixed handles resetting
entries outside the sparse checkout definition. Additionally, a performance
"bug" in next_cache_entry with sparse index is corrected, preventing
repeatedly looping over already-searched entries.
The p2000 tests demonstrate a ~70% execution time reduction in git reset
using a sparse index, and no change (within expected variability [2]) using
a full index. Results summarized below [3, 4]:
Test base [5/8]
-----------------------------------------------------------------------
git reset --hard (full-v3) 1.00(0.50+0.39) 0.97(0.50+0.37) -3.0%
git reset --hard (full-v4) 1.00(0.51+0.38) 0.96(0.50+0.36) -4.0%
git reset --hard (sparse-v3) 1.68(1.17+0.39) 1.37(0.91+0.35) -18.5%
git reset --hard (sparse-v4) 1.70(1.18+0.40) 1.41(0.94+0.35) -17.1%
Test base [6/8]
-----------------------------------------------------------------------
git reset --hard (full-v3) 1.00(0.50+0.39) 0.94(0.48+0.34) -6.0%
git reset --hard (full-v4) 1.00(0.51+0.38) 0.95(0.51+0.34) -5.0%
git reset --hard (sparse-v3) 1.68(1.17+0.39) 0.46(0.05+0.29) -72.6%
git reset --hard (sparse-v4) 1.70(1.18+0.40) 0.46(0.06+0.29) -72.9%
Test base [7/8]
---------------------------------------------------------------------------
git reset (full-v3) 0.77(0.27+0.37) 0.72(0.26+0.32) -6.5%
git reset (full-v4) 0.75(0.27+0.34) 0.73(0.26+0.32) -2.7%
git reset (sparse-v3) 1.44(0.96+0.36) 0.43(0.04+0.96) -70.1%
git reset (sparse-v4) 1.46(0.97+0.36) 0.43(0.05+0.79) -70.5%
git reset -- missing (full-v3) 0.72(0.26+0.32) 0.69(0.26+0.30) -4.2%
git reset -- missing (full-v4) 0.74(0.28+0.33) 0.71(0.27+0.32) -4.1%
git reset -- missing (sparse-v3) 1.45(0.97+0.35) 0.81(0.42+0.90) -44.1%
git reset -- missing (sparse-v4) 1.41(0.94+0.34) 0.79(0.42+0.76) -44.0%
Test base [8/8]
---------------------------------------------------------------------------
git reset -- missing (full-v3) 0.72(0.26+0.32) 0.73(0.26+0.33) +1.4%
git reset -- missing (full-v4) 0.74(0.28+0.33) 0.74(0.27+0.32) +0.0%
git reset -- missing (sparse-v3) 1.45(0.97+0.35) 0.43(0.05+0.80) -70.3%
git reset -- missing (sparse-v4) 1.41(0.94+0.34) 0.44(0.05+0.76) -68.8%
Changes since V1
================
* Add --force-full-index option to update-index. The option is used
circumvent changing command_requires_full_index from its default value -
right now this is effectively a no-op, but will change once update-index
is integrated with sparse index. By using this option in the t1092
expand/collapse test, the command used to test will not need to be
updated with subsequent sparse index integrations.
* Update implementation of mixed reset for entries outside sparse checkout
definition. The condition in which a file should be checked out before
index reset is simplified to "if it has skip-worktree enabled and a reset
would change the file, check it out".
* After checking the behavior of update_index_from_diff with renames,
found that the diff used by reset does not produce diff queue entries
with different pathnames for one and two. Because of this, and that
nothing in the implementation seems to rely on identical path names, no
BUG check is added.
* Correct a bug in the sparse index is not expanded tests in t1092 where
failure of a git reset --mixed test was not being reported. Test now
verifies an appropriate scenario with corrected failure-checking.
Changes since V2
================
* Replace patch adding checkouts for git reset --mixed with sparse checkout
with preserving the skip-worktree flag (including a new test for git
reset --mixed and update to t1092 - checkout and reset (mixed))
* Move rename of is_missing into its own patch
* Further extend t1092 tests and remove unnecessary commands/tests where
possible
* Refine logic determining which pathspecs require ensure_full_index in git
reset --mixed, add related ensure_not_expanded tests
* Add index_search_mode enum to index_name_stage_pos
* Clean up variable usage & remove unnecessary subtree_path in
prime_cache_tree_rec
* Update cover letter performance data
* More thoroughly explain changes in each commit message
Changes since V3
================
* Replace git update-index --force-full-index with git reset update-folder1
-- folder1/a, remove introduction of new --force-full-index option
entirely, and add comment clarifying the intent of sparse-index is
expanded and converted back test
* Fix authorship on reset: preserve skip-worktree bit in mixed reset
(current patch fully replaces original patch, but metadata of the
original wasn't properly replaced)
Changes since V4
================
* Update t1092 test 'checkout and reset (mixed)' to explicitly verify
differences between sparse and full checkouts
Changes since V5
================
* Update t1092 test 'reset with wildcard pathspec' with more cases and
better checks
* Add "special case" wildcard pathspec check when determining whether to
expand the index (avoids double-loop over pathspecs & index entries)
Thanks! -Victoria
[1] microsoft@6b8a074 [2]
https://lore.kernel.org/git/8b9fe3f8-f0e3-4567-b20b-17c92bd1a5c5@github.com/
[3] If a test and/or commit is not mentioned, there is no significant change
to performance [4] Pathspec "does-not-exist" is changed to "missing" to save
space in performance report
Victoria Dye (8):
reset: rename is_missing to !is_in_reset_tree
reset: preserve skip-worktree bit in mixed reset
sparse-index: update command for expand/collapse test
reset: expand test coverage for sparse checkouts
reset: integrate with sparse index
reset: make sparse-aware (except --mixed)
reset: make --mixed sparse-aware
unpack-trees: improve performance of next_cache_entry
builtin/reset.c | 113 ++++++++++++++++-
cache-tree.c | 46 ++++++-
cache.h | 10 ++
read-cache.c | 27 ++--
t/perf/p2000-sparse-operations.sh | 3 +
t/t1092-sparse-checkout-compatibility.sh | 154 ++++++++++++++++++++---
t/t7102-reset.sh | 17 +++
unpack-trees.c | 23 +++-
8 files changed, 356 insertions(+), 37 deletions(-)
base-commit: cefe983a320c03d7843ac78e73bd513a27806845
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1048%2Fvdye%2Fvdye%2Fsparse-index-part1-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1048/vdye/vdye/sparse-index-part1-v6
Pull-Request: https://github.com/gitgitgadget/git/pull/1048
Range-diff vs v5:
1: ad7013a31aa = 1: ad7013a31aa reset: rename is_missing to !is_in_reset_tree
2: b221b00b7e0 = 2: b221b00b7e0 reset: preserve skip-worktree bit in mixed reset
3: 1bb2ca92c60 = 3: 1bb2ca92c60 sparse-index: update command for expand/collapse test
4: cc76c694647 ! 4: 741a2c9ffaa reset: expand test coverage for sparse checkouts
@@ t/t1092-sparse-checkout-compatibility.sh: test_expect_success 'checkout and rese
+test_expect_success 'reset with wildcard pathspec' '
+ init_repos &&
+
-+ test_all_match git checkout -b reset-test update-deep &&
-+ test_all_match git reset base -- \*/a &&
-+ test_all_match git status --porcelain=v2 &&
-+ test_all_match git rev-parse HEAD:folder1/a &&
++ test_all_match git reset update-deep -- deep\* &&
++ test_all_match git ls-files -s -- deep &&
+
-+ test_all_match git reset base -- folder\* &&
-+ test_all_match git status --porcelain=v2 &&
-+ test_all_match git rev-parse HEAD:folder2
++ test_all_match git reset deepest -- deep\*\*\* &&
++ test_all_match git ls-files -s -- deep &&
++
++ # The following `git reset`s result in updating the index on files with
++ # `skip-worktree` enabled. To avoid failing due to discrepencies in reported
++ # "modified" files, `test_sparse_match` reset is performed separately from
++ # "full-checkout" reset, then the index contents of all repos are verified.
++
++ test_sparse_match git reset update-folder1 -- \*/a &&
++ git -C full-checkout reset update-folder1 -- \*/a &&
++ test_all_match git ls-files -s -- deep/a folder1/a &&
++
++ test_sparse_match git reset update-folder2 -- folder\* &&
++ git -C full-checkout reset update-folder2 -- folder\* &&
++ test_all_match git ls-files -s -- folder10 folder1 folder2 &&
++
++ test_sparse_match git reset base -- folder1/\* &&
++ git -C full-checkout reset base -- folder1/\* &&
++ test_all_match git ls-files -s -- folder1
+'
+
test_expect_success 'merge, cherry-pick, and rebase' '
5: 217ae445418 = 5: 65b0eafd27c reset: integrate with sparse index
6: a3e2fd59867 = 6: 908c84005b9 reset: make sparse-aware (except --mixed)
7: a9135a5ed64 ! 7: 822d7344587 reset: make --mixed sparse-aware
@@ Commit message
Remove the `ensure_full_index` guard on `read_from_tree` and update `git
reset --mixed` to ensure it can use sparse directory index entries wherever
- possible. Sparse directory entries are reset use `diff_tree_oid`, which
+ possible. Sparse directory entries are reset using `diff_tree_oid`, which
requires `change` and `add_remove` functions to process the internal
contents of the sparse directory. The `recursive` diff option handles cases
in which `reset --mixed` must diff/merge files that are nested multiple
@@ builtin/reset.c: static void update_index_from_diff(struct diff_queue_struct *q,
+ * (since we can reset whole sparse directories without expanding them).
+ */
+ if (item.nowildcard_len < item.len) {
++ /*
++ * Special case: if the pattern is a path inside the cone
++ * followed by only wildcards, the pattern cannot match
++ * partial sparse directories, so we don't expand the index.
++ */
++ if (path_in_cone_mode_sparse_checkout(item.original, &the_index) &&
++ strspn(item.original + item.nowildcard_len, "*") == item.len - item.nowildcard_len)
++ continue;
++
+ for (pos = 0; pos < active_nr; pos++) {
+ struct cache_entry *ce = active_cache[pos];
+
8: f91d1dcf024 = 8: ddd97fb2837 unpack-trees: improve performance of next_cache_entry
--
gitgitgadget
From: Victoria Dye via GitGitGadget <hidden> Date: 2021-11-29 19:35:08
From: Victoria Dye <redacted>
Rename and invert value of `is_missing` to `is_in_reset_tree` to make the
variable more descriptive of what it represents.
Signed-off-by: Victoria Dye <redacted>
---
builtin/reset.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
From: Victoria Dye via GitGitGadget <hidden> Date: 2021-11-29 19:35:42
From: Victoria Dye <redacted>
Change `update_index_from_diff` to set `skip-worktree` when applicable for
new index entries. When `git reset --mixed <tree-ish>` is run, entries in
the index with differences between the pre-reset HEAD and reset <tree-ish>
are identified and handled with `update_index_from_diff`. For each file, a
new cache entry in inserted into the index, created from the <tree-ish> side
of the reset (without changing the working tree). However, the newly-created
entry must have `skip-worktree` explicitly set in either of the following
scenarios:
1. the file is in the current index and has `skip-worktree` set
2. the file is not in the current index but is outside of a defined sparse
checkout definition
Not setting the `skip-worktree` bit leads to likely-undesirable results for
a user. It causes `skip-worktree` settings to disappear on the
"diff"-containing files (but *only* the diff-containing files), leading to
those files now showing modifications in `git status`. For example, when
running `git reset --mixed` in a sparse checkout, some file entries outside
of sparse checkout could show up as deleted, despite the user never deleting
anything (and not wanting them on-disk anyway).
Additionally, add a test to `t7102` to ensure `skip-worktree` is preserved
in a basic `git reset --mixed` scenario and update a failure-documenting
test from 19a0acc (t1092: test interesting sparse-checkout scenarios,
2021-01-23) with new expected behavior.
Helped-by: Junio C Hamano [off-list ref]
Signed-off-by: Victoria Dye <redacted>
---
builtin/reset.c | 14 ++++++++++++++
t/t1092-sparse-checkout-compatibility.sh | 22 ++++++++--------------
t/t7102-reset.sh | 17 +++++++++++++++++
3 files changed, 39 insertions(+), 14 deletions(-)
@@ -459,26 +459,20 @@ test_expect_failure 'blame with pathspec outside sparse definition' 'test_all_matchgitblamedeep/deeper2/deepest/a'-# NEEDSWORK: a sparse-checkout behaves differently from a full checkout-# in this scenario, but it shouldn't.-test_expect_failure'checkout and reset (mixed)''+test_expect_success'checkout and reset (mixed)''init_repos&&test_all_matchgitcheckout-breset-testupdate-deep&&test_all_matchgitresetdeepest&&-test_all_matchgitresetupdate-folder1&&-test_all_matchgitresetupdate-folder2-'--# NEEDSWORK: a sparse-checkout behaves differently from a full checkout-# in this scenario, but it shouldn't.-test_expect_success'checkout and reset (mixed) [sparse]''-init_repos&&-test_sparse_matchgitcheckout-breset-testupdate-deep&&-test_sparse_matchgitresetdeepest&&+# Because skip-worktree is preserved, resetting to update-folder1+# will show worktree changes for folder1/a in full-checkout, but not+# in sparse-checkout or sparse-index.+git-Cfull-checkoutresetupdate-folder1>full-checkout-out&&test_sparse_matchgitresetupdate-folder1&&-test_sparse_matchgitresetupdate-folder2+grep"M folder1/a"full-checkout-out&&+!grep"M folder1/a"sparse-checkout-out&&+run_on_sparsetest_path_is_missingfolder1' test_expect_success'merge, cherry-pick, and rebase''
@@ -472,6 +472,23 @@ test_expect_success '--mixed refreshes the index' 'test_cmpexpectoutput'+test_expect_success'--mixed preserves skip-worktree''+echo123>>file2&&+gitaddfile2&&+gitupdate-index--skip-worktreefile2&&+gitreset--mixedHEAD>output&&+test_must_be_emptyoutput&&++cat>expect<<-\EOF&&+Unstagedchangesafterreset:+Mfile2+EOF+gitupdate-index--no-skip-worktreefile2&&+gitaddfile2&&+gitreset--mixedHEAD>output&&+test_cmpexpectoutput+'+ test_expect_success'resetting specific path that is unmerged''gitrm--cachedfile2&&F1=$(gitrev-parseHEAD:file1)&&
From: Victoria Dye via GitGitGadget <hidden> Date: 2021-11-29 19:35:43
From: Victoria Dye <redacted>
In anticipation of `git reset --hard` being able to use the sparse index
without expanding it, replace the command in `sparse-index is expanded and
converted back` with `git reset -- folder1/a`. This command will need to
expand the index to work properly, even after integrating the rest of
`reset` with sparse index.
Helped-by: Derrick Stolee [off-list ref]
Signed-off-by: Victoria Dye <redacted>
---
t/t1092-sparse-checkout-compatibility.sh | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
@@ -634,11 +634,15 @@ test_expect_success 'submodule handling' 'grep"160000 commit $(git-Cinitial-reporev-parseHEAD) modules/sub"cache'+# When working with a sparse index, some commands will need to expand the+# index to operate properly. If those commands also write the index back+# to disk, they need to convert the index to sparse before writing.+# This test verifies that both of these events are logged in trace2 logs. test_expect_success'sparse-index is expanded and converted back''init_repos&&GIT_TRACE2_EVENT="$(pwd)/trace2.txt"GIT_TRACE2_EVENT_NESTING=10\-git-Csparse-index-ccore.fsmonitor=""reset--hard&&+git-Csparse-indexreset--folder1/a&&test_regionindexconvert_to_sparsetrace2.txt&&test_regionindexensure_full_indextrace2.txt'
From: Victoria Dye via GitGitGadget <hidden> Date: 2021-11-29 19:35:47
From: Victoria Dye <redacted>
Add new tests for `--merge` and `--keep` modes, as well as mixed reset with
pathspecs. New performance test cases exercise various execution paths for
`reset`.
Co-authored-by: Derrick Stolee [off-list ref]
Signed-off-by: Derrick Stolee <redacted>
Signed-off-by: Victoria Dye <redacted>
---
t/perf/p2000-sparse-operations.sh | 3 +
t/t1092-sparse-checkout-compatibility.sh | 98 ++++++++++++++++++++++++
2 files changed, 101 insertions(+)
@@ -475,6 +475,104 @@ test_expect_success 'checkout and reset (mixed)' 'run_on_sparsetest_path_is_missingfolder1'+test_expect_success'checkout and reset (merge)''+init_repos&&++write_scriptedit-contents<<-\EOF&&+echotext>>$1+EOF++test_all_matchgitcheckout-breset-testupdate-deep&&+run_on_all../edit-contentsa&&+test_all_matchgitreset--mergedeepest&&+test_all_matchgitstatus--porcelain=v2&&++test_all_matchgitreset--hardupdate-deep&&+run_on_all../edit-contentsdeep/a&&+test_all_matchtest_must_failgitreset--mergedeepest+'++test_expect_success'checkout and reset (keep)''+init_repos&&++write_scriptedit-contents<<-\EOF&&+echotext>>$1+EOF++test_all_matchgitcheckout-breset-testupdate-deep&&+run_on_all../edit-contentsa&&+test_all_matchgitreset--keepdeepest&&+test_all_matchgitstatus--porcelain=v2&&++test_all_matchgitreset--hardupdate-deep&&+run_on_all../edit-contentsdeep/a&&+test_all_matchtest_must_failgitreset--keepdeepest+'++test_expect_success'reset with pathspecs inside sparse definition''+init_repos&&++write_scriptedit-contents<<-\EOF&&+echotext>>$1+EOF++test_all_matchgitcheckout-breset-testupdate-deep&&+run_on_all../edit-contentsdeep/a&&++test_all_matchgitresetbase--deep/a&&+test_all_matchgitstatus--porcelain=v2&&++test_all_matchgitresetbase--nonexistent-file&&+test_all_matchgitstatus--porcelain=v2&&++test_all_matchgitresetdeepest--deep&&+test_all_matchgitstatus--porcelain=v2+'++# Although the working tree differs between full and sparse checkouts after+# reset, the state of the index is the same.+test_expect_success'reset with pathspecs outside sparse definition''+init_repos&&+test_all_matchgitcheckout-breset-testbase&&++test_sparse_matchgitresetupdate-folder1--folder1&&+git-Cfull-checkoutresetupdate-folder1--folder1&&+test_sparse_matchgitstatus--porcelain=v2&&+test_all_matchgitrev-parseHEAD:folder1&&++test_sparse_matchgitresetupdate-folder2--folder2/a&&+git-Cfull-checkoutresetupdate-folder2--folder2/a&&+test_sparse_matchgitstatus--porcelain=v2&&+test_all_matchgitrev-parseHEAD:folder2/a+'++test_expect_success'reset with wildcard pathspec''+init_repos&&++test_all_matchgitresetupdate-deep--deep\*&&+test_all_matchgitls-files-s--deep&&++test_all_matchgitresetdeepest--deep\*\*\*&&+test_all_matchgitls-files-s--deep&&++# The following `git reset`s result in updating the index on files with+# `skip-worktree` enabled. To avoid failing due to discrepencies in reported+# "modified" files, `test_sparse_match` reset is performed separately from+# "full-checkout" reset, then the index contents of all repos are verified.++test_sparse_matchgitresetupdate-folder1--\*/a&&+git-Cfull-checkoutresetupdate-folder1--\*/a&&+test_all_matchgitls-files-s--deep/afolder1/a&&++test_sparse_matchgitresetupdate-folder2--folder\*&&+git-Cfull-checkoutresetupdate-folder2--folder\*&&+test_all_matchgitls-files-s--folder10folder1folder2&&++test_sparse_matchgitresetbase--folder1/\*&&+git-Cfull-checkoutresetbase--folder1/\*&&+test_all_matchgitls-files-s--folder1+'+ test_expect_success'merge, cherry-pick, and rebase''init_repos&&
From: Victoria Dye via GitGitGadget <hidden> Date: 2021-11-29 19:35:48
From: Victoria Dye <redacted>
Disable `command_requires_full_index` repo setting and add
`ensure_full_index` guards around code paths that cannot yet use sparse
directory index entries. `reset --soft` does not modify the index, so no
compatibility changes are needed for it to function without expanding the
index. For all other reset modes (`--mixed`, `--hard`, `--keep`, `--merge`),
the full index is expanded to prevent cache tree corruption and invalid
variable accesses.
Additionally, the `read_cache()` check verifying an uncorrupted index is
moved after argument parsing and preparing the repo settings. The index is
not used by the preceding argument handling, but `read_cache()` must be run
*after* enabling sparse index for the command (so that the index is not
expanded unnecessarily) and *before* using the index for reset (so that it
is verified as uncorrupted).
Signed-off-by: Victoria Dye <redacted>
---
builtin/reset.c | 10 +++++++---
cache-tree.c | 1 +
2 files changed, 8 insertions(+), 3 deletions(-)
@@ -405,6 +403,12 @@ int cmd_reset(int argc, const char **argv, const char *prefix)if(intent_to_add&&reset_type!=MIXED)die(_("-N can only be used with --mixed"));+prepare_repo_settings(the_repository);+the_repository->settings.command_requires_full_index=0;++if(read_cache()<0)+die(_("index file corrupt"));+/* Soft reset does not touch the index file nor the working tree*atall,butrequirestheminagoodorder.Otherresetsreset*theindexfiletothetreeobjectweareswitchingto.*/
From: Victoria Dye via GitGitGadget <hidden> Date: 2021-11-29 19:35:50
From: Victoria Dye <redacted>
Remove `ensure_full_index` guard on `prime_cache_tree` and update
`prime_cache_tree_rec` to correctly reconstruct sparse directory entries in
the cache tree. While processing a tree's entries, `prime_cache_tree_rec`
must determine whether a directory entry is sparse or not by searching for
it in the index (*without* expanding the index). If a matching sparse
directory index entry is found, no subtrees are added to the cache tree
entry and the entry count is set to 1 (representing the sparse directory
itself). Otherwise, the tree is assumed to not be sparse and its subtrees
are recursively added to the cache tree.
Helped-by: Elijah Newren [off-list ref]
Signed-off-by: Victoria Dye <redacted>
---
cache-tree.c | 47 ++++++++++++++++++++++--
cache.h | 10 +++++
read-cache.c | 27 ++++++++++----
t/t1092-sparse-checkout-compatibility.sh | 15 +++++++-
4 files changed, 86 insertions(+), 13 deletions(-)
@@ -551,7 +556,10 @@ int cache_name_stage_compare(const char *name1, int len1, int stage1, const charreturn0;}-staticintindex_name_stage_pos(structindex_state*istate,constchar*name,intnamelen,intstage)+staticintindex_name_stage_pos(structindex_state*istate,+constchar*name,intnamelen,+intstage,+enumindex_search_modesearch_mode){intfirst,last;
@@ -570,7 +578,7 @@ static int index_name_stage_pos(struct index_state *istate, const char *name, infirst=next+1;}-if(istate->sparse_index&&+if(search_mode==EXPAND_SPARSE&&istate->sparse_index&&first>0){/* Note: first <= istate->cache_nr */structcache_entry*ce=istate->cache[first-1];
@@ -1222,7 +1235,7 @@ static int has_dir_name(struct index_state *istate,*/}-pos=index_name_stage_pos(istate,name,len,stage);+pos=index_name_stage_pos(istate,name,len,stage,EXPAND_SPARSE);if(pos>=0){/**Foundone,butnotsofast.Thiscould
@@ -1322,7 +1335,7 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_estrcmp(ce->name,istate->cache[istate->cache_nr-1]->name)>0)pos=index_pos_to_insert_pos(istate->cache_nr);else-pos=index_name_stage_pos(istate,ce->name,ce_namelen(ce),ce_stage(ce));+pos=index_name_stage_pos(istate,ce->name,ce_namelen(ce),ce_stage(ce),EXPAND_SPARSE);/* existing match? Just replace it. */if(pos>=0){
@@ -1357,7 +1370,7 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_eif(!ok_to_replace)returnerror(_("'%s' appears as both a file and as a directory"),ce->name);-pos=index_name_stage_pos(istate,ce->name,ce_namelen(ce),ce_stage(ce));+pos=index_name_stage_pos(istate,ce->name,ce_namelen(ce),ce_stage(ce),EXPAND_SPARSE);pos=-pos-1;}returnpos+1;
@@ -777,9 +777,9 @@ test_expect_success 'sparse-index is not expanded' 'ensure_not_expandedcheckout-&&ensure_not_expandedswitchrename-out-to-out&&ensure_not_expandedswitch-&&-git-Csparse-indexreset--hard&&+ensure_not_expandedreset--hard&&ensure_not_expandedcheckoutrename-out-to-out--deep/deeper1&&-git-Csparse-indexreset--hard&&+ensure_not_expandedreset--hard&&ensure_not_expandedrestore-srename-out-to-out--deep/deeper1&&echo>>sparse-index/README.md&&
@@ -789,6 +789,17 @@ test_expect_success 'sparse-index is not expanded' 'echo>>sparse-index/untracked.txt&&ensure_not_expandedadd.&&+forrefinupdate-deepupdate-folder1update-folder2update-deep+do+echo>>sparse-index/README.md&&+ensure_not_expandedreset--hard$ref||return1+done&&++ensure_not_expandedreset--hardupdate-deep&&+ensure_not_expandedreset--keepbase&&+ensure_not_expandedreset--mergeupdate-deep&&+ensure_not_expandedreset--hard&&+ensure_not_expandedcheckout-fupdate-deep&&test_config-Csparse-indexpull.twoheadort&&(
From: Victoria Dye via GitGitGadget <hidden> Date: 2021-11-29 19:35:52
From: Victoria Dye <redacted>
Remove the `ensure_full_index` guard on `read_from_tree` and update `git
reset --mixed` to ensure it can use sparse directory index entries wherever
possible. Sparse directory entries are reset using `diff_tree_oid`, which
requires `change` and `add_remove` functions to process the internal
contents of the sparse directory. The `recursive` diff option handles cases
in which `reset --mixed` must diff/merge files that are nested multiple
levels deep in a sparse directory.
The use of pathspecs with `git reset --mixed` introduces scenarios in which
internal contents of sparse directories may be matched by the pathspec. In
order to reset *all* files in the repo that may match the pathspec, the
following conditions on the pathspec require index expansion before
performing the reset:
* "magic" pathspecs
* wildcard pathspecs that do not match only in-cone files or entire sparse
directories
* literal pathspecs matching something outside the sparse checkout
definition
Helped-by: Elijah Newren [off-list ref]
Signed-off-by: Victoria Dye <redacted>
---
builtin/reset.c | 87 +++++++++++++++++++++++-
t/t1092-sparse-checkout-compatibility.sh | 17 +++++
2 files changed, 102 insertions(+), 2 deletions(-)
@@ -795,11 +795,28 @@ test_expect_success 'sparse-index is not expanded' 'ensure_not_expandedreset--hard$ref||return1done&&+ensure_not_expandedreset--mixedbase&&ensure_not_expandedreset--hardupdate-deep&&ensure_not_expandedreset--keepbase&&ensure_not_expandedreset--mergeupdate-deep&&ensure_not_expandedreset--hard&&+ensure_not_expandedresetbase--deep/a&&+ensure_not_expandedresetbase--nonexistent-file&&+ensure_not_expandedresetdeepest--deep&&++# Although folder1 is outside the sparse definition, it exists as a+# directory entry in the index, so the pathspec will not force the+# index to be expanded.+ensure_not_expandedresetdeepest--folder1&&+ensure_not_expandedresetdeepest--folder1/&&++# Wildcard identifies only in-cone files, no index expansion+ensure_not_expandedresetdeepest--deep/\*&&++# Wildcard identifies only full sparse directories, no index expansion+ensure_not_expandedresetdeepest--folder\*&&+ensure_not_expandedcheckout-fupdate-deep&&test_config-Csparse-indexpull.twoheadort&&(
From: Victoria Dye via GitGitGadget <hidden> Date: 2021-11-29 19:35:53
From: Victoria Dye <redacted>
To find the first non-unpacked cache entry, `next_cache_entry` iterates
through index, starting at `cache_bottom`. The performance of this in full
indexes is helped by `cache_bottom` advancing with each invocation of
`mark_ce_used` (called by `unpack_index_entry`). However, the presence of
sparse directories can prevent the `cache_bottom` from advancing in a sparse
index case, effectively forcing `next_cache_entry` to search from the
beginning of the index each time it is called.
The `cache_bottom` must be preserved for the sparse index (see 17a1bb570b
(unpack-trees: preserve cache_bottom, 2021-07-14)). Therefore, to retain the
benefit `cache_bottom` provides in non-sparse index cases, a separate `hint`
position indicates the first position `next_cache_entry` should search,
updated each execution with a new position.
Signed-off-by: Victoria Dye <redacted>
---
unpack-trees.c | 23 +++++++++++++++++------
1 file changed, 17 insertions(+), 6 deletions(-)
@@ -1365,12 +1372,13 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str/* Are we supposed to look at the index too? */if(o->merge){+inthint=-1;while(1){intcmp;structcache_entry*ce;if(o->diff_index_cached)-ce=next_cache_entry(o);+ce=next_cache_entry(o,&hint);elsece=find_cache_entry(info,p);
@@ -1690,7 +1698,7 @@ static int verify_absent(const struct cache_entry *,intunpack_trees(unsignedlen,structtree_desc*t,structunpack_trees_options*o){structrepository*repo=the_repository;-inti,ret;+inti,hint,ret;staticstructcache_entry*dfc;structpattern_listpl;intfree_pattern_list=0;
@@ -1790,8 +1800,9 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options/* Any left-over entries in the index? */if(o->merge){+hint=-1;while(1){-structcache_entry*ce=next_cache_entry(o);+structcache_entry*ce=next_cache_entry(o,&hint);if(!ce)break;if(unpack_index_entry(ce,o)<0)
Hi,
On Mon, Nov 29, 2021 at 7:52 AM Victoria Dye via GitGitGadget
[off-list ref] wrote:
Changes since V5
================
* Update t1092 test 'reset with wildcard pathspec' with more cases and
better checks
* Add "special case" wildcard pathspec check when determining whether to
expand the index (avoids double-loop over pathspecs & index entries)
Looks pretty good. However, I'm worried this special case you added
at my prodding might be problematic, and that I may have been wrong to
prod you into it...
Thanks! -Victoria
Range-diff vs v5:
7: a9135a5ed64 ! 7: 822d7344587 reset: make --mixed sparse-aware
@@ Commit message
Remove the `ensure_full_index` guard on `read_from_tree` and update `git
reset --mixed` to ensure it can use sparse directory index entries wherever
- possible. Sparse directory entries are reset use `diff_tree_oid`, which
+ possible. Sparse directory entries are reset using `diff_tree_oid`, which
requires `change` and `add_remove` functions to process the internal
contents of the sparse directory. The `recursive` diff option handles cases
in which `reset --mixed` must diff/merge files that are nested multiple
@@ builtin/reset.c: static void update_index_from_diff(struct diff_queue_struct *q,
+ * (since we can reset whole sparse directories without expanding them).
+ */
+ if (item.nowildcard_len < item.len) {
++ /*
++ * Special case: if the pattern is a path inside the cone
++ * followed by only wildcards, the pattern cannot match
++ * partial sparse directories, so we don't expand the index.
++ */
++ if (path_in_cone_mode_sparse_checkout(item.original, &the_index) &&
++ strspn(item.original + item.nowildcard_len, "*") == item.len - item.nowildcard_len)
I usually expect in an &&-chain to see the cheaper function call first
(because that ordering often avoids the need to call the second
function), and I would presume that strspn() would be the cheaper of
the two. Did you switch the order because you expect the strspn call
to nearly always return true, though?
Could the strspn() call be replaced by a `item.len ==
item.nowildcard_len + 1`? I mean, sure, folks could list multiple
asterisks in a row in their pathspec, but that seems super unlikely
and even if it does happen the code will just fall back to the slower
codepath and still give them the right answer. And the simpler check
feels a lot easier to parse for human readers.
But I'm worried there's a deeper issue here:
Is the wildcard character (or characters) in path treated as a literal
by path_in_cone_mode_sparse_checkout()? I think it is...and I'm
worried that may be incorrect. For example, if the path is
foo/*
and the user has done a
git sparse-checkout set foo/bar/
Then 'foo/baz/file' is not in the sparse checkout. However, 'foo/*'
should match 'foo/baz/file' and yet 'foo/*' when treated as a literal
path would be considered in the sparse checkout by
path_in_cone_mode_sparse_checkout. Does this result in the code
returning an incorrect answer? (Or did I misunderstand something so
far?)
I'm wondering if I misled you earlier in my musings about whether we
could avoid the slow codepath for pathspecs with wildcard characters.
Maybe there's no safe optimization here and wildcard characters should
always go through the slower codepath.
From: Victoria Dye <hidden> Date: 2021-11-29 23:07:27
Elijah Newren wrote:
Hi,
On Mon, Nov 29, 2021 at 7:52 AM Victoria Dye via GitGitGadget
[off-list ref] wrote:
quoted
Changes since V5
================
* Update t1092 test 'reset with wildcard pathspec' with more cases and
better checks
* Add "special case" wildcard pathspec check when determining whether to
expand the index (avoids double-loop over pathspecs & index entries)
Looks pretty good. However, I'm worried this special case you added
at my prodding might be problematic, and that I may have been wrong to
prod you into it...
quoted
Thanks! -Victoria
Range-diff vs v5:
7: a9135a5ed64 ! 7: 822d7344587 reset: make --mixed sparse-aware
@@ Commit message
Remove the `ensure_full_index` guard on `read_from_tree` and update `git
reset --mixed` to ensure it can use sparse directory index entries wherever
- possible. Sparse directory entries are reset use `diff_tree_oid`, which
+ possible. Sparse directory entries are reset using `diff_tree_oid`, which
requires `change` and `add_remove` functions to process the internal
contents of the sparse directory. The `recursive` diff option handles cases
in which `reset --mixed` must diff/merge files that are nested multiple
@@ builtin/reset.c: static void update_index_from_diff(struct diff_queue_struct *q,
+ * (since we can reset whole sparse directories without expanding them).
+ */
+ if (item.nowildcard_len < item.len) {
++ /*
++ * Special case: if the pattern is a path inside the cone
++ * followed by only wildcards, the pattern cannot match
++ * partial sparse directories, so we don't expand the index.
++ */
++ if (path_in_cone_mode_sparse_checkout(item.original, &the_index) &&
++ strspn(item.original + item.nowildcard_len, "*") == item.len - item.nowildcard_len)
I usually expect in an &&-chain to see the cheaper function call first
(because that ordering often avoids the need to call the second
function), and I would presume that strspn() would be the cheaper of
the two. Did you switch the order because you expect the strspn call
to nearly always return true, though?
This is a miss on my part, the `strspn()` check is probably less expensive
and should be first.
Could the strspn() call be replaced by a `item.len ==
item.nowildcard_len + 1`? I mean, sure, folks could list multiple
asterisks in a row in their pathspec, but that seems super unlikely
and even if it does happen the code will just fall back to the slower
codepath and still give them the right answer. And the simpler check
feels a lot easier to parse for human readers.
Agreed on wanting better readability - if the multiple-wildcard case is
unlikely, the `PATHSPEC_ONESTAR` flag would indicate whether the pathspec
ends in a single wildcard character. If that flag is still too obscure,
though, I can stick with the length comparison.
But I'm worried there's a deeper issue here:
Is the wildcard character (or characters) in path treated as a literal
by path_in_cone_mode_sparse_checkout()? I think it is...and I'm
worried that may be incorrect. For example, if the path is
foo/*
and the user has done a
git sparse-checkout set foo/bar/
Then 'foo/baz/file' is not in the sparse checkout. However, 'foo/*'
should match 'foo/baz/file' and yet 'foo/*' when treated as a literal
path would be considered in the sparse checkout by
path_in_cone_mode_sparse_checkout. Does this result in the code
returning an incorrect answer? (Or did I misunderstand something so
far?)
Correct: `path_in_cone_mode_sparse_checkout` interprets the wildcard
literally, and the checks here take that into account. The goal of
`pathspec_needs_expanded_index` is to determine if the pathspec *may* match
only partial contents of a sparse directory (like '*.c', or 'f*le'). For a
`git reset --mixed`, only this scenario requires expansion; if an entire
sparse directory is matched by a pathspec, the entire sparse directory is
reset.
Using your example, 'foo/*' does match 'foo/baz/file', but it also matches
'foo/' itself; as a result, the `foo/` sparse directory index entry is reset
(rather than some individual files contained within it). The same goes for a
patchspec like 'fo*' ("in-cone" and ending in a wildcard). Conversely, a
pathspec like 'foo/ba*' would _not_ work (it wouldn't match something like
'foo/test-file'), and neither would 'f*o' (it would match all of 'foo', but
would only match files ending in "o" in a directory 'f/').
Hope that helps!
I'm wondering if I misled you earlier in my musings about whether we
could avoid the slow codepath for pathspecs with wildcard characters.
Maybe there's no safe optimization here and wildcard characters should
always go through the slower codepath.
On Mon, Nov 29, 2021 at 11:44 AM Victoria Dye [off-list ref] wrote:
Elijah Newren wrote:
quoted
Hi,
On Mon, Nov 29, 2021 at 7:52 AM Victoria Dye via GitGitGadget
[off-list ref] wrote:
quoted
Changes since V5
================
* Update t1092 test 'reset with wildcard pathspec' with more cases and
better checks
* Add "special case" wildcard pathspec check when determining whether to
expand the index (avoids double-loop over pathspecs & index entries)
Looks pretty good. However, I'm worried this special case you added
at my prodding might be problematic, and that I may have been wrong to
prod you into it...
quoted
Thanks! -Victoria
Range-diff vs v5:
7: a9135a5ed64 ! 7: 822d7344587 reset: make --mixed sparse-aware
@@ Commit message
Remove the `ensure_full_index` guard on `read_from_tree` and update `git
reset --mixed` to ensure it can use sparse directory index entries wherever
- possible. Sparse directory entries are reset use `diff_tree_oid`, which
+ possible. Sparse directory entries are reset using `diff_tree_oid`, which
requires `change` and `add_remove` functions to process the internal
contents of the sparse directory. The `recursive` diff option handles cases
in which `reset --mixed` must diff/merge files that are nested multiple
@@ builtin/reset.c: static void update_index_from_diff(struct diff_queue_struct *q,
+ * (since we can reset whole sparse directories without expanding them).
+ */
+ if (item.nowildcard_len < item.len) {
++ /*
++ * Special case: if the pattern is a path inside the cone
++ * followed by only wildcards, the pattern cannot match
++ * partial sparse directories, so we don't expand the index.
++ */
++ if (path_in_cone_mode_sparse_checkout(item.original, &the_index) &&
++ strspn(item.original + item.nowildcard_len, "*") == item.len - item.nowildcard_len)
I usually expect in an &&-chain to see the cheaper function call first
(because that ordering often avoids the need to call the second
function), and I would presume that strspn() would be the cheaper of
the two. Did you switch the order because you expect the strspn call
to nearly always return true, though?
This is a miss on my part, the `strspn()` check is probably less expensive
and should be first.
quoted
Could the strspn() call be replaced by a `item.len ==
item.nowildcard_len + 1`? I mean, sure, folks could list multiple
asterisks in a row in their pathspec, but that seems super unlikely
and even if it does happen the code will just fall back to the slower
codepath and still give them the right answer. And the simpler check
feels a lot easier to parse for human readers.
Agreed on wanting better readability - if the multiple-wildcard case is
unlikely, the `PATHSPEC_ONESTAR` flag would indicate whether the pathspec
ends in a single wildcard character. If that flag is still too obscure,
though, I can stick with the length comparison.
quoted
But I'm worried there's a deeper issue here:
Is the wildcard character (or characters) in path treated as a literal
by path_in_cone_mode_sparse_checkout()? I think it is...and I'm
worried that may be incorrect. For example, if the path is
foo/*
and the user has done a
git sparse-checkout set foo/bar/
Then 'foo/baz/file' is not in the sparse checkout. However, 'foo/*'
should match 'foo/baz/file' and yet 'foo/*' when treated as a literal
path would be considered in the sparse checkout by
path_in_cone_mode_sparse_checkout. Does this result in the code
returning an incorrect answer? (Or did I misunderstand something so
far?)
Correct: `path_in_cone_mode_sparse_checkout` interprets the wildcard
literally, and the checks here take that into account. The goal of
`pathspec_needs_expanded_index` is to determine if the pathspec *may* match
only partial contents of a sparse directory (like '*.c', or 'f*le'). For a
`git reset --mixed`, only this scenario requires expansion; if an entire
sparse directory is matched by a pathspec, the entire sparse directory is
reset.
Using your example, 'foo/*' does match 'foo/baz/file', but it also matches
'foo/' itself; as a result, the `foo/` sparse directory index entry is reset
(rather than some individual files contained within it). The same goes for a
patchspec like 'fo*' ("in-cone" and ending in a wildcard). Conversely, a
pathspec like 'foo/ba*' would _not_ work (it wouldn't match something like
'foo/test-file'), and neither would 'f*o' (it would match all of 'foo', but
would only match files ending in "o" in a directory 'f/').
Hope that helps!
Ah, yes, thanks for the explanation. :-)
quoted
I'm wondering if I misled you earlier in my musings about whether we
could avoid the slow codepath for pathspecs with wildcard characters.
Maybe there's no safe optimization here and wildcard characters should
always go through the slower codepath.
Hi,
On Mon, Nov 29, 2021 at 7:52 AM Victoria Dye via GitGitGadget
[off-list ref] wrote:
quoted
Changes since V5
================
* Update t1092 test 'reset with wildcard pathspec' with more cases and
better checks
* Add "special case" wildcard pathspec check when determining whether to
expand the index (avoids double-loop over pathspecs & index entries)
Looks pretty good. However, I'm worried this special case you added
at my prodding might be problematic, and that I may have been wrong to
prod you into it...
quoted
Thanks! -Victoria
Range-diff vs v5:
7: a9135a5ed64 ! 7: 822d7344587 reset: make --mixed sparse-aware
@@ Commit message
Remove the `ensure_full_index` guard on `read_from_tree` and update `git
reset --mixed` to ensure it can use sparse directory index entries wherever
- possible. Sparse directory entries are reset use `diff_tree_oid`, which
+ possible. Sparse directory entries are reset using `diff_tree_oid`, which
requires `change` and `add_remove` functions to process the internal
contents of the sparse directory. The `recursive` diff option handles cases
in which `reset --mixed` must diff/merge files that are nested multiple
@@ builtin/reset.c: static void update_index_from_diff(struct diff_queue_struct *q,
+ * (since we can reset whole sparse directories without expanding them).
+ */
+ if (item.nowildcard_len < item.len) {
++ /*
++ * Special case: if the pattern is a path inside the cone
++ * followed by only wildcards, the pattern cannot match
++ * partial sparse directories, so we don't expand the index.
++ */
++ if (path_in_cone_mode_sparse_checkout(item.original, &the_index) &&
++ strspn(item.original + item.nowildcard_len, "*") == item.len - item.nowildcard_len)
I usually expect in an &&-chain to see the cheaper function call first
(because that ordering often avoids the need to call the second
function), and I would presume that strspn() would be the cheaper of
the two. Did you switch the order because you expect the strspn call
to nearly always return true, though?
This is a miss on my part, the `strspn()` check is probably less expensive
and should be first.
I doubt it matters either way, and I didn't look into this to any degree
of carefulness.
But having followed the breadcrumb trail from the "What's Cooking"
discussion & looked at the code one thing that stuck out for me was that
path_in_cone_mode_sparse_checkout() appears returns 1 inconditionally in
some cases based on global state:
/*
* 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;
So moreso than the nano-optimization of strspn()
v.s. path_in_cone_mode_sparse_checkout() I found it a bit odd that we're
calling something in a loop where presumably we can punt out a lot
earlier, and at least make that "continue" a "break" or "return" in that
case.
I.e. something in this direction (this patch obviously doesn't even
compile, but should clarify what I'm blathering about :); but again, I
really haven't looked at this properly, so just food for thought: