Documentation: refactor common operations into variables
[gitweb.git] / config.c
index 47ce8d8b8eadb965f33044a1d19cb17e1c2875cb..039647d247e0ac1e3a523800bbb90c86d89354de 100644 (file)
--- a/config.c
+++ b/config.c
@@ -9,23 +9,80 @@
 #include "exec_cmd.h"
 #include "strbuf.h"
 #include "quote.h"
-
-#define MAXNAME (256)
-
-typedef struct config_file {
-       struct config_file *prev;
-       FILE *f;
+#include "hashmap.h"
+#include "string-list.h"
+
+struct config_source {
+       struct config_source *prev;
+       union {
+               FILE *file;
+               struct config_buf {
+                       const char *buf;
+                       size_t len;
+                       size_t pos;
+               } buf;
+       } u;
        const char *name;
+       const char *path;
+       int die_on_error;
        int linenr;
        int eof;
        struct strbuf value;
-       char var[MAXNAME];
-} config_file;
+       struct strbuf var;
 
-static config_file *cf;
+       int (*do_fgetc)(struct config_source *c);
+       int (*do_ungetc)(int c, struct config_source *conf);
+       long (*do_ftell)(struct config_source *c);
+};
+
+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);
+}
+
+static int config_file_ungetc(int c, struct config_source *conf)
+{
+       return ungetc(c, conf->u.file);
+}
+
+static long config_file_ftell(struct config_source *conf)
+{
+       return ftell(conf->u.file);
+}
+
+
+static int config_buf_fgetc(struct config_source *conf)
+{
+       if (conf->u.buf.pos < conf->u.buf.len)
+               return conf->u.buf.buf[conf->u.buf.pos++];
+
+       return EOF;
+}
+
+static int config_buf_ungetc(int c, struct config_source *conf)
+{
+       if (conf->u.buf.pos > 0)
+               return conf->u.buf.buf[--conf->u.buf.pos];
+
+       return EOF;
+}
+
+static long config_buf_ftell(struct config_source *conf)
+{
+       return conf->u.buf.pos;
+}
+
 #define MAX_INCLUDE_DEPTH 10
 static const char include_depth_advice[] =
 "exceeded maximum include depth (%d) while including\n"
@@ -54,17 +111,17 @@ static int handle_path_include(const char *path, struct config_include_data *inc
        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;
        }
 
