Merge branch 'cc/split-index-config'
authorJunio C Hamano <gitster@pobox.com>
Fri, 17 Mar 2017 20:50:23 +0000 (13:50 -0700)
committerJunio C Hamano <gitster@pobox.com>
Fri, 17 Mar 2017 20:50:23 +0000 (13:50 -0700)
The experimental "split index" feature has gained a few
configuration variables to make it easier to use.

* cc/split-index-config: (22 commits)
Documentation/git-update-index: explain splitIndex.*
Documentation/config: add splitIndex.sharedIndexExpire
read-cache: use freshen_shared_index() in read_index_from()
read-cache: refactor read_index_from()
t1700: test shared index file expiration
read-cache: unlink old sharedindex files
config: add git_config_get_expiry() from gc.c
read-cache: touch shared index files when used
sha1_file: make check_and_freshen_file() non static
Documentation/config: add splitIndex.maxPercentChange
t1700: add tests for splitIndex.maxPercentChange
read-cache: regenerate shared index if necessary
config: add git_config_get_max_percent_split_change()
Documentation/git-update-index: talk about core.splitIndex config var
Documentation/config: add information for core.splitIndex
t1700: add tests for core.splitIndex
update-index: warn in case of split-index incoherency
read-cache: add and then use tweak_split_index()
split-index: add {add,remove}_split_index() functions
config: add git_config_get_split_index()
...

Documentation/config.txt
Documentation/git-update-index.txt
builtin/gc.c
builtin/update-index.c
cache.h
config.c
read-cache.c
sha1_file.c
split-index.c
split-index.h
t/t1700-split-index.sh
index 5e5c2ae5fb378dc3226e58a4a0df7c3c63472f60..eccc012672925a1d7967d1e2349152a0b240e85a 100644 (file)
@@ -334,6 +334,10 @@ core.trustctime::
        crawlers and some backup systems).
        See linkgit:git-update-index[1]. True by default.
 
+core.splitIndex::
+       If true, the split-index feature of the index will be used.
+       See linkgit:git-update-index[1]. False by default.
+
 core.untrackedCache::
        Determines what to do about the untracked cache feature of the
        index. It will be kept, if this variable is unset or set to
@@ -2850,6 +2854,31 @@ showbranch.default::
        The default set of branches for linkgit:git-show-branch[1].
        See linkgit:git-show-branch[1].
 
+splitIndex.maxPercentChange::
+       When the split index feature is used, this specifies the
+       percent of entries the split index can contain compared to the
+       total number of entries in both the split index and the shared
+       index before a new shared index is written.
+       The value should be between 0 and 100. If the value is 0 then
+       a new shared index is always written, if it is 100 a new
+       shared index is never written.
+       By default the value is 20, so a new shared index is written
+       if the number of entries in the split index would be greater
+       than 20 percent of the total number of entries.
+       See linkgit:git-update-index[1].
+
+splitIndex.sharedIndexExpire::
+       When the split index feature is used, shared index files that
+       were not modified since the time this variable specifies will
+       be removed when a new shared index file is created. The value
+       "now" expires all entries immediately, and "never" suppresses
+       expiration altogether.
+       The default value is "2.weeks.ago".
+       Note that a shared index file is considered modified (for the
+       purpose of expiration) each time a new split-index file is
+       either created based on it or read from it.
+       See linkgit:git-update-index[1].
+
 status.relativePaths::
        By default, linkgit:git-status[1] shows paths relative to the
        current directory. Setting this variable to `false` shows paths
index 7386c931627f1c2bd1303407075db9831ec4045c..1579abf3c3b45f4e63f8f17746dde31a06a3a564 100644 (file)
@@ -163,14 +163,16 @@ may not support it yet.
 
 --split-index::
 --no-split-index::
-       Enable or disable split index mode. If enabled, the index is
-       split into two files, $GIT_DIR/index and $GIT_DIR/sharedindex.<SHA-1>.
-       Changes are accumulated in $GIT_DIR/index while the shared
-       index file contains all index entries stays unchanged. If
-       split-index mode is already enabled and `--split-index` is
-       given again, all changes in $GIT_DIR/index are pushed back to
-       the shared index file. This mode is designed for very large
-       indexes that take a significant amount of time to read or write.
+       Enable or disable split index mode. If split-index mode is
+       already enabled and `--split-index` is given again, all
+       changes in $GIT_DIR/index are pushed back to the shared index
+       file.
++
+These options take effect whatever the value of the `core.splitIndex`
+configuration variable (see linkgit:git-config[1]). But a warning is
+emitted when the change goes against the configured value, as the
+configured value will take effect next time the index is read and this
+will remove the intended effect of the option.
 
 --untracked-cache::
 --no-untracked-cache::
