Merge branch 'nd/conditional-config-include'
authorJunio C Hamano <gitster@pobox.com>
Tue, 21 Mar 2017 22:07:18 +0000 (15:07 -0700)
committerJunio C Hamano <gitster@pobox.com>
Tue, 21 Mar 2017 22:07:18 +0000 (15:07 -0700)
The configuration file learned a new "includeIf.<condition>.path"
that includes the contents of the given path only when the
condition holds. This allows you to say "include this work-related
bit only in the repositories under my ~/work/ directory".

* nd/conditional-config-include:
config: add conditional include
config.txt: reflow the second include.path paragraph
config.txt: clarify multiple key values in include.path

1  2 
Documentation/config.txt
config.c
diff --combined Documentation/config.txt
index eccc012672925a1d7967d1e2349152a0b240e85a,5faabc7934446f4deaecc0e7c58983a6e6bb7919..cf281f64f1f2d5910b605bae95e881b8db29a8ac
@@@ -79,18 -79,69 +79,69 @@@ escape sequences) are invalid
  Includes
  ~~~~~~~~
  
- You can include one config file from another by setting the special
+ You can include a config file from another by setting the special
  `include.path` variable to the name of the file to be included. The
  variable takes a pathname as its value, and is subject to tilde
- expansion.
+ expansion. `include.path` can be given multiple times.
  
- The
- included file is expanded immediately, as if its contents had been
+ The included file is expanded immediately, as if its contents had been
  found at the location of the include directive. If the value of the
- `include.path` variable is a relative path, the path is considered to be
- relative to the configuration file in which the include directive was
- found.  See below for examples.
+ `include.path` variable is a relative path, the path is considered to
+ be relative to the configuration file in which the include directive
was found.  See below for examples.
  
+ Conditional includes
+ ~~~~~~~~~~~~~~~~~~~~
+ You can include a config file from another conditionally by setting a
+ `includeIf.<condition>.path` variable to the name of the file to be
+ included. The variable's value is treated the same way as
+ `include.path`. `includeIf.<condition>.path` can be given multiple times.
+ The condition starts with a keyword followed by a colon and some data
+ whose format and meaning depends on the keyword. Supported keywords
+ are:
+ `gitdir`::
+       The data that follows the keyword `gitdir:` is used as a glob
+       pattern. If the location of the .git directory matches the
+       pattern, the include condition is met.
+ +
+ The .git location may be auto-discovered, or come from `$GIT_DIR`
+ environment variable. If the repository is auto discovered via a .git
+ file (e.g. from submodules, or a linked worktree), the .git location
+ would be the final location where the .git directory is, not where the
+ .git file is.
+ +
+ The pattern can contain standard globbing wildcards and two additional
+ ones, `**/` and `/**`, that can match multiple path components. Please
+ refer to linkgit:gitignore[5] for details. For convenience:
+  * If the pattern starts with `~/`, `~` will be substituted with the
+    content of the environment variable `HOME`.
+  * If the pattern starts with `./`, it is replaced with the directory
+    containing the current config file.
+  * If the pattern does not start with either `~/`, `./` or `/`, `**/`
+    will be automatically prepended. For example, the pattern `foo/bar`
+    becomes `**/foo/bar` and would match `/any/path/to/foo/bar`.
+  * If the pattern ends with `/`, `**` will be automatically added. For
+    example, the pattern `foo/` becomes `foo/**`. In other words, it
+    matches "foo" and everything inside, recursively.
+ `gitdir/i`::
+       This is the same as `gitdir` except that matching is done
+       case-insensitively (e.g. on case-insensitive file sytems)
+ A few more notes on matching via `gitdir` and `gitdir/i`:
+  * Symlinks in `$GIT_DIR` are not resolved before matching.
+  * Note that "../" is not special and will match literally, which is
+    unlikely what you want.
  
  Example
  ~~~~~~~
                path = foo ; expand "foo" relative to the current file
                path = ~/foo ; expand "foo" in your `$HOME` directory
  
+       ; include if $GIT_DIR is /path/to/foo/.git
+       [includeIf "gitdir:/path/to/foo/.git"]
+               path = /path/to/foo.inc
+       ; include for all repositories inside /path/to/group
+       [includeIf "gitdir:/path/to/group/"]
+               path = /path/to/foo.inc
+       ; include for all repositories inside $HOME/to/group
+       [includeIf "gitdir:~/to/group/"]
+               path = /path/to/foo.inc
  
  Values
  ~~~~~~