-       if (!access(path, R_OK)) {
+       if (!access_or_die(path, R_OK, 0)) {
                if (++inc->depth > MAX_INCLUDE_DEPTH)
                        die(include_depth_advice, MAX_INCLUDE_DEPTH, path,
                            cf && cf->name ? cf->name : "the command line");
@@ -79,7 +136,6 @@ static int handle_path_include(const char *path, struct config_include_data *inc
 int git_config_include(const char *var, const char *value, void *data)
 {
        struct config_include_data *inc = data;
-       const char *type;
        int ret;
 
        /*
@@ -90,21 +146,11 @@ int git_config_include(const char *var, const char *value, void *data)
        if (ret < 0)
                return ret;
 
-       type = skip_prefix(var, "include.");
-       if (!type)
-               return ret;
-
-       if (!strcmp(type, "path"))
+       if (!strcmp(var, "include.path"))
                ret = handle_path_include(value, inc);
        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;
@@ -121,19 +167,27 @@ void git_config_push_parameter(const char *text)
 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);
-       if (fn(pair[0]->buf, pair[1] ? pair[1]->buf : NULL, data) < 0) {
+       strbuf_tolower(pair[0]);
+       if (fn(pair[0]->buf, value, data) < 0) {
                strbuf_list_free(pair);
                return -1;
        }
@@ -174,27 +228,23 @@ int git_config_from_parameters(config_fn_t fn, void *data)
 
 static int get_next_char(void)
 {
-       int c;
-       FILE *f;
-
-       c = '\n';
-       if (cf && ((f = cf->f) != NULL)) {
-               c = fgetc(f);
-               if (c == '\r') {
-                       /* DOS like systems */
-                       c = fgetc(f);
-                       if (c != '\n') {
-                               ungetc(c, f);
-                               c = '\r';
-                       }
-               }
-               if (c == '\n')
-                       cf->linenr++;
-               if (c == EOF) {
-                       cf->eof = 1;
-                       c = '\n';
+       int c = cf->do_fgetc(cf);
+
+       if (c == '\r') {
+               /* DOS like systems */
+               c = cf->do_fgetc(cf);
+               if (c != '\n') {
+                       cf->do_ungetc(c, cf);
+                       c = '\r';
                }
        }
+       if (c == '\n')
+               cf->linenr++;
+       if (c == EOF) {
+               cf->eof = 1;
+               cf->linenr++;
+               c = '\n';
+       }
        return c;
 }
 
@@ -264,10 +314,11 @@ static inline int iskeychar(int c)
        return isalnum(c) || c == '-';
 }
 
-static int get_value(config_fn_t fn, void *data, char *name, unsigned int len)
+static int get_value(config_fn_t fn, void *data, struct strbuf *name)
 {
        int c;
        char *value;
+       int ret;
 
        /* Get the full name */
        for (;;) {
@@ -276,11 +327,9 @@ static int get_value(config_fn_t fn, void *data, char *name, unsigned int len)
                        break;
                if (!iskeychar(c))
                        break;
-               name[len++] = tolower(c);
-               if (len >= MAXNAME)
-                       return -1;
+               strbuf_addch(name, tolower(c));
        }
-       name[len] = 0;
+
        while (c == ' ' || c == '\t')
                c = get_next_char();
 
@@ -292,10 +341,18 @@ static int get_value(config_fn_t fn, void *data, char *name, unsigned int len)
                if (!value)
                        return -1;
        }
-       return fn(name, value, data);
+       /*
+        * We already consumed the \n, but we need linenr to point to
+        * the line we just parsed during the call to fn to get
+        * accurate line number in error messages.
+        */
+       cf->linenr--;
+       ret = fn(name->buf, value, data);
+       cf->linenr++;
+       return ret;
 }
 
-static int get_extended_base_var(char *name, int baselen, int c)
+static int get_extended_base_var(struct strbuf *name, int c)
 {
        do {
                if (c == '\n')
@@ -306,7 +363,7 @@ static int get_extended_base_var(char *name, int baselen, int c)
        /* We require the format to be '[base "extension"]' */
        if (c != '"')
                return -1;
-       name[baselen++] = '.';
+       strbuf_addch(name, '.');
 
        for (;;) {
                int c = get_next_char();
@@ -319,45 +376,39 @@ static int get_extended_base_var(char *name, int baselen, int c)
                        if (c == '\n')
                                goto error_incomplete_line;
                }
-               name[baselen++] = c;
-               if (baselen > MAXNAME / 2)
-                       return -1;
+               strbuf_addch(name, c);
        }
 
        /* Final ']' */
        if (get_next_char() != ']')
                return -1;
-       return baselen;
+       return 0;
 error_incomplete_line:
        cf->linenr--;
        return -1;
 }
 
-static int get_base_var(char *name)
+static int get_base_var(struct strbuf *name)
 {
-       int baselen = 0;
-
        for (;;) {
                int c = get_next_char();
                if (cf->eof)
                        return -1;
                if (c == ']')
-                       return baselen;
+                       return 0;
                if (isspace(c))
-                       return get_extended_base_var(name, baselen, c);
+                       return get_extended_base_var(name, c);
                if (!iskeychar(c) && c != '.')
                        return -1;
-               if (baselen > MAXNAME / 2)
-                       return -1;
-               name[baselen++] = tolower(c);
+               strbuf_addch(name, tolower(c));
        }
 }
 
-static int git_parse_file(config_fn_t fn, void *data)
+static int git_parse_source(config_fn_t fn, void *data)
 {
        int comment = 0;
        int baselen = 0;
-       char *var = cf->var;
+       struct strbuf *var = &cf->var;
 
        /* U+FEFF Byte Order Mark in UTF8 */
        static const unsigned char *utf8_bom = (unsigned char *) "\xef\xbb\xbf";
@@ -393,20 +444,30 @@ static int git_parse_file(config_fn_t fn, void *data)
                        continue;
                }
                if (c == '[') {
-                       baselen = get_base_var(var);
-                       if (baselen <= 0)
+                       /* Reset prior to determining a new stem */
+                       strbuf_reset(var);
+                       if (get_base_var(var) < 0 || var->len < 1)
                                break;
-                       var[baselen++] = '.';
-                       var[baselen] = 0;
+                       strbuf_addch(var, '.');
+                       baselen = var->len;
                        continue;
                }
                if (!isalpha(c))
                        break;
-               var[baselen] = tolower(c);
-               if (get_value(fn, data, var, baselen+1) < 0)
+               /*
+                * Truncate the var name back to the section header
+                * stem prior to grabbing the suffix part of the name
+                * and the value.
+                */
+               strbuf_setlen(var, baselen);
+               strbuf_addch(var, tolower(c));
+               if (get_value(fn, data, var) < 0)
                        break;
        }