@@ -388,6 +390,31 @@ Although this bit looks similar to assume-unchanged bit, its goal is
 different from assume-unchanged bit's. Skip-worktree also takes
 precedence over assume-unchanged bit when both are set.
 
+Split index
+-----------
+
+This mode is designed for repositories with very large indexes, and
+aims at reducing the time it takes to repeatedly write these indexes.
+
+In this mode, the index is split into two files, $GIT_DIR/index and
+$GIT_DIR/sharedindex.<SHA-1>. Changes are accumulated in
+$GIT_DIR/index, the split index, while the shared index file contains
+all index entries and stays unchanged.
+
+All changes in the split index are pushed back to the shared index
+file when the number of entries in the split index reaches a level
+specified by the splitIndex.maxPercentChange config variable (see
+linkgit:git-config[1]).
+
+Each time a new shared index file is created, the old shared index
+files are deleted if their modification time is older than what is
+specified by the splitIndex.sharedIndexExpire config variable (see
+linkgit:git-config[1]).
+
+To avoid deleting a shared index file that is still used, its
+modification time is updated to the current time everytime a new split
+index based on the shared index file is either created or read from.
+
 Untracked cache
 ---------------
 
index a2b9e8924e1a3e3a537d86c4a3da1fa1967c1863..c2c61a57bb359a37373ffd49aba79e5d5514a9fc 100644 (file)
@@ -64,17 +64,6 @@ static void report_pack_garbage(unsigned seen_bits, const char *path)
                string_list_append(&pack_garbage, path);
 }
 
-static void git_config_date_string(const char *key, const char **output)
-{
-       if (git_config_get_string_const(key, output))
-               return;
-       if (strcmp(*output, "now")) {
-               unsigned long now = approxidate("now");
-               if (approxidate(*output) >= now)
-                       git_die_config(key, _("Invalid %s: '%s'"), key, *output);
-       }
-}
-
 static void process_log_file(void)
 {
        struct stat st;
@@ -131,9 +120,9 @@ static void gc_config(void)
        git_config_get_int("gc.auto", &gc_auto_threshold);
        git_config_get_int("gc.autopacklimit", &gc_auto_pack_limit);
        git_config_get_bool("gc.autodetach", &detach_auto);
-       git_config_date_string("gc.pruneexpire", &prune_expire);
-       git_config_date_string("gc.worktreepruneexpire", &prune_worktrees_expire);
-       git_config_date_string("gc.logexpiry", &gc_log_expire);
+       git_config_get_expiry("gc.pruneexpire", &prune_expire);
+       git_config_get_expiry("gc.worktreepruneexpire", &prune_worktrees_expire);
+       git_config_get_expiry("gc.logexpiry", &gc_log_expire);
 
        git_config(git_default_config, NULL);
 }