@@@ -334,10 -396,6 +396,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
@@@ -354,19 -412,16 +416,19 @@@ core.checkStat:
        all fields, including the sub-second part of mtime and ctime.
  
  core.quotePath::
 -      The commands that output paths (e.g. 'ls-files',
 -      'diff'), when not given the `-z` option, will quote
 -      "unusual" characters in the pathname by enclosing the
 -      pathname in a double-quote pair and with backslashes the
 -      same way strings in C source code are quoted.  If this
 -      variable is set to false, the bytes higher than 0x80 are
 -      not quoted but output as verbatim.  Note that double
 -      quote, backslash and control characters are always
 -      quoted without `-z` regardless of the setting of this
 -      variable.
 +      Commands that output paths (e.g. 'ls-files', 'diff'), will
 +      quote "unusual" characters in the pathname by enclosing the
 +      pathname in double-quotes and escaping those characters with
 +      backslashes in the same way C escapes control characters (e.g.
 +      `\t` for TAB, `\n` for LF, `\\` for backslash) or bytes with
 +      values larger than 0x80 (e.g. octal `\302\265` for "micro" in
 +      UTF-8).  If this variable is set to false, bytes higher than
 +      0x80 are not considered "unusual" any more. Double-quotes,
 +      backslash and control characters are always escaped regardless
 +      of the setting of this variable.  A simple space character is
 +      not considered "unusual".  Many commands can output pathnames
 +      completely verbatim using the `-z` option. The default value
 +      is true.
  
  core.eol::
        Sets the line ending type to use in the working directory for
@@@ -1409,12 -1464,6 +1471,12 @@@ gc.autoDetach:
        Make `git gc --auto` return immediately and run in background
        if the system supports it. Default is true.
  
 +gc.logExpiry::
 +      If the file gc.log exists, then `git gc --auto` won't run
 +      unless that file is more than 'gc.logExpiry' old.  Default is
 +      "1.day".  See `gc.pruneExpire` for more ways to specify its
 +      value.
 +
  gc.packRefs::
        Running `git pack-refs` in a repository renders it
        unclonable by Git versions prior to 1.5.1.2 over dumb
@@@ -1932,10 -1981,7 +1994,10 @@@ http.<url>.*:
    must match exactly between the config key and the URL.
  
  . Host/domain name (e.g., `example.com` in `https://example.com/`).
 -  This field must match exactly between the config key and the URL.
 +  This field must match between the config key and the URL. It is
 +  possible to specify a `*` as part of the host name to match all subdomains
 +  at this level. `https://*.example.com/` for example would match
 +  `https://foo.example.com/`, but not `https://foo.bar.example.com/`.
  
  . Port number (e.g., `8080` in `http://example.com:8080/`).
    This field must match exactly between the config key and the URL.
@@@ -1970,17 -2016,6 +2032,17 @@@ Environment variable settings always ov
  matched against are those given directly to Git commands.  This means any URLs
  visited as a result of a redirection do not participate in matching.
  
 +ssh.variant::
 +      Depending on the value of the environment variables `GIT_SSH` or
 +      `GIT_SSH_COMMAND`, or the config setting `core.sshCommand`, Git
 +      auto-detects whether to adjust its command-line parameters for use
 +      with plink or tortoiseplink, as opposed to the default (OpenSSH).
 ++
 +The config variable `ssh.variant` can be set to override this auto-detection;
 +valid values are `ssh`, `plink`, `putty` or `tortoiseplink`. Any other value
 +will be treated as normal ssh. This setting can be overridden via the
 +environment variable `GIT_SSH_VARIANT`.
 +
  i18n.commitEncoding::
        Character encoding the commit messages are stored in; Git itself
        does not care per se, but this information is necessary e.g. when
@@@ -2854,31 -2889,6 +2916,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
diff --combined config.c
index eb7e310a11e453f68c366d0212b115de43f33433,0dac0f4cb2b69ce0163f34c685cfc94c734fd239..1a4d85537b3c96749999250a34216257db134714
+++ b/config.c
@@@ -13,6 -13,7 +13,7 @@@
  #include "hashmap.h"
  #include "string-list.h"
  #include "utf8.h"