-       die("bad config file line %d in %s", cf->linenr, cf->name);
+       if (cf->die_on_error)
+               die(_("bad config file line %d in %s"), cf->linenr, cf->name);
+       else
+               return error(_("bad config file line %d in %s"), cf->linenr, cf->name);
 }
 
 static int parse_unit_factor(const char *end, uintmax_t *val)
@@ -428,7 +489,7 @@ static int parse_unit_factor(const char *end, uintmax_t *val)
        return 0;
 }
 
-static int git_parse_long(const char *value, long *ret)
+static int git_parse_signed(const char *value, intmax_t *ret, intmax_t max)
 {
        if (value && *value) {
                char *end;
@@ -440,21 +501,25 @@ static int git_parse_long(const char *value, long *ret)
                val = strtoimax(value, &end, 0);
                if (errno == ERANGE)
                        return 0;
-               if (!parse_unit_factor(end, &factor))
+               if (!parse_unit_factor(end, &factor)) {
+                       errno = EINVAL;
                        return 0;
+               }
                uval = abs(val);
                uval *= factor;
-               if ((uval > maximum_signed_value_of_type(long)) ||
-                   (abs(val) > uval))
+               if (uval > max || abs(val) > uval) {
+                       errno = ERANGE;
                        return 0;
+               }
                val *= factor;
                *ret = val;
                return 1;
        }
+       errno = EINVAL;
        return 0;
 }
 
-int git_parse_ulong(const char *value, unsigned long *ret)
+static int git_parse_unsigned(const char *value, uintmax_t *ret, uintmax_t max)
 {
        if (value && *value) {
                char *end;
@@ -466,29 +531,76 @@ int git_parse_ulong(const char *value, unsigned long *ret)
                if (errno == ERANGE)
                        return 0;
                oldval = val;
-               if (!parse_unit_factor(end, &val))
+               if (!parse_unit_factor(end, &val)) {
+                       errno = EINVAL;
                        return 0;
-               if ((val > maximum_unsigned_value_of_type(long)) ||
-                   (oldval > val))
+               }
+               if (val > max || oldval > val) {
+                       errno = ERANGE;
                        return 0;
+               }
                *ret = val;
                return 1;
        }
+       errno = EINVAL;
        return 0;
 }
 
-static void die_bad_config(const char *name)
+static int git_parse_int(const char *value, int *ret)
+{
+       intmax_t tmp;
+       if (!git_parse_signed(value, &tmp, maximum_signed_value_of_type(int)))
+               return 0;
+       *ret = tmp;
+       return 1;
+}
+
+static int git_parse_int64(const char *value, int64_t *ret)
+{
+       intmax_t tmp;
+       if (!git_parse_signed(value, &tmp, maximum_signed_value_of_type(int64_t)))
+               return 0;
+       *ret = tmp;
+       return 1;
+}
+
+int git_parse_ulong(const char *value, unsigned long *ret)
 {
+       uintmax_t tmp;
+       if (!git_parse_unsigned(value, &tmp, maximum_unsigned_value_of_type(long)))
+               return 0;
+       *ret = tmp;
+       return 1;
+}
+
+NORETURN
+static void die_bad_number(const char *name, const char *value)
+{
+       const char *reason = errno == ERANGE ?
+                            "out of range" :
+                            "invalid unit";
+       if (!value)
+               value = "";
+
        if (cf && cf->name)
-               die("bad config value for '%s' in %s", name, cf->name);
-       die("bad config value for '%s'", name);
+               die(_("bad numeric config value '%s' for '%s' in %s: %s"),
+                   value, name, cf->name, reason);
+       die(_("bad numeric config value '%s' for '%s': %s"), value, name, reason);
 }
 
 int git_config_int(const char *name, const char *value)
 {
-       long ret = 0;
-       if (!git_parse_long(value, &ret))
-               die_bad_config(name);
+       int ret;
+       if (!git_parse_int(value, &ret))
+               die_bad_number(name, value);
+       return ret;
+}
+
+int64_t git_config_int64(const char *name, const char *value)
+{
+       int64_t ret;
+       if (!git_parse_int64(value, &ret))
+               die_bad_number(name, value);
        return ret;
 }
 
@@ -496,7 +608,7 @@ unsigned long git_config_ulong(const char *name, const char *value)
 {
        unsigned long ret;
        if (!git_parse_ulong(value, &ret))
-               die_bad_config(name);
+               die_bad_number(name, value);
        return ret;
 }
 