index d530e89368b42bf1465784b80163b9158a06585a..d74d72cc7fad93980abd409d2c9dc10ae4652fd3 100644 (file)
@@ -1099,17 +1099,20 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
        }
 
        if (split_index > 0) {
-               init_split_index(&the_index);
-               the_index.cache_changed |= SPLIT_INDEX_ORDERED;
-       } else if (!split_index && the_index.split_index) {
-               /*
-                * can't discard_split_index(&the_index); because that
-                * will destroy split_index->base->cache[], which may
-                * be shared with the_index.cache[]. So yeah we're
-                * leaking a bit here.
-                */
-               the_index.split_index = NULL;
-               the_index.cache_changed |= SOMETHING_CHANGED;
+               if (git_config_get_split_index() == 0)
+                       warning(_("core.splitIndex is set to false; "
+                                 "remove or change it, if you really want to "
+                                 "enable split index"));
+               if (the_index.split_index)
+                       the_index.cache_changed |= SPLIT_INDEX_ORDERED;
+               else
+                       add_split_index(&the_index);
+       } else if (!split_index) {
+               if (git_config_get_split_index() == 1)
+                       warning(_("core.splitIndex is set to true; "
+                                 "remove or change it, if you really want to "
+                                 "disable split index"));
+               remove_split_index(&the_index);
        }
 
        switch (untracked_cache) {
diff --git a/cache.h b/cache.h
index c958269715cc73e218604f6c3b82f547d376b050..d3265cca1239099f342ffaca3d28fde3d735fdc2 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -1270,6 +1270,9 @@ extern int has_pack_index(const unsigned char *sha1);
 
 extern void assert_sha1_type(const unsigned char *sha1, enum object_type expect);
 
+/* Helper to check and "touch" a file */
+extern int check_and_freshen_file(const char *fn, int freshen);
+
 extern const signed char hexval_table[256];
 static inline unsigned int hexval(unsigned char c)
 {
@@ -1956,6 +1959,11 @@ extern int git_config_get_bool_or_int(const char *key, int *is_bool, int *dest);
 extern int git_config_get_maybe_bool(const char *key, int *dest);
 extern int git_config_get_pathname(const char *key, const char **dest);
 extern int git_config_get_untracked_cache(void);
+extern int git_config_get_split_index(void);
+extern int git_config_get_max_percent_split_change(void);
+
+/* This dies if the configured or default date is in the future */
+extern int git_config_get_expiry(const char *key, const char **output);
 
 /*
  * This is a hack for test programs like test-dump-untracked-cache to
index 0e9e1ebefc0a6771de7bf20615e41176ed986179..bd286b3e3610b23e289bd1aa55e4cc2e79ec6590 100644 (file)
--- a/config.c
+++ b/config.c
@@ -1803,6 +1803,19 @@ int git_config_get_pathname(const char *key, const char **dest)
        return ret;
 }
 
+int git_config_get_expiry(const char *key, const char **output)
+{
+       int ret = git_config_get_string_const(key, output);
+       if (ret)
+               return ret;
+       if (strcmp(*output, "now")) {
+               unsigned long now = approxidate("now");
+               if (approxidate(*output) >= now)
+                       git_die_config(key, _("Invalid %s: '%s'"), key, *output);
+       }
+       return ret;
+}
+
 int git_config_get_untracked_cache(void)
 {
        int val = -1;
@@ -1819,14 +1832,39 @@ int git_config_get_untracked_cache(void)
                if (!strcasecmp(v, "keep"))
                        return -1;
 
-               error("unknown core.untrackedCache value '%s'; "
-                     "using 'keep' default value", v);
+               error(_("unknown core.untrackedCache value '%s'; "
+                       "using 'keep' default value"), v);
                return -1;
        }
 
        return -1; /* default value */
 }
 
+int git_config_get_split_index(void)
+{
+       int val;
+
+       if (!git_config_get_maybe_bool("core.splitindex", &val))
+               return val;
+
+       return -1; /* default value */
+}
+
+int git_config_get_max_percent_split_change(void)
+{
+       int val = -1;
+
+       if (!git_config_get_int("splitindex.maxpercentchange", &val)) {
+               if (0 <= val && val <= 100)
+                       return val;
+
+               return error(_("splitIndex.maxPercentChange value '%d' "
+                              "should be between 0 and 100"), val);
+       }
+
+       return -1; /* default value */
+}
+
 NORETURN
 void git_die_config_linenr(const char *key, const char *filename, int linenr)
 {
index 9054369dd0c6ddfd35794fc63f0e0296ec0c9c91..e44775182392c5672fd6ec84eb954db339e5a1ea 100644 (file)
@@ -1558,10 +1558,27 @@ static void tweak_untracked_cache(struct index_state *istate)
        }
 }
 
+static void tweak_split_index(struct index_state *istate)
+{
+       switch (git_config_get_split_index()) {
+       case -1: /* unset: do nothing */
+               break;
+       case 0: /* false */
+               remove_split_index(istate);
+               break;
+       case 1: /* true */
+               add_split_index(istate);
+               break;
+       default: /* unknown value: do nothing */
+               break;
+       }
+}
+
 static void post_read_index_from(struct index_state *istate)
 {
        check_ce_order(istate);
        tweak_untracked_cache(istate);
+       tweak_split_index(istate);
 }
 
 /* remember to discard_cache() before reading a different cache! */
@@ -1657,10 +1674,25 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist)
        die("index file corrupt");
 }
 