+ #include "dir.h"
  
  struct config_source {
        struct config_source *prev;
@@@ -170,9 -171,94 +171,94 @@@ static int handle_path_include(const ch
        return ret;
  }
  
+ static int prepare_include_condition_pattern(struct strbuf *pat)
+ {
+       struct strbuf path = STRBUF_INIT;
+       char *expanded;
+       int prefix = 0;
+       expanded = expand_user_path(pat->buf);
+       if (expanded) {
+               strbuf_reset(pat);
+               strbuf_addstr(pat, expanded);
+               free(expanded);
+       }
+       if (pat->buf[0] == '.' && is_dir_sep(pat->buf[1])) {
+               const char *slash;
+               if (!cf || !cf->path)
+                       return error(_("relative config include "
+                                      "conditionals must come from files"));
+               strbuf_add_absolute_path(&path, cf->path);
+               slash = find_last_dir_sep(path.buf);
+               if (!slash)
+                       die("BUG: how is this possible?");
+               strbuf_splice(pat, 0, 1, path.buf, slash - path.buf);
+               prefix = slash - path.buf + 1 /* slash */;
+       } else if (!is_absolute_path(pat->buf))
+               strbuf_insert(pat, 0, "**/", 3);
+       if (pat->len && is_dir_sep(pat->buf[pat->len - 1]))
+               strbuf_addstr(pat, "**");
+       strbuf_release(&path);
+       return prefix;
+ }
+ static int include_by_gitdir(const char *cond, size_t cond_len, int icase)
+ {
+       struct strbuf text = STRBUF_INIT;
+       struct strbuf pattern = STRBUF_INIT;
+       int ret = 0, prefix;
+       strbuf_add_absolute_path(&text, get_git_dir());
+       strbuf_add(&pattern, cond, cond_len);
+       prefix = prepare_include_condition_pattern(&pattern);
+       if (prefix < 0)
+               goto done;
+       if (prefix > 0) {
+               /*
+                * perform literal matching on the prefix part so that
+                * any wildcard character in it can't create side effects.
+                */
+               if (text.len < prefix)
+                       goto done;
+               if (!icase && strncmp(pattern.buf, text.buf, prefix))
+                       goto done;
+               if (icase && strncasecmp(pattern.buf, text.buf, prefix))
+                       goto done;
+       }
+       ret = !wildmatch(pattern.buf + prefix, text.buf + prefix,
+                        icase ? WM_CASEFOLD : 0, NULL);
+ done:
+       strbuf_release(&pattern);
+       strbuf_release(&text);
+       return ret;
+ }
+ static int include_condition_is_true(const char *cond, size_t cond_len)
+ {
+       if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
+               return include_by_gitdir(cond, cond_len, 0);
+       else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
+               return include_by_gitdir(cond, cond_len, 1);
+       /* unknown conditionals are always false */
+       return 0;
+ }
  int git_config_include(const char *var, const char *value, void *data)
  {
        struct config_include_data *inc = data;
+       const char *cond, *key;
+       int cond_len;
        int ret;
  
        /*
  
        if (!strcmp(var, "include.path"))
                ret = handle_path_include(value, inc);
+       if (!parse_config_key(var, "includeif", &cond, &cond_len, &key) &&
+           (cond && include_condition_is_true(cond, cond_len)) &&
+           !strcmp(key, "path"))
+               ret = handle_path_include(value, inc);
        return ret;
  }
  
@@@ -201,105 -293,11 +293,105 @@@ void git_config_push_parameter(const ch
        strbuf_release(&env);
  }
  
 +static inline int iskeychar(int c)
 +{
 +      return isalnum(c) || c == '-';
 +}
 +
 +/*
 + * Auxiliary function to sanity-check and split the key into the section
 + * identifier and variable name.
 + *
 + * Returns 0 on success, -1 when there is an invalid character in the key and
 + * -2 if there is no section name in the key.
 + *
 + * store_key - pointer to char* which will hold a copy of the key with
 + *             lowercase section and variable name
 + * baselen - pointer to int which will hold the length of the
 + *           section + subsection part, can be NULL
 + */
 +static int git_config_parse_key_1(const char *key, char **store_key, int *baselen_, int quiet)
 +{
 +      int i, dot, baselen;
 +      const char *last_dot = strrchr(key, '.');
 +
 +      /*
 +       * Since "key" actually contains the section name and the real
 +       * key name separated by a dot, we have to know where the dot is.
 +       */
 +
 +      if (last_dot == NULL || last_dot == key) {
 +              if (!quiet)
 +                      error("key does not contain a section: %s", key);
 +              return -CONFIG_NO_SECTION_OR_NAME;
 +      }
 +
 +      if (!last_dot[1]) {
 +              if (!quiet)
 +                      error("key does not contain variable name: %s", key);
 +              return -CONFIG_NO_SECTION_OR_NAME;
 +      }
 +
 +      baselen = last_dot - key;
 +      if (baselen_)
 +              *baselen_ = baselen;
 +
 +      /*
 +       * Validate the key and while at it, lower case it for matching.
 +       */
 +      if (store_key)
 +              *store_key = xmallocz(strlen(key));
 +
 +      dot = 0;
 +      for (i = 0; key[i]; i++) {
 +              unsigned char c = key[i];
 +              if (c == '.')
 +                      dot = 1;
 +              /* Leave the extended basename untouched.. */
 +              if (!dot || i > baselen) {
 +                      if (!iskeychar(c) ||
 +                          (i == baselen + 1 && !isalpha(c))) {
 +                              if (!quiet)
 +                                      error("invalid key: %s", key);
 +                              goto out_free_ret_1;
 +                      }
 +                      c = tolower(c);
 +              } else if (c == '\n') {
 +                      if (!quiet)
 +                              error("invalid key (newline): %s", key);
 +                      goto out_free_ret_1;
 +              }
 +              if (store_key)
 +                      (*store_key)[i] = c;
 +      }
 +
 +      return 0;
 +
 +out_free_ret_1:
 +      if (store_key) {
 +              free(*store_key);
 +              *store_key = NULL;
 +      }
 +      return -CONFIG_INVALID_KEY;
 +}
 +
 +int git_config_parse_key(const char *key, char **store_key, int *baselen)
 +{
 +      return git_config_parse_key_1(key, store_key, baselen, 0);
 +}
 +
 +int git_config_key_is_valid(const char *key)
 +{
 +      return !git_config_parse_key_1(key, NULL, NULL, 1);
 +}
 +
  int git_config_parse_parameter(const char *text,
                               config_fn_t fn, void *data)
  {
        const char *value;
 +      char *canonical_name;
        struct strbuf **pair;
 +      int ret;
  
        pair = strbuf_split_str(text, '=', 2);
        if (!pair[0])
                strbuf_list_free(pair);
                return error("bogus config parameter: %s", text);
        }
 -      strbuf_tolower(pair[0]);
 -      if (fn(pair[0]->buf, value, data) < 0) {
 -              strbuf_list_free(pair);
 -              return -1;
 +
 +      if (git_config_parse_key(pair[0]->buf, &canonical_name, NULL)) {
 +              ret = -1;
 +      } else {
 +              ret = (fn(canonical_name, value, data) < 0) ? -1 : 0;
 +              free(canonical_name);
        }
        strbuf_list_free(pair);
 -      return 0;
 +      return ret;
  }
  
  int git_config_from_parameters(config_fn_t fn, void *data)
@@@ -452,6 -448,11 +544,6 @@@ static char *parse_value(void
        }
  }
  
 -static inline int iskeychar(int c)
 -{
 -      return isalnum(c) || c == '-';
 -}
 -
  static int get_value(config_fn_t fn, void *data, struct strbuf *name)
  {
        int c;
@@@ -1503,31 -1504,6 +1595,31 @@@ static void configset_iter(struct confi
        }
  }
  
 +void read_early_config(config_fn_t cb, void *data)
 +{
 +      struct strbuf buf = STRBUF_INIT;
 +
 +      git_config_with_options(cb, data, NULL, 1);
 +
 +      /*
 +       * When setup_git_directory() was not yet asked to discover the
 +       * GIT_DIR, we ask discover_git_directory() to figure out whether there
 +       * is any repository config we should use (but unlike
 +       * setup_git_directory_gently(), no global state is changed, most
 +       * notably, the current working directory is still the same after the
 +       * call).
 +       */
 +      if (!have_git_dir() && discover_git_directory(&buf)) {
 +              struct git_config_source repo_config;
 +
 +              memset(&repo_config, 0, sizeof(repo_config));
 +              strbuf_addstr(&buf, "/config");
 +              repo_config.file = buf.buf;
 +              git_config_with_options(cb, data, &repo_config, 1);
 +      }
 +      strbuf_release(&buf);
 +}
 +
  static void git_config_check_init(void);
  
  void git_config(config_fn_t fn, void *data)
@@@ -1828,19 -1804,6 +1920,19 @@@ int git_config_get_pathname(const char 
        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;
                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)
  {
@@@ -2143,6 -2081,93 +2235,6 @@@ void git_config_set(const char *key, co
        git_config_set_multivar(key, value, NULL, 0);
  }
  
 -/*
 - * Auxiliary function to sanity-check and split the key into the section
 - * identifier and variable name.
 - *
 - * Returns 0 on success, -1 when there is an invalid character in the key and
 - * -2 if there is no section name in the key.
 - *
 - * store_key - pointer to char* which will hold a copy of the key with
 - *             lowercase section and variable name
 - * baselen - pointer to int which will hold the length of the
 - *           section + subsection part, can be NULL
 - */
 -static int git_config_parse_key_1(const char *key, char **store_key, int *baselen_, int quiet)
 -{
 -      int i, dot, baselen;
 -      const char *last_dot = strrchr(key, '.');
 -
 -      /*
 -       * Since "key" actually contains the section name and the real
 -       * key name separated by a dot, we have to know where the dot is.
 -       */
 -
 -      if (last_dot == NULL || last_dot == key) {
 -              if (!quiet)
 -                      error("key does not contain a section: %s", key);
 -              return -CONFIG_NO_SECTION_OR_NAME;
 -      }
 -
 -      if (!last_dot[1]) {
 -              if (!quiet)
 -                      error("key does not contain variable name: %s", key);
 -              return -CONFIG_NO_SECTION_OR_NAME;
 -      }
 -
 -      baselen = last_dot - key;
 -      if (baselen_)
 -              *baselen_ = baselen;
 -
 -      /*
 -       * Validate the key and while at it, lower case it for matching.
 -       */
 -      if (store_key)
 -              *store_key = xmallocz(strlen(key));
 -
 -      dot = 0;
 -      for (i = 0; key[i]; i++) {
 -              unsigned char c = key[i];
 -              if (c == '.')
 -                      dot = 1;
 -              /* Leave the extended basename untouched.. */
 -              if (!dot || i > baselen) {
 -                      if (!iskeychar(c) ||
 -                          (i == baselen + 1 && !isalpha(c))) {
 -                              if (!quiet)
 -                                      error("invalid key: %s", key);
 -                              goto out_free_ret_1;
 -                      }
 -                      c = tolower(c);
 -              } else if (c == '\n') {
 -                      if (!quiet)
 -                              error("invalid key (newline): %s", key);
 -                      goto out_free_ret_1;
 -              }
 -              if (store_key)
 -                      (*store_key)[i] = c;
 -      }
 -
 -      return 0;
 -
 -out_free_ret_1:
 -      if (store_key) {
 -              free(*store_key);
 -              *store_key = NULL;
 -      }
 -      return -CONFIG_INVALID_KEY;
 -}
 -
 -int git_config_parse_key(const char *key, char **store_key, int *baselen)
 -{
 -      return git_config_parse_key_1(key, store_key, baselen, 0);
 -}
 -
 -int git_config_key_is_valid(const char *key)
 -{
 -      return !git_config_parse_key_1(key, NULL, NULL, 1);
 -}
 -
  /*
   * If value==NULL, unset in (remove from) config,
   * if value_regex!=NULL, disregard key/value pairs where value does not match.
@@@ -2603,10 -2628,11 +2695,10 @@@ int parse_config_key(const char *var
                     const char **subsection, int *subsection_len,
                     const char **key)
  {
 -      int section_len = strlen(section);
        const char *dot;
  
        /* Does it start with "section." ? */
 -      if (!starts_with(var, section) || var[section_len] != '.')
 +      if (!skip_prefix(var, section, &var) || *var != '.')
                return -1;
  
        /*
        *key = dot + 1;
  
        /* Did we have a subsection at all? */
 -      if (dot == var + section_len) {
 -              *subsection = NULL;
 -              *subsection_len = 0;
 +      if (dot == var) {
 +              if (subsection) {
 +                      *subsection = NULL;
 +                      *subsection_len = 0;
 +              }
        }
        else {
 -              *subsection = var + section_len + 1;
 +              if (!subsection)
 +                      return -1;
 +              *subsection = var + 1;
                *subsection_len = dot - *subsection;
        }