Merge branch 'jk/command-line-config-empty-string'
authorJunio C Hamano <gitster@pobox.com>
Tue, 9 Sep 2014 19:53:56 +0000 (12:53 -0700)
committerJunio C Hamano <gitster@pobox.com>
Tue, 9 Sep 2014 19:53:57 +0000 (12:53 -0700)
"git -c section.var command" and "git -c section.var= command"
should pass the configuration differently (the former should be
a boolean true, the latter should be an empty string).

* jk/command-line-config-empty-string:
config: teach "git -c" to recognize an empty string

1  2 
Documentation/git.txt
config.c
t/t1300-repo-config.sh
diff --combined Documentation/git.txt
index de7b870a35e63e43652be6468e6e5a17d2708a14,1557cd919d4ecf9133d1cd2cd449ead4b78c7f7f..26de4dd54804ed6d14f9e568547154996b919bd4
@@@ -29,7 -29,7 +29,7 @@@ in-depth introduction
  After you mastered the basic concepts, you can come back to this
  page to learn what commands Git offers.  You can learn more about
  individual Git commands with "git help command".  linkgit:gitcli[7]
 -manual page gives you an overview of the command line command syntax.
 +manual page gives you an overview of the command-line command syntax.
  
  Formatted and hyperlinked version of the latest Git documentation
  can be viewed at `http://git-htmldocs.googlecode.com/git/git.html`.
@@@ -39,33 -39,10 +39,33 @@@ ifdef::stalenotes[
  ============
  
  You are reading the documentation for the latest (possibly
 -unreleased) version of Git, that is available from 'master'
 +unreleased) version of Git, that is available from the 'master'
  branch of the `git.git` repository.
  Documentation for older releases are available here:
  
 +* link:v2.1.0/git.html[documentation for release 2.1]
 +
 +* release notes for
 +  link:RelNotes/2.1.0.txt[2.1].
 +
 +* link:v2.0.4/git.html[documentation for release 2.0.4]
 +
 +* release notes for
 +  link:RelNotes/2.0.4.txt[2.0.4],
 +  link:RelNotes/2.0.3.txt[2.0.3],
 +  link:RelNotes/2.0.2.txt[2.0.2],
 +  link:RelNotes/2.0.1.txt[2.0.1],
 +  link:RelNotes/2.0.0.txt[2.0.0].
 +
 +* link:v1.9.4/git.html[documentation for release 1.9.4]
 +
 +* release notes for
 +  link:RelNotes/1.9.4.txt[1.9.4],
 +  link:RelNotes/1.9.3.txt[1.9.3],
 +  link:RelNotes/1.9.2.txt[1.9.2],
 +  link:RelNotes/1.9.1.txt[1.9.1],
 +  link:RelNotes/1.9.0.txt[1.9.0].
 +
  * link:v1.8.5.5/git.html[documentation for release 1.8.5.5]
  
  * release notes for
@@@ -452,6 -429,11 +452,11 @@@ example the following invocations are e
        given will override values from configuration files.
        The <name> is expected in the same format as listed by
        'git config' (subkeys separated by dots).
+ +
+ Note that omitting the `=` in `git -c foo.bar ...` is allowed and sets
+ `foo.bar` to the boolean true value (just like `[foo]bar` would in a
+ config file). Including the equals but with an empty value (like `git -c
+ foo.bar= ...`) sets `foo.bar` to the empty string.
  
  --exec-path[=<path>]::
        Path to wherever your core Git programs are installed.
@@@ -738,11 -720,6 +743,11 @@@ Git so take care if using Cogito etc
        index file. If not specified, the default of `$GIT_DIR/index`
        is used.
  
 +'GIT_INDEX_VERSION'::
 +      This environment variable allows the specification of an index
 +      version for new repositories.  It won't affect existing index
 +      files.  By default index file version [23] is used.
 +
  'GIT_OBJECT_DIRECTORY'::
        If the object storage directory is specified via this
        environment variable then the sha1 directories are created
  
  'GIT_WORK_TREE'::
        Set the path to the root of the working tree.
 -      This can also be controlled by the '--work-tree' command line
 +      This can also be controlled by the '--work-tree' command-line
        option and the core.worktree configuration variable.
  
  'GIT_NAMESPACE'::
@@@ -838,15 -815,6 +843,15 @@@ temporary file --- it is removed when '
  +
  For a path that is unmerged, 'GIT_EXTERNAL_DIFF' is called with 1
  parameter, <path>.
 ++
 +For each path 'GIT_EXTERNAL_DIFF' is called, two environment variables,
 +'GIT_DIFF_PATH_COUNTER' and 'GIT_DIFF_PATH_TOTAL' are set.
 +
 +'GIT_DIFF_PATH_COUNTER'::
 +      A 1-based counter incremented by one for every path.
 +
 +'GIT_DIFF_PATH_TOTAL'::
 +      The total number of paths.
  
  other
  ~~~~~
