From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:17:56
Here is the second patch series submission coming out of the sparse-index
RFC [1].
[1]
https://lore.kernel.org/git/pull.847.git.1611596533.gitgitgadget@gmail.com/
This is based on v3 of the format series [2].
[2]
https://lore.kernel.org/git/pull.883.v3.git.1615912983.gitgitgadget@gmail.com/
The point of this series is to insert protections for the consumers of the
in-memory index. We mark certain regions of code as needing a full index, so
we call ensure_full_index() to expand a sparse index to a full one, if
necessary. These protections are inserted file-by-file in every loop over
all cache entries. Well, "most" loops, because some are going to be handled
in the very next series so I leave them out.
Many callers use index_name_pos() to find a path by name. In these cases, we
can check if that position resolves to a sparse directory instance. In those
cases, we just expand to a full index and run the search again.
The last few patches deal with the name-hash hashtable for doing O(1)
lookups.
These protections don't do much right now, since the previous series created
the_repository->settings.command_requires_full_index to guard all index
reads and writes to ensure the in-memory copy is full for commands that have
not been tested with the sparse index yet.
However, after this series is complete, we now have a straight-forward plan
for making commands "sparse aware" one-by-one:
1. Disable settings.command_requires_full_index to allow an in-memory
sparse-index.
2. Run versions of that command under a debugger, breaking on
ensure_full_index().
3. Examine the call stack to determine the context of that expansion, then
implement the proper behavior in those locations.
4. Add tests to ensure we are checking this logic in the presence of sparse
directory entries.
I will admit that mostly it is the writing of the test cases that takes the
most time in the conversions I've done so far.
Thanks, -Stolee
Derrick Stolee (27):
*: remove 'const' qualifier for struct index_state
read-cache: expand on query into sparse-directory entry
sparse-index: API protection strategy
cache: move ensure_full_index() to cache.h
add: ensure full index
checkout-index: ensure full index
checkout: ensure full index
commit: ensure full index
difftool: ensure full index
fsck: ensure full index
grep: ensure full index
ls-files: ensure full index
merge-index: ensure full index
rm: ensure full index
sparse-checkout: ensure full index
update-index: ensure full index
diff-lib: ensure full index
dir: ensure full index
entry: ensure full index
merge-ort: ensure full index
merge-recursive: ensure full index
pathspec: ensure full index
read-cache: ensure full index
resolve-undo: ensure full index
revision: ensure full index
sparse-index: expand_to_path()
name-hash: use expand_to_path()
Documentation/technical/sparse-index.txt | 32 ++++++++++-
attr.c | 14 ++---
attr.h | 4 +-
builtin/add.c | 1 +
builtin/checkout-index.c | 1 +
builtin/checkout.c | 2 +
builtin/commit.c | 2 +
builtin/difftool.c | 2 +
builtin/fsck.c | 1 +
builtin/grep.c | 1 +
builtin/ls-files.c | 12 ++--
builtin/merge-index.c | 3 +
builtin/rm.c | 1 +
builtin/stash.c | 1 +
builtin/update-index.c | 1 +
cache.h | 7 ++-
convert.c | 26 ++++-----
convert.h | 20 +++----
diff-lib.c | 1 +
dir.c | 13 +++--
dir.h | 8 +--
entry.c | 1 +
merge-ort.c | 1 +
merge-recursive.c | 3 +-
name-hash.c | 10 ++++
pathspec.c | 7 ++-
pathspec.h | 6 +-
read-cache.c | 33 +++++++++--
resolve-undo.c | 2 +
revision.c | 1 +
sparse-index.c | 72 ++++++++++++++++++++++++
sparse-index.h | 14 ++++-
submodule.c | 6 +-
submodule.h | 6 +-
34 files changed, 243 insertions(+), 72 deletions(-)
base-commit: 3db06ac46dd5c61e83d7fc4747615d616fdbbdda
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-906%2Fderrickstolee%2Fsparse-index%2Fprotections-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-906/derrickstolee/sparse-index/protections-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/906
--
gitgitgadget
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:17:56
From: Derrick Stolee <redacted>
Callers to index_name_pos() or index_name_stage_pos() have a specific
path in mind. If that happens to be a path with an ancestor being a
sparse-directory entry, it can lead to unexpected results.
In the case that we did not find the requested path, check to see if the
position _before_ the inserted position is a sparse directory entry that
matches the initial segment of the input path (including the directory
separator at the end of the directory name). If so, then expand the
index to be a full index and search again. This expansion will only
happen once per index read.
Future enhancements could be more careful to expand only the necessary
sparse directory entry, but then we would have a special "not fully
sparse, but also not fully expanded" mode that could affect writing the
index to file. Since this only occurs if a specific file is requested
outside of the sparse checkout definition, this is unlikely to be a
common situation.
Signed-off-by: Derrick Stolee <redacted>
---
read-cache.c | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:17:56
From: Derrick Stolee <redacted>
Edit and expand the sparse-index design document with the plan for
guarding index operations with ensure_full_index().
Notably, the plan has changed to not have an expand_to_path() method in
favor of checking for a sparse-directory hit inside of the
index_path_pos() API.
The changes that follow this one will incrementally add
ensure_full_index() guards to iterations over all cache entries. Some
iterations over the cache entries are not protected due to a few
categories listed in the document. Since these are not being modified,
here is a short list of the files and methods that will not receive
these guards:
Looking for non-zero stage:
* builtin/add.c:chmod_pathspec()
* builtin/merge.c:count_unmerged_entries()
* read-cache.c:unmerged_index()
* rerere.c:check_one_conflict(), find_conflict(), rerere_remaining()
* revision.c:prepare_show_merge()
* sequencer.c:append_conflicts_hint()
* wt-status.c:wt_status_collect_changes_initial()
Looking for submodules:
* builtin/submodule--helper.c:module_list_compute()
* submodule.c: several methods
* worktree.c:validate_no_submodules()
Part of the index API:
* name-hash.c: lazy init methods
* preload-index.c:preload_thread(), preload_index()
* read-cache.c: file format methods
Checking for correct order of cache entries:
* read-cache.c:check_ce_order()
Ignores SKIP_WORKTREE entries or already aware:
* unpack-trees.c:mark_new_skip_worktree()
* wt-status.c:wt_status_check_sparse_checkout()
Signed-off-by: Derrick Stolee <redacted>
---
Documentation/technical/sparse-index.txt | 32 +++++++++++++++++++++---
1 file changed, 29 insertions(+), 3 deletions(-)
@@ -82,9 +82,35 @@ also introduce other features that have been considered for improving the index, as well. Next, consumers of the index will be guarded against operating on a-sparse-index by inserting calls to `ensure_full_index()` or-`expand_index_to_path()`. After these guards are in place, we can begin-leaving sparse-directory entries in the in-memory index structure.+sparse-index by inserting calls to `ensure_full_index()` before iterating+over all cache entries. If a specific path is requested, then those will+be protected from within the `index_file_exists()` and `index_name_pos()`+API calls: they will call `ensure_full_index()` if necessary.++During a scan of the codebase, not every iteration of the cache entries+needs an `ensure_full_index()` check. The basic reasons include:++1. The loop is scanning for entries with non-zero stage. These entries+ are not collapsed into a sparse-directory entry.++2. The loop is scanning for submodules. These entries are not collapsed+ into a sparse-directory entry.++3. The loop is part of the index API, especially around reading or+ writing the format.++4. The loop is checking for correct order of cache entries and that is+ correct if and only if the sparse-directory entries are in the correct+ location.++5. The loop ignores entries with the `SKIP_WORKTREE` bit set, or is+ otherwise already aware of sparse directory entries.++6. The sparse-index is disabled at this point when using the split-index+ feature, so no effort is made to protect the split-index API.++After these guards are in place, we can begin leaving sparse-directory+entries in the in-memory index structure. Even after inserting these guards, we will keep expanding sparse-indexes for most Git commands using the `command_requires_full_index` repository
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:17:56
From: Derrick Stolee <redacted>
Soon we will insert ensure_full_index() calls across the codebase.
Instead of also adding include statements for sparse-index.h, let's just
use the fact that anything that cares about the index already has
cache.h in its includes.
Signed-off-by: Derrick Stolee <redacted>
---
cache.h | 1 +
sparse-index.h | 1 -
2 files changed, 1 insertion(+), 1 deletion(-)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:17:56
From: Derrick Stolee <redacted>
Several methods specify that they take a 'struct index_state' pointer
with the 'const' qualifier because they intend to only query the data,
not change it. However, we will be introducing a step very low in the
method stack that might modify a sparse-index to become a full index in
the case that our queries venture inside a sparse-directory entry.
This change only removes the 'const' qualifiers that are necessary for
the following change which will actually modify the implementation of
index_name_stage_pos().
Signed-off-by: Derrick Stolee <redacted>
---
attr.c | 14 +++++++-------
attr.h | 4 ++--
builtin/ls-files.c | 10 +++++-----
cache.h | 6 +++---
convert.c | 26 +++++++++++++-------------
convert.h | 20 ++++++++++----------
dir.c | 12 ++++++------
dir.h | 8 ++++----
merge-recursive.c | 2 +-
pathspec.c | 6 +++---
pathspec.h | 6 +++---
read-cache.c | 10 +++++-----
submodule.c | 6 +++---
submodule.h | 6 +++---
14 files changed, 68 insertions(+), 68 deletions(-)
@@ -850,8 +850,8 @@ int add_file_to_index(struct index_state *, const char *path, int flags);intchmod_index_entry(structindex_state*,structcache_entry*ce,charflip);intce_same_name(conststructcache_entry*a,conststructcache_entry*b);voidset_object_name_for_intent_to_add_entry(structcache_entry*ce);-intindex_name_is_other(conststructindex_state*,constchar*,int);-void*read_blob_data_from_index(conststructindex_state*,constchar*,unsignedlong*);+intindex_name_is_other(structindex_state*,constchar*,int);+void*read_blob_data_from_index(structindex_state*,constchar*,unsignedlong*);/* do stat comparison even if CE_VALID is true */#define CE_MATCH_IGNORE_VALID 01
@@ -548,7 +548,7 @@ int cache_name_stage_compare(const char *name1, int len1, int stage1, const charreturn0;}-staticintindex_name_stage_pos(conststructindex_state*istate,constchar*name,intnamelen,intstage)+staticintindex_name_stage_pos(structindex_state*istate,constchar*name,intnamelen,intstage){intfirst,last;
@@ -33,7 +33,7 @@ static struct oid_array ref_tips_after_fetch;*willbedisabledbecausewecan'tguesswhatmightbeconfiguredin*.gitmodulesunlesstheuserresolvestheconflict.*/-intis_gitmodules_unmerged(conststructindex_state*istate)+intis_gitmodules_unmerged(structindex_state*istate){intpos=index_name_pos(istate,GITMODULES_FILE,strlen(GITMODULES_FILE));if(pos<0){/* .gitmodules not found or isn't merged */
@@ -301,7 +301,7 @@ int is_submodule_populated_gently(const char *path, int *return_error_code)/**Diesiftheprovided'prefix'correspondstoanunpopulatedsubmodule*/-voiddie_in_unpopulated_submodule(conststructindex_state*istate,+voiddie_in_unpopulated_submodule(structindex_state*istate,constchar*prefix){inti,prefixlen;
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:26
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full one to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/merge-index.c | 3 +++
1 file changed, 3 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:27
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full one to avoid missing files.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/ls-files.c | 2 ++
1 file changed, 2 insertions(+)
@@ -495,6 +496,7 @@ void overlay_tree_on_index(struct index_state *istate,die("bad tree-ish %s",tree_name);/* Hoist the unmerged entries up to stage #3 to make room */+ensure_full_index(istate);for(i=0;i<istate->cache_nr;i++){structcache_entry*ce=istate->cache[i];if(!ce_stage(ce))
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:28
From: Derrick Stolee <redacted>
These two loops iterate over all cache entries, so ensure that a sparse
index is expanded to a full index before we do so.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/commit.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:28
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/add.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:28
From: Derrick Stolee <redacted>
Before iterating over all cache entries in the checkout builtin, ensure
that we have a full index to avoid any unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/checkout.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:28
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index has
been expanded to a full one to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/difftool.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:28
From: Derrick Stolee <redacted>
When adjusting the sparse-checkout definition, ensure that a sparse
index is expanded to a full one to avoid unexpected behavior. This is a
top candidate for later integration with the sparse-index, but requires
careful testing.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/stash.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:28
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/rm.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:28
From: Derrick Stolee <redacted>
When verifying all blobs reachable from the index, ensure that a sparse
index has been expanded to a full one to avoid missing some blobs.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/fsck.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:28
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full one so we do not miss blobs to scan. Later, this can
integrate more carefully with sparse indexes with proper testing.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/grep.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:28
From: Derrick Stolee <redacted>
A sparse-index loads the name-hash data for its entries, including the
sparse-directory entries. If a caller asks for a path that is contained
within a sparse-directory entry, we need to expand to a full index and
recalculate the name hash table before returning the result. Insert
calls to expand_to_path() to protect against this case.
Signed-off-by: Derrick Stolee <redacted>
---
name-hash.c | 10 ++++++++++
1 file changed, 10 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:28
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/update-index.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:29
From: Derrick Stolee <redacted>
Before we iterate over all cache entries, ensure that the index is not
sparse. This loop in checkout_all() might be safe to iterate over a
sparse index, but let's put this protection here until it can be
carefully tested.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/checkout-index.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:29
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
dir.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:29
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
diff-lib.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:29
From: Derrick Stolee <redacted>
Some users of the index API have a specific path they are looking for,
but choose to use index_file_exists() to rely on the name-hash hashtable
instead of doing binary search with index_name_pos(). These users only
need to know a yes/no answer, not a position within the cache array.
When the index is sparse, the name-hash hash table does not contain the
full list of paths within sparse directories. It _does_ contain the
directory names for the sparse-directory entries.
Create a helper function, expand_to_path(), for intended use with the
name-hash hashtable functions. The integration with name-hash.c will
follow in a later change.
The solution here is to use ensure_full_index() when we determine that
the requested path is within a sparse directory entry. This will
populate the name-hash hashtable as the index is recomputed from
scratch.
There may be cases where the caller is trying to find an untracked path
that is not in the index but also is not within a sparse directory
entry. We want to minimize the overhead for these requests. If we used
index_name_pos() to find the insertion order of the path, then we could
determine from that position if a sparse-directory exists. (In fact,
just calling index_name_pos() in that case would lead to expanding the
index to a full index.) However, this takes O(log N) time where N is the
number of cache entries.
To keep the performance of this call based mostly on the input string,
use index_file_exists() to look for the ancestors of the path. Using the
heuristic that a sparse directory is likely to have a small number of
parent directories, we start from the bottom and build up. Use a string
buffer to allow mutating the path name to terminate after each slash for
each hashset test.
Signed-off-by: Derrick Stolee <redacted>
---
sparse-index.c | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++
sparse-index.h | 13 +++++++++
2 files changed, 85 insertions(+)
@@ -291,3 +291,75 @@ void ensure_full_index(struct index_state *istate)trace2_region_leave("index","ensure_full_index",istate->repo);}++/*+*Thisstaticglobalhelpsavoidinfiniterecursionbetween+*expand_to_path()andindex_file_exists().+*/+staticintin_expand_to_path=0;++voidexpand_to_path(structindex_state*istate,+constchar*path,size_tpathlen,inticase)+{+structstrbufpath_mutable=STRBUF_INIT;+size_tsubstr_len;++/* prevent extra recursion */+if(in_expand_to_path)+return;++if(!istate||!istate->sparse_index)+return;++if(!istate->repo)+istate->repo=the_repository;++in_expand_to_path=1;++/*+*Weonlyneedtoactuallyexpandaregionifthe+*followingarebothtrue:+*+*1.'path'isnotalreadyintheindex.+*2.Someparentdirectoryof'path'isasparsedirectory.+*/++if(index_file_exists(istate,path,pathlen,icase))+gotocleanup;++strbuf_add(&path_mutable,path,pathlen);+strbuf_addch(&path_mutable,'/');++/* Check the name hash for all parent directories */+substr_len=0;+while(substr_len<pathlen){+chartemp;+char*replace=strchr(path_mutable.buf+substr_len,'/');++if(!replace)+break;++/* replace the character _after_ the slash */+replace++;+temp=*replace;+*replace='\0';+if(index_file_exists(istate,path_mutable.buf,+path_mutable.len,icase)){+/*+*Wefoundaparentdirectoryinthename-hash+*hashtable,whichmeansthatthisentrycould+*existwithinasparse-directoryentry.Expand+*accordingly.+*/+ensure_full_index(istate);+break;+}++*replace=temp;+substr_len=replace-path_mutable.buf;+}++cleanup:+strbuf_release(&path_mutable);+in_expand_to_path=0;+}
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:29
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
read-cache.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:29
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
resolve-undo.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:29
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
merge-recursive.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:29
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
entry.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:29
From: Derrick Stolee <redacted>
Before iterating over all index entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior. This case could
be integrated later by ensuring that we walk the tree in the
sparse-directory entry, but the current behavior is only expecting
blobs. Save this integration for later when it can be properly tested.
Signed-off-by: Derrick Stolee <redacted>
---
revision.c | 1 +
1 file changed, 1 insertion(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:29
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full one to avoid unexpected behavior.
This is a top candidate for updating later with a proper integration
with the sparse index, since we will likely enable the ORT strategy by
default when the sparse index is enabled. This appears to be the only
place where the ORT strategy interacts with the index in such a global
way, so that integration should be clear once the ORT strategy and the
sparse index topics stabilize.
Signed-off-by: Derrick Stolee <redacted>
---
merge-ort.c | 1 +
1 file changed, 1 insertion(+)
@@ -3112,6 +3112,7 @@ static int record_conflicted_index_entries(struct merge_options *opt,if(strmap_empty(conflicted))return0;+ensure_full_index(index);original_cache_nr=index->cache_nr;/* Put every entry from paths into plist, then sort */
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-03-16 21:18:30
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
pathspec.c | 1 +
1 file changed, 1 insertion(+)
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
quoted hunk
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/add.c | 1 +
1 file changed, 1 insertion(+)
@@ -141,6 +141,7 @@ static int renormalize_tracked_files(const struct pathspec *pathspec, int flags){inti,retval=0;+ensure_full_index(&the_index);for(i=0;i<active_nr;i++){structcache_entry*ce=active_cache[i];
I'm not so sure about this one; from git-add(1):
--renormalize
Apply the "clean" process freshly to all tracked files to forcibly
add them again to the index. This is useful after changing
core.autocrlf configuration or the text attribute in order to
correct files added with wrong CRLF/LF line endings. This option
implies -u.
... to "all tracked files". Should that be interpreted as all files
within the sparsity paths, especially considering that we're trying to
enable folks to work within the set of files of interest to them? If
it doesn't imply that, wouldn't users want an extra option to be able
to behave that way? And if we add an option, why are we adding a
special option for people wanting to behave sparsely in a
sparse-index-cone-mode-sparse-checkout rather than for the special
case of folks wanting to behave densely in such a setup?
The code below already has the following check below:
if (!S_ISREG(ce->ce_mode) && !S_ISLNK(ce->ce_mode))
continue; /* do not touch non blobs */
suggesting that it'd correctly skip over the sparse directories, so I
think this one just isn't needed.
However, on a tangent, that S_ISLNK looks rather suspicious to me.
Why would we renormalize symlinks? I mean, sure, symlinks aren't
likely to have CRLF/LF characters in them, but if they did, wouldn't
it be wrong to renormalize them?
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
quoted hunk
From: Derrick Stolee <redacted>
Before we iterate over all cache entries, ensure that the index is not
sparse. This loop in checkout_all() might be safe to iterate over a
sparse index, but let's put this protection here until it can be
carefully tested.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/checkout-index.c | 1 +
1 file changed, 1 insertion(+)
With the caveat in the commit message, this change looks okay, but
checkout-index may be buggy regardless of the presence of
ensure_full_index(). If ensure_full_index() really is needed here
because it needs to operate on all SKIP_WORKTREE paths and not just
leading directories, that's because it's writing all those
SKIP_WORKTREE entries to the working tree. When it writes them to the
working tree, is it clearing the SKIP_WORKTREE bit? If not, we're in
a bit of a pickle...
Might be nice to add a
/* TODO: audit if this is needed; if it is, we may have other bugs... */
or something like that. But then again, perhaps you're considering
all uses of ensure_full_index() to be need-to-be-reaudited codepaths?
If so, and we determine we really do need one and want to keep it
indefinitely, will we mark those with a comment about why it's
considered correct?
I just want a way to know what still needs to be audited and what
doesn't without doing a lot of history spelunking...
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
Here is the second patch series submission coming out of the sparse-index
RFC [1].
[1]
https://lore.kernel.org/git/pull.847.git.1611596533.gitgitgadget@gmail.com/
This is based on v3 of the format series [2].
[2]
https://lore.kernel.org/git/pull.883.v3.git.1615912983.gitgitgadget@gmail.com/
The point of this series is to insert protections for the consumers of the
in-memory index. We mark certain regions of code as needing a full index, so
we call ensure_full_index() to expand a sparse index to a full one, if
necessary. These protections are inserted file-by-file in every loop over
all cache entries. Well, "most" loops, because some are going to be handled
in the very next series so I leave them out.
Many callers use index_name_pos() to find a path by name. In these cases, we
can check if that position resolves to a sparse directory instance. In those
cases, we just expand to a full index and run the search again.
The last few patches deal with the name-hash hashtable for doing O(1)
lookups.
These protections don't do much right now, since the previous series created
the_repository->settings.command_requires_full_index to guard all index
reads and writes to ensure the in-memory copy is full for commands that have
not been tested with the sparse index yet.
However, after this series is complete, we now have a straight-forward plan
for making commands "sparse aware" one-by-one:
1. Disable settings.command_requires_full_index to allow an in-memory
sparse-index.
2. Run versions of that command under a debugger, breaking on
ensure_full_index().
3. Examine the call stack to determine the context of that expansion, then
implement the proper behavior in those locations.
4. Add tests to ensure we are checking this logic in the presence of sparse
directory entries.
I started reading the series, then noticed I didn't like the first few
additions of ensure_full_index(). The first was because I thought it
just wasn't needed as per a few lines of code later, but the more
important one that stuck out to me was another where if the
ensure_full_index() call is needed to avoid the code blowing up, then
the code has a good chance that it is doing something inherently wrong
in a sparse-checkout/sparse-index anyway.
So I guess that brings me to the question I asked in 07/27 -- is the
presence of ensure_full_index() a note that the code needs to be later
audited and tweaked to work with sparse-indexes? If so, then good,
this series may make sense. However, will that always be the case?
If we think some of these will be left in the code, is there a plan to
annotate each one that has been audited and determined that it needs
to stay? If not, then each ensure_full_index() might or might not
have been audited for correctness and it becomes a pain to know what's
left to fix. If the plan is that these are to be audited, and to be
marked if they are truly deemed necessary, then the series makes sense
to me. If not, then I'm confused about something with the series and
need some help with the goals and plans.
If I'm confused about the goals and the plans, then my reviews will
probably be less than helpful, so I'll suspend reading the series
until I understand the plan a little better.
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
With the caveat in the commit message, this change looks okay, but
checkout-index may be buggy regardless of the presence of
ensure_full_index(). If ensure_full_index() really is needed here
because it needs to operate on all SKIP_WORKTREE paths and not just
leading directories, that's because it's writing all those
SKIP_WORKTREE entries to the working tree. When it writes them to the
working tree, is it clearing the SKIP_WORKTREE bit? If not, we're in
a bit of a pickle...
Perhaps I'm unclear in my intentions with this series: _every_
insertion of ensure_full_index() is intended to be audited with
tests in the future. Some might need behavior change, and others
will not. In this series, I'm just putting in the protections so
we don't accidentally trigger unexpected behavior.
Since tests take time to write and review, I was hoping that these
insertions were minimal enough to get us to a safe place where we
can remove the guards carefully.
So with that in mind...
Might be nice to add a
/* TODO: audit if this is needed; if it is, we may have other bugs... */
or something like that. But then again, perhaps you're considering
all uses of ensure_full_index() to be need-to-be-reaudited codepaths?
If so, and we determine we really do need one and want to keep it
indefinitely, will we mark those with a comment about why it's
considered correct?
I just want a way to know what still needs to be audited and what
doesn't without doing a lot of history spelunking...
...every insertion "needs to be audited" in the future. That's a
big part of the next "phases" in the implementation plan.
As you suggest, it might be a good idea to add a comment to every
insertion, to mark it as un-audited, such as:
/* TODO: test if ensure_full_index() is necessary */
We can come back later to delete the comment if it truly is
necessary (and add tests to guarantee correct behavior). We can
also remove the comment _and_ the call by modifying the loop
behavior to do the right thing in some cases.
Thanks,
-Stolee
On Wed, Mar 17, 2021 at 2:36 PM Elijah Newren [off-list ref] wrote:
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
quoted
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/add.c | 1 +
1 file changed, 1 insertion(+)
@@ -141,6 +141,7 @@ static int renormalize_tracked_files(const struct pathspec *pathspec, int flags){inti,retval=0;+ensure_full_index(&the_index);for(i=0;i<active_nr;i++){structcache_entry*ce=active_cache[i];
I'm not so sure about this one; from git-add(1):
--renormalize
Apply the "clean" process freshly to all tracked files to forcibly
add them again to the index. This is useful after changing
core.autocrlf configuration or the text attribute in order to
correct files added with wrong CRLF/LF line endings. This option
implies -u.
... to "all tracked files". Should that be interpreted as all files
within the sparsity paths, especially considering that we're trying to
enable folks to work within the set of files of interest to them?
I had the same question when working on the add/rm sparse-checkout
series. As seen at [1], --renormalize and --chmod are, currently, the
only options in git-add that do not honor the sparsity patterns and do
update SKIP_WORKTREE paths too.
But is this by design or possibly just an oversight? In my series I
ended up making both options honor the sparsity rules, with a warning
when requested to update any sparse path. (If we are going with this
approach, perhaps should we also amend the docs to remove any
ambiguity as for what "all tracked files" means in this case?)
The code below already has the following check below:
if (!S_ISREG(ce->ce_mode) && !S_ISLNK(ce->ce_mode))
continue; /* do not touch non blobs */
suggesting that it'd correctly skip over the sparse directories, so I
think this one just isn't needed.
But if sparse index is not enabled, it does not skip over the sparse
files, right? So isn't the ensure_full_index() call here (and in
chmod_pathspec()) important to be consistent with the case when sparse
index is not in use? Or maybe Stolee could rebase this on top of my
series, where both these places already skip the sparse paths?
(Assuming that's the behavior we are looking for. If not, I can also
fix my series.)
[1]: https://lore.kernel.org/git/d93c3f51465a3e409819db63bd81802e7ef20ea9.1615588108.git.matheus.bernardino@usp.br/
On 3/17/2021 4:35 PM, Matheus Tavares Bernardino wrote:
On Wed, Mar 17, 2021 at 2:36 PM Elijah Newren [off-list ref] wrote:
quoted
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:>> I'm not so sure about this one; from git-add(1):
--renormalize
Apply the "clean" process freshly to all tracked files to forcibly
add them again to the index. This is useful after changing
core.autocrlf configuration or the text attribute in order to
correct files added with wrong CRLF/LF line endings. This option
implies -u.
... to "all tracked files". Should that be interpreted as all files
within the sparsity paths, especially considering that we're trying to
enable folks to work within the set of files of interest to them?
I wrote in reply to another similar comment that I'm being overly
cautious in creating these protections. When I can come back later
with careful tests, we can ensure that everything behaves properly.
I had the same question when working on the add/rm sparse-checkout
series. As seen at [1], --renormalize and --chmod are, currently, the
only options in git-add that do not honor the sparsity patterns and do
update SKIP_WORKTREE paths too.
But is this by design or possibly just an oversight? In my series I
ended up making both options honor the sparsity rules, with a warning
when requested to update any sparse path. (If we are going with this
approach, perhaps should we also amend the docs to remove any
ambiguity as for what "all tracked files" means in this case?)
I'd be happy to see a resolution here, and it can happen in parallel
to what I'm doing here.
quoted
The code below already has the following check below:
if (!S_ISREG(ce->ce_mode) && !S_ISLNK(ce->ce_mode))
continue; /* do not touch non blobs */
suggesting that it'd correctly skip over the sparse directories, so I
think this one just isn't needed.
But if sparse index is not enabled, it does not skip over the sparse
files, right? So isn't the ensure_full_index() call here (and in
chmod_pathspec()) important to be consistent with the case when sparse
index is not in use?
The tests I am adding in t1092-sparse-checkout-compatibility.sh are
focused on ensuring the same behavior across sparse-checkouts with
and without the sparse-index, and also a full checkout (when possible).
Since the sparse-index requires cone-mode sparse-checkout (currently),
and the sparse files to be within a directory (or no index change
happens), the tests you created in t3705-add-sparse-checkout.sh don't
seem to apply. I'll need to find some important scenarios to duplicate
in t1092 without the full depth of t3705.
Or maybe Stolee could rebase this on top of my
series, where both these places already skip the sparse paths?
(Assuming that's the behavior we are looking for. If not, I can also> fix my series.)
I think they can proceed in parallel and then continue more carefully
in the future. The series _after_ this one makes 'git status' and
'git add' work with the sparse-index, so at that point we will be
removing these protections at the right time, with tests. In addition,
those tests will ensure that we don't expand the sparse-index unless
absolutely necessary.
If that work collides with your approach, then I'll rebase onto a
merge of that topic and this one.
On Wed, Mar 17, 2021 at 1:05 PM Derrick Stolee [off-list ref] wrote:
On 3/17/2021 1:50 PM, Elijah Newren wrote:
quoted
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
With the caveat in the commit message, this change looks okay, but
checkout-index may be buggy regardless of the presence of
ensure_full_index(). If ensure_full_index() really is needed here
because it needs to operate on all SKIP_WORKTREE paths and not just
leading directories, that's because it's writing all those
SKIP_WORKTREE entries to the working tree. When it writes them to the
working tree, is it clearing the SKIP_WORKTREE bit? If not, we're in
a bit of a pickle...
Perhaps I'm unclear in my intentions with this series: _every_
insertion of ensure_full_index() is intended to be audited with
tests in the future. Some might need behavior change, and others
will not. In this series, I'm just putting in the protections so
we don't accidentally trigger unexpected behavior.
I think this may be part of my qualms -- what do you mean by not
accidentally triggering unexpected behavior? In particular, does your
statement imply that whatever behavior you get after putting in
ensure_full_index() is "expected"? I think I'm reading that
implication into it, and objecting that the behavior with the
ensure_full_index() still isn't expected. You've only removed a
certain class of unexpected behavior, namely code that wasn't written
to expect tree entries that suddenly gets them. You haven't handled
the class of "user wants to work with a subset of files, why are all
these unrelated files being munged/updated/computed/shown/etc."
unexpected behavior.
I'm worrying that expectations are being set up such that working with
just a small section of the code will be unusably hard. There may be
several commands/flags where it could make sense to operate on either
(a) all files in the repo or (b) just on files within your sparse
paths. If, though, folks interpret operate-on-all-files as the
"normal" mode (and history suggests they will), then people start
adding all kinds of --no-do-this-sparsely flags to each command, and
then users who want sparse operation have to remember to type such a
flag with each and every command they ever run -- despite having taken
at least three steps already to get a sparse-index.
I believe the extended discussions (for _months_!) on just grep & rm,
plus watching a --sparse patch being floated just in the last day for
ls-files suggest to me that this is a _very_ likely outcome and I'm
worried about it.
Since tests take time to write and review, I was hoping that these
insertions were minimal enough to get us to a safe place where we
can remove the guards carefully.
So with that in mind...
quoted
Might be nice to add a
/* TODO: audit if this is needed; if it is, we may have other bugs... */
or something like that. But then again, perhaps you're considering
all uses of ensure_full_index() to be need-to-be-reaudited codepaths?
If so, and we determine we really do need one and want to keep it
indefinitely, will we mark those with a comment about why it's
considered correct?
I just want a way to know what still needs to be audited and what
doesn't without doing a lot of history spelunking...
...every insertion "needs to be audited" in the future. That's a
big part of the next "phases" in the implementation plan.
As you suggest, it might be a good idea to add a comment to every
insertion, to mark it as un-audited, such as:
/* TODO: test if ensure_full_index() is necessary */
We can come back later to delete the comment if it truly is
necessary (and add tests to guarantee correct behavior). We can
also remove the comment _and_ the call by modifying the loop
behavior to do the right thing in some cases.
If it's "needs to be audited for both performance reasons (can we
operate on fewer entries as an invisible doesn't-change-results
optimization) and correctness reasons (should we operate on fewer
entries and given a modified result within a sparse-index because
users would expect that, but maybe provide a special flag for the
users who want to operate on all files in the repo)" and there's also
an agreement that either audited or unaudited ones will be marked (or
both), then great, I'm happy. If not, can we discuss which part of my
performance/correctness/marking we aren't in agreement on?
Thanks,
Elijah
On Wed, Mar 17, 2021 at 1:05 PM Derrick Stolee [off-list ref] wrote:
quoted
On 3/17/2021 1:50 PM, Elijah Newren wrote:
quoted
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
With the caveat in the commit message, this change looks okay, but
checkout-index may be buggy regardless of the presence of
ensure_full_index(). If ensure_full_index() really is needed here
because it needs to operate on all SKIP_WORKTREE paths and not just
leading directories, that's because it's writing all those
SKIP_WORKTREE entries to the working tree. When it writes them to the
working tree, is it clearing the SKIP_WORKTREE bit? If not, we're in
a bit of a pickle...
Perhaps I'm unclear in my intentions with this series: _every_
insertion of ensure_full_index() is intended to be audited with
tests in the future. Some might need behavior change, and others
will not. In this series, I'm just putting in the protections so
we don't accidentally trigger unexpected behavior.
I think this may be part of my qualms -- what do you mean by not
accidentally triggering unexpected behavior? In particular, does your
statement imply that whatever behavior you get after putting in
ensure_full_index() is "expected"? I think I'm reading that
implication into it, and objecting that the behavior with the
ensure_full_index() still isn't expected. You've only removed a
certain class of unexpected behavior, namely code that wasn't written
to expect tree entries that suddenly gets them. You haven't handled
the class of "user wants to work with a subset of files, why are all
these unrelated files being munged/updated/computed/shown/etc."
unexpected behavior.
My intention is to ensure that (at this moment) choosing to use
the on-disk sparse-index format does not alter Git's end-to-end
behavior.
I want to avoid as much as possible a state where enabling the
sparse-index can start changing how Git commands behave, perhaps
in destructive ways.
By adding these checks, we ensure the in-memory data structure
matches whatever a full index would have created, and then the
behavior matches what Git would do there. It might not be the
"correct" behavior, but it is _consistent_.
I'm worrying that expectations are being set up such that working with
just a small section of the code will be unusably hard. There may be
several commands/flags where it could make sense to operate on either
(a) all files in the repo or (b) just on files within your sparse
paths. If, though, folks interpret operate-on-all-files as the
"normal" mode (and history suggests they will), then people start
adding all kinds of --no-do-this-sparsely flags to each command, and
then users who want sparse operation have to remember to type such a
flag with each and every command they ever run -- despite having taken
at least three steps already to get a sparse-index.
I believe the extended discussions (for _months_!) on just grep & rm,
plus watching a --sparse patch being floated just in the last day for
ls-files suggest to me that this is a _very_ likely outcome and I'm
worried about it.
It's these behavior changes that I would like to delay as much as
possible and focus on the format and making commands fast that don't
need a change in behavior.
(Yes, there will be exceptions, like when "git add" specifically
adds a file that is in a directory that should be out of the cone,
but the user added it anyway. Atypical behavior like that can be
slow for now.)
quoted
Since tests take time to write and review, I was hoping that these
insertions were minimal enough to get us to a safe place where we
can remove the guards carefully.
So with that in mind...
quoted
Might be nice to add a
/* TODO: audit if this is needed; if it is, we may have other bugs... */
or something like that. But then again, perhaps you're considering
all uses of ensure_full_index() to be need-to-be-reaudited codepaths?
If so, and we determine we really do need one and want to keep it
indefinitely, will we mark those with a comment about why it's
considered correct?
I just want a way to know what still needs to be audited and what
doesn't without doing a lot of history spelunking...
...every insertion "needs to be audited" in the future. That's a
big part of the next "phases" in the implementation plan.
As you suggest, it might be a good idea to add a comment to every
insertion, to mark it as un-audited, such as:
/* TODO: test if ensure_full_index() is necessary */
We can come back later to delete the comment if it truly is
necessary (and add tests to guarantee correct behavior). We can
also remove the comment _and_ the call by modifying the loop
behavior to do the right thing in some cases.
If it's "needs to be audited for both performance reasons (can we
operate on fewer entries as an invisible doesn't-change-results
optimization) and correctness reasons (should we operate on fewer
entries and given a modified result within a sparse-index because
users would expect that, but maybe provide a special flag for the
users who want to operate on all files in the repo)" and there's also
an agreement that either audited or unaudited ones will be marked (or
both), then great, I'm happy. If not, can we discuss which part of my
performance/correctness/marking we aren't in agreement on?
I will mark all of the ones I'm inserting. My hope is to eventually
remove it entirely except for when disabling the sparse-index. That
is likely too far out to really hope for, but it is the direction I
am trying to go.
As I indicate that we should carefully test each of these instances
where ensure_full_index() _might_ be necessary before removing them,
it is even more important to test the scenarios where the behavior
changes from a full index with sparse-checkout. Preferably, we just
change the behavior under sparse-checkout and then the sparse-index
can match that (see "test_sparse_match" in t1092).
Thanks,
-Stolee
On Wed, Mar 17, 2021 at 2:33 PM Derrick Stolee [off-list ref] wrote:
On 3/17/2021 5:10 PM, Elijah Newren wrote:
quoted
On Wed, Mar 17, 2021 at 1:05 PM Derrick Stolee [off-list ref] wrote:
quoted
On 3/17/2021 1:50 PM, Elijah Newren wrote:
quoted
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
With the caveat in the commit message, this change looks okay, but
checkout-index may be buggy regardless of the presence of
ensure_full_index(). If ensure_full_index() really is needed here
because it needs to operate on all SKIP_WORKTREE paths and not just
leading directories, that's because it's writing all those
SKIP_WORKTREE entries to the working tree. When it writes them to the
working tree, is it clearing the SKIP_WORKTREE bit? If not, we're in
a bit of a pickle...
Perhaps I'm unclear in my intentions with this series: _every_
insertion of ensure_full_index() is intended to be audited with
tests in the future. Some might need behavior change, and others
will not. In this series, I'm just putting in the protections so
we don't accidentally trigger unexpected behavior.
I think this may be part of my qualms -- what do you mean by not
accidentally triggering unexpected behavior? In particular, does your
statement imply that whatever behavior you get after putting in
ensure_full_index() is "expected"? I think I'm reading that
implication into it, and objecting that the behavior with the
ensure_full_index() still isn't expected. You've only removed a
certain class of unexpected behavior, namely code that wasn't written
to expect tree entries that suddenly gets them. You haven't handled
the class of "user wants to work with a subset of files, why are all
these unrelated files being munged/updated/computed/shown/etc."
unexpected behavior.
My intention is to ensure that (at this moment) choosing to use
the on-disk sparse-index format does not alter Git's end-to-end
behavior.
I want to avoid as much as possible a state where enabling the
sparse-index can start changing how Git commands behave, perhaps
in destructive ways.
By adding these checks, we ensure the in-memory data structure
matches whatever a full index would have created, and then the
behavior matches what Git would do there. It might not be the
"correct" behavior, but it is _consistent_.
That sounds good. Could this be included in
Documentation/technical/sparse-index.txt? There's only an oblique
reference to it when talking about grep and rm, which incidentally
were already updated by Matheus. Perhaps also a reference to the
warning in Documentation/git-sparse-checkout.txt would be worthwhile.
quoted
I'm worrying that expectations are being set up such that working with
just a small section of the code will be unusably hard. There may be
several commands/flags where it could make sense to operate on either
(a) all files in the repo or (b) just on files within your sparse
paths. If, though, folks interpret operate-on-all-files as the
"normal" mode (and history suggests they will), then people start
adding all kinds of --no-do-this-sparsely flags to each command, and
then users who want sparse operation have to remember to type such a
flag with each and every command they ever run -- despite having taken
at least three steps already to get a sparse-index.
I believe the extended discussions (for _months_!) on just grep & rm,
plus watching a --sparse patch being floated just in the last day for
ls-files suggest to me that this is a _very_ likely outcome and I'm
worried about it.
It's these behavior changes that I would like to delay as much as
possible and focus on the format and making commands fast that don't
need a change in behavior.
Delaying sounds great, just so long as that delay doesn't also cement
the behavior and confuse consistency for correctness.
I still think we don't have correct behavior for sparse-checkouts in
many cases (I mean, when "git reset --hard" throws errors about not
removing files and then removes them, we've obviously got some
problems), but we had a decade long cementing of sorts with
SKIP_WORKTREE and now a year-or-two long cementing since
sparse-checkout was introduced and we never went through and cleaned
up the commands. We should at some point, especially since we put the
huge scary warning in Documenation/git-sparse-checkout.txt expressly
for this purpose.
(I would have started this sooner, but trying to feed merge-ort and
keep up with patch review already keeps me at less time on non-git
projects than I think is expected for me. Once merge-ort is done...)
(Yes, there will be exceptions, like when "git add" specifically
adds a file that is in a directory that should be out of the cone,
but the user added it anyway. Atypical behavior like that can be
slow for now.)
Since tests take time to write and review, I was hoping that these
insertions were minimal enough to get us to a safe place where we
can remove the guards carefully.
So with that in mind...
quoted
Might be nice to add a
/* TODO: audit if this is needed; if it is, we may have other bugs... */
or something like that. But then again, perhaps you're considering
all uses of ensure_full_index() to be need-to-be-reaudited codepaths?
If so, and we determine we really do need one and want to keep it
indefinitely, will we mark those with a comment about why it's
considered correct?
I just want a way to know what still needs to be audited and what
doesn't without doing a lot of history spelunking...
...every insertion "needs to be audited" in the future. That's a
big part of the next "phases" in the implementation plan.
As you suggest, it might be a good idea to add a comment to every
insertion, to mark it as un-audited, such as:
/* TODO: test if ensure_full_index() is necessary */
We can come back later to delete the comment if it truly is
necessary (and add tests to guarantee correct behavior). We can
also remove the comment _and_ the call by modifying the loop
behavior to do the right thing in some cases.
If it's "needs to be audited for both performance reasons (can we
operate on fewer entries as an invisible doesn't-change-results
optimization) and correctness reasons (should we operate on fewer
entries and given a modified result within a sparse-index because
users would expect that, but maybe provide a special flag for the
users who want to operate on all files in the repo)" and there's also
an agreement that either audited or unaudited ones will be marked (or
both), then great, I'm happy. If not, can we discuss which part of my
performance/correctness/marking we aren't in agreement on?
I will mark all of the ones I'm inserting. My hope is to eventually
remove it entirely except for when disabling the sparse-index. That
is likely too far out to really hope for, but it is the direction I
am trying to go.
As I indicate that we should carefully test each of these instances
where ensure_full_index() _might_ be necessary before removing them,
it is even more important to test the scenarios where the behavior
changes from a full index with sparse-checkout. Preferably, we just
change the behavior under sparse-checkout and then the sparse-index
can match that (see "test_sparse_match" in t1092).
Makes sense. I agree that it'd be nice to have the two generally
match, though I think we should be open to there being special cases
that differ. The only one I can think of right now is `git ls-files`
(list the entries in the index) -- since there are tree entries in a
sparse-index, ls-files would naturally show the tree entries.
However, we can discuss that and any other cases -- if there are any
-- later.
On Wed, Mar 17, 2021 at 2:33 PM Derrick Stolee [off-list ref] wrote:
quoted
On 3/17/2021 5:10 PM, Elijah Newren wrote:
quoted
On Wed, Mar 17, 2021 at 1:05 PM Derrick Stolee [off-list ref] wrote:
quoted
On 3/17/2021 1:50 PM, Elijah Newren wrote:
quoted
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
With the caveat in the commit message, this change looks okay, but
checkout-index may be buggy regardless of the presence of
ensure_full_index(). If ensure_full_index() really is needed here
because it needs to operate on all SKIP_WORKTREE paths and not just
leading directories, that's because it's writing all those
SKIP_WORKTREE entries to the working tree. When it writes them to the
working tree, is it clearing the SKIP_WORKTREE bit? If not, we're in
a bit of a pickle...
Perhaps I'm unclear in my intentions with this series: _every_
insertion of ensure_full_index() is intended to be audited with
tests in the future. Some might need behavior change, and others
will not. In this series, I'm just putting in the protections so
we don't accidentally trigger unexpected behavior.
I think this may be part of my qualms -- what do you mean by not
accidentally triggering unexpected behavior? In particular, does your
statement imply that whatever behavior you get after putting in
ensure_full_index() is "expected"? I think I'm reading that
implication into it, and objecting that the behavior with the
ensure_full_index() still isn't expected. You've only removed a
certain class of unexpected behavior, namely code that wasn't written
to expect tree entries that suddenly gets them. You haven't handled
the class of "user wants to work with a subset of files, why are all
these unrelated files being munged/updated/computed/shown/etc."
unexpected behavior.
My intention is to ensure that (at this moment) choosing to use
the on-disk sparse-index format does not alter Git's end-to-end
behavior.
I want to avoid as much as possible a state where enabling the
sparse-index can start changing how Git commands behave, perhaps
in destructive ways.
By adding these checks, we ensure the in-memory data structure
matches whatever a full index would have created, and then the
behavior matches what Git would do there. It might not be the
"correct" behavior, but it is _consistent_.
That sounds good. Could this be included in
Documentation/technical/sparse-index.txt? There's only an oblique
reference to it when talking about grep and rm, which incidentally
were already updated by Matheus. Perhaps also a reference to the
warning in Documentation/git-sparse-checkout.txt would be worthwhile.
As I was thinking about how to make this more clear, I realized
that an update to that doc early in this series would be wise.
Thanks.
quoted
quoted
I'm worrying that expectations are being set up such that working with
just a small section of the code will be unusably hard. There may be
several commands/flags where it could make sense to operate on either
(a) all files in the repo or (b) just on files within your sparse
paths. If, though, folks interpret operate-on-all-files as the
"normal" mode (and history suggests they will), then people start
adding all kinds of --no-do-this-sparsely flags to each command, and
then users who want sparse operation have to remember to type such a
flag with each and every command they ever run -- despite having taken
at least three steps already to get a sparse-index.
I believe the extended discussions (for _months_!) on just grep & rm,
plus watching a --sparse patch being floated just in the last day for
ls-files suggest to me that this is a _very_ likely outcome and I'm
worried about it.
It's these behavior changes that I would like to delay as much as
possible and focus on the format and making commands fast that don't
need a change in behavior.
Delaying sounds great, just so long as that delay doesn't also cement
the behavior and confuse consistency for correctness.
I still think we don't have correct behavior for sparse-checkouts in
many cases (I mean, when "git reset --hard" throws errors about not
removing files and then removes them, we've obviously got some
problems), but we had a decade long cementing of sorts with
SKIP_WORKTREE and now a year-or-two long cementing since
sparse-checkout was introduced and we never went through and cleaned
up the commands. We should at some point, especially since we put the
huge scary warning in Documenation/git-sparse-checkout.txt expressly
for this purpose.
(I would have started this sooner, but trying to feed merge-ort and
keep up with patch review already keeps me at less time on non-git
projects than I think is expected for me. Once merge-ort is done...)
quoted
(Yes, there will be exceptions, like when "git add" specifically
adds a file that is in a directory that should be out of the cone,
but the user added it anyway. Atypical behavior like that can be
slow for now.)
I agree that there will be exceptions where we can't make the behavior
be fast, but I disagree with that specific example. "git add" should
just give a warning message and not add any file outside the cone, for
both sparse-index and sparse-checkout. That can be done quickly.
That would be great! I would love behavior changes that make the
performance work I'm interested in easier.
I'm in full support of these ideas to change the behavior, when
possible. I would love to see that those changes make it really
easy to integrate the sparse-index into the commands.
quoted
quoted
quoted
Since tests take time to write and review, I was hoping that these
insertions were minimal enough to get us to a safe place where we
can remove the guards carefully.
So with that in mind...
quoted
Might be nice to add a
/* TODO: audit if this is needed; if it is, we may have other bugs... */
or something like that. But then again, perhaps you're considering
all uses of ensure_full_index() to be need-to-be-reaudited codepaths?
If so, and we determine we really do need one and want to keep it
indefinitely, will we mark those with a comment about why it's
considered correct?
I just want a way to know what still needs to be audited and what
doesn't without doing a lot of history spelunking...
...every insertion "needs to be audited" in the future. That's a
big part of the next "phases" in the implementation plan.
As you suggest, it might be a good idea to add a comment to every
insertion, to mark it as un-audited, such as:
/* TODO: test if ensure_full_index() is necessary */
We can come back later to delete the comment if it truly is
necessary (and add tests to guarantee correct behavior). We can
also remove the comment _and_ the call by modifying the loop
behavior to do the right thing in some cases.
If it's "needs to be audited for both performance reasons (can we
operate on fewer entries as an invisible doesn't-change-results
optimization) and correctness reasons (should we operate on fewer
entries and given a modified result within a sparse-index because
users would expect that, but maybe provide a special flag for the
users who want to operate on all files in the repo)" and there's also
an agreement that either audited or unaudited ones will be marked (or
both), then great, I'm happy. If not, can we discuss which part of my
performance/correctness/marking we aren't in agreement on?
I will mark all of the ones I'm inserting. My hope is to eventually
remove it entirely except for when disabling the sparse-index. That
is likely too far out to really hope for, but it is the direction I
am trying to go.
As I indicate that we should carefully test each of these instances
where ensure_full_index() _might_ be necessary before removing them,
it is even more important to test the scenarios where the behavior
changes from a full index with sparse-checkout. Preferably, we just
change the behavior under sparse-checkout and then the sparse-index
can match that (see "test_sparse_match" in t1092).
Makes sense. I agree that it'd be nice to have the two generally
match, though I think we should be open to there being special cases
that differ. The only one I can think of right now is `git ls-files`
(list the entries in the index) -- since there are tree entries in a
sparse-index, ls-files would naturally show the tree entries.
However, we can discuss that and any other cases -- if there are any
-- later.
Here, I at least want to make it very clear that the change is
happening and include updates to the documentation. This is a
case where it is unclear what the "promise" is to _external_
tools that depend on ls-files (somehow). What is their intended
use of ls-files, and how do those expectations change when in a
sparse-checkout?
It's questions like this where I at least want to be making a
clear change that motivates why a change in behavior is merited,
then test that behavior to demonstrate that we expect that to be
the same in perpetuity.
Thanks,
-Stolee
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
From: Derrick Stolee <redacted>
When adjusting the sparse-checkout definition, ensure that a sparse
index is expanded to a full one to avoid unexpected behavior. This is a
top candidate for later integration with the sparse-index, but requires
careful testing.
I was going to comment on most of these collectively, but your commit
message is completely divorced from the patch; it appears you either
place the wrong commit message here or copied it from the one for a
sparse-checkout. The code modifies stash, though.
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
quoted hunk
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
diff-lib.c | 1 +
1 file changed, 1 insertion(+)
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full one to avoid unexpected behavior.
This is a top candidate for updating later with a proper integration
with the sparse index, since we will likely enable the ORT strategy by
default when the sparse index is enabled. This appears to be the only
s/appears to be/is/ :-)
place where the ORT strategy interacts with the index in such a global
way, so that integration should be clear once the ORT strategy and the
sparse index topics stabilize.
@@ -3112,6 +3112,7 @@ static int record_conflicted_index_entries(struct merge_options *opt,if(strmap_empty(conflicted))return0;+ensure_full_index(index);original_cache_nr=index->cache_nr;/* Put every entry from paths into plist, then sort */--
On Wed, Mar 17, 2021 at 11:03 AM Elijah Newren [off-list ref] wrote:
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
quoted
The point of this series is to insert protections for the consumers of the
in-memory index. We mark certain regions of code as needing a full index, so
we call ensure_full_index() to expand a sparse index to a full one, if
necessary. These protections are inserted file-by-file in every loop over
all cache entries. Well, "most" loops, because some are going to be handled
in the very next series so I leave them out.
...
quoted
However, after this series is complete, we now have a straight-forward plan
for making commands "sparse aware" one-by-one:
1. Disable settings.command_requires_full_index to allow an in-memory
sparse-index.
2. Run versions of that command under a debugger, breaking on
ensure_full_index().
3. Examine the call stack to determine the context of that expansion, then
implement the proper behavior in those locations.
4. Add tests to ensure we are checking this logic in the presence of sparse
directory entries.
...
If I'm confused about the goals and the plans, then my reviews will
probably be less than helpful, so I'll suspend reading the series
until I understand the plan a little better.
Thanks for patiently responding to all my other queries. I read
through more of the series today. I inserted comments on a couple
specific patches, but most of patches 6-25 are all along the same
theme.
I have some overall comments on those patches in 6-25 (none of which
need to be addressed in this series, but are meant as hopefully
helpful guides about future work):
add/rm: both of these were just above loops that had a
skip-the-SKIP_WORKTREE entries, at least once Matheus' series is
merged (https://lore.kernel.org/git/cover.1615588108.git.matheus.bernardino@usp.br/).
I think the upshot is just that these become easier to convert.
checkout/commit -- I think these follow the add/rm model and likewise
become easy to convert.
ls-files - I commented elsewhere in this thread about how I think it'd
make sense to have it list the entries in the index, as always.
Obviously, that'd give different output than a full index, because
there are different entries present in the index when using a
sparse-index vs. a full one.
However, all of these, as you say, can wait until later.
I also noted that patches 26 and 27 were based on ones from the RFC
series that I reviewed before, and I noticed you fixed an issue or two
I flagged there, but you also made some other changes and it's too
late at night for my brain to continue thinking and try to compare
them; I may try again tomorrow.
Other than patch 26 which I'm too tired to think through right now,
and the patches I specifically commented on, the rest of the series
looks good to me.
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
quoted
From: Derrick Stolee <redacted>
When adjusting the sparse-checkout definition, ensure that a sparse
index is expanded to a full one to avoid unexpected behavior. This is a
top candidate for later integration with the sparse-index, but requires
careful testing.
I was going to comment on most of these collectively, but your commit
message is completely divorced from the patch; it appears you either
place the wrong commit message here or copied it from the one for a
sparse-checkout. The code modifies stash, though.
Thanks for pointing this out. Must have been a bad squash during one
of many rebases.
-Stolee
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
quoted
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
diff-lib.c | 1 +
1 file changed, 1 insertion(+)
@@ -102,6 +102,7 @@ int run_diff_files(struct rev_info *revs, unsigned int option)if(diff_unmerged_stage<0)diff_unmerged_stage=2;+entries=istate->cache_nr;for(i=0;i<entries;i++){unsignedintoldmode,newmode;
I don't think adding a blank newline will ensure the index is expanded. ;-)
Oops! This is one where I think we determined the loop doesn't
need the guard, but I didn't remove the newline and hence the
patch wasn't dropped.
Thanks,
-Stolee
On Tue, Mar 16, 2021 at 2:17 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
quoted
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full one to avoid unexpected behavior.
This is a top candidate for updating later with a proper integration
with the sparse index, since we will likely enable the ORT strategy by
default when the sparse index is enabled. This appears to be the only
s/appears to be/is/ :-)
quoted
place where the ORT strategy interacts with the index in such a global
way, so that integration should be clear once the ORT strategy and the
sparse index topics stabilize.
Yes, I noticed that that was one of the conflicts introduced with
this series.
I took a closer look at that patch and method, and it seems like
it only cares about paths with SKIP_WORKTREE _and_ is conflicted.
If it has a conflict, then I believe we won't collapse those
entries to a sparse-directory entry.
I think I will drop this patch for now, and rely on the fact that
I plan to enable merge-ORT directly if sparse-index is enabled.
At that time, I will carefully test this logic.
For now, the 'command_requires_full_index' guard will be
sufficient to preserve behavior.
Thanks,
-Stolee
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:01
From: Derrick Stolee <redacted>
Edit and expand the sparse-index design document with the plan for
guarding index operations with ensure_full_index().
Notably, the plan has changed to not have an expand_to_path() method in
favor of checking for a sparse-directory hit inside of the
index_path_pos() API.
The changes that follow this one will incrementally add
ensure_full_index() guards to iterations over all cache entries. Some
iterations over the cache entries are not protected due to a few
categories listed in the document. Since these are not being modified,
here is a short list of the files and methods that will not receive
these guards:
Looking for non-zero stage:
* builtin/add.c:chmod_pathspec()
* builtin/merge.c:count_unmerged_entries()
* merge-ort.c:record_conflicted_index_entries()
* read-cache.c:unmerged_index()
* rerere.c:check_one_conflict(), find_conflict(), rerere_remaining()
* revision.c:prepare_show_merge()
* sequencer.c:append_conflicts_hint()
* wt-status.c:wt_status_collect_changes_initial()
Looking for submodules:
* builtin/submodule--helper.c:module_list_compute()
* submodule.c: several methods
* worktree.c:validate_no_submodules()
Part of the index API:
* name-hash.c: lazy init methods
* preload-index.c:preload_thread(), preload_index()
* read-cache.c: file format methods
Checking for correct order of cache entries:
* read-cache.c:check_ce_order()
Ignores SKIP_WORKTREE entries or already aware:
* unpack-trees.c:mark_new_skip_worktree()
* wt-status.c:wt_status_check_sparse_checkout()
Signed-off-by: Derrick Stolee <redacted>
---
Documentation/technical/sparse-index.txt | 37 ++++++++++++++++++++++--
1 file changed, 35 insertions(+), 2 deletions(-)
@@ -85,8 +85,41 @@ index, as well. Next, consumers of the index will be guarded against operating on a sparse-index by inserting calls to `ensure_full_index()` or-`expand_index_to_path()`. After these guards are in place, we can begin-leaving sparse-directory entries in the in-memory index structure.+`expand_index_to_path()`. If a specific path is requested, then those will+be protected from within the `index_file_exists()` and `index_name_pos()`+API calls: they will call `ensure_full_index()` if necessary. The+intention here is to preserve existing behavior when interacting with a+sparse-checkout. We don't want a change to happen by accident, without+tests. Many of these locations may not need any change before removing the+guards, but we should not do so without tests to ensure the expected+behavior happens.++It may be desirable to _change_ the behavior of some commands in the+presence of a sparse index or more generally in any sparse-checkout+scenario. In such cases, these should be carefully communicated and+tested. No such behavior changes are intended during this phase.++During a scan of the codebase, not every iteration of the cache entries+needs an `ensure_full_index()` check. The basic reasons include:++1. The loop is scanning for entries with non-zero stage. These entries+ are not collapsed into a sparse-directory entry.++2. The loop is scanning for submodules. These entries are not collapsed+ into a sparse-directory entry.++3. The loop is part of the index API, especially around reading or+ writing the format.++4. The loop is checking for correct order of cache entries and that is+ correct if and only if the sparse-directory entries are in the correct+ location.++5. The loop ignores entries with the `SKIP_WORKTREE` bit set, or is+ otherwise already aware of sparse directory entries.++6. The sparse-index is disabled at this point when using the split-index+ feature, so no effort is made to protect the split-index API. Even after inserting these guards, we will keep expanding sparse-indexes for most Git commands using the `command_requires_full_index` repository
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:02
From: Derrick Stolee <redacted>
Callers to index_name_pos() or index_name_stage_pos() have a specific
path in mind. If that happens to be a path with an ancestor being a
sparse-directory entry, it can lead to unexpected results.
In the case that we did not find the requested path, check to see if the
position _before_ the inserted position is a sparse directory entry that
matches the initial segment of the input path (including the directory
separator at the end of the directory name). If so, then expand the
index to be a full index and search again. This expansion will only
happen once per index read.
Future enhancements could be more careful to expand only the necessary
sparse directory entry, but then we would have a special "not fully
sparse, but also not fully expanded" mode that could affect writing the
index to file. Since this only occurs if a specific file is requested
outside of the sparse checkout definition, this is unlikely to be a
common situation.
Signed-off-by: Derrick Stolee <redacted>
---
read-cache.c | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:02
Here is the second patch series submission coming out of the sparse-index
RFC [1].
[1]
https://lore.kernel.org/git/pull.847.git.1611596533.gitgitgadget@gmail.com/
This is based on v3 of the format series [2].
[2]
https://lore.kernel.org/git/pull.883.v3.git.1615912983.gitgitgadget@gmail.com/
The point of this series is to insert protections for the consumers of the
in-memory index to avoid unintended behavior change when using a sparse
index versus a full one.
We mark certain regions of code as needing a full index, so we call
ensure_full_index() to expand a sparse index to a full one, if necessary.
These protections are inserted file-by-file in every loop over all cache
entries. Well, "most" loops, because some are going to be handled in the
very next series so I leave them out.
Many callers use index_name_pos() to find a path by name. In these cases, we
can check if that position resolves to a sparse directory instance. In those
cases, we just expand to a full index and run the search again.
The last few patches deal with the name-hash hashtable for doing O(1)
lookups.
These protections don't do much right now, since the previous series created
the_repository->settings.command_requires_full_index to guard all index
reads and writes to ensure the in-memory copy is full for commands that have
not been tested with the sparse index yet.
However, after this series is complete, we now have a straight-forward plan
for making commands "sparse aware" one-by-one:
1. Disable settings.command_requires_full_index to allow an in-memory
sparse-index.
2. Run versions of that command under a debugger, breaking on
ensure_full_index().
3. Examine the call stack to determine the context of that expansion, then
implement the proper behavior in those locations.
4. Add tests to ensure we are checking this logic in the presence of sparse
directory entries.
I will admit that mostly it is the writing of the test cases that takes the
most time in the conversions I've done so far.
Updates in v2
=============
* Rebased onto v5 of ds/sparse-index
* Updated the technical doc to describe how these protections are guards to
keep behavior consistent between a sparse-index and a full index. Whether
or not that behavior is "correct" can be interrogated later.
* Calls to ensure_full_index() are marked with a TODO comment saying these
calls should be audited later (with tests).
* Fixed an incorrectly squashed commit message.
* Dropped the diff-lib.c commit because it was erroneously included in v2.
* Dropped the merge-ort.c commit because of conflicts with work in flight
and a quick audit that it is not needed.
* I reviewed the merge of this topic with mt/add-rm-in-sparse-checkout and
found it equivalent to what I would have done.
Thanks, -Stolee
Derrick Stolee (25):
sparse-index: API protection strategy
*: remove 'const' qualifier for struct index_state
read-cache: expand on query into sparse-directory entry
cache: move ensure_full_index() to cache.h
add: ensure full index
checkout-index: ensure full index
checkout: ensure full index
commit: ensure full index
difftool: ensure full index
fsck: ensure full index
grep: ensure full index
ls-files: ensure full index
merge-index: ensure full index
rm: ensure full index
stash: ensure full index
update-index: ensure full index
dir: ensure full index
entry: ensure full index
merge-recursive: ensure full index
pathspec: ensure full index
read-cache: ensure full index
resolve-undo: ensure full index
revision: ensure full index
sparse-index: expand_to_path()
name-hash: use expand_to_path()
Documentation/technical/sparse-index.txt | 37 +++++++++++-
attr.c | 14 ++---
attr.h | 4 +-
builtin/add.c | 2 +
builtin/checkout-index.c | 2 +
builtin/checkout.c | 5 ++
builtin/commit.c | 4 ++
builtin/difftool.c | 3 +
builtin/fsck.c | 2 +
builtin/grep.c | 2 +
builtin/ls-files.c | 14 +++--
builtin/merge-index.c | 5 ++
builtin/rm.c | 2 +
builtin/stash.c | 2 +
builtin/update-index.c | 2 +
cache.h | 7 ++-
convert.c | 26 ++++-----
convert.h | 20 +++----
dir.c | 14 +++--
dir.h | 8 +--
entry.c | 2 +
merge-recursive.c | 4 +-
name-hash.c | 10 ++++
pathspec.c | 8 ++-
pathspec.h | 6 +-
read-cache.c | 35 ++++++++++--
resolve-undo.c | 4 ++
revision.c | 2 +
sparse-index.c | 72 ++++++++++++++++++++++++
sparse-index.h | 14 ++++-
submodule.c | 6 +-
submodule.h | 6 +-
32 files changed, 273 insertions(+), 71 deletions(-)
base-commit: 66602733cc95d9a53594520cd8b28d3338e258ea
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-906%2Fderrickstolee%2Fsparse-index%2Fprotections-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-906/derrickstolee/sparse-index/protections-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/906
Range-diff vs v1:
3: bbf19f8a2be5 ! 1: 7484e085e342 sparse-index: API protection strategy
@@ Commit message
Looking for non-zero stage:
* builtin/add.c:chmod_pathspec()
* builtin/merge.c:count_unmerged_entries()
+ * merge-ort.c:record_conflicted_index_entries()
* read-cache.c:unmerged_index()
* rerere.c:check_one_conflict(), find_conflict(), rerere_remaining()
* revision.c:prepare_show_merge()
@@ Commit message
Signed-off-by: Derrick Stolee [off-list ref]
## Documentation/technical/sparse-index.txt ##
-@@ Documentation/technical/sparse-index.txt: also introduce other features that have been considered for improving the
- index, as well.
+@@ Documentation/technical/sparse-index.txt: index, as well.
Next, consumers of the index will be guarded against operating on a
--sparse-index by inserting calls to `ensure_full_index()` or
+ sparse-index by inserting calls to `ensure_full_index()` or
-`expand_index_to_path()`. After these guards are in place, we can begin
-leaving sparse-directory entries in the in-memory index structure.
-+sparse-index by inserting calls to `ensure_full_index()` before iterating
-+over all cache entries. If a specific path is requested, then those will
++`expand_index_to_path()`. If a specific path is requested, then those will
+be protected from within the `index_file_exists()` and `index_name_pos()`
-+API calls: they will call `ensure_full_index()` if necessary.
++API calls: they will call `ensure_full_index()` if necessary. The
++intention here is to preserve existing behavior when interacting with a
++sparse-checkout. We don't want a change to happen by accident, without
++tests. Many of these locations may not need any change before removing the
++guards, but we should not do so without tests to ensure the expected
++behavior happens.
++
++It may be desirable to _change_ the behavior of some commands in the
++presence of a sparse index or more generally in any sparse-checkout
++scenario. In such cases, these should be carefully communicated and
++tested. No such behavior changes are intended during this phase.
+
+During a scan of the codebase, not every iteration of the cache entries
+needs an `ensure_full_index()` check. The basic reasons include:
@@ Documentation/technical/sparse-index.txt: also introduce other features that hav
+
+6. The sparse-index is disabled at this point when using the split-index
+ feature, so no effort is made to protect the split-index API.
-+
-+After these guards are in place, we can begin leaving sparse-directory
-+entries in the in-memory index structure.
Even after inserting these guards, we will keep expanding sparse-indexes
for most Git commands using the `command_requires_full_index` repository
1: 628e16dd3fc7 = 2: 098b2c9ef352 *: remove 'const' qualifier for struct index_state
2: 8e11e8917019 = 3: 737d27e18d64 read-cache: expand on query into sparse-directory entry
4: 7e4079c48eb2 = 4: db5c100f3e2b cache: move ensure_full_index() to cache.h
5: 142df1ab8e3e ! 5: 4a5fc2eb5a9f add: ensure full index
@@ builtin/add.c: static int renormalize_tracked_files(const struct pathspec *paths
{
int i, retval = 0;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (i = 0; i < active_nr; i++) {
struct cache_entry *ce = active_cache[i];
6: bfa0164cc3c1 ! 6: 11c38f7277c5 checkout-index: ensure full index
@@ builtin/checkout-index.c: static void checkout_all(const char *prefix, int prefi
int i, errs = 0;
struct cache_entry *last_ce = NULL;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (i = 0; i < active_nr ; i++) {
struct cache_entry *ce = active_cache[i];
7: 1bb7b6117e41 ! 7: fd04adbb3f79 checkout: ensure full index
@@ builtin/checkout.c: static int checkout_worktree(const struct checkout_opts *opt
NULL);
enable_delayed_checkout(&state);
++
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (pos = 0; pos < active_nr; pos++) {
struct cache_entry *ce = active_cache[pos];
@@ builtin/checkout.c: static int checkout_paths(const struct checkout_opts *opts,
* Make sure all pathspecs participated in locating the paths
* to be checked out.
*/
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (pos = 0; pos < active_nr; pos++)
if (opts->overlay_mode)
8: a59e45c4ae8f ! 8: 65704f39edc9 commit: ensure full index
@@ builtin/commit.c: static int list_paths(struct string_list *list, const char *wi
free(max_prefix);
}
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (i = 0; i < active_nr; i++) {
const struct cache_entry *ce = active_cache[i];
@@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const cha
if (get_oid(parent, &oid)) {
int i, ita_nr = 0;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (i = 0; i < active_nr; i++)
if (ce_intent_to_add(active_cache[i]))
9: 83040e7558f3 ! 9: 739f3fe9edf2 difftool: ensure full index
@@ builtin/difftool.c: static int run_dir_diff(const char *extcmd, int symlinks, co
setenv("GIT_DIFFTOOL_DIRDIFF", "true", 1);
rc = run_command_v_opt(helper_argv, flags);
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&wtindex);
+
/*
10: 988f7bd2d736 ! 10: 779a86ad1ec4 fsck: ensure full index
@@ builtin/fsck.c: int cmd_fsck(int argc, const char **argv, const char *prefix)
verify_index_checksum = 1;
verify_ce_order = 1;
read_cache();
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (i = 0; i < active_nr; i++) {
unsigned int mode;
11: abe189051a0c ! 11: 8c0d377054fa grep: ensure full index
@@ builtin/grep.c: static int grep_cache(struct grep_opt *opt,
if (repo_read_index(repo) < 0)
die(_("index file corrupt"));
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(repo->index);
for (nr = 0; nr < repo->index->cache_nr; nr++) {
const struct cache_entry *ce = repo->index->cache[nr];
12: 00c8a7e1d119 ! 12: beaa1467cabb ls-files: ensure full index
@@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
if (!(show_cached || show_stage || show_deleted || show_modified))
return;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(repo->index);
for (i = 0; i < repo->index->cache_nr; i++) {
const struct cache_entry *ce = repo->index->cache[i];
@@ builtin/ls-files.c: void overlay_tree_on_index(struct index_state *istate,
die("bad tree-ish %s", tree_name);
/* Hoist the unmerged entries up to stage #3 to make room */
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(istate);
for (i = 0; i < istate->cache_nr; i++) {
struct cache_entry *ce = istate->cache[i];
13: 1288f02177d2 ! 13: 73684141fcff merge-index: ensure full index
@@ builtin/merge-index.c: static void merge_one_path(const char *path)
static void merge_all(void)
{
int i;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (i = 0; i < active_nr; i++) {
const struct cache_entry *ce = active_cache[i];
@@ builtin/merge-index.c: int cmd_merge_index(int argc, const char **argv, const ch
read_cache();
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
+
i = 1;
14: d6816560b32f ! 14: 6ea81a49c6b5 rm: ensure full index
@@ builtin/rm.c: int cmd_rm(int argc, const char **argv, const char *prefix)
seen = xcalloc(pathspec.nr, 1);
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (i = 0; i < active_nr; i++) {
const struct cache_entry *ce = active_cache[i];
15: 92ccd31dd343 ! 15: 49ca5ed05c8d sparse-checkout: ensure full index
@@ Metadata
Author: Derrick Stolee [off-list ref]
## Commit message ##
- sparse-checkout: ensure full index
+ stash: ensure full index
- When adjusting the sparse-checkout definition, ensure that a sparse
- index is expanded to a full one to avoid unexpected behavior. This is a
- top candidate for later integration with the sparse-index, but requires
- careful testing.
+ Before iterating over all cache entries, ensure that a sparse index is
+ expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee [off-list ref]
@@ builtin/stash.c: static int do_push_stash(const struct pathspec *ps, const char
int i;
char *ps_matched = xcalloc(ps->nr, 1);
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (i = 0; i < active_nr; i++)
ce_path_match(&the_index, active_cache[i], ps,
16: 796449758a08 ! 16: 9c4bb187c15d update-index: ensure full index
@@ builtin/update-index.c: static int do_reupdate(int ac, const char **av,
*/
has_head = 0;
redo:
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(&the_index);
for (pos = 0; pos < active_nr; pos++) {
const struct cache_entry *ce = active_cache[pos];
17: 45bbed6150a2 < -: ------------ diff-lib: ensure full index
18: 8ce1f80985a4 ! 17: fae4c078c3ef dir: ensure full index
@@ dir.c: static void connect_wt_gitdir_in_nested(const char *sub_worktree,
if (repo_read_index(&subrepo) < 0)
die(_("index file corrupt in repo %s"), subrepo.gitdir);
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(subrepo.index);
for (i = 0; i < subrepo.index->cache_nr; i++) {
const struct cache_entry *ce = subrepo.index->cache[i];
19: 6e0b452f44c1 ! 18: 2b9180ee77d3 entry: ensure full index
@@ entry.c: static void mark_colliding_entries(const struct checkout *state,
ce->ce_flags |= CE_MATCHED;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(state->istate);
for (i = 0; i < state->istate->cache_nr; i++) {
struct cache_entry *dup = state->istate->cache[i];
20: fb4c7038b746 < -: ------------ merge-ort: ensure full index
21: 57d59825627f ! 19: 1e3f6085a405 merge-recursive: ensure full index
@@ merge-recursive.c: static struct string_list *get_unmerged(struct index_state *i
unmerged->strdup_strings = 1;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(istate);
for (i = 0; i < istate->cache_nr; i++) {
struct string_list_item *item;
22: f4c470686b27 ! 20: e62a597a9725 pathspec: ensure full index
@@ pathspec.c: void add_pathspec_matches_against_index(const struct pathspec *paths
num_unmatched++;
if (!num_unmatched)
return;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(istate);
for (i = 0; i < istate->cache_nr; i++) {
const struct cache_entry *ce = istate->cache[i];
23: 81519782d4b2 ! 21: ebfffdbdd6ad read-cache: ensure full index
@@ read-cache.c: int refresh_index(struct index_state *istate, unsigned int flags,
*/
preload_index(istate, pathspec, 0);
trace2_region_enter("index", "refresh", NULL);
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(istate);
for (i = 0; i < istate->cache_nr; i++) {
struct cache_entry *ce, *new_entry;
@@ read-cache.c: int repo_index_has_changes(struct repository *repo,
diff_flush(&opt);
return opt.flags.has_changes != 0;
} else {
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(istate);
for (i = 0; sb && i < istate->cache_nr; i++) {
if (i)
24: 39e8bea0c1ca ! 22: 495b07a87973 resolve-undo: ensure full index
@@ resolve-undo.c: void unmerge_marked_index(struct index_state *istate)
if (!istate->resolve_undo)
return;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(istate);
for (i = 0; i < istate->cache_nr; i++) {
const struct cache_entry *ce = istate->cache[i];
@@ resolve-undo.c: void unmerge_index(struct index_state *istate, const struct path
if (!istate->resolve_undo)
return;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(istate);
for (i = 0; i < istate->cache_nr; i++) {
const struct cache_entry *ce = istate->cache[i];
25: 6abb1813ae10 ! 23: 3144114d1a75 revision: ensure full index
@@ revision.c: static void do_add_index_objects_to_pending(struct rev_info *revs,
{
int i;
++ /* TODO: audit for interaction with sparse-index. */
+ ensure_full_index(istate);
for (i = 0; i < istate->cache_nr; i++) {
struct cache_entry *ce = istate->cache[i];
26: 1690d33c11e0 = 24: d52c72b4a7b9 sparse-index: expand_to_path()
27: 40cfc59f695a = 25: 7e2d3fae9a2a name-hash: use expand_to_path()
--
gitgitgadget
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Soon we will insert ensure_full_index() calls across the codebase.
Instead of also adding include statements for sparse-index.h, let's just
use the fact that anything that cares about the index already has
cache.h in its includes.
Signed-off-by: Derrick Stolee <redacted>
---
cache.h | 1 +
sparse-index.h | 1 -
2 files changed, 1 insertion(+), 1 deletion(-)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/add.c | 2 ++
1 file changed, 2 insertions(+)
@@ -141,6 +141,8 @@ static int renormalize_tracked_files(const struct pathspec *pathspec, int flags){inti,retval=0;+/* TODO: audit for interaction with sparse-index. */+ensure_full_index(&the_index);for(i=0;i<active_nr;i++){structcache_entry*ce=active_cache[i];
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Several methods specify that they take a 'struct index_state' pointer
with the 'const' qualifier because they intend to only query the data,
not change it. However, we will be introducing a step very low in the
method stack that might modify a sparse-index to become a full index in
the case that our queries venture inside a sparse-directory entry.
This change only removes the 'const' qualifiers that are necessary for
the following change which will actually modify the implementation of
index_name_stage_pos().
Signed-off-by: Derrick Stolee <redacted>
---
attr.c | 14 +++++++-------
attr.h | 4 ++--
builtin/ls-files.c | 10 +++++-----
cache.h | 6 +++---
convert.c | 26 +++++++++++++-------------
convert.h | 20 ++++++++++----------
dir.c | 12 ++++++------
dir.h | 8 ++++----
merge-recursive.c | 2 +-
pathspec.c | 6 +++---
pathspec.h | 6 +++---
read-cache.c | 10 +++++-----
submodule.c | 6 +++---
submodule.h | 6 +++---
14 files changed, 68 insertions(+), 68 deletions(-)
@@ -850,8 +850,8 @@ int add_file_to_index(struct index_state *, const char *path, int flags);intchmod_index_entry(structindex_state*,structcache_entry*ce,charflip);intce_same_name(conststructcache_entry*a,conststructcache_entry*b);voidset_object_name_for_intent_to_add_entry(structcache_entry*ce);-intindex_name_is_other(conststructindex_state*,constchar*,int);-void*read_blob_data_from_index(conststructindex_state*,constchar*,unsignedlong*);+intindex_name_is_other(structindex_state*,constchar*,int);+void*read_blob_data_from_index(structindex_state*,constchar*,unsignedlong*);/* do stat comparison even if CE_VALID is true */#define CE_MATCH_IGNORE_VALID 01
@@ -549,7 +549,7 @@ int cache_name_stage_compare(const char *name1, int len1, int stage1, const charreturn0;}-staticintindex_name_stage_pos(conststructindex_state*istate,constchar*name,intnamelen,intstage)+staticintindex_name_stage_pos(structindex_state*istate,constchar*name,intnamelen,intstage){intfirst,last;
@@ -33,7 +33,7 @@ static struct oid_array ref_tips_after_fetch;*willbedisabledbecausewecan'tguesswhatmightbeconfiguredin*.gitmodulesunlesstheuserresolvestheconflict.*/-intis_gitmodules_unmerged(conststructindex_state*istate)+intis_gitmodules_unmerged(structindex_state*istate){intpos=index_name_pos(istate,GITMODULES_FILE,strlen(GITMODULES_FILE));if(pos<0){/* .gitmodules not found or isn't merged */
@@ -301,7 +301,7 @@ int is_submodule_populated_gently(const char *path, int *return_error_code)/**Diesiftheprovided'prefix'correspondstoanunpopulatedsubmodule*/-voiddie_in_unpopulated_submodule(conststructindex_state*istate,+voiddie_in_unpopulated_submodule(structindex_state*istate,constchar*prefix){inti,prefixlen;
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
When verifying all blobs reachable from the index, ensure that a sparse
index has been expanded to a full one to avoid missing some blobs.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/fsck.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Before we iterate over all cache entries, ensure that the index is not
sparse. This loop in checkout_all() might be safe to iterate over a
sparse index, but let's put this protection here until it can be
carefully tested.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/checkout-index.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
These two loops iterate over all cache entries, so ensure that a sparse
index is expanded to a full index before we do so.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/commit.c | 4 ++++
1 file changed, 4 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full one so we do not miss blobs to scan. Later, this can
integrate more carefully with sparse indexes with proper testing.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/grep.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Before iterating over all cache entries in the checkout builtin, ensure
that we have a full index to avoid any unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/checkout.c | 5 +++++
1 file changed, 5 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
dir.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full one to avoid missing files.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/ls-files.c | 4 ++++
1 file changed, 4 insertions(+)
@@ -317,6 +317,8 @@ static void show_files(struct repository *repo, struct dir_struct *dir)if(!(show_cached||show_stage||show_deleted||show_modified))return;+/* TODO: audit for interaction with sparse-index. */+ensure_full_index(repo->index);for(i=0;i<repo->index->cache_nr;i++){conststructcache_entry*ce=repo->index->cache[i];structstatst;
@@ -494,6 +496,8 @@ void overlay_tree_on_index(struct index_state *istate,die("bad tree-ish %s",tree_name);/* Hoist the unmerged entries up to stage #3 to make room */+/* TODO: audit for interaction with sparse-index. */+ensure_full_index(istate);for(i=0;i<istate->cache_nr;i++){structcache_entry*ce=istate->cache[i];if(!ce_stage(ce))
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/stash.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/rm.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:03
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index has
been expanded to a full one to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/difftool.c | 3 +++
1 file changed, 3 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:04
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
merge-recursive.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:04
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
pathspec.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:04
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
entry.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:04
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full one to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/merge-index.c | 5 +++++
1 file changed, 5 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:04
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
read-cache.c | 4 ++++
1 file changed, 4 insertions(+)
@@ -1577,6 +1577,8 @@ int refresh_index(struct index_state *istate, unsigned int flags,*/preload_index(istate,pathspec,0);trace2_region_enter("index","refresh",NULL);+/* TODO: audit for interaction with sparse-index. */+ensure_full_index(istate);for(i=0;i<istate->cache_nr;i++){structcache_entry*ce,*new_entry;intcache_errno=0;
@@ -2498,6 +2500,8 @@ int repo_index_has_changes(struct repository *repo,diff_flush(&opt);returnopt.flags.has_changes!=0;}else{+/* TODO: audit for interaction with sparse-index. */+ensure_full_index(istate);for(i=0;sb&&i<istate->cache_nr;i++){if(i)strbuf_addch(sb,' ');
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:04
From: Derrick Stolee <redacted>
Before iterating over all index entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior. This case could
be integrated later by ensuring that we walk the tree in the
sparse-directory entry, but the current behavior is only expecting
blobs. Save this integration for later when it can be properly tested.
Signed-off-by: Derrick Stolee <redacted>
---
revision.c | 2 ++
1 file changed, 2 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:04
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
resolve-undo.c | 4 ++++
1 file changed, 4 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:04
From: Derrick Stolee <redacted>
Some users of the index API have a specific path they are looking for,
but choose to use index_file_exists() to rely on the name-hash hashtable
instead of doing binary search with index_name_pos(). These users only
need to know a yes/no answer, not a position within the cache array.
When the index is sparse, the name-hash hash table does not contain the
full list of paths within sparse directories. It _does_ contain the
directory names for the sparse-directory entries.
Create a helper function, expand_to_path(), for intended use with the
name-hash hashtable functions. The integration with name-hash.c will
follow in a later change.
The solution here is to use ensure_full_index() when we determine that
the requested path is within a sparse directory entry. This will
populate the name-hash hashtable as the index is recomputed from
scratch.
There may be cases where the caller is trying to find an untracked path
that is not in the index but also is not within a sparse directory
entry. We want to minimize the overhead for these requests. If we used
index_name_pos() to find the insertion order of the path, then we could
determine from that position if a sparse-directory exists. (In fact,
just calling index_name_pos() in that case would lead to expanding the
index to a full index.) However, this takes O(log N) time where N is the
number of cache entries.
To keep the performance of this call based mostly on the input string,
use index_file_exists() to look for the ancestors of the path. Using the
heuristic that a sparse directory is likely to have a small number of
parent directories, we start from the bottom and build up. Use a string
buffer to allow mutating the path name to terminate after each slash for
each hashset test.
Signed-off-by: Derrick Stolee <redacted>
---
sparse-index.c | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++
sparse-index.h | 13 +++++++++
2 files changed, 85 insertions(+)
@@ -283,3 +283,75 @@ void ensure_full_index(struct index_state *istate)trace2_region_leave("index","ensure_full_index",istate->repo);}++/*+*Thisstaticglobalhelpsavoidinfiniterecursionbetween+*expand_to_path()andindex_file_exists().+*/+staticintin_expand_to_path=0;++voidexpand_to_path(structindex_state*istate,+constchar*path,size_tpathlen,inticase)+{+structstrbufpath_mutable=STRBUF_INIT;+size_tsubstr_len;++/* prevent extra recursion */+if(in_expand_to_path)+return;++if(!istate||!istate->sparse_index)+return;++if(!istate->repo)+istate->repo=the_repository;++in_expand_to_path=1;++/*+*Weonlyneedtoactuallyexpandaregionifthe+*followingarebothtrue:+*+*1.'path'isnotalreadyintheindex.+*2.Someparentdirectoryof'path'isasparsedirectory.+*/++if(index_file_exists(istate,path,pathlen,icase))+gotocleanup;++strbuf_add(&path_mutable,path,pathlen);+strbuf_addch(&path_mutable,'/');++/* Check the name hash for all parent directories */+substr_len=0;+while(substr_len<pathlen){+chartemp;+char*replace=strchr(path_mutable.buf+substr_len,'/');++if(!replace)+break;++/* replace the character _after_ the slash */+replace++;+temp=*replace;+*replace='\0';+if(index_file_exists(istate,path_mutable.buf,+path_mutable.len,icase)){+/*+*Wefoundaparentdirectoryinthename-hash+*hashtable,whichmeansthatthisentrycould+*existwithinasparse-directoryentry.Expand+*accordingly.+*/+ensure_full_index(istate);+break;+}++*replace=temp;+substr_len=replace-path_mutable.buf;+}++cleanup:+strbuf_release(&path_mutable);+in_expand_to_path=0;+}
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:04
From: Derrick Stolee <redacted>
A sparse-index loads the name-hash data for its entries, including the
sparse-directory entries. If a caller asks for a path that is contained
within a sparse-directory entry, we need to expand to a full index and
recalculate the name hash table before returning the result. Insert
calls to expand_to_path() to protect against this case.
Signed-off-by: Derrick Stolee <redacted>
---
name-hash.c | 10 ++++++++++
1 file changed, 10 insertions(+)
From: Derrick Stolee via GitGitGadget <hidden> Date: 2021-04-01 01:51:04
From: Derrick Stolee <redacted>
Before iterating over all cache entries, ensure that a sparse index is
expanded to a full index to avoid unexpected behavior.
Signed-off-by: Derrick Stolee <redacted>
---
builtin/update-index.c | 2 ++
1 file changed, 2 insertions(+)
From: Junio C Hamano <hidden> Date: 2021-04-01 07:07:47
"Derrick Stolee via GitGitGadget" [off-list ref] writes:
Here is the second patch series submission coming out of the sparse-index
RFC [1].
Queued. There were a few conflicts with new codepaths and I've
touched up a few of them (Alban's merge-strategies-in-c moves some
code that needs to dehydrate the sparse index entries from
builtin/merge-index.c to a new file, Elijah's ort has "const"
pointer to the istate that you need to be writable), but there may
be more that I missed. Please check the tip of 'seen' when it gets
pushed out.
Thanks.
"Derrick Stolee via GitGitGadget" [off-list ref] writes:
quoted
Here is the second patch series submission coming out of the sparse-index
RFC [1].
Queued. There were a few conflicts with new codepaths and I've
touched up a few of them (Alban's merge-strategies-in-c moves some
code that needs to dehydrate the sparse index entries from
builtin/merge-index.c to a new file, Elijah's ort has "const"
pointer to the istate that you need to be writable), but there may
be more that I missed. Please check the tip of 'seen' when it gets
pushed out.
Thank you for dealing with these complicated merges. I promise
that the series after this one are much more focused in their
code footprint. Thus, they should be easier to merge with
ongoing topics.
I validated your merge by rebasing the rest of my current work
in this area onto 'seen'. The tests for 'git status' and 'git
add' with sparse-indexes adds to the confidence of the merge.
I did see a small conflict with mt/add-rm-in-sparse-checkout
in one of my future patches because of an adjacent edit. I'll
be sure to base my next submission on a merge with that topic.
Thanks,
-Stolee
On Wed, Mar 31, 2021 at 6:50 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
quoted hunk
From: Derrick Stolee <redacted>
Some users of the index API have a specific path they are looking for,
but choose to use index_file_exists() to rely on the name-hash hashtable
instead of doing binary search with index_name_pos(). These users only
need to know a yes/no answer, not a position within the cache array.
When the index is sparse, the name-hash hash table does not contain the
full list of paths within sparse directories. It _does_ contain the
directory names for the sparse-directory entries.
Create a helper function, expand_to_path(), for intended use with the
name-hash hashtable functions. The integration with name-hash.c will
follow in a later change.
The solution here is to use ensure_full_index() when we determine that
the requested path is within a sparse directory entry. This will
populate the name-hash hashtable as the index is recomputed from
scratch.
There may be cases where the caller is trying to find an untracked path
that is not in the index but also is not within a sparse directory
entry. We want to minimize the overhead for these requests. If we used
index_name_pos() to find the insertion order of the path, then we could
determine from that position if a sparse-directory exists. (In fact,
just calling index_name_pos() in that case would lead to expanding the
index to a full index.) However, this takes O(log N) time where N is the
number of cache entries.
To keep the performance of this call based mostly on the input string,
use index_file_exists() to look for the ancestors of the path. Using the
heuristic that a sparse directory is likely to have a small number of
parent directories, we start from the bottom and build up. Use a string
buffer to allow mutating the path name to terminate after each slash for
each hashset test.
Signed-off-by: Derrick Stolee <redacted>
---
sparse-index.c | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++
sparse-index.h | 13 +++++++++
2 files changed, 85 insertions(+)
@@ -283,3 +283,75 @@ void ensure_full_index(struct index_state *istate)trace2_region_leave("index","ensure_full_index",istate->repo);}++/*+*Thisstaticglobalhelpsavoidinfiniterecursionbetween+*expand_to_path()andindex_file_exists().+*/+staticintin_expand_to_path=0;++voidexpand_to_path(structindex_state*istate,+constchar*path,size_tpathlen,inticase)+{+structstrbufpath_mutable=STRBUF_INIT;+size_tsubstr_len;++/* prevent extra recursion */+if(in_expand_to_path)+return;++if(!istate||!istate->sparse_index)+return;++if(!istate->repo)+istate->repo=the_repository;++in_expand_to_path=1;++/*+*Weonlyneedtoactuallyexpandaregionifthe+*followingarebothtrue:+*+*1.'path'isnotalreadyintheindex.+*2.Someparentdirectoryof'path'isasparsedirectory.+*/++if(index_file_exists(istate,path,pathlen,icase))+gotocleanup;++strbuf_add(&path_mutable,path,pathlen);+strbuf_addch(&path_mutable,'/');++/* Check the name hash for all parent directories */+substr_len=0;+while(substr_len<pathlen){+chartemp;+char*replace=strchr(path_mutable.buf+substr_len,'/');++if(!replace)+break;++/* replace the character _after_ the slash */+replace++;+temp=*replace;+*replace='\0';+if(index_file_exists(istate,path_mutable.buf,+path_mutable.len,icase)){+/*+*Wefoundaparentdirectoryinthename-hash+*hashtable,whichmeansthatthisentrycould+*existwithinasparse-directoryentry.Expand+*accordingly.
"this entry" is a bit unclear; it might be worth referring to the
"path" variable instead. I think it'd also help to explain the
reasoning for using the '/' character, because while it's clear when
looking at the series, just running across it in the code it might be
easy to forget or miss. Perhaps:
* We found a parent directory in the name-hash
* hashtable, because only sparse directory entries
* have a trailing '/' character. Since "path" wasn't
* in the index, perhaps it exists within this
* sparse-directory. Expand accordingly.
On Wed, Mar 31, 2021 at 6:50 PM Derrick Stolee via GitGitGadget
[off-list ref] wrote:
quoted hunk
From: Derrick Stolee <redacted>
A sparse-index loads the name-hash data for its entries, including the
sparse-directory entries. If a caller asks for a path that is contained
within a sparse-directory entry, we need to expand to a full index and
recalculate the name hash table before returning the result. Insert
calls to expand_to_path() to protect against this case.
Signed-off-by: Derrick Stolee <redacted>
---
name-hash.c | 10 ++++++++++
1 file changed, 10 insertions(+)
I don't see how this relates to the commit message. Was this meant to
be included in another commit? If it is intended to be included here,
could an explanation be added?