@@ -519,10 +631,10 @@ static int git_config_maybe_bool_text(const char *name, const char *value)
 
 int git_config_maybe_bool(const char *name, const char *value)
 {
-       long v = git_config_maybe_bool_text(name, value);
+       int v = git_config_maybe_bool_text(name, value);
        if (0 <= v)
                return v;
-       if (git_parse_long(value, &v))
+       if (git_parse_int(value, &v))
                return !!v;
        return -1;
 }
@@ -558,7 +670,7 @@ int git_config_pathname(const char **dest, const char *var, const char *value)
                return config_error_nonbool(var);
        *dest = expand_user_path(value);
        if (!*dest)
-               die("Failed to expand user dir in: '%s'", value);
+               die(_("failed to expand user dir in: '%s'"), value);
        return 0;
 }
 
@@ -573,6 +685,12 @@ static int git_default_core_config(const char *var, const char *value)
                trust_ctime = git_config_bool(var, value);
                return 0;
        }
+       if (!strcmp(var, "core.checkstat")) {
+               if (!strcasecmp(value, "default"))
+                       check_stat = 1;
+               else if (!strcasecmp(value, "minimal"))
+                       check_stat = 0;
+       }
 
        if (!strcmp(var, "core.quotepath")) {
                quote_path_fully = git_config_bool(var, value);
@@ -630,7 +748,7 @@ static int git_default_core_config(const char *var, const char *value)
                if (level == -1)
                        level = Z_DEFAULT_COMPRESSION;
                else if (level < 0 || level > Z_BEST_COMPRESSION)
-                       die("bad zlib compression level %d", level);
+                       die(_("bad zlib compression level %d"), level);
                zlib_compression_level = level;
                zlib_compression_seen = 1;
                return 0;
@@ -641,7 +759,7 @@ static int git_default_core_config(const char *var, const char *value)
                if (level == -1)
                        level = Z_DEFAULT_COMPRESSION;
                else if (level < 0 || level > Z_BEST_COMPRESSION)
-                       die("bad zlib compression level %d", level);
+                       die(_("bad zlib compression level %d"), level);
                core_compression_level = level;
                core_compression_seen = 1;
                if (!zlib_compression_seen)
@@ -676,9 +794,6 @@ static int git_default_core_config(const char *var, const char *value)
                return 0;
        }
 
-       if (!strcmp(var, "core.logpackaccess"))
-               return git_config_string(&log_pack_access, var, value);
-
        if (!strcmp(var, "core.autocrlf")) {
                if (value && !strcasecmp(value, "input")) {
                        if (core_eol == EOL_CRLF)
@@ -724,6 +839,19 @@ static int git_default_core_config(const char *var, const char *value)
        if (!strcmp(var, "core.editor"))
                return git_config_string(&editor_program, var, value);
 
+       if (!strcmp(var, "core.commentchar")) {
+               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"))
                return git_config_string(&askpass_program, var, value);
 
@@ -753,7 +881,7 @@ static int git_default_core_config(const char *var, const char *value)
                else if (!strcmp(value, "link"))
                        object_creation_mode = OBJECT_CREATION_USES_HARDLINKS;
                else
-                       die("Invalid mode for object creation: %s", value);
+                       die(_("invalid mode for object creation: %s"), value);
                return 0;
        }
 
@@ -762,25 +890,8 @@ static int git_default_core_config(const char *var, const char *value)
                return 0;
        }
 
-       /* Add other config variables here and to Documentation/config.txt. */
-       return 0;
-}
-
-static int git_default_user_config(const char *var, const char *value)
-{
-       if (!strcmp(var, "user.name")) {
-               if (!value)
-                       return config_error_nonbool(var);
-               strlcpy(git_default_name, value, sizeof(git_default_name));
-               user_ident_explicitly_given |= IDENT_NAME_GIVEN;
-               return 0;
-       }
-
-       if (!strcmp(var, "user.email")) {
-               if (!value)
-                       return config_error_nonbool(var);
-               strlcpy(git_default_email, value, sizeof(git_default_email));
-               user_ident_explicitly_given |= IDENT_MAIL_GIVEN;
+       if (!strcmp(var, "core.precomposeunicode")) {
+               precomposed_unicode = git_config_bool(var, value);
                return 0;
        }
 
@@ -839,6 +950,8 @@ static int git_default_push_config(const char *var, const char *value)
                        push_default = PUSH_DEFAULT_NOTHING;
                else if (!strcmp(value, "matching"))
                        push_default = PUSH_DEFAULT_MATCHING;
+               else if (!strcmp(value, "simple"))
+                       push_default = PUSH_DEFAULT_SIMPLE;
                else if (!strcmp(value, "upstream"))
                        push_default = PUSH_DEFAULT_UPSTREAM;
                else if (!strcmp(value, "tracking")) /* deprecated */
@@ -847,8 +960,8 @@ static int git_default_push_config(const char *var, const char *value)
                        push_default = PUSH_DEFAULT_CURRENT;
                else {
                        error("Malformed value for %s: %s", var, value);
-                       return error("Must be one of nothing, matching, "
-                                    "tracking or current.");
+                       return error("Must be one of nothing, matching, simple, "
+                                    "upstream or current.");
                }
                return 0;
        }
@@ -860,7 +973,9 @@ static int git_default_push_config(const char *var, const char *value)
 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);
 
        /* Add other config variables here and to Documentation/config.txt. */
        return 0;