@@@ -889,7 -857,7 +894,7 @@@ for further details
  'GIT_ASKPASS'::
        If this environment variable is set, then Git commands which need to
        acquire passwords or passphrases (e.g. for HTTP or IMAP authentication)
 -      will call this program with a suitable prompt as command line argument
 +      will call this program with a suitable prompt as command-line argument
        and read the password from its STDOUT. See also the 'core.askpass'
        option in linkgit:git-config[1].
  
        based on whether stdout appears to be redirected to a file or not.
  
  'GIT_TRACE'::
 -      If this variable is set to "1", "2" or "true" (comparison
 -      is case insensitive), Git will print `trace:` messages on
 -      stderr telling about alias expansion, built-in command
 -      execution and external command execution.
 -      If this variable is set to an integer value greater than 1
 -      and lower than 10 (strictly) then Git will interpret this
 -      value as an open file descriptor and will try to write the
 -      trace messages into this file descriptor.
 -      Alternatively, if this variable is set to an absolute path
 -      (starting with a '/' character), Git will interpret this
 -      as a file path and will try to write the trace messages
 -      into it.
 +      Enables general trace messages, e.g. alias expansion, built-in
 +      command execution and external command execution.
 ++
 +If this variable is set to "1", "2" or "true" (comparison
 +is case insensitive), trace messages will be printed to
 +stderr.
 ++
 +If the variable is set to an integer value greater than 2
 +and lower than 10 (strictly) then Git will interpret this
 +value as an open file descriptor and will try to write the
 +trace messages into this file descriptor.
 ++
 +Alternatively, if the variable is set to an absolute path
 +(starting with a '/' character), Git will interpret this
 +as a file path and will try to write the trace messages
 +into it.
 ++
 +Unsetting the variable, or setting it to empty, "0" or
 +"false" (case insensitive) disables trace messages.
  
  'GIT_TRACE_PACK_ACCESS'::
 -      If this variable is set to a path, a file will be created at
 -      the given path logging all accesses to any packs. For each
 +      Enables trace messages for all accesses to any packs. For each
        access, the pack file name and an offset in the pack is
        recorded. This may be helpful for troubleshooting some
        pack-related performance problems.
 +      See 'GIT_TRACE' for available trace output options.
  
  'GIT_TRACE_PACKET'::
 -      If this variable is set, it shows a trace of all packets
 -      coming in or out of a given program. This can help with
 -      debugging object negotiation or other protocol issues. Tracing
 -      is turned off at a packet starting with "PACK".
 +      Enables trace messages for all packets coming in or out of a
 +      given program. This can help with debugging object negotiation
 +      or other protocol issues. Tracing is turned off at a packet
 +      starting with "PACK".
 +      See 'GIT_TRACE' for available trace output options.
 +
 +'GIT_TRACE_PERFORMANCE'::
 +      Enables performance related trace messages, e.g. total execution
 +      time of each Git command.
 +      See 'GIT_TRACE' for available trace output options.
 +
 +'GIT_TRACE_SETUP'::
 +      Enables trace messages printing the .git, working tree and current
 +      working directory after Git has completed its setup phase.
 +      See 'GIT_TRACE' for available trace output options.
 +
 +'GIT_TRACE_SHALLOW'::
 +      Enables trace messages that can help debugging fetching /
 +      cloning of shallow repositories.
 +      See 'GIT_TRACE' for available trace output options.
  
  GIT_LITERAL_PATHSPECS::
        Setting this variable to `1` will cause Git to treat all
@@@ -1074,7 -1019,7 +1079,7 @@@ Author
  -------
  Git was started by Linus Torvalds, and is currently maintained by Junio
  C Hamano. Numerous contributions have come from the Git mailing list
 -<git@vger.kernel.org>.  http://www.ohloh.net/p/git/contributors/summary
 +<git@vger.kernel.org>.  http://www.openhub.net/p/git/contributors/summary
  gives you a more complete list of contributors.
  
  If you have a clone of git.git itself, the