+/*
+ * Signal that the shared index is used by updating its mtime.
+ *
+ * This way, shared index can be removed if they have not been used
+ * for some time.
+ */
+static void freshen_shared_index(char *base_sha1_hex, int warn)
+{
+       const char *shared_index = git_path("sharedindex.%s", base_sha1_hex);
+       if (!check_and_freshen_file(shared_index, 1) && warn)
+               warning("could not freshen shared index '%s'", shared_index);
+}
+
 int read_index_from(struct index_state *istate, const char *path)
 {
        struct split_index *split_index;
        int ret;
+       char *base_sha1_hex;
+       const char *base_path;
 
        /* istate->initialized covers both .git/index and .git/sharedindex.xxx */
        if (istate->initialized)
@@ -1678,15 +1710,16 @@ int read_index_from(struct index_state *istate, const char *path)
                discard_index(split_index->base);
        else
                split_index->base = xcalloc(1, sizeof(*split_index->base));
-       ret = do_read_index(split_index->base,
-                           git_path("sharedindex.%s",
-                                    sha1_to_hex(split_index->base_sha1)), 1);
+
+       base_sha1_hex = sha1_to_hex(split_index->base_sha1);
+       base_path = git_path("sharedindex.%s", base_sha1_hex);
+       ret = do_read_index(split_index->base, base_path, 1);
        if (hashcmp(split_index->base_sha1, split_index->base->sha1))
                die("broken index, expect %s in %s, got %s",
-                   sha1_to_hex(split_index->base_sha1),
-                   git_path("sharedindex.%s",
-                            sha1_to_hex(split_index->base_sha1)),
+                   base_sha1_hex, base_path,
                    sha1_to_hex(split_index->base->sha1));
+
+       freshen_shared_index(base_sha1_hex, 0);
        merge_base_index(istate);
        post_read_index_from(istate);
        return ret;
@@ -2169,6 +2202,65 @@ static int write_split_index(struct index_state *istate,
        return ret;
 }
 
+static const char *shared_index_expire = "2.weeks.ago";
+
+static unsigned long get_shared_index_expire_date(void)
+{
+       static unsigned long shared_index_expire_date;
+       static int shared_index_expire_date_prepared;
+
+       if (!shared_index_expire_date_prepared) {
+               git_config_get_expiry("splitindex.sharedindexexpire",
+                                     &shared_index_expire);
+               shared_index_expire_date = approxidate(shared_index_expire);
+               shared_index_expire_date_prepared = 1;
+       }
+
+       return shared_index_expire_date;
+}
+
+static int should_delete_shared_index(const char *shared_index_path)
+{
+       struct stat st;
+       unsigned long expiration;
+
+       /* Check timestamp */
+       expiration = get_shared_index_expire_date();
+       if (!expiration)
+               return 0;
+       if (stat(shared_index_path, &st))
+               return error_errno(_("could not stat '%s"), shared_index_path);
+       if (st.st_mtime > expiration)
+               return 0;
+
+       return 1;
+}
+
+static int clean_shared_index_files(const char *current_hex)
+{
+       struct dirent *de;
+       DIR *dir = opendir(get_git_dir());
+
+       if (!dir)
+               return error_errno(_("unable to open git dir: %s"), get_git_dir());
+
+       while ((de = readdir(dir)) != NULL) {
+               const char *sha1_hex;
+               const char *shared_index_path;
+               if (!skip_prefix(de->d_name, "sharedindex.", &sha1_hex))
+                       continue;
+               if (!strcmp(sha1_hex, current_hex))
+                       continue;
+               shared_index_path = git_path("%s", de->d_name);
+               if (should_delete_shared_index(shared_index_path) > 0 &&
+                   unlink(shared_index_path))
+                       warning_errno(_("unable to unlink: %s"), shared_index_path);
+       }
+       closedir(dir);
+
+       return 0;
+}
+
 static struct tempfile temporary_sharedindex;
 
 static int write_shared_index(struct index_state *istate,
@@ -2190,14 +2282,48 @@ static int write_shared_index(struct index_state *istate,
        }
        ret = rename_tempfile(&temporary_sharedindex,
                              git_path("sharedindex.%s", sha1_to_hex(si->base->sha1)));
-       if (!ret)
+       if (!ret) {
                hashcpy(si->base_sha1, si->base->sha1);
+               clean_shared_index_files(sha1_to_hex(si->base->sha1));
+       }
+
        return ret;
 }
 
+static const int default_max_percent_split_change = 20;
+
+static int too_many_not_shared_entries(struct index_state *istate)
+{
+       int i, not_shared = 0;
+       int max_split = git_config_get_max_percent_split_change();
+
+       switch (max_split) {
+       case -1:
+               /* not or badly configured: use the default value */
+               max_split = default_max_percent_split_change;
+               break;
+       case 0:
+               return 1; /* 0% means always write a new shared index */
+       case 100:
+               return 0; /* 100% means never write a new shared index */
+       default:
+               break; /* just use the configured value */
+       }
+
+       /* Count not shared entries */
+       for (i = 0; i < istate->cache_nr; i++) {
+               struct cache_entry *ce = istate->cache[i];
+               if (!ce->index)
+                       not_shared++;
+       }
+
+       return (int64_t)istate->cache_nr * max_split < (int64_t)not_shared * 100;
+}
+
 int write_locked_index(struct index_state *istate, struct lock_file *lock,
                       unsigned flags)
 {
+       int new_shared_index, ret;
        struct split_index *si = istate->split_index;
 
        if (!si || alternate_index_output ||
@@ -2212,13 +2338,24 @@ int write_locked_index(struct index_state *istate, struct lock_file *lock,
                if ((v & 15) < 6)
                        istate->cache_changed |= SPLIT_INDEX_ORDERED;
        }
-       if (istate->cache_changed & SPLIT_INDEX_ORDERED) {
-               int ret = write_shared_index(istate, lock, flags);
+       if (too_many_not_shared_entries(istate))
+               istate->cache_changed |= SPLIT_INDEX_ORDERED;
+
+       new_shared_index = istate->cache_changed & SPLIT_INDEX_ORDERED;
+
+       if (new_shared_index) {
+               ret = write_shared_index(istate, lock, flags);
                if (ret)
                        return ret;
        }
 
-       return write_split_index(istate, lock, flags);
+       ret = write_split_index(istate, lock, flags);
+
+       /* Freshen the shared index only if the split-index was written */
+       if (!ret && !new_shared_index)
+               freshen_shared_index(sha1_to_hex(si->base_sha1), 1);
+
+       return ret;
 }
 
 /*
index 2ee3c617a6b45bc0dd52b634bbecec4d011a74bf..f1c83a92487818e40af3b0abd032d448d1838760 100644 (file)
@@ -667,7 +667,7 @@ static int freshen_file(const char *fn)
  * either does not exist on disk, or has a stale mtime and may be subject to
  * pruning).
  */
-static int check_and_freshen_file(const char *fn, int freshen)
+int check_and_freshen_file(const char *fn, int freshen)
 {
        if (access(fn, F_OK))
                return 0;
index 615f4cac05f7d381b3c31831b6fd9e8a72e1f982..f519e60f87578386ec9b4ea764bf530be0f64836 100644 (file)
@@ -317,3 +317,25 @@ void replace_index_entry_in_base(struct index_state *istate,
                istate->split_index->base->cache[new->index - 1] = new;
        }
 }
+
+void add_split_index(struct index_state *istate)
+{
+       if (!istate->split_index) {
+               init_split_index(istate);
+               istate->cache_changed |= SPLIT_INDEX_ORDERED;
+       }
+}
+
+void remove_split_index(struct index_state *istate)
+{
+       if (istate->split_index) {
+               /*
+                * can't discard_split_index(&the_index); because that
+                * will destroy split_index->base->cache[], which may
+                * be shared with the_index.cache[]. So yeah we're
+                * leaking a bit here.
+                */
+               istate->split_index = NULL;
+               istate->cache_changed |= SOMETHING_CHANGED;
+       }
+}
index c1324f521a87c427c531464c5e6424fe38d7d35b..df91c1bda8117fe7d0f25a0aaedce79370801ce0 100644 (file)
@@ -31,5 +31,7 @@ void merge_base_index(struct index_state *istate);
 void prepare_to_write_split_index(struct index_state *istate);
 void finish_writing_split_index(struct index_state *istate);
 void discard_split_index(struct index_state *istate);
+void add_split_index(struct index_state *istate);
+void remove_split_index(struct index_state *istate);
 
 #endif
index 6096f2c630947241a2dee410255aeebb818b14f9..af3ec0da5ac0f5e0f6e19616d9c55b611ba5a701 100755 (executable)
@@ -8,6 +8,7 @@ test_description='split index mode tests'
 sane_unset GIT_TEST_SPLIT_INDEX
 
 test_expect_success 'enable split index' '
+       git config splitIndex.maxPercentChange 100 &&
        git update-index --split-index &&
        test-dump-split-index .git/index >actual &&
        indexversion=$(test-index-version <.git/index) &&
@@ -19,12 +20,12 @@ test_expect_success 'enable split index' '
                own=8299b0bcd1ac364e5f1d7768efb62fa2da79a339
                base=39d890139ee5356c7ef572216cebcd27aa41f9df
        fi &&
-       cat >expect <<EOF &&
-own $own
-base $base
-replacements:
-deletions:
-EOF
+       cat >expect <<-EOF &&
+       own $own
+       base $base
+       replacements:
+       deletions:
+       EOF
        test_cmp expect actual
 '
 
@@ -32,51 +33,51 @@ test_expect_success 'add one file' '
        : >one &&
        git update-index --add one &&
        git ls-files --stage >ls-files.actual &&
-       cat >ls-files.expect <<EOF &&
-100644 $EMPTY_BLOB 0   one
-EOF
+       cat >ls-files.expect <<-EOF &&
+       100644 $EMPTY_BLOB 0    one
+       EOF
        test_cmp ls-files.expect ls-files.actual &&
 
        test-dump-split-index .git/index | sed "/^own/d" >actual &&
-       cat >expect <<EOF &&
-base $base
-100644 $EMPTY_BLOB 0   one
-replacements:
-deletions:
-EOF
+       cat >expect <<-EOF &&
+       base $base
+       100644 $EMPTY_BLOB 0    one
+       replacements:
+       deletions:
+       EOF
        test_cmp expect actual
 '
 
 test_expect_success 'disable split index' '
        git update-index --no-split-index &&
        git ls-files --stage >ls-files.actual &&
-       cat >ls-files.expect <<EOF &&
-100644 $EMPTY_BLOB 0   one
-EOF
+       cat >ls-files.expect <<-EOF &&
+       100644 $EMPTY_BLOB 0    one
+       EOF
        test_cmp ls-files.expect ls-files.actual &&
 
        BASE=$(test-dump-split-index .git/index | grep "^own" | sed "s/own/base/") &&
        test-dump-split-index .git/index | sed "/^own/d" >actual &&
-       cat >expect <<EOF &&
-not a split index
-EOF
+       cat >expect <<-EOF &&
+       not a split index
+       EOF
        test_cmp expect actual
 '
 
 test_expect_success 'enable split index again, "one" now belongs to base index"' '
        git update-index --split-index &&
        git ls-files --stage >ls-files.actual &&
-       cat >ls-files.expect <<EOF &&
-100644 $EMPTY_BLOB 0   one
-EOF
+       cat >ls-files.expect <<-EOF &&
+       100644 $EMPTY_BLOB 0    one
+       EOF
        test_cmp ls-files.expect ls-files.actual &&
 
        test-dump-split-index .git/index | sed "/^own/d" >actual &&
-       cat >expect <<EOF &&
-$BASE
-replacements:
-deletions:
-EOF
+       cat >expect <<-EOF &&
+       $BASE
+       replacements:
+       deletions:
+       EOF
        test_cmp expect actual
 '
 
@@ -84,18 +85,18 @@ test_expect_success 'modify original file, base index untouched' '
        echo modified >one &&
        git update-index one &&
        git ls-files --stage >ls-files.actual &&
-       cat >ls-files.expect <<EOF &&
-100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0      one
-EOF
+       cat >ls-files.expect <<-EOF &&
+       100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0       one
+       EOF
        test_cmp ls-files.expect ls-files.actual &&
 
        test-dump-split-index .git/index | sed "/^own/d" >actual &&
-       q_to_tab >expect <<EOF &&
-$BASE
-100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0Q
-replacements: 0
-deletions:
-EOF
+       q_to_tab >expect <<-EOF &&
+       $BASE
+       100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0Q
+       replacements: 0
+       deletions:
+       EOF
        test_cmp expect actual
 '
 
@@ -103,54 +104,54 @@ test_expect_success 'add another file, which stays index' '
        : >two &&
        git update-index --add two &&
        git ls-files --stage >ls-files.actual &&
-       cat >ls-files.expect <<EOF &&
-100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0      one
-100644 $EMPTY_BLOB 0   two
-EOF
+       cat >ls-files.expect <<-EOF &&
+       100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0       one
+       100644 $EMPTY_BLOB 0    two
+       EOF
        test_cmp ls-files.expect ls-files.actual &&
 
        test-dump-split-index .git/index | sed "/^own/d" >actual &&
-       q_to_tab >expect <<EOF &&
-$BASE
-100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0Q
-100644 $EMPTY_BLOB 0   two
-replacements: 0
-deletions:
-EOF
+       q_to_tab >expect <<-EOF &&
+       $BASE
+       100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0Q
+       100644 $EMPTY_BLOB 0    two
+       replacements: 0
+       deletions:
+       EOF
        test_cmp expect actual
 '
 
 test_expect_success 'remove file not in base index' '
        git update-index --force-remove two &&
        git ls-files --stage >ls-files.actual &&
-       cat >ls-files.expect <<EOF &&
-100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0      one
-EOF
+       cat >ls-files.expect <<-EOF &&
+       100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0       one
+       EOF
        test_cmp ls-files.expect ls-files.actual &&
 
        test-dump-split-index .git/index | sed "/^own/d" >actual &&
-       q_to_tab >expect <<EOF &&
-$BASE
-100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0Q
-replacements: 0
-deletions:
-EOF
+       q_to_tab >expect <<-EOF &&
+       $BASE
+       100644 2e0996000b7e9019eabcad29391bf0f5c7702f0b 0Q
+       replacements: 0
+       deletions:
+       EOF
        test_cmp expect actual
 '
 
 test_expect_success 'remove file in base index' '
        git update-index --force-remove one &&
        git ls-files --stage >ls-files.actual &&
-       cat >ls-files.expect <<EOF &&
-EOF
+       cat >ls-files.expect <<-EOF &&
+       EOF
        test_cmp ls-files.expect ls-files.actual &&
 
        test-dump-split-index .git/index | sed "/^own/d" >actual &&
-       cat >expect <<EOF &&
-$BASE
-replacements:
-deletions: 0
-EOF
+       cat >expect <<-EOF &&
+       $BASE
+       replacements:
+       deletions: 0
+       EOF
        test_cmp expect actual
 '
 
@@ -158,18 +159,18 @@ test_expect_success 'add original file back' '
        : >one &&
        git update-index --add one &&
        git ls-files --stage >ls-files.actual &&
-       cat >ls-files.expect <<EOF &&
-100644 $EMPTY_BLOB 0   one
-EOF
+       cat >ls-files.expect <<-EOF &&
+       100644 $EMPTY_BLOB 0    one
+       EOF
        test_cmp ls-files.expect ls-files.actual &&
 
        test-dump-split-index .git/index | sed "/^own/d" >actual &&
-       cat >expect <<EOF &&
-$BASE
-100644 $EMPTY_BLOB 0   one
-replacements:
-deletions: 0
-EOF
+       cat >expect <<-EOF &&
+       $BASE
+       100644 $EMPTY_BLOB 0    one
+       replacements:
+       deletions: 0
+       EOF
        test_cmp expect actual
 '
 
@@ -177,26 +178,26 @@ test_expect_success 'add new file' '
        : >two &&
        git update-index --add two &&
        git ls-files --stage >actual &&
-       cat >expect <<EOF &&
-100644 $EMPTY_BLOB 0   one
-100644 $EMPTY_BLOB 0   two
-EOF
+       cat >expect <<-EOF &&
+       100644 $EMPTY_BLOB 0    one
+       100644 $EMPTY_BLOB 0    two
+       EOF
        test_cmp expect actual
 '
 
 test_expect_success 'unify index, two files remain' '
        git update-index --no-split-index &&
        git ls-files --stage >ls-files.actual &&
-       cat >ls-files.expect <<EOF &&
-100644 $EMPTY_BLOB 0   one
-100644 $EMPTY_BLOB 0   two
-EOF
+       cat >ls-files.expect <<-EOF &&
+       100644 $EMPTY_BLOB 0    one
+       100644 $EMPTY_BLOB 0    two
+       EOF
        test_cmp ls-files.expect ls-files.actual &&
 
        test-dump-split-index .git/index | sed "/^own/d" >actual &&
-       cat >expect <<EOF &&
-not a split index
-EOF
+       cat >expect <<-EOF &&
+       not a split index
+       EOF
        test_cmp expect actual
 '
 
@@ -216,4 +217,157 @@ test_expect_success 'rev-parse --shared-index-path' '
        )
 '
 
+test_expect_success 'set core.splitIndex config variable to true' '
+       git config core.splitIndex true &&
+       : >three &&
+       git update-index --add three &&
+       git ls-files --stage >ls-files.actual &&
+       cat >ls-files.expect <<-EOF &&
+       100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       one
+       100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       three
+       100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       two
+       EOF
+       test_cmp ls-files.expect ls-files.actual &&
+       BASE=$(test-dump-split-index .git/index | grep "^base") &&
+       test-dump-split-index .git/index | sed "/^own/d" >actual &&
+       cat >expect <<-EOF &&
+       $BASE
+       replacements:
+       deletions:
+       EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'set core.splitIndex config variable to false' '
+       git config core.splitIndex false &&
+       git update-index --force-remove three &&
+       git ls-files --stage >ls-files.actual &&
+       cat >ls-files.expect <<-EOF &&
+       100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       one
+       100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       two
+       EOF
+       test_cmp ls-files.expect ls-files.actual &&
+       test-dump-split-index .git/index | sed "/^own/d" >actual &&
+       cat >expect <<-EOF &&
+       not a split index
+       EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'set core.splitIndex config variable to true' '
+       git config core.splitIndex true &&
+       : >three &&
+       git update-index --add three &&
+       BASE=$(test-dump-split-index .git/index | grep "^base") &&
+       test-dump-split-index .git/index | sed "/^own/d" >actual &&
+       cat >expect <<-EOF &&
+       $BASE
+       replacements:
+       deletions:
+       EOF
+       test_cmp expect actual &&
+       : >four &&
+       git update-index --add four &&
+       test-dump-split-index .git/index | sed "/^own/d" >actual &&
+       cat >expect <<-EOF &&
+       $BASE
+       100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       four
+       replacements:
+       deletions:
+       EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'check behavior with splitIndex.maxPercentChange unset' '
+       git config --unset splitIndex.maxPercentChange &&
+       : >five &&
+       git update-index --add five &&
+       BASE=$(test-dump-split-index .git/index | grep "^base") &&
+       test-dump-split-index .git/index | sed "/^own/d" >actual &&
+       cat >expect <<-EOF &&
+       $BASE
+       replacements:
+       deletions:
+       EOF
+       test_cmp expect actual &&
+       : >six &&
+       git update-index --add six &&
+       test-dump-split-index .git/index | sed "/^own/d" >actual &&
+       cat >expect <<-EOF &&
+       $BASE
+       100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       six
+       replacements:
+       deletions:
+       EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'check splitIndex.maxPercentChange set to 0' '
+       git config splitIndex.maxPercentChange 0 &&
+       : >seven &&
+       git update-index --add seven &&
+       BASE=$(test-dump-split-index .git/index | grep "^base") &&
+       test-dump-split-index .git/index | sed "/^own/d" >actual &&
+       cat >expect <<-EOF &&
+       $BASE
+       replacements:
+       deletions:
+       EOF
+       test_cmp expect actual &&
+       : >eight &&
+       git update-index --add eight &&
+       BASE=$(test-dump-split-index .git/index | grep "^base") &&
+       test-dump-split-index .git/index | sed "/^own/d" >actual &&
+       cat >expect <<-EOF &&
+       $BASE
+       replacements:
+       deletions:
+       EOF
+       test_cmp expect actual
+'
+
+test_expect_success 'shared index files expire after 2 weeks by default' '
+       : >ten &&
+       git update-index --add ten &&
+       test $(ls .git/sharedindex.* | wc -l) -gt 2 &&
+       just_under_2_weeks_ago=$((5-14*86400)) &&
+       test-chmtime =$just_under_2_weeks_ago .git/sharedindex.* &&
+       : >eleven &&
+       git update-index --add eleven &&
+       test $(ls .git/sharedindex.* | wc -l) -gt 2 &&
+       just_over_2_weeks_ago=$((-1-14*86400)) &&
+       test-chmtime =$just_over_2_weeks_ago .git/sharedindex.* &&
+       : >twelve &&
+       git update-index --add twelve &&
+       test $(ls .git/sharedindex.* | wc -l) -le 2
+'
+
+test_expect_success 'check splitIndex.sharedIndexExpire set to 16 days' '
+       git config splitIndex.sharedIndexExpire "16.days.ago" &&
+       test-chmtime =$just_over_2_weeks_ago .git/sharedindex.* &&
+       : >thirteen &&
+       git update-index --add thirteen &&
+       test $(ls .git/sharedindex.* | wc -l) -gt 2 &&
+       just_over_16_days_ago=$((-1-16*86400)) &&
+       test-chmtime =$just_over_16_days_ago .git/sharedindex.* &&
+       : >fourteen &&
+       git update-index --add fourteen &&
+       test $(ls .git/sharedindex.* | wc -l) -le 2
+'
+
+test_expect_success 'check splitIndex.sharedIndexExpire set to "never" and "now"' '
+       git config splitIndex.sharedIndexExpire never &&
+       just_10_years_ago=$((-365*10*86400)) &&
+       test-chmtime =$just_10_years_ago .git/sharedindex.* &&
+       : >fifteen &&
+       git update-index --add fifteen &&
+       test $(ls .git/sharedindex.* | wc -l) -gt 2 &&
+       git config splitIndex.sharedIndexExpire now &&
+       just_1_second_ago=-1 &&
+       test-chmtime =$just_1_second_ago .git/sharedindex.* &&
+       : >sixteen &&
+       git update-index --add sixteen &&
+       test $(ls .git/sharedindex.* | wc -l) -le 2
+'
+
 test_done