diff --git a/Documentation/config/extensions.txt b/Documentation/config/extensions.txt index 4e23d73cdcadf6..5c86b3648732ec 100644 --- a/Documentation/config/extensions.txt +++ b/Documentation/config/extensions.txt @@ -6,3 +6,10 @@ extensions.objectFormat:: Note that this setting should only be set by linkgit:git-init[1] or linkgit:git-clone[1]. Trying to change it after initialization will not work and will produce hard-to-diagnose issues. + +extensions.sparseIndex:: + When combined with `core.sparseCheckout=true` and + `core.sparseCheckoutCone=true`, the index may contain entries + corresponding to directories outside of the sparse-checkout + definition. Versions of Git that do not understand this extension + do not expect directory entries in the index. diff --git a/Documentation/git-sparse-checkout.txt b/Documentation/git-sparse-checkout.txt index a0eeaeb02ee310..b51b8450cfd981 100644 --- a/Documentation/git-sparse-checkout.txt +++ b/Documentation/git-sparse-checkout.txt @@ -45,6 +45,20 @@ To avoid interfering with other worktrees, it first enables the When `--cone` is provided, the `core.sparseCheckoutCone` setting is also set, allowing for better performance with a limited set of patterns (see 'CONE PATTERN SET' below). ++ +Use the `--[no-]sparse-index` option to toggle the use of the sparse +index format. This reduces the size of the index to be more closely +aligned with your sparse-checkout definition. This can have significant +performance advantages for commands such as `git status` or `git add`. +This feature is still experimental. Some commands might be slower with +a sparse index until they are properly integrated with the feature. ++ +**WARNING:** Using a sparse index requires modifying the index in a way +that is not completely understood by other tools. Enabling sparse index +enables the `extensions.spareseIndex` config value, which might cause +other tools to stop working with your repository. If you have trouble with +this compatibility, then run `git sparse-checkout sparse-index disable` to +remove this config and rewrite your index to not be sparse. 'set':: Write a set of patterns to the sparse-checkout file, as given as diff --git a/Makefile b/Makefile index 7b64106930a615..77564ae3b783f7 100644 --- a/Makefile +++ b/Makefile @@ -999,6 +999,7 @@ LIB_OBJS += sha1-name.o LIB_OBJS += shallow.o 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 diff --git a/apply.c b/apply.c index 668b16e9893d2f..5bfbd928b38d48 100644 --- a/apply.c +++ b/apply.c @@ -3523,6 +3523,8 @@ static int load_current(struct apply_state *state, if (!patch->is_new) BUG("patch to %s is not a creation", patch->old_name); + ensure_full_index(state->repo->index); + pos = index_name_pos(state->repo->index, name, strlen(name)); if (pos < 0) return error(_("%s: does not exist in index"), name); @@ -3692,7 +3694,11 @@ static int check_preimage(struct apply_state *state, } if (state->check_index && !previous) { - int pos = index_name_pos(state->repo->index, old_name, + int pos; + + ensure_full_index(state->repo->index); + + pos = index_name_pos(state->repo->index, old_name, strlen(old_name)); if (pos < 0) { if (patch->is_new < 0) @@ -3751,6 +3757,8 @@ static int check_to_create(struct apply_state *state, if (state->check_index && (!ok_if_exists || !state->cached)) { int pos; + ensure_full_index(state->repo->index); + pos = index_name_pos(state->repo->index, new_name, strlen(new_name)); if (pos >= 0) { struct cache_entry *ce = state->repo->index->cache[pos]; diff --git a/blame.c b/blame.c index a5044fcfaa6264..0aa368a35cfa4c 100644 --- a/blame.c +++ b/blame.c @@ -108,6 +108,7 @@ static void verify_working_tree_path(struct repository *r, return; } + ensure_full_index(r->index); pos = index_name_pos(r->index, path, strlen(path)); if (pos >= 0) ; /* path is in the index */ @@ -277,7 +278,11 @@ static struct commit *fake_working_tree_commit(struct repository *r, len = strlen(path); if (!mode) { - int pos = index_name_pos(r->index, path, len); + int pos; + + ensure_full_index(r->index); + + pos = index_name_pos(r->index, path, len); if (0 <= pos) mode = r->index->cache[pos]->ce_mode; else diff --git a/builtin/add.c b/builtin/add.c index a825887c503dd3..b73f8d51de6494 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -491,6 +491,9 @@ int cmd_add(int argc, const char **argv, const char *prefix) add_new_files = !take_worktree_changes && !refresh_only && !add_renormalize; require_pathspec = !(take_worktree_changes || (0 < addremove_explicit)); + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR); /* diff --git a/builtin/checkout-index.c b/builtin/checkout-index.c index 4bbfc92dce5a0e..24c85b1c1250e3 100644 --- a/builtin/checkout-index.c +++ b/builtin/checkout-index.c @@ -48,11 +48,14 @@ static void write_tempfile_record(const char *name, const char *prefix) static int checkout_file(const char *name, const char *prefix) { int namelen = strlen(name); - int pos = cache_name_pos(name, namelen); + int pos; int has_same_name = 0; int did_checkout = 0; int errs = 0; + ensure_full_index(the_repository->index); + pos = index_name_pos(the_repository->index, name, namelen); + if (pos < 0) pos = -pos - 1; diff --git a/builtin/commit.c b/builtin/commit.c index 505fe60956db38..543aa0caeaecbd 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1400,6 +1400,9 @@ int cmd_status(int argc, const char **argv, const char *prefix) if (argc == 2 && !strcmp(argv[1], "-h")) usage_with_options(builtin_status_usage, builtin_status_options); + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + status_init_config(&s, git_status_config); argc = parse_options(argc, argv, prefix, builtin_status_options, diff --git a/builtin/grep.c b/builtin/grep.c index ca259af4416318..e53cf817204436 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -506,6 +506,8 @@ static int grep_cache(struct grep_opt *opt, if (repo_read_index(repo) < 0) die(_("index file corrupt")); + ensure_full_index(repo->index); + for (nr = 0; nr < repo->index->cache_nr; nr++) { const struct cache_entry *ce = repo->index->cache[nr]; strbuf_setlen(&name, name_base_len); diff --git a/builtin/ls-files.c b/builtin/ls-files.c index c8eae899b82a83..933e259cdbe93d 100644 --- a/builtin/ls-files.c +++ b/builtin/ls-files.c @@ -150,7 +150,7 @@ static void show_other_files(const struct index_state *istate, } } -static void show_killed_files(const struct index_state *istate, +static void show_killed_files(struct index_state *istate, const struct dir_struct *dir) { int i; @@ -159,6 +159,8 @@ static void show_killed_files(const struct index_state *istate, char *cp, *sp; int pos, len, killed = 0; + ensure_full_index(istate); + for (cp = ent->name; cp - ent->name < ent->len; cp = sp + 1) { sp = strchr(cp, '/'); if (!sp) { @@ -313,6 +315,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir) show_killed_files(repo->index, dir); } if (show_cached || show_stage) { + ensure_full_index(repo->index); for (i = 0; i < repo->index->cache_nr; i++) { const struct cache_entry *ce = repo->index->cache[i]; @@ -332,6 +335,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir) } } if (show_deleted || show_modified) { + ensure_full_index(repo->index); for (i = 0; i < repo->index->cache_nr; i++) { const struct cache_entry *ce = repo->index->cache[i]; struct stat st; @@ -368,6 +372,7 @@ static void prune_index(struct index_state *istate, if (!prefix || !istate->cache_nr) return; + ensure_full_index(istate); pos = index_name_pos(istate, prefix, prefixlen); if (pos < 0) pos = -pos-1; @@ -428,6 +433,8 @@ void overlay_tree_on_index(struct index_state *istate, if (!tree) die("bad tree-ish %s", tree_name); + ensure_full_index(istate); + /* Hoist the unmerged entries up to stage #3 to make room */ for (i = 0; i < istate->cache_nr; i++) { struct cache_entry *ce = istate->cache[i]; diff --git a/builtin/merge-index.c b/builtin/merge-index.c index 38ea6ad6ca25d5..3e1ddabd650c0b 100644 --- a/builtin/merge-index.c +++ b/builtin/merge-index.c @@ -80,6 +80,8 @@ int cmd_merge_index(int argc, const char **argv, const char *prefix) read_cache(); + ensure_full_index(&the_index); + i = 1; if (!strcmp(argv[i], "-o")) { one_shot = 1; diff --git a/builtin/mv.c b/builtin/mv.c index 7dac714af90878..2ab6416fce9ae2 100644 --- a/builtin/mv.c +++ b/builtin/mv.c @@ -145,6 +145,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix) if (read_cache() < 0) die(_("index file corrupt")); + ensure_full_index(&the_index); + source = internal_prefix_pathspec(prefix, argv, argc, 0); modes = xcalloc(argc, sizeof(enum update_mode)); /* diff --git a/builtin/rm.c b/builtin/rm.c index 4858631e0f02c5..2db4fcd22d90a8 100644 --- a/builtin/rm.c +++ b/builtin/rm.c @@ -291,6 +291,8 @@ int cmd_rm(int argc, const char **argv, const char *prefix) refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, &pathspec, NULL, NULL); + ensure_full_index(&the_index); + seen = xcalloc(pathspec.nr, 1); for (i = 0; i < active_nr; i++) { diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c index 2306a9ad98e08a..14022b5e1826ff 100644 --- a/builtin/sparse-checkout.c +++ b/builtin/sparse-checkout.c @@ -14,6 +14,7 @@ #include "unpack-trees.h" #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)) return UPDATE_SPARSITY_SUCCESS; + r->index->sparse_checkout_patterns = pl; + memset(&o, 0, sizeof(o)); o.verbose_update = isatty(2); o.update = 1; @@ -120,6 +123,7 @@ static int update_working_directory(struct pattern_list *pl) o.pl = pl; setup_work_tree(); + ensure_full_index(r->index); repo_hold_locked_index(r, &lock_file, LOCK_DIE_ON_ERROR); @@ -138,6 +142,7 @@ static int update_working_directory(struct pattern_list *pl) else rollback_lock_file(&lock_file); + r->index->sparse_checkout_patterns = NULL; return result; } @@ -280,12 +285,13 @@ static int set_config(enum sparse_checkout_mode mode) } 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 +306,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,6 +333,15 @@ 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; + } + /* If we already have a sparse-checkout file, use it. */ if (res >= 0) { free(sparse_filename); @@ -517,19 +536,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 +557,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; } diff --git a/builtin/update-index.c b/builtin/update-index.c index 79087bccea4b8b..521a6c23c757f7 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -1088,6 +1088,8 @@ int cmd_update_index(int argc, const char **argv, const char *prefix) the_index.updated_skipworktree = 1; + ensure_full_index(&the_index); + /* * Custom copy of parse_options() because we want to handle * filename arguments as they come. diff --git a/cache-tree.c b/cache-tree.c index 2fb483d3c08389..9da6a4394e03ae 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -6,6 +6,7 @@ #include "object-store.h" #include "replace-object.h" #include "promisor-remote.h" +#include "sparse-index.h" #ifndef DEBUG_CACHE_TREE #define DEBUG_CACHE_TREE 0 @@ -255,6 +256,24 @@ static int update_one(struct cache_tree *it, *skip_count = 0; + /* + * If the first entry of this region is a sparse directory + * entry corresponding exactly to 'base', then this cache_tree + * struct is a "leaf" in the data structure, pointing to the + * tree OID specified in the entry. + */ + if (entries > 0) { + const struct cache_entry *ce = cache[0]; + + if (S_ISSPARSEDIR(ce) && + ce->ce_namelen == baselen && + !strncmp(ce->name, base, baselen)) { + it->entry_count = 1; + oidcpy(&it->oid, &ce->oid); + return 1; + } + } + if (0 <= it->entry_count && has_object_file(&it->oid)) return it->entry_count; @@ -442,6 +461,8 @@ int cache_tree_update(struct index_state *istate, int flags) if (i) return i; + ensure_full_index(istate); + if (!istate->cache_tree) istate->cache_tree = cache_tree(); diff --git a/cache.h b/cache.h index f9c7a603841893..306eab444b996a 100644 --- a/cache.h +++ b/cache.h @@ -204,6 +204,10 @@ struct cache_entry { #error "CE_EXTENDED_FLAGS out of range" #endif +#define CE_MODE_SPARSE_DIRECTORY 01000755 +#define SPARSE_DIR_MODE 0100 +#define S_ISSPARSEDIR(m) ((m)->ce_mode == CE_MODE_SPARSE_DIRECTORY) + /* Forward structure decls */ struct pathspec; struct child_process; @@ -249,6 +253,8 @@ static inline unsigned int create_ce_mode(unsigned int mode) { if (S_ISLNK(mode)) return S_IFLNK; + if (mode == SPARSE_DIR_MODE) + return CE_MODE_SPARSE_DIRECTORY; if (S_ISDIR(mode) || S_ISGITLINK(mode)) return S_IFGITLINK; return S_IFREG | ce_permissions(mode); @@ -305,6 +311,7 @@ static inline unsigned int canon_mode(unsigned int mode) struct split_index; struct untracked_cache; struct progress; +struct pattern_list; struct index_state { struct cache_entry **cache; @@ -319,7 +326,8 @@ struct index_state { drop_cache_tree : 1, updated_workdir : 1, updated_skipworktree : 1, - fsmonitor_has_run_once : 1; + fsmonitor_has_run_once : 1, + sparse_index : 1; struct hashmap name_hash; struct hashmap dir_hash; struct object_id oid; @@ -329,6 +337,7 @@ struct index_state { struct mem_pool *ce_mem_pool; struct progress *progress; struct repository *repo; + struct pattern_list *sparse_checkout_patterns; }; /* Name hashing */ @@ -337,6 +346,7 @@ void add_name_hash(struct index_state *istate, struct cache_entry *ce); void remove_name_hash(struct index_state *istate, struct cache_entry *ce); void free_name_hash(struct index_state *istate); +void ensure_full_index(struct index_state *istate); /* Cache entry creation and cleanup */ @@ -721,6 +731,8 @@ int read_index_from(struct index_state *, const char *path, const char *gitdir); int is_index_unborn(struct index_state *); +void ensure_full_index(struct index_state *istate); + /* For use with `write_locked_index()`. */ #define COMMIT_LOCK (1 << 0) #define SKIP_IF_UNCHANGED (1 << 1) @@ -1043,6 +1055,7 @@ struct repository_format { int worktree_config; int is_bare; int hash_algo; + int sparse_index; char *work_tree; struct string_list unknown_extensions; struct string_list v1_only_extensions; diff --git a/diff.c b/diff.c index 2253ec880298b4..02fafee85879d7 100644 --- a/diff.c +++ b/diff.c @@ -3901,6 +3901,8 @@ static int reuse_worktree_file(struct index_state *istate, if (!want_file && would_convert_to_git(istate, name)) return 0; + ensure_full_index(istate); + len = strlen(name); pos = index_name_pos(istate, name, len); if (pos < 0) diff --git a/dir.c b/dir.c index d153a63bbd14e1..7df8d3b1da099a 100644 --- a/dir.c +++ b/dir.c @@ -18,6 +18,7 @@ #include "ewah/ewok.h" #include "fsmonitor.h" #include "submodule-config.h" +#include "sparse-index.h" /* * Tells read_directory_recursive how a file or directory should be treated. @@ -892,7 +893,7 @@ void add_pattern(const char *string, const char *base, add_pattern_to_hashsets(pl, pattern); } -static int read_skip_worktree_file_from_index(const struct index_state *istate, +static int read_skip_worktree_file_from_index(struct index_state *istate, const char *path, size_t *size_out, char **data_out, struct oid_stat *oid_stat) @@ -900,6 +901,8 @@ static int read_skip_worktree_file_from_index(const struct index_state *istate, int pos, len; len = strlen(path); + + expand_to_path(istate, path, len, 0); pos = index_name_pos(istate, path, len); if (pos < 0) return -1; @@ -1088,6 +1091,10 @@ static int add_patterns(const char *fname, const char *base, int baselen, close(fd); if (oid_stat) { int pos; + + if (istate) + expand_to_path(istate, fname, strlen(fname), 0); + if (oid_stat->valid && !match_stat_data_racy(istate, &oid_stat->stat, &st)) ; /* no content change, oid_stat->oid still good */ @@ -1378,6 +1385,11 @@ enum pattern_match_result path_matches_pattern_list( strbuf_addch(&parent_pathname, '/'); strbuf_add(&parent_pathname, pathname, pathlen); + /* Directory requests should be added as if they are a file */ + if (parent_pathname.len > 1 && + parent_pathname.buf[parent_pathname.len - 1] == '/') + strbuf_add(&parent_pathname, "-", 1); + if (hashmap_contains_path(&pl->recursive_hashmap, &parent_pathname)) { result = MATCHED_RECURSIVE; @@ -1696,6 +1708,7 @@ static enum exist_status directory_exists_in_index(struct index_state *istate, if (ignore_case) return directory_exists_in_index_icase(istate, dirname, len); + expand_to_path(istate, dirname, len, 0); pos = index_name_pos(istate, dirname, len); if (pos < 0) pos = -pos-1; @@ -2050,6 +2063,8 @@ static int get_index_dtype(struct index_state *istate, int pos; const struct cache_entry *ce; + ensure_full_index(istate); + ce = index_file_exists(istate, path, len, 0); if (ce) { if (!ce_uptodate(ce)) @@ -3536,6 +3551,8 @@ 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); + ensure_full_index(subrepo.index); + for (i = 0; i < subrepo.index->cache_nr; i++) { const struct cache_entry *ce = subrepo.index->cache[i]; diff --git a/dir.h b/dir.h index facfae47402adc..300305ec33557d 100644 --- a/dir.h +++ b/dir.h @@ -503,7 +503,7 @@ static inline int ce_path_match(const struct index_state *istate, char *seen) { return match_pathspec(istate, pathspec, ce->name, ce_namelen(ce), 0, seen, - S_ISDIR(ce->ce_mode) || S_ISGITLINK(ce->ce_mode)); + S_ISSPARSEDIR(ce) || S_ISDIR(ce->ce_mode) || S_ISGITLINK(ce->ce_mode)); } static inline int dir_path_match(const struct index_state *istate, diff --git a/entry.c b/entry.c index a0532f1f00007b..d505e6f2c6edab 100644 --- a/entry.c +++ b/entry.c @@ -412,6 +412,8 @@ static void mark_colliding_entries(const struct checkout *state, ce->ce_flags |= CE_MATCHED; + ensure_full_index(state->istate); + for (i = 0; i < state->istate->cache_nr; i++) { struct cache_entry *dup = state->istate->cache[i]; diff --git a/fsmonitor.c b/fsmonitor.c index fe9e9d7baf4450..99b26576baab3e 100644 --- a/fsmonitor.c +++ b/fsmonitor.c @@ -58,6 +58,9 @@ int read_fsmonitor_extension(struct index_state *istate, const void *data, uint64_t timestamp; struct strbuf last_update = STRBUF_INIT; + if (istate->sparse_index) + return 0; + if (sz < sizeof(uint32_t) + 1 + sizeof(uint32_t)) return error("corrupt fsmonitor extension (too short)"); @@ -97,6 +100,10 @@ int read_fsmonitor_extension(struct index_state *istate, const void *data, void fill_fsmonitor_bitmap(struct index_state *istate) { unsigned int i, skipped = 0; + + if (istate->sparse_index) + return; + istate->fsmonitor_dirty = ewah_new(); for (i = 0; i < istate->cache_nr; i++) { if (istate->cache[i]->ce_flags & CE_REMOVE) @@ -183,7 +190,8 @@ void refresh_fsmonitor(struct index_state *istate) char *buf; unsigned int i; - if (!core_fsmonitor || istate->fsmonitor_has_run_once) + if (!core_fsmonitor || istate->fsmonitor_has_run_once || + istate->sparse_index) return; hook_version = fsmonitor_hook_version(); @@ -293,6 +301,9 @@ void add_fsmonitor(struct index_state *istate) unsigned int i; struct strbuf last_update = STRBUF_INIT; + if (istate->sparse_index) + return; + if (!istate->fsmonitor_last_update) { trace_printf_key(&trace_fsmonitor, "add fsmonitor"); istate->cache_changed |= FSMONITOR_CHANGED; @@ -328,8 +339,13 @@ void tweak_fsmonitor(struct index_state *istate) unsigned int i; int fsmonitor_enabled = git_config_get_fsmonitor(); + if (istate->sparse_index) + fsmonitor_enabled = 0; + if (istate->fsmonitor_dirty) { if (fsmonitor_enabled) { + ensure_full_index(istate); + /* Mark all entries valid */ for (i = 0; i < istate->cache_nr; i++) { istate->cache[i]->ce_flags |= CE_FSMONITOR_VALID; diff --git a/merge-recursive.c b/merge-recursive.c index f736a0f63234fe..12109f37723baf 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -522,6 +522,8 @@ static struct string_list *get_unmerged(struct index_state *istate) unmerged->strdup_strings = 1; + ensure_full_index(istate); + for (i = 0; i < istate->cache_nr; i++) { struct string_list_item *item; struct stage_data *e; @@ -762,6 +764,8 @@ static int dir_in_way(struct index_state *istate, const char *path, strbuf_addstr(&dirpath, path); strbuf_addch(&dirpath, '/'); + ensure_full_index(istate); + pos = index_name_pos(istate, dirpath.buf, dirpath.len); if (pos < 0) @@ -785,9 +789,13 @@ static int dir_in_way(struct index_state *istate, const char *path, static int was_tracked_and_matches(struct merge_options *opt, const char *path, const struct diff_filespec *blob) { - int pos = index_name_pos(&opt->priv->orig_index, path, strlen(path)); + int pos; struct cache_entry *ce; + ensure_full_index(&opt->priv->orig_index); + + pos = index_name_pos(&opt->priv->orig_index, path, strlen(path)); + if (0 > pos) /* we were not tracking this path before the merge */ return 0; @@ -802,7 +810,11 @@ static int was_tracked_and_matches(struct merge_options *opt, const char *path, */ static int was_tracked(struct merge_options *opt, const char *path) { - int pos = index_name_pos(&opt->priv->orig_index, path, strlen(path)); + int pos; + + ensure_full_index(&opt->priv->orig_index); + + pos = index_name_pos(&opt->priv->orig_index, path, strlen(path)); if (0 <= pos) /* we were tracking this path before the merge */ @@ -814,6 +826,9 @@ static int was_tracked(struct merge_options *opt, const char *path) static int would_lose_untracked(struct merge_options *opt, const char *path) { struct index_state *istate = opt->repo->index; + int pos; + + ensure_full_index(istate); /* * This may look like it can be simplified to: @@ -832,7 +847,7 @@ static int would_lose_untracked(struct merge_options *opt, const char *path) * update_file()/would_lose_untracked(); see every comment in this * file which mentions "update_stages". */ - int pos = index_name_pos(istate, path, strlen(path)); + pos = index_name_pos(istate, path, strlen(path)); if (pos < 0) pos = -1 - pos; @@ -3086,6 +3101,7 @@ static int handle_content_merge(struct merge_file_info *mfi, * flag to avoid making the file appear as if it were * deleted by the user. */ + ensure_full_index(&opt->priv->orig_index); pos = index_name_pos(&opt->priv->orig_index, path, strlen(path)); ce = opt->priv->orig_index.cache[pos]; if (ce_skip_worktree(ce)) { diff --git a/name-hash.c b/name-hash.c index 4e03fac9bb1259..cb0f316f652fb5 100644 --- a/name-hash.c +++ b/name-hash.c @@ -8,6 +8,7 @@ #include "cache.h" #include "thread-utils.h" #include "trace2.h" +#include "sparse-index.h" struct dir_entry { struct hashmap_entry ent; @@ -109,6 +110,12 @@ static void hash_index_entry(struct index_state *istate, struct cache_entry *ce) if (ce->ce_flags & CE_HASHED) return; ce->ce_flags |= CE_HASHED; + + if (ce->ce_mode == CE_MODE_SPARSE_DIRECTORY) { + add_dir_entry(istate, ce); + return; + } + hashmap_entry_init(&ce->ent, memihash(ce->name, ce_namelen(ce))); hashmap_add(&istate->name_hash, &ce->ent); @@ -680,6 +687,7 @@ int index_dir_exists(struct index_state *istate, const char *name, int namelen) struct dir_entry *dir; lazy_init_name_hash(istate); + expand_to_path(istate, name, namelen, 0); dir = find_dir_entry(istate, name, namelen); return dir && dir->nr; } @@ -690,6 +698,7 @@ void adjust_dirname_case(struct index_state *istate, char *name) const char *ptr = startPtr; lazy_init_name_hash(istate); + expand_to_path(istate, name, strlen(name), 0); while (*ptr) { while (*ptr && *ptr != '/') ptr++; @@ -713,6 +722,7 @@ struct cache_entry *index_file_exists(struct index_state *istate, const char *na unsigned int hash = memihash(name, namelen); lazy_init_name_hash(istate); + expand_to_path(istate, name, namelen, icase); ce = hashmap_get_entry_from_hash(&istate->name_hash, hash, NULL, struct cache_entry, ent); diff --git a/pathspec.c b/pathspec.c index 7a229d8d22f2f6..61dc771aa02bfa 100644 --- a/pathspec.c +++ b/pathspec.c @@ -20,7 +20,7 @@ * to use find_pathspecs_matching_against_index() instead. */ void add_pathspec_matches_against_index(const struct pathspec *pathspec, - const struct index_state *istate, + struct index_state *istate, char *seen) { int num_unmatched = 0, i; @@ -51,7 +51,7 @@ void add_pathspec_matches_against_index(const struct pathspec *pathspec, * given pathspecs achieves against all items in the index. */ char *find_pathspecs_matching_against_index(const struct pathspec *pathspec, - const struct index_state *istate) + struct index_state *istate) { char *seen = xcalloc(pathspec->nr, 1); add_pathspec_matches_against_index(pathspec, istate, seen); diff --git a/pathspec.h b/pathspec.h index 454ce364fac776..f19c5dcf022b7d 100644 --- a/pathspec.h +++ b/pathspec.h @@ -150,10 +150,10 @@ static inline int ps_strcmp(const struct pathspec_item *item, } void add_pathspec_matches_against_index(const struct pathspec *pathspec, - const struct index_state *istate, + struct index_state *istate, char *seen); char *find_pathspecs_matching_against_index(const struct pathspec *pathspec, - const struct index_state *istate); + struct index_state *istate); int match_pathspec_attrs(const struct index_state *istate, const char *name, int namelen, const struct pathspec_item *item); diff --git a/preload-index.c b/preload-index.c index ed6eaa47388af8..323fc8c5100182 100644 --- a/preload-index.c +++ b/preload-index.c @@ -54,6 +54,8 @@ static void *preload_thread(void *_data) continue; if (S_ISGITLINK(ce->ce_mode)) continue; + if (S_ISSPARSEDIR(ce)) + continue; if (ce_uptodate(ce)) continue; if (ce_skip_worktree(ce)) diff --git a/read-cache.c b/read-cache.c index ecf6f689940556..96d9b95128a397 100644 --- a/read-cache.c +++ b/read-cache.c @@ -25,6 +25,7 @@ #include "fsmonitor.h" #include "thread-utils.h" #include "progress.h" +#include "sparse-index.h" /* Mask for the name length in ce_flags in the on-disk index */ @@ -101,6 +102,9 @@ static const char *alternate_index_output; static void set_index_entry(struct index_state *istate, int nr, struct cache_entry *ce) { + if (S_ISSPARSEDIR(ce)) + istate->sparse_index = 1; + istate->cache[nr] = ce; add_name_hash(istate, ce); } @@ -618,7 +622,11 @@ void remove_marked_cache_entries(struct index_state *istate, int invalidate) int remove_file_from_index(struct index_state *istate, const char *path) { - int pos = index_name_pos(istate, path, strlen(path)); + int pos; + + ensure_full_index(istate); + + pos = index_name_pos(istate, path, strlen(path)); if (pos < 0) pos = -pos-1; cache_tree_invalidate_path(istate, path); @@ -636,9 +644,12 @@ static int compare_name(struct cache_entry *ce, const char *path, int namelen) static int index_name_pos_also_unmerged(struct index_state *istate, const char *path, int namelen) { - int pos = index_name_pos(istate, path, namelen); + int pos; struct cache_entry *ce; + expand_to_path(istate, path, namelen, 0); + + pos = index_name_pos(istate, path, namelen); if (pos >= 0) return pos; @@ -720,6 +731,8 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st, return error(_("%s: can only add regular files, symbolic links or git-directories"), path); namelen = strlen(path); + expand_to_path(istate, path, namelen, 0); + if (S_ISDIR(st_mode)) { if (resolve_gitlink_ref(path, "HEAD", &oid) < 0) return error(_("'%s' does not have a commit checked out"), path); @@ -999,8 +1012,15 @@ int verify_path(const char *path, unsigned mode) c = *path++; if ((c == '.' && !verify_dotfile(path, mode)) || - is_dir_sep(c) || c == '\0') + is_dir_sep(c)) return 0; + /* + * allow terminating directory separators for + * sparse directory enries. + */ + if (c == '\0') + return mode == CE_MODE_SPARSE_DIRECTORY || + mode == SPARSE_DIR_MODE; } else if (c == '\\' && protect_ntfs) { if (is_ntfs_dotgit(path)) return 0; @@ -1084,6 +1104,8 @@ static int has_dir_name(struct index_state *istate, size_t len_eq_last; int cmp_last = 0; + expand_to_path(istate, ce->name, ce->ce_namelen, 0); + /* * We are frequently called during an iteration on a sorted * list of pathnames and while building a new index. Therefore, @@ -1327,6 +1349,8 @@ int add_index_entry(struct index_state *istate, struct cache_entry *ce, int opti { int pos; + expand_to_path(istate, ce->name, ce->ce_namelen, 0); + if (option & ADD_CACHE_JUST_APPEND) pos = istate->cache_nr; else { @@ -1536,6 +1560,7 @@ int refresh_index(struct index_state *istate, unsigned int flags, * we only have to do the special cases that are left. */ preload_index(istate, pathspec, 0); + for (i = 0; i < istate->cache_nr; i++) { struct cache_entry *ce, *new_entry; int cache_errno = 0; @@ -1546,6 +1571,9 @@ int refresh_index(struct index_state *istate, unsigned int flags, if (ignore_submodules && S_ISGITLINK(ce->ce_mode)) continue; + if (istate->sparse_index && S_ISSPARSEDIR(ce)) + continue; + if (pathspec && !ce_path_match(istate, ce, pathspec, seen)) filtered = 1; @@ -2255,6 +2283,12 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist) trace2_data_intmax("index", the_repository, "read/cache_nr", istate->cache_nr); + if (!istate->repo) + istate->repo = the_repository; + prepare_repo_settings(istate->repo); + if (istate->repo->settings.command_requires_full_index) + ensure_full_index(istate); + return istate->cache_nr; unmap: @@ -2983,7 +3017,8 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile, if (err) return -1; } - if (!strip_extensions && istate->fsmonitor_last_update) { + if (!strip_extensions && istate->fsmonitor_last_update && + !istate->sparse_index) { struct strbuf sb = STRBUF_INIT; write_fsmonitor_extension(&sb, istate); @@ -3053,6 +3088,13 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l { int ret; + ret = convert_to_sparse(istate); + + if (ret) { + warning(_("failed to convert to a sparse-index")); + return ret; + } + /* * TODO trace2: replace "the_repository" with the actual repo instance * that is associated with the given "istate". @@ -3156,6 +3198,7 @@ static int write_shared_index(struct index_state *istate, int ret; move_cache_to_base_index(istate); + convert_to_sparse(istate); trace2_region_enter_printf("index", "shared/do_write_index", the_repository, "%s", (*temp)->filename.buf); diff --git a/repo-settings.c b/repo-settings.c index f7fff0f5ab837e..9677d50f9238e7 100644 --- a/repo-settings.c +++ b/repo-settings.c @@ -77,4 +77,19 @@ void prepare_repo_settings(struct repository *r) UPDATE_DEFAULT_BOOL(r->settings.core_untracked_cache, UNTRACKED_CACHE_KEEP); UPDATE_DEFAULT_BOOL(r->settings.fetch_negotiation_algorithm, FETCH_NEGOTIATION_DEFAULT); + + /* + * This setting guards all index reads to require a full index + * over a sparse index. After suitable guards are placed in the + * codebase around uses of the index, this setting will be + * removed. + */ + r->settings.command_requires_full_index = 1; + + /* + * Initialize this as off. + */ + r->settings.sparse_index = 0; + if (!repo_config_get_bool(r, "extensions.sparseindex", &value) && value) + r->settings.sparse_index = 1; } diff --git a/repository.c b/repository.c index c98298acd017b5..a8acae002f712c 100644 --- a/repository.c +++ b/repository.c @@ -10,6 +10,7 @@ #include "object.h" #include "lockfile.h" #include "submodule-config.h" +#include "sparse-index.h" /* The main repository */ static struct repository the_repo; @@ -261,6 +262,8 @@ void repo_clear(struct repository *repo) int repo_read_index(struct repository *repo) { + int res; + if (!repo->index) repo->index = xcalloc(1, sizeof(*repo->index)); @@ -270,7 +273,13 @@ int repo_read_index(struct repository *repo) else if (repo->index->repo != repo) BUG("repo's index should point back at itself"); - return read_index_from(repo->index, repo->index_file, repo->gitdir); + res = read_index_from(repo->index, repo->index_file, repo->gitdir); + + prepare_repo_settings(repo); + if (repo->settings.command_requires_full_index) + ensure_full_index(repo->index); + + return res; } int repo_hold_locked_index(struct repository *repo, diff --git a/repository.h b/repository.h index b385ca3c94b62b..a45f7520fd9e12 100644 --- a/repository.h +++ b/repository.h @@ -41,6 +41,9 @@ struct repo_settings { enum fetch_negotiation_setting fetch_negotiation_algorithm; int core_multi_pack_index; + + unsigned command_requires_full_index:1, + sparse_index:1; }; struct repository { diff --git a/rerere.c b/rerere.c index 9281131a9f10cd..1836a6cfbcf33f 100644 --- a/rerere.c +++ b/rerere.c @@ -962,6 +962,8 @@ static int handle_cache(struct index_state *istate, struct rerere_io_mem io; int marker_size = ll_merge_marker_size(istate, path); + ensure_full_index(istate); + /* * Reproduce the conflicted merge in-core */ diff --git a/resolve-undo.c b/resolve-undo.c index 236320f179cbf6..a4265834977e8f 100644 --- a/resolve-undo.c +++ b/resolve-undo.c @@ -125,6 +125,8 @@ int unmerge_index_entry_at(struct index_state *istate, int pos) if (!istate->resolve_undo) return pos; + ensure_full_index(istate); + ce = istate->cache[pos]; if (ce_stage(ce)) { /* already unmerged */ @@ -172,6 +174,8 @@ void unmerge_marked_index(struct index_state *istate) if (!istate->resolve_undo) return; + ensure_full_index(istate); + for (i = 0; i < istate->cache_nr; i++) { const struct cache_entry *ce = istate->cache[i]; if (ce->ce_flags & CE_MATCHED) @@ -186,6 +190,8 @@ void unmerge_index(struct index_state *istate, const struct pathspec *pathspec) if (!istate->resolve_undo) return; + ensure_full_index(istate); + for (i = 0; i < istate->cache_nr; i++) { const struct cache_entry *ce = istate->cache[i]; if (!ce_path_match(istate, ce, pathspec, NULL)) diff --git a/setup.c b/setup.c index c04cd25a30dfe0..cd83945646136f 100644 --- a/setup.c +++ b/setup.c @@ -500,6 +500,9 @@ static enum extension_result handle_extension(const char *var, return error("invalid value for 'extensions.objectformat'"); data->hash_algo = format; return EXTENSION_OK; + } else if (!strcmp(ext, "sparseindex")) { + data->sparse_index = 1; + return EXTENSION_OK; } return EXTENSION_UNKNOWN; } diff --git a/sha1-name.c b/sha1-name.c index 0b23b86ceb4433..c2f17e526ab35f 100644 --- a/sha1-name.c +++ b/sha1-name.c @@ -1734,6 +1734,8 @@ static void diagnose_invalid_index_path(struct repository *r, if (!prefix) prefix = ""; + ensure_full_index(r->index); + /* Wrong stage number? */ pos = index_name_pos(istate, filename, namelen); if (pos < 0) @@ -1854,6 +1856,7 @@ static enum get_oid_result get_oid_with_context_1(struct repository *repo, if (!repo->index || !repo->index->cache) repo_read_index(repo); + ensure_full_index(repo->index); pos = index_name_pos(repo->index, cp, namelen); if (pos < 0) pos = -pos - 1; diff --git a/sparse-index.c b/sparse-index.c new file mode 100644 index 00000000000000..9ea3b32140028f --- /dev/null +++ b/sparse-index.c @@ -0,0 +1,360 @@ +#include "cache.h" +#include "repository.h" +#include "sparse-index.h" +#include "tree.h" +#include "pathspec.h" +#include "trace2.h" +#include "cache-tree.h" +#include "config.h" +#include "dir.h" +#include "fsmonitor.h" + +static struct cache_entry *construct_sparse_dir_entry( + struct index_state *istate, + const char *sparse_dir, + struct cache_tree *tree) +{ + struct cache_entry *de; + + de = make_cache_entry(istate, SPARSE_DIR_MODE, &tree->oid, sparse_dir, 0, 0); + + de->ce_flags |= CE_SKIP_WORKTREE; + return de; +} + +/* + * Returns the number of entries "inserted" into the index. + */ +static int convert_to_sparse_rec(struct index_state *istate, + int num_converted, + int start, int end, + const char *ct_path, size_t ct_pathlen, + struct cache_tree *ct) +{ + int i, can_convert = 1; + int start_converted = num_converted; + enum pattern_match_result match; + int dtype; + struct strbuf child_path = STRBUF_INIT; + struct pattern_list *pl = istate->sparse_checkout_patterns; + + /* + * Is the current path outside of the sparse cone? + * Then check if the region can be replaced by a sparse + * directory entry (everything is sparse and merged). + */ + match = path_matches_pattern_list(ct_path, ct_pathlen, + NULL, &dtype, pl, istate); + if (match != NOT_MATCHED) + can_convert = 0; + + for (i = start; can_convert && i < end; i++) { + struct cache_entry *ce = istate->cache[i]; + + if (ce_stage(ce) || + S_ISGITLINK(ce->ce_mode) || + !(ce->ce_flags & CE_SKIP_WORKTREE)) + can_convert = 0; + } + + if (can_convert) { + struct cache_entry *se; + se = construct_sparse_dir_entry(istate, ct_path, ct); + + istate->cache[num_converted++] = se; + return 1; + } + + for (i = start; i < end; ) { + int count, span, pos = -1; + const char *base, *slash; + struct cache_entry *ce = istate->cache[i]; + + /* + * Detect if this is a normal entry oustide of any subtree + * entry. + */ + base = ce->name + ct_pathlen; + slash = strchr(base, '/'); + + if (slash) + pos = cache_tree_subtree_pos(ct, base, slash - base); + + if (pos < 0) { + istate->cache[num_converted++] = ce; + i++; + continue; + } + + strbuf_setlen(&child_path, 0); + strbuf_add(&child_path, ce->name, slash - ce->name + 1); + + span = ct->down[pos]->cache_tree->entry_count; + count = convert_to_sparse_rec(istate, + num_converted, i, i + span, + child_path.buf, child_path.len, + ct->down[pos]->cache_tree); + num_converted += count; + i += span; + } + + strbuf_release(&child_path); + return num_converted - start_converted; +} + +static int enable_sparse_index(struct repository *repo) +{ + int res; + + if (upgrade_repository_format(1) < 0) { + warning(_("unable to upgrade repository format to enable sparse-index")); + return -1; + } + res = git_config_set_gently("extensions.sparseindex", "true"); + + prepare_repo_settings(repo); + repo->settings.sparse_index = 1; + return res; +} + +int set_sparse_index_config(struct repository *repo, int enable) +{ + int res; + + if (enable) + return enable_sparse_index(repo); + + /* Don't downgrade repository format, just remove the extension. */ + res = git_config_set_multivar_gently("extensions.sparseindex", NULL, "", + CONFIG_FLAGS_MULTI_REPLACE); + + prepare_repo_settings(repo); + repo->settings.sparse_index = 0; + return res; +} + +int convert_to_sparse(struct index_state *istate) +{ + int test_env; + if (istate->split_index || istate->sparse_index || + !core_apply_sparse_checkout || !core_sparse_checkout_cone) + return 0; + + if (!istate->repo) + istate->repo = the_repository; + + /* + * If GIT_TEST_SPARSE_INDEX=1, then trigger extensions.sparseIndex + * to be fully enabled. If GIT_TEST_SPARSE_INDEX=0 (set explicitly), + * then purposefully disable the setting. + */ + test_env = git_env_bool("GIT_TEST_SPARSE_INDEX", -1); + if (test_env >= 0) + set_sparse_index_config(istate->repo, test_env); + + /* + * Only convert to sparse if extensions.sparseIndex is set. + */ + prepare_repo_settings(istate->repo); + if (!istate->repo->settings.sparse_index) + return 0; + + if (!istate->sparse_checkout_patterns) { + istate->sparse_checkout_patterns = xcalloc(1, sizeof(struct pattern_list)); + if (get_sparse_checkout_patterns(istate->sparse_checkout_patterns) < 0) + return 0; + } + + if (!istate->sparse_checkout_patterns->use_cone_patterns) { + warning(_("attempting to use sparse-index without cone mode")); + return -1; + } + + if (cache_tree_update(istate, 0)) { + warning(_("unable to update cache-tree, staying full")); + return -1; + } + + remove_fsmonitor(istate); + + trace2_region_enter("index", "convert_to_sparse", istate->repo); + istate->cache_nr = convert_to_sparse_rec(istate, + 0, 0, istate->cache_nr, + "", 0, istate->cache_tree); + + /* Clear and recompute the cache-tree */ + cache_tree_free(&istate->cache_tree); + cache_tree_update(istate, 0); + + istate->sparse_index = 1; + trace2_region_leave("index", "convert_to_sparse", istate->repo); + return 0; +} + +static void set_index_entry(struct index_state *istate, int nr, struct cache_entry *ce) +{ + ALLOC_GROW(istate->cache, nr + 1, istate->cache_alloc); + + istate->cache[nr] = ce; + add_name_hash(istate, ce); +} + +static int add_path_to_index(const struct object_id *oid, + struct strbuf *base, const char *path, + unsigned int mode, int stage, void *context) +{ + struct index_state *istate = (struct index_state *)context; + struct cache_entry *ce; + size_t len = base->len; + + if (S_ISDIR(mode)) + return READ_TREE_RECURSIVE; + + strbuf_addstr(base, path); + + ce = make_cache_entry(istate, mode, oid, base->buf, 0, 0); + ce->ce_flags |= CE_SKIP_WORKTREE; + set_index_entry(istate, istate->cache_nr++, ce); + + strbuf_setlen(base, len); + return 0; +} + +void ensure_full_index(struct index_state *istate) +{ + int i; + struct index_state *full; + + if (!istate || !istate->sparse_index) + return; + + if (!istate->repo) + istate->repo = the_repository; + + trace2_region_enter("index", "ensure_full_index", istate->repo); + + /* initialize basics of new index */ + full = xcalloc(1, sizeof(struct index_state)); + memcpy(full, istate, sizeof(struct index_state)); + + /* then change the necessary things */ + full->sparse_index = 0; + full->cache_alloc = (3 * istate->cache_alloc) / 2; + full->cache_nr = 0; + ALLOC_ARRAY(full->cache, full->cache_alloc); + + for (i = 0; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + struct tree *tree; + struct pathspec ps; + + if (!S_ISSPARSEDIR(ce)) { + set_index_entry(full, full->cache_nr++, ce); + continue; + } + if (!(ce->ce_flags & CE_SKIP_WORKTREE)) + warning(_("index entry is a directory, but not sparse (%08x)"), + ce->ce_flags); + + /* recursively walk into cd->name */ + tree = lookup_tree(istate->repo, &ce->oid); + + memset(&ps, 0, sizeof(ps)); + ps.recursive = 1; + ps.has_wildcard = 1; + ps.max_depth = -1; + + read_tree_recursive(istate->repo, tree, + ce->name, strlen(ce->name), + 0, &ps, + add_path_to_index, full); + + /* free directory entries. full entries are re-used */ + discard_cache_entry(ce); + } + + /* Copy back into original index. */ + memcpy(&istate->name_hash, &full->name_hash, sizeof(full->name_hash)); + istate->sparse_index = 0; + istate->cache = full->cache; + istate->cache_nr = full->cache_nr; + istate->cache_alloc = full->cache_alloc; + + free(full); + + /* Clear and recompute the cache-tree */ + cache_tree_free(&istate->cache_tree); + cache_tree_update(istate, 0); + + trace2_region_leave("index", "ensure_full_index", istate->repo); +} + +static int in_expand_to_path = 0; + +void expand_to_path(struct index_state *istate, + const char *path, size_t pathlen, int icase) +{ + struct cache_entry *ce = NULL; + struct strbuf path_as_dir = STRBUF_INIT; + int pos; + + /* 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; + + /* + * We only need to actually expand a region if the + * following are both true: + * + * 1. 'path' is not already in the index. + * 2. Some parent directory of 'path' is a sparse directory. + */ + + strbuf_add(&path_as_dir, path, pathlen); + strbuf_addch(&path_as_dir, '/'); + + /* in_expand_to_path prevents infinite recursion here */ + if (index_file_exists(istate, path, pathlen, icase)) + goto cleanup; + + pos = index_name_pos(istate, path_as_dir.buf, path_as_dir.len); + + if (pos < 0) + pos = -pos - 1; + if (pos < istate->cache_nr) + ce = istate->cache[pos]; + + /* + * If we didn't land on a sparse directory, then there is + * nothing to expand. + */ + if (ce && !S_ISSPARSEDIR(ce)) + goto cleanup; + /* + * If that sparse directory is not a prefix of the path we + * are looking for, then we don't need to expand. + */ + if (ce && + (ce->ce_namelen >= path_as_dir.len || + strncmp(ce->name, path_as_dir.buf, ce->ce_namelen))) + goto cleanup; + + trace2_region_enter("index", "expand_to_path", istate->repo); + + /* for now, do the obviously-correct, slow thing */ + ensure_full_index(istate); + + trace2_region_leave("index", "expand_to_path", istate->repo); + +cleanup: + strbuf_release(&path_as_dir); + in_expand_to_path = 0; +} diff --git a/sparse-index.h b/sparse-index.h new file mode 100644 index 00000000000000..549e4171f1ae88 --- /dev/null +++ b/sparse-index.h @@ -0,0 +1,23 @@ +#ifndef SPARSE_INDEX_H__ +#define SPARSE_INDEX_H__ + +struct index_state; +void ensure_full_index(struct index_state *istate); +int convert_to_sparse(struct index_state *istate); +/* + * Some places in the codebase expect to search for a specific path. + * This path might be outside of the sparse-checkout definition, in + * which case a sparse-index may not contain a path for that index. + * + * Given an index and a path, check to see if a leading directory for + * 'path' exists in the index as a sparse directory. In that case, + * expand that sparse directory to a full range of cache entries and + * populate the index accordingly. + */ +void expand_to_path(struct index_state *istate, + const char *path, size_t pathlen, int icase); + +struct repository; +int set_sparse_index_config(struct repository *repo, int enable); + +#endif \ No newline at end of file diff --git a/split-index.c b/split-index.c index c0e8ad670d0a17..3150fa6476aabe 100644 --- a/split-index.c +++ b/split-index.c @@ -4,6 +4,8 @@ struct split_index *init_split_index(struct index_state *istate) { + ensure_full_index(istate); + if (!istate->split_index) { istate->split_index = xcalloc(1, sizeof(*istate->split_index)); istate->split_index->refcount = 1; diff --git a/submodule.c b/submodule.c index b3bb59f0664473..487d083e4ef9a7 100644 --- a/submodule.c +++ b/submodule.c @@ -33,9 +33,13 @@ static struct oid_array ref_tips_after_fetch; * will be disabled because we can't guess what might be configured in * .gitmodules unless the user resolves the conflict. */ -int is_gitmodules_unmerged(const struct index_state *istate) +int is_gitmodules_unmerged(struct index_state *istate) { - int pos = index_name_pos(istate, GITMODULES_FILE, strlen(GITMODULES_FILE)); + int pos; + + ensure_full_index(istate); + + pos = index_name_pos(istate, GITMODULES_FILE, strlen(GITMODULES_FILE)); if (pos < 0) { /* .gitmodules not found or isn't merged */ pos = -1 - pos; if (istate->cache_nr > pos) { /* there is a .gitmodules */ @@ -77,7 +81,11 @@ int is_writing_gitmodules_ok(void) */ int is_staging_gitmodules_ok(struct index_state *istate) { - int pos = index_name_pos(istate, GITMODULES_FILE, strlen(GITMODULES_FILE)); + int pos; + + ensure_full_index(istate); + + pos = index_name_pos(istate, GITMODULES_FILE, strlen(GITMODULES_FILE)); if ((pos >= 0) && (pos < istate->cache_nr)) { struct stat st; @@ -301,7 +309,7 @@ int is_submodule_populated_gently(const char *path, int *return_error_code) /* * Dies if the provided 'prefix' corresponds to an unpopulated submodule */ -void die_in_unpopulated_submodule(const struct index_state *istate, +void die_in_unpopulated_submodule(struct index_state *istate, const char *prefix) { int i, prefixlen; @@ -311,6 +319,8 @@ void die_in_unpopulated_submodule(const struct index_state *istate, prefixlen = strlen(prefix); + ensure_full_index(istate); + for (i = 0; i < istate->cache_nr; i++) { struct cache_entry *ce = istate->cache[i]; int ce_len = ce_namelen(ce); @@ -331,7 +341,7 @@ void die_in_unpopulated_submodule(const struct index_state *istate, /* * Dies if any paths in the provided pathspec descends into a submodule */ -void die_path_inside_submodule(const struct index_state *istate, +void die_path_inside_submodule(struct index_state *istate, const struct pathspec *ps) { int i, j; @@ -1420,6 +1430,8 @@ static int get_next_submodule(struct child_process *cp, { struct submodule_parallel_fetch *spf = data; + ensure_full_index(spf->r->index); + for (; spf->count < spf->r->index->cache_nr; spf->count++) { const struct cache_entry *ce = spf->r->index->cache[spf->count]; const char *default_argv; diff --git a/submodule.h b/submodule.h index 4ac6e31cf1f7dd..84640c49c1149d 100644 --- a/submodule.h +++ b/submodule.h @@ -39,7 +39,7 @@ struct submodule_update_strategy { }; #define SUBMODULE_UPDATE_STRATEGY_INIT {SM_UPDATE_UNSPECIFIED, NULL} -int is_gitmodules_unmerged(const struct index_state *istate); +int is_gitmodules_unmerged(struct index_state *istate); int is_writing_gitmodules_ok(void); int is_staging_gitmodules_ok(struct index_state *istate); int update_path_in_gitmodules(const char *oldpath, const char *newpath); @@ -60,9 +60,9 @@ int is_submodule_active(struct repository *repo, const char *path); * Otherwise the return error code is the same as of resolve_gitdir_gently. */ int is_submodule_populated_gently(const char *path, int *return_error_code); -void die_in_unpopulated_submodule(const struct index_state *istate, +void die_in_unpopulated_submodule(struct index_state *istate, const char *prefix); -void die_path_inside_submodule(const struct index_state *istate, +void die_path_inside_submodule(struct index_state *istate, const struct pathspec *ps); enum submodule_update_type parse_submodule_update_type(const char *value); int parse_submodule_update_strategy(const char *value, diff --git a/t/helper/test-read-cache.c b/t/helper/test-read-cache.c index 244977a29bdfda..3c45dfeb3cb867 100644 --- a/t/helper/test-read-cache.c +++ b/t/helper/test-read-cache.c @@ -1,36 +1,93 @@ #include "test-tool.h" #include "cache.h" #include "config.h" +#include "sparse-index.h" + +static void print_cache_entry(struct cache_entry *ce, unsigned stat) +{ + if (stat) { + /* stat info */ + printf("%08x %08x %08x %08x %08x %08x ", + ce->ce_stat_data.sd_ctime.sec, + ce->ce_stat_data.sd_ctime.nsec, + ce->ce_stat_data.sd_mtime.sec, + ce->ce_stat_data.sd_mtime.nsec, + ce->ce_stat_data.sd_dev, + ce->ce_stat_data.sd_ino); + } + + /* mode in binary */ + printf("0b%d%d%d%d ", + (ce->ce_mode >> 15) & 1, + (ce->ce_mode >> 14) & 1, + (ce->ce_mode >> 13) & 1, + (ce->ce_mode >> 12) & 1); + + /* output permissions? */ + printf("%04o ", ce->ce_mode & 01777); + + printf("%s ", oid_to_hex(&ce->oid)); + + printf("%s\n", ce->name); +} + +static void print_cache(struct index_state *cache, unsigned stat) +{ + int i; + for (i = 0; i < the_index.cache_nr; i++) + print_cache_entry(the_index.cache[i], stat); +} int cmd__read_cache(int argc, const char **argv) { + struct repository *r = the_repository; int i, cnt = 1; const char *name = NULL; + int table = 0; + int stat = 1; + int expand = 0; + + initialize_the_repository(); + prepare_repo_settings(r); + r->settings.command_requires_full_index = 0; - if (argc > 1 && skip_prefix(argv[1], "--print-and-refresh=", &name)) { - argc--; - argv++; + for (++argv, --argc; *argv && starts_with(*argv, "--"); ++argv, --argc) { + if (skip_prefix(*argv, "--print-and-refresh=", &name)) + continue; + if (!strcmp(*argv, "--table")) + table = 1; + else if (!strcmp(*argv, "--no-stat")) + stat = 0; + else if (!strcmp(*argv, "--expand")) + expand = 1; } - if (argc == 2) - cnt = strtol(argv[1], NULL, 0); + if (argc == 1) + cnt = strtol(argv[0], NULL, 0); setup_git_directory(); git_config(git_default_config, NULL); + for (i = 0; i < cnt; i++) { - read_cache(); + repo_read_index(r); + + if (expand) + ensure_full_index(r->index); + if (name) { int pos; - refresh_index(&the_index, REFRESH_QUIET, + refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL); - pos = index_name_pos(&the_index, name, strlen(name)); + pos = index_name_pos(r->index, name, strlen(name)); if (pos < 0) die("%s not in index", name); printf("%s is%s up to date\n", name, - ce_uptodate(the_index.cache[pos]) ? "" : " not"); + ce_uptodate(r->index->cache[pos]) ? "" : " not"); write_file(name, "%d\n", i); } - discard_cache(); + if (table) + print_cache(r->index, stat); + discard_index(r->index); } return 0; } diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index 8cd3e5a8d2277f..1cdf33a402559c 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -2,11 +2,16 @@ test_description='compare full workdir to sparse workdir' +GIT_TEST_CHECK_CACHE_TREE=0 +GIT_TEST_SPLIT_INDEX=0 +GIT_TEST_SPARSE_INDEX= + . ./test-lib.sh test_expect_success 'setup' ' git init initial-repo && ( + (GIT_TEST_SPARSE_INDEX=0 && export GIT_TEST_SPARSE_INDEX) && cd initial-repo && echo a >a && echo "after deep" >e && @@ -87,23 +92,33 @@ init_repos () { cp -r initial-repo sparse-checkout && git -C sparse-checkout reset --hard && - git -C sparse-checkout sparse-checkout init --cone && + + cp -r initial-repo sparse-index && + git -C sparse-index reset --hard && # initialize sparse-checkout definitions - git -C sparse-checkout sparse-checkout set deep + git -C sparse-checkout sparse-checkout init --cone && + git -C sparse-checkout sparse-checkout set deep && + git -C sparse-index sparse-checkout init --cone --sparse-index && + test_cmp_config -C sparse-index true extensions.sparseindex && + git -C sparse-index sparse-checkout set deep } run_on_sparse () { ( cd sparse-checkout && - $* >../sparse-checkout-out 2>../sparse-checkout-err + GIT_TEST_SPARSE_INDEX=0 $* >../sparse-checkout-out 2>../sparse-checkout-err + ) && + ( + cd sparse-index && + $* >../sparse-index-out 2>../sparse-index-err ) } run_on_all () { ( cd full-checkout && - $* >../full-checkout-out 2>../full-checkout-err + GIT_TEST_SPARSE_INDEX=0 $* >../full-checkout-out 2>../full-checkout-err ) && run_on_sparse $* } @@ -111,11 +126,57 @@ run_on_all () { test_all_match () { run_on_all $* && test_cmp full-checkout-out sparse-checkout-out && - test_cmp full-checkout-err sparse-checkout-err + test_cmp full-checkout-out sparse-index-out && + test_cmp full-checkout-err sparse-checkout-err && + test_cmp full-checkout-err sparse-index-err +} + +test_sparse_match () { + run_on_sparse $* && + test_cmp sparse-checkout-out sparse-index-out && + test_cmp sparse-checkout-err sparse-index-err } +test_expect_success 'sparse-index contents' ' + init_repos && + + test-tool -C sparse-index read-cache --table --no-stat >cache && + for dir in folder1 folder2 x + do + TREE=$(git -C sparse-index rev-parse HEAD:$dir) && + grep "0b0000 0755 $TREE $dir/" cache \ + || return 1 + done && + + git -C sparse-index sparse-checkout set folder1 && + + test-tool -C sparse-index read-cache --table --no-stat >cache && + for dir in deep folder2 x + do + TREE=$(git -C sparse-index rev-parse HEAD:$dir) && + grep "0b0000 0755 $TREE $dir/" cache \ + || return 1 + done && + + git -C sparse-index sparse-checkout set deep/deeper1 && + + test-tool -C sparse-index read-cache --table --no-stat >cache && + for dir in deep/deeper2 folder1 folder2 x + do + TREE=$(git -C sparse-index rev-parse HEAD:$dir) && + grep "0b0000 0755 $TREE $dir/" cache \ + || return 1 + done +' + +test_expect_success 'expanded in-memory index matches full index' ' + init_repos && + test_sparse_match test-tool read-cache --expand --table --no-stat +' + test_expect_success 'status with options' ' init_repos && + test_sparse_match ls && test_all_match git status --porcelain=v2 && test_all_match git status --porcelain=v2 -z -u && test_all_match git status --porcelain=v2 -uno && @@ -129,6 +190,14 @@ test_expect_success 'status with options' ' test_all_match git status --porcelain=v2 -uno ' +test_expect_success 'status reports sparse-checkout' ' + init_repos && + git -C sparse-checkout status >full && + git -C sparse-index status >sparse && + test_i18ngrep "You are in a sparse checkout with " full && + test_i18ngrep "You are in a sparse checkout." sparse +' + test_expect_success 'add, commit, checkout' ' init_repos && @@ -148,7 +217,7 @@ test_expect_success 'add, commit, checkout' ' test_all_match git add -A && test_all_match git status --porcelain=v2 && - test_all_match git commit -m "Extend README.md" && + test_all_match git commit -m "Extend-README.md" && test_all_match git checkout HEAD~1 && test_all_match git checkout - && @@ -252,6 +321,17 @@ test_expect_failure 'checkout and reset (mixed)' ' test_all_match git reset update-folder2 ' +# Ensure that sparse-index behaves identically to +# sparse-checkout with a full index. +test_expect_success 'checkout and reset (mixed) [sparse]' ' + init_repos && + + test_sparse_match git checkout -b reset-test update-deep && + test_sparse_match git reset deepest && + test_sparse_match git reset update-folder1 && + test_sparse_match git reset update-folder2 +' + test_expect_success 'merge' ' init_repos && @@ -288,14 +368,50 @@ test_expect_success 'clean' ' test_all_match git status --porcelain=v2 && test_all_match git clean -f && test_all_match git status --porcelain=v2 && + test_sparse_match ls && + test_sparse_match ls folder1 && test_all_match git clean -xf && test_all_match git status --porcelain=v2 && + test_sparse_match ls && + test_sparse_match ls folder1 && test_all_match git clean -xdf && test_all_match git status --porcelain=v2 && + test_sparse_match ls && + test_sparse_match ls folder1 && + + test_sparse_match test_path_is_dir folder1 +' + +test_expect_success 'sparse-index is expanded and converted back' ' + init_repos && + + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + git -C sparse-index reset --hard && + test_region index convert_to_sparse trace2.txt && + test_region index ensure_full_index trace2.txt +' + +test_expect_success 'sparse-index is not expanded' ' + init_repos && - test_path_is_dir sparse-checkout/folder1 + rm -f trace2.txt && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + git -C sparse-index status -uno && + test_region ! index ensure_full_index trace2.txt && + + rm trace2.txt && + echo >>sparse-index/README.md && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + git -C sparse-index add -A && + test_region ! index ensure_full_index trace2.txt && + + rm trace2.txt && + echo >>sparse-index/extra.txt && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \ + git -C sparse-index add extra.txt && + test_region ! index ensure_full_index trace2.txt ' test_done diff --git a/tree.c b/tree.c index e76517f6b180e4..60f575440c8a4e 100644 --- a/tree.c +++ b/tree.c @@ -170,6 +170,8 @@ int read_tree(struct repository *r, struct tree *tree, int stage, * to matter. */ + ensure_full_index(istate); + /* * See if we have cache entry at the stage. If so, * do it the original slow way, otherwise, append and then diff --git a/unpack-trees.c b/unpack-trees.c index f5f668f532d8d5..90644856a8072a 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -583,6 +583,13 @@ static void mark_ce_used(struct cache_entry *ce, struct unpack_trees_options *o) { ce->ce_flags |= CE_UNPACKED; + /* + * If this is a sparse directory, don't advance cache_bottom. + * That will be advanced later using the cache-tree data. + */ + if (S_ISSPARSEDIR(ce)) + return; + if (o->cache_bottom < o->src_index->cache_nr && o->src_index->cache[o->cache_bottom] == ce) { int bottom = o->cache_bottom; @@ -746,9 +753,12 @@ static int index_pos_by_traverse_info(struct name_entry *names, strbuf_make_traverse_path(&name, info, names->path, names->pathlen); strbuf_addch(&name, '/'); pos = index_name_pos(o->src_index, name.buf, name.len); - if (pos >= 0) - BUG("This is a directory and should not exist in index"); - pos = -pos - 1; + if (pos >= 0) { + if (!o->src_index->sparse_index || + !(o->src_index->cache[pos]->ce_flags & CE_SKIP_WORKTREE)) + BUG("This is a directory and should not exist in index"); + } else + pos = -pos - 1; if (pos >= o->src_index->cache_nr || !starts_with(o->src_index->cache[pos]->name, name.buf) || (pos > 0 && starts_with(o->src_index->cache[pos-1]->name, name.buf))) @@ -977,6 +987,9 @@ static int do_compare_entry(const struct cache_entry *ce, ce_len -= pathlen; ce_name = ce->name + pathlen; + /* remove directory separator if a sparse directory entry */ + if (S_ISSPARSEDIR(ce)) + ce_len--; return df_name_compare(ce_name, ce_len, S_IFREG, name, namelen, mode); } @@ -986,6 +999,10 @@ static int compare_entry(const struct cache_entry *ce, const struct traverse_inf if (cmp) return cmp; + /* If ce is a sparse directory, then allow equality here. */ + if (S_ISSPARSEDIR(ce)) + return 0; + /* * Even if the beginning compared identically, the ce should * compare as bigger than a directory leading up to it! @@ -1236,6 +1253,7 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str struct cache_entry *src[MAX_UNPACK_TREES + 1] = { NULL, }; struct unpack_trees_options *o = info->data; const struct name_entry *p = names; + unsigned recurse = 1; /* Find first entry with a real name (we could use "mask" too) */ while (!p->mode) @@ -1277,12 +1295,16 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str } } src[0] = ce; + + if (S_ISSPARSEDIR(ce)) + recurse = 0; } break; } } - if (unpack_nondirectories(n, mask, dirmask, src, names, info) < 0) + if (recurse && + unpack_nondirectories(n, mask, dirmask, src, names, info) < 0) return -1; if (o->merge && src[0]) { @@ -1312,7 +1334,8 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str } } - if (traverse_trees_recursive(n, dirmask, mask & ~dirmask, + if (recurse && + traverse_trees_recursive(n, dirmask, mask & ~dirmask, names, info) < 0) return -1; return mask; @@ -1567,6 +1590,7 @@ static int verify_absent(const struct cache_entry *, */ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options *o) { + struct repository *repo = the_repository; int i, ret; static struct cache_entry *dfc; struct pattern_list pl; @@ -1578,6 +1602,12 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options trace_performance_enter(); trace2_region_enter("unpack_trees", "unpack_trees", the_repository); + prepare_repo_settings(repo); + if (repo->settings.command_requires_full_index) { + ensure_full_index(o->src_index); + ensure_full_index(o->dst_index); + } + if (!core_apply_sparse_checkout || !o->update) o->skip_sparse_checkout = 1; if (!o->skip_sparse_checkout && !o->pl) { diff --git a/wt-status.c b/wt-status.c index 7074bbdd53cc11..46c9d71068e7c5 100644 --- a/wt-status.c +++ b/wt-status.c @@ -509,6 +509,8 @@ static int unmerged_mask(struct index_state *istate, const char *path) int pos, mask; const struct cache_entry *ce; + ensure_full_index(istate); + pos = index_name_pos(istate, path, strlen(path)); if (0 <= pos) return 0; @@ -657,6 +659,8 @@ static void wt_status_collect_changes_initial(struct wt_status *s) struct index_state *istate = s->repo->index; int i; + ensure_full_index(istate); + for (i = 0; i < istate->cache_nr; i++) { struct string_list_item *it; struct wt_status_change_data *d; @@ -1488,9 +1492,12 @@ static void show_sparse_checkout_in_use(struct wt_status *s, if (s->state.sparse_checkout_percentage == SPARSE_CHECKOUT_DISABLED) return; - status_printf_ln(s, color, - _("You are in a sparse checkout with %d%% of tracked files present."), - s->state.sparse_checkout_percentage); + if (s->state.sparse_checkout_percentage == SPARSE_CHECKOUT_SPARSE_INDEX) + status_printf_ln(s, color, _("You are in a sparse checkout.")); + else + status_printf_ln(s, color, + _("You are in a sparse checkout with %d%% of tracked files present."), + s->state.sparse_checkout_percentage); wt_longstatus_print_trailer(s); } @@ -1648,6 +1655,11 @@ static void wt_status_check_sparse_checkout(struct repository *r, return; } + if (r->index->sparse_index) { + state->sparse_checkout_percentage = SPARSE_CHECKOUT_SPARSE_INDEX; + return; + } + for (i = 0; i < r->index->cache_nr; i++) { struct cache_entry *ce = r->index->cache[i]; if (ce_skip_worktree(ce)) @@ -2295,6 +2307,9 @@ static void wt_porcelain_v2_print_unmerged_entry( */ memset(stages, 0, sizeof(stages)); sum = 0; + + ensure_full_index(istate); + pos = index_name_pos(istate, it->string, strlen(it->string)); assert(pos < 0); pos = -pos-1; diff --git a/wt-status.h b/wt-status.h index 35b44c388edf0c..3cb0c200244171 100644 --- a/wt-status.h +++ b/wt-status.h @@ -80,6 +80,7 @@ enum wt_status_format { #define HEAD_DETACHED_AT _("HEAD detached at ") #define HEAD_DETACHED_FROM _("HEAD detached from ") #define SPARSE_CHECKOUT_DISABLED -1 +#define SPARSE_CHECKOUT_SPARSE_INDEX -2 struct wt_status_state { int merge_in_progress;