@@ -868,25 +983,25 @@ static int git_default_mailmap_config(const char *var, const char *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."))
-               return git_default_user_config(var, value);
+       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")) {
@@ -902,35 +1017,120 @@ int git_default_config(const char *var, const char *value, void *dummy)
        return 0;
 }
 
-int git_config_from_file(config_fn_t fn, const char *filename, void *data)
+/*
+ * All source specific fields in the union, die_on_error, name and the callbacks
+ * fgetc, ungetc, ftell of top need to be initialized before calling
+ * this function.
+ */
+static int do_config_from(struct config_source *top, config_fn_t fn, void *data)
 {
        int ret;
-       FILE *f = fopen(filename, "r");
 
-       ret = -1;
-       if (f) {
-               config_file top;
+       /* push config-file parsing state stack */
+       top->prev = cf;
+       top->linenr = 1;
+       top->eof = 0;
+       strbuf_init(&top->value, 1024);
+       strbuf_init(&top->var, 1024);
+       cf = top;
 
-               /* push config-file parsing state stack */
-               top.prev = cf;
-               top.f = f;
-               top.name = filename;
-               top.linenr = 1;
-               top.eof = 0;
-               strbuf_init(&top.value, 1024);
-               cf = &top;
+       ret = git_parse_source(fn, data);
 
-               ret = git_parse_file(fn, data);
+       /* pop config-file parsing state stack */
+       strbuf_release(&top->value);
+       strbuf_release(&top->var);
+       cf = top->prev;
 
-               /* pop config-file parsing state stack */
-               strbuf_release(&top.value);
-               cf = top.prev;
+       return ret;
+}
+
+static int do_config_from_file(config_fn_t fn,
+               const char *name, const char *path, FILE *f, void *data)
+{
+       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;
+
+       return 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;
 }
 
+int git_config_from_buf(config_fn_t fn, const char *name, const char *buf,
+                       size_t len, void *data)
+{
+       struct config_source top;
+
+       top.u.buf.buf = buf;
+       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;
+       top.do_ftell = config_buf_ftell;
+
+       return do_config_from(&top, fn, data);
+}
+
+static int git_config_from_blob_sha1(config_fn_t fn,
+                                    const char *name,
+                                    const unsigned char *sha1,
+                                    void *data)
+{
+       enum object_type type;
+       char *buf;
+       unsigned long size;
+       int ret;
+
+       buf = read_sha1_file(sha1, &type, &size);
+       if (!buf)
+               return error("unable to load config blob object '%s'", name);
+       if (type != OBJ_BLOB) {
+               free(buf);
+               return error("reference '%s' does not point to a blob", name);
+       }
+
+       ret = git_config_from_buf(fn, name, buf, size, data);
+       free(buf);
+
+       return ret;
+}
+
+static int git_config_from_blob_ref(config_fn_t fn,
+                                   const char *name,
+                                   void *data)
+{
+       unsigned char sha1[20];
+
+       if (get_sha1(name, sha1) < 0)
+               return error("unable to resolve config blob '%s'", name);
+       return git_config_from_blob_sha1(fn, name, sha1, data);
+}
+
 const char *git_etc_gitconfig(void)
 {
        static const char *system_wide;
@@ -939,12 +1139,28 @@ const char *git_etc_gitconfig(void)
        return system_wide;
 }
 
+/*
+ * Parse environment variable 'k' as a boolean (in various
+ * possible spellings); if missing, use the default value 'def'.
+ */
 int git_env_bool(const char *k, int def)
 {
        const char *v = getenv(k);
        return v ? git_config_bool(k, v) : def;
 }
 
+/*
+ * Parse environment variable 'k' as ulong with possibly a unit
+ * suffix; if missing, use the default value 'val'.
+ */
+unsigned long git_env_ulong(const char *k, unsigned long val)
+{
+       const char *v = getenv(k);
+       if (v && !git_parse_ulong(v, &val))
+               die("failed to parse %s", k);
+       return val;
+}
+
 int git_config_system(void)
 {
        return !git_env_bool("GIT_CONFIG_NOSYSTEM", 0);
@@ -953,32 +1169,35 @@ int git_config_system(void)
 int git_config_early(config_fn_t fn, void *data, const char *repo_config)
 {
        int ret = 0, found = 0;
-       const char *home = NULL;
+       char *xdg_config = NULL;
+       char *user_config = NULL;
+
+       home_config_paths(&user_config, &xdg_config, "config");
 
-       if (git_config_system() && !access(git_etc_gitconfig(), R_OK)) {
+       if (git_config_system() && !access_or_die(git_etc_gitconfig(), R_OK, 0)) {
                ret += git_config_from_file(fn, git_etc_gitconfig(),
                                            data);
                found += 1;
        }
 
-       home = getenv("HOME");
-       if (home) {
-               char buf[PATH_MAX];
-               char *user_config = mksnpath(buf, sizeof(buf), "%s/.gitconfig", home);
-               if (!access(user_config, R_OK)) {
-                       ret += git_config_from_file(fn, user_config, data);
-                       found += 1;
-               }
+       if (xdg_config && !access_or_die(xdg_config, R_OK, ACCESS_EACCES_OK)) {
+               ret += git_config_from_file(fn, xdg_config, data);
+               found += 1;
        }
 
-       if (repo_config && !access(repo_config, R_OK)) {
+       if (user_config && !access_or_die(user_config, R_OK, ACCESS_EACCES_OK)) {
+               ret += git_config_from_file(fn, user_config, data);
+               found += 1;
+       }
+
+       if (repo_config && !access_or_die(repo_config, R_OK, 0)) {
                ret += git_config_from_file(fn, repo_config, data);
                found += 1;
        }
 
        switch (git_config_from_parameters(fn, data)) {
        case -1: /* error */
-               die("unable to parse command-line config");
+               die(_("unable to parse command-line config"));
                break;
        case 0: /* found nothing */
                break;
@@ -987,11 +1206,14 @@ int git_config_early(config_fn_t fn, void *data, const char *repo_config)
                break;
        }
 
+       free(xdg_config);
+       free(user_config);
        return ret == 0 ? found : ret;
 }
 
 int git_config_with_options(config_fn_t fn, void *data,
-                           const char *filename, int respect_includes)
+                           struct git_config_source *config_source,
+                           int respect_includes)
 {
        char *repo_config = NULL;
        int ret;
@@ -1008,8 +1230,12 @@ int git_config_with_options(config_fn_t fn, void *data,
         * If we have a specific filename, use it. Otherwise, follow the
         * regular lookup sequence.
         */
-       if (filename)
-               return git_config_from_file(fn, filename, 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);
@@ -1018,53 +1244,412 @@ int git_config_with_options(config_fn_t fn, void *data,
        return ret;
 }
 
-int git_config(config_fn_t fn, void *data)
+static void git_config_raw(config_fn_t fn, void *data)
+{
+       if (git_config_with_options(fn, data, NULL, 1) < 0)
+               /*
+                * git_config_with_options() normally returns only
+                * positive values, as most errors are fatal, and
+                * non-fatal potential errors are guarded by "if"
+                * statements that are entered only when no error is
+                * possible.
+                *
+                * If we ever encounter a non-fatal error, it means
+                * something went really wrong and we should stop
+                * immediately.
+                */
+               die(_("unknown error occured while reading the configuration files"));
+}
+
+static void configset_iter(struct config_set *cs, config_fn_t fn, void *data)
+{
+       int i, value_index;
+       struct string_list *values;
+       struct config_set_element *entry;
+       struct configset_list *list = &cs->list;
+       struct key_value_info *kv_info;
+
+       for (i = 0; i < list->nr; i++) {
+               entry = list->items[i].e;
+               value_index = list->items[i].value_index;
+               values = &entry->value_list;
+               if (fn(entry->key, values->items[value_index].string, data) < 0) {
+                       kv_info = values->items[value_index].util;
+                       git_die_config_linenr(entry->key, kv_info->filename, kv_info->linenr);
+               }
+       }
+}
+
+static void git_config_check_init(void);
+
+void git_config(config_fn_t fn, void *data)
+{
+       git_config_check_init();
+       configset_iter(&the_config_set, fn, data);
+}
+
+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;
+       struct string_list_item *si;
+       struct configset_list_item *l_item;
+       struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
+
+       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);
+       }
+       si = string_list_append_nodup(&e->value_list, value ? xstrdup(value) : NULL);
+
+       ALLOC_GROW(cs->list.items, cs->list.nr + 1, cs->list.alloc);
+       l_item = &cs->list.items[cs->list.nr++];
+       l_item->e = e;
+       l_item->value_index = e->value_list.nr - 1;
+
+       if (cf) {
+               kv_info->filename = strintern(cf->name);
+               kv_info->linenr = cf->linenr;
+       } else {
+               /* for values read from `git_config_from_parameters()` */
+               kv_info->filename = NULL;
+               kv_info->linenr = -1;
+       }
+       si->util = kv_info;
+
+       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;
+       cs->list.nr = 0;
+       cs->list.alloc = 0;
+       cs->list.items = NULL;
+}
+
+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, 1);
+       }
+       hashmap_free(&cs->config_hash, 1);
+       cs->hash_initialized = 0;
+       free(cs->list.items);
+       cs->list.nr = 0;
+       cs->list.alloc = 0;
+       cs->list.items = NULL;
+}
+
+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_raw(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)
+{
+       int ret;
+       git_config_check_init();
+       ret = git_configset_get_string_const(&the_config_set, key, dest);
+       if (ret < 0)
+               git_die_config(key, NULL);
+       return ret;
+}
+
+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)
+{
+       int ret;
+       git_config_check_init();
+       ret = git_configset_get_pathname(&the_config_set, key, dest);
+       if (ret < 0)
+               git_die_config(key, NULL);
+       return ret;
+}
+
+NORETURN
+void git_die_config_linenr(const char *key, const char *filename, int linenr)
 {
-       return git_config_with_options(fn, data, NULL, 1);
+       if (!filename)
+               die(_("unable to parse '%s' from command-line config"), key);
+       else
+               die(_("bad config variable '%s' in file '%s' at line %d"),
+                   key, filename, linenr);
+}
+
+NORETURN __attribute__((format(printf, 2, 3)))
+void git_die_config(const char *key, const char *err, ...)
+{
+       const struct string_list *values;
+       struct key_value_info *kv_info;
+
+       if (err) {
+               va_list params;
+               va_start(params, err);
+               vreportf("error: ", err, params);
+               va_end(params);
+       }
+       values = git_config_get_value_multi(key);
+       kv_info = values->items[values->nr - 1].util;
+       git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
 }
 
 /*
  * 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;
 
 static int matches(const char *key, const char *value)
 {
-       return !strcmp(key, store.key) &&
-               (store.value_regex == NULL ||
-                (store.do_not_match ^
-                 !regexec(store.value_regex, value, 0, NULL, 0)));
+       if (strcmp(key, store.key))
+               return 0; /* not ours */
+       if (!store.value_regex)
+               return 1; /* always matches */
+       if (store.value_regex == CONFIG_REGEX_NONE)
+               return 0; /* never matches */
+
+       return store.do_not_match ^
+               (value && !regexec(store.value_regex, value, 0, NULL, 0));
 }
 
 static int store_aux(const char *key, const char *value, void *cb)
 {
        const char *ep;
        size_t section_len;
-       FILE *f = cf->f;
 
        switch (store.state) {
        case KEY_SEEN:
                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;
+                               warning(_("%s has multiple values"), key);
                        }
 
-                       store.offset[store.seen] = ftell(f);
+                       ALLOC_GROW(store.offset, store.seen + 1,
+                                  store.offset_alloc);
+
+                       store.offset[store.seen] = cf->do_ftell(cf);
                        store.seen++;
                }
                break;