diff --combined config.c
index a191328a9d6a85909a7720b8120b70d98c12e092,5390a63acb372eaa18ba4e83022789155fcc1c1f..01e0bdbafd4d43a7fb983449c78d711d22dd2375
+++ b/config.c
@@@ -9,8 -9,6 +9,8 @@@
  #include "exec_cmd.h"
  #include "strbuf.h"
  #include "quote.h"
 +#include "hashmap.h"
 +#include "string-list.h"
  
  struct config_source {
        struct config_source *prev;
@@@ -23,7 -21,6 +23,7 @@@
                } buf;
        } u;
        const char *name;
 +      const char *path;
        int die_on_error;
        int linenr;
        int eof;
        long (*do_ftell)(struct config_source *c);
  };
  
 +struct config_set_element {
 +      struct hashmap_entry ent;
 +      char *key;
 +      struct string_list value_list;
 +};
 +
  static struct config_source *cf;
  
  static int zlib_compression_seen;
  
 +/*
 + * Default config_set that contains key-value pairs from the usual set of config
 + * config files (i.e repo specific .git/config, user wide ~/.gitconfig, XDG
 + * config file and the global /etc/gitconfig)
 + */
 +static struct config_set the_config_set;
 +
  static int config_file_fgetc(struct config_source *conf)
  {
        return fgetc(conf->u.file);
@@@ -100,12 -84,8 +100,12 @@@ static int handle_path_include(const ch
  {
        int ret = 0;
        struct strbuf buf = STRBUF_INIT;
 -      char *expanded = expand_user_path(path);
 +      char *expanded;
  
 +      if (!path)
 +              return config_error_nonbool("include.path");
 +
 +      expanded = expand_user_path(path);
        if (!expanded)
                return error("Could not expand include path '%s'", path);
        path = expanded;
        if (!is_absolute_path(path)) {
                char *slash;
  
 -              if (!cf || !cf->name)
 +              if (!cf || !cf->path)
                        return error("relative config includes must come from files");
  
 -              slash = find_last_dir_sep(cf->name);
 +              slash = find_last_dir_sep(cf->path);
                if (slash)
 -                      strbuf_add(&buf, cf->name, slash - cf->name + 1);
 +                      strbuf_add(&buf, cf->path, slash - cf->path + 1);
                strbuf_addstr(&buf, path);
                path = buf.buf;
        }
@@@ -153,7 -133,8 +153,7 @@@ int git_config_include(const char *var
        if (ret < 0)
                return ret;
  
 -      type = skip_prefix(var, "include.");
 -      if (!type)
 +      if (!skip_prefix(var, "include.", &type))
                return ret;
  
        if (!strcmp(type, "path"))
        return ret;
  }
  
 -static void lowercase(char *p)
 -{
 -      for (; *p; p++)
 -              *p = tolower(*p);
 -}
 -
  void git_config_push_parameter(const char *text)
  {
        struct strbuf env = STRBUF_INIT;
  int git_config_parse_parameter(const char *text,
                               config_fn_t fn, void *data)
  {
+       const char *value;
        struct strbuf **pair;
        pair = strbuf_split_str(text, '=', 2);
        if (!pair[0])
                return error("bogus config parameter: %s", text);
-       if (pair[0]->len && pair[0]->buf[pair[0]->len - 1] == '=')
+       if (pair[0]->len && pair[0]->buf[pair[0]->len - 1] == '=') {
                strbuf_setlen(pair[0], pair[0]->len - 1);
+               value = pair[1] ? pair[1]->buf : "";
+       } else {
+               value = NULL;
+       }
        strbuf_trim(pair[0]);
        if (!pair[0]->len) {
                strbuf_list_free(pair);
                return error("bogus config parameter: %s", text);
        }
 -      lowercase(pair[0]->buf);
 +      strbuf_tolower(pair[0]);
-       if (fn(pair[0]->buf, pair[1] ? pair[1]->buf : NULL, data) < 0) {
+       if (fn(pair[0]->buf, value, data) < 0) {
                strbuf_list_free(pair);
                return -1;
        }
@@@ -565,7 -560,6 +573,7 @@@ int git_parse_ulong(const char *value, 
        return 1;
  }
  
 +NORETURN
  static void die_bad_number(const char *name, const char *value)
  {
        const char *reason = errno == ERANGE ?
@@@ -677,7 -671,20 +685,7 @@@ static int git_default_core_config(cons
                trust_ctime = git_config_bool(var, value);
                return 0;
        }
 -      if (!strcmp(var, "core.statinfo") ||
 -          !strcmp(var, "core.checkstat")) {
 -              /*
 -               * NEEDSWORK: statinfo was a typo in v1.8.2 that has
 -               * never been advertised.  we will remove it at Git
 -               * 2.0 boundary.
 -               */
 -              if (!strcmp(var, "core.statinfo")) {
 -                      static int warned;
 -                      if (!warned++) {
 -                              warning("'core.statinfo' will be removed in Git 2.0; "
 -                                      "use 'core.checkstat' instead.");
 -                      }
 -              }
 +      if (!strcmp(var, "core.checkstat")) {
                if (!strcasecmp(value, "default"))
                        check_stat = 1;
                else if (!strcasecmp(value, "minimal"))
                return git_config_string(&editor_program, var, value);
  
        if (!strcmp(var, "core.commentchar")) {
 -              const char *comment;
 -              int ret = git_config_string(&comment, var, value);
 -              if (!ret)
 -                      comment_line_char = comment[0];
 -              return ret;
 +              if (!value)
 +                      return config_error_nonbool(var);
 +              else if (!strcasecmp(value, "auto"))
 +                      auto_comment_line_char = 1;
 +              else if (value[0] && !value[1]) {
 +                      comment_line_char = value[0];
 +                      auto_comment_line_char = 0;
 +              } else
 +                      return error("core.commentChar should only be one character");
 +              return 0;
        }
  
        if (!strcmp(var, "core.askpass"))
@@@ -965,7 -967,7 +973,7 @@@ static int git_default_push_config(cons
  static int git_default_mailmap_config(const char *var, const char *value)
  {
        if (!strcmp(var, "mailmap.file"))
 -              return git_config_string(&git_mailmap_file, var, value);
 +              return git_config_pathname(&git_mailmap_file, var, value);
        if (!strcmp(var, "mailmap.blob"))
                return git_config_string(&git_mailmap_blob, var, value);
  
  
  int git_default_config(const char *var, const char *value, void *dummy)
  {
 -      if (!prefixcmp(var, "core."))
 +      if (starts_with(var, "core."))
                return git_default_core_config(var, value);
  
 -      if (!prefixcmp(var, "user."))
 +      if (starts_with(var, "user."))
                return git_ident_config(var, value, dummy);
  
 -      if (!prefixcmp(var, "i18n."))
 +      if (starts_with(var, "i18n."))
                return git_default_i18n_config(var, value);
  
 -      if (!prefixcmp(var, "branch."))
 +      if (starts_with(var, "branch."))
                return git_default_branch_config(var, value);
  
 -      if (!prefixcmp(var, "push."))
 +      if (starts_with(var, "push."))
                return git_default_push_config(var, value);
  
 -      if (!prefixcmp(var, "mailmap."))
 +      if (starts_with(var, "mailmap."))
                return git_default_mailmap_config(var, value);
  
 -      if (!prefixcmp(var, "advice."))
 +      if (starts_with(var, "advice."))
                return git_default_advice_config(var, value);
  
        if (!strcmp(var, "pager.color") || !strcmp(var, "color.pager")) {
@@@ -1036,35 -1038,24 +1044,35 @@@ static int do_config_from(struct config
        return ret;
  }
  
 -int git_config_from_file(config_fn_t fn, const char *filename, void *data)
 +static int do_config_from_file(config_fn_t fn,
 +              const char *name, const char *path, FILE *f, void *data)
  {
 -      int ret;
 -      FILE *f = fopen(filename, "r");
 +      struct config_source top;
  
 -      ret = -1;
 -      if (f) {
 -              struct config_source top;
 +      top.u.file = f;
 +      top.name = name;
 +      top.path = path;
 +      top.die_on_error = 1;
 +      top.do_fgetc = config_file_fgetc;
 +      top.do_ungetc = config_file_ungetc;
 +      top.do_ftell = config_file_ftell;
  
 -              top.u.file = f;
 -              top.name = filename;
 -              top.die_on_error = 1;
 -              top.do_fgetc = config_file_fgetc;
 -              top.do_ungetc = config_file_ungetc;
 -              top.do_ftell = config_file_ftell;
 +      return do_config_from(&top, fn, data);
 +}
  
 -              ret = do_config_from(&top, fn, data);
 +static int git_config_from_stdin(config_fn_t fn, void *data)
 +{
 +      return do_config_from_file(fn, "<stdin>", NULL, stdin, data);
 +}
  
 +int git_config_from_file(config_fn_t fn, const char *filename, void *data)
 +{
 +      int ret = -1;
 +      FILE *f;
 +
 +      f = fopen(filename, "r");
 +      if (f) {
 +              ret = do_config_from_file(fn, filename, filename, f, data);
                fclose(f);
        }
        return ret;
@@@ -1079,7 -1070,6 +1087,7 @@@ int git_config_from_buf(config_fn_t fn
        top.u.buf.len = len;
        top.u.buf.pos = 0;
        top.name = name;
 +      top.path = NULL;
        top.die_on_error = 0;
        top.do_fgetc = config_buf_fgetc;
        top.do_ungetc = config_buf_ungetc;
@@@ -1188,7 -1178,8 +1196,7 @@@ int git_config_early(config_fn_t fn, vo
  }
  
  int git_config_with_options(config_fn_t fn, void *data,
 -                          const char *filename,
 -                          const char *blob_ref,
 +                          struct git_config_source *config_source,
                            int respect_includes)
  {
        char *repo_config = NULL;
         * If we have a specific filename, use it. Otherwise, follow the
         * regular lookup sequence.
         */
 -      if (filename)
 -              return git_config_from_file(fn, filename, data);
 -      else if (blob_ref)
 -              return git_config_from_blob_ref(fn, blob_ref, data);
 +      if (config_source && config_source->use_stdin)
 +              return git_config_from_stdin(fn, data);
 +      else if (config_source && config_source->file)
 +              return git_config_from_file(fn, config_source->file, data);
 +      else if (config_source && config_source->blob)
 +              return git_config_from_blob_ref(fn, config_source->blob, data);
  
        repo_config = git_pathdup("config");
        ret = git_config_early(fn, data, repo_config);
  
  int git_config(config_fn_t fn, void *data)
  {
 -      return git_config_with_options(fn, data, NULL, NULL, 1);
 +      return git_config_with_options(fn, data, NULL, 1);
 +}
 +
 +static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
 +{
 +      struct config_set_element k;
 +      struct config_set_element *found_entry;
 +      char *normalized_key;
 +      int ret;
 +      /*
 +       * `key` may come from the user, so normalize it before using it
 +       * for querying entries from the hashmap.
 +       */
 +      ret = git_config_parse_key(key, &normalized_key, NULL);
 +
 +      if (ret)
 +              return NULL;
 +
 +      hashmap_entry_init(&k, strhash(normalized_key));
 +      k.key = normalized_key;
 +      found_entry = hashmap_get(&cs->config_hash, &k, NULL);
 +      free(normalized_key);
 +      return found_entry;
 +}
 +
 +static int configset_add_value(struct config_set *cs, const char *key, const char *value)
 +{
 +      struct config_set_element *e;
 +      e = configset_find_element(cs, key);
 +      /*
 +       * Since the keys are being fed by git_config*() callback mechanism, they
 +       * are already normalized. So simply add them without any further munging.
 +       */
 +      if (!e) {
 +              e = xmalloc(sizeof(*e));
 +              hashmap_entry_init(e, strhash(key));
 +              e->key = xstrdup(key);
 +              string_list_init(&e->value_list, 1);
 +              hashmap_add(&cs->config_hash, e);
 +      }
 +      string_list_append_nodup(&e->value_list, value ? xstrdup(value) : NULL);
 +
 +      return 0;
 +}
 +
 +static int config_set_element_cmp(const struct config_set_element *e1,
 +                               const struct config_set_element *e2, const void *unused)
 +{
 +      return strcmp(e1->key, e2->key);
 +}
 +
 +void git_configset_init(struct config_set *cs)
 +{
 +      hashmap_init(&cs->config_hash, (hashmap_cmp_fn)config_set_element_cmp, 0);
 +      cs->hash_initialized = 1;
 +}
 +
 +void git_configset_clear(struct config_set *cs)
 +{
 +      struct config_set_element *entry;
 +      struct hashmap_iter iter;
 +      if (!cs->hash_initialized)
 +              return;
 +
 +      hashmap_iter_init(&cs->config_hash, &iter);
 +      while ((entry = hashmap_iter_next(&iter))) {
 +              free(entry->key);
 +              string_list_clear(&entry->value_list, 0);
 +      }
 +      hashmap_free(&cs->config_hash, 1);
 +      cs->hash_initialized = 0;
 +}
 +
 +static int config_set_callback(const char *key, const char *value, void *cb)
 +{
 +      struct config_set *cs = cb;
 +      configset_add_value(cs, key, value);
 +      return 0;
 +}
 +
 +int git_configset_add_file(struct config_set *cs, const char *filename)
 +{
 +      return git_config_from_file(config_set_callback, filename, cs);
 +}
 +
 +int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
 +{
 +      const struct string_list *values = NULL;
 +      /*
 +       * Follows "last one wins" semantic, i.e., if there are multiple matches for the
 +       * queried key in the files of the configset, the value returned will be the last
 +       * value in the value list for that key.
 +       */
 +      values = git_configset_get_value_multi(cs, key);
 +
 +      if (!values)
 +              return 1;
 +      assert(values->nr > 0);
 +      *value = values->items[values->nr - 1].string;
 +      return 0;
 +}
 +
 +const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
 +{
 +      struct config_set_element *e = configset_find_element(cs, key);
 +      return e ? &e->value_list : NULL;
 +}
 +
 +int git_configset_get_string_const(struct config_set *cs, const char *key, const char **dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value))
 +              return git_config_string(dest, key, value);
 +      else
 +              return 1;
 +}
 +
 +int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
 +{
 +      return git_configset_get_string_const(cs, key, (const char **)dest);
 +}
 +
 +int git_configset_get_int(struct config_set *cs, const char *key, int *dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value)) {
 +              *dest = git_config_int(key, value);
 +              return 0;
 +      } else
 +              return 1;
 +}
 +
 +int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value)) {
 +              *dest = git_config_ulong(key, value);
 +              return 0;
 +      } else
 +              return 1;
 +}
 +
 +int git_configset_get_bool(struct config_set *cs, const char *key, int *dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value)) {
 +              *dest = git_config_bool(key, value);
 +              return 0;
 +      } else
 +              return 1;
 +}
 +
 +int git_configset_get_bool_or_int(struct config_set *cs, const char *key,
 +                              int *is_bool, int *dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value)) {
 +              *dest = git_config_bool_or_int(key, value, is_bool);
 +              return 0;
 +      } else
 +              return 1;
 +}
 +
 +int git_configset_get_maybe_bool(struct config_set *cs, const char *key, int *dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value)) {
 +              *dest = git_config_maybe_bool(key, value);
 +              if (*dest == -1)
 +                      return -1;
 +              return 0;
 +      } else
 +              return 1;
 +}
 +
 +int git_configset_get_pathname(struct config_set *cs, const char *key, const char **dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value))
 +              return git_config_pathname(dest, key, value);
 +      else
 +              return 1;
 +}
 +
 +static void git_config_check_init(void)
 +{
 +      if (the_config_set.hash_initialized)
 +              return;
 +      git_configset_init(&the_config_set);
 +      git_config(config_set_callback, &the_config_set);
 +}
 +
 +void git_config_clear(void)
 +{
 +      if (!the_config_set.hash_initialized)
 +              return;
 +      git_configset_clear(&the_config_set);
 +}
 +
 +int git_config_get_value(const char *key, const char **value)
 +{
 +      git_config_check_init();
 +      return git_configset_get_value(&the_config_set, key, value);
 +}
 +
 +const struct string_list *git_config_get_value_multi(const char *key)
 +{
 +      git_config_check_init();
 +      return git_configset_get_value_multi(&the_config_set, key);
 +}
 +
 +int git_config_get_string_const(const char *key, const char **dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_string_const(&the_config_set, key, dest);
 +}
 +
 +int git_config_get_string(const char *key, char **dest)
 +{
 +      git_config_check_init();
 +      return git_config_get_string_const(key, (const char **)dest);
 +}
 +
 +int git_config_get_int(const char *key, int *dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_int(&the_config_set, key, dest);
 +}
 +
 +int git_config_get_ulong(const char *key, unsigned long *dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_ulong(&the_config_set, key, dest);
 +}
 +
 +int git_config_get_bool(const char *key, int *dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_bool(&the_config_set, key, dest);
 +}
 +
 +int git_config_get_bool_or_int(const char *key, int *is_bool, int *dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_bool_or_int(&the_config_set, key, is_bool, dest);
 +}
 +
 +int git_config_get_maybe_bool(const char *key, int *dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_maybe_bool(&the_config_set, key, dest);
 +}
 +
 +int git_config_get_pathname(const char *key, const char **dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_pathname(&the_config_set, key, dest);
  }
  
  /*
   * Find all the stuff for git_config_set() below.
   */
  
 -#define MAX_MATCHES 512
 -
  static struct {
        int baselen;
        char *key;
        int do_not_match;
        regex_t *value_regex;
        int multi_replace;
 -      size_t offset[MAX_MATCHES];
 +      size_t *offset;
 +      unsigned int offset_alloc;
        enum { START, SECTION_SEEN, SECTION_END_SEEN, KEY_SEEN } state;
        int seen;
  } store;
@@@ -1515,11 -1249,11 +1523,11 @@@ static int store_aux(const char *key, c
                if (matches(key, value)) {
                        if (store.seen == 1 && store.multi_replace == 0) {
                                warning("%s has multiple values", key);
 -                      } else if (store.seen >= MAX_MATCHES) {
 -                              error("too many matches for %s", key);
 -                              return 1;
                        }
  
 +                      ALLOC_GROW(store.offset, store.seen + 1,
 +                                 store.offset_alloc);
 +
                        store.offset[store.seen] = cf->do_ftell(cf);
                        store.seen++;
                }
                 * Do not increment matches: this is no match, but we
                 * just made sure we are in the desired section.
                 */
 +              ALLOC_GROW(store.offset, store.seen + 1,
 +                         store.offset_alloc);
                store.offset[store.seen] = cf->do_ftell(cf);
                /* fallthru */
        case SECTION_END_SEEN:
        case START:
                if (matches(key, value)) {
 +                      ALLOC_GROW(store.offset, store.seen + 1,
 +                                 store.offset_alloc);
                        store.offset[store.seen] = cf->do_ftell(cf);
                        store.state = KEY_SEEN;
                        store.seen++;
                        if (strrchr(key, '.') - key == store.baselen &&
                              !strncmp(key, store.key, store.baselen)) {
                                        store.state = SECTION_SEEN;
 +                                      ALLOC_GROW(store.offset,
 +                                                 store.seen + 1,
 +                                                 store.offset_alloc);
                                        store.offset[store.seen] = cf->do_ftell(cf);
                        }
                }
@@@ -1807,7 -1534,7 +1815,7 @@@ int git_config_set_multivar_in_file(con
         * The lock serves a purpose in addition to locking: the new
         * contents of .git/config will be written into it.
         */
 -      lock = xcalloc(sizeof(struct lock_file), 1);
 +      lock = xcalloc(1, sizeof(struct lock_file));
        fd = hold_lock_file_for_update(lock, config_filename, 0);
        if (fd < 0) {
                error("could not lock config file %s: %s", config_filename, strerror(errno));
                        }
                }
  
 +              ALLOC_GROW(store.offset, 1, store.offset_alloc);
                store.offset[0] = 0;
                store.state = START;
                store.seen = 0;
                        MAP_PRIVATE, in_fd, 0);
                close(in_fd);
  
 +              if (chmod(lock->filename, st.st_mode & 07777) < 0) {
 +                      error("chmod on %s failed: %s",
 +                              lock->filename, strerror(errno));
 +                      ret = CONFIG_NO_WRITE;
 +                      goto out_free;
 +              }
 +
                if (store.seen == 0)
                        store.seen = 1;
  
        lock = NULL;
        ret = 0;
  
 +      /* Invalidate the config cache */
 +      git_config_clear();
 +
  out_free:
        if (lock)
                rollback_lock_file(lock);
@@@ -2063,7 -1779,6 +2071,7 @@@ int git_config_rename_section_in_file(c
        int out_fd;
        char buf[1024];
        FILE *config_file;
 +      struct stat st;
  
        if (new_name && !section_name_is_ok(new_name)) {
                ret = error("invalid section name: %s", new_name);
        if (!config_filename)
                config_filename = filename_buf = git_pathdup("config");
  
 -      lock = xcalloc(sizeof(struct lock_file), 1);
 +      lock = xcalloc(1, sizeof(struct lock_file));
        out_fd = hold_lock_file_for_update(lock, config_filename, 0);
        if (out_fd < 0) {
                ret = error("could not lock config file %s", config_filename);
                goto unlock_and_out;
        }
  
 +      fstat(fileno(config_file), &st);
 +
 +      if (chmod(lock->filename, st.st_mode & 07777) < 0) {
 +              ret = error("chmod on %s failed: %s",
 +                              lock->filename, strerror(errno));
 +              goto out;
 +      }
 +
        while (fgets(buf, sizeof(buf), config_file)) {
                int i;
                int length;
@@@ -2173,7 -1880,7 +2181,7 @@@ int parse_config_key(const char *var
        const char *dot;
  
        /* Does it start with "section." ? */
 -      if (prefixcmp(var, section) || var[section_len] != '.')
 +      if (!starts_with(var, section) || var[section_len] != '.')
                return -1;
  
        /*
diff --combined t/t1300-repo-config.sh
index fb871d09cde59b87b47b92c798aacd1acf1375e2,38917da0ef764be20b58a59a5c254ebdbb2c891e..938fc8bfd76c9889ebb2129642f878c9449acc9f
@@@ -461,7 -461,7 +461,7 @@@ test_expect_success 'new variable inser
        test_cmp expect .git/config
  '
  
 -test_expect_success 'alternative GIT_CONFIG (non-existing file should fail)' '
 +test_expect_success 'alternative --file (non-existing file should fail)' '
        test_must_fail git config --file non-existing-config -l
  '
  
@@@ -475,28 -475,15 +475,28 @@@ ein.bahn=strass
  EOF
  
  test_expect_success 'alternative GIT_CONFIG' '
 -      GIT_CONFIG=other-config git config -l >output &&
 +      GIT_CONFIG=other-config git config --list >output &&
        test_cmp expect output
  '
  
  test_expect_success 'alternative GIT_CONFIG (--file)' '
 -      git config --file other-config -l > output &&
 +      git config --file other-config --list >output &&
        test_cmp expect output
  '
  
 +test_expect_success 'alternative GIT_CONFIG (--file=-)' '
 +      git config --file - --list <other-config >output &&
 +      test_cmp expect output
 +'
 +
 +test_expect_success 'setting a value in stdin is an error' '
 +      test_must_fail git config --file - some.value foo
 +'
 +
 +test_expect_success 'editing stdin is an error' '
 +      test_must_fail git config --file - --edit
 +'
 +
  test_expect_success 'refer config from subdirectory' '
        mkdir x &&
        (
  
  '
  
 -test_expect_success 'refer config from subdirectory via GIT_CONFIG' '
 +test_expect_success 'refer config from subdirectory via --file' '
        (
                cd x &&
 -              GIT_CONFIG=../other-config git config --get ein.bahn >actual &&
 +              git config --file=../other-config --get ein.bahn >actual &&
                test_cmp expect actual
        )
  '
@@@ -523,8 -510,8 +523,8 @@@ cat > expect << EO
        park = ausweis
  EOF
  
 -test_expect_success '--set in alternative GIT_CONFIG' '
 -      GIT_CONFIG=other-config git config anwohner.park ausweis &&
 +test_expect_success '--set in alternative file' '
 +      git config --file=other-config anwohner.park ausweis &&
        test_cmp expect other-config
  '
  
@@@ -824,14 -811,14 +824,14 @@@ cat >expect <<\EO
        trailingtilde = foo~
  EOF
  
 -test_expect_success NOT_MINGW 'set --path' '
 +test_expect_success !MINGW 'set --path' '
        rm -f .git/config &&
        git config --path path.home "~/" &&
        git config --path path.normal "/dev/null" &&
        git config --path path.trailingtilde "foo~" &&
        test_cmp expect .git/config'
  
 -if test_have_prereq NOT_MINGW && test "${HOME+set}"
 +if test_have_prereq !MINGW && test "${HOME+set}"
  then
        test_set_prereq HOMEVAR
  fi
@@@ -854,7 -841,7 +854,7 @@@ cat >expect <<\EO
  foo~
  EOF
  
 -test_expect_success NOT_MINGW 'get --path copes with unset $HOME' '
 +test_expect_success !MINGW 'get --path copes with unset $HOME' '
        (
                unset HOME;
                test_must_fail git config --get --path path.home \
@@@ -955,11 -942,11 +955,11 @@@ test_expect_success 'inner whitespace k
  
  test_expect_success SYMLINKS 'symlinked configuration' '
        ln -s notyet myconfig &&
 -      GIT_CONFIG=myconfig git config test.frotz nitfol &&
 +      git config --file=myconfig test.frotz nitfol &&
        test -h myconfig &&
        test -f notyet &&
 -      test "z$(GIT_CONFIG=notyet git config test.frotz)" = znitfol &&
 -      GIT_CONFIG=myconfig git config test.xyzzy rezrov &&
 +      test "z$(git config --file=notyet test.frotz)" = znitfol &&
 +      git config --file=myconfig test.xyzzy rezrov &&
        test -h myconfig &&
        test -f notyet &&
        cat >expect <<-\EOF &&
        rezrov
        EOF
        {
 -              GIT_CONFIG=notyet git config test.frotz &&
 -              GIT_CONFIG=notyet git config test.xyzzy
 +              git config --file=notyet test.frotz &&
 +              git config --file=notyet test.xyzzy
        } >actual &&
        test_cmp expect actual
  '
  
  test_expect_success 'nonexistent configuration' '
 -      (
 -              GIT_CONFIG=doesnotexist &&
 -              export GIT_CONFIG &&
 -              test_must_fail git config --list &&
 -              test_must_fail git config test.xyzzy
 -      )
 +      test_must_fail git config --file=doesnotexist --list &&
 +      test_must_fail git config --file=doesnotexist test.xyzzy
  '
  
  test_expect_success SYMLINKS 'symlink to nonexistent configuration' '
        ln -s doesnotexist linktonada &&
        ln -s linktonada linktolinktonada &&
 -      (
 -              GIT_CONFIG=linktonada &&
 -              export GIT_CONFIG &&
 -              test_must_fail git config --list &&
 -              GIT_CONFIG=linktolinktonada &&
 -              test_must_fail git config --list
 -      )
 +      test_must_fail git config --file=linktonada --list &&
 +      test_must_fail git config --file=linktolinktonada --list
  '
  
  test_expect_success 'check split_cmdline return' "
@@@ -1010,6 -1006,17 +1010,17 @@@ test_expect_success 'git -c "key=value
        test_must_fail git -c name=value config core.name
  '
  
+ # We just need a type-specifier here that cares about the
+ # distinction internally between a NULL boolean and a real
+ # string (because most of git's internal parsers do care).
+ # Using "--path" works, but we do not otherwise care about
+ # its semantics.
+ test_expect_success 'git -c can represent empty string' '
+       echo >expect &&
+       git -c foo.empty= config --path foo.empty >actual &&
+       test_cmp expect actual
+ '
  test_expect_success 'key sanity-checking' '
        test_must_fail git config foo=bar &&
        test_must_fail git config foo=.bar &&
@@@ -1158,14 -1165,4 +1169,14 @@@ test_expect_failure 'adding a key into 
        test_cmp expect .git/config
  '
  
 +test_expect_success POSIXPERM,PERL 'preserves existing permissions' '
 +      chmod 0600 .git/config &&
 +      git config imap.pass Hunter2 &&
 +      perl -e \
 +        "die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600" &&
 +      git config --rename-section imap pop &&
 +      perl -e \
 +        "die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600"
 +'
 +
  test_done