-
Notifications
You must be signed in to change notification settings - Fork 191
Sparse Index: Design, Format, Tests #883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7b600d5
202253e
437a0f1
b7e1bf5
e41d55d
7bfbfbd
a1b8135
dd84a2a
b276d2e
c3651e2
f926cf8
c870ae5
bcf0da9
7191b48
57be9b4
c22b411
75fe9b0
7f55a23
3659018
9b068c4
6660273
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,13 @@ Git index format | |
| localization, no special casing of directory separator '/'). Entries | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Elijah Newren wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Derrick Stolee wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Elijah Newren wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Elijah Newren wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Junio C Hamano wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Derrick Stolee wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Junio C Hamano wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Derrick Stolee wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, SZEDER Gábor wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Junio C Hamano wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Derrick Stolee wrote (reply to this): |
||
| with the same name are sorted by their stage field. | ||
|
|
||
| An index entry typically represents a file. However, if sparse-checkout | ||
| is enabled in cone mode (`core.sparseCheckoutCone` is enabled) and the | ||
| `extensions.sparseIndex` extension is enabled, then the index may | ||
| contain entries for directories outside of the sparse-checkout definition. | ||
| These entries have mode `040000`, include the `SKIP_WORKTREE` bit, and | ||
| the path ends in a directory separator. | ||
|
|
||
| 32-bit ctime seconds, the last time a file's metadata changed | ||
| this is stat(2) data | ||
|
|
||
|
|
@@ -385,3 +392,15 @@ The remaining data of each directory block is grouped by type: | |
| in this block of entries. | ||
|
|
||
| - 32-bit count of cache entries in this block | ||
|
|
||
| == Sparse Directory Entries | ||
|
|
||
| When using sparse-checkout in cone mode, some entire directories within | ||
| the index can be summarized by pointing to a tree object instead of the | ||
| entire expanded list of paths within that tree. An index containing such | ||
| entries is a "sparse index". Index format versions 4 and less were not | ||
| implemented with such entries in mind. Thus, for these versions, an | ||
| index containing sparse directory entries will include this extension | ||
| with signature { 's', 'd', 'i', 'r' }. Like the split-index extension, | ||
| tools should avoid interacting with a sparse index unless they understand | ||
| this extension. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| Git Sparse-Index Design Document | ||
| ================================ | ||
|
|
||
| The sparse-checkout feature allows users to focus a working directory on | ||
| a subset of the files at HEAD. The cone mode patterns, enabled by | ||
| `core.sparseCheckoutCone`, allow for very fast pattern matching to | ||
| discover which files at HEAD belong in the sparse-checkout cone. | ||
|
|
||
| Three important scale dimensions for a Git working directory are: | ||
|
|
||
| * `HEAD`: How many files are present at `HEAD`? | ||
|
|
||
| * Populated: How many files are within the sparse-checkout cone. | ||
|
|
||
| * Modified: How many files has the user modified in the working directory? | ||
|
|
||
| We will use big-O notation -- O(X) -- to denote how expensive certain | ||
| operations are in terms of these dimensions. | ||
|
|
||
| These dimensions are ordered by their magnitude: users (typically) modify | ||
| fewer files than are populated, and we can only populate files at `HEAD`. | ||
|
|
||
| Problems occur if there is an extreme imbalance in these dimensions. For | ||
| example, if `HEAD` contains millions of paths but the populated set has | ||
| only tens of thousands, then commands like `git status` and `git add` can | ||
| be dominated by operations that require O(`HEAD`) operations instead of | ||
| O(Populated). Primarily, the cost is in parsing and rewriting the index, | ||
| which is filled primarily with files at `HEAD` that are marked with the | ||
| `SKIP_WORKTREE` bit. | ||
|
|
||
| The sparse-index intends to take these commands that read and modify the | ||
| index from O(`HEAD`) to O(Populated). To do this, we need to modify the | ||
| index format in a significant way: add "sparse directory" entries. | ||
|
|
||
| With cone mode patterns, it is possible to detect when an entire | ||
| directory will have its contents outside of the sparse-checkout definition. | ||
| Instead of listing all of the files it contains as individual entries, a | ||
| sparse-index contains an entry with the directory name, referencing the | ||
| object ID of the tree at `HEAD` and marked with the `SKIP_WORKTREE` bit. | ||
| If we need to discover the details for paths within that directory, we | ||
| can parse trees to find that list. | ||
|
|
||
| At time of writing, sparse-directory entries violate expectations about the | ||
| index format and its in-memory data structure. There are many consumers in | ||
| the codebase that expect to iterate through all of the index entries and | ||
| see only files. In fact, these loops expect to see a reference to every | ||
| staged file. One way to handle this is to parse trees to replace a | ||
| sparse-directory entry with all of the files within that tree as the index | ||
| is loaded. However, parsing trees is slower than parsing the index format, | ||
| so that is a slower operation than if we left the index alone. The plan is | ||
| to make all of these integrations "sparse aware" so this expansion through | ||
| tree parsing is unnecessary and they use fewer resources than when using a | ||
| full index. | ||
|
|
||
| The implementation plan below follows four phases to slowly integrate with | ||
| the sparse-index. The intention is to incrementally update Git commands to | ||
| interact safely with the sparse-index without significant slowdowns. This | ||
| may not always be possible, but the hope is that the primary commands that | ||
| users need in their daily work are dramatically improved. | ||
|
|
||
| Phase I: Format and initial speedups | ||
| ------------------------------------ | ||
|
|
||
| During this phase, Git learns to enable the sparse-index and safely parse | ||
| one. Protections are put in place so that every consumer of the in-memory | ||
| data structure can operate with its current assumption of every file at | ||
| `HEAD`. | ||
|
|
||
| At first, every index parse will call a helper method, | ||
| `ensure_full_index()`, which scans the index for sparse-directory entries | ||
| (pointing to trees) and replaces them with the full list of paths (with | ||
| blob contents) by parsing tree objects. This will be slower in all cases. | ||
| The only noticeable change in behavior will be that the serialized index | ||
| file contains sparse-directory entries. | ||
|
|
||
| To start, we use a new required index extension, `sdir`, to allow | ||
| inserting sparse-directory entries into indexes with file format | ||
| versions 2, 3, and 4. This prevents Git versions that do not understand | ||
| the sparse-index from operating on one, while allowing tools that do not | ||
| understand the sparse-index to operate on repositories as long as they do | ||
| not interact with the index. A new format, index v5, will be introduced | ||
| that includes sparse-directory entries by default. It might 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. | ||
|
|
||
| Even after inserting these guards, we will keep expanding sparse-indexes | ||
| for most Git commands using the `command_requires_full_index` repository | ||
| setting. This setting will be on by default and disabled one builtin at a | ||
| time until we have sufficient confidence that all of the index operations | ||
| are properly guarded. | ||
|
|
||
| To complete this phase, the commands `git status` and `git add` will be | ||
| integrated with the sparse-index so that they operate with O(Populated) | ||
| performance. They will be carefully tested for operations within and | ||
| outside the sparse-checkout definition. | ||
|
|
||
| Phase II: Careful integrations | ||
| ------------------------------ | ||
|
|
||
| This phase focuses on ensuring that all index extensions and APIs work | ||
| well with a sparse-index. This requires significant increases to our test | ||
| coverage, especially for operations that interact with the working | ||
| directory outside of the sparse-checkout definition. Some of these | ||
| behaviors may not be the desirable ones, such as some tests already | ||
| marked for failure in `t1092-sparse-checkout-compatibility.sh`. | ||
|
|
||
| The index extensions that may require special integrations are: | ||
|
|
||
| * FS Monitor | ||
| * Untracked cache | ||
|
|
||
| While integrating with these features, we should look for patterns that | ||
| might lead to better APIs for interacting with the index. Coalescing | ||
| common usage patterns into an API call can reduce the number of places | ||
| where sparse-directories need to be handled carefully. | ||
|
|
||
| Phase III: Important command speedups | ||
| ------------------------------------- | ||
|
|
||
| At this point, the patterns for testing and implementing sparse-directory | ||
| logic should be relatively stable. This phase focuses on updating some of | ||
| the most common builtins that use the index to operate as O(Populated). | ||
| Here is a potential list of commands that could be valuable to integrate | ||
| at this point: | ||
|
|
||
| * `git commit` | ||
| * `git checkout` | ||
| * `git merge` | ||
| * `git rebase` | ||
|
|
||
| Hopefully, commands such as `git merge` and `git rebase` can benefit | ||
| instead from merge algorithms that do not use the index as a data | ||
| structure, such as the merge-ORT strategy. As these topics mature, we | ||
| may enable the ORT strategy by default for repositories using the | ||
| sparse-index feature. | ||
|
|
||
| Along with `git status` and `git add`, these commands cover the majority | ||
| of users' interactions with the working directory. In addition, we can | ||
| integrate with these commands: | ||
|
|
||
| * `git grep` | ||
| * `git rm` | ||
|
|
||
| These have been proposed as some whose behavior could change when in a | ||
| repo with a sparse-checkout definition. It would be good to include this | ||
| behavior automatically when using a sparse-index. Some clarity is needed | ||
| to make the behavior switch clear to the user. | ||
|
|
||
| This phase is the first where parallel work might be possible without too | ||
| much conflicts between topics. | ||
|
|
||
| Phase IV: The long tail | ||
| ----------------------- | ||
|
|
||
| This last phase is less a "phase" and more "the new normal" after all of | ||
| the previous work. | ||
|
|
||
| To start, the `command_requires_full_index` option could be removed in | ||
| favor of expanding only when hitting an API guard. | ||
|
|
||
| There are many Git commands that could use special attention to operate as | ||
| O(Populated), while some might be so rare that it is acceptable to leave | ||
| them with additional overhead when a sparse-index is present. | ||
|
|
||
| Here are some commands that might be useful to update: | ||
|
|
||
| * `git sparse-checkout set` | ||
| * `git am` | ||
| * `git clean` | ||
| * `git stash` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -985,6 +985,7 @@ LIB_OBJS += setup.o | |
| LIB_OBJS += shallow.o | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Elijah Newren wrote (reply to this): |
||
| LIB_OBJS += sideband.o | ||
| LIB_OBJS += sigchain.o | ||
| LIB_OBJS += sparse-index.o | ||
| LIB_OBJS += split-index.o | ||
| LIB_OBJS += stable-qsort.o | ||
| LIB_OBJS += strbuf.o | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| #include "unpack-trees.h" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, SZEDER Gábor wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Derrick Stolee wrote (reply to this): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Derrick Stolee wrote (reply to this): |
||
| #include "wt-status.h" | ||
| #include "quote.h" | ||
| #include "sparse-index.h" | ||
|
|
||
| static const char *empty_base = ""; | ||
|
|
||
|
|
@@ -110,6 +111,8 @@ static int update_working_directory(struct pattern_list *pl) | |
| if (is_index_unborn(r->index)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Git mailing list, Elijah Newren wrote (reply to this): |
||
| return UPDATE_SPARSITY_SUCCESS; | ||
|
|
||
| r->index->sparse_checkout_patterns = pl; | ||
|
|
||
| memset(&o, 0, sizeof(o)); | ||
| o.verbose_update = isatty(2); | ||
| o.update = 1; | ||
|
|
@@ -138,6 +141,7 @@ static int update_working_directory(struct pattern_list *pl) | |
| else | ||
| rollback_lock_file(&lock_file); | ||
|
|
||
| r->index->sparse_checkout_patterns = NULL; | ||
| return result; | ||
| } | ||
|
|
||
|
|
@@ -276,16 +280,20 @@ static int set_config(enum sparse_checkout_mode mode) | |
| "core.sparseCheckoutCone", | ||
| mode == MODE_CONE_PATTERNS ? "true" : NULL); | ||
|
|
||
| if (mode == MODE_NO_PATTERNS) | ||
| set_sparse_index_config(the_repository, 0); | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| static char const * const builtin_sparse_checkout_init_usage[] = { | ||
| N_("git sparse-checkout init [--cone]"), | ||
| N_("git sparse-checkout init [--cone] [--[no-]sparse-index]"), | ||
| NULL | ||
| }; | ||
|
|
||
| static struct sparse_checkout_init_opts { | ||
| int cone_mode; | ||
| int sparse_index; | ||
| } init_opts; | ||
|
|
||
| static int sparse_checkout_init(int argc, const char **argv) | ||
|
|
@@ -300,11 +308,15 @@ static int sparse_checkout_init(int argc, const char **argv) | |
| static struct option builtin_sparse_checkout_init_options[] = { | ||
| OPT_BOOL(0, "cone", &init_opts.cone_mode, | ||
| N_("initialize the sparse-checkout in cone mode")), | ||
| OPT_BOOL(0, "sparse-index", &init_opts.sparse_index, | ||
| N_("toggle the use of a sparse index")), | ||
| OPT_END(), | ||
| }; | ||
|
|
||
| repo_read_index(the_repository); | ||
|
|
||
| init_opts.sparse_index = -1; | ||
|
|
||
| argc = parse_options(argc, argv, NULL, | ||
| builtin_sparse_checkout_init_options, | ||
| builtin_sparse_checkout_init_usage, 0); | ||
|
|
@@ -323,10 +335,20 @@ static int sparse_checkout_init(int argc, const char **argv) | |
| sparse_filename = get_sparse_checkout_filename(); | ||
| res = add_patterns_from_file_to_list(sparse_filename, "", 0, &pl, NULL); | ||
|
|
||
| if (init_opts.sparse_index >= 0) { | ||
| if (set_sparse_index_config(the_repository, init_opts.sparse_index) < 0) | ||
| die(_("failed to modify sparse-index config")); | ||
|
|
||
| /* force an index rewrite */ | ||
| repo_read_index(the_repository); | ||
| the_repository->index->updated_workdir = 1; | ||
| } | ||
|
|
||
| core_apply_sparse_checkout = 1; | ||
|
|
||
| /* If we already have a sparse-checkout file, use it. */ | ||
| if (res >= 0) { | ||
| free(sparse_filename); | ||
| core_apply_sparse_checkout = 1; | ||
| return update_working_directory(NULL); | ||
| } | ||
|
|
||
|
|
@@ -348,6 +370,7 @@ static int sparse_checkout_init(int argc, const char **argv) | |
| add_pattern(strbuf_detach(&pattern, NULL), empty_base, 0, &pl, 0); | ||
| strbuf_addstr(&pattern, "!/*/"); | ||
| add_pattern(strbuf_detach(&pattern, NULL), empty_base, 0, &pl, 0); | ||
| pl.use_cone_patterns = init_opts.cone_mode; | ||
|
|
||
| return write_patterns_and_update(&pl); | ||
| } | ||
|
|
@@ -517,19 +540,18 @@ static int modify_pattern_list(int argc, const char **argv, enum modify_type m) | |
| { | ||
| int result; | ||
| int changed_config = 0; | ||
| struct pattern_list pl; | ||
| memset(&pl, 0, sizeof(pl)); | ||
| struct pattern_list *pl = xcalloc(1, sizeof(*pl)); | ||
|
|
||
| switch (m) { | ||
| case ADD: | ||
| if (core_sparse_checkout_cone) | ||
| add_patterns_cone_mode(argc, argv, &pl); | ||
| add_patterns_cone_mode(argc, argv, pl); | ||
| else | ||
| add_patterns_literal(argc, argv, &pl); | ||
| add_patterns_literal(argc, argv, pl); | ||
| break; | ||
|
|
||
| case REPLACE: | ||
| add_patterns_from_input(&pl, argc, argv); | ||
| add_patterns_from_input(pl, argc, argv); | ||
| break; | ||
| } | ||
|
|
||
|
|
@@ -539,12 +561,13 @@ static int modify_pattern_list(int argc, const char **argv, enum modify_type m) | |
| changed_config = 1; | ||
| } | ||
|
|
||
| result = write_patterns_and_update(&pl); | ||
| result = write_patterns_and_update(pl); | ||
|
|
||
| if (result && changed_config) | ||
| set_config(MODE_NO_PATTERNS); | ||
|
|
||
| clear_pattern_list(&pl); | ||
| clear_pattern_list(pl); | ||
| free(pl); | ||
| return result; | ||
| } | ||
|
|
||
|
|
@@ -614,6 +637,9 @@ static int sparse_checkout_disable(int argc, const char **argv) | |
| strbuf_addstr(&match_all, "/*"); | ||
| add_pattern(strbuf_detach(&match_all, NULL), empty_base, 0, &pl, 0); | ||
|
|
||
| prepare_repo_settings(the_repository); | ||
| the_repository->settings.sparse_index = 0; | ||
|
|
||
| if (update_working_directory(&pl)) | ||
| die(_("error while refreshing working directory")); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the Git mailing list, Martin Ågren wrote (reply to this):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the Git mailing list, Derrick Stolee wrote (reply to this):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the Git mailing list, Elijah Newren wrote (reply to this):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the Git mailing list, Derrick Stolee wrote (reply to this):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the Git mailing list, Elijah Newren wrote (reply to this):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the Git mailing list, Martin Ågren wrote (reply to this):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the Git mailing list, Derrick Stolee wrote (reply to this):