@@ -1091,19 +1676,26 @@ static int store_aux(const char *key, const char *value, void *cb)
                 * Do not increment matches: this is no match, but we
                 * just made sure we are in the desired section.
                 */
-               store.offset[store.seen] = ftell(f);
+               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)) {
-                       store.offset[store.seen] = ftell(f);
+                       ALLOC_GROW(store.offset, store.seen + 1,
+                                  store.offset_alloc);
+                       store.offset[store.seen] = cf->do_ftell(cf);
                        store.state = KEY_SEEN;
                        store.seen++;
                } else {
                        if (strrchr(key, '.') - key == store.baselen &&
                              !strncmp(key, store.key, store.baselen)) {
                                        store.state = SECTION_SEEN;
-                                       store.offset[store.seen] = ftell(f);
+                                       ALLOC_GROW(store.offset,
+                                                  store.seen + 1,
+                                                  store.offset_alloc);
+                                       store.offset[store.seen] = cf->do_ftell(cf);
                        }
                }
        }
@@ -1294,12 +1886,15 @@ int git_config_parse_key(const char *key, char **store_key, int *baselen_)
 
 out_free_ret_1:
        free(*store_key);
+       *store_key = NULL;
        return -CONFIG_INVALID_KEY;
 }
 
 /*
  * If value==NULL, unset in (remove from) config,
  * if value_regex!=NULL, disregard key/value pairs where value does not match.
+ * if value_regex==CONFIG_REGEX_NONE, do not match any existing values
+ *     (only add a new one)
  * if multi_replace==0, nothing, or only one matching key/value is replaced,
  *     else all matching key/values (regardless how many) are removed,
  *     before the new pair is written.
@@ -1343,7 +1938,7 @@ int git_config_set_multivar_in_file(const char *config_filename,
         * 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));
@@ -1383,6 +1978,8 @@ int git_config_set_multivar_in_file(const char *config_filename,
 
                if (value_regex == NULL)
                        store.value_regex = NULL;
+               else if (value_regex == CONFIG_REGEX_NONE)
+                       store.value_regex = CONFIG_REGEX_NONE;
                else {
                        if (value_regex[0] == '!') {
                                store.do_not_match = 1;
@@ -1400,6 +1997,7 @@ int git_config_set_multivar_in_file(const char *config_filename,
                        }
                }
 
+               ALLOC_GROW(store.offset, 1, store.offset_alloc);
                store.offset[0] = 0;
                store.state = START;
                store.seen = 0;
@@ -1413,7 +2011,8 @@ int git_config_set_multivar_in_file(const char *config_filename,
                if (git_config_from_file(store_aux, config_filename, NULL)) {
                        error("invalid config file %s", config_filename);
                        free(store.key);
-                       if (store.value_regex != NULL) {
+                       if (store.value_regex != NULL &&
+                           store.value_regex != CONFIG_REGEX_NONE) {
                                regfree(store.value_regex);
                                free(store.value_regex);
                        }
@@ -1422,7 +2021,8 @@ int git_config_set_multivar_in_file(const char *config_filename,
                }
 
                free(store.key);
-               if (store.value_regex != NULL) {
+               if (store.value_regex != NULL &&
+                   store.value_regex != CONFIG_REGEX_NONE) {
                        regfree(store.value_regex);
                        free(store.value_regex);
                }
@@ -1440,6 +2040,13 @@ int git_config_set_multivar_in_file(const char *config_filename,
                        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;
 
@@ -1504,6 +2111,9 @@ int git_config_set_multivar_in_file(const char *config_filename,
        lock = NULL;
        ret = 0;
 
+       /* Invalidate the config cache */
+       git_config_clear();
+
 out_free:
        if (lock)
                rollback_lock_file(lock);
@@ -1562,20 +2172,43 @@ static int section_name_match (const char *buf, const char *name)
        return 0;
 }
 
+static int section_name_is_ok(const char *name)
+{
+       /* Empty section names are bogus. */
+       if (!*name)
+               return 0;
+
+       /*
+        * Before a dot, we must be alphanumeric or dash. After the first dot,
+        * anything goes, so we can stop checking.
+        */
+       for (; *name && *name != '.'; name++)
+               if (*name != '-' && !isalnum(*name))
+                       return 0;
+       return 1;
+}
+
 /* if new_name == NULL, the section is removed instead */
 int git_config_rename_section_in_file(const char *config_filename,
                                      const char *old_name, const char *new_name)
 {
        int ret = 0, remove = 0;
        char *filename_buf = NULL;
-       struct lock_file *lock = xcalloc(sizeof(struct lock_file), 1);
+       struct lock_file *lock;
        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);
+               goto out;
+       }
 
        if (!config_filename)
                config_filename = filename_buf = git_pathdup("config");
 
+       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);
@@ -1587,6 +2220,14 @@ int git_config_rename_section_in_file(const char *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;
@@ -1652,7 +2293,41 @@ int git_config_rename_section(const char *old_name, const char *new_name)
  * Call this to report error for your variable that should not
  * get a boolean value (i.e. "[my] var" means "true").
  */
+#undef config_error_nonbool
 int config_error_nonbool(const char *var)
 {
        return error("Missing value for '%s'", var);
 }
+
+int parse_config_key(const char *var,
+                    const char *section,
+                    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] != '.')
+               return -1;
+
+       /*
+        * Find the key; we don't know yet if we have a subsection, but we must
+        * parse backwards from the end, since the subsection may have dots in
+        * it, too.
+        */
+       dot = strrchr(var, '.');
+       *key = dot + 1;
+
+       /* Did we have a subsection at all? */
+       if (dot == var + section_len) {
+               *subsection = NULL;
+               *subsection_len = 0;
+       }
+       else {
+               *subsection = var + section_len + 1;
+               *subsection_len = dot - *subsection;
+       }
+
+       return 0;
+}