{"thread":{"id":"58696","subject":"[PATCH 00/10] config API: make \"multi\" safe, fix numerous segfaults","startedAt":"2022-10-26T15:36:42Z","lastAt":"2023-04-07T15:51:39Z","messageCount":134,"participants":["Ævar Arnfjörð Bjarmason","SZEDER Gábor","Junio C Hamano","Taylor Blau","Glen Choo"],"isPatch":true,"patchVersion":1,"patchTotal":10},"messages":[{"id":"465794","messageId":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":null,"subject":"[PATCH 00/10] config API: make \"multi\" safe, fix numerous segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:13Z","receivedAt":"2022-10-26T15:36:42Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This series is a follow-up to an earlier RFC Stolee sent about making\nthe *_multi() config API return non-NULL, and instead give you an\nempty string list[1].\n\nI also think that part of the config API is a wart, but that we should\ngo for a different solution. It's the only config function that\ndoesn't return an \"int\" indicating whether we found the key.\n\nCode that wants to use the values should then check that return\nvalue. I.e. Stolee's version allows you to do:\n\n\tconst struct string_list *list = git_config_get_value_multi(key);\n\tfor_each_string_list_item(item, list) { ... found = 1 ... }\n\nWhereas in this proposal we instead do (same as for non-multi):\n\n\tif (!git_config_get_const_value_multi(key, &list))\n\t\tfor_each_string_list_item(item, list) { ... found = 1 ... }\n\nMid-series that's made nicer by adjusting the string_list API to have\nsensible \"const\"'s (so we don't need catsing), and using utility\nfunctions from there. I.e. the recently added code in builtin/gc.c\nbecomes (Stolee's at [3]):\n\n\tif (!git_config_get_knownkey_value_multi(key, &list))\n\t\tfound = unsorted_string_list_has_string(list, maintpath);\n\nBut anyway.\n\nOnce I started poking at this approach I discovered that we have a\nmuch larger issue here than whether the top-level value is NULL or an empty list.\n\nAs noted I don't think it's an issue that the *_multi() returns NULL\nif we have no key, that's easy to handle.\n\nBut *_multi() doesn't have the equivalent of a wrapper that coerces\nthe values on the list into one of our types (as in \"git config\n--type=<type>\").\n\nA not so well known edge case in our config format (see 8/10) is that\nvalue-less keys are represented as NULL's, and a \"struct string_list\"\nis perfectly happy to have a \"char *string\" member that's NULL.\n\nI.e.:\n\n\t[a]key=x\n\t[a]key\n\t[a]key=y\n\nIs represented as:\n\n\t{ \"x\", NULL, \"y\" }\n\nNot, as existing code apparently expected:\n\n\t{ \"x\", \"\", \"y\" }\n\nAs a result existing code using *_multi() would segfault when reading\nconfig like that. See 9/10, which fixes segfaults in 6 commits as old\nas from 2015, and a couple of recent ones: One in the last release,\nand one on \"master\" but not released yet.\n\nThe fix is thoroughly boring, we just start doing for *_multi() what\nwe've been doing for other config since Junio's 2008 fix (see 9/10) to\nfix the same issue for non-multi config variables.\n\nI.e. we provide a safer \"I want strings, please\" variant of the API,\njust for *_multi(). At the culmination of this topic only the\ntest-helper uses the underlying unsafe API, and only because it needs\nto check that we're still parsing the config correctly.\n\n1. https://lore.kernel.org/git/pull.1369.git.1664287711.gitgitgadget@gmail.com/\n2. https://lore.kernel.org/git/220928.868rm3w9d4.gmgdl@evledraar.gmail.com\n3. https://lore.kernel.org/git/e06cb4df081bc2222731f9185a22ed7ad67e3814.1664287711.git.gitgitgadget@gmail.com/\n\nÆvar Arnfjörð Bjarmason (10):\n  config API: have *_multi() return an \"int\" and take a \"dest\"\n  for-each-repo: error on bad --config\n  config API: mark *_multi() with RESULT_MUST_BE_USED\n  string-list API: mark \"struct_string_list\" to \"for_each_string_list\"\n    const\n  string-list API: make has_string() and list_lookup() \"const\"\n  builtin/gc.c: use \"unsorted_string_list_has_string()\" where\n    appropriate\n  config API: add and use \"lookup_value\" functions\n  config tests: add \"NULL\" tests for *_get_value_multi()\n  config API: add \"string\" version of *_value_multi(), fix segfaults\n  for-each-repo: with bad config, don't conflate <path> and <cmd>\n\n builtin/for-each-repo.c        |  14 ++-\n builtin/gc.c                   |  29 +-----\n builtin/log.c                  |   6 +-\n builtin/submodule--helper.c    |   7 +-\n builtin/worktree.c             |   3 +-\n config.c                       | 164 +++++++++++++++++++++++++++++----\n config.h                       | 110 ++++++++++++++++++++--\n pack-bitmap.c                  |   7 +-\n string-list.c                  |   6 +-\n string-list.h                  |   6 +-\n submodule.c                    |   3 +-\n t/helper/test-config.c         |   6 +-\n t/t0068-for-each-repo.sh       |  19 ++++\n t/t1308-config-set.sh          |  30 ++++++\n t/t4202-log.sh                 |  15 +++\n t/t5310-pack-bitmaps.sh        |  21 +++++\n t/t7004-tag.sh                 |  17 ++++\n t/t7413-submodule-is-active.sh |  16 ++++\n t/t7900-maintenance.sh         |  38 ++++++++\n versioncmp.c                   |  18 +++-\n 20 files changed, 451 insertions(+), 84 deletions(-)\n\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465795","messageId":"patch-01.10-eefa253ab1f-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH 01/10] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:14Z","receivedAt":"2022-10-26T15:36:44Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"The git_configset_get_value_multi() function added in 3c8687a73ee (add\n`config_set` API for caching config-like files, 2014-07-28) is a\nfundamental part of of the config API, and\ne.g. \"git_config_get_value()\" and others are implemented in terms of\nit.\n\nBut it has had the limitation that configset_find_element() calls\ngit_config_parse_key(), but then throws away the distinction between a\n\"ret < 1\" return value from it, and return values that indicate a key\ndoesn't exist. As a result the git_config_get_value_multi() function\nwould either return a \"const struct string_list *\", or NULL.\n\nBy changing the *_multi() function to return an \"int\" for the status\nand to write to a \"const struct string_list **dest\" parameter we can\navoid losing this information. API callers can now do:\n\n\tconst struct string_list *dest;\n\tint ret;\n\n\tret = git_config_get_value_multi(key, &dest);\n\tif (ret < 1)\n\t\tdie(\"bad key: %s\", key);\n\telse if (ret)\n\t\t; /* key does not exist */\n\telse\n\t\t; /* got key, can use \"dest\" */\n\nA \"get_knownkey_value_multi\" variant is also provided, which will\nBUG() out in the \"ret < 1\" case. This is useful in the cases where we\nhardcode the keyname in our source code, and therefore use the more\nidiomatic pattern of:\n\n\tif (!git_config_get_value_multi(key, &dest)\n\t\t; /* got key, can use \"dest\" */\n\telse\n\t\t; /* key does not exist */\n\nThe \"knownkey\" name was picked instead of e.g. \"const\" to avoid a\nrepeat of the issues noted in f1de981e8b6 (config: fix leaks from\ngit_config_get_string_const(), 2020-08-14) and 9a53219f69b (config:\ndrop git_config_get_string_const(), 2020-08-17). API users might think\nthat \"const\" means that the value(s) don't need to be free'd.\n\nAs noted in commentary here we treat git_die_config() as a\nspecial-case, i.e. we assume that a value we're complaining about has\nalready had its key pass the git_config_parse_key() check.\n\nLikewise we consider the keys passed to \"t/helper/test-config.c\" to be\n\"knownkey\", and will emit a BUG() if they don't pass\ngit_config_parse_key(). Those will come from our *.sh tests, so\nthey're also \"known keys\" coming from our sources.\n\nA logical follow-up to this would be to change the various \"*_get_*()\"\nfunctions to ferry the git_configset_get_value() return value to their\nown callers, e.g.:\n\n\tdiff --git a/config.c b/config.c\n\tindex 094ad899e0b..7e8ee4cfec1 100644\n\t--- a/config.c\n\t+++ b/config.c\n\t@@ -2479,11 +2479,14 @@ static int git_configset_get_string_tmp(struct config_set *cs, const char *key,\n\t int git_configset_get_int(struct config_set *cs, const char *key, int *dest)\n\t {\n\t \tconst char *value;\n\t-\tif (!git_configset_get_value(cs, key, &value)) {\n\t-\t\t*dest = git_config_int(key, value);\n\t-\t\treturn 0;\n\t-\t} else\n\t-\t\treturn 1;\n\t+\tint ret;\n\t+\n\t+\tif ((ret = git_configset_get_value(cs, key, &value)))\n\t+\t\tgoto done;\n\t+\n\t+\t*dest = git_config_int(key, value);\n\t+done:\n\t+\treturn ret;\n\t }\n\n\t int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest)\n\nMost of those callers don't care, and call those functions as\n\"if (!func(...))\", but if they do they'll be able to tell key\nnon-existence from errors we encounter. Before this change those API\nusers would have been unable to tell the two conditions apart, as\ngit_configset_get_value() hid the difference.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c     |  5 +-\n builtin/gc.c                |  6 +--\n builtin/log.c               |  6 +--\n builtin/submodule--helper.c |  6 ++-\n config.c                    | 94 ++++++++++++++++++++++++++++++-------\n config.h                    | 52 ++++++++++++++++----\n pack-bitmap.c               |  7 ++-\n submodule.c                 |  3 +-\n t/helper/test-config.c      |  6 +--\n versioncmp.c                | 10 ++--\n 10 files changed, 148 insertions(+), 47 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex fd86e5a8619..b01721762ef 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -28,7 +28,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n {\n \tstatic const char *config_key = NULL;\n \tint i, result = 0;\n-\tconst struct string_list *values;\n+\tconst struct string_list *values = NULL;\n \n \tconst struct option options[] = {\n \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n@@ -42,8 +42,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\tvalues = repo_config_get_value_multi(the_repository,\n-\t\t\t\t\t     config_key);\n+\trepo_config_get_value_multi(the_repository, config_key, &values);\n \n \t/*\n \t * Do nothing on an empty list, which is equivalent to the case\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 243ee85d283..04c48638ef4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1485,8 +1485,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \telse\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tlist = git_config_get_value_multi(key);\n-\tif (list) {\n+\tif (!git_config_get_knownkey_value_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1542,8 +1541,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tusage_with_options(builtin_maintenance_unregister_usage,\n \t\t\t\t   options);\n \n-\tlist = git_config_get_value_multi(key);\n-\tif (list) {\n+\tif (!git_config_get_knownkey_value_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex ee19dc5d450..75464c96ccf 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tint i;\n \tchar *value = NULL;\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n-\tconst struct string_list *config_exclude =\n-\t\t\tgit_config_get_value_multi(\"log.excludeDecoration\");\n+\tconst struct string_list *config_exclude;\n \n-\tif (config_exclude) {\n+\tif (!git_config_get_knownkey_value_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t      &config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 0b4acb442b2..1f8fe6a8e0d 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -541,6 +541,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t\tNULL\n \t};\n \tint ret = 1;\n+\tconst struct string_list *values;\n \n \targc = parse_options(argc, argv, prefix, module_init_options,\n \t\t\t     git_submodule_helper_usage, 0);\n@@ -552,7 +553,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t * If there are no path args and submodule.active is set then,\n \t * by default, only initialize 'active' modules.\n \t */\n-\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n \t\tmodule_list_active(&list);\n \n \tinfo.prefix = prefix;\n@@ -2708,6 +2709,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \tif (opt.init) {\n \t\tstruct module_list list = MODULE_LIST_INIT;\n \t\tstruct init_cb info = INIT_CB_INIT;\n+\t\tconst struct string_list *values;\n \n \t\tif (module_list_compute(argc, argv, opt.prefix,\n \t\t\t\t\t&pathspec2, &list) < 0) {\n@@ -2720,7 +2722,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \t\t * If there are no path args and submodule.active is set then,\n \t\t * by default, only initialize 'active' modules.\n \t\t */\n-\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n \t\t\tmodule_list_active(&list);\n \n \t\tinfo.prefix = opt.prefix;\ndiff --git a/config.c b/config.c\nindex cbb5a3bab74..2100b29b689 100644\n--- a/config.c\n+++ b/config.c\n@@ -2275,23 +2275,28 @@ void read_very_early_config(config_fn_t cb, void *data)\n \tconfig_with_options(cb, data, NULL, &opts);\n }\n \n-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n+static int configset_find_element(struct config_set *cs, const char *key,\n+\t\t\t\t  struct config_set_element **dest)\n {\n \tstruct config_set_element k;\n \tstruct config_set_element *found_entry;\n \tchar *normalized_key;\n+\tint ret;\n+\n \t/*\n \t * `key` may come from the user, so normalize it before using it\n \t * for querying entries from the hashmap.\n \t */\n-\tif (git_config_parse_key(key, &normalized_key, NULL))\n-\t\treturn NULL;\n+\tret = git_config_parse_key(key, &normalized_key, NULL);\n+\tif (ret < 0)\n+\t\treturn ret;\n \n \thashmap_entry_init(&k.ent, strhash(normalized_key));\n \tk.key = normalized_key;\n \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n \tfree(normalized_key);\n-\treturn found_entry;\n+\t*dest = found_entry;\n+\treturn 0;\n }\n \n static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n@@ -2300,8 +2305,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n \tstruct string_list_item *si;\n \tstruct configset_list_item *l_item;\n \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n+\tint ret;\n \n-\te = configset_find_element(cs, key);\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret < 0)\n+\t\treturn ret;\n \t/*\n \t * Since the keys are being fed by git_config*() callback mechanism, they\n \t * are already normalized. So simply add them without any further munging.\n@@ -2400,24 +2408,54 @@ int git_configset_add_parameters(struct config_set *cs)\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n+\tint ret;\n+\n \t/*\n \t * Follows \"last one wins\" semantic, i.e., if there are multiple matches for the\n \t * queried key in the files of the configset, the value returned will be the last\n \t * value in the value list for that key.\n \t */\n-\tvalues = git_configset_get_value_multi(cs, key);\n+\tret = git_configset_get_value_multi(cs, key, &values);\n \n-\tif (!values)\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!values)\n \t\treturn 1;\n \tassert(values->nr > 0);\n \t*value = values->items[values->nr - 1].string;\n \treturn 0;\n }\n \n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n+static int git_configset_get_value_multi_1(struct config_set *cs, const char *key,\n+\t\t\t\t\t   const struct string_list **dest,\n+\t\t\t\t\t   int knownkey)\n {\n-\tstruct config_set_element *e = configset_find_element(cs, key);\n-\treturn e ? &e->value_list : NULL;\n+\tstruct config_set_element *e;\n+\tint ret;\n+\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret < 0 && knownkey)\n+\t\tBUG(\"*_get_knownkey_*() only accepts known-good (hardcoded) keys, but '%s' is bad!\", key);\n+\telse if (ret < 0)\n+\t\treturn ret;\n+\telse if (!e)\n+\t\treturn 1;\n+\t*dest = &e->value_list;\n+\n+\treturn 0;\n+}\n+\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest)\n+{\n+\treturn git_configset_get_value_multi_1(cs, key, dest, 0);\n+}\n+\n+int git_configset_get_knownkey_value_multi(struct config_set *cs,\n+\t\t\t\t\t   const char *const key,\n+\t\t\t\t\t   const struct string_list **dest)\n+{\n+\treturn git_configset_get_value_multi_1(cs, key, dest, 1);\n }\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n@@ -2563,11 +2601,20 @@ int repo_config_get_value(struct repository *repo,\n \treturn git_configset_get_value(repo->config, key, value);\n }\n \n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key)\n+int repo_config_get_value_multi(struct repository *repo,\n+\t\t\t\tconst char *key,\n+\t\t\t\tconst struct string_list **dest)\n {\n \tgit_config_check_init(repo);\n-\treturn git_configset_get_value_multi(repo->config, key);\n+\treturn git_configset_get_value_multi(repo->config, key, dest);\n+}\n+\n+int repo_config_get_knownkey_value_multi(struct repository *repo,\n+\t\t\t\t\t const char *const key,\n+\t\t\t\t\t const struct string_list **dest)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_knownkey_value_multi(repo->config, key, dest);\n }\n \n int repo_config_get_string(struct repository *repo,\n@@ -2684,9 +2731,15 @@ int git_config_get_value(const char *key, const char **value)\n \treturn repo_config_get_value(the_repository, key, value);\n }\n \n-const struct string_list *git_config_get_value_multi(const char *key)\n+int git_config_get_value_multi(const char *key, const struct string_list **dest)\n+{\n+\treturn repo_config_get_value_multi(the_repository, key, dest);\n+}\n+\n+int git_config_get_knownkey_value_multi(const char *const key,\n+\t\t\t\t\tconst struct string_list **dest)\n {\n-\treturn repo_config_get_value_multi(the_repository, key);\n+\treturn repo_config_get_knownkey_value_multi(the_repository, key, dest);\n }\n \n int git_config_get_string(const char *key, char **dest)\n@@ -2833,7 +2886,16 @@ void git_die_config(const char *key, const char *err, ...)\n \t\terror_fn(err, params);\n \t\tva_end(params);\n \t}\n-\tvalues = git_config_get_value_multi(key);\n+\n+\t/*\n+\t * We don't have a \"const\" key here, but we should definitely\n+\t * have one that's passed git_config_parse_key() already, if\n+\t * we're at the point of complaining about its value. So let's\n+\t * use *_knownkey_value_multi() here to get that BUG(...).\n+\t */\n+\tif (git_config_get_knownkey_value_multi(key, &values))\n+\t\tBUG(\"key '%s' does not exist, should not be given to git_die_config()\",\n+\t\t    key);\n \tkv_info = values->items[values->nr - 1].util;\n \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n }\ndiff --git a/config.h b/config.h\nindex ca994d77147..c88619b7dcf 100644\n--- a/config.h\n+++ b/config.h\n@@ -457,11 +457,30 @@ int git_configset_add_parameters(struct config_set *cs);\n \n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n- * for the configuration variable `key` and config set `cs`. When the\n- * configuration variable `key` is not found, returns NULL. The caller\n- * should not free or modify the returned pointer, as it is owned by the cache.\n+ * for the configuration variable `key` and config set `cs`.\n+ *\n+ * When the configuration variable `key` is not found, returns 1\n+ * without touching `value`.\n+ *\n+ * The key will be parsed for validity with git_config_parse_key(), on\n+ * error a negative value will be returned. See\n+ * git_configset_get_knownkey_value_multi() for a version of this which\n+ * BUG()s out on negative return values.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n+ */\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest);\n+\n+/**\n+ * Like git_configset_get_value_multi(), but BUG()s out if the return\n+ * value is < 0. Use it for keys known to pass git_config_parse_key(),\n+ * i.e. those hardcoded in the code, and never user-provided keys.\n  */\n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n+int git_configset_get_knownkey_value_multi(struct config_set *cs,\n+\t\t\t\t\t   const char *const key,\n+\t\t\t\t\t   const struct string_list **dest);\n \n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n@@ -495,8 +514,12 @@ struct repository;\n void repo_config(struct repository *repo, config_fn_t fn, void *data);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key);\n+int repo_config_get_value_multi(struct repository *repo,\n+\t\t\t\tconst char *key,\n+\t\t\t\tconst struct string_list **dest);\n+int repo_config_get_knownkey_value_multi(struct repository *repo,\n+\t\t\t\t\t const char *const key,\n+\t\t\t\t\t const struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -543,10 +566,21 @@ int git_config_get_value(const char *key, const char **value);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key`. When the configuration variable\n- * `key` is not found, returns NULL. The caller should not free or modify\n- * the returned pointer, as it is owned by the cache.\n+ * `key` is not found, returns 1 without touching `value`.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n+ */\n+int git_config_get_value_multi(const char *key,\n+\t\t\t       const struct string_list **dest);\n+\n+/**\n+ * A wrapper for git_config_get_value_multi() which does for it what\n+ * git_configset_get_knownkey_value_multi() does for\n+ * git_configset_get_value_multi().\n  */\n-const struct string_list *git_config_get_value_multi(const char *key);\n+int git_config_get_knownkey_value_multi(const char *const key,\n+\t\t\t\t\tconst struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 440407f1be7..0b4e73abbfa 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2301,7 +2301,12 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)\n \n const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n-\treturn repo_config_get_value_multi(r, \"pack.preferbitmaptips\");\n+\tconst struct string_list *dest;\n+\n+\tif (!repo_config_get_knownkey_value_multi(r, \"pack.preferbitmaptips\",\n+\t\t\t\t\t       &dest))\n+\t\treturn dest;\n+\treturn NULL;\n }\n \n int bitmap_is_preferred_refname(struct repository *r, const char *refname)\ndiff --git a/submodule.c b/submodule.c\nindex bf7a2c79183..e8c4362743d 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tsl = repo_config_get_value_multi(repo, \"submodule.active\");\n-\tif (sl) {\n+\tif (!repo_config_get_knownkey_value_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex 4ba9eb65606..f0d476d2376 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -95,8 +95,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\tgoto exit1;\n \t\t}\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_value_multi\")) {\n-\t\tstrptr = git_config_get_value_multi(argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_config_get_knownkey_value_multi(argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\n@@ -159,8 +158,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\t\tgoto exit2;\n \t\t\t}\n \t\t}\n-\t\tstrptr = git_configset_get_value_multi(&cs, argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_configset_get_knownkey_value_multi(&cs, argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 069ee94a4d7..9064478dc4a 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -160,10 +160,14 @@ int versioncmp(const char *s1, const char *s2)\n \t}\n \n \tif (!initialized) {\n-\t\tconst struct string_list *deprecated_prereleases;\n+\t\tconst struct string_list *deprecated_prereleases = NULL;\n+\n \t\tinitialized = 1;\n-\t\tprereleases = git_config_get_value_multi(\"versionsort.suffix\");\n-\t\tdeprecated_prereleases = git_config_get_value_multi(\"versionsort.prereleasesuffix\");\n+\t\tgit_config_get_knownkey_value_multi(\"versionsort.suffix\",\n+\t\t\t\t\t\t &prereleases);\n+\t\tgit_config_get_value_multi(\"versionsort.prereleasesuffix\",\n+\t\t\t\t\t   &deprecated_prereleases);\n+\n \t\tif (prereleases) {\n \t\t\tif (deprecated_prereleases)\n \t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465796","messageId":"patch-02.10-e17de2a2664-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH 02/10] for-each-repo: error on bad --config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:15Z","receivedAt":"2022-10-26T15:36:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in 6c62f015520 (for-each-repo: do nothing on empty config,\n2021-01-08) this command wants to ignore a non-existing config key,\nbut let's not conflate that with bad config.\n\nWe could preserve the comment added in 6c62f015520, but now that we're\ndirectly using the documented repo_config_get_value_multi() value it's\njust narrating something that should be obvious from the API use, so\nlet's drop it.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  | 15 +++++++--------\n t/t0068-for-each-repo.sh |  6 ++++++\n 2 files changed, 13 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex b01721762ef..16e9a76d04a 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -28,7 +28,8 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n {\n \tstatic const char *config_key = NULL;\n \tint i, result = 0;\n-\tconst struct string_list *values = NULL;\n+\tconst struct string_list *values;\n+\tint err;\n \n \tconst struct option options[] = {\n \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n@@ -42,13 +43,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\trepo_config_get_value_multi(the_repository, config_key, &values);\n-\n-\t/*\n-\t * Do nothing on an empty list, which is equivalent to the case\n-\t * where the config variable does not exist at all.\n-\t */\n-\tif (!values)\n+\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\tif (err < 0)\n+\t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n+\t\t\t       for_each_repo_usage, options, config_key);\n+\telse if (err)\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 4675e852517..115221c9ca5 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -33,4 +33,10 @@ test_expect_success 'do nothing on empty config' '\n \tgit for-each-repo --config=bogus.config -- help --no-such-option\n '\n \n+test_expect_success 'error on bad config keys' '\n+\ttest_expect_code 129 git for-each-repo --config=a &&\n+\ttest_expect_code 129 git for-each-repo --config=a.b. &&\n+\ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n+'\n+\n test_done\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465797","messageId":"patch-03.10-3519d3de010-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH 03/10] config API: mark *_multi() with RESULT_MUST_BE_USED","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:16Z","receivedAt":"2022-10-26T15:36:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Use the RESULT_MUST_BE_USED attribute to assert that all users of\nthe *_multi() API use the return values, in the preceding commit\n\"for-each-repo\" started using the return value meaningfully.\n\nThis requires changing versioncmp() so that we use the \"ret\" versions\nof the return values, and don't implicitly rely on\n\"deprecated_prereleases\" being set to NULL if the key didn't exist.\n\nSee 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\nreturn values, 2022-09-01) for the introduction of\nRESULT_MUST_BE_USED.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n config.h     |  6 ++++++\n versioncmp.c | 22 +++++++++++++---------\n 2 files changed, 19 insertions(+), 9 deletions(-)\n\ndiff --git a/config.h b/config.h\nindex c88619b7dcf..a5710c5856e 100644\n--- a/config.h\n+++ b/config.h\n@@ -470,6 +470,7 @@ int git_configset_add_parameters(struct config_set *cs);\n  * The caller should not free or modify the returned pointer, as it is\n  * owned by the cache.\n  */\n+RESULT_MUST_BE_USED\n int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \t\t\t\t  const struct string_list **dest);\n \n@@ -478,6 +479,7 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,\n  * value is < 0. Use it for keys known to pass git_config_parse_key(),\n  * i.e. those hardcoded in the code, and never user-provided keys.\n  */\n+RESULT_MUST_BE_USED\n int git_configset_get_knownkey_value_multi(struct config_set *cs,\n \t\t\t\t\t   const char *const key,\n \t\t\t\t\t   const struct string_list **dest);\n@@ -514,9 +516,11 @@ struct repository;\n void repo_config(struct repository *repo, config_fn_t fn, void *data);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n+RESULT_MUST_BE_USED\n int repo_config_get_value_multi(struct repository *repo,\n \t\t\t\tconst char *key,\n \t\t\t\tconst struct string_list **dest);\n+RESULT_MUST_BE_USED\n int repo_config_get_knownkey_value_multi(struct repository *repo,\n \t\t\t\t\t const char *const key,\n \t\t\t\t\t const struct string_list **dest);\n@@ -571,6 +575,7 @@ int git_config_get_value(const char *key, const char **value);\n  * The caller should not free or modify the returned pointer, as it is\n  * owned by the cache.\n  */\n+RESULT_MUST_BE_USED\n int git_config_get_value_multi(const char *key,\n \t\t\t       const struct string_list **dest);\n \n@@ -579,6 +584,7 @@ int git_config_get_value_multi(const char *key,\n  * git_configset_get_knownkey_value_multi() does for\n  * git_configset_get_value_multi().\n  */\n+RESULT_MUST_BE_USED\n int git_config_get_knownkey_value_multi(const char *const key,\n \t\t\t\t\tconst struct string_list **dest);\n \ndiff --git a/versioncmp.c b/versioncmp.c\nindex 9064478dc4a..effe1a6a6be 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -160,19 +160,23 @@ int versioncmp(const char *s1, const char *s2)\n \t}\n \n \tif (!initialized) {\n-\t\tconst struct string_list *deprecated_prereleases = NULL;\n+\t\tconst struct string_list *deprecated_prereleases;\n+\t\tint prereleases_ret, deprecated_prereleases_ret;\n \n \t\tinitialized = 1;\n-\t\tgit_config_get_knownkey_value_multi(\"versionsort.suffix\",\n-\t\t\t\t\t\t &prereleases);\n-\t\tgit_config_get_value_multi(\"versionsort.prereleasesuffix\",\n-\t\t\t\t\t   &deprecated_prereleases);\n-\n-\t\tif (prereleases) {\n-\t\t\tif (deprecated_prereleases)\n+\t\tprereleases_ret =\n+\t\t\tgit_config_get_knownkey_value_multi(\"versionsort.suffix\",\n+\t\t\t\t\t\t\t    &prereleases);\n+\t\tdeprecated_prereleases_ret =\n+\t\t\tgit_config_get_knownkey_value_multi(\"versionsort.prereleasesuffix\",\n+\t\t\t\t\t\t\t    &deprecated_prereleases);\n+\n+\t\tif (!prereleases_ret) {\n+\t\t\tif (!deprecated_prereleases_ret)\n \t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n-\t\t} else\n+\t\t} else if (!deprecated_prereleases_ret) {\n \t\t\tprereleases = deprecated_prereleases;\n+\t\t}\n \t}\n \tif (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,\n \t\t\t\t\t    &diff))\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465798","messageId":"patch-04.10-40b3cc9b8d4-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH 04/10] string-list API: mark \"struct_string_list\" to \"for_each_string_list\" const","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:17Z","receivedAt":"2022-10-26T15:36:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a \"const\" to the \"struct string_list *\" passed to\nfor_each_string_list().\n\nThis is arguably abuse of the type system, as the\n\"string_list_each_func_t fn\" take a \"struct string_list_item *\",\ni.e. not one with a \"const\", and those functions *can* modify those\nitems.\n\nBut as we'll see in a subsequent commit we have other such iteration\nfunctions that could benefit from a \"const\", i.e. to declare that\nwe're not altering the list itself, even though we might be calling\nfunctions that alter its values.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n string-list.c | 2 +-\n string-list.h | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/string-list.c b/string-list.c\nindex 549fc416d68..d8957466d25 100644\n--- a/string-list.c\n+++ b/string-list.c\n@@ -129,7 +129,7 @@ void string_list_remove_duplicates(struct string_list *list, int free_util)\n \t}\n }\n \n-int for_each_string_list(struct string_list *list,\n+int for_each_string_list(const struct string_list *list,\n \t\t\t string_list_each_func_t fn, void *cb_data)\n {\n \tint i, ret = 0;\ndiff --git a/string-list.h b/string-list.h\nindex c7b0d5d0008..7153cb79154 100644\n--- a/string-list.h\n+++ b/string-list.h\n@@ -138,7 +138,7 @@ void string_list_clear_func(struct string_list *list, string_list_clear_func_t c\n  * Apply `func` to each item. If `func` returns nonzero, the\n  * iteration aborts and the return value is propagated.\n  */\n-int for_each_string_list(struct string_list *list,\n+int for_each_string_list(const struct string_list *list,\n \t\t\t string_list_each_func_t func, void *cb_data);\n \n /**\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465799","messageId":"patch-05.10-b32b2e99aba-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH 05/10] string-list API: make has_string() and list_lookup() \"const\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:18Z","receivedAt":"2022-10-26T15:36:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Ever since these were added in the \"path_list\" predecessor of this API\nin 6d297f81373 (Status update on merge-recursive in C, 2006-07-08)\nthey haven't been \"const\", but as the compiler validates for us adding\nthat attribute to them is correct.\n\nNote that they will return a non-const \"struct string_list_item *\",\nbut the \"struct string_list *\" itself that's passed in can be marked\n\"const\".\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n string-list.c | 4 ++--\n string-list.h | 4 ++--\n 2 files changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/string-list.c b/string-list.c\nindex d8957466d25..d97a8f61c02 100644\n--- a/string-list.c\n+++ b/string-list.c\n@@ -245,7 +245,7 @@ void string_list_sort(struct string_list *list)\n \tQSORT_S(list->items, list->nr, cmp_items, &sort_ctx);\n }\n \n-struct string_list_item *unsorted_string_list_lookup(struct string_list *list,\n+struct string_list_item *unsorted_string_list_lookup(const struct string_list *list,\n \t\t\t\t\t\t     const char *string)\n {\n \tstruct string_list_item *item;\n@@ -257,7 +257,7 @@ struct string_list_item *unsorted_string_list_lookup(struct string_list *list,\n \treturn NULL;\n }\n \n-int unsorted_string_list_has_string(struct string_list *list,\n+int unsorted_string_list_has_string(const struct string_list *list,\n \t\t\t\t    const char *string)\n {\n \treturn unsorted_string_list_lookup(list, string) != NULL;\ndiff --git a/string-list.h b/string-list.h\nindex 7153cb79154..3589afee2ee 100644\n--- a/string-list.h\n+++ b/string-list.h\n@@ -227,13 +227,13 @@ void string_list_sort(struct string_list *list);\n  * Like `string_list_has_string()` but for unsorted lists. Linear in\n  * size of the list.\n  */\n-int unsorted_string_list_has_string(struct string_list *list, const char *string);\n+int unsorted_string_list_has_string(const struct string_list *list, const char *string);\n \n /**\n  * Like `string_list_lookup()` but for unsorted lists. Linear in size\n  * of the list.\n  */\n-struct string_list_item *unsorted_string_list_lookup(struct string_list *list,\n+struct string_list_item *unsorted_string_list_lookup(const struct string_list *list,\n \t\t\t\t\t\t     const char *string);\n /**\n  * Remove an item from a string_list. The `string` pointer of the\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465800","messageId":"patch-06.10-9c36f17481b-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH 06/10] builtin/gc.c: use \"unsorted_string_list_has_string()\" where appropriate","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:19Z","receivedAt":"2022-10-26T15:37:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor a \"do I have an element like this?\" pattern added in [1] and\n[2] to use unsorted_string_list_has_string() instead of a\nfor_each_string_list_item() loop.\n\nA preceding commit added a \"const\" to the \"struct string_list *\"\nargument of unsorted_string_list_has_string(), it'll thus play nicely\nwith git_config_get_const_value_multi() without needing a cast here.\n\n1. 1ebe6b02970 (maintenance: add 'unregister --force', 2022-09-27)\n2. 50a044f1e40 (gc: replace config subprocesses with API calls,\n   2022-09-27)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c | 22 ++++------------------\n 1 file changed, 4 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 04c48638ef4..f435eda2e73 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1467,7 +1467,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tconst char *key = \"maintenance.repo\";\n \tchar *config_value;\n \tchar *maintpath = get_maintpath();\n-\tstruct string_list_item *item;\n \tconst struct string_list *list;\n \n \targc = parse_options(argc, argv, prefix, options,\n@@ -1485,14 +1484,8 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \telse\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tif (!git_config_get_knownkey_value_multi(key, &list)) {\n-\t\tfor_each_string_list_item(item, list) {\n-\t\t\tif (!strcmp(maintpath, item->string)) {\n-\t\t\t\tfound = 1;\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t}\n-\t}\n+\tif (!git_config_get_knownkey_value_multi(key, &list))\n+\t\tfound = unsorted_string_list_has_string(list, maintpath);\n \n \tif (!found) {\n \t\tint rc;\n@@ -1532,7 +1525,6 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \tconst char *key = \"maintenance.repo\";\n \tchar *maintpath = get_maintpath();\n \tint found = 0;\n-\tstruct string_list_item *item;\n \tconst struct string_list *list;\n \n \targc = parse_options(argc, argv, prefix, options,\n@@ -1541,14 +1533,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tusage_with_options(builtin_maintenance_unregister_usage,\n \t\t\t\t   options);\n \n-\tif (!git_config_get_knownkey_value_multi(key, &list)) {\n-\t\tfor_each_string_list_item(item, list) {\n-\t\t\tif (!strcmp(maintpath, item->string)) {\n-\t\t\t\tfound = 1;\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t}\n-\t}\n+\tif (!git_config_get_knownkey_value_multi(key, &list))\n+\t\tfound = unsorted_string_list_has_string(list, maintpath);\n \n \tif (found) {\n \t\tint rc;\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465801","messageId":"patch-08.10-e7568dbe6fe-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH 08/10] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:21Z","receivedAt":"2022-10-26T15:37:18Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"A less well known edge case in the config format is that keys can be\nvalue-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\nare equivalent as far as \"--type=bool\" is concerned:\n\n\t[a]key\n\t[a]key = true\n\nBut as far as our parser is concerned the values for these two are\nNULL, and \"true\". I.e. for a sequence like:\n\n\t[a]key=x\n\t[a]key\n\t[a]key=y\n\nWe get a \"struct string_list\" with \"string\" members with \".string\"\nvalues of:\n\n\t{ \"x\", NULL, \"y\" }\n\nThis behavior goes back to the initial implementation of\ngit_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n2005-10-10).\n\nWhen the \"t/t1308-config-set.sh\" tests were added in [1] only one of\nthe three \"(NULL)\" lines in \"t/helper/test-config.c\" had any test\ncoverage. This change adds tests that stress the remaining two.\n\n1. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t1308-config-set.sh | 30 ++++++++++++++++++++++++++++++\n 1 file changed, 30 insertions(+)\n\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex b38e158d3b2..561e82f1808 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -146,6 +146,36 @@ test_expect_success 'find multiple values' '\n \tcheck_config get_value_multi case.baz sam bat hask\n '\n \n+test_expect_success 'emit multi values from configset with NULL entry' '\n+\ttest_when_finished \"rm -f my.config\" &&\n+\tcat >my.config <<-\\EOF &&\n+\t[a]key=x\n+\t[a]key\n+\t[a]key=y\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\tx\n+\t(NULL)\n+\ty\n+\tEOF\n+\ttest-tool config configset_get_value_multi a.key my.config >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'multi values from configset with a last NULL entry' '\n+\ttest_when_finished \"rm -f my.config\" &&\n+\tcat >my.config <<-\\EOF &&\n+\t[a]key=x\n+\t[a]key=y\n+\t[a]key\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\t(NULL)\n+\tEOF\n+\ttest-tool config configset_get_value a.key my.config >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'find value from a configset' '\n \tcat >config2 <<-\\EOF &&\n \t[case]\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465802","messageId":"patch-07.10-c01f7d85c94-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH 07/10] config API: add and use \"lookup_value\" functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:20Z","receivedAt":"2022-10-26T15:37:20Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Change various users of the config API who only wanted to ask if a\nconfiguration key existed to use a new *_config*_lookup_value() family\nof functions. Unlike the existing API functions in the API this one\ndoesn't take a \"dest\" argument.\n\nSome of these were using either git_config_get_string() or\ngit_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a\nconfigure_added_submodule() leak, 2022-09-01) for a recent example. We\ncan now use a helper function that doesn't require a throwaway\nvariable.\n\nWe could have changed git_configset_get_value_multi() to accept a\n\"NULL\" as a \"dest\" for all callers, but let's avoid changing the\nbehavior of existing API users. The new \"lookup\" API and the older API\ncall our static \"git_configset_get_value_multi_1()\" helper with a new\n\"read_only\" argument instead.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                |  5 +----\n builtin/submodule--helper.c |  9 +++------\n builtin/worktree.c          |  3 +--\n config.c                    | 25 +++++++++++++++++++++----\n config.h                    | 12 ++++++++++++\n 5 files changed, 38 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f435eda2e73..3e94fa5e20f 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1465,7 +1465,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \t};\n \tint found = 0;\n \tconst char *key = \"maintenance.repo\";\n-\tchar *config_value;\n \tchar *maintpath = get_maintpath();\n \tconst struct string_list *list;\n \n@@ -1479,9 +1478,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tgit_config_set(\"maintenance.auto\", \"false\");\n \n \t/* Set maintenance strategy, if unset */\n-\tif (!git_config_get_string(\"maintenance.strategy\", &config_value))\n-\t\tfree(config_value);\n-\telse\n+\tif (git_config_lookup_value(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n \tif (!git_config_get_knownkey_value_multi(key, &list))\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 1f8fe6a8e0d..b758255f816 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -541,7 +541,6 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t\tNULL\n \t};\n \tint ret = 1;\n-\tconst struct string_list *values;\n \n \targc = parse_options(argc, argv, prefix, module_init_options,\n \t\t\t     git_submodule_helper_usage, 0);\n@@ -553,7 +552,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t * If there are no path args and submodule.active is set then,\n \t * by default, only initialize 'active' modules.\n \t */\n-\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n+\tif (!argc && !git_config_lookup_value(\"submodule.active\"))\n \t\tmodule_list_active(&list);\n \n \tinfo.prefix = prefix;\n@@ -2709,7 +2708,6 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \tif (opt.init) {\n \t\tstruct module_list list = MODULE_LIST_INIT;\n \t\tstruct init_cb info = INIT_CB_INIT;\n-\t\tconst struct string_list *values;\n \n \t\tif (module_list_compute(argc, argv, opt.prefix,\n \t\t\t\t\t&pathspec2, &list) < 0) {\n@@ -2722,7 +2720,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \t\t * If there are no path args and submodule.active is set then,\n \t\t * by default, only initialize 'active' modules.\n \t\t */\n-\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n+\t\tif (!argc && !git_config_lookup_value(\"submodule.active\"))\n \t\t\tmodule_list_active(&list);\n \n \t\tinfo.prefix = opt.prefix;\n@@ -3166,7 +3164,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con\n static void configure_added_submodule(struct add_data *add_data)\n {\n \tchar *key;\n-\tconst char *val;\n \tstruct child_process add_submod = CHILD_PROCESS_INIT;\n \tstruct child_process add_gitmodules = CHILD_PROCESS_INIT;\n \n@@ -3211,7 +3208,7 @@ static void configure_added_submodule(struct add_data *add_data)\n \t * is_submodule_active(), since that function needs to find\n \t * out the value of \"submodule.active\" again anyway.\n \t */\n-\tif (!git_config_get_string_tmp(\"submodule.active\", &val)) {\n+\tif (!git_config_lookup_value(\"submodule.active\")) {\n \t\t/*\n \t\t * If the submodule being added isn't already covered by the\n \t\t * current configured pathspec, set the submodule's active flag\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex c6710b25520..5ab16631dbc 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -260,7 +260,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \n \tif (file_exists(from_file)) {\n \t\tstruct config_set cs = { { 0 } };\n-\t\tconst char *core_worktree;\n \t\tint bare;\n \n \t\tif (safe_create_leading_directories(to_file) ||\n@@ -279,7 +278,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \t\t\t\tto_file, \"core.bare\", NULL, \"true\", 0))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\n \t\t\t\t\"core.bare\", to_file);\n-\t\tif (!git_configset_get_value(&cs, \"core.worktree\", &core_worktree) &&\n+\t\tif (!git_configset_lookup_value(&cs, \"core.worktree\") &&\n \t\t\tgit_config_set_in_file_gently(to_file,\n \t\t\t\t\t\t\t\"core.worktree\", NULL))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\ndiff --git a/config.c b/config.c\nindex 2100b29b689..5cd130ddbb9 100644\n--- a/config.c\n+++ b/config.c\n@@ -2428,7 +2428,7 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *\n \n static int git_configset_get_value_multi_1(struct config_set *cs, const char *key,\n \t\t\t\t\t   const struct string_list **dest,\n-\t\t\t\t\t   int knownkey)\n+\t\t\t\t\t   int read_only, int knownkey)\n {\n \tstruct config_set_element *e;\n \tint ret;\n@@ -2440,7 +2440,8 @@ static int git_configset_get_value_multi_1(struct config_set *cs, const char *ke\n \t\treturn ret;\n \telse if (!e)\n \t\treturn 1;\n-\t*dest = &e->value_list;\n+\tif (!read_only)\n+\t\t*dest = &e->value_list;\n \n \treturn 0;\n }\n@@ -2448,14 +2449,19 @@ static int git_configset_get_value_multi_1(struct config_set *cs, const char *ke\n int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \t\t\t\t  const struct string_list **dest)\n {\n-\treturn git_configset_get_value_multi_1(cs, key, dest, 0);\n+\treturn git_configset_get_value_multi_1(cs, key, dest, 0, 0);\n }\n \n int git_configset_get_knownkey_value_multi(struct config_set *cs,\n \t\t\t\t\t   const char *const key,\n \t\t\t\t\t   const struct string_list **dest)\n {\n-\treturn git_configset_get_value_multi_1(cs, key, dest, 1);\n+\treturn git_configset_get_value_multi_1(cs, key, dest, 0, 1);\n+}\n+\n+int git_configset_lookup_value(struct config_set *cs, const char *key)\n+{\n+\treturn git_configset_get_value_multi_1(cs, key, NULL, 1, 0);\n }\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n@@ -2594,6 +2600,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)\n \tconfigset_iter(repo->config, fn, data);\n }\n \n+int repo_config_lookup_value(struct repository *repo, const char *key)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_value_multi_1(repo->config, key, NULL, 1, 0);\n+}\n+\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value)\n {\n@@ -2726,6 +2738,11 @@ void git_config_clear(void)\n \trepo_config_clear(the_repository);\n }\n \n+int git_config_lookup_value(const char *key)\n+{\n+\treturn repo_config_lookup_value(the_repository, key);\n+}\n+\n int git_config_get_value(const char *key, const char **value)\n {\n \treturn repo_config_get_value(the_repository, key, value);\ndiff --git a/config.h b/config.h\nindex a5710c5856e..cf1ae7862a8 100644\n--- a/config.h\n+++ b/config.h\n@@ -502,6 +502,8 @@ void git_configset_clear(struct config_set *cs);\n  * is owned by the cache.\n  */\n int git_configset_get_value(struct config_set *cs, const char *key, const char **dest);\n+RESULT_MUST_BE_USED\n+int git_configset_lookup_value(struct config_set *cs, const char *key);\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest);\n int git_configset_get_int(struct config_set *cs, const char *key, int *dest);\n@@ -524,6 +526,8 @@ RESULT_MUST_BE_USED\n int repo_config_get_knownkey_value_multi(struct repository *repo,\n \t\t\t\t\t const char *const key,\n \t\t\t\t\t const struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int repo_config_lookup_value(struct repository *repo, const char *key);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -588,6 +592,14 @@ RESULT_MUST_BE_USED\n int git_config_get_knownkey_value_multi(const char *const key,\n \t\t\t\t\tconst struct string_list **dest);\n \n+/**\n+ * The same as git_config_value(), except without the extra work to\n+ * return the value to the user, used to check if a value for a key\n+ * exists.\n+ */\n+RESULT_MUST_BE_USED\n+int git_config_lookup_value(const char *key);\n+\n /**\n  * Resets and invalidates the config cache.\n  */\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465803","messageId":"patch-10.10-b59cbed8f61-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH 10/10] for-each-repo: with bad config, don't conflate <path> and <cmd>","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:23Z","receivedAt":"2022-10-26T15:37:23Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on\nconfigured repos, 2020-09-11). Due to assuming that elements returned\nfrom the repo_config_get_value_multi() call wouldn't be \"NULL\" we'd\nconflate the <path> and <command> part of the argument list when\nrunning commands.\n\nAs noted in the preceding commit the fix is to move to a safer\n\"*_multi_string()\" version of the *__multi() API. This change is\nseparated from the rest because those all segfaulted. In this change\nwe ended up with different behavior.\n\nWhen using the \"--config=<config>\" form we take each element of the\nlist as a path to a repository. E.g. with a configuration like:\n\n\t[repo] list = /some/repo\n\nWe would, with this command:\n\n\tgit for-each-repo --config=repo.list status builtin\n\nRun a \"git status\" in /some/repo, as:\n\n\tgit -C /some/repo status builtin\n\nI.e. ask \"status\" to report on the \"builtin\" directory. But since a\nconfiguration such as this would result in a \"struct string_list *\"\nwith one element, whose \"string\" member is \"NULL\":\n\n\t[repo] list\n\nWe would, when constructing our command-line in\n\"builtin/for-each-repo.c\"...\n\n\tstrvec_pushl(&child.args, \"-C\", path, NULL);\n\tfor (i = 0; i < argc; i++)\n\t\tstrvec_push(&child.args, argv[i]);\n\n...have that \"path\" be \"NULL\", and as strvec_pushl() stops when it\nsees NULL we'd end with the first \"argv\" element as the argument to\nthe \"-C\" option, e.g.:\n\n\tgit -C status builtin\n\nI.e. we'd run the command \"builtin\" in the \"status\" directory.\n\nIn another context this might be an interesting security\nvulnerability, but I think that this amounts to a nothingburger on\nthat front.\n\nA hypothetical attacker would need to be able to write config for the\nvictim to run, if they're able to do that there's more interesting\nattack vectors. See the \"safe.directory\" facility added in\n8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).\n\nAn even more unlikely possibility would be an attacker able to\ngenerate the config used for \"for-each-repo --config=<key>\", but\nnothing else (e.g. an automated system producing that list).\n\nEven in that case the attack vector is limited to the user running\ncommands whose name matches a directory that's interesting to the\nattacker (e.g. a \"log\" directory in a repository). The second\nargument (if any) of the command is likely to make git die without\ndoing anything interesting (e.g. \"-p\" to \"log\", there being no \"-p\"\nbuilt-in command to run).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  |  2 +-\n t/t0068-for-each-repo.sh | 13 +++++++++++++\n 2 files changed, 14 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 16e9a76d04a..125901f2fc0 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -43,7 +43,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\terr = repo_config_get_value_multi_string(the_repository, config_key, &values);\n \tif (err < 0)\n \t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n \t\t\t       for_each_repo_usage, options, config_key);\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 115221c9ca5..c27d4dc5f71 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -39,4 +39,17 @@ test_expect_success 'error on bad config keys' '\n \ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n '\n \n+test_expect_success 'error on NULL value for config keys' '\n+\tcat >>.git/config <<-\\EOF &&\n+\t[empty]\n+\t\tkey\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''empty.key'\\''\n+\tEOF\n+\ttest_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465804","messageId":"patch-09.10-bda9d504b89-20221026T151328Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH 09/10] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T15:35:22Z","receivedAt":"2022-10-26T15:37:26Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix numerous and mostly long-standing segfaults in consumers of\nthe *_config_*value_multi() API. As discussed in the preceding commit\nan empty key in the config syntax yields a \"NULL\" string, which these\nusers would give to strcmp() (or similar), resulting in segfaults.\n\nAs this change shows, these non-test users of\nthe *_config_*value_multi() API didn't really want such an an unsafe\nand low-level API, let's give them something with the safety of\ngit_config_get_string() instead.\n\nThis fix is similar to what the *_string() functions and others\nacquired in[1] and [2]. Namely introducing and using a\nsafer *_config_*value_multi_string() variant of the\nlow-level *_config_*value_multi_string() function.\n\nThis fixes segfaults in code introduced in:\n\n  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)\n  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)\n  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)\n  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)\n  - 92156291ca8 (log: add default decoration filter, 2022-08-05)\n  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)\n\nThere are now two remaining user of the low-level API, one is the\n\"t/helper/test-config.c\" code added in [3]. The other we'll address in\na subsequent commit.\n\nAs seen in the preceding commit we need to give the\n\"t/helper/test-config.c\" caller these \"NULL\" entries. We thus cannot\nalter the underlying git_configset_get_value_multi_1() function itself\nto make it \"safe\".\n\nSuch a thing would also be undesirable, as casting or forbidding NULL\nvalues might only be one potential use-case of the underlying\nfunction. It's better to have a \"raw\" low-level function, and\ncorresponding wrapper functions that coerce its values. The callback\npattern being used here will make it easy to introduce e.g. a \"multi\"\nvariant which coerces its values to \"bool\", \"int\", \"path\" etc.\n\n1. 40ea4ed9032 (Add config_error_nonbool() helper function,\n   2008-02-11)\n2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,\n   2008-02-11).\n3. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                   |  4 +--\n builtin/log.c                  |  2 +-\n config.c                       | 63 +++++++++++++++++++++++++++++++---\n config.h                       | 40 +++++++++++++++++++++\n pack-bitmap.c                  |  2 +-\n submodule.c                    |  2 +-\n t/t4202-log.sh                 | 15 ++++++++\n t/t5310-pack-bitmaps.sh        | 21 ++++++++++++\n t/t7004-tag.sh                 | 17 +++++++++\n t/t7413-submodule-is-active.sh | 16 +++++++++\n t/t7900-maintenance.sh         | 38 ++++++++++++++++++++\n versioncmp.c                   |  8 ++---\n 12 files changed, 214 insertions(+), 14 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 3e94fa5e20f..3fc759b1f0c 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1481,7 +1481,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_lookup_value(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tif (!git_config_get_knownkey_value_multi(key, &list))\n+\tif (!git_config_get_knownkey_value_multi_string(key, &list))\n \t\tfound = unsorted_string_list_has_string(list, maintpath);\n \n \tif (!found) {\n@@ -1530,7 +1530,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tusage_with_options(builtin_maintenance_unregister_usage,\n \t\t\t\t   options);\n \n-\tif (!git_config_get_knownkey_value_multi(key, &list))\n+\tif (!git_config_get_knownkey_value_multi_string(key, &list))\n \t\tfound = unsorted_string_list_has_string(list, maintpath);\n \n \tif (found) {\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 75464c96ccf..d6b1c75ea2e 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -184,7 +184,7 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n \tconst struct string_list *config_exclude;\n \n-\tif (!git_config_get_knownkey_value_multi(\"log.excludeDecoration\",\n+\tif (!git_config_get_knownkey_value_multi_string(\"log.excludeDecoration\",\n \t\t\t\t\t      &config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\ndiff --git a/config.c b/config.c\nindex 5cd130ddbb9..25bb6514f81 100644\n--- a/config.c\n+++ b/config.c\n@@ -2428,7 +2428,8 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *\n \n static int git_configset_get_value_multi_1(struct config_set *cs, const char *key,\n \t\t\t\t\t   const struct string_list **dest,\n-\t\t\t\t\t   int read_only, int knownkey)\n+\t\t\t\t\t   int read_only, int knownkey,\n+\t\t\t\t\t   string_list_each_func_t check_fn)\n {\n \tstruct config_set_element *e;\n \tint ret;\n@@ -2440,28 +2441,51 @@ static int git_configset_get_value_multi_1(struct config_set *cs, const char *ke\n \t\treturn ret;\n \telse if (!e)\n \t\treturn 1;\n+\tif (check_fn &&\n+\t    (ret = for_each_string_list(&e->value_list, check_fn, (void *)key)))\n+\t\treturn ret;\n \tif (!read_only)\n \t\t*dest = &e->value_list;\n \n \treturn 0;\n }\n \n+static int check_multi_string(struct string_list_item *item, void *util)\n+{\n+\treturn item->string ? 0 : config_error_nonbool(util);\n+}\n+\n int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \t\t\t\t  const struct string_list **dest)\n {\n-\treturn git_configset_get_value_multi_1(cs, key, dest, 0, 0);\n+\treturn git_configset_get_value_multi_1(cs, key, dest, 0, 0, NULL);\n }\n \n int git_configset_get_knownkey_value_multi(struct config_set *cs,\n \t\t\t\t\t   const char *const key,\n \t\t\t\t\t   const struct string_list **dest)\n {\n-\treturn git_configset_get_value_multi_1(cs, key, dest, 0, 1);\n+\treturn git_configset_get_value_multi_1(cs, key, dest, 0, 1, NULL);\n+}\n+\n+int git_configset_get_value_multi_string(struct config_set *cs, const char *key,\n+\t\t\t\t\t const struct string_list **dest)\n+{\n+\treturn git_configset_get_value_multi_1(cs, key, dest, 0, 0,\n+\t\t\t\t\t       check_multi_string);\n+}\n+\n+int git_configset_get_knownkey_value_multi_string(struct config_set *cs,\n+\t\t\t\t\t\t  const char *const key,\n+\t\t\t\t\t\t  const struct string_list **dest)\n+{\n+\treturn git_configset_get_value_multi_1(cs, key, dest, 0, 1,\n+\t\t\t\t\t       check_multi_string);\n }\n \n int git_configset_lookup_value(struct config_set *cs, const char *key)\n {\n-\treturn git_configset_get_value_multi_1(cs, key, NULL, 1, 0);\n+\treturn git_configset_get_value_multi_1(cs, key, NULL, 1, 0, NULL);\n }\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n@@ -2603,7 +2627,8 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)\n int repo_config_lookup_value(struct repository *repo, const char *key)\n {\n \tgit_config_check_init(repo);\n-\treturn git_configset_get_value_multi_1(repo->config, key, NULL, 1, 0);\n+\treturn git_configset_get_value_multi_1(repo->config, key, NULL, 1, 0,\n+\t\t\t\t\t       NULL);\n }\n \n int repo_config_get_value(struct repository *repo,\n@@ -2629,6 +2654,22 @@ int repo_config_get_knownkey_value_multi(struct repository *repo,\n \treturn git_configset_get_knownkey_value_multi(repo->config, key, dest);\n }\n \n+int repo_config_get_value_multi_string(struct repository *repo,\n+\t\t\t\t       const char *key,\n+\t\t\t\t       const struct string_list **dest)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_value_multi_string(repo->config, key, dest);\n+}\n+\n+int repo_config_get_knownkey_value_multi_string(struct repository *repo,\n+\t\t\t\t\t\tconst char *key,\n+\t\t\t\t\t\tconst struct string_list **dest)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_knownkey_value_multi_string(repo->config, key, dest);\n+}\n+\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest)\n {\n@@ -2759,6 +2800,18 @@ int git_config_get_knownkey_value_multi(const char *const key,\n \treturn repo_config_get_knownkey_value_multi(the_repository, key, dest);\n }\n \n+int git_config_get_value_multi_string(const char *key,\n+\t\t\t\t      const struct string_list **dest)\n+{\n+\treturn repo_config_get_value_multi_string(the_repository, key, dest);\n+}\n+\n+int git_config_get_knownkey_value_multi_string(const char *key,\n+\t\t\t\t\t       const struct string_list **dest)\n+{\n+\treturn repo_config_get_knownkey_value_multi_string(the_repository, key, dest);\n+}\n+\n int git_config_get_string(const char *key, char **dest)\n {\n \treturn repo_config_get_string(the_repository, key, dest);\ndiff --git a/config.h b/config.h\nindex cf1ae7862a8..047ef83afc6 100644\n--- a/config.h\n+++ b/config.h\n@@ -484,6 +484,30 @@ int git_configset_get_knownkey_value_multi(struct config_set *cs,\n \t\t\t\t\t   const char *const key,\n \t\t\t\t\t   const struct string_list **dest);\n \n+/**\n+ * A validation wrapper for git_configset_get_value_multi() which does\n+ * for it what git_configset_get_string() does for\n+ * git_configset_get_value().\n+ *\n+ * The configuration syntax allows for \"[section] key\", which will\n+ * give us a NULL entry in the \"struct string_list\", as opposed to\n+ * \"[section] key =\" which is the empty string. Most users of the API\n+ * are not prepared to handle NULL in a \"struct string_list\".\n+ */\n+int git_configset_get_value_multi_string(struct config_set *cs, const char *key,\n+\t\t\t\t\t const struct string_list **dest);\n+\n+/**\n+ * A wrapper for git_configset_get_value_multi_string() which does for\n+ * it what git_configset_get_knownkey_value_multi() does for\n+ * git_configset_get_value_multi().\n+ */\n+RESULT_MUST_BE_USED\n+int git_configset_get_knownkey_value_multi_string(struct config_set *cs,\n+\t\t\t\t\t\t  const char *const key,\n+\t\t\t\t\t\t  const struct string_list **dest);\n+\n+\n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n  */\n@@ -527,6 +551,14 @@ int repo_config_get_knownkey_value_multi(struct repository *repo,\n \t\t\t\t\t const char *const key,\n \t\t\t\t\t const struct string_list **dest);\n RESULT_MUST_BE_USED\n+int repo_config_get_value_multi_string(struct repository *repo,\n+\t\t\t\t       const char *key,\n+\t\t\t\t       const struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int repo_config_get_knownkey_value_multi_string(struct repository *repo,\n+\t\t\t\t\t\tconst char *const key,\n+\t\t\t\t\t\tconst struct string_list **dest);\n+RESULT_MUST_BE_USED\n int repo_config_lookup_value(struct repository *repo, const char *key);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n@@ -592,6 +624,14 @@ RESULT_MUST_BE_USED\n int git_config_get_knownkey_value_multi(const char *const key,\n \t\t\t\t\tconst struct string_list **dest);\n \n+RESULT_MUST_BE_USED\n+int git_config_get_value_multi_string(const char *key,\n+\t\t\t\t      const struct string_list **dest);\n+\n+RESULT_MUST_BE_USED\n+int git_config_get_knownkey_value_multi_string(const char *const key,\n+\t\t\t\t\t       const struct string_list **dest);\n+\n /**\n  * The same as git_config_value(), except without the extra work to\n  * return the value to the user, used to check if a value for a key\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 0b4e73abbfa..9a61d9ff9a8 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2303,7 +2303,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n \tconst struct string_list *dest;\n \n-\tif (!repo_config_get_knownkey_value_multi(r, \"pack.preferbitmaptips\",\n+\tif (!repo_config_get_knownkey_value_multi_string(r, \"pack.preferbitmaptips\",\n \t\t\t\t\t       &dest))\n \t\treturn dest;\n \treturn NULL;\ndiff --git a/submodule.c b/submodule.c\nindex e8c4362743d..f84b253154e 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tif (!repo_config_get_knownkey_value_multi(repo, \"submodule.active\", &sl)) {\n+\tif (!repo_config_get_knownkey_value_multi_string(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 2ce2b41174d..ae73aef922f 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,6 +835,21 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n+test_expect_success 'parse log.excludeDecoration with no value' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[log]\n+\t\texcludeDecoration\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''log.excludeDecoration'\\''\n+\tEOF\n+\tgit log --decorate=short 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'decorate-refs with glob' '\n \tcat >expect.decorate <<-\\EOF &&\n \tMerge-tag-reach\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 6d693eef82f..68195a1de36 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,6 +404,27 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n+\ttest_expect_success 'pack.preferBitmapTips' '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -rf repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\tgit config pack.writeBitmapLookupTable '\"$writeLookupTable\"' &&\n+\t\t\ttest_commit_bulk --message=\"%s\" 103 &&\n+\n+\t\t\tcat >>.git/config <<-\\EOF &&\n+\t\t\t[pack]\n+\t\t\t\tpreferBitmapTips\n+\t\t\tEOF\n+\n+\t\t\tcat >expect <<-\\EOF &&\n+\t\t\terror: missing value for '\\''pack.preferbitmaptips'\\''\n+\t\t\tEOF\n+\t\t\tgit repack -adb 2>actual &&\n+\t\t\ttest_cmp expect actual\n+\t\t)\n+\t'\n+\n \ttest_expect_success 'complains about multiple pack bitmaps' '\n \t\trm -fr repo &&\n \t\tgit init repo &&\ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex 9aa1660651b..f4a31ada79a 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,6 +1843,23 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[versionsort]\n+\t\tprereleaseSuffix\n+\t\tsuffix\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''versionsort.suffix'\\''\n+\terror: missing value for '\\''versionsort.prereleasesuffix'\\''\n+\tEOF\n+\tgit tag -l --sort=version:refname 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'version sort with prerelease reordering' '\n \ttest_config versionsort.prereleaseSuffix -rc &&\n \tgit tag foo1.6-rc1 &&\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex 7cdc2637649..887d181b72e 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,6 +51,22 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n+test_expect_success 'is-active handles submodule.active config missing a value' '\n+\tcp super/.git/config super/.git/config.orig &&\n+\ttest_when_finished mv super/.git/config.orig super/.git/config &&\n+\n+\tcat >>super/.git/config <<-\\EOF &&\n+\t[submodule]\n+\t\tactive\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''submodule.active'\\''\n+\tEOF\n+\ttest-tool -C super submodule is-active sub1 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'is-active works with basic submodule.active config' '\n \ttest_when_finished \"git -C super config submodule.sub1.URL ../sub\" &&\n \ttest_when_finished \"git -C super config --unset-all submodule.active\" &&\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 96bdd420456..1201866c8d0 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -505,6 +505,44 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --force\n '\n \n+test_expect_success 'register with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\tgit maintenance register 2>actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n+'\n+\n+test_expect_success 'unregister with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\ttest_expect_code 128 git maintenance unregister 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo &&\n+\n+\tgit maintenance unregister --force 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n+'\n+\n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \tMETA=\"a+b*c\" &&\n \tgit init \"$META\" &&\ndiff --git a/versioncmp.c b/versioncmp.c\nindex effe1a6a6be..4efb5f9e621 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -165,11 +165,11 @@ int versioncmp(const char *s1, const char *s2)\n \n \t\tinitialized = 1;\n \t\tprereleases_ret =\n-\t\t\tgit_config_get_knownkey_value_multi(\"versionsort.suffix\",\n-\t\t\t\t\t\t\t    &prereleases);\n+\t\t\tgit_config_get_knownkey_value_multi_string(\"versionsort.suffix\",\n+\t\t\t\t\t\t\t\t   &prereleases);\n \t\tdeprecated_prereleases_ret =\n-\t\t\tgit_config_get_knownkey_value_multi(\"versionsort.prereleasesuffix\",\n-\t\t\t\t\t\t\t    &deprecated_prereleases);\n+\t\t\tgit_config_get_knownkey_value_multi_string(\"versionsort.prereleasesuffix\",\n+\t\t\t\t\t\t\t\t   &deprecated_prereleases);\n \n \t\tif (!prereleases_ret) {\n \t\t\tif (!deprecated_prereleases_ret)\n-- \n2.38.0.1251.g3eefdfb5e7a\n\n"},{"id":"465813","messageId":"20221026184915.GA1828@szeder.dev","threadId":"58696","inReplyTo":"patch-01.10-eefa253ab1f-20221026T151328Z-avarab@gmail.com","subject":"Re: [PATCH 01/10] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2022-10-26T18:49:15Z","receivedAt":"2022-10-26T18:49:24Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Wed, Oct 26, 2022 at 05:35:14PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> The git_configset_get_value_multi() function added in 3c8687a73ee (add\n> `config_set` API for caching config-like files, 2014-07-28) is a\n> fundamental part of of the config API, and\n> e.g. \"git_config_get_value()\" and others are implemented in terms of\n> it.\n> \n> But it has had the limitation that configset_find_element() calls\n> git_config_parse_key(), but then throws away the distinction between a\n> \"ret < 1\" return value from it, and return values that indicate a key\n\nShouldn't that be \"ret < 0\"?\n\n> doesn't exist. As a result the git_config_get_value_multi() function\n> would either return a \"const struct string_list *\", or NULL.\n> \n> By changing the *_multi() function to return an \"int\" for the status\n> and to write to a \"const struct string_list **dest\" parameter we can\n> avoid losing this information. API callers can now do:\n> \n> \tconst struct string_list *dest;\n> \tint ret;\n> \n> \tret = git_config_get_value_multi(key, &dest);\n> \tif (ret < 1)\n\nThis catches all negative values and zero.\n\n> \t\tdie(\"bad key: %s\", key);\n> \telse if (ret)\n\nThis catches all non-zero values.\n\n> \t\t; /* key does not exist */\n> \telse\n\nSo how could this ever be executed?!\n\n> \t\t; /* got key, can use \"dest\" */\n> \n> A \"get_knownkey_value_multi\" variant is also provided, which will\n> BUG() out in the \"ret < 1\" case. This is useful in the cases where we\n\nShouldn't that be \"ret < 0\" as well?  The condition in that \"knownkey\"\nvariant added in this patch is:\n\n  +\tret = configset_find_element(cs, key, &e);\n  +\tif (ret < 0 && knownkey)\n  +\t\tBUG(\"*_get_knownkey_*() only accepts known-good (hardcoded) keys, but '%s' is bad!\", key);\n\n> hardcode the keyname in our source code, and therefore use the more\n> idiomatic pattern of:\n> \n> \tif (!git_config_get_value_multi(key, &dest)\n> \t\t; /* got key, can use \"dest\" */\n> \telse\n> \t\t; /* key does not exist */\n> \n> The \"knownkey\" name was picked instead of e.g. \"const\" to avoid a\n> repeat of the issues noted in f1de981e8b6 (config: fix leaks from\n> git_config_get_string_const(), 2020-08-14) and 9a53219f69b (config:\n> drop git_config_get_string_const(), 2020-08-17). API users might think\n> that \"const\" means that the value(s) don't need to be free'd.\n> \n> As noted in commentary here we treat git_die_config() as a\n> special-case, i.e. we assume that a value we're complaining about has\n> already had its key pass the git_config_parse_key() check.\n> \n> Likewise we consider the keys passed to \"t/helper/test-config.c\" to be\n> \"knownkey\", and will emit a BUG() if they don't pass\n> git_config_parse_key(). Those will come from our *.sh tests, so\n> they're also \"known keys\" coming from our sources.\n> \n> A logical follow-up to this would be to change the various \"*_get_*()\"\n> functions to ferry the git_configset_get_value() return value to their\n> own callers, e.g.:\n> \n> \tdiff --git a/config.c b/config.c\n> \tindex 094ad899e0b..7e8ee4cfec1 100644\n> \t--- a/config.c\n> \t+++ b/config.c\n> \t@@ -2479,11 +2479,14 @@ static int git_configset_get_string_tmp(struct config_set *cs, const char *key,\n> \t int git_configset_get_int(struct config_set *cs, const char *key, int *dest)\n> \t {\n> \t \tconst char *value;\n> \t-\tif (!git_configset_get_value(cs, key, &value)) {\n> \t-\t\t*dest = git_config_int(key, value);\n> \t-\t\treturn 0;\n> \t-\t} else\n> \t-\t\treturn 1;\n> \t+\tint ret;\n> \t+\n> \t+\tif ((ret = git_configset_get_value(cs, key, &value)))\n> \t+\t\tgoto done;\n> \t+\n> \t+\t*dest = git_config_int(key, value);\n> \t+done:\n> \t+\treturn ret;\n> \t }\n> \n> \t int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest)\n> \n> Most of those callers don't care, and call those functions as\n> \"if (!func(...))\", but if they do they'll be able to tell key\n> non-existence from errors we encounter. Before this change those API\n> users would have been unable to tell the two conditions apart, as\n> git_configset_get_value() hid the difference.\n> \n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  builtin/for-each-repo.c     |  5 +-\n>  builtin/gc.c                |  6 +--\n>  builtin/log.c               |  6 +--\n>  builtin/submodule--helper.c |  6 ++-\n>  config.c                    | 94 ++++++++++++++++++++++++++++++-------\n>  config.h                    | 52 ++++++++++++++++----\n>  pack-bitmap.c               |  7 ++-\n>  submodule.c                 |  3 +-\n>  t/helper/test-config.c      |  6 +--\n>  versioncmp.c                | 10 ++--\n>  10 files changed, 148 insertions(+), 47 deletions(-)\n> \n> diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\n> index fd86e5a8619..b01721762ef 100644\n> --- a/builtin/for-each-repo.c\n> +++ b/builtin/for-each-repo.c\n> @@ -28,7 +28,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n>  {\n>  \tstatic const char *config_key = NULL;\n>  \tint i, result = 0;\n> -\tconst struct string_list *values;\n> +\tconst struct string_list *values = NULL;\n>  \n>  \tconst struct option options[] = {\n>  \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n> @@ -42,8 +42,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n>  \tif (!config_key)\n>  \t\tdie(_(\"missing --config=<config>\"));\n>  \n> -\tvalues = repo_config_get_value_multi(the_repository,\n> -\t\t\t\t\t     config_key);\n> +\trepo_config_get_value_multi(the_repository, config_key, &values);\n>  \n>  \t/*\n>  \t * Do nothing on an empty list, which is equivalent to the case\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index 243ee85d283..04c48638ef4 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -1485,8 +1485,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n>  \telse\n>  \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n>  \n> -\tlist = git_config_get_value_multi(key);\n> -\tif (list) {\n> +\tif (!git_config_get_knownkey_value_multi(key, &list)) {\n>  \t\tfor_each_string_list_item(item, list) {\n>  \t\t\tif (!strcmp(maintpath, item->string)) {\n>  \t\t\t\tfound = 1;\n> @@ -1542,8 +1541,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n>  \t\tusage_with_options(builtin_maintenance_unregister_usage,\n>  \t\t\t\t   options);\n>  \n> -\tlist = git_config_get_value_multi(key);\n> -\tif (list) {\n> +\tif (!git_config_get_knownkey_value_multi(key, &list)) {\n>  \t\tfor_each_string_list_item(item, list) {\n>  \t\t\tif (!strcmp(maintpath, item->string)) {\n>  \t\t\t\tfound = 1;\n> diff --git a/builtin/log.c b/builtin/log.c\n> index ee19dc5d450..75464c96ccf 100644\n> --- a/builtin/log.c\n> +++ b/builtin/log.c\n> @@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n>  \tint i;\n>  \tchar *value = NULL;\n>  \tstruct string_list *include = decoration_filter->include_ref_pattern;\n> -\tconst struct string_list *config_exclude =\n> -\t\t\tgit_config_get_value_multi(\"log.excludeDecoration\");\n> +\tconst struct string_list *config_exclude;\n>  \n> -\tif (config_exclude) {\n> +\tif (!git_config_get_knownkey_value_multi(\"log.excludeDecoration\",\n> +\t\t\t\t\t      &config_exclude)) {\n>  \t\tstruct string_list_item *item;\n>  \t\tfor_each_string_list_item(item, config_exclude)\n>  \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index 0b4acb442b2..1f8fe6a8e0d 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -541,6 +541,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n>  \t\tNULL\n>  \t};\n>  \tint ret = 1;\n> +\tconst struct string_list *values;\n>  \n>  \targc = parse_options(argc, argv, prefix, module_init_options,\n>  \t\t\t     git_submodule_helper_usage, 0);\n> @@ -552,7 +553,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n>  \t * If there are no path args and submodule.active is set then,\n>  \t * by default, only initialize 'active' modules.\n>  \t */\n> -\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n> +\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n>  \t\tmodule_list_active(&list);\n>  \n>  \tinfo.prefix = prefix;\n> @@ -2708,6 +2709,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n>  \tif (opt.init) {\n>  \t\tstruct module_list list = MODULE_LIST_INIT;\n>  \t\tstruct init_cb info = INIT_CB_INIT;\n> +\t\tconst struct string_list *values;\n>  \n>  \t\tif (module_list_compute(argc, argv, opt.prefix,\n>  \t\t\t\t\t&pathspec2, &list) < 0) {\n> @@ -2720,7 +2722,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n>  \t\t * If there are no path args and submodule.active is set then,\n>  \t\t * by default, only initialize 'active' modules.\n>  \t\t */\n> -\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n> +\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n>  \t\t\tmodule_list_active(&list);\n>  \n>  \t\tinfo.prefix = opt.prefix;\n> diff --git a/config.c b/config.c\n> index cbb5a3bab74..2100b29b689 100644\n> --- a/config.c\n> +++ b/config.c\n> @@ -2275,23 +2275,28 @@ void read_very_early_config(config_fn_t cb, void *data)\n>  \tconfig_with_options(cb, data, NULL, &opts);\n>  }\n>  \n> -static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n> +static int configset_find_element(struct config_set *cs, const char *key,\n> +\t\t\t\t  struct config_set_element **dest)\n>  {\n>  \tstruct config_set_element k;\n>  \tstruct config_set_element *found_entry;\n>  \tchar *normalized_key;\n> +\tint ret;\n> +\n>  \t/*\n>  \t * `key` may come from the user, so normalize it before using it\n>  \t * for querying entries from the hashmap.\n>  \t */\n> -\tif (git_config_parse_key(key, &normalized_key, NULL))\n> -\t\treturn NULL;\n> +\tret = git_config_parse_key(key, &normalized_key, NULL);\n> +\tif (ret < 0)\n> +\t\treturn ret;\n>  \n>  \thashmap_entry_init(&k.ent, strhash(normalized_key));\n>  \tk.key = normalized_key;\n>  \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n>  \tfree(normalized_key);\n> -\treturn found_entry;\n> +\t*dest = found_entry;\n> +\treturn 0;\n>  }\n>  \n>  static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n> @@ -2300,8 +2305,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n>  \tstruct string_list_item *si;\n>  \tstruct configset_list_item *l_item;\n>  \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n> +\tint ret;\n>  \n> -\te = configset_find_element(cs, key);\n> +\tret = configset_find_element(cs, key, &e);\n> +\tif (ret < 0)\n> +\t\treturn ret;\n>  \t/*\n>  \t * Since the keys are being fed by git_config*() callback mechanism, they\n>  \t * are already normalized. So simply add them without any further munging.\n> @@ -2400,24 +2408,54 @@ int git_configset_add_parameters(struct config_set *cs)\n>  int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n>  {\n>  \tconst struct string_list *values = NULL;\n> +\tint ret;\n> +\n>  \t/*\n>  \t * Follows \"last one wins\" semantic, i.e., if there are multiple matches for the\n>  \t * queried key in the files of the configset, the value returned will be the last\n>  \t * value in the value list for that key.\n>  \t */\n> -\tvalues = git_configset_get_value_multi(cs, key);\n> +\tret = git_configset_get_value_multi(cs, key, &values);\n>  \n> -\tif (!values)\n> +\tif (ret < 0)\n> +\t\treturn ret;\n> +\telse if (!values)\n>  \t\treturn 1;\n>  \tassert(values->nr > 0);\n>  \t*value = values->items[values->nr - 1].string;\n>  \treturn 0;\n>  }\n>  \n> -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n> +static int git_configset_get_value_multi_1(struct config_set *cs, const char *key,\n> +\t\t\t\t\t   const struct string_list **dest,\n> +\t\t\t\t\t   int knownkey)\n>  {\n> -\tstruct config_set_element *e = configset_find_element(cs, key);\n> -\treturn e ? &e->value_list : NULL;\n> +\tstruct config_set_element *e;\n> +\tint ret;\n> +\n> +\tret = configset_find_element(cs, key, &e);\n> +\tif (ret < 0 && knownkey)\n> +\t\tBUG(\"*_get_knownkey_*() only accepts known-good (hardcoded) keys, but '%s' is bad!\", key);\n> +\telse if (ret < 0)\n> +\t\treturn ret;\n> +\telse if (!e)\n> +\t\treturn 1;\n> +\t*dest = &e->value_list;\n> +\n> +\treturn 0;\n> +}\n> +\n> +int git_configset_get_value_multi(struct config_set *cs, const char *key,\n> +\t\t\t\t  const struct string_list **dest)\n> +{\n> +\treturn git_configset_get_value_multi_1(cs, key, dest, 0);\n> +}\n> +\n> +int git_configset_get_knownkey_value_multi(struct config_set *cs,\n> +\t\t\t\t\t   const char *const key,\n> +\t\t\t\t\t   const struct string_list **dest)\n> +{\n> +\treturn git_configset_get_value_multi_1(cs, key, dest, 1);\n>  }\n>  \n>  int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n> @@ -2563,11 +2601,20 @@ int repo_config_get_value(struct repository *repo,\n>  \treturn git_configset_get_value(repo->config, key, value);\n>  }\n>  \n> -const struct string_list *repo_config_get_value_multi(struct repository *repo,\n> -\t\t\t\t\t\t      const char *key)\n> +int repo_config_get_value_multi(struct repository *repo,\n> +\t\t\t\tconst char *key,\n> +\t\t\t\tconst struct string_list **dest)\n>  {\n>  \tgit_config_check_init(repo);\n> -\treturn git_configset_get_value_multi(repo->config, key);\n> +\treturn git_configset_get_value_multi(repo->config, key, dest);\n> +}\n> +\n> +int repo_config_get_knownkey_value_multi(struct repository *repo,\n> +\t\t\t\t\t const char *const key,\n> +\t\t\t\t\t const struct string_list **dest)\n> +{\n> +\tgit_config_check_init(repo);\n> +\treturn git_configset_get_knownkey_value_multi(repo->config, key, dest);\n>  }\n>  \n>  int repo_config_get_string(struct repository *repo,\n> @@ -2684,9 +2731,15 @@ int git_config_get_value(const char *key, const char **value)\n>  \treturn repo_config_get_value(the_repository, key, value);\n>  }\n>  \n> -const struct string_list *git_config_get_value_multi(const char *key)\n> +int git_config_get_value_multi(const char *key, const struct string_list **dest)\n> +{\n> +\treturn repo_config_get_value_multi(the_repository, key, dest);\n> +}\n> +\n> +int git_config_get_knownkey_value_multi(const char *const key,\n> +\t\t\t\t\tconst struct string_list **dest)\n>  {\n> -\treturn repo_config_get_value_multi(the_repository, key);\n> +\treturn repo_config_get_knownkey_value_multi(the_repository, key, dest);\n>  }\n>  \n>  int git_config_get_string(const char *key, char **dest)\n> @@ -2833,7 +2886,16 @@ void git_die_config(const char *key, const char *err, ...)\n>  \t\terror_fn(err, params);\n>  \t\tva_end(params);\n>  \t}\n> -\tvalues = git_config_get_value_multi(key);\n> +\n> +\t/*\n> +\t * We don't have a \"const\" key here, but we should definitely\n> +\t * have one that's passed git_config_parse_key() already, if\n> +\t * we're at the point of complaining about its value. So let's\n> +\t * use *_knownkey_value_multi() here to get that BUG(...).\n> +\t */\n> +\tif (git_config_get_knownkey_value_multi(key, &values))\n> +\t\tBUG(\"key '%s' does not exist, should not be given to git_die_config()\",\n> +\t\t    key);\n>  \tkv_info = values->items[values->nr - 1].util;\n>  \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n>  }\n> diff --git a/config.h b/config.h\n> index ca994d77147..c88619b7dcf 100644\n> --- a/config.h\n> +++ b/config.h\n> @@ -457,11 +457,30 @@ int git_configset_add_parameters(struct config_set *cs);\n>  \n>  /**\n>   * Finds and returns the value list, sorted in order of increasing priority\n> - * for the configuration variable `key` and config set `cs`. When the\n> - * configuration variable `key` is not found, returns NULL. The caller\n> - * should not free or modify the returned pointer, as it is owned by the cache.\n> + * for the configuration variable `key` and config set `cs`.\n> + *\n> + * When the configuration variable `key` is not found, returns 1\n> + * without touching `value`.\n> + *\n> + * The key will be parsed for validity with git_config_parse_key(), on\n> + * error a negative value will be returned. See\n> + * git_configset_get_knownkey_value_multi() for a version of this which\n> + * BUG()s out on negative return values.\n> + *\n> + * The caller should not free or modify the returned pointer, as it is\n> + * owned by the cache.\n> + */\n> +int git_configset_get_value_multi(struct config_set *cs, const char *key,\n> +\t\t\t\t  const struct string_list **dest);\n> +\n> +/**\n> + * Like git_configset_get_value_multi(), but BUG()s out if the return\n> + * value is < 0. Use it for keys known to pass git_config_parse_key(),\n> + * i.e. those hardcoded in the code, and never user-provided keys.\n>   */\n> -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n> +int git_configset_get_knownkey_value_multi(struct config_set *cs,\n> +\t\t\t\t\t   const char *const key,\n> +\t\t\t\t\t   const struct string_list **dest);\n>  \n>  /**\n>   * Clears `config_set` structure, removes all saved variable-value pairs.\n> @@ -495,8 +514,12 @@ struct repository;\n>  void repo_config(struct repository *repo, config_fn_t fn, void *data);\n>  int repo_config_get_value(struct repository *repo,\n>  \t\t\t  const char *key, const char **value);\n> -const struct string_list *repo_config_get_value_multi(struct repository *repo,\n> -\t\t\t\t\t\t      const char *key);\n> +int repo_config_get_value_multi(struct repository *repo,\n> +\t\t\t\tconst char *key,\n> +\t\t\t\tconst struct string_list **dest);\n> +int repo_config_get_knownkey_value_multi(struct repository *repo,\n> +\t\t\t\t\t const char *const key,\n> +\t\t\t\t\t const struct string_list **dest);\n>  int repo_config_get_string(struct repository *repo,\n>  \t\t\t   const char *key, char **dest);\n>  int repo_config_get_string_tmp(struct repository *repo,\n> @@ -543,10 +566,21 @@ int git_config_get_value(const char *key, const char **value);\n>  /**\n>   * Finds and returns the value list, sorted in order of increasing priority\n>   * for the configuration variable `key`. When the configuration variable\n> - * `key` is not found, returns NULL. The caller should not free or modify\n> - * the returned pointer, as it is owned by the cache.\n> + * `key` is not found, returns 1 without touching `value`.\n> + *\n> + * The caller should not free or modify the returned pointer, as it is\n> + * owned by the cache.\n> + */\n> +int git_config_get_value_multi(const char *key,\n> +\t\t\t       const struct string_list **dest);\n> +\n> +/**\n> + * A wrapper for git_config_get_value_multi() which does for it what\n> + * git_configset_get_knownkey_value_multi() does for\n> + * git_configset_get_value_multi().\n>   */\n> -const struct string_list *git_config_get_value_multi(const char *key);\n> +int git_config_get_knownkey_value_multi(const char *const key,\n> +\t\t\t\t\tconst struct string_list **dest);\n>  \n>  /**\n>   * Resets and invalidates the config cache.\n> diff --git a/pack-bitmap.c b/pack-bitmap.c\n> index 440407f1be7..0b4e73abbfa 100644\n> --- a/pack-bitmap.c\n> +++ b/pack-bitmap.c\n> @@ -2301,7 +2301,12 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)\n>  \n>  const struct string_list *bitmap_preferred_tips(struct repository *r)\n>  {\n> -\treturn repo_config_get_value_multi(r, \"pack.preferbitmaptips\");\n> +\tconst struct string_list *dest;\n> +\n> +\tif (!repo_config_get_knownkey_value_multi(r, \"pack.preferbitmaptips\",\n> +\t\t\t\t\t       &dest))\n> +\t\treturn dest;\n> +\treturn NULL;\n>  }\n>  \n>  int bitmap_is_preferred_refname(struct repository *r, const char *refname)\n> diff --git a/submodule.c b/submodule.c\n> index bf7a2c79183..e8c4362743d 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n>  \tfree(key);\n>  \n>  \t/* submodule.active is set */\n> -\tsl = repo_config_get_value_multi(repo, \"submodule.active\");\n> -\tif (sl) {\n> +\tif (!repo_config_get_knownkey_value_multi(repo, \"submodule.active\", &sl)) {\n>  \t\tstruct pathspec ps;\n>  \t\tstruct strvec args = STRVEC_INIT;\n>  \t\tconst struct string_list_item *item;\n> diff --git a/t/helper/test-config.c b/t/helper/test-config.c\n> index 4ba9eb65606..f0d476d2376 100644\n> --- a/t/helper/test-config.c\n> +++ b/t/helper/test-config.c\n> @@ -95,8 +95,7 @@ int cmd__config(int argc, const char **argv)\n>  \t\t\tgoto exit1;\n>  \t\t}\n>  \t} else if (argc == 3 && !strcmp(argv[1], \"get_value_multi\")) {\n> -\t\tstrptr = git_config_get_value_multi(argv[2]);\n> -\t\tif (strptr) {\n> +\t\tif (!git_config_get_knownkey_value_multi(argv[2], &strptr)) {\n>  \t\t\tfor (i = 0; i < strptr->nr; i++) {\n>  \t\t\t\tv = strptr->items[i].string;\n>  \t\t\t\tif (!v)\n> @@ -159,8 +158,7 @@ int cmd__config(int argc, const char **argv)\n>  \t\t\t\tgoto exit2;\n>  \t\t\t}\n>  \t\t}\n> -\t\tstrptr = git_configset_get_value_multi(&cs, argv[2]);\n> -\t\tif (strptr) {\n> +\t\tif (!git_configset_get_knownkey_value_multi(&cs, argv[2], &strptr)) {\n>  \t\t\tfor (i = 0; i < strptr->nr; i++) {\n>  \t\t\t\tv = strptr->items[i].string;\n>  \t\t\t\tif (!v)\n> diff --git a/versioncmp.c b/versioncmp.c\n> index 069ee94a4d7..9064478dc4a 100644\n> --- a/versioncmp.c\n> +++ b/versioncmp.c\n> @@ -160,10 +160,14 @@ int versioncmp(const char *s1, const char *s2)\n>  \t}\n>  \n>  \tif (!initialized) {\n> -\t\tconst struct string_list *deprecated_prereleases;\n> +\t\tconst struct string_list *deprecated_prereleases = NULL;\n> +\n>  \t\tinitialized = 1;\n> -\t\tprereleases = git_config_get_value_multi(\"versionsort.suffix\");\n> -\t\tdeprecated_prereleases = git_config_get_value_multi(\"versionsort.prereleasesuffix\");\n> +\t\tgit_config_get_knownkey_value_multi(\"versionsort.suffix\",\n> +\t\t\t\t\t\t &prereleases);\n> +\t\tgit_config_get_value_multi(\"versionsort.prereleasesuffix\",\n> +\t\t\t\t\t   &deprecated_prereleases);\n> +\n>  \t\tif (prereleases) {\n>  \t\t\tif (deprecated_prereleases)\n>  \t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n> -- \n> 2.38.0.1251.g3eefdfb5e7a\n> \n"},{"id":"465819","messageId":"221026.86pmeebcj9.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"20221026184915.GA1828@szeder.dev","subject":"Re: [PATCH 01/10] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-26T19:33:34Z","receivedAt":"2022-10-26T19:39:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Oct 26 2022, SZEDER Gábor wrote:\n\n> On Wed, Oct 26, 2022 at 05:35:14PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> The git_configset_get_value_multi() function added in 3c8687a73ee (add\n>> `config_set` API for caching config-like files, 2014-07-28) is a\n>> fundamental part of of the config API, and\n>> e.g. \"git_config_get_value()\" and others are implemented in terms of\n>> it.\n>> \n>> But it has had the limitation that configset_find_element() calls\n>> git_config_parse_key(), but then throws away the distinction between a\n>> \"ret < 1\" return value from it, and return values that indicate a key\n>\n> Shouldn't that be \"ret < 0\"?\n\nYes, sorry, that's just a typo. It's <0 for API errors (e.g. unable to\nparse your key bad key), 0 for OK, 1 for key doesn't exist.\n\n>> doesn't exist. As a result the git_config_get_value_multi() function\n>> would either return a \"const struct string_list *\", or NULL.\n>> \n>> By changing the *_multi() function to return an \"int\" for the status\n>> and to write to a \"const struct string_list **dest\" parameter we can\n>> avoid losing this information. API callers can now do:\n>> \n>> \tconst struct string_list *dest;\n>> \tint ret;\n>> \n>> \tret = git_config_get_value_multi(key, &dest);\n>> \tif (ret < 1)\n>\n> This catches all negative values and zero.\n>\n>> \t\tdie(\"bad key: %s\", key);\n>> \telse if (ret)\n>\n> This catches all non-zero values.\n>\n>> \t\t; /* key does not exist */\n>> \telse\n>\n> So how could this ever be executed?!\n\nYes, sorry. It's the same typo/thinko.\n\n>> \t\t; /* got key, can use \"dest\" */\n>> \n>> A \"get_knownkey_value_multi\" variant is also provided, which will\n>> BUG() out in the \"ret < 1\" case. This is useful in the cases where we\n>\n> Shouldn't that be \"ret < 0\" as well?  The condition in that \"knownkey\"\n> variant added in this patch is:\n>\n>   +\tret = configset_find_element(cs, key, &e);\n>   +\tif (ret < 0 && knownkey)\n>   +\t\tBUG(\"*_get_knownkey_*() only accepts known-good (hardcoded) keys, but '%s' is bad!\", key);\n\nYes, FWIW the code isn't incorrect in this regard, I just screwed up the\ncommit message, sorry.\n\nThe canonical example that isn't tricky is in builtin/for-each-repo.c, i.e.:\n\n        err = repo_config_get_value_multi_string(the_repository, config_key, &values);\n        if (err < 0)\n                usage_msg_optf(_(\"got bad config --config=%s\"),\n                               for_each_repo_usage, options, config_key);\n        else if (err)\n                return 0;\n\nI.e. it wants to ignore non-existing config (\"else if\"), but now we\ndistinguish that from errors. The *_multi() API on master doesn't allow\nfor that.\n"},{"id":"465896","messageId":"xmqqsfj9kqwx.fsf@gitster.g","threadId":"58696","inReplyTo":"patch-01.10-eefa253ab1f-20221026T151328Z-avarab@gmail.com","subject":"Re: [PATCH 01/10] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-10-27T19:27:58Z","receivedAt":"2022-10-27T19:28:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> By changing the *_multi() function to return an \"int\" for the status\n> and to write to a \"const struct string_list **dest\" parameter we can\n> avoid losing this information. API callers can now do:\n>\n> \tconst struct string_list *dest;\n> \tint ret;\n>\n> \tret = git_config_get_value_multi(key, &dest);\n> \tif (ret < 1)\n> \t\tdie(\"bad key: %s\", key);\n> \telse if (ret)\n> \t\t; /* key does not exist */\n> \telse\n> \t\t; /* got key, can use \"dest\" */\n\nIt is a good thing to allow the callers to tell \"no such key-value\npair exists\", \"key is malformed\", and \"here are the values for the\nkey\".  And the above if/else if/else cascade is a reasonable\ninterface to give the callers for that (modulo that \"negative is\nbad\" should be kept to match our API convention).\n\n>\n> A \"get_knownkey_value_multi\" variant is also provided, which will\n> BUG() out in the \"ret < 1\" case. This is useful in the cases where we\n> hardcode the keyname in our source code, and therefore use the more\n> idiomatic pattern of:\n>\n> \tif (!git_config_get_value_multi(key, &dest)\n> \t\t; /* got key, can use \"dest\" */\n> \telse\n> \t\t; /* key does not exist */\n\nI doubt it is a good idea to add such a specialized interface begin\nwith.  Let's not bloat the API for little benefit.\n"},{"id":"465897","messageId":"xmqqlep1kqpy.fsf@gitster.g","threadId":"58696","inReplyTo":"patch-04.10-40b3cc9b8d4-20221026T151328Z-avarab@gmail.com","subject":"Re: [PATCH 04/10] string-list API: mark \"struct_string_list\" to \"for_each_string_list\" const","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-10-27T19:32:09Z","receivedAt":"2022-10-27T19:32:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> Add a \"const\" to the \"struct string_list *\" passed to\n> for_each_string_list().\n>\n> This is arguably abuse of the type system, as the\n> \"string_list_each_func_t fn\" take a \"struct string_list_item *\",\n> i.e. not one with a \"const\", and those functions *can* modify those\n> items.\n>\n> But as we'll see in a subsequent commit we have other such iteration\n> functions that could benefit from a \"const\", i.e. to declare that\n> we're not altering the list itself, even though we might be calling\n> functions that alter its values.\n\nThe callback functions are allowed to (by taking a non-const\npointer) modify the items, but are there ones that actually modify\nthem?\n\n"},{"id":"465898","messageId":"xmqqh6zpkqgo.fsf@gitster.g","threadId":"58696","inReplyTo":"patch-06.10-9c36f17481b-20221026T151328Z-avarab@gmail.com","subject":"Re: [PATCH 06/10] builtin/gc.c: use \"unsorted_string_list_has_string()\" where appropriate","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-10-27T19:37:43Z","receivedAt":"2022-10-27T19:37:49Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> Refactor a \"do I have an element like this?\" pattern added in [1] and\n> [2] to use unsorted_string_list_has_string() instead of a\n> for_each_string_list_item() loop.\n\nIn the longer term, I am not sure if we want to keep such code that\nuses string-list as a \"database to be looked up with the string as\nthe key\".  I am not sure it is worth our review bandwidth to change\na for-each-string-list that terminates early to its shorthand\nunsorted_string_list_has_string().  Surely each such conversation\nwould allow us to lose 4 to 5 lines, but longer term we should be\ndiscuraging the use of unsorted_string_list_has_string() in the\nfirst place.\n"},{"id":"465899","messageId":"xmqqczadkq9f.fsf@gitster.g","threadId":"58696","inReplyTo":"patch-07.10-c01f7d85c94-20221026T151328Z-avarab@gmail.com","subject":"Re: [PATCH 07/10] config API: add and use \"lookup_value\" functions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-10-27T19:42:04Z","receivedAt":"2022-10-27T19:42:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> Change various users of the config API who only wanted to ask if a\n> configuration key existed to use a new *_config*_lookup_value() family\n> of functions. Unlike the existing API functions in the API this one\n> doesn't take a \"dest\" argument.\n\nWhen I hear \"lookup-value\", the first thing I would expect was\n\"look-up by value\" (i.e. the reverse look-up to find key).  That is\nnot what is going on.\n\nWhat is presented here is \"does the key have corresponding value\ndefined in the configuration system, yes/no?\", isn't it?\n\nI would expect such a function to be named *_config_key_exists().\n\n> diff --git a/config.h b/config.h\n> index a5710c5856e..cf1ae7862a8 100644\n> --- a/config.h\n> +++ b/config.h\n> @@ -502,6 +502,8 @@ void git_configset_clear(struct config_set *cs);\n>   * is owned by the cache.\n>   */\n>  int git_configset_get_value(struct config_set *cs, const char *key, const char **dest);\n> +RESULT_MUST_BE_USED\n> +int git_configset_lookup_value(struct config_set *cs, const char *key);\n\nThis must be documented, especially if we give it such a bad name ;-).\n"},{"id":"465900","messageId":"xmqq8rl1kq73.fsf@gitster.g","threadId":"58696","inReplyTo":"patch-08.10-e7568dbe6fe-20221026T151328Z-avarab@gmail.com","subject":"Re: [PATCH 08/10] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-10-27T19:43:28Z","receivedAt":"2022-10-27T19:43:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> A less well known edge case in the config format is that keys can be\n> value-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\n> are equivalent as far as \"--type=bool\" is concerned:\n>\n> \t[a]key\n> \t[a]key = true\n>\n> But as far as our parser is concerned the values for these two are\n> NULL, and \"true\". I.e. for a sequence like:\n>\n> \t[a]key=x\n> \t[a]key\n> \t[a]key=y\n>\n> We get a \"struct string_list\" with \"string\" members with \".string\"\n> values of:\n>\n> \t{ \"x\", NULL, \"y\" }\n>\n> This behavior goes back to the initial implementation of\n> git_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n> 2005-10-10).\n>\n> When the \"t/t1308-config-set.sh\" tests were added in [1] only one of\n> the three \"(NULL)\" lines in \"t/helper/test-config.c\" had any test\n> coverage. This change adds tests that stress the remaining two.\n\nGood.\n"},{"id":"465901","messageId":"xmqq4jvpkpxd.fsf@gitster.g","threadId":"58696","inReplyTo":"patch-09.10-bda9d504b89-20221026T151328Z-avarab@gmail.com","subject":"Re: [PATCH 09/10] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-10-27T19:49:18Z","receivedAt":"2022-10-27T19:49:22Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> Fix numerous and mostly long-standing segfaults in consumers of\n> the *_config_*value_multi() API. As discussed in the preceding commit\n> an empty key in the config syntax yields a \"NULL\" string, which these\n> users would give to strcmp() (or similar), resulting in segfaults.\n\nSounds like a good idea.\n\nI would have called them _nonbool(), not _string(), especially\nbecause we are not going to have other variants like _int(), though.\n\n\n"},{"id":"465902","messageId":"xmqqzgdhjb89.fsf@gitster.g","threadId":"58696","inReplyTo":"xmqq4jvpkpxd.fsf@gitster.g","subject":"Re: [PATCH 09/10] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-10-27T19:52:06Z","receivedAt":"2022-10-27T19:52:12Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n>\n>> Fix numerous and mostly long-standing segfaults in consumers of\n>> the *_config_*value_multi() API. As discussed in the preceding commit\n>> an empty key in the config syntax yields a \"NULL\" string, which these\n>> users would give to strcmp() (or similar), resulting in segfaults.\n>\n> Sounds like a good idea.\n>\n> I would have called them _nonbool(), not _string(), especially\n> because we are not going to have other variants like _int(), though.\n\nActually, I take it back.  Instead of introducing _string(), how\nabout introducing _bool() and convert those minority callers that do\nwant to see boolean values to use the new one, while rejecting NULLs\nfor everybody else that calls the traditional \"get_value\" family of\nfunctions?  That would \"optimize\" for the majority of simpler users,\nwouldn't it?\n\n"},{"id":"465904","messageId":"xmqqsfj9jaav.fsf@gitster.g","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"Re: [PATCH 00/10] config API: make \"multi\" safe, fix numerous segfaults","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-10-27T20:12:08Z","receivedAt":"2022-10-27T20:12:38Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> I also think that part of the config API is a wart, but that we should\n> go for a different solution. It's the only config function that\n> doesn't return an \"int\" indicating whether we found the key.\n\nOverall I saw some things to like in the series, but was not\nimpressed by others.  The _multi() thing in the earliest patch is a\nwelcome change, giving an option to call nonbool() is a good idea\n(but I have doubts about the exectuion), and \"does the key exist?\"\nmay be a good thing to have.  Others ranged between \"Meh?\" to \"it\nmight be good, but why does it have to be done here now?\".\n\nThanks.\n"},{"id":"465916","messageId":"221028.86a65gam0o.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"xmqqlep1kqpy.fsf@gitster.g","subject":"Re: [PATCH 04/10] string-list API: mark \"struct_string_list\" to \"for_each_string_list\" const","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-27T23:04:05Z","receivedAt":"2022-10-27T23:23:58Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Oct 27 2022, Junio C Hamano wrote:\n\n> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n>\n>> Add a \"const\" to the \"struct string_list *\" passed to\n>> for_each_string_list().\n>>\n>> This is arguably abuse of the type system, as the\n>> \"string_list_each_func_t fn\" take a \"struct string_list_item *\",\n>> i.e. not one with a \"const\", and those functions *can* modify those\n>> items.\n>>\n>> But as we'll see in a subsequent commit we have other such iteration\n>> functions that could benefit from a \"const\", i.e. to declare that\n>> we're not altering the list itself, even though we might be calling\n>> functions that alter its values.\n>\n> The callback functions are allowed to (by taking a non-const\n> pointer) modify the items, but are there ones that actually modify\n> them?\n\nTree-wide that's:\n\n\t 11 files changed, 18 insertions(+), 18 deletions(-)\n\nI.e. a bunch of changes like:\n\n\t-static int get_notes_refs(struct string_list_item *item, void *arg)\n\t+static int get_notes_refs(const struct string_list_item *item, void *arg)\n\nIt turns out there's a grand total of one user of that:\n\t\n\tsetup.c: In function ‘canonicalize_ceiling_entry’:\n\tsetup.c:1102:30: error: assignment of member ‘string’ in read-only object\n\t 1102 |                 item->string = real_path;\n\t      |                              ^\n\nBut note that that's for the \"filter\" variant. In any case using the\nsame function pointer type in eb5f0c7a616 (string_list: add a new\nfunction, filter_string_list(), 2012-09-12) for both was probably a\nmistake.\n\nBut still, I think it's best not to do anything about *that*. I.e. it\nmakes sense for such an interface to say that the iterator helper takes\nyour const list, i.e. unlike filter_string_list() it's not expected to\nbe changing the list itself.\n\nBut you as as the caller are then free to change list items you're\ngiven.\n"},{"id":"465918","messageId":"221028.865yg4alcm.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"xmqqh6zpkqgo.fsf@gitster.g","subject":"Re: [PATCH 06/10] builtin/gc.c: use \"unsorted_string_list_has_string()\" where appropriate","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-27T23:25:53Z","receivedAt":"2022-10-27T23:38:24Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Oct 27 2022, Junio C Hamano wrote:\n\n> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n>\n>> Refactor a \"do I have an element like this?\" pattern added in [1] and\n>> [2] to use unsorted_string_list_has_string() instead of a\n>> for_each_string_list_item() loop.\n>\n> In the longer term, I am not sure if we want to keep such code that\n> uses string-list as a \"database to be looked up with the string as\n> the key\".  I am not sure it is worth our review bandwidth to change\n> a for-each-string-list that terminates early to its shorthand\n> unsorted_string_list_has_string().  Surely each such conversation\n> would allow us to lose 4 to 5 lines, but longer term we should be\n> discuraging the use of unsorted_string_list_has_string() in the\n> first place.\n\nI haven't benchmarked, but I'd think on modern computers O(n) for such\nshort lists would be more performance due to cache locality, i.e. not\nworth pre-sorting it, or making it a hash table.\n\nBut for such small amounts of data I'd think it would be fine either\nway.\n\nAs to the change, I'm fine with leaving this out.\n\nThe reason it's in here is because this series came out as a reply to\nStolee's earlier RFC.\n\nI think it's a fair summary to say that the reason we started talking\nabout this at all is because the topic at hand was how to make this\nexact code in builtin/gc.c safer and more idiomatic. I.e. see:\n\n\thttps://lore.kernel.org/git/e06cb4df081bc2222731f9185a22ed7ad67e3814.1664287711.git.gitgitgadget@gmail.com/\n\nAnd my earlier summary of that, the very beginning showing the API forms\nunder discussion:\n\n\thttps://lore.kernel.org/git/220928.868rm3w9d4.gmgdl@evledraar.gmail.com/\n\nSo Re this & your \"it might be good, but why does it have to be done\nhere now?\" reply to the CL: Yeah I can eject some of this, but having a\nseries (and a predecessor RFC) whose main reason for existing is making\nthis API safer & nicer seems incomplete unless we're also converting\ncallers to use those supposedly nicer patterns.\n\nIn general I think this sort of change is exactly the sort of thing\nyou'd want in such a series. A test of a good API isn't just that it\nlooks or acts nicely in isolation, but that it's easily combined with\nother things you might expect to use with it.\n\nHence this 04-06/10. I.e. the original contention in the RFC was that we\nhad to return a dummy string list to make these sort of patterns\nsafer/nicer/idiomatic. I think these patches serve as a convincing\ncounter-argument to that.\n"},{"id":"465920","messageId":"221028.861qqsajx6.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"xmqqzgdhjb89.fsf@gitster.g","subject":"Re: [PATCH 09/10] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-27T23:44:19Z","receivedAt":"2022-10-28T00:09:15Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Oct 27 2022, Junio C Hamano wrote:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n>>\n>>> Fix numerous and mostly long-standing segfaults in consumers of\n>>> the *_config_*value_multi() API. As discussed in the preceding commit\n>>> an empty key in the config syntax yields a \"NULL\" string, which these\n>>> users would give to strcmp() (or similar), resulting in segfaults.\n>>\n>> Sounds like a good idea.\n>>\n>> I would have called them _nonbool(), not _string(), especially\n>> because we are not going to have other variants like _int(), though.\n>\n> Actually, I take it back.  Instead of introducing _string(), how\n> about introducing _bool() and convert those minority callers that do\n> want to see boolean values to use the new one, while rejecting NULLs\n> for everybody else that calls the traditional \"get_value\" family of\n> functions?  That would \"optimize\" for the majority of simpler users,\n> wouldn't it?\n\nI don't think the goal should be just to optimize for those current\nusers, but to leave the config API in a state where it makes sense\nconceptually.\n\nFor the scalar (single) values we have a low-level \"low-level\" function,\nand then variants to get it as a bool, path, string, int etc.\n\nI think a \"multi\" function should just be the logical result of applying\none of those \"types\" to list. I.e. (pseudocode):\n\n\ta_raw = get_config_raw(\"a.key\");\n\ta_string = stringify(a_raw);\n\nAnd, as a list:\n\n\tlist_raw = get_config_raw_multi(\"a.key\");\n\tlist_strings = map { stringify(item) } list_raw;\n\nNow, if we don't supply the equivalent of the \"raw, but multi-value\"\nfunction we'll make it hard to use the API, because now you can't think\nabout it as the \"multi\" just being a list version of what you get with\nthe scalar version.\n\nE.g. what should we do about \"[a]key\" in a list API that stringifies by\ndefault? If you then want \"stringified bool\" we're only left with bad choices:\n\n - If you die that's bed, because that's a legit true value\n - If you coerce it to \"\" to help the string use case you get the wrong\n   answer, because \"[a]key=\" (empty string) is false, but \"[a]key\"\n   (value-less) is true.\n - Ditto if you prune it out, as then it won't be seen in the bool list.\n\nWhich is why I went for the end-state here. I.e. it's now easy to add\nother \"multi\" variants (we'd need to add coercion, but that's easy\nenough).\n"},{"id":"465967","messageId":"xmqqmt9fiws7.fsf@gitster.g","threadId":"58696","inReplyTo":"221028.861qqsajx6.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 09/10] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-10-28T19:16:24Z","receivedAt":"2022-10-28T19:16:33Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n>> Actually, I take it back.  Instead of introducing _string(), how\n>> about introducing _bool() and convert those minority callers that do\n>> want to see boolean values to use the new one, while rejecting NULLs\n>> for everybody else that calls the traditional \"get_value\" family of\n>> functions?  That would \"optimize\" for the majority of simpler users,\n>> wouldn't it?\n>\n> I don't think the goal should be just to optimize for those current\n> users, but to leave the config API in a state where it makes sense\n> conceptually.\n\nIt is more like guiding a conceptually clean design using the need\nof the current users to rein in pursuit of theoretical \"elegance\".\n\n> Now, if we don't supply the equivalent of the \"raw, but multi-value\"\n> function we'll make it hard to use the API, because now you can't think\n> about it as the \"multi\" just being a list version of what you get with\n> the scalar version.\n\nI am not interested in _bool() variant that \"stringifies\" NULL to\n\"true\" at all.  What I was suggesting was:\n\n * Reserve the current get and get_multi for those who should have\n   been calling config_error_nonbool() themselves (because your\n   _string() has not been available to them, they were lazy not to\n   bother, leading to NULL dereference given certain end-user data).\n   And do the config_error_nonbool() inside the updated get and\n   get_multi without introducing _string() variant at all.\n\n * The above alone WILL break callers who are prepared to handle\n   \"bool\" and \"bool plus some other string\", because they are fully\n   expecting that the get API will give them NULL but the above\n   update will instead stop before they see the NULL they are\n   prepared to handle themselves.  Introduce _bool variants and make\n   them call them.\n\nwithout any \"stringifying\" at all.\n\n"},{"id":"466106","messageId":"221031.868rkv7s70.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"xmqqmt9fiws7.fsf@gitster.g","subject":"Re: [PATCH 09/10] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-10-31T18:22:45Z","receivedAt":"2022-10-31T18:40:13Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Oct 28 2022, Junio C Hamano wrote:\n\n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>\n>>> Actually, I take it back.  Instead of introducing _string(), how\n>>> about introducing _bool() and convert those minority callers that do\n>>> want to see boolean values to use the new one, while rejecting NULLs\n>>> for everybody else that calls the traditional \"get_value\" family of\n>>> functions?  That would \"optimize\" for the majority of simpler users,\n>>> wouldn't it?\n>>\n>> I don't think the goal should be just to optimize for those current\n>> users, but to leave the config API in a state where it makes sense\n>> conceptually.\n>\n> It is more like guiding a conceptually clean design using the need\n> of the current users to rein in pursuit of theoretical \"elegance\".\n\nI agree that the current code users don't care either way, they'll be\ngetting the same thing.\n\nBut being able to readily understand an API is valuable too. The config\nAPI is bad enough with all repetition of:\n\n\t{git_configset,repo_config,git_config}_get_value()\n\t{git_configset,repo_config,git_config}_get_string()\n\t{git_configset,repo_config,git_config}_get_bool()\n        [...]\n\nI think it's worth it to be able to say that:\n\n\t{git_configset,repo_config,git_config}_get_value_multi()\n\t{git_configset,repo_config,git_config}_get_value_string()\n        <ditto \"bool\">\n\nAre \"just like the scalar version, but multi\". Actually when I summarize\nit like that I realize I should really make it:\n\n\t{git_configset,repo_config,git_config}_get_string_multi()\n\nI.e. \"*_get_string_multi()\", not \"*_get_value_multi_string()\". I don't\nknow what I was thinking.\n\nBut aside from that, the point is I think it's worth it not to have it\ninstead be:\n\n        # \"non-string\" doesn't exist, but get it via some use of\n        # (currently static) configset_find_element()\n\n        # \"get value\", but really \"get string, for multi\"\n\t{git_configset,repo_config,git_config}_get_value_multi()\n\n        # ???\n\t{git_configset,repo_config,git_config}_get_bool_multi()\n\nWe currently don't need/have a \"*_get_bool_multi()\", which I think is\nbesides the point. We might in the future, and should forsee that we're\npicking a nonsensical naming convention.\n\nWe also have similar gaps in the current API (not all variants of all\nfunctions exist, for the scalar variants), but at least those that we do\nhave behave consistently.\n\n>> Now, if we don't supply the equivalent of the \"raw, but multi-value\"\n>> function we'll make it hard to use the API, because now you can't think\n>> about it as the \"multi\" just being a list version of what you get with\n>> the scalar version.\n>\n> I am not interested in _bool() variant that \"stringifies\" NULL to\n> \"true\" at all.  What I was suggesting was:\n>\n>  * Reserve the current get and get_multi for those who should have\n>    been calling config_error_nonbool() themselves (because your\n>    _string() has not been available to them, they were lazy not to\n>    bother, leading to NULL dereference given certain end-user data).\n>    And do the config_error_nonbool() inside the updated get and\n>    get_multi without introducing _string() variant at all.\n\nI get what you're saying, I just think it suffers from the problem\noutlined above, and that it's worth solving it.\n\n>  * The above alone WILL break callers who are prepared to handle\n>    \"bool\" and \"bool plus some other string\", because they are fully\n>    expecting that the get API will give them NULL but the above\n>    update will instead stop before they see the NULL they are\n>    prepared to handle themselves.  Introduce _bool variants and make\n>    them call them.\n\nEven if it wasn't for the naming question, I think the arrangement in\nthis series is also better in that I need to go and change each caller\nto the new variant, and explain for each one why it's OK.\n\nWhereas if we just sneakconfig_error_nonbool() into the low-level API\nwe're going to have a smaller change, but also one that's basically\n\"trust me, I read the code of all the callers, this should be fine...\".\n\n"},{"id":"466256","messageId":"patch-v2-1.9-b8fd3bea4d1-20221101T225823Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"[PATCH v2 1/9] for-each-repo tests: test bad --config keys","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-01T23:05:12Z","receivedAt":"2022-11-01T23:05:45Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in 6c62f015520 (for-each-repo: do nothing on empty config,\n2021-01-08) this command wants to ignore a non-existing config key,\nbut it's been conflating that with bad config keys.\n\nA subsequent commit will address that, but for now let's fix the gaps\nin test coverage, and show what we're currently doing in these cases.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t0068-for-each-repo.sh | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 4675e852517..6bba0c5f4c2 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -33,4 +33,10 @@ test_expect_success 'do nothing on empty config' '\n \tgit for-each-repo --config=bogus.config -- help --no-such-option\n '\n \n+test_expect_success 'bad config keys' '\n+\tgit for-each-repo --config=a &&\n+\tgit for-each-repo --config=a.b. &&\n+\tgit for-each-repo --config=\"'\\''.b\"\n+'\n+\n test_done\n-- \n2.38.0.1280.g8136eb6fab2\n\n"},{"id":"466257","messageId":"patch-v2-2.9-6cd0d6faf3c-20221101T225823Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"[PATCH v2 2/9] config tests: cover blind spots in git_die_config() tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-01T23:05:13Z","receivedAt":"2022-11-01T23:05:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"There were no tests checking for the output of the git_die_config()\nfunction in the config API, added in 5a80e97c827 (config: add\n`git_die_config()` to the config-set API, 2014-08-07). We only tested\n\"test_must_fail\", but didn't assert the output.\n\nLet's check for that by extending the existing tests, and adding a new\none for \"fetch.negotiationAlgorithm\" so that we have a test for a user\nof git_config_get_string*() calling git_die_config().\n\nThe other ones are testing:\n\n- For *-resolve.sh: A custom call to git_die_config(), or via\n  git_config_get_notes_strategy()\n- For *-prune.sh: A call via git_config_get_expiry().\n\nWe also cover both the \"from command-line config\" and \"in file..at\nline\" cases here.\n\nThe clobbering of existing \".git/config\" files here is so that we're\nnot implicitly testing the line count of the default config.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-\n t/t5304-prune.sh                     | 12 ++++++++++--\n t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++\n 3 files changed, 32 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh\nindex 141d3e4ca4d..9bd5dbf341f 100755\n--- a/t/t3309-notes-merge-auto-resolve.sh\n+++ b/t/t3309-notes-merge-auto-resolve.sh\n@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '\n \n test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '\n \tgit config core.notesRef refs/notes/y &&\n-\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z &&\n+\tcat >expect <<-\\EOF &&\n+\terror: unknown notes merge strategy foo\n+\tfatal: unable to parse '\\''notes.mergeStrategy'\\'' from command-line config\n+\tEOF\n+\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z 2>actual &&\n+\ttest_cmp expect actual &&\n \t# Verify no changes (y)\n \tverify_notes y y\n '\ndiff --git a/t/t5304-prune.sh b/t/t5304-prune.sh\nindex 8ae314af585..c8fa962b397 100755\n--- a/t/t5304-prune.sh\n+++ b/t/t5304-prune.sh\n@@ -64,8 +64,16 @@ test_expect_success 'gc: implicit prune --expire' '\n '\n \n test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '\n-\tgit config gc.pruneExpire invalid &&\n-\ttest_must_fail git gc\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t>repo/.git/config &&\n+\tgit -C repo config gc.pruneExpire invalid &&\n+\tcat >expect <<-\\EOF &&\n+\terror: Invalid gc.pruneexpire: '\\''invalid'\\''\n+\tfatal: bad config variable '\\''gc.pruneexpire'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_must_fail git -C repo gc 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'gc: start with ok gc.pruneExpire' '\ndiff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh\nindex 165427d57e5..b55a9f65e6b 100755\n--- a/t/t5552-skipping-fetch-negotiator.sh\n+++ b/t/t5552-skipping-fetch-negotiator.sh\n@@ -3,6 +3,22 @@\n test_description='test skipping fetch negotiator'\n . ./test-lib.sh\n \n+test_expect_success 'fetch.negotiationalgorithm config' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\tcat >repo/.git/config <<-\\EOF &&\n+\t[fetch]\n+\tnegotiationAlgorithm\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''fetch.negotiationalgorithm'\\''\n+\tfatal: bad config variable '\\''fetch.negotiationalgorithm'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_expect_code 128 git -C repo fetch >out 2>actual &&\n+\ttest_must_be_empty out &&\n+\ttest_cmp expect actual\n+'\n+\n have_sent () {\n \twhile test \"$#\" -ne 0\n \tdo\n-- \n2.38.0.1280.g8136eb6fab2\n\n"},{"id":"466258","messageId":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com","subject":"[PATCH v2 0/9] config API: make \"multi\" safe, fix numerous segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-01T23:05:11Z","receivedAt":"2022-11-01T23:05:52Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This series fixes numerous segfaults in config API users, because they\ndidn't expect *_get_multi() to hand them a string_list with a NULL in\nit given config like \"[a] key\" (note, no \"=\"'s).\n\nA larger general overview at:\nhttps://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/\n\nChanges since v1:\n \n * Fixed that \"ret < 1\" v.s. \"ret < 0\" thinko in the commit message\n   (code was fine).\n\n * This is now much reduced in scope. The v1 was 10 patches, this is\n   9, but as the range-diff shows there's 3x new tests at the\n   beginning. So the meat of this is smaller.\n\n * The previous main fix is now in 7-8/9 instead of one patch, I split\n   up the test addition (starting with test_expect_failure) and the\n   fix.\n\n * There's no more \"known key\" API that'll BUG() out if we get < 0.\n\n * There's no more \"lookup_value\". We just leave the API users that\n   only care if there is a list in-place.\n\n * The digression to add \"const\"-ing to the \"struct string_list\" is\n   gone, and the change to use unsorted_string_list_has_string() in\n   builtin/gc.c. I can submit that on top of this.\n\n * Rewrote/redid some things to make subsequent diffs\n   smaller. E.g. 4/9 makes 5/9 and especialy 6/9 smaller.\n\n * Renamed the new helper from git_config_get_value_multi() to\n   git_config_get_string_multi().\n\n * There's still a low-level git_config_get_value_multi(). The updated\n   8/9 commit message makes the case for it, i.e. as opposed to having\n   all of *_multi() have the equivalent of \"--type=string\" semantics\n   (although we don't expose that via the \"git config\" tool...).\n\nPassing CI for this at:\nhttps://github.com/avar/git/tree/avar/have-git_configset_get_value-use-dest-and-int-pattern-2\n\nÆvar Arnfjörð Bjarmason (9):\n  for-each-repo tests: test bad --config keys\n  config tests: cover blind spots in git_die_config() tests\n  config tests: add \"NULL\" tests for *_get_value_multi()\n  versioncmp.c: refactor config reading next commit\n  config API: have *_multi() return an \"int\" and take a \"dest\"\n  for-each-repo: error on bad --config\n  config API users: test for *_get_value_multi() segfaults\n  config API: add \"string\" version of *_value_multi(), fix segfaults\n  for-each-repo: with bad config, don't conflate <path> and <cmd>\n\n builtin/for-each-repo.c              | 14 ++---\n builtin/gc.c                         |  6 +-\n builtin/log.c                        |  5 +-\n builtin/submodule--helper.c          |  6 +-\n config.c                             | 88 +++++++++++++++++++++++-----\n config.h                             | 50 +++++++++++++---\n pack-bitmap.c                        |  6 +-\n submodule.c                          |  3 +-\n t/helper/test-config.c               |  6 +-\n t/t0068-for-each-repo.sh             | 19 ++++++\n t/t1308-config-set.sh                | 30 ++++++++++\n t/t3309-notes-merge-auto-resolve.sh  |  7 ++-\n t/t4202-log.sh                       | 15 +++++\n t/t5304-prune.sh                     | 12 +++-\n t/t5310-pack-bitmaps.sh              | 21 +++++++\n t/t5552-skipping-fetch-negotiator.sh | 16 +++++\n t/t7004-tag.sh                       | 17 ++++++\n t/t7413-submodule-is-active.sh       | 16 +++++\n t/t7900-maintenance.sh               | 38 ++++++++++++\n versioncmp.c                         | 22 ++++---\n 20 files changed, 337 insertions(+), 60 deletions(-)\n\nRange-diff against v1:\n -:  ----------- >  1:  b8fd3bea4d1 for-each-repo tests: test bad --config keys\n -:  ----------- >  2:  6cd0d6faf3c config tests: cover blind spots in git_die_config() tests\n 8:  e7568dbe6fe =  3:  f2a8766a802 config tests: add \"NULL\" tests for *_get_value_multi()\n -:  ----------- >  4:  42cfc61202d versioncmp.c: refactor config reading next commit\n 1:  eefa253ab1f !  5:  48fb7cbf585 config API: have *_multi() return an \"int\" and take a \"dest\"\n    @@ Metadata\n      ## Commit message ##\n         config API: have *_multi() return an \"int\" and take a \"dest\"\n     \n    -    The git_configset_get_value_multi() function added in 3c8687a73ee (add\n    -    `config_set` API for caching config-like files, 2014-07-28) is a\n    -    fundamental part of of the config API, and\n    -    e.g. \"git_config_get_value()\" and others are implemented in terms of\n    -    it.\n    +    Have the \"git_configset_get_value_multi()\" function and its siblings\n    +    return an \"int\" and populate a \"**dest\" parameter like every other\n    +    git_configset_get_*()\" in the API.\n     \n    -    But it has had the limitation that configset_find_element() calls\n    -    git_config_parse_key(), but then throws away the distinction between a\n    -    \"ret < 1\" return value from it, and return values that indicate a key\n    -    doesn't exist. As a result the git_config_get_value_multi() function\n    -    would either return a \"const struct string_list *\", or NULL.\n    +    As we'll see in in subsequent commits this fixes a blind spot in the\n    +    API where it wasn't possible to tell whether a list was empty from\n    +    whether a config key existed. We'll take advantage of that in\n    +    subsequent commits, but for now we're faithfully converting existing\n    +    API callers.\n     \n    -    By changing the *_multi() function to return an \"int\" for the status\n    -    and to write to a \"const struct string_list **dest\" parameter we can\n    -    avoid losing this information. API callers can now do:\n    +    See [1] for the initial addition of \"git_configset_get_value_multi()\"\n     \n    -            const struct string_list *dest;\n    -            int ret;\n    +    1. 3c8687a73ee (add `config_set` API for caching config-like files,\n    +       2014-07-28).\n     \n    -            ret = git_config_get_value_multi(key, &dest);\n    -            if (ret < 1)\n    -                    die(\"bad key: %s\", key);\n    -            else if (ret)\n    -                    ; /* key does not exist */\n    -            else\n    -                    ; /* got key, can use \"dest\" */\n    -\n    -    A \"get_knownkey_value_multi\" variant is also provided, which will\n    -    BUG() out in the \"ret < 1\" case. This is useful in the cases where we\n    -    hardcode the keyname in our source code, and therefore use the more\n    -    idiomatic pattern of:\n    -\n    -            if (!git_config_get_value_multi(key, &dest)\n    -                    ; /* got key, can use \"dest\" */\n    -            else\n    -                    ; /* key does not exist */\n    +    A logical follow-up to this would be to change the various \"*_get_*()\"\n    +    functions to ferry the git_configset_get_value() return value to their\n    +    own callers, e.g. git_configset_get_int() returns \"1\" rather than\n    +    ferrying up the \"-1\" that \"git_configset_get_value()\" might return,\n    +    but that's not being done in this series\n     \n    -    The \"knownkey\" name was picked instead of e.g. \"const\" to avoid a\n    -    repeat of the issues noted in f1de981e8b6 (config: fix leaks from\n    -    git_config_get_string_const(), 2020-08-14) and 9a53219f69b (config:\n    -    drop git_config_get_string_const(), 2020-08-17). API users might think\n    -    that \"const\" means that the value(s) don't need to be free'd.\n    +    Most of this is straightforward, commentary on cases that stand out:\n     \n    -    As noted in commentary here we treat git_die_config() as a\n    -    special-case, i.e. we assume that a value we're complaining about has\n    -    already had its key pass the git_config_parse_key() check.\n    +    - As we've tested for in a preceding commit we can rely on getting the\n    +      config list in git_die_config(), and as we need to handle the new\n    +      return value let's BUG() out if we can't acquire it.\n     \n    -    Likewise we consider the keys passed to \"t/helper/test-config.c\" to be\n    -    \"knownkey\", and will emit a BUG() if they don't pass\n    -    git_config_parse_key(). Those will come from our *.sh tests, so\n    -    they're also \"known keys\" coming from our sources.\n    +    - In \"builtin/for-each-ref.c\" we could preserve the comment added in\n    +      6c62f015520, but now that we're directly using the documented\n    +      repo_config_get_value_multi() value it's just narrating something that\n    +      should be obvious from the API use, so let's drop it.\n     \n    -    A logical follow-up to this would be to change the various \"*_get_*()\"\n    -    functions to ferry the git_configset_get_value() return value to their\n    -    own callers, e.g.:\n    +    - The loops after getting the \"list\" value in \"builtin/gc.c\" could\n    +      also make use of \"unsorted_string_list_has_string()\" instead of using\n    +      that loop, but let's leave that for now.\n     \n    -            diff --git a/config.c b/config.c\n    -            index 094ad899e0b..7e8ee4cfec1 100644\n    -            --- a/config.c\n    -            +++ b/config.c\n    -            @@ -2479,11 +2479,14 @@ static int git_configset_get_string_tmp(struct config_set *cs, const char *key,\n    -             int git_configset_get_int(struct config_set *cs, const char *key, int *dest)\n    -             {\n    -                    const char *value;\n    -            -       if (!git_configset_get_value(cs, key, &value)) {\n    -            -               *dest = git_config_int(key, value);\n    -            -               return 0;\n    -            -       } else\n    -            -               return 1;\n    -            +       int ret;\n    -            +\n    -            +       if ((ret = git_configset_get_value(cs, key, &value)))\n    -            +               goto done;\n    -            +\n    -            +       *dest = git_config_int(key, value);\n    -            +done:\n    -            +       return ret;\n    -             }\n    +    - We have code e.g. in \"builtin/submodule--helper.c\" that only wants\n    +      to check if a config key exists, and would be better served with\n    +      another API, but let's keep using \"git_configset_get_value_multi()\"\n    +      for now.\n     \n    -             int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest)\n    +    - In \"versioncmp.c\" we now use the return value of the functions,\n    +      instead of checking if the lists are still non-NULL. This is strictly\n    +      speaking unnecessary, but makes the API use consistent with the rest,\n    +      but more importantly...\n     \n    -    Most of those callers don't care, and call those functions as\n    -    \"if (!func(...))\", but if they do they'll be able to tell key\n    -    non-existence from errors we encounter. Before this change those API\n    -    users would have been unable to tell the two conditions apart, as\n    -    git_configset_get_value() hid the difference.\n    +    - ...because we always check our return values we can assert that with\n    +      the RESULT_MUST_BE_USED macro added in 1e8697b5c4e (submodule--helper:\n    +      check repo{_submodule,}_init() return values, 2022-09-01)\n     \n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/for-each-repo.c ##\n     @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n    - {\n      \tstatic const char *config_key = NULL;\n      \tint i, result = 0;\n    --\tconst struct string_list *values;\n    -+\tconst struct string_list *values = NULL;\n    + \tconst struct string_list *values;\n    ++\tint err;\n      \n      \tconst struct option options[] = {\n      \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n    @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, cons\n      \n     -\tvalues = repo_config_get_value_multi(the_repository,\n     -\t\t\t\t\t     config_key);\n    -+\trepo_config_get_value_multi(the_repository, config_key, &values);\n    +-\n    +-\t/*\n    +-\t * Do nothing on an empty list, which is equivalent to the case\n    +-\t * where the config variable does not exist at all.\n    +-\t */\n    +-\tif (!values)\n    ++\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n    ++\tif (err < 0)\n    ++\t\treturn 0;\n    ++\telse if (err)\n    + \t\treturn 0;\n      \n    - \t/*\n    - \t * Do nothing on an empty list, which is equivalent to the case\n    + \tfor (i = 0; !result && i < values->nr; i++)\n     \n      ## builtin/gc.c ##\n     @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const char *prefix)\n    @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const\n      \n     -\tlist = git_config_get_value_multi(key);\n     -\tif (list) {\n    -+\tif (!git_config_get_knownkey_value_multi(key, &list)) {\n    ++\tif (!git_config_get_value_multi(key, &list)) {\n      \t\tfor_each_string_list_item(item, list) {\n      \t\t\tif (!strcmp(maintpath, item->string)) {\n      \t\t\t\tfound = 1;\n    @@ builtin/gc.c: static int maintenance_unregister(int argc, const char **argv, con\n      \n     -\tlist = git_config_get_value_multi(key);\n     -\tif (list) {\n    -+\tif (!git_config_get_knownkey_value_multi(key, &list)) {\n    ++\tif (!git_config_get_value_multi(key, &list)) {\n      \t\tfor_each_string_list_item(item, list) {\n      \t\t\tif (!strcmp(maintpath, item->string)) {\n      \t\t\t\tfound = 1;\n    @@ builtin/log.c: static void set_default_decoration_filter(struct decoration_filte\n     +\tconst struct string_list *config_exclude;\n      \n     -\tif (config_exclude) {\n    -+\tif (!git_config_get_knownkey_value_multi(\"log.excludeDecoration\",\n    -+\t\t\t\t\t      &config_exclude)) {\n    ++\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n    ++\t\t\t\t\t&config_exclude)) {\n      \t\tstruct string_list_item *item;\n      \t\tfor_each_string_list_item(item, config_exclude)\n      \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\n    @@ builtin/submodule--helper.c: static int module_update(int argc, const char **arg\n      \t\tstruct init_cb info = INIT_CB_INIT;\n     +\t\tconst struct string_list *values;\n      \n    - \t\tif (module_list_compute(argc, argv, opt.prefix,\n    + \t\tif (module_list_compute(argv, opt.prefix,\n      \t\t\t\t\t&pathspec2, &list) < 0) {\n     @@ builtin/submodule--helper.c: static int module_update(int argc, const char **argv, const char *prefix)\n      \t\t * If there are no path args and submodule.active is set then,\n    @@ config.c: static int configset_add_value(struct config_set *cs, const char *key,\n      \t/*\n      \t * Since the keys are being fed by git_config*() callback mechanism, they\n      \t * are already normalized. So simply add them without any further munging.\n    -@@ config.c: int git_configset_add_parameters(struct config_set *cs)\n    +@@ config.c: int git_configset_add_file(struct config_set *cs, const char *filename)\n      int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n      {\n      \tconst struct string_list *values = NULL;\n    @@ config.c: int git_configset_add_parameters(struct config_set *cs)\n      }\n      \n     -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n    -+static int git_configset_get_value_multi_1(struct config_set *cs, const char *key,\n    -+\t\t\t\t\t   const struct string_list **dest,\n    -+\t\t\t\t\t   int knownkey)\n    ++int git_configset_get_value_multi(struct config_set *cs, const char *key,\n    ++\t\t\t\t  const struct string_list **dest)\n      {\n     -\tstruct config_set_element *e = configset_find_element(cs, key);\n     -\treturn e ? &e->value_list : NULL;\n    @@ config.c: int git_configset_add_parameters(struct config_set *cs)\n     +\tint ret;\n     +\n     +\tret = configset_find_element(cs, key, &e);\n    -+\tif (ret < 0 && knownkey)\n    -+\t\tBUG(\"*_get_knownkey_*() only accepts known-good (hardcoded) keys, but '%s' is bad!\", key);\n    -+\telse if (ret < 0)\n    ++\tif (ret < 0)\n     +\t\treturn ret;\n     +\telse if (!e)\n     +\t\treturn 1;\n     +\t*dest = &e->value_list;\n     +\n     +\treturn 0;\n    -+}\n    -+\n    -+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n    -+\t\t\t\t  const struct string_list **dest)\n    -+{\n    -+\treturn git_configset_get_value_multi_1(cs, key, dest, 0);\n    -+}\n    -+\n    -+int git_configset_get_knownkey_value_multi(struct config_set *cs,\n    -+\t\t\t\t\t   const char *const key,\n    -+\t\t\t\t\t   const struct string_list **dest)\n    -+{\n    -+\treturn git_configset_get_value_multi_1(cs, key, dest, 1);\n      }\n      \n      int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n    @@ config.c: int repo_config_get_value(struct repository *repo,\n      \n     -const struct string_list *repo_config_get_value_multi(struct repository *repo,\n     -\t\t\t\t\t\t      const char *key)\n    -+int repo_config_get_value_multi(struct repository *repo,\n    -+\t\t\t\tconst char *key,\n    ++int repo_config_get_value_multi(struct repository *repo, const char *key,\n     +\t\t\t\tconst struct string_list **dest)\n      {\n      \tgit_config_check_init(repo);\n     -\treturn git_configset_get_value_multi(repo->config, key);\n     +\treturn git_configset_get_value_multi(repo->config, key, dest);\n    -+}\n    -+\n    -+int repo_config_get_knownkey_value_multi(struct repository *repo,\n    -+\t\t\t\t\t const char *const key,\n    -+\t\t\t\t\t const struct string_list **dest)\n    -+{\n    -+\tgit_config_check_init(repo);\n    -+\treturn git_configset_get_knownkey_value_multi(repo->config, key, dest);\n      }\n      \n      int repo_config_get_string(struct repository *repo,\n    @@ config.c: int git_config_get_value(const char *key, const char **value)\n      \n     -const struct string_list *git_config_get_value_multi(const char *key)\n     +int git_config_get_value_multi(const char *key, const struct string_list **dest)\n    -+{\n    -+\treturn repo_config_get_value_multi(the_repository, key, dest);\n    -+}\n    -+\n    -+int git_config_get_knownkey_value_multi(const char *const key,\n    -+\t\t\t\t\tconst struct string_list **dest)\n      {\n     -\treturn repo_config_get_value_multi(the_repository, key);\n    -+\treturn repo_config_get_knownkey_value_multi(the_repository, key, dest);\n    ++\treturn repo_config_get_value_multi(the_repository, key, dest);\n      }\n      \n      int git_config_get_string(const char *key, char **dest)\n    @@ config.c: void git_die_config(const char *key, const char *err, ...)\n      \t\tva_end(params);\n      \t}\n     -\tvalues = git_config_get_value_multi(key);\n    -+\n    -+\t/*\n    -+\t * We don't have a \"const\" key here, but we should definitely\n    -+\t * have one that's passed git_config_parse_key() already, if\n    -+\t * we're at the point of complaining about its value. So let's\n    -+\t * use *_knownkey_value_multi() here to get that BUG(...).\n    -+\t */\n    -+\tif (git_config_get_knownkey_value_multi(key, &values))\n    -+\t\tBUG(\"key '%s' does not exist, should not be given to git_die_config()\",\n    -+\t\t    key);\n    ++\tif (git_config_get_value_multi(key, &values))\n    ++\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n      \tkv_info = values->items[values->nr - 1].util;\n      \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n      }\n     \n      ## config.h ##\n     @@ config.h: int git_configset_add_parameters(struct config_set *cs);\n    - \n      /**\n       * Finds and returns the value list, sorted in order of increasing priority\n    -- * for the configuration variable `key` and config set `cs`. When the\n    +  * for the configuration variable `key` and config set `cs`. When the\n     - * configuration variable `key` is not found, returns NULL. The caller\n     - * should not free or modify the returned pointer, as it is owned by the cache.\n    -+ * for the configuration variable `key` and config set `cs`.\n    -+ *\n    -+ * When the configuration variable `key` is not found, returns 1\n    -+ * without touching `value`.\n    ++ * configuration variable `key` is not found, returns 1 without touching\n    ++ * `value`.\n     + *\n     + * The key will be parsed for validity with git_config_parse_key(), on\n    -+ * error a negative value will be returned. See\n    -+ * git_configset_get_knownkey_value_multi() for a version of this which\n    -+ * BUG()s out on negative return values.\n    ++ * error a negative value will be returned.\n     + *\n     + * The caller should not free or modify the returned pointer, as it is\n     + * owned by the cache.\n    -+ */\n    -+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n    -+\t\t\t\t  const struct string_list **dest);\n    -+\n    -+/**\n    -+ * Like git_configset_get_value_multi(), but BUG()s out if the return\n    -+ * value is < 0. Use it for keys known to pass git_config_parse_key(),\n    -+ * i.e. those hardcoded in the code, and never user-provided keys.\n       */\n     -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n    -+int git_configset_get_knownkey_value_multi(struct config_set *cs,\n    -+\t\t\t\t\t   const char *const key,\n    -+\t\t\t\t\t   const struct string_list **dest);\n    ++RESULT_MUST_BE_USED\n    ++int git_configset_get_value_multi(struct config_set *cs, const char *key,\n    ++\t\t\t\t  const struct string_list **dest);\n      \n      /**\n       * Clears `config_set` structure, removes all saved variable-value pairs.\n    @@ config.h: struct repository;\n      \t\t\t  const char *key, const char **value);\n     -const struct string_list *repo_config_get_value_multi(struct repository *repo,\n     -\t\t\t\t\t\t      const char *key);\n    -+int repo_config_get_value_multi(struct repository *repo,\n    -+\t\t\t\tconst char *key,\n    ++RESULT_MUST_BE_USED\n    ++int repo_config_get_value_multi(struct repository *repo, const char *key,\n     +\t\t\t\tconst struct string_list **dest);\n    -+int repo_config_get_knownkey_value_multi(struct repository *repo,\n    -+\t\t\t\t\t const char *const key,\n    -+\t\t\t\t\t const struct string_list **dest);\n      int repo_config_get_string(struct repository *repo,\n      \t\t\t   const char *key, char **dest);\n      int repo_config_get_string_tmp(struct repository *repo,\n    @@ config.h: int git_config_get_value(const char *key, const char **value);\n     + *\n     + * The caller should not free or modify the returned pointer, as it is\n     + * owned by the cache.\n    -+ */\n    -+int git_config_get_value_multi(const char *key,\n    -+\t\t\t       const struct string_list **dest);\n    -+\n    -+/**\n    -+ * A wrapper for git_config_get_value_multi() which does for it what\n    -+ * git_configset_get_knownkey_value_multi() does for\n    -+ * git_configset_get_value_multi().\n       */\n     -const struct string_list *git_config_get_value_multi(const char *key);\n    -+int git_config_get_knownkey_value_multi(const char *const key,\n    -+\t\t\t\t\tconst struct string_list **dest);\n    ++RESULT_MUST_BE_USED\n    ++int git_config_get_value_multi(const char *key,\n    ++\t\t\t       const struct string_list **dest);\n      \n      /**\n       * Resets and invalidates the config cache.\n    @@ pack-bitmap.c: int bitmap_is_midx(struct bitmap_index *bitmap_git)\n     -\treturn repo_config_get_value_multi(r, \"pack.preferbitmaptips\");\n     +\tconst struct string_list *dest;\n     +\n    -+\tif (!repo_config_get_knownkey_value_multi(r, \"pack.preferbitmaptips\",\n    -+\t\t\t\t\t       &dest))\n    ++\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n     +\t\treturn dest;\n     +\treturn NULL;\n      }\n    @@ submodule.c: int is_tree_submodule_active(struct repository *repo,\n      \t/* submodule.active is set */\n     -\tsl = repo_config_get_value_multi(repo, \"submodule.active\");\n     -\tif (sl) {\n    -+\tif (!repo_config_get_knownkey_value_multi(repo, \"submodule.active\", &sl)) {\n    ++\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n      \t\tstruct pathspec ps;\n      \t\tstruct strvec args = STRVEC_INIT;\n      \t\tconst struct string_list_item *item;\n    @@ t/helper/test-config.c: int cmd__config(int argc, const char **argv)\n      \t} else if (argc == 3 && !strcmp(argv[1], \"get_value_multi\")) {\n     -\t\tstrptr = git_config_get_value_multi(argv[2]);\n     -\t\tif (strptr) {\n    -+\t\tif (!git_config_get_knownkey_value_multi(argv[2], &strptr)) {\n    ++\t\tif (!git_config_get_value_multi(argv[2], &strptr)) {\n      \t\t\tfor (i = 0; i < strptr->nr; i++) {\n      \t\t\t\tv = strptr->items[i].string;\n      \t\t\t\tif (!v)\n    @@ t/helper/test-config.c: int cmd__config(int argc, const char **argv)\n      \t\t}\n     -\t\tstrptr = git_configset_get_value_multi(&cs, argv[2]);\n     -\t\tif (strptr) {\n    -+\t\tif (!git_configset_get_knownkey_value_multi(&cs, argv[2], &strptr)) {\n    ++\t\tif (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {\n      \t\t\tfor (i = 0; i < strptr->nr; i++) {\n      \t\t\t\tv = strptr->items[i].string;\n      \t\t\t\tif (!v)\n     \n      ## versioncmp.c ##\n     @@ versioncmp.c: int versioncmp(const char *s1, const char *s2)\n    - \t}\n    - \n      \tif (!initialized) {\n    --\t\tconst struct string_list *deprecated_prereleases;\n    -+\t\tconst struct string_list *deprecated_prereleases = NULL;\n    -+\n    + \t\tconst char *const newk = \"versionsort.suffix\";\n    + \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n    ++\t\tconst struct string_list *newl;\n    + \t\tconst struct string_list *oldl;\n    ++\t\tint new = git_config_get_value_multi(newk, &newl);\n    ++\t\tint old = git_config_get_value_multi(oldk, &oldl);\n    + \n    +-\t\tprereleases = git_config_get_value_multi(newk);\n    +-\t\toldl = git_config_get_value_multi(oldk);\n    +-\t\tif (prereleases && oldl)\n    ++\t\tif (!new && !old)\n    + \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n    +-\t\telse if (!prereleases)\n    ++\t\tif (!new)\n    ++\t\t\tprereleases = newl;\n    ++\t\telse if (!old)\n    + \t\t\tprereleases = oldl;\n    + \n      \t\tinitialized = 1;\n    --\t\tprereleases = git_config_get_value_multi(\"versionsort.suffix\");\n    --\t\tdeprecated_prereleases = git_config_get_value_multi(\"versionsort.prereleasesuffix\");\n    -+\t\tgit_config_get_knownkey_value_multi(\"versionsort.suffix\",\n    -+\t\t\t\t\t\t &prereleases);\n    -+\t\tgit_config_get_value_multi(\"versionsort.prereleasesuffix\",\n    -+\t\t\t\t\t   &deprecated_prereleases);\n    -+\n    - \t\tif (prereleases) {\n    - \t\t\tif (deprecated_prereleases)\n    - \t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n 2:  e17de2a2664 <  -:  ----------- for-each-repo: error on bad --config\n 3:  3519d3de010 <  -:  ----------- config API: mark *_multi() with RESULT_MUST_BE_USED\n 4:  40b3cc9b8d4 <  -:  ----------- string-list API: mark \"struct_string_list\" to \"for_each_string_list\" const\n 5:  b32b2e99aba <  -:  ----------- string-list API: make has_string() and list_lookup() \"const\"\n 6:  9c36f17481b <  -:  ----------- builtin/gc.c: use \"unsorted_string_list_has_string()\" where appropriate\n 7:  c01f7d85c94 <  -:  ----------- config API: add and use \"lookup_value\" functions\n 9:  bda9d504b89 <  -:  ----------- config API: add \"string\" version of *_value_multi(), fix segfaults\n -:  ----------- >  6:  a0c29d46556 for-each-repo: error on bad --config\n -:  ----------- >  7:  c12805f3d55 config API users: test for *_get_value_multi() segfaults\n -:  ----------- >  8:  6b76f9eac90 config API: add \"string\" version of *_value_multi(), fix segfaults\n10:  b59cbed8f61 !  9:  e2f8f7c52e3 for-each-repo: with bad config, don't conflate <path> and <cmd>\n    @@ Commit message\n         running commands.\n     \n         As noted in the preceding commit the fix is to move to a safer\n    -    \"*_multi_string()\" version of the *__multi() API. This change is\n    +    \"*_string_multi()\" version of the *_multi() API. This change is\n         separated from the rest because those all segfaulted. In this change\n         we ended up with different behavior.\n     \n    @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, cons\n      \t\tdie(_(\"missing --config=<config>\"));\n      \n     -\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n    -+\terr = repo_config_get_value_multi_string(the_repository, config_key, &values);\n    ++\terr = repo_config_get_string_multi(the_repository, config_key, &values);\n      \tif (err < 0)\n      \t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n      \t\t\t       for_each_repo_usage, options, config_key);\n-- \n2.38.0.1280.g8136eb6fab2\n\n"},{"id":"466259","messageId":"patch-v2-3.9-f2a8766a802-20221101T225823Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"[PATCH v2 3/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-01T23:05:14Z","receivedAt":"2022-11-01T23:05:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"A less well known edge case in the config format is that keys can be\nvalue-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\nare equivalent as far as \"--type=bool\" is concerned:\n\n\t[a]key\n\t[a]key = true\n\nBut as far as our parser is concerned the values for these two are\nNULL, and \"true\". I.e. for a sequence like:\n\n\t[a]key=x\n\t[a]key\n\t[a]key=y\n\nWe get a \"struct string_list\" with \"string\" members with \".string\"\nvalues of:\n\n\t{ \"x\", NULL, \"y\" }\n\nThis behavior goes back to the initial implementation of\ngit_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n2005-10-10).\n\nWhen the \"t/t1308-config-set.sh\" tests were added in [1] only one of\nthe three \"(NULL)\" lines in \"t/helper/test-config.c\" had any test\ncoverage. This change adds tests that stress the remaining two.\n\n1. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t1308-config-set.sh | 30 ++++++++++++++++++++++++++++++\n 1 file changed, 30 insertions(+)\n\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex b38e158d3b2..561e82f1808 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -146,6 +146,36 @@ test_expect_success 'find multiple values' '\n \tcheck_config get_value_multi case.baz sam bat hask\n '\n \n+test_expect_success 'emit multi values from configset with NULL entry' '\n+\ttest_when_finished \"rm -f my.config\" &&\n+\tcat >my.config <<-\\EOF &&\n+\t[a]key=x\n+\t[a]key\n+\t[a]key=y\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\tx\n+\t(NULL)\n+\ty\n+\tEOF\n+\ttest-tool config configset_get_value_multi a.key my.config >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'multi values from configset with a last NULL entry' '\n+\ttest_when_finished \"rm -f my.config\" &&\n+\tcat >my.config <<-\\EOF &&\n+\t[a]key=x\n+\t[a]key=y\n+\t[a]key\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\t(NULL)\n+\tEOF\n+\ttest-tool config configset_get_value a.key my.config >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'find value from a configset' '\n \tcat >config2 <<-\\EOF &&\n \t[case]\n-- \n2.38.0.1280.g8136eb6fab2\n\n"},{"id":"466260","messageId":"patch-v2-4.9-42cfc61202d-20221101T225823Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"[PATCH v2 4/9] versioncmp.c: refactor config reading next commit","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-01T23:05:15Z","receivedAt":"2022-11-01T23:05:56Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the reading of the versionSort.suffix and\nversionSort.prereleaseSuffix configuration variables to stay within\nthe bounds of our CodingGuidelines when it comes to line length, and\nta avoid repeating ourselves.\n\nLet's also split out the names of the config variables into variables\nof our own, so we don't have to repeat ourselves, and refactor the\nnested if/else to avoid indenting it, and the existing bracing style\nissue.\n\nThis all helps with the subsequent commit, where we'll need to start\nchecking different git_config_get_value_multi() return value. See\nc026557a373 (versioncmp: generalize version sort suffix reordering,\n2016-12-08) for the original implementation of most of this.\n\nMoving the \"initialized = 1\" assignment allows us to move some of this\nto the variable declarations in the subsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n versioncmp.c | 19 +++++++++++--------\n 1 file changed, 11 insertions(+), 8 deletions(-)\n\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 069ee94a4d7..323f5d35ea8 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)\n \t}\n \n \tif (!initialized) {\n-\t\tconst struct string_list *deprecated_prereleases;\n+\t\tconst char *const newk = \"versionsort.suffix\";\n+\t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *oldl;\n+\n+\t\tprereleases = git_config_get_value_multi(newk);\n+\t\toldl = git_config_get_value_multi(oldk);\n+\t\tif (prereleases && oldl)\n+\t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n+\t\telse if (!prereleases)\n+\t\t\tprereleases = oldl;\n+\n \t\tinitialized = 1;\n-\t\tprereleases = git_config_get_value_multi(\"versionsort.suffix\");\n-\t\tdeprecated_prereleases = git_config_get_value_multi(\"versionsort.prereleasesuffix\");\n-\t\tif (prereleases) {\n-\t\t\tif (deprecated_prereleases)\n-\t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n-\t\t} else\n-\t\t\tprereleases = deprecated_prereleases;\n \t}\n \tif (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,\n \t\t\t\t\t    &diff))\n-- \n2.38.0.1280.g8136eb6fab2\n\n"},{"id":"466261","messageId":"patch-v2-5.9-48fb7cbf585-20221101T225823Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"[PATCH v2 5/9] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-01T23:05:16Z","receivedAt":"2022-11-01T23:06:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Have the \"git_configset_get_value_multi()\" function and its siblings\nreturn an \"int\" and populate a \"**dest\" parameter like every other\ngit_configset_get_*()\" in the API.\n\nAs we'll see in in subsequent commits this fixes a blind spot in the\nAPI where it wasn't possible to tell whether a list was empty from\nwhether a config key existed. We'll take advantage of that in\nsubsequent commits, but for now we're faithfully converting existing\nAPI callers.\n\nSee [1] for the initial addition of \"git_configset_get_value_multi()\"\n\n1. 3c8687a73ee (add `config_set` API for caching config-like files,\n   2014-07-28).\n\nA logical follow-up to this would be to change the various \"*_get_*()\"\nfunctions to ferry the git_configset_get_value() return value to their\nown callers, e.g. git_configset_get_int() returns \"1\" rather than\nferrying up the \"-1\" that \"git_configset_get_value()\" might return,\nbut that's not being done in this series\n\nMost of this is straightforward, commentary on cases that stand out:\n\n- As we've tested for in a preceding commit we can rely on getting the\n  config list in git_die_config(), and as we need to handle the new\n  return value let's BUG() out if we can't acquire it.\n\n- In \"builtin/for-each-ref.c\" we could preserve the comment added in\n  6c62f015520, but now that we're directly using the documented\n  repo_config_get_value_multi() value it's just narrating something that\n  should be obvious from the API use, so let's drop it.\n\n- The loops after getting the \"list\" value in \"builtin/gc.c\" could\n  also make use of \"unsorted_string_list_has_string()\" instead of using\n  that loop, but let's leave that for now.\n\n- We have code e.g. in \"builtin/submodule--helper.c\" that only wants\n  to check if a config key exists, and would be better served with\n  another API, but let's keep using \"git_configset_get_value_multi()\"\n  for now.\n\n- In \"versioncmp.c\" we now use the return value of the functions,\n  instead of checking if the lists are still non-NULL. This is strictly\n  speaking unnecessary, but makes the API use consistent with the rest,\n  but more importantly...\n\n- ...because we always check our return values we can assert that with\n  the RESULT_MUST_BE_USED macro added in 1e8697b5c4e (submodule--helper:\n  check repo{_submodule,}_init() return values, 2022-09-01)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c     | 13 ++++-----\n builtin/gc.c                |  6 ++--\n builtin/log.c               |  6 ++--\n builtin/submodule--helper.c |  6 ++--\n config.c                    | 55 ++++++++++++++++++++++++++-----------\n config.h                    | 29 +++++++++++++------\n pack-bitmap.c               |  6 +++-\n submodule.c                 |  3 +-\n t/helper/test-config.c      |  6 ++--\n versioncmp.c                | 11 +++++---\n 10 files changed, 89 insertions(+), 52 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex d45d873f579..7d7685c8a1a 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -29,6 +29,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tstatic const char *config_key = NULL;\n \tint i, result = 0;\n \tconst struct string_list *values;\n+\tint err;\n \n \tconst struct option options[] = {\n \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n@@ -42,14 +43,10 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\tvalues = repo_config_get_value_multi(the_repository,\n-\t\t\t\t\t     config_key);\n-\n-\t/*\n-\t * Do nothing on an empty list, which is equivalent to the case\n-\t * where the config variable does not exist at all.\n-\t */\n-\tif (!values)\n+\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\tif (err < 0)\n+\t\treturn 0;\n+\telse if (err)\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 24ea85c7afd..76cee01e442 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1485,8 +1485,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \telse\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tlist = git_config_get_value_multi(key);\n-\tif (list) {\n+\tif (!git_config_get_value_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1542,8 +1541,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tusage_with_options(builtin_maintenance_unregister_usage,\n \t\t\t\t   options);\n \n-\tlist = git_config_get_value_multi(key);\n-\tif (list) {\n+\tif (!git_config_get_value_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 5eafcf26b49..cc9d92f95da 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tint i;\n \tchar *value = NULL;\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n-\tconst struct string_list *config_exclude =\n-\t\t\tgit_config_get_value_multi(\"log.excludeDecoration\");\n+\tconst struct string_list *config_exclude;\n \n-\tif (config_exclude) {\n+\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t&config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex a7683d35299..53afc2de4af 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -541,6 +541,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t\tNULL\n \t};\n \tint ret = 1;\n+\tconst struct string_list *values;\n \n \targc = parse_options(argc, argv, prefix, module_init_options,\n \t\t\t     git_submodule_helper_usage, 0);\n@@ -552,7 +553,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t * If there are no path args and submodule.active is set then,\n \t * by default, only initialize 'active' modules.\n \t */\n-\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n \t\tmodule_list_active(&list);\n \n \tinfo.prefix = prefix;\n@@ -2716,6 +2717,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \tif (opt.init) {\n \t\tstruct module_list list = MODULE_LIST_INIT;\n \t\tstruct init_cb info = INIT_CB_INIT;\n+\t\tconst struct string_list *values;\n \n \t\tif (module_list_compute(argv, opt.prefix,\n \t\t\t\t\t&pathspec2, &list) < 0) {\n@@ -2728,7 +2730,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \t\t * If there are no path args and submodule.active is set then,\n \t\t * by default, only initialize 'active' modules.\n \t\t */\n-\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n \t\t\tmodule_list_active(&list);\n \n \t\tinfo.prefix = opt.prefix;\ndiff --git a/config.c b/config.c\nindex c058b2c70c3..0b07045ed8c 100644\n--- a/config.c\n+++ b/config.c\n@@ -2275,23 +2275,28 @@ void read_very_early_config(config_fn_t cb, void *data)\n \tconfig_with_options(cb, data, NULL, &opts);\n }\n \n-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n+static int configset_find_element(struct config_set *cs, const char *key,\n+\t\t\t\t  struct config_set_element **dest)\n {\n \tstruct config_set_element k;\n \tstruct config_set_element *found_entry;\n \tchar *normalized_key;\n+\tint ret;\n+\n \t/*\n \t * `key` may come from the user, so normalize it before using it\n \t * for querying entries from the hashmap.\n \t */\n-\tif (git_config_parse_key(key, &normalized_key, NULL))\n-\t\treturn NULL;\n+\tret = git_config_parse_key(key, &normalized_key, NULL);\n+\tif (ret < 0)\n+\t\treturn ret;\n \n \thashmap_entry_init(&k.ent, strhash(normalized_key));\n \tk.key = normalized_key;\n \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n \tfree(normalized_key);\n-\treturn found_entry;\n+\t*dest = found_entry;\n+\treturn 0;\n }\n \n static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n@@ -2300,8 +2305,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n \tstruct string_list_item *si;\n \tstruct configset_list_item *l_item;\n \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n+\tint ret;\n \n-\te = configset_find_element(cs, key);\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret < 0)\n+\t\treturn ret;\n \t/*\n \t * Since the keys are being fed by git_config*() callback mechanism, they\n \t * are already normalized. So simply add them without any further munging.\n@@ -2395,24 +2403,38 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n+\tint ret;\n+\n \t/*\n \t * Follows \"last one wins\" semantic, i.e., if there are multiple matches for the\n \t * queried key in the files of the configset, the value returned will be the last\n \t * value in the value list for that key.\n \t */\n-\tvalues = git_configset_get_value_multi(cs, key);\n+\tret = git_configset_get_value_multi(cs, key, &values);\n \n-\tif (!values)\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!values)\n \t\treturn 1;\n \tassert(values->nr > 0);\n \t*value = values->items[values->nr - 1].string;\n \treturn 0;\n }\n \n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest)\n {\n-\tstruct config_set_element *e = configset_find_element(cs, key);\n-\treturn e ? &e->value_list : NULL;\n+\tstruct config_set_element *e;\n+\tint ret;\n+\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!e)\n+\t\treturn 1;\n+\t*dest = &e->value_list;\n+\n+\treturn 0;\n }\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n@@ -2558,11 +2580,11 @@ int repo_config_get_value(struct repository *repo,\n \treturn git_configset_get_value(repo->config, key, value);\n }\n \n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key)\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest)\n {\n \tgit_config_check_init(repo);\n-\treturn git_configset_get_value_multi(repo->config, key);\n+\treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n int repo_config_get_string(struct repository *repo,\n@@ -2670,9 +2692,9 @@ int git_config_get_value(const char *key, const char **value)\n \treturn repo_config_get_value(the_repository, key, value);\n }\n \n-const struct string_list *git_config_get_value_multi(const char *key)\n+int git_config_get_value_multi(const char *key, const struct string_list **dest)\n {\n-\treturn repo_config_get_value_multi(the_repository, key);\n+\treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n int git_config_get_string(const char *key, char **dest)\n@@ -2819,7 +2841,8 @@ void git_die_config(const char *key, const char *err, ...)\n \t\terror_fn(err, params);\n \t\tva_end(params);\n \t}\n-\tvalues = git_config_get_value_multi(key);\n+\tif (git_config_get_value_multi(key, &values))\n+\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n \tkv_info = values->items[values->nr - 1].util;\n \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n }\ndiff --git a/config.h b/config.h\nindex ef9eade6414..7f6ce6f2fb5 100644\n--- a/config.h\n+++ b/config.h\n@@ -459,10 +459,18 @@ int git_configset_add_parameters(struct config_set *cs);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n- * configuration variable `key` is not found, returns NULL. The caller\n- * should not free or modify the returned pointer, as it is owned by the cache.\n+ * configuration variable `key` is not found, returns 1 without touching\n+ * `value`.\n+ *\n+ * The key will be parsed for validity with git_config_parse_key(), on\n+ * error a negative value will be returned.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n+RESULT_MUST_BE_USED\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest);\n \n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n@@ -496,8 +504,9 @@ struct repository;\n void repo_config(struct repository *repo, config_fn_t fn, void *data);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key);\n+RESULT_MUST_BE_USED\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -544,10 +553,14 @@ int git_config_get_value(const char *key, const char **value);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key`. When the configuration variable\n- * `key` is not found, returns NULL. The caller should not free or modify\n- * the returned pointer, as it is owned by the cache.\n+ * `key` is not found, returns 1 without touching `value`.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_config_get_value_multi(const char *key);\n+RESULT_MUST_BE_USED\n+int git_config_get_value_multi(const char *key,\n+\t\t\t       const struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 440407f1be7..81f0c0e016b 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2301,7 +2301,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)\n \n const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n-\treturn repo_config_get_value_multi(r, \"pack.preferbitmaptips\");\n+\tconst struct string_list *dest;\n+\n+\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\t\treturn dest;\n+\treturn NULL;\n }\n \n int bitmap_is_preferred_refname(struct repository *r, const char *refname)\ndiff --git a/submodule.c b/submodule.c\nindex b958162d286..05ebe5cab4c 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tsl = repo_config_get_value_multi(repo, \"submodule.active\");\n-\tif (sl) {\n+\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex 4ba9eb65606..8f70beb6c9d 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -95,8 +95,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\tgoto exit1;\n \t\t}\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_value_multi\")) {\n-\t\tstrptr = git_config_get_value_multi(argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_config_get_value_multi(argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\n@@ -159,8 +158,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\t\tgoto exit2;\n \t\t\t}\n \t\t}\n-\t\tstrptr = git_configset_get_value_multi(&cs, argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 323f5d35ea8..60c3a517122 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)\n \tif (!initialized) {\n \t\tconst char *const newk = \"versionsort.suffix\";\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n+\t\tint new = git_config_get_value_multi(newk, &newl);\n+\t\tint old = git_config_get_value_multi(oldk, &oldl);\n \n-\t\tprereleases = git_config_get_value_multi(newk);\n-\t\toldl = git_config_get_value_multi(oldk);\n-\t\tif (prereleases && oldl)\n+\t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-\t\telse if (!prereleases)\n+\t\tif (!new)\n+\t\t\tprereleases = newl;\n+\t\telse if (!old)\n \t\t\tprereleases = oldl;\n \n \t\tinitialized = 1;\n-- \n2.38.0.1280.g8136eb6fab2\n\n"},{"id":"466262","messageId":"patch-v2-7.9-c12805f3d55-20221101T225823Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"[PATCH v2 7/9] config API users: test for *_get_value_multi() segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-01T23:05:18Z","receivedAt":"2022-11-01T23:06:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As we'll discus in the subsequent commit these tests all\nshow *_get_value_multi() API users unable to handle there being a\nvalue-less key in the config, which is represented with a \"NULL\" for\nthat entry in the \"string\" member of the returned \"struct\nstring_list\", causing a segfault.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t4202-log.sh                 | 11 +++++++++++\n t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++\n t/t7004-tag.sh                 | 12 ++++++++++++\n t/t7413-submodule-is-active.sh | 12 ++++++++++++\n t/t7900-maintenance.sh         | 26 ++++++++++++++++++++++++++\n 5 files changed, 77 insertions(+)\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 2ce2b41174d..e4f02d8208b 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n+test_expect_failure 'parse log.excludeDecoration with no value' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[log]\n+\t\texcludeDecoration\n+\tEOF\n+\tgit log --decorate=short\n+'\n+\n test_expect_success 'decorate-refs with glob' '\n \tcat >expect.decorate <<-\\EOF &&\n \tMerge-tag-reach\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 6d693eef82f..2e65c8139c4 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,6 +404,22 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n+\ttest_expect_failure 'pack.preferBitmapTips' '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -rf repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\tgit config pack.writeBitmapLookupTable '\"$writeLookupTable\"' &&\n+\t\t\ttest_commit_bulk --message=\"%s\" 103 &&\n+\n+\t\t\tcat >>.git/config <<-\\EOF &&\n+\t\t\t[pack]\n+\t\t\t\tpreferBitmapTips\n+\t\t\tEOF\n+\t\t\tgit repack -adb\n+\t\t)\n+\t'\n+\n \ttest_expect_success 'complains about multiple pack bitmaps' '\n \t\trm -fr repo &&\n \t\tgit init repo &&\ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex 9aa1660651b..f343551a7d4 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[versionsort]\n+\t\tprereleaseSuffix\n+\t\tsuffix\n+\tEOF\n+\tgit tag -l --sort=version:refname\n+'\n+\n test_expect_success 'version sort with prerelease reordering' '\n \ttest_config versionsort.prereleaseSuffix -rc &&\n \tgit tag foo1.6-rc1 &&\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex 7cdc2637649..bfe27e50732 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n+test_expect_failure 'is-active handles submodule.active config missing a value' '\n+\tcp super/.git/config super/.git/config.orig &&\n+\ttest_when_finished mv super/.git/config.orig super/.git/config &&\n+\n+\tcat >>super/.git/config <<-\\EOF &&\n+\t[submodule]\n+\t\tactive\n+\tEOF\n+\n+\ttest-tool -C super submodule is-active sub1\n+'\n+\n test_expect_success 'is-active works with basic submodule.active config' '\n \ttest_when_finished \"git -C super config submodule.sub1.URL ../sub\" &&\n \ttest_when_finished \"git -C super config --unset-all submodule.active\" &&\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 96bdd420456..958d906f245 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -505,6 +505,32 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --force\n '\n \n+test_expect_failure 'register with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance register\n+'\n+\n+test_expect_failure 'unregister with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\tgit maintenance unregister &&\n+\tgit maintenance unregister --force\n+'\n+\n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \tMETA=\"a+b*c\" &&\n \tgit init \"$META\" &&\n-- \n2.38.0.1280.g8136eb6fab2\n\n"},{"id":"466263","messageId":"patch-v2-6.9-a0c29d46556-20221101T225823Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"[PATCH v2 6/9] for-each-repo: error on bad --config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-01T23:05:17Z","receivedAt":"2022-11-01T23:06:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in 6c62f015520 (for-each-repo: do nothing on empty config,\n2021-01-08) this command wants to ignore a non-existing config key,\nbut let's not conflate that with bad config.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c     | 3 ++-\n builtin/submodule--helper.c | 8 ++++----\n t/t0068-for-each-repo.sh    | 8 ++++----\n 3 files changed, 10 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 7d7685c8a1a..96caf90139b 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -45,7 +45,8 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \n \terr = repo_config_get_value_multi(the_repository, config_key, &values);\n \tif (err < 0)\n-\t\treturn 0;\n+\t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n+\t\t\t       for_each_repo_usage, options, config_key);\n \telse if (err)\n \t\treturn 0;\n \ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 53afc2de4af..ad7ecaafc83 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -541,7 +541,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t\tNULL\n \t};\n \tint ret = 1;\n-\tconst struct string_list *values;\n+\tconst struct string_list *unused;\n \n \targc = parse_options(argc, argv, prefix, module_init_options,\n \t\t\t     git_submodule_helper_usage, 0);\n@@ -553,7 +553,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t * If there are no path args and submodule.active is set then,\n \t * by default, only initialize 'active' modules.\n \t */\n-\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n+\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &unused))\n \t\tmodule_list_active(&list);\n \n \tinfo.prefix = prefix;\n@@ -2717,7 +2717,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \tif (opt.init) {\n \t\tstruct module_list list = MODULE_LIST_INIT;\n \t\tstruct init_cb info = INIT_CB_INIT;\n-\t\tconst struct string_list *values;\n+\t\tconst struct string_list *unused;\n \n \t\tif (module_list_compute(argv, opt.prefix,\n \t\t\t\t\t&pathspec2, &list) < 0) {\n@@ -2730,7 +2730,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \t\t * If there are no path args and submodule.active is set then,\n \t\t * by default, only initialize 'active' modules.\n \t\t */\n-\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n+\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &unused))\n \t\t\tmodule_list_active(&list);\n \n \t\tinfo.prefix = opt.prefix;\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 6bba0c5f4c2..115221c9ca5 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -33,10 +33,10 @@ test_expect_success 'do nothing on empty config' '\n \tgit for-each-repo --config=bogus.config -- help --no-such-option\n '\n \n-test_expect_success 'bad config keys' '\n-\tgit for-each-repo --config=a &&\n-\tgit for-each-repo --config=a.b. &&\n-\tgit for-each-repo --config=\"'\\''.b\"\n+test_expect_success 'error on bad config keys' '\n+\ttest_expect_code 129 git for-each-repo --config=a &&\n+\ttest_expect_code 129 git for-each-repo --config=a.b. &&\n+\ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n '\n \n test_done\n-- \n2.38.0.1280.g8136eb6fab2\n\n"},{"id":"466264","messageId":"patch-v2-8.9-6b76f9eac90-20221101T225823Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"[PATCH v2 8/9] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-01T23:05:19Z","receivedAt":"2022-11-01T23:06:17Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix numerous and mostly long-standing segfaults in consumers of\nthe *_config_*value_multi() API. As discussed in the preceding commit\nan empty key in the config syntax yields a \"NULL\" string, which these\nusers would give to strcmp() (or similar), resulting in segfaults.\n\nAs this change shows, most users users of the *_config_*value_multi()\nAPI didn't really want such an an unsafe and low-level API, let's give\nthem something with the safety of git_config_get_string() instead.\n\nThis fix is similar to what the *_string() functions and others\nacquired in[1] and [2]. Namely introducing and using a safer\n\"*_get_string_multi()\" variant of the low-level \"_*value_multi()\"\nfunction.\n\nThis fixes segfaults in code introduced in:\n\n  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)\n  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)\n  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)\n  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)\n  - 92156291ca8 (log: add default decoration filter, 2022-08-05)\n  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)\n\nThere are now three remaining files using the low-level API:\n\n- Two cases in \"builtin/submodule--helper.c\", where it's used safely\n  to see if any config exists.\n- One in \"builtin/for-each-repo.c\", which we'll convert in a\n  subsequent commit.\n- The \"t/helper/test-config.c\" code added in [3].\n\nAs seen in the preceding commit we need to give the\n\"t/helper/test-config.c\" caller these \"NULL\" entries.\n\nWe could also alter the underlying git_configset_get_value_multi()\nfunction to be \"string safe\", but doing so would leave no room for\nother variants of \"*_get_value_multi()\" that coerce to other types.\n\nSuch coercion can't be built on the string version, since as we've\nestablished \"NULL\" is a true value in the boolean context, but if we\ncoerced it to \"\" for use in a list of strings it'll be subsequently\ncoerced to \"false\" as a boolean.\n\nThe callback pattern being used here will make it easy to introduce\ne.g. a \"multi\" variant which coerces its values to \"bool\", \"int\",\n\"path\" etc.\n\n1. 40ea4ed9032 (Add config_error_nonbool() helper function,\n   2008-02-11)\n2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,\n   2008-02-11).\n3. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                   |  4 ++--\n builtin/log.c                  |  3 +--\n config.c                       | 35 +++++++++++++++++++++++++++++++++-\n config.h                       | 19 ++++++++++++++++++\n pack-bitmap.c                  |  2 +-\n submodule.c                    |  2 +-\n t/t4202-log.sh                 |  8 ++++++--\n t/t5310-pack-bitmaps.sh        |  9 +++++++--\n t/t7004-tag.sh                 |  9 +++++++--\n t/t7413-submodule-is-active.sh |  8 ++++++--\n t/t7900-maintenance.sh         | 22 ++++++++++++++++-----\n versioncmp.c                   |  4 ++--\n 12 files changed, 103 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 76cee01e442..f887dc7a3f3 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1485,7 +1485,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \telse\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tif (!git_config_get_value_multi(key, &list)) {\n+\tif (!git_config_get_string_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1541,7 +1541,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tusage_with_options(builtin_maintenance_unregister_usage,\n \t\t\t\t   options);\n \n-\tif (!git_config_get_value_multi(key, &list)) {\n+\tif (!git_config_get_string_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex cc9d92f95da..9b19ae0a736 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -184,8 +184,7 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n \tconst struct string_list *config_exclude;\n \n-\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n-\t\t\t\t\t&config_exclude)) {\n+\tif (!git_config_get_string_multi(\"log.excludeDecoration\", &config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex 0b07045ed8c..f656d1cd99d 100644\n--- a/config.c\n+++ b/config.c\n@@ -2437,6 +2437,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \treturn 0;\n }\n \n+static int check_multi_string(struct string_list_item *item, void *util)\n+{\n+\treturn item->string ? 0 : config_error_nonbool(util);\n+}\n+\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest)\n+{\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value_multi(cs, key, dest)))\n+\t\treturn ret;\n+\tif ((ret = for_each_string_list((struct string_list *)*dest,\n+\t\t\t\t\tcheck_multi_string, (void *)key)))\n+\t\treturn ret;\n+\n+\treturn 0;\n+}\n+\n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n {\n \tconst char *value;\n@@ -2587,6 +2606,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,\n \treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_string_multi(repo->config, key, dest);\n+}\n+\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest)\n {\n@@ -2697,6 +2723,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)\n \treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest)\n+{\n+\treturn repo_config_get_string_multi(the_repository, key, dest);\n+}\n+\n int git_config_get_string(const char *key, char **dest)\n {\n \treturn repo_config_get_string(the_repository, key, dest);\n@@ -2842,7 +2874,8 @@ void git_die_config(const char *key, const char *err, ...)\n \t\tva_end(params);\n \t}\n \tif (git_config_get_value_multi(key, &values))\n-\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n+\t\tBUG(\"key '%s' does not exist, should not be given to git_die_config()\",\n+\t\t    key);\n \tkv_info = values->items[values->nr - 1].util;\n \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n }\ndiff --git a/config.h b/config.h\nindex 7f6ce6f2fb5..3079d60a860 100644\n--- a/config.h\n+++ b/config.h\n@@ -472,6 +472,19 @@ RESULT_MUST_BE_USED\n int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \t\t\t\t  const struct string_list **dest);\n \n+/**\n+ * A validation wrapper for git_configset_get_value_multi() which does\n+ * for it what git_configset_get_string() does for\n+ * git_configset_get_value().\n+ *\n+ * The configuration syntax allows for \"[section] key\", which will\n+ * give us a NULL entry in the \"struct string_list\", as opposed to\n+ * \"[section] key =\" which is the empty string. Most users of the API\n+ * are not prepared to handle NULL in a \"struct string_list\".\n+ */\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest);\n+\n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n  */\n@@ -507,6 +520,9 @@ int repo_config_get_value(struct repository *repo,\n RESULT_MUST_BE_USED\n int repo_config_get_value_multi(struct repository *repo, const char *key,\n \t\t\t\tconst struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -561,6 +577,9 @@ int git_config_get_value(const char *key, const char **value);\n RESULT_MUST_BE_USED\n int git_config_get_value_multi(const char *key,\n \t\t\t       const struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 81f0c0e016b..dd05ab03ca0 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2303,7 +2303,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n \tconst struct string_list *dest;\n \n-\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\tif (!repo_config_get_string_multi(r, \"pack.preferbitmaptips\", &dest))\n \t\treturn dest;\n \treturn NULL;\n }\ndiff --git a/submodule.c b/submodule.c\nindex 05ebe5cab4c..6151e5c67a2 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n+\tif (!repo_config_get_string_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex e4f02d8208b..ae73aef922f 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n-test_expect_failure 'parse log.excludeDecoration with no value' '\n+test_expect_success 'parse log.excludeDecoration with no value' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '\n \t[log]\n \t\texcludeDecoration\n \tEOF\n-\tgit log --decorate=short\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''log.excludeDecoration'\\''\n+\tEOF\n+\tgit log --decorate=short 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'decorate-refs with glob' '\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 2e65c8139c4..68195a1de36 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,7 +404,7 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n-\ttest_expect_failure 'pack.preferBitmapTips' '\n+\ttest_expect_success 'pack.preferBitmapTips' '\n \t\tgit init repo &&\n \t\ttest_when_finished \"rm -rf repo\" &&\n \t\t(\n@@ -416,7 +416,12 @@ test_bitmap_cases () {\n \t\t\t[pack]\n \t\t\t\tpreferBitmapTips\n \t\t\tEOF\n-\t\t\tgit repack -adb\n+\n+\t\t\tcat >expect <<-\\EOF &&\n+\t\t\terror: missing value for '\\''pack.preferbitmaptips'\\''\n+\t\t\tEOF\n+\t\t\tgit repack -adb 2>actual &&\n+\t\t\ttest_cmp expect actual\n \t\t)\n \t'\n \ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex f343551a7d4..f4a31ada79a 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas\n \t\tprereleaseSuffix\n \t\tsuffix\n \tEOF\n-\tgit tag -l --sort=version:refname\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''versionsort.suffix'\\''\n+\terror: missing value for '\\''versionsort.prereleasesuffix'\\''\n+\tEOF\n+\tgit tag -l --sort=version:refname 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'version sort with prerelease reordering' '\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex bfe27e50732..887d181b72e 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n-test_expect_failure 'is-active handles submodule.active config missing a value' '\n+test_expect_success 'is-active handles submodule.active config missing a value' '\n \tcp super/.git/config super/.git/config.orig &&\n \ttest_when_finished mv super/.git/config.orig super/.git/config &&\n \n@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'\n \t\tactive\n \tEOF\n \n-\ttest-tool -C super submodule is-active sub1\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''submodule.active'\\''\n+\tEOF\n+\ttest-tool -C super submodule is-active sub1 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'is-active works with basic submodule.active config' '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 958d906f245..1201866c8d0 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -505,7 +505,7 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --force\n '\n \n-test_expect_failure 'register with no value for maintenance.repo' '\n+test_expect_success 'register with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -513,10 +513,15 @@ test_expect_failure 'register with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance register\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\tgit maintenance register 2>actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n-test_expect_failure 'unregister with no value for maintenance.repo' '\n+test_expect_success 'unregister with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -527,8 +532,15 @@ test_expect_failure 'unregister with no value for maintenance.repo' '\n \tcat >expect <<-\\EOF &&\n \terror: missing value for '\\''maintenance.repo'\\''\n \tEOF\n-\tgit maintenance unregister &&\n-\tgit maintenance unregister --force\n+\ttest_expect_code 128 git maintenance unregister 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo &&\n+\n+\tgit maintenance unregister --force 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 60c3a517122..7498da96e0e 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n \t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n-\t\tint new = git_config_get_value_multi(newk, &newl);\n-\t\tint old = git_config_get_value_multi(oldk, &oldl);\n+\t\tint new = git_config_get_string_multi(newk, &newl);\n+\t\tint old = git_config_get_string_multi(oldk, &oldl);\n \n \t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-- \n2.38.0.1280.g8136eb6fab2\n\n"},{"id":"466265","messageId":"patch-v2-9.9-e2f8f7c52e3-20221101T225823Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"[PATCH v2 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-01T23:05:20Z","receivedAt":"2022-11-01T23:06:19Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on\nconfigured repos, 2020-09-11). Due to assuming that elements returned\nfrom the repo_config_get_value_multi() call wouldn't be \"NULL\" we'd\nconflate the <path> and <command> part of the argument list when\nrunning commands.\n\nAs noted in the preceding commit the fix is to move to a safer\n\"*_string_multi()\" version of the *_multi() API. This change is\nseparated from the rest because those all segfaulted. In this change\nwe ended up with different behavior.\n\nWhen using the \"--config=<config>\" form we take each element of the\nlist as a path to a repository. E.g. with a configuration like:\n\n\t[repo] list = /some/repo\n\nWe would, with this command:\n\n\tgit for-each-repo --config=repo.list status builtin\n\nRun a \"git status\" in /some/repo, as:\n\n\tgit -C /some/repo status builtin\n\nI.e. ask \"status\" to report on the \"builtin\" directory. But since a\nconfiguration such as this would result in a \"struct string_list *\"\nwith one element, whose \"string\" member is \"NULL\":\n\n\t[repo] list\n\nWe would, when constructing our command-line in\n\"builtin/for-each-repo.c\"...\n\n\tstrvec_pushl(&child.args, \"-C\", path, NULL);\n\tfor (i = 0; i < argc; i++)\n\t\tstrvec_push(&child.args, argv[i]);\n\n...have that \"path\" be \"NULL\", and as strvec_pushl() stops when it\nsees NULL we'd end with the first \"argv\" element as the argument to\nthe \"-C\" option, e.g.:\n\n\tgit -C status builtin\n\nI.e. we'd run the command \"builtin\" in the \"status\" directory.\n\nIn another context this might be an interesting security\nvulnerability, but I think that this amounts to a nothingburger on\nthat front.\n\nA hypothetical attacker would need to be able to write config for the\nvictim to run, if they're able to do that there's more interesting\nattack vectors. See the \"safe.directory\" facility added in\n8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).\n\nAn even more unlikely possibility would be an attacker able to\ngenerate the config used for \"for-each-repo --config=<key>\", but\nnothing else (e.g. an automated system producing that list).\n\nEven in that case the attack vector is limited to the user running\ncommands whose name matches a directory that's interesting to the\nattacker (e.g. a \"log\" directory in a repository). The second\nargument (if any) of the command is likely to make git die without\ndoing anything interesting (e.g. \"-p\" to \"log\", there being no \"-p\"\nbuilt-in command to run).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  |  2 +-\n t/t0068-for-each-repo.sh | 13 +++++++++++++\n 2 files changed, 14 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 96caf90139b..9f52d6e0568 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -43,7 +43,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\terr = repo_config_get_string_multi(the_repository, config_key, &values);\n \tif (err < 0)\n \t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n \t\t\t       for_each_repo_usage, options, config_key);\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 115221c9ca5..c27d4dc5f71 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -39,4 +39,17 @@ test_expect_success 'error on bad config keys' '\n \ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n '\n \n+test_expect_success 'error on NULL value for config keys' '\n+\tcat >>.git/config <<-\\EOF &&\n+\t[empty]\n+\t\tkey\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''empty.key'\\''\n+\tEOF\n+\ttest_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.38.0.1280.g8136eb6fab2\n\n"},{"id":"466274","messageId":"Y2G+qczur9eLVtk5@nand.local","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"Re: [PATCH v2 0/9] config API: make \"multi\" safe, fix numerous segfaults","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2022-11-02T00:49:45Z","receivedAt":"2022-11-02T00:49:51Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Wed, Nov 02, 2022 at 12:05:11AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> Ævar Arnfjörð Bjarmason (9):\n>   for-each-repo tests: test bad --config keys\n>   config tests: cover blind spots in git_die_config() tests\n>   config tests: add \"NULL\" tests for *_get_value_multi()\n>   versioncmp.c: refactor config reading next commit\n>   config API: have *_multi() return an \"int\" and take a \"dest\"\n>   for-each-repo: error on bad --config\n>   config API users: test for *_get_value_multi() segfaults\n>   config API: add \"string\" version of *_value_multi(), fix segfaults\n>   for-each-repo: with bad config, don't conflate <path> and <cmd>\n\nThanks. I took the updated round, and will review it more closely when I\nhave a chance to tomorrow.\n\nThanks,\nTaylor\n"},{"id":"467984","messageId":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com","subject":"[PATCH v3 0/9] config API: make \"multi\" safe, fix numerous segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-25T09:50:01Z","receivedAt":"2022-11-25T09:54:44Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This series fixes numerous segfaults in config API users, because they\ndidn't expect *_get_multi() to hand them a string_list with a NULL in\nit given config like \"[a] key\" (note, no \"=\"'s).\n\nA larger general overview at v1[1], but note the API changes in\nv2[2]. Changes since v2:\n\n* Rebased on the now-landed \"rp/maintenance-qol\", which had conflicts\n  in builtin/gc.c.\n* Re-wrap some of the code properly at 79 characters.\n* Drop the stray submodule--helper from \"for-each-repo\", it was from a\n  mistaken earlier rebase in v2.\n* Avoid minor whitespace changes in a test.\n* Don't change the BUG() message in config.c later in the series\n  (another earlier rebase change when slimming down the v2).\n\nJunio: I'm sending this re-roll because I see\n\"ab/config-multi-and-nonbool\" got ejected (presumably due to the\nconflicts). Per [3] I think the \"mixed bag\" note in \"What's Cooking\"\nrefers to the state of v1, which I tried to address in v2.\n\n1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/\n2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/\n3. https://lore.kernel.org/git/221107.86leomx2dg.gmgdl@evledraar.gmail.com/\n\nCI & branch at:\nhttps://github.com/avar/git/tree/avar/have-git_configset_get_value-use-dest-and-int-pattern-3\n\nÆvar Arnfjörð Bjarmason (9):\n  for-each-repo tests: test bad --config keys\n  config tests: cover blind spots in git_die_config() tests\n  config tests: add \"NULL\" tests for *_get_value_multi()\n  versioncmp.c: refactor config reading next commit\n  config API: have *_multi() return an \"int\" and take a \"dest\"\n  for-each-repo: error on bad --config\n  config API users: test for *_get_value_multi() segfaults\n  config API: add \"string\" version of *_value_multi(), fix segfaults\n  for-each-repo: with bad config, don't conflate <path> and <cmd>\n\n builtin/for-each-repo.c              | 14 ++---\n builtin/gc.c                         | 10 ++--\n builtin/log.c                        |  6 +-\n builtin/submodule--helper.c          |  7 ++-\n config.c                             | 87 +++++++++++++++++++++++-----\n config.h                             | 50 +++++++++++++---\n pack-bitmap.c                        |  6 +-\n submodule.c                          |  3 +-\n t/helper/test-config.c               |  6 +-\n t/t0068-for-each-repo.sh             | 19 ++++++\n t/t1308-config-set.sh                | 30 ++++++++++\n t/t3309-notes-merge-auto-resolve.sh  |  7 ++-\n t/t4202-log.sh                       | 15 +++++\n t/t5304-prune.sh                     | 12 +++-\n t/t5310-pack-bitmaps.sh              | 20 +++++++\n t/t5552-skipping-fetch-negotiator.sh | 16 +++++\n t/t7004-tag.sh                       | 17 ++++++\n t/t7413-submodule-is-active.sh       | 16 +++++\n t/t7900-maintenance.sh               | 38 ++++++++++++\n versioncmp.c                         | 22 ++++---\n 20 files changed, 339 insertions(+), 62 deletions(-)\n\nRange-diff against v2:\n 1:  b8fd3bea4d1 =  1:  5c8819ff388 for-each-repo tests: test bad --config keys\n 2:  6cd0d6faf3c =  2:  3eb8da6086d config tests: cover blind spots in git_die_config() tests\n 3:  f2a8766a802 =  3:  14b08dfc162 config tests: add \"NULL\" tests for *_get_value_multi()\n 4:  42cfc61202d =  4:  cb802b30cd8 versioncmp.c: refactor config reading next commit\n 5:  48fb7cbf585 !  5:  e0e6ade3f38 config API: have *_multi() return an \"int\" and take a \"dest\"\n    @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const\n      \t\t\tif (!strcmp(maintpath, item->string)) {\n      \t\t\t\tfound = 1;\n     @@ builtin/gc.c: static int maintenance_unregister(int argc, const char **argv, const char *prefi\n    - \t\tusage_with_options(builtin_maintenance_unregister_usage,\n    - \t\t\t\t   options);\n    - \n    --\tlist = git_config_get_value_multi(key);\n    + \tif (config_file) {\n    + \t\tgit_configset_init(&cs);\n    + \t\tgit_configset_add_file(&cs, config_file);\n    +-\t\tlist = git_configset_get_value_multi(&cs, key);\n    +-\t} else {\n    +-\t\tlist = git_config_get_value_multi(key);\n    + \t}\n     -\tif (list) {\n    -+\tif (!git_config_get_value_multi(key, &list)) {\n    ++\tif (!(config_file\n    ++\t      ? git_configset_get_value_multi(&cs, key, &list)\n    ++\t      : git_config_get_value_multi(key, &list))) {\n      \t\tfor_each_string_list_item(item, list) {\n      \t\t\tif (!strcmp(maintpath, item->string)) {\n      \t\t\t\tfound = 1;\n    @@ builtin/submodule--helper.c: static int module_update(int argc, const char **arg\n      \t\t * by default, only initialize 'active' modules.\n      \t\t */\n     -\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n    -+\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n    ++\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\",\n    ++\t\t\t\t\t\t\t &values))\n      \t\t\tmodule_list_active(&list);\n      \n      \t\tinfo.prefix = opt.prefix;\n 6:  a0c29d46556 !  6:  06d502bc577 for-each-repo: error on bad --config\n    @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, cons\n      \t\treturn 0;\n      \n     \n    - ## builtin/submodule--helper.c ##\n    -@@ builtin/submodule--helper.c: static int module_init(int argc, const char **argv, const char *prefix)\n    - \t\tNULL\n    - \t};\n    - \tint ret = 1;\n    --\tconst struct string_list *values;\n    -+\tconst struct string_list *unused;\n    - \n    - \targc = parse_options(argc, argv, prefix, module_init_options,\n    - \t\t\t     git_submodule_helper_usage, 0);\n    -@@ builtin/submodule--helper.c: static int module_init(int argc, const char **argv, const char *prefix)\n    - \t * If there are no path args and submodule.active is set then,\n    - \t * by default, only initialize 'active' modules.\n    - \t */\n    --\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n    -+\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &unused))\n    - \t\tmodule_list_active(&list);\n    - \n    - \tinfo.prefix = prefix;\n    -@@ builtin/submodule--helper.c: static int module_update(int argc, const char **argv, const char *prefix)\n    - \tif (opt.init) {\n    - \t\tstruct module_list list = MODULE_LIST_INIT;\n    - \t\tstruct init_cb info = INIT_CB_INIT;\n    --\t\tconst struct string_list *values;\n    -+\t\tconst struct string_list *unused;\n    - \n    - \t\tif (module_list_compute(argv, opt.prefix,\n    - \t\t\t\t\t&pathspec2, &list) < 0) {\n    -@@ builtin/submodule--helper.c: static int module_update(int argc, const char **argv, const char *prefix)\n    - \t\t * If there are no path args and submodule.active is set then,\n    - \t\t * by default, only initialize 'active' modules.\n    - \t\t */\n    --\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n    -+\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &unused))\n    - \t\t\tmodule_list_active(&list);\n    - \n    - \t\tinfo.prefix = opt.prefix;\n    -\n      ## t/t0068-for-each-repo.sh ##\n     @@ t/t0068-for-each-repo.sh: test_expect_success 'do nothing on empty config' '\n      \tgit for-each-repo --config=bogus.config -- help --no-such-option\n 7:  c12805f3d55 !  7:  f35aacef4ca config API users: test for *_get_value_multi() segfaults\n    @@ t/t7413-submodule-is-active.sh: test_expect_success 'is-active works with submod\n     \n      ## t/t7900-maintenance.sh ##\n     @@ t/t7900-maintenance.sh: test_expect_success 'register and unregister' '\n    - \tgit maintenance unregister --force\n    + \tgit maintenance unregister --config-file ./other --force\n      '\n      \n     +test_expect_failure 'register with no value for maintenance.repo' '\n 8:  6b76f9eac90 !  8:  b45189b4624 config API: add \"string\" version of *_value_multi(), fix segfaults\n    @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const\n      \t\t\tif (!strcmp(maintpath, item->string)) {\n      \t\t\t\tfound = 1;\n     @@ builtin/gc.c: static int maintenance_unregister(int argc, const char **argv, const char *prefi\n    - \t\tusage_with_options(builtin_maintenance_unregister_usage,\n    - \t\t\t\t   options);\n    - \n    --\tif (!git_config_get_value_multi(key, &list)) {\n    -+\tif (!git_config_get_string_multi(key, &list)) {\n    + \t\tgit_configset_add_file(&cs, config_file);\n    + \t}\n    + \tif (!(config_file\n    +-\t      ? git_configset_get_value_multi(&cs, key, &list)\n    +-\t      : git_config_get_value_multi(key, &list))) {\n    ++\t      ? git_configset_get_string_multi(&cs, key, &list)\n    ++\t      : git_config_get_string_multi(key, &list))) {\n      \t\tfor_each_string_list_item(item, list) {\n      \t\t\tif (!strcmp(maintpath, item->string)) {\n      \t\t\t\tfound = 1;\n    @@ builtin/log.c: static void set_default_decoration_filter(struct decoration_filte\n      \n     -\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n     -\t\t\t\t\t&config_exclude)) {\n    -+\tif (!git_config_get_string_multi(\"log.excludeDecoration\", &config_exclude)) {\n    ++\tif (!git_config_get_string_multi(\"log.excludeDecoration\",\n    ++\t\t\t\t\t &config_exclude)) {\n      \t\tstruct string_list_item *item;\n      \t\tfor_each_string_list_item(item, config_exclude)\n      \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\n    @@ config.c: int git_config_get_value_multi(const char *key, const struct string_li\n      int git_config_get_string(const char *key, char **dest)\n      {\n      \treturn repo_config_get_string(the_repository, key, dest);\n    -@@ config.c: void git_die_config(const char *key, const char *err, ...)\n    - \t\tva_end(params);\n    - \t}\n    - \tif (git_config_get_value_multi(key, &values))\n    --\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n    -+\t\tBUG(\"key '%s' does not exist, should not be given to git_die_config()\",\n    -+\t\t    key);\n    - \tkv_info = values->items[values->nr - 1].util;\n    - \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n    - }\n     \n      ## config.h ##\n     @@ config.h: RESULT_MUST_BE_USED\n    @@ t/t5310-pack-bitmaps.sh: test_bitmap_cases () {\n      \t\t\t\tpreferBitmapTips\n      \t\t\tEOF\n     -\t\t\tgit repack -adb\n    -+\n     +\t\t\tcat >expect <<-\\EOF &&\n     +\t\t\terror: missing value for '\\''pack.preferbitmaptips'\\''\n     +\t\t\tEOF\n    @@ t/t7413-submodule-is-active.sh: test_expect_failure 'is-active handles submodule\n     \n      ## t/t7900-maintenance.sh ##\n     @@ t/t7900-maintenance.sh: test_expect_success 'register and unregister' '\n    - \tgit maintenance unregister --force\n    + \tgit maintenance unregister --config-file ./other --force\n      '\n      \n     -test_expect_failure 'register with no value for maintenance.repo' '\n 9:  e2f8f7c52e3 =  9:  58ead3ca555 for-each-repo: with bad config, don't conflate <path> and <cmd>\n-- \n2.39.0.rc0.955.ge9b241be664\n\n"},{"id":"467985","messageId":"patch-v3-1.9-5c8819ff388-20221125T093159Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"[PATCH v3 1/9] for-each-repo tests: test bad --config keys","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-25T09:50:02Z","receivedAt":"2022-11-25T09:54:46Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in 6c62f015520 (for-each-repo: do nothing on empty config,\n2021-01-08) this command wants to ignore a non-existing config key,\nbut it's been conflating that with bad config keys.\n\nA subsequent commit will address that, but for now let's fix the gaps\nin test coverage, and show what we're currently doing in these cases.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t0068-for-each-repo.sh | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex c6e0d655630..a099abc652e 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -39,4 +39,10 @@ test_expect_success 'do nothing on empty config' '\n \tgit for-each-repo --config=bogus.config -- help --no-such-option\n '\n \n+test_expect_success 'bad config keys' '\n+\tgit for-each-repo --config=a &&\n+\tgit for-each-repo --config=a.b. &&\n+\tgit for-each-repo --config=\"'\\''.b\"\n+'\n+\n test_done\n-- \n2.39.0.rc0.955.ge9b241be664\n\n"},{"id":"467986","messageId":"patch-v3-4.9-cb802b30cd8-20221125T093159Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"[PATCH v3 4/9] versioncmp.c: refactor config reading next commit","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-25T09:50:05Z","receivedAt":"2022-11-25T09:54:54Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the reading of the versionSort.suffix and\nversionSort.prereleaseSuffix configuration variables to stay within\nthe bounds of our CodingGuidelines when it comes to line length, and\nta avoid repeating ourselves.\n\nLet's also split out the names of the config variables into variables\nof our own, so we don't have to repeat ourselves, and refactor the\nnested if/else to avoid indenting it, and the existing bracing style\nissue.\n\nThis all helps with the subsequent commit, where we'll need to start\nchecking different git_config_get_value_multi() return value. See\nc026557a373 (versioncmp: generalize version sort suffix reordering,\n2016-12-08) for the original implementation of most of this.\n\nMoving the \"initialized = 1\" assignment allows us to move some of this\nto the variable declarations in the subsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n versioncmp.c | 19 +++++++++++--------\n 1 file changed, 11 insertions(+), 8 deletions(-)\n\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 069ee94a4d7..323f5d35ea8 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)\n \t}\n \n \tif (!initialized) {\n-\t\tconst struct string_list *deprecated_prereleases;\n+\t\tconst char *const newk = \"versionsort.suffix\";\n+\t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *oldl;\n+\n+\t\tprereleases = git_config_get_value_multi(newk);\n+\t\toldl = git_config_get_value_multi(oldk);\n+\t\tif (prereleases && oldl)\n+\t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n+\t\telse if (!prereleases)\n+\t\t\tprereleases = oldl;\n+\n \t\tinitialized = 1;\n-\t\tprereleases = git_config_get_value_multi(\"versionsort.suffix\");\n-\t\tdeprecated_prereleases = git_config_get_value_multi(\"versionsort.prereleasesuffix\");\n-\t\tif (prereleases) {\n-\t\t\tif (deprecated_prereleases)\n-\t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n-\t\t} else\n-\t\t\tprereleases = deprecated_prereleases;\n \t}\n \tif (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,\n \t\t\t\t\t    &diff))\n-- \n2.39.0.rc0.955.ge9b241be664\n\n"},{"id":"467987","messageId":"patch-v3-3.9-14b08dfc162-20221125T093159Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"[PATCH v3 3/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-25T09:50:04Z","receivedAt":"2022-11-25T09:54:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"A less well known edge case in the config format is that keys can be\nvalue-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\nare equivalent as far as \"--type=bool\" is concerned:\n\n\t[a]key\n\t[a]key = true\n\nBut as far as our parser is concerned the values for these two are\nNULL, and \"true\". I.e. for a sequence like:\n\n\t[a]key=x\n\t[a]key\n\t[a]key=y\n\nWe get a \"struct string_list\" with \"string\" members with \".string\"\nvalues of:\n\n\t{ \"x\", NULL, \"y\" }\n\nThis behavior goes back to the initial implementation of\ngit_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n2005-10-10).\n\nWhen the \"t/t1308-config-set.sh\" tests were added in [1] only one of\nthe three \"(NULL)\" lines in \"t/helper/test-config.c\" had any test\ncoverage. This change adds tests that stress the remaining two.\n\n1. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t1308-config-set.sh | 30 ++++++++++++++++++++++++++++++\n 1 file changed, 30 insertions(+)\n\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex b38e158d3b2..561e82f1808 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -146,6 +146,36 @@ test_expect_success 'find multiple values' '\n \tcheck_config get_value_multi case.baz sam bat hask\n '\n \n+test_expect_success 'emit multi values from configset with NULL entry' '\n+\ttest_when_finished \"rm -f my.config\" &&\n+\tcat >my.config <<-\\EOF &&\n+\t[a]key=x\n+\t[a]key\n+\t[a]key=y\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\tx\n+\t(NULL)\n+\ty\n+\tEOF\n+\ttest-tool config configset_get_value_multi a.key my.config >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'multi values from configset with a last NULL entry' '\n+\ttest_when_finished \"rm -f my.config\" &&\n+\tcat >my.config <<-\\EOF &&\n+\t[a]key=x\n+\t[a]key=y\n+\t[a]key\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\t(NULL)\n+\tEOF\n+\ttest-tool config configset_get_value a.key my.config >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'find value from a configset' '\n \tcat >config2 <<-\\EOF &&\n \t[case]\n-- \n2.39.0.rc0.955.ge9b241be664\n\n"},{"id":"467988","messageId":"patch-v3-2.9-3eb8da6086d-20221125T093159Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"[PATCH v3 2/9] config tests: cover blind spots in git_die_config() tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-25T09:50:03Z","receivedAt":"2022-11-25T09:54:58Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"There were no tests checking for the output of the git_die_config()\nfunction in the config API, added in 5a80e97c827 (config: add\n`git_die_config()` to the config-set API, 2014-08-07). We only tested\n\"test_must_fail\", but didn't assert the output.\n\nLet's check for that by extending the existing tests, and adding a new\none for \"fetch.negotiationAlgorithm\" so that we have a test for a user\nof git_config_get_string*() calling git_die_config().\n\nThe other ones are testing:\n\n- For *-resolve.sh: A custom call to git_die_config(), or via\n  git_config_get_notes_strategy()\n- For *-prune.sh: A call via git_config_get_expiry().\n\nWe also cover both the \"from command-line config\" and \"in file..at\nline\" cases here.\n\nThe clobbering of existing \".git/config\" files here is so that we're\nnot implicitly testing the line count of the default config.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-\n t/t5304-prune.sh                     | 12 ++++++++++--\n t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++\n 3 files changed, 32 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh\nindex 141d3e4ca4d..9bd5dbf341f 100755\n--- a/t/t3309-notes-merge-auto-resolve.sh\n+++ b/t/t3309-notes-merge-auto-resolve.sh\n@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '\n \n test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '\n \tgit config core.notesRef refs/notes/y &&\n-\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z &&\n+\tcat >expect <<-\\EOF &&\n+\terror: unknown notes merge strategy foo\n+\tfatal: unable to parse '\\''notes.mergeStrategy'\\'' from command-line config\n+\tEOF\n+\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z 2>actual &&\n+\ttest_cmp expect actual &&\n \t# Verify no changes (y)\n \tverify_notes y y\n '\ndiff --git a/t/t5304-prune.sh b/t/t5304-prune.sh\nindex 8ae314af585..c8fa962b397 100755\n--- a/t/t5304-prune.sh\n+++ b/t/t5304-prune.sh\n@@ -64,8 +64,16 @@ test_expect_success 'gc: implicit prune --expire' '\n '\n \n test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '\n-\tgit config gc.pruneExpire invalid &&\n-\ttest_must_fail git gc\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t>repo/.git/config &&\n+\tgit -C repo config gc.pruneExpire invalid &&\n+\tcat >expect <<-\\EOF &&\n+\terror: Invalid gc.pruneexpire: '\\''invalid'\\''\n+\tfatal: bad config variable '\\''gc.pruneexpire'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_must_fail git -C repo gc 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'gc: start with ok gc.pruneExpire' '\ndiff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh\nindex 165427d57e5..b55a9f65e6b 100755\n--- a/t/t5552-skipping-fetch-negotiator.sh\n+++ b/t/t5552-skipping-fetch-negotiator.sh\n@@ -3,6 +3,22 @@\n test_description='test skipping fetch negotiator'\n . ./test-lib.sh\n \n+test_expect_success 'fetch.negotiationalgorithm config' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\tcat >repo/.git/config <<-\\EOF &&\n+\t[fetch]\n+\tnegotiationAlgorithm\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''fetch.negotiationalgorithm'\\''\n+\tfatal: bad config variable '\\''fetch.negotiationalgorithm'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_expect_code 128 git -C repo fetch >out 2>actual &&\n+\ttest_must_be_empty out &&\n+\ttest_cmp expect actual\n+'\n+\n have_sent () {\n \twhile test \"$#\" -ne 0\n \tdo\n-- \n2.39.0.rc0.955.ge9b241be664\n\n"},{"id":"467989","messageId":"patch-v3-6.9-06d502bc577-20221125T093159Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"[PATCH v3 6/9] for-each-repo: error on bad --config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-25T09:50:07Z","receivedAt":"2022-11-25T09:54:59Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in 6c62f015520 (for-each-repo: do nothing on empty config,\n2021-01-08) this command wants to ignore a non-existing config key,\nbut let's not conflate that with bad config.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  | 3 ++-\n t/t0068-for-each-repo.sh | 8 ++++----\n 2 files changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 7cc41847635..224164addb3 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -48,7 +48,8 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \n \terr = repo_config_get_value_multi(the_repository, config_key, &values);\n \tif (err < 0)\n-\t\treturn 0;\n+\t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n+\t\t\t       for_each_repo_usage, options, config_key);\n \telse if (err)\n \t\treturn 0;\n \ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex a099abc652e..19ceaa546ea 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -39,10 +39,10 @@ test_expect_success 'do nothing on empty config' '\n \tgit for-each-repo --config=bogus.config -- help --no-such-option\n '\n \n-test_expect_success 'bad config keys' '\n-\tgit for-each-repo --config=a &&\n-\tgit for-each-repo --config=a.b. &&\n-\tgit for-each-repo --config=\"'\\''.b\"\n+test_expect_success 'error on bad config keys' '\n+\ttest_expect_code 129 git for-each-repo --config=a &&\n+\ttest_expect_code 129 git for-each-repo --config=a.b. &&\n+\ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n '\n \n test_done\n-- \n2.39.0.rc0.955.ge9b241be664\n\n"},{"id":"467990","messageId":"patch-v3-5.9-e0e6ade3f38-20221125T093159Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"[PATCH v3 5/9] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-25T09:50:06Z","receivedAt":"2022-11-25T09:55:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Have the \"git_configset_get_value_multi()\" function and its siblings\nreturn an \"int\" and populate a \"**dest\" parameter like every other\ngit_configset_get_*()\" in the API.\n\nAs we'll see in in subsequent commits this fixes a blind spot in the\nAPI where it wasn't possible to tell whether a list was empty from\nwhether a config key existed. We'll take advantage of that in\nsubsequent commits, but for now we're faithfully converting existing\nAPI callers.\n\nSee [1] for the initial addition of \"git_configset_get_value_multi()\"\n\n1. 3c8687a73ee (add `config_set` API for caching config-like files,\n   2014-07-28).\n\nA logical follow-up to this would be to change the various \"*_get_*()\"\nfunctions to ferry the git_configset_get_value() return value to their\nown callers, e.g. git_configset_get_int() returns \"1\" rather than\nferrying up the \"-1\" that \"git_configset_get_value()\" might return,\nbut that's not being done in this series\n\nMost of this is straightforward, commentary on cases that stand out:\n\n- As we've tested for in a preceding commit we can rely on getting the\n  config list in git_die_config(), and as we need to handle the new\n  return value let's BUG() out if we can't acquire it.\n\n- In \"builtin/for-each-ref.c\" we could preserve the comment added in\n  6c62f015520, but now that we're directly using the documented\n  repo_config_get_value_multi() value it's just narrating something that\n  should be obvious from the API use, so let's drop it.\n\n- The loops after getting the \"list\" value in \"builtin/gc.c\" could\n  also make use of \"unsorted_string_list_has_string()\" instead of using\n  that loop, but let's leave that for now.\n\n- We have code e.g. in \"builtin/submodule--helper.c\" that only wants\n  to check if a config key exists, and would be better served with\n  another API, but let's keep using \"git_configset_get_value_multi()\"\n  for now.\n\n- In \"versioncmp.c\" we now use the return value of the functions,\n  instead of checking if the lists are still non-NULL. This is strictly\n  speaking unnecessary, but makes the API use consistent with the rest,\n  but more importantly...\n\n- ...because we always check our return values we can assert that with\n  the RESULT_MUST_BE_USED macro added in 1e8697b5c4e (submodule--helper:\n  check repo{_submodule,}_init() return values, 2022-09-01)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c     | 13 ++++-----\n builtin/gc.c                | 10 +++----\n builtin/log.c               |  6 ++--\n builtin/submodule--helper.c |  7 +++--\n config.c                    | 55 ++++++++++++++++++++++++++-----------\n config.h                    | 29 +++++++++++++------\n pack-bitmap.c               |  6 +++-\n submodule.c                 |  3 +-\n t/helper/test-config.c      |  6 ++--\n versioncmp.c                | 11 +++++---\n 10 files changed, 92 insertions(+), 54 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 6aeac371488..7cc41847635 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tstatic const char *config_key = NULL;\n \tint i, result = 0;\n \tconst struct string_list *values;\n+\tint err;\n \n \tconst struct option options[] = {\n \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n@@ -45,14 +46,10 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\tvalues = repo_config_get_value_multi(the_repository,\n-\t\t\t\t\t     config_key);\n-\n-\t/*\n-\t * Do nothing on an empty list, which is equivalent to the case\n-\t * where the config variable does not exist at all.\n-\t */\n-\tif (!values)\n+\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\tif (err < 0)\n+\t\treturn 0;\n+\telse if (err)\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 02455fdcd73..69503e0a023 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1513,8 +1513,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \telse\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tlist = git_config_get_value_multi(key);\n-\tif (list) {\n+\tif (!git_config_get_value_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1580,11 +1579,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \tif (config_file) {\n \t\tgit_configset_init(&cs);\n \t\tgit_configset_add_file(&cs, config_file);\n-\t\tlist = git_configset_get_value_multi(&cs, key);\n-\t} else {\n-\t\tlist = git_config_get_value_multi(key);\n \t}\n-\tif (list) {\n+\tif (!(config_file\n+\t      ? git_configset_get_value_multi(&cs, key, &list)\n+\t      : git_config_get_value_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 5eafcf26b49..cc9d92f95da 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tint i;\n \tchar *value = NULL;\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n-\tconst struct string_list *config_exclude =\n-\t\t\tgit_config_get_value_multi(\"log.excludeDecoration\");\n+\tconst struct string_list *config_exclude;\n \n-\tif (config_exclude) {\n+\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t&config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex c75e9e86b06..08c12b25375 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -541,6 +541,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t\tNULL\n \t};\n \tint ret = 1;\n+\tconst struct string_list *values;\n \n \targc = parse_options(argc, argv, prefix, module_init_options,\n \t\t\t     git_submodule_helper_usage, 0);\n@@ -552,7 +553,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t * If there are no path args and submodule.active is set then,\n \t * by default, only initialize 'active' modules.\n \t */\n-\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n \t\tmodule_list_active(&list);\n \n \tinfo.prefix = prefix;\n@@ -2714,6 +2715,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \tif (opt.init) {\n \t\tstruct module_list list = MODULE_LIST_INIT;\n \t\tstruct init_cb info = INIT_CB_INIT;\n+\t\tconst struct string_list *values;\n \n \t\tif (module_list_compute(argv, opt.prefix,\n \t\t\t\t\t&pathspec2, &list) < 0) {\n@@ -2726,7 +2728,8 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \t\t * If there are no path args and submodule.active is set then,\n \t\t * by default, only initialize 'active' modules.\n \t\t */\n-\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\",\n+\t\t\t\t\t\t\t &values))\n \t\t\tmodule_list_active(&list);\n \n \t\tinfo.prefix = opt.prefix;\ndiff --git a/config.c b/config.c\nindex c058b2c70c3..0b07045ed8c 100644\n--- a/config.c\n+++ b/config.c\n@@ -2275,23 +2275,28 @@ void read_very_early_config(config_fn_t cb, void *data)\n \tconfig_with_options(cb, data, NULL, &opts);\n }\n \n-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n+static int configset_find_element(struct config_set *cs, const char *key,\n+\t\t\t\t  struct config_set_element **dest)\n {\n \tstruct config_set_element k;\n \tstruct config_set_element *found_entry;\n \tchar *normalized_key;\n+\tint ret;\n+\n \t/*\n \t * `key` may come from the user, so normalize it before using it\n \t * for querying entries from the hashmap.\n \t */\n-\tif (git_config_parse_key(key, &normalized_key, NULL))\n-\t\treturn NULL;\n+\tret = git_config_parse_key(key, &normalized_key, NULL);\n+\tif (ret < 0)\n+\t\treturn ret;\n \n \thashmap_entry_init(&k.ent, strhash(normalized_key));\n \tk.key = normalized_key;\n \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n \tfree(normalized_key);\n-\treturn found_entry;\n+\t*dest = found_entry;\n+\treturn 0;\n }\n \n static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n@@ -2300,8 +2305,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n \tstruct string_list_item *si;\n \tstruct configset_list_item *l_item;\n \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n+\tint ret;\n \n-\te = configset_find_element(cs, key);\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret < 0)\n+\t\treturn ret;\n \t/*\n \t * Since the keys are being fed by git_config*() callback mechanism, they\n \t * are already normalized. So simply add them without any further munging.\n@@ -2395,24 +2403,38 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n+\tint ret;\n+\n \t/*\n \t * Follows \"last one wins\" semantic, i.e., if there are multiple matches for the\n \t * queried key in the files of the configset, the value returned will be the last\n \t * value in the value list for that key.\n \t */\n-\tvalues = git_configset_get_value_multi(cs, key);\n+\tret = git_configset_get_value_multi(cs, key, &values);\n \n-\tif (!values)\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!values)\n \t\treturn 1;\n \tassert(values->nr > 0);\n \t*value = values->items[values->nr - 1].string;\n \treturn 0;\n }\n \n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest)\n {\n-\tstruct config_set_element *e = configset_find_element(cs, key);\n-\treturn e ? &e->value_list : NULL;\n+\tstruct config_set_element *e;\n+\tint ret;\n+\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!e)\n+\t\treturn 1;\n+\t*dest = &e->value_list;\n+\n+\treturn 0;\n }\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n@@ -2558,11 +2580,11 @@ int repo_config_get_value(struct repository *repo,\n \treturn git_configset_get_value(repo->config, key, value);\n }\n \n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key)\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest)\n {\n \tgit_config_check_init(repo);\n-\treturn git_configset_get_value_multi(repo->config, key);\n+\treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n int repo_config_get_string(struct repository *repo,\n@@ -2670,9 +2692,9 @@ int git_config_get_value(const char *key, const char **value)\n \treturn repo_config_get_value(the_repository, key, value);\n }\n \n-const struct string_list *git_config_get_value_multi(const char *key)\n+int git_config_get_value_multi(const char *key, const struct string_list **dest)\n {\n-\treturn repo_config_get_value_multi(the_repository, key);\n+\treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n int git_config_get_string(const char *key, char **dest)\n@@ -2819,7 +2841,8 @@ void git_die_config(const char *key, const char *err, ...)\n \t\terror_fn(err, params);\n \t\tva_end(params);\n \t}\n-\tvalues = git_config_get_value_multi(key);\n+\tif (git_config_get_value_multi(key, &values))\n+\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n \tkv_info = values->items[values->nr - 1].util;\n \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n }\ndiff --git a/config.h b/config.h\nindex ef9eade6414..7f6ce6f2fb5 100644\n--- a/config.h\n+++ b/config.h\n@@ -459,10 +459,18 @@ int git_configset_add_parameters(struct config_set *cs);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n- * configuration variable `key` is not found, returns NULL. The caller\n- * should not free or modify the returned pointer, as it is owned by the cache.\n+ * configuration variable `key` is not found, returns 1 without touching\n+ * `value`.\n+ *\n+ * The key will be parsed for validity with git_config_parse_key(), on\n+ * error a negative value will be returned.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n+RESULT_MUST_BE_USED\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest);\n \n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n@@ -496,8 +504,9 @@ struct repository;\n void repo_config(struct repository *repo, config_fn_t fn, void *data);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key);\n+RESULT_MUST_BE_USED\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -544,10 +553,14 @@ int git_config_get_value(const char *key, const char **value);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key`. When the configuration variable\n- * `key` is not found, returns NULL. The caller should not free or modify\n- * the returned pointer, as it is owned by the cache.\n+ * `key` is not found, returns 1 without touching `value`.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_config_get_value_multi(const char *key);\n+RESULT_MUST_BE_USED\n+int git_config_get_value_multi(const char *key,\n+\t\t\t       const struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 440407f1be7..81f0c0e016b 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2301,7 +2301,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)\n \n const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n-\treturn repo_config_get_value_multi(r, \"pack.preferbitmaptips\");\n+\tconst struct string_list *dest;\n+\n+\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\t\treturn dest;\n+\treturn NULL;\n }\n \n int bitmap_is_preferred_refname(struct repository *r, const char *refname)\ndiff --git a/submodule.c b/submodule.c\nindex 8ac2fca855d..e43c4230ba3 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tsl = repo_config_get_value_multi(repo, \"submodule.active\");\n-\tif (sl) {\n+\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex 4ba9eb65606..8f70beb6c9d 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -95,8 +95,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\tgoto exit1;\n \t\t}\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_value_multi\")) {\n-\t\tstrptr = git_config_get_value_multi(argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_config_get_value_multi(argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\n@@ -159,8 +158,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\t\tgoto exit2;\n \t\t\t}\n \t\t}\n-\t\tstrptr = git_configset_get_value_multi(&cs, argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 323f5d35ea8..60c3a517122 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)\n \tif (!initialized) {\n \t\tconst char *const newk = \"versionsort.suffix\";\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n+\t\tint new = git_config_get_value_multi(newk, &newl);\n+\t\tint old = git_config_get_value_multi(oldk, &oldl);\n \n-\t\tprereleases = git_config_get_value_multi(newk);\n-\t\toldl = git_config_get_value_multi(oldk);\n-\t\tif (prereleases && oldl)\n+\t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-\t\telse if (!prereleases)\n+\t\tif (!new)\n+\t\t\tprereleases = newl;\n+\t\telse if (!old)\n \t\t\tprereleases = oldl;\n \n \t\tinitialized = 1;\n-- \n2.39.0.rc0.955.ge9b241be664\n\n"},{"id":"467991","messageId":"patch-v3-7.9-f35aacef4ca-20221125T093159Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"[PATCH v3 7/9] config API users: test for *_get_value_multi() segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-25T09:50:08Z","receivedAt":"2022-11-25T09:55:02Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As we'll discus in the subsequent commit these tests all\nshow *_get_value_multi() API users unable to handle there being a\nvalue-less key in the config, which is represented with a \"NULL\" for\nthat entry in the \"string\" member of the returned \"struct\nstring_list\", causing a segfault.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t4202-log.sh                 | 11 +++++++++++\n t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++\n t/t7004-tag.sh                 | 12 ++++++++++++\n t/t7413-submodule-is-active.sh | 12 ++++++++++++\n t/t7900-maintenance.sh         | 26 ++++++++++++++++++++++++++\n 5 files changed, 77 insertions(+)\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 2ce2b41174d..e4f02d8208b 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n+test_expect_failure 'parse log.excludeDecoration with no value' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[log]\n+\t\texcludeDecoration\n+\tEOF\n+\tgit log --decorate=short\n+'\n+\n test_expect_success 'decorate-refs with glob' '\n \tcat >expect.decorate <<-\\EOF &&\n \tMerge-tag-reach\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 6d693eef82f..2e65c8139c4 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,6 +404,22 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n+\ttest_expect_failure 'pack.preferBitmapTips' '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -rf repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\tgit config pack.writeBitmapLookupTable '\"$writeLookupTable\"' &&\n+\t\t\ttest_commit_bulk --message=\"%s\" 103 &&\n+\n+\t\t\tcat >>.git/config <<-\\EOF &&\n+\t\t\t[pack]\n+\t\t\t\tpreferBitmapTips\n+\t\t\tEOF\n+\t\t\tgit repack -adb\n+\t\t)\n+\t'\n+\n \ttest_expect_success 'complains about multiple pack bitmaps' '\n \t\trm -fr repo &&\n \t\tgit init repo &&\ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex 9aa1660651b..f343551a7d4 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[versionsort]\n+\t\tprereleaseSuffix\n+\t\tsuffix\n+\tEOF\n+\tgit tag -l --sort=version:refname\n+'\n+\n test_expect_success 'version sort with prerelease reordering' '\n \ttest_config versionsort.prereleaseSuffix -rc &&\n \tgit tag foo1.6-rc1 &&\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex 7cdc2637649..bfe27e50732 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n+test_expect_failure 'is-active handles submodule.active config missing a value' '\n+\tcp super/.git/config super/.git/config.orig &&\n+\ttest_when_finished mv super/.git/config.orig super/.git/config &&\n+\n+\tcat >>super/.git/config <<-\\EOF &&\n+\t[submodule]\n+\t\tactive\n+\tEOF\n+\n+\ttest-tool -C super submodule is-active sub1\n+'\n+\n test_expect_success 'is-active works with basic submodule.active config' '\n \ttest_when_finished \"git -C super config submodule.sub1.URL ../sub\" &&\n \ttest_when_finished \"git -C super config --unset-all submodule.active\" &&\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 823331e44a0..2dbac07be83 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,6 +524,32 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n+test_expect_failure 'register with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance register\n+'\n+\n+test_expect_failure 'unregister with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\tgit maintenance unregister &&\n+\tgit maintenance unregister --force\n+'\n+\n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \tMETA=\"a+b*c\" &&\n \tgit init \"$META\" &&\n-- \n2.39.0.rc0.955.ge9b241be664\n\n"},{"id":"467992","messageId":"patch-v3-8.9-b45189b4624-20221125T093159Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"[PATCH v3 8/9] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-25T09:50:09Z","receivedAt":"2022-11-25T09:55:06Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix numerous and mostly long-standing segfaults in consumers of\nthe *_config_*value_multi() API. As discussed in the preceding commit\nan empty key in the config syntax yields a \"NULL\" string, which these\nusers would give to strcmp() (or similar), resulting in segfaults.\n\nAs this change shows, most users users of the *_config_*value_multi()\nAPI didn't really want such an an unsafe and low-level API, let's give\nthem something with the safety of git_config_get_string() instead.\n\nThis fix is similar to what the *_string() functions and others\nacquired in[1] and [2]. Namely introducing and using a safer\n\"*_get_string_multi()\" variant of the low-level \"_*value_multi()\"\nfunction.\n\nThis fixes segfaults in code introduced in:\n\n  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)\n  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)\n  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)\n  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)\n  - 92156291ca8 (log: add default decoration filter, 2022-08-05)\n  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)\n\nThere are now three remaining files using the low-level API:\n\n- Two cases in \"builtin/submodule--helper.c\", where it's used safely\n  to see if any config exists.\n- One in \"builtin/for-each-repo.c\", which we'll convert in a\n  subsequent commit.\n- The \"t/helper/test-config.c\" code added in [3].\n\nAs seen in the preceding commit we need to give the\n\"t/helper/test-config.c\" caller these \"NULL\" entries.\n\nWe could also alter the underlying git_configset_get_value_multi()\nfunction to be \"string safe\", but doing so would leave no room for\nother variants of \"*_get_value_multi()\" that coerce to other types.\n\nSuch coercion can't be built on the string version, since as we've\nestablished \"NULL\" is a true value in the boolean context, but if we\ncoerced it to \"\" for use in a list of strings it'll be subsequently\ncoerced to \"false\" as a boolean.\n\nThe callback pattern being used here will make it easy to introduce\ne.g. a \"multi\" variant which coerces its values to \"bool\", \"int\",\n\"path\" etc.\n\n1. 40ea4ed9032 (Add config_error_nonbool() helper function,\n   2008-02-11)\n2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,\n   2008-02-11).\n3. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                   |  6 +++---\n builtin/log.c                  |  4 ++--\n config.c                       | 32 ++++++++++++++++++++++++++++++++\n config.h                       | 19 +++++++++++++++++++\n pack-bitmap.c                  |  2 +-\n submodule.c                    |  2 +-\n t/t4202-log.sh                 |  8 ++++++--\n t/t5310-pack-bitmaps.sh        |  8 ++++++--\n t/t7004-tag.sh                 |  9 +++++++--\n t/t7413-submodule-is-active.sh |  8 ++++++--\n t/t7900-maintenance.sh         | 22 +++++++++++++++++-----\n versioncmp.c                   |  4 ++--\n 12 files changed, 102 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 69503e0a023..2d95c5b29aa 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1513,7 +1513,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \telse\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tif (!git_config_get_value_multi(key, &list)) {\n+\tif (!git_config_get_string_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1581,8 +1581,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tgit_configset_add_file(&cs, config_file);\n \t}\n \tif (!(config_file\n-\t      ? git_configset_get_value_multi(&cs, key, &list)\n-\t      : git_config_get_value_multi(key, &list))) {\n+\t      ? git_configset_get_string_multi(&cs, key, &list)\n+\t      : git_config_get_string_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex cc9d92f95da..cd17f311712 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n \tconst struct string_list *config_exclude;\n \n-\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n-\t\t\t\t\t&config_exclude)) {\n+\tif (!git_config_get_string_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t &config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex 0b07045ed8c..9bd43189c02 100644\n--- a/config.c\n+++ b/config.c\n@@ -2437,6 +2437,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \treturn 0;\n }\n \n+static int check_multi_string(struct string_list_item *item, void *util)\n+{\n+\treturn item->string ? 0 : config_error_nonbool(util);\n+}\n+\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest)\n+{\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value_multi(cs, key, dest)))\n+\t\treturn ret;\n+\tif ((ret = for_each_string_list((struct string_list *)*dest,\n+\t\t\t\t\tcheck_multi_string, (void *)key)))\n+\t\treturn ret;\n+\n+\treturn 0;\n+}\n+\n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n {\n \tconst char *value;\n@@ -2587,6 +2606,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,\n \treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_string_multi(repo->config, key, dest);\n+}\n+\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest)\n {\n@@ -2697,6 +2723,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)\n \treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest)\n+{\n+\treturn repo_config_get_string_multi(the_repository, key, dest);\n+}\n+\n int git_config_get_string(const char *key, char **dest)\n {\n \treturn repo_config_get_string(the_repository, key, dest);\ndiff --git a/config.h b/config.h\nindex 7f6ce6f2fb5..3079d60a860 100644\n--- a/config.h\n+++ b/config.h\n@@ -472,6 +472,19 @@ RESULT_MUST_BE_USED\n int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \t\t\t\t  const struct string_list **dest);\n \n+/**\n+ * A validation wrapper for git_configset_get_value_multi() which does\n+ * for it what git_configset_get_string() does for\n+ * git_configset_get_value().\n+ *\n+ * The configuration syntax allows for \"[section] key\", which will\n+ * give us a NULL entry in the \"struct string_list\", as opposed to\n+ * \"[section] key =\" which is the empty string. Most users of the API\n+ * are not prepared to handle NULL in a \"struct string_list\".\n+ */\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest);\n+\n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n  */\n@@ -507,6 +520,9 @@ int repo_config_get_value(struct repository *repo,\n RESULT_MUST_BE_USED\n int repo_config_get_value_multi(struct repository *repo, const char *key,\n \t\t\t\tconst struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -561,6 +577,9 @@ int git_config_get_value(const char *key, const char **value);\n RESULT_MUST_BE_USED\n int git_config_get_value_multi(const char *key,\n \t\t\t       const struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 81f0c0e016b..dd05ab03ca0 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2303,7 +2303,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n \tconst struct string_list *dest;\n \n-\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\tif (!repo_config_get_string_multi(r, \"pack.preferbitmaptips\", &dest))\n \t\treturn dest;\n \treturn NULL;\n }\ndiff --git a/submodule.c b/submodule.c\nindex e43c4230ba3..0f6cf864ed9 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n+\tif (!repo_config_get_string_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex e4f02d8208b..ae73aef922f 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n-test_expect_failure 'parse log.excludeDecoration with no value' '\n+test_expect_success 'parse log.excludeDecoration with no value' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '\n \t[log]\n \t\texcludeDecoration\n \tEOF\n-\tgit log --decorate=short\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''log.excludeDecoration'\\''\n+\tEOF\n+\tgit log --decorate=short 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'decorate-refs with glob' '\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 2e65c8139c4..894c750080c 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,7 +404,7 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n-\ttest_expect_failure 'pack.preferBitmapTips' '\n+\ttest_expect_success 'pack.preferBitmapTips' '\n \t\tgit init repo &&\n \t\ttest_when_finished \"rm -rf repo\" &&\n \t\t(\n@@ -416,7 +416,11 @@ test_bitmap_cases () {\n \t\t\t[pack]\n \t\t\t\tpreferBitmapTips\n \t\t\tEOF\n-\t\t\tgit repack -adb\n+\t\t\tcat >expect <<-\\EOF &&\n+\t\t\terror: missing value for '\\''pack.preferbitmaptips'\\''\n+\t\t\tEOF\n+\t\t\tgit repack -adb 2>actual &&\n+\t\t\ttest_cmp expect actual\n \t\t)\n \t'\n \ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex f343551a7d4..f4a31ada79a 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas\n \t\tprereleaseSuffix\n \t\tsuffix\n \tEOF\n-\tgit tag -l --sort=version:refname\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''versionsort.suffix'\\''\n+\terror: missing value for '\\''versionsort.prereleasesuffix'\\''\n+\tEOF\n+\tgit tag -l --sort=version:refname 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'version sort with prerelease reordering' '\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex bfe27e50732..887d181b72e 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n-test_expect_failure 'is-active handles submodule.active config missing a value' '\n+test_expect_success 'is-active handles submodule.active config missing a value' '\n \tcp super/.git/config super/.git/config.orig &&\n \ttest_when_finished mv super/.git/config.orig super/.git/config &&\n \n@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'\n \t\tactive\n \tEOF\n \n-\ttest-tool -C super submodule is-active sub1\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''submodule.active'\\''\n+\tEOF\n+\ttest-tool -C super submodule is-active sub1 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'is-active works with basic submodule.active config' '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 2dbac07be83..487e326b3fa 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n-test_expect_failure 'register with no value for maintenance.repo' '\n+test_expect_success 'register with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance register\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\tgit maintenance register 2>actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n-test_expect_failure 'unregister with no value for maintenance.repo' '\n+test_expect_success 'unregister with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -546,8 +551,15 @@ test_expect_failure 'unregister with no value for maintenance.repo' '\n \tcat >expect <<-\\EOF &&\n \terror: missing value for '\\''maintenance.repo'\\''\n \tEOF\n-\tgit maintenance unregister &&\n-\tgit maintenance unregister --force\n+\ttest_expect_code 128 git maintenance unregister 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo &&\n+\n+\tgit maintenance unregister --force 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 60c3a517122..7498da96e0e 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n \t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n-\t\tint new = git_config_get_value_multi(newk, &newl);\n-\t\tint old = git_config_get_value_multi(oldk, &oldl);\n+\t\tint new = git_config_get_string_multi(newk, &newl);\n+\t\tint old = git_config_get_string_multi(oldk, &oldl);\n \n \t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-- \n2.39.0.rc0.955.ge9b241be664\n\n"},{"id":"467993","messageId":"patch-v3-9.9-58ead3ca555-20221125T093159Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"[PATCH v3 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-11-25T09:50:10Z","receivedAt":"2022-11-25T09:55:25Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on\nconfigured repos, 2020-09-11). Due to assuming that elements returned\nfrom the repo_config_get_value_multi() call wouldn't be \"NULL\" we'd\nconflate the <path> and <command> part of the argument list when\nrunning commands.\n\nAs noted in the preceding commit the fix is to move to a safer\n\"*_string_multi()\" version of the *_multi() API. This change is\nseparated from the rest because those all segfaulted. In this change\nwe ended up with different behavior.\n\nWhen using the \"--config=<config>\" form we take each element of the\nlist as a path to a repository. E.g. with a configuration like:\n\n\t[repo] list = /some/repo\n\nWe would, with this command:\n\n\tgit for-each-repo --config=repo.list status builtin\n\nRun a \"git status\" in /some/repo, as:\n\n\tgit -C /some/repo status builtin\n\nI.e. ask \"status\" to report on the \"builtin\" directory. But since a\nconfiguration such as this would result in a \"struct string_list *\"\nwith one element, whose \"string\" member is \"NULL\":\n\n\t[repo] list\n\nWe would, when constructing our command-line in\n\"builtin/for-each-repo.c\"...\n\n\tstrvec_pushl(&child.args, \"-C\", path, NULL);\n\tfor (i = 0; i < argc; i++)\n\t\tstrvec_push(&child.args, argv[i]);\n\n...have that \"path\" be \"NULL\", and as strvec_pushl() stops when it\nsees NULL we'd end with the first \"argv\" element as the argument to\nthe \"-C\" option, e.g.:\n\n\tgit -C status builtin\n\nI.e. we'd run the command \"builtin\" in the \"status\" directory.\n\nIn another context this might be an interesting security\nvulnerability, but I think that this amounts to a nothingburger on\nthat front.\n\nA hypothetical attacker would need to be able to write config for the\nvictim to run, if they're able to do that there's more interesting\nattack vectors. See the \"safe.directory\" facility added in\n8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).\n\nAn even more unlikely possibility would be an attacker able to\ngenerate the config used for \"for-each-repo --config=<key>\", but\nnothing else (e.g. an automated system producing that list).\n\nEven in that case the attack vector is limited to the user running\ncommands whose name matches a directory that's interesting to the\nattacker (e.g. a \"log\" directory in a repository). The second\nargument (if any) of the command is likely to make git die without\ndoing anything interesting (e.g. \"-p\" to \"log\", there being no \"-p\"\nbuilt-in command to run).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  |  2 +-\n t/t0068-for-each-repo.sh | 13 +++++++++++++\n 2 files changed, 14 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 224164addb3..ce8f7a99086 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\terr = repo_config_get_string_multi(the_repository, config_key, &values);\n \tif (err < 0)\n \t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n \t\t\t       for_each_repo_usage, options, config_key);\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 19ceaa546ea..48187a40d64 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -45,4 +45,17 @@ test_expect_success 'error on bad config keys' '\n \ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n '\n \n+test_expect_success 'error on NULL value for config keys' '\n+\tcat >>.git/config <<-\\EOF &&\n+\t[empty]\n+\t\tkey\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''empty.key'\\''\n+\tEOF\n+\ttest_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.39.0.rc0.955.ge9b241be664\n\n"},{"id":"470686","messageId":"kl6llelzo044.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"Re: [PATCH v3 0/9] config API: make \"multi\" safe, fix numerous segfaults","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-01-19T00:10:19Z","receivedAt":"2023-01-19T00:10:31Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"We covered this at Review Club this week (thanks for coming, Ævar!). You\ncan find the notes at:\n\n  https://docs.google.com/document/d/14L8BAumGTpsXpjDY8VzZ4rRtpAjuGrFSRqn3stCuS_w/edit\n\nThe overall sentiment from the meeting was that this is a positive\ndirection for the config API to go in. My personal opinion is that this\nseries is close to mergeable and I had mostly minor comments.\n\nÆvar Arnfjörð Bjarmason         <avarab@gmail.com> writes:\n\n> This series fixes numerous segfaults in config API users, because they\n> didn't expect *_get_multi() to hand them a string_list with a NULL in\n> it given config like \"[a] key\" (note, no \"=\"'s).\n\nAs you mentioned in Review Club, this series also fixes a wart in\nconfig.h where *_get_value_multi() returned a \"struct string_list\"\ninstead of an error code like all other getters. So this series is\ntechnically doing two sort-of-different things...\n\n> Ævar Arnfjörð Bjarmason (9):\n>   for-each-repo tests: test bad --config keys\n>   config tests: cover blind spots in git_die_config() tests\n>   config tests: add \"NULL\" tests for *_get_value_multi()\n>   versioncmp.c: refactor config reading next commit\n>   config API: have *_multi() return an \"int\" and take a \"dest\"\n>   for-each-repo: error on bad --config\n\nFix the wart..\n\n>   config API users: test for *_get_value_multi() segfaults\n>   config API: add \"string\" version of *_value_multi(), fix segfaults\n>   for-each-repo: with bad config, don't conflate <path> and <cmd>\n\nand introduce the better API that won't segfault, but I think it's okay\nto have the series do both since they're closely related enough and the\nlatter is quite small anyway.\n"},{"id":"470687","messageId":"kl6lilh3nzv5.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v3-2.9-3eb8da6086d-20221125T093159Z-avarab@gmail.com","subject":"Re: [PATCH v3 2/9] config tests: cover blind spots in git_die_config() tests","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-01-19T00:15:42Z","receivedAt":"2023-01-19T00:15:48Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:\n\n> There were no tests checking for the output of the git_die_config()\n> function in the config API, added in 5a80e97c827 (config: add\n> `git_die_config()` to the config-set API, 2014-08-07). We only tested\n> \"test_must_fail\", but didn't assert the output.\n\nIt wasn't immediately obvious to me why this was relevant to this\nseries; but reading ahead to 5/9 shows that git_die_config() is a caller\nof a *_get_value_multi() function that we are changing, so we want to\nassert on the output so that we know that git_die_config() is still\ndoing the right thing (since test_must_fail alone won't tell us whether\nwe introduced bugs in git_die_config()).\n\nMight be good to include that extra context, but I don't feel strongly\nabout it.\n"},{"id":"470688","messageId":"kl6lfsc7nzam.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v3-3.9-14b08dfc162-20221125T093159Z-avarab@gmail.com","subject":"Re: [PATCH v3 3/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-01-19T00:28:01Z","receivedAt":"2023-01-19T00:28:12Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:\n\n> A less well known edge case in the config format is that keys can be\n> value-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\n> are equivalent as far as \"--type=bool\" is concerned:\n>\n> \t[a]key\n> \t[a]key = true\n>\n> But as far as our parser is concerned the values for these two are\n> NULL, and \"true\". I.e. for a sequence like:\n>\n> \t[a]key=x\n> \t[a]key\n> \t[a]key=y\n>\n> We get a \"struct string_list\" with \"string\" members with \".string\"\n> values of:\n>\n> \t{ \"x\", NULL, \"y\" }\n>\n> This behavior goes back to the initial implementation of\n> git_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n> 2005-10-10).\n\nI didn't know about this behavior before, actually. Thanks for the\nexplanation.\n\n> When the \"t/t1308-config-set.sh\" tests were added in [1] only one of\n> the three \"(NULL)\" lines in \"t/helper/test-config.c\" had any test\n> coverage. This change adds tests that stress the remaining two.\n\nI initially read this as testing that t/helper/test-config.c is doing\nthe right thing, which would be the antipattern of writing tests for our\ntests.\n\nDuring Review Club, you mentioned that the motivation was something\nelse, which IIRC is closer to exercising the internals of the configset\nAPI, which makes sense to me, thought it would be helpful to clarify\nthat better in the commit message.\n\n> +test_expect_success 'emit multi values from configset with NULL entry' '\n> +\ttest_when_finished \"rm -f my.config\" &&\n> +\tcat >my.config <<-\\EOF &&\n> +\t[a]key=x\n> +\t[a]key\n> +\t[a]key=y\n> +\tEOF\n> +\tcat >expect <<-\\EOF &&\n> +\tx\n> +\t(NULL)\n> +\ty\n> +\tEOF\n> +\ttest-tool config configset_get_value_multi a.key my.config >actual &&\n> +\ttest_cmp expect actual\n> +'\n\nSo if this meant to exercise configset_get_value_multi(), maybe it would\nbe even clearer to just say so in the test name, e.g.\n'configset_get_value_multi with NULL entry'.\n\nSide comment: by the end of the series, *_get_value_multi() has no\nlegitimate callers outside of config.c and the test code, and if we\nremove it from config.h, this scenario wouldn't ever bother us in actual\n`git` usage, but we probably still want to test for it since we want to\nexercise the internals.\n"},{"id":"470689","messageId":"kl6lcz7bny91.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v3-5.9-e0e6ade3f38-20221125T093159Z-avarab@gmail.com","subject":"Re: [PATCH v3 5/9] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-01-19T00:50:34Z","receivedAt":"2023-01-19T00:50:40Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:\n\n> Have the \"git_configset_get_value_multi()\" function and its siblings\n> return an \"int\" and populate a \"**dest\" parameter like every other\n> git_configset_get_*()\" in the API.\n\nIndeed, this is the only function that's inconsistent. Great to see that\nit's being fixed :)\n\n> As we'll see in in subsequent commits this fixes a blind spot in the\n> API where it wasn't possible to tell whether a list was empty from\n> whether a config key existed. We'll take advantage of that in\n> subsequent commits, but for now we're faithfully converting existing\n> API callers.\n\nSounds good.\n\n> Most of this is straightforward, commentary on cases that stand out:\n\nThanks for this btw, I found it quite helpful for navigating the patch.\n>\n> - As we've tested for in a preceding commit we can rely on getting the\n>   config list in git_die_config(), and as we need to handle the new\n>   return value let's BUG() out if we can't acquire it.\n\nThis wasn't immediately clear to me; I'll explain more in the code.\n\n> - In \"builtin/for-each-ref.c\" we could preserve the comment added in\n\nI think you meant for-each-repo.\n\n> @@ -45,14 +46,10 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n>  \tif (!config_key)\n>  \t\tdie(_(\"missing --config=<config>\"));\n>  \n> -\tvalues = repo_config_get_value_multi(the_repository,\n> -\t\t\t\t\t     config_key);\n> -\n> -\t/*\n> -\t * Do nothing on an empty list, which is equivalent to the case\n> -\t * where the config variable does not exist at all.\n> -\t */\n> -\tif (!values)\n> +\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n> +\tif (err < 0)\n> +\t\treturn 0;\n> +\telse if (err)\n>  \t\treturn 0;\n\nThis conditional could be collapsed into \"if (err)\", but it's like this\nbecause the next patch distinguishes between the two cases. Not really\nworth the callout in commentary, but FYI for others who might be\nwondering the same thing.\n\n> diff --git a/config.c b/config.c\n> index c058b2c70c3..0b07045ed8c 100644\n> --- a/config.c\n> +++ b/config.c\n> @@ -2275,23 +2275,28 @@ void read_very_early_config(config_fn_t cb, void *data)\n>  \tconfig_with_options(cb, data, NULL, &opts);\n>  }\n>  \n> -static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n> +static int configset_find_element(struct config_set *cs, const char *key,\n> +\t\t\t\t  struct config_set_element **dest)\n>  {\n>  \tstruct config_set_element k;\n>  \tstruct config_set_element *found_entry;\n>  \tchar *normalized_key;\n> +\tint ret;\n> +\n>  \t/*\n>  \t * `key` may come from the user, so normalize it before using it\n>  \t * for querying entries from the hashmap.\n>  \t */\n> -\tif (git_config_parse_key(key, &normalized_key, NULL))\n> -\t\treturn NULL;\n> +\tret = git_config_parse_key(key, &normalized_key, NULL);\n> +\tif (ret < 0)\n> +\t\treturn ret;\n>  \n>  \thashmap_entry_init(&k.ent, strhash(normalized_key));\n>  \tk.key = normalized_key;\n>  \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n>  \tfree(normalized_key);\n> -\treturn found_entry;\n> +\t*dest = found_entry;\n> +\treturn 0;\n>  }\n>  \n>  static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n> @@ -2300,8 +2305,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n>  \tstruct string_list_item *si;\n>  \tstruct configset_list_item *l_item;\n>  \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n> +\tint ret;\n>  \n> -\te = configset_find_element(cs, key);\n> +\tret = configset_find_element(cs, key, &e);\n> +\tif (ret < 0)\n> +\t\treturn ret;\n>  \t/*\n>  \t * Since the keys are being fed by git_config*() callback mechanism, they\n>  \t * are already normalized. So simply add them without any further munging.\n> @@ -2395,24 +2403,38 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n>  int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n>  {\n>  \tconst struct string_list *values = NULL;\n> +\tint ret;\n> +\n>  \t/*\n>  \t * Follows \"last one wins\" semantic, i.e., if there are multiple matches for the\n>  \t * queried key in the files of the configset, the value returned will be the last\n>  \t * value in the value list for that key.\n>  \t */\n> -\tvalues = git_configset_get_value_multi(cs, key);\n> +\tret = git_configset_get_value_multi(cs, key, &values);\n>  \n> -\tif (!values)\n> +\tif (ret < 0)\n> +\t\treturn ret;\n> +\telse if (!values)\n>  \t\treturn 1;\n>  \tassert(values->nr > 0);\n>  \t*value = values->items[values->nr - 1].string;\n>  \treturn 0;\n>  }\n>  \n> -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n> +int git_configset_get_value_multi(struct config_set *cs, const char *key,\n> +\t\t\t\t  const struct string_list **dest)\n>  {\n> -\tstruct config_set_element *e = configset_find_element(cs, key);\n> -\treturn e ? &e->value_list : NULL;\n> +\tstruct config_set_element *e;\n> +\tint ret;\n> +\n> +\tret = configset_find_element(cs, key, &e);\n> +\tif (ret < 0)\n> +\t\treturn ret;\n> +\telse if (!e)\n> +\t\treturn 1;\n> +\t*dest = &e->value_list;\n> +\n> +\treturn 0;\n>  }\n\nThe changes here and the call sites look quite straightforward.\n\n>  int git_config_get_string(const char *key, char **dest)\n> @@ -2819,7 +2841,8 @@ void git_die_config(const char *key, const char *err, ...)\n>  \t\terror_fn(err, params);\n>  \t\tva_end(params);\n>  \t}\n> -\tvalues = git_config_get_value_multi(key);\n> +\tif (git_config_get_value_multi(key, &values))\n> +\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n>  \tkv_info = values->items[values->nr - 1].util;\n>  \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n>  }\n\nHere is the BUG() call that wasn't immediately clear to me. What wasn't\nobvious from the commentary is that this was an 'unhandled error'\nbefore (we didn't check if the returned value was NULL). Arguably we\nshould have had this BUG call before, but we didn't enforce this until\nwe added RESULT_MUST_BE_USED.\n\nAnd this should be a BUG(), and not e.g. error(), since git_die_config()\nis meant to report bad config values, so git_config_get_value_multi()\nshould never fail if we've already managed to get a value, looks good.\n\n> diff --git a/config.h b/config.h\n> index ef9eade6414..7f6ce6f2fb5 100644\n> --- a/config.h\n> +++ b/config.h\n> @@ -459,10 +459,18 @@ int git_configset_add_parameters(struct config_set *cs);\n>  /**\n>   * Finds and returns the value list, sorted in order of increasing priority\n>   * for the configuration variable `key` and config set `cs`. When the\n> - * configuration variable `key` is not found, returns NULL. The caller\n> - * should not free or modify the returned pointer, as it is owned by the cache.\n> + * configuration variable `key` is not found, returns 1 without touching\n> + * `value`.\n> + *\n> + * The key will be parsed for validity with git_config_parse_key(), on\n> + * error a negative value will be returned.\n> + *\n> + * The caller should not free or modify the returned pointer, as it is\n> + * owned by the cache.\n>   */\n> -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n> +RESULT_MUST_BE_USED\n> +int git_configset_get_value_multi(struct config_set *cs, const char *key,\n> +\t\t\t\t  const struct string_list **dest);\n\nUpdated comments look good too.\n"},{"id":"470690","messageId":"kl6la62fny7s.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v3-7.9-f35aacef4ca-20221125T093159Z-avarab@gmail.com","subject":"Re: [PATCH v3 7/9] config API users: test for *_get_value_multi() segfaults","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-01-19T00:51:19Z","receivedAt":"2023-01-19T00:51:25Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:\n\n> +test_expect_failure 'unregister with no value for maintenance.repo' '\n> +\tcp .git/config .git/config.orig &&\n> +\ttest_when_finished mv .git/config.orig .git/config &&\n> +\n> +\tcat >>.git/config <<-\\EOF &&\n> +\t[maintenance]\n> +\t\trepo\n> +\tEOF\n> +\tcat >expect <<-\\EOF &&\n> +\terror: missing value for '\\''maintenance.repo'\\''\n> +\tEOF\n> +\tgit maintenance unregister &&\n> +\tgit maintenance unregister --force\n> +'\n> +\n\nMechanical error: This 'expect' was probably meant for the next patch.\n"},{"id":"470691","messageId":"kl6l7cxjnxnr.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v3-8.9-b45189b4624-20221125T093159Z-avarab@gmail.com","subject":"Re: [PATCH v3 8/9] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-01-19T01:03:20Z","receivedAt":"2023-01-19T01:03:28Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:\n\n> Fix numerous and mostly long-standing segfaults in consumers of\n> the *_config_*value_multi() API. As discussed in the preceding commit\n> an empty key in the config syntax yields a \"NULL\" string, which these\n> users would give to strcmp() (or similar), resulting in segfaults.\n>\n> As this change shows, most users users of the *_config_*value_multi()\n> API didn't really want such an an unsafe and low-level API, let's give\n> them something with the safety of git_config_get_string() instead.\n\nI think the low-level API argument makes sense. All of the other\n*_get_*() functions perform some kind of validation, e.g.\nconfig_parse_*() for non-string types and config_error_nonbool() for\nstrings. In effect, *_get_value_multi() was returning raw output from\nthe config parser without any concern for the caller.\n\n> This fix is similar to what the *_string() functions and others\n> acquired in[1] and [2]. Namely introducing and using a safer\n> \"*_get_string_multi()\" variant of the low-level \"_*value_multi()\"\n> function.\n\nThis suggests to me that we should really get rid of\n*_get_value_multi(), since nobody outside of config.c should need it. I\ndon't think we'd ever end up in a situation where the caller wants the\nraw strings from the config parser (unless we had a config\nkey which accepted values of different types? but that sounds like a\nterrible mistake).\n\n> There are now three remaining files using the low-level API:\n>\n> - Two cases in \"builtin/submodule--helper.c\", where it's used safely\n>   to see if any config exists.\n> - One in \"builtin/for-each-repo.c\", which we'll convert in a\n>   subsequent commit.\n> - The \"t/helper/test-config.c\" code added in [3].\n\nAs you noted, the only remaining non-test caller of the low-level API is\nbuiltin/submodule--helper.c, which maybe we could safely convert anyway\nand get rid of the API altogether. I'm okay with that being a leftover\nbit, but maybe that's worth noting in the CL.\n\n> The callback pattern being used here will make it easy to introduce\n> e.g. a \"multi\" variant which coerces its values to \"bool\", \"int\",\n> \"path\" etc.\n\nI like that this is quite easily extensible, e.g.\n\n> diff --git a/config.c b/config.c\n> index 0b07045ed8c..9bd43189c02 100644\n> --- a/config.c\n> +++ b/config.c\n> @@ -2437,6 +2437,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,\n>  \treturn 0;\n>  }\n>  \n> +static int check_multi_string(struct string_list_item *item, void *util)\n> +{\n> +\treturn item->string ? 0 : config_error_nonbool(util);\n> +}\n> +\n> +int git_configset_get_string_multi(struct config_set *cs, const char *key,\n> +\t\t\t\t   const struct string_list **dest)\n> +{\n> +\tint ret;\n> +\n> +\tif ((ret = git_configset_get_value_multi(cs, key, dest)))\n> +\t\treturn ret;\n> +\tif ((ret = for_each_string_list((struct string_list *)*dest,\n> +\t\t\t\t\tcheck_multi_string, (void *)key)))\n> +\t\treturn ret;\n> +\n> +\treturn 0;\n> +}\n\nwe could just use config_parse_<typename>() if we want to add, e.g.\n*_get_bool_multi().\n\nAnd as a reasonableness check, config_error_nonbool() is what we use to\nvalidate the *_get_string() functions, so it makes sense to reuse it for\nthe string list version.\n"},{"id":"471351","messageId":"patch-v4-1.9-4ae56cab7c7-20230202T131155Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","subject":"[PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-02T13:27:13Z","receivedAt":"2023-02-02T13:27:54Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"There were no tests checking for the output of the git_die_config()\nfunction in the config API, added in 5a80e97c827 (config: add\n`git_die_config()` to the config-set API, 2014-08-07). We only tested\n\"test_must_fail\", but didn't assert the output.\n\nWe need tests for this because a subsequent commit will alter the\nreturn value of git_config_get_value_multi(), which is used to get the\nconfig values in the git_die_config() function. This test coverage\nhelps to build confidence in that subsequent change.\n\nThese tests cover different interactions with git_die_config():\n\n- The \"notes.mergeStrategy\" test in\n  \"t/t3309-notes-merge-auto-resolve.sh\" is a case where a function\n  outside of config.c (git_config_get_notes_strategy()) calls\n  git_die_config().\n\n- The \"gc.pruneExpire\" test in \"t5304-prune.sh\" is a case where\n  git_config_get_expiry() calls git_die_config(), covering a different\n  \"type\" than the \"string\" test for \"notes.mergeStrategy\".\n\n- The \"fetch.negotiationAlgorithm\" test in\n  \"t/t5552-skipping-fetch-negotiator.sh\" is a case where\n  git_config_get_string*() calls git_die_config().\n\nWe also cover both the \"from command-line config\" and \"in file..at\nline\" cases here.\n\nThe clobbering of existing \".git/config\" files here is so that we're\nnot implicitly testing the line count of the default config.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-\n t/t5304-prune.sh                     | 12 ++++++++++--\n t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++\n 3 files changed, 32 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh\nindex 141d3e4ca4d..9bd5dbf341f 100755\n--- a/t/t3309-notes-merge-auto-resolve.sh\n+++ b/t/t3309-notes-merge-auto-resolve.sh\n@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '\n \n test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '\n \tgit config core.notesRef refs/notes/y &&\n-\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z &&\n+\tcat >expect <<-\\EOF &&\n+\terror: unknown notes merge strategy foo\n+\tfatal: unable to parse '\\''notes.mergeStrategy'\\'' from command-line config\n+\tEOF\n+\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z 2>actual &&\n+\ttest_cmp expect actual &&\n \t# Verify no changes (y)\n \tverify_notes y y\n '\ndiff --git a/t/t5304-prune.sh b/t/t5304-prune.sh\nindex d65a5f94b4b..5500dd08426 100755\n--- a/t/t5304-prune.sh\n+++ b/t/t5304-prune.sh\n@@ -72,8 +72,16 @@ test_expect_success 'gc: implicit prune --expire' '\n '\n \n test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '\n-\tgit config gc.pruneExpire invalid &&\n-\ttest_must_fail git gc\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t>repo/.git/config &&\n+\tgit -C repo config gc.pruneExpire invalid &&\n+\tcat >expect <<-\\EOF &&\n+\terror: Invalid gc.pruneexpire: '\\''invalid'\\''\n+\tfatal: bad config variable '\\''gc.pruneexpire'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_must_fail git -C repo gc 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'gc: start with ok gc.pruneExpire' '\ndiff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh\nindex 165427d57e5..b55a9f65e6b 100755\n--- a/t/t5552-skipping-fetch-negotiator.sh\n+++ b/t/t5552-skipping-fetch-negotiator.sh\n@@ -3,6 +3,22 @@\n test_description='test skipping fetch negotiator'\n . ./test-lib.sh\n \n+test_expect_success 'fetch.negotiationalgorithm config' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\tcat >repo/.git/config <<-\\EOF &&\n+\t[fetch]\n+\tnegotiationAlgorithm\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''fetch.negotiationalgorithm'\\''\n+\tfatal: bad config variable '\\''fetch.negotiationalgorithm'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_expect_code 128 git -C repo fetch >out 2>actual &&\n+\ttest_must_be_empty out &&\n+\ttest_cmp expect actual\n+'\n+\n have_sent () {\n \twhile test \"$#\" -ne 0\n \tdo\n-- \n2.39.1.1397.g8c8c074958d\n\n"},{"id":"471352","messageId":"patch-v4-2.9-1f0f8bdcde9-20230202T131155Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","subject":"[PATCH v4 2/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-02T13:27:14Z","receivedAt":"2023-02-02T13:27:56Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"A less well known edge case in the config format is that keys can be\nvalue-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\nare equivalent as far as \"--type=bool\" is concerned:\n\n\t[a]key\n\t[a]key = true\n\nBut as far as our parser is concerned the values for these two are\nNULL, and \"true\". I.e. for a sequence like:\n\n\t[a]key=x\n\t[a]key\n\t[a]key=y\n\nWe get a \"struct string_list\" with \"string\" members with \".string\"\nvalues of:\n\n\t{ \"x\", NULL, \"y\" }\n\nThis behavior goes back to the initial implementation of\ngit_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n2005-10-10).\n\nWhen parts of the config_set API were tested for in [1] they didn't\nadd coverage for 3/4 of the \"(NULL)\" cases handled in\n\"t/helper/test-config.c\". We'd test that case for \"get_value\", but not\n\"get_value_multi\", \"configset_get_value\" and\n\"configset_get_value_multi\".\n\nWe now cover all of those cases, which in turn expose the details of\nhow this part of the config API works.\n\n1. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t1308-config-set.sh | 41 +++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 41 insertions(+)\n\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex b38e158d3b2..b172565f92a 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -146,6 +146,47 @@ test_expect_success 'find multiple values' '\n \tcheck_config get_value_multi case.baz sam bat hask\n '\n \n+test_NULL_in_multi () {\n+\tlocal op=\"$1\" &&\n+\tlocal file=\"$2\" &&\n+\n+\ttest_expect_success \"$op: NULL value in config${file:+ in $file}\" '\n+\t\tconfig=\"$file\" &&\n+\t\tif test -z \"$config\"\n+\t\tthen\n+\t\t\tconfig=.git/config &&\n+\t\t\ttest_when_finished \"mv $config.old $config\" &&\n+\t\t\tmv \"$config\" \"$config\".old\n+\t\tfi &&\n+\n+\t\tcat >\"$config\" <<-\\EOF &&\n+\t\t[a]key=x\n+\t\t[a]key\n+\t\t[a]key=y\n+\t\tEOF\n+\t\tcase \"$op\" in\n+\t\t*_multi)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\tx\n+\t\t\t(NULL)\n+\t\t\ty\n+\t\t\tEOF\n+\t\t\t;;\n+\t\t*)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\ty\n+\t\t\tEOF\n+\t\t\t;;\n+\t\tesac &&\n+\t\ttest-tool config \"$op\" a.key $file >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+}\n+\n+test_NULL_in_multi \"get_value_multi\"\n+test_NULL_in_multi \"configset_get_value\" \"my.config\"\n+test_NULL_in_multi \"configset_get_value_multi\" \"my.config\"\n+\n test_expect_success 'find value from a configset' '\n \tcat >config2 <<-\\EOF &&\n \t[case]\n-- \n2.39.1.1397.g8c8c074958d\n\n"},{"id":"471353","messageId":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com","subject":"[PATCH v4 0/9] config API: make \"multi\" safe, fix numerous segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-02T13:27:12Z","receivedAt":"2023-02-02T13:27:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This series fixes numerous segfaults in config API users, because they\ndidn't expect *_get_multi() to hand them a string_list with a NULL in\nit given config like \"[a] key\" (note, no \"=\"'s).\n\nA larger general overview at v1[1], but note the API changes in\nv2[2]. Changes since v3[3] (in particular thanks to this series being\nfeatured in the Review Club[4]):\n\n* It wasn't clear from the early test commit messages why certain\n  things were being tested, if the test were exhaustive etc. Covered\n  that.\n\n* Rewrote 2/9 to use a test helper function, which gives us better\n  coverage.\n\n* The v1 included a \"lookup_value\" family of functions, as some of the\n  \"multi\" users are only using the API to check key existence.\n\n  The feedback on that was that the API naming din't make sense[6],\n  which I agree with. Rather than having e.g. a git_config_exists() we\n  introduce a git_config_get(), this is just like\n  git_config_get_{value,string,int,...}(), except that we don't have a\n  \"dest\" argument.\n\n  Other than that it works the same way, i.e. the return value\n  indicates existence (or other errors).\n\n  This helps to make subsequent changes smaller, as our \"real\" API\n  conversion no longer needs to deal with these callers.\n\n* Various other small tidbits, see the range-diff below.\n\nCI & branch for this topic at:\nhttps://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/\n\n1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/\n2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/\n3. https://lore.kernel.org/git/cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com/\n4. https://docs.google.com/document/d/14L8BAumGTpsXpjDY8VzZ4rRtpAjuGrFSRqn3stCuS_w\n5. https://lore.kernel.org/git/patch-07.10-c01f7d85c94-20221026T151328Z-avarab@gmail.com/\n6. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/\n\nÆvar Arnfjörð Bjarmason (9):\n  config tests: cover blind spots in git_die_config() tests\n  config tests: add \"NULL\" tests for *_get_value_multi()\n  config API: add and use a \"git_config_get()\" family of functions\n  versioncmp.c: refactor config reading next commit\n  config API: have *_multi() return an \"int\" and take a \"dest\"\n  for-each-repo: error on bad --config\n  config API users: test for *_get_value_multi() segfaults\n  config API: add \"string\" version of *_value_multi(), fix segfaults\n  for-each-repo: with bad config, don't conflate <path> and <cmd>\n\n builtin/for-each-repo.c              |  14 ++--\n builtin/gc.c                         |  15 ++--\n builtin/log.c                        |   6 +-\n builtin/submodule--helper.c          |   7 +-\n builtin/worktree.c                   |   3 +-\n config.c                             | 108 ++++++++++++++++++++++-----\n config.h                             |  69 ++++++++++++++---\n pack-bitmap.c                        |   6 +-\n submodule.c                          |   3 +-\n t/helper/test-config.c               |   6 +-\n t/t0068-for-each-repo.sh             |  19 +++++\n t/t1308-config-set.sh                |  41 ++++++++++\n t/t3309-notes-merge-auto-resolve.sh  |   7 +-\n t/t4202-log.sh                       |  15 ++++\n t/t5304-prune.sh                     |  12 ++-\n t/t5310-pack-bitmaps.sh              |  20 +++++\n t/t5552-skipping-fetch-negotiator.sh |  16 ++++\n t/t7004-tag.sh                       |  17 +++++\n t/t7413-submodule-is-active.sh       |  16 ++++\n t/t7900-maintenance.sh               |  38 ++++++++++\n versioncmp.c                         |  22 ++++--\n 21 files changed, 388 insertions(+), 72 deletions(-)\n\nRange-diff against v3:\n 1:  5c8819ff388 <  -:  ----------- for-each-repo tests: test bad --config keys\n 2:  3eb8da6086d !  1:  4ae56cab7c7 config tests: cover blind spots in git_die_config() tests\n    @@ Commit message\n         `git_die_config()` to the config-set API, 2014-08-07). We only tested\n         \"test_must_fail\", but didn't assert the output.\n     \n    -    Let's check for that by extending the existing tests, and adding a new\n    -    one for \"fetch.negotiationAlgorithm\" so that we have a test for a user\n    -    of git_config_get_string*() calling git_die_config().\n    +    We need tests for this because a subsequent commit will alter the\n    +    return value of git_config_get_value_multi(), which is used to get the\n    +    config values in the git_die_config() function. This test coverage\n    +    helps to build confidence in that subsequent change.\n     \n    -    The other ones are testing:\n    +    These tests cover different interactions with git_die_config():\n     \n    -    - For *-resolve.sh: A custom call to git_die_config(), or via\n    -      git_config_get_notes_strategy()\n    -    - For *-prune.sh: A call via git_config_get_expiry().\n    +    - The \"notes.mergeStrategy\" test in\n    +      \"t/t3309-notes-merge-auto-resolve.sh\" is a case where a function\n    +      outside of config.c (git_config_get_notes_strategy()) calls\n    +      git_die_config().\n    +\n    +    - The \"gc.pruneExpire\" test in \"t5304-prune.sh\" is a case where\n    +      git_config_get_expiry() calls git_die_config(), covering a different\n    +      \"type\" than the \"string\" test for \"notes.mergeStrategy\".\n    +\n    +    - The \"fetch.negotiationAlgorithm\" test in\n    +      \"t/t5552-skipping-fetch-negotiator.sh\" is a case where\n    +      git_config_get_string*() calls git_die_config().\n     \n         We also cover both the \"from command-line config\" and \"in file..at\n         line\" cases here.\n 3:  14b08dfc162 !  2:  1f0f8bdcde9 config tests: add \"NULL\" tests for *_get_value_multi()\n    @@ Commit message\n         git_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n         2005-10-10).\n     \n    -    When the \"t/t1308-config-set.sh\" tests were added in [1] only one of\n    -    the three \"(NULL)\" lines in \"t/helper/test-config.c\" had any test\n    -    coverage. This change adds tests that stress the remaining two.\n    +    When parts of the config_set API were tested for in [1] they didn't\n    +    add coverage for 3/4 of the \"(NULL)\" cases handled in\n    +    \"t/helper/test-config.c\". We'd test that case for \"get_value\", but not\n    +    \"get_value_multi\", \"configset_get_value\" and\n    +    \"configset_get_value_multi\".\n    +\n    +    We now cover all of those cases, which in turn expose the details of\n    +    how this part of the config API works.\n     \n         1. 4c715ebb96a (test-config: add tests for the config_set API,\n            2014-07-28)\n    @@ t/t1308-config-set.sh: test_expect_success 'find multiple values' '\n      \tcheck_config get_value_multi case.baz sam bat hask\n      '\n      \n    -+test_expect_success 'emit multi values from configset with NULL entry' '\n    -+\ttest_when_finished \"rm -f my.config\" &&\n    -+\tcat >my.config <<-\\EOF &&\n    -+\t[a]key=x\n    -+\t[a]key\n    -+\t[a]key=y\n    -+\tEOF\n    -+\tcat >expect <<-\\EOF &&\n    -+\tx\n    -+\t(NULL)\n    -+\ty\n    -+\tEOF\n    -+\ttest-tool config configset_get_value_multi a.key my.config >actual &&\n    -+\ttest_cmp expect actual\n    -+'\n    ++test_NULL_in_multi () {\n    ++\tlocal op=\"$1\" &&\n    ++\tlocal file=\"$2\" &&\n    ++\n    ++\ttest_expect_success \"$op: NULL value in config${file:+ in $file}\" '\n    ++\t\tconfig=\"$file\" &&\n    ++\t\tif test -z \"$config\"\n    ++\t\tthen\n    ++\t\t\tconfig=.git/config &&\n    ++\t\t\ttest_when_finished \"mv $config.old $config\" &&\n    ++\t\t\tmv \"$config\" \"$config\".old\n    ++\t\tfi &&\n    ++\n    ++\t\tcat >\"$config\" <<-\\EOF &&\n    ++\t\t[a]key=x\n    ++\t\t[a]key\n    ++\t\t[a]key=y\n    ++\t\tEOF\n    ++\t\tcase \"$op\" in\n    ++\t\t*_multi)\n    ++\t\t\tcat >expect <<-\\EOF\n    ++\t\t\tx\n    ++\t\t\t(NULL)\n    ++\t\t\ty\n    ++\t\t\tEOF\n    ++\t\t\t;;\n    ++\t\t*)\n    ++\t\t\tcat >expect <<-\\EOF\n    ++\t\t\ty\n    ++\t\t\tEOF\n    ++\t\t\t;;\n    ++\t\tesac &&\n    ++\t\ttest-tool config \"$op\" a.key $file >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t'\n    ++}\n     +\n    -+test_expect_success 'multi values from configset with a last NULL entry' '\n    -+\ttest_when_finished \"rm -f my.config\" &&\n    -+\tcat >my.config <<-\\EOF &&\n    -+\t[a]key=x\n    -+\t[a]key=y\n    -+\t[a]key\n    -+\tEOF\n    -+\tcat >expect <<-\\EOF &&\n    -+\t(NULL)\n    -+\tEOF\n    -+\ttest-tool config configset_get_value a.key my.config >actual &&\n    -+\ttest_cmp expect actual\n    -+'\n    ++test_NULL_in_multi \"get_value_multi\"\n    ++test_NULL_in_multi \"configset_get_value\" \"my.config\"\n    ++test_NULL_in_multi \"configset_get_value_multi\" \"my.config\"\n     +\n      test_expect_success 'find value from a configset' '\n      \tcat >config2 <<-\\EOF &&\n -:  ----------- >  3:  998b11ae4bc config API: add and use a \"git_config_get()\" family of functions\n 4:  cb802b30cd8 !  4:  aae1d5c12a9 versioncmp.c: refactor config reading next commit\n    @@ Commit message\n         Refactor the reading of the versionSort.suffix and\n         versionSort.prereleaseSuffix configuration variables to stay within\n         the bounds of our CodingGuidelines when it comes to line length, and\n    -    ta avoid repeating ourselves.\n    +    to avoid repeating ourselves.\n     \n         Let's also split out the names of the config variables into variables\n         of our own, so we don't have to repeat ourselves, and refactor the\n 5:  e0e6ade3f38 !  5:  23449ff2c4e config API: have *_multi() return an \"int\" and take a \"dest\"\n    @@ Commit message\n         subsequent commits, but for now we're faithfully converting existing\n         API callers.\n     \n    -    See [1] for the initial addition of \"git_configset_get_value_multi()\"\n    -\n    -    1. 3c8687a73ee (add `config_set` API for caching config-like files,\n    -       2014-07-28).\n    -\n         A logical follow-up to this would be to change the various \"*_get_*()\"\n         functions to ferry the git_configset_get_value() return value to their\n         own callers, e.g. git_configset_get_int() returns \"1\" rather than\n    @@ Commit message\n     \n         Most of this is straightforward, commentary on cases that stand out:\n     \n    -    - As we've tested for in a preceding commit we can rely on getting the\n    -      config list in git_die_config(), and as we need to handle the new\n    -      return value let's BUG() out if we can't acquire it.\n    +    - To ensure that we'll properly use the return values of this function\n    +      in the future we're using the \"RESULT_MUST_BE_USED\" macro introduced\n    +      in [1].\n     \n    -    - In \"builtin/for-each-ref.c\" we could preserve the comment added in\n    -      6c62f015520, but now that we're directly using the documented\n    -      repo_config_get_value_multi() value it's just narrating something that\n    -      should be obvious from the API use, so let's drop it.\n    +      As git_die_config() now has to handle this return value let's have\n    +      it BUG() if it can't find the config entry. As tested for in a\n    +      preceding commit we can rely on getting the config list in\n    +      git_die_config().\n     \n         - The loops after getting the \"list\" value in \"builtin/gc.c\" could\n           also make use of \"unsorted_string_list_has_string()\" instead of using\n           that loop, but let's leave that for now.\n     \n    -    - We have code e.g. in \"builtin/submodule--helper.c\" that only wants\n    -      to check if a config key exists, and would be better served with\n    -      another API, but let's keep using \"git_configset_get_value_multi()\"\n    -      for now.\n    -\n         - In \"versioncmp.c\" we now use the return value of the functions,\n    -      instead of checking if the lists are still non-NULL. This is strictly\n    -      speaking unnecessary, but makes the API use consistent with the rest,\n    -      but more importantly...\n    +      instead of checking if the lists are still non-NULL.\n     \n    -    - ...because we always check our return values we can assert that with\n    -      the RESULT_MUST_BE_USED macro added in 1e8697b5c4e (submodule--helper:\n    -      check repo{_submodule,}_init() return values, 2022-09-01)\n    +    1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n    +       return values, 2022-09-01),\n     \n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/for-each-repo.c ##\n    -@@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n    - \tstatic const char *config_key = NULL;\n    - \tint i, result = 0;\n    - \tconst struct string_list *values;\n    -+\tint err;\n    - \n    - \tconst struct option options[] = {\n    - \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n     @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n      \tif (!config_key)\n      \t\tdie(_(\"missing --config=<config>\"));\n    @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, cons\n     -\tvalues = repo_config_get_value_multi(the_repository,\n     -\t\t\t\t\t     config_key);\n     -\n    --\t/*\n    --\t * Do nothing on an empty list, which is equivalent to the case\n    --\t * where the config variable does not exist at all.\n    --\t */\n    + \t/*\n    + \t * Do nothing on an empty list, which is equivalent to the case\n    + \t * where the config variable does not exist at all.\n    + \t */\n     -\tif (!values)\n    -+\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n    -+\tif (err < 0)\n    -+\t\treturn 0;\n    -+\telse if (err)\n    ++\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n      \t\treturn 0;\n      \n      \tfor (i = 0; !result && i < values->nr; i++)\n     \n      ## builtin/gc.c ##\n     @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const char *prefix)\n    - \telse\n    + \tif (git_config_get(\"maintenance.strategy\"))\n      \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n      \n     -\tlist = git_config_get_value_multi(key);\n    @@ builtin/log.c: static void set_default_decoration_filter(struct decoration_filte\n      \t\tfor_each_string_list_item(item, config_exclude)\n      \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\n     \n    - ## builtin/submodule--helper.c ##\n    -@@ builtin/submodule--helper.c: static int module_init(int argc, const char **argv, const char *prefix)\n    - \t\tNULL\n    - \t};\n    - \tint ret = 1;\n    -+\tconst struct string_list *values;\n    - \n    - \targc = parse_options(argc, argv, prefix, module_init_options,\n    - \t\t\t     git_submodule_helper_usage, 0);\n    -@@ builtin/submodule--helper.c: static int module_init(int argc, const char **argv, const char *prefix)\n    - \t * If there are no path args and submodule.active is set then,\n    - \t * by default, only initialize 'active' modules.\n    - \t */\n    --\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n    -+\tif (!argc && !git_config_get_value_multi(\"submodule.active\", &values))\n    - \t\tmodule_list_active(&list);\n    - \n    - \tinfo.prefix = prefix;\n    -@@ builtin/submodule--helper.c: static int module_update(int argc, const char **argv, const char *prefix)\n    - \tif (opt.init) {\n    - \t\tstruct module_list list = MODULE_LIST_INIT;\n    - \t\tstruct init_cb info = INIT_CB_INIT;\n    -+\t\tconst struct string_list *values;\n    - \n    - \t\tif (module_list_compute(argv, opt.prefix,\n    - \t\t\t\t\t&pathspec2, &list) < 0) {\n    -@@ builtin/submodule--helper.c: static int module_update(int argc, const char **argv, const char *prefix)\n    - \t\t * If there are no path args and submodule.active is set then,\n    - \t\t * by default, only initialize 'active' modules.\n    - \t\t */\n    --\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n    -+\t\tif (!argc && !git_config_get_value_multi(\"submodule.active\",\n    -+\t\t\t\t\t\t\t &values))\n    - \t\t\tmodule_list_active(&list);\n    - \n    - \t\tinfo.prefix = opt.prefix;\n    -\n      ## config.c ##\n    -@@ config.c: void read_very_early_config(config_fn_t cb, void *data)\n    - \tconfig_with_options(cb, data, NULL, &opts);\n    - }\n    - \n    --static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n    -+static int configset_find_element(struct config_set *cs, const char *key,\n    -+\t\t\t\t  struct config_set_element **dest)\n    - {\n    - \tstruct config_set_element k;\n    - \tstruct config_set_element *found_entry;\n    - \tchar *normalized_key;\n    -+\tint ret;\n    -+\n    - \t/*\n    - \t * `key` may come from the user, so normalize it before using it\n    - \t * for querying entries from the hashmap.\n    - \t */\n    --\tif (git_config_parse_key(key, &normalized_key, NULL))\n    --\t\treturn NULL;\n    -+\tret = git_config_parse_key(key, &normalized_key, NULL);\n    -+\tif (ret < 0)\n    -+\t\treturn ret;\n    - \n    - \thashmap_entry_init(&k.ent, strhash(normalized_key));\n    - \tk.key = normalized_key;\n    - \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n    - \tfree(normalized_key);\n    --\treturn found_entry;\n    -+\t*dest = found_entry;\n    -+\treturn 0;\n    - }\n    - \n    - static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n    -@@ config.c: static int configset_add_value(struct config_set *cs, const char *key, const cha\n    - \tstruct string_list_item *si;\n    - \tstruct configset_list_item *l_item;\n    - \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n    -+\tint ret;\n    - \n    --\te = configset_find_element(cs, key);\n    -+\tret = configset_find_element(cs, key, &e);\n    -+\tif (ret < 0)\n    -+\t\treturn ret;\n    - \t/*\n    - \t * Since the keys are being fed by git_config*() callback mechanism, they\n    - \t * are already normalized. So simply add them without any further munging.\n     @@ config.c: int git_configset_add_file(struct config_set *cs, const char *filename)\n      int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n      {\n    @@ config.c: int git_configset_add_file(struct config_set *cs, const char *filename\n      \t * value in the value list for that key.\n      \t */\n     -\tvalues = git_configset_get_value_multi(cs, key);\n    -+\tret = git_configset_get_value_multi(cs, key, &values);\n    ++\tif ((ret = git_configset_get_value_multi(cs, key, &values)))\n    ++\t\treturn ret;\n      \n     -\tif (!values)\n    -+\tif (ret < 0)\n    -+\t\treturn ret;\n    -+\telse if (!values)\n    - \t\treturn 1;\n    +-\t\treturn 1;\n      \tassert(values->nr > 0);\n      \t*value = values->items[values->nr - 1].string;\n      \treturn 0;\n    @@ config.c: int git_configset_add_file(struct config_set *cs, const char *filename\n     +int git_configset_get_value_multi(struct config_set *cs, const char *key,\n     +\t\t\t\t  const struct string_list **dest)\n      {\n    --\tstruct config_set_element *e = configset_find_element(cs, key);\n    --\treturn e ? &e->value_list : NULL;\n    -+\tstruct config_set_element *e;\n    + \tstruct config_set_element *e;\n     +\tint ret;\n    -+\n    -+\tret = configset_find_element(cs, key, &e);\n    -+\tif (ret < 0)\n    + \n    +-\tif (configset_find_element(cs, key, &e))\n    +-\t\treturn NULL;\n    ++\tif ((ret = configset_find_element(cs, key, &e)))\n     +\t\treturn ret;\n    -+\telse if (!e)\n    + \telse if (!e)\n    +-\t\treturn NULL;\n    +-\treturn &e->value_list;\n     +\t\treturn 1;\n     +\t*dest = &e->value_list;\n     +\n     +\treturn 0;\n      }\n      \n    - int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n    + int git_configset_get(struct config_set *cs, const char *key)\n     @@ config.c: int repo_config_get_value(struct repository *repo,\n      \treturn git_configset_get_value(repo->config, key, value);\n      }\n    @@ config.h: int git_configset_add_parameters(struct config_set *cs);\n      \n      /**\n       * Clears `config_set` structure, removes all saved variable-value pairs.\n    -@@ config.h: struct repository;\n    - void repo_config(struct repository *repo, config_fn_t fn, void *data);\n    +@@ config.h: RESULT_MUST_BE_USED\n    + int repo_config_get(struct repository *repo, const char *key);\n      int repo_config_get_value(struct repository *repo,\n      \t\t\t  const char *key, const char **value);\n     -const struct string_list *repo_config_get_value_multi(struct repository *repo,\n 6:  06d502bc577 !  6:  17c1218e74c for-each-repo: error on bad --config\n    @@ Commit message\n         2021-01-08) this command wants to ignore a non-existing config key,\n         but let's not conflate that with bad config.\n     \n    +    Before this, all these added tests would pass with an exit code of 0.\n    +\n    +    We could preserve the comment added in 6c62f015520, but now that we're\n    +    directly using the documented repo_config_get_value_multi() value it's\n    +    just narrating something that should be obvious from the API use, so\n    +    let's drop it.\n    +\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/for-each-repo.c ##\n     @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n    + \tstatic const char *config_key = NULL;\n    + \tint i, result = 0;\n    + \tconst struct string_list *values;\n    ++\tint err;\n    + \n    + \tconst struct option options[] = {\n    + \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n    +@@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n    + \tif (!config_key)\n    + \t\tdie(_(\"missing --config=<config>\"));\n      \n    - \terr = repo_config_get_value_multi(the_repository, config_key, &values);\n    - \tif (err < 0)\n    --\t\treturn 0;\n    +-\t/*\n    +-\t * Do nothing on an empty list, which is equivalent to the case\n    +-\t * where the config variable does not exist at all.\n    +-\t */\n    +-\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n    ++\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n    ++\tif (err < 0)\n     +\t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n     +\t\t\t       for_each_repo_usage, options, config_key);\n    - \telse if (err)\n    ++\telse if (err)\n      \t\treturn 0;\n      \n    + \tfor (i = 0; !result && i < values->nr; i++)\n     \n      ## t/t0068-for-each-repo.sh ##\n     @@ t/t0068-for-each-repo.sh: test_expect_success 'do nothing on empty config' '\n      \tgit for-each-repo --config=bogus.config -- help --no-such-option\n      '\n      \n    --test_expect_success 'bad config keys' '\n    --\tgit for-each-repo --config=a &&\n    --\tgit for-each-repo --config=a.b. &&\n    --\tgit for-each-repo --config=\"'\\''.b\"\n     +test_expect_success 'error on bad config keys' '\n     +\ttest_expect_code 129 git for-each-repo --config=a &&\n     +\ttest_expect_code 129 git for-each-repo --config=a.b. &&\n     +\ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n    - '\n    - \n    ++'\n    ++\n      test_done\n 7:  f35aacef4ca !  7:  7fc91eaf747 config API users: test for *_get_value_multi() segfaults\n    @@ Metadata\n      ## Commit message ##\n         config API users: test for *_get_value_multi() segfaults\n     \n    -    As we'll discus in the subsequent commit these tests all\n    +    As we'll discuss in the subsequent commit these tests all\n         show *_get_value_multi() API users unable to handle there being a\n         value-less key in the config, which is represented with a \"NULL\" for\n         that entry in the \"string\" member of the returned \"struct\n         string_list\", causing a segfault.\n     \n    +    These added tests exhaustively test for that issue, as we'll see in a\n    +    subsequent commit we'll need to change all of the API users\n    +    of *_get_value_multi(). These cases were discovered by triggering each\n    +    one individually, and then adding these tests.\n    +\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## t/t4202-log.sh ##\n    @@ t/t7900-maintenance.sh: test_expect_success 'register and unregister' '\n     +\t[maintenance]\n     +\t\trepo\n     +\tEOF\n    -+\tcat >expect <<-\\EOF &&\n    -+\terror: missing value for '\\''maintenance.repo'\\''\n    -+\tEOF\n     +\tgit maintenance unregister &&\n     +\tgit maintenance unregister --force\n     +'\n 8:  b45189b4624 !  8:  a391ee17617 config API: add \"string\" version of *_value_multi(), fix segfaults\n    @@ Commit message\n     \n         - Two cases in \"builtin/submodule--helper.c\", where it's used safely\n           to see if any config exists.\n    +\n    +      We could refactor these away from \"multi\" to some \"does it exist?\"\n    +      function, as [4] did, but as that's orthogonal to the \"string\"\n    +      safety we're introducing here let's leave them for now.\n    +\n         - One in \"builtin/for-each-repo.c\", which we'll convert in a\n           subsequent commit.\n    -    - The \"t/helper/test-config.c\" code added in [3].\n    +\n    +    - The \"t/helper/test-config.c\" code added in [4].\n     \n         As seen in the preceding commit we need to give the\n         \"t/helper/test-config.c\" caller these \"NULL\" entries.\n    @@ Commit message\n            2008-02-11)\n         2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,\n            2008-02-11).\n    -    3. 4c715ebb96a (test-config: add tests for the config_set API,\n    +    3. https://lore.kernel.org/git/patch-07.10-c01f7d85c94-20221026T151328Z-avarab@gmail.com/\n    +    4. 4c715ebb96a (test-config: add tests for the config_set API,\n            2014-07-28)\n     \n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/gc.c ##\n     @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const char *prefix)\n    - \telse\n    + \tif (git_config_get(\"maintenance.strategy\"))\n      \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n      \n     -\tif (!git_config_get_value_multi(key, &list)) {\n    @@ config.c: int git_configset_get_value_multi(struct config_set *cs, const char *k\n     +\treturn 0;\n     +}\n     +\n    - int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n    + int git_configset_get(struct config_set *cs, const char *key)\n      {\n    - \tconst char *value;\n    + \tstruct config_set_element *e;\n     @@ config.c: int repo_config_get_value_multi(struct repository *repo, const char *key,\n      \treturn git_configset_get_value_multi(repo->config, key, dest);\n      }\n    @@ t/t7900-maintenance.sh: test_expect_failure 'register with no value for maintena\n      \ttest_when_finished mv .git/config.orig .git/config &&\n      \n     @@ t/t7900-maintenance.sh: test_expect_failure 'unregister with no value for maintenance.repo' '\n    - \tcat >expect <<-\\EOF &&\n    - \terror: missing value for '\\''maintenance.repo'\\''\n    + \t[maintenance]\n    + \t\trepo\n      \tEOF\n     -\tgit maintenance unregister &&\n     -\tgit maintenance unregister --force\n    ++\tcat >expect <<-\\EOF &&\n    ++\terror: missing value for '\\''maintenance.repo'\\''\n    ++\tEOF\n     +\ttest_expect_code 128 git maintenance unregister 2>actual.raw &&\n     +\tgrep ^error actual.raw >actual &&\n     +\ttest_cmp expect actual &&\n 9:  58ead3ca555 =  9:  c7a5f5b4133 for-each-repo: with bad config, don't conflate <path> and <cmd>\n-- \n2.39.1.1397.g8c8c074958d\n\n"},{"id":"471354","messageId":"patch-v4-4.9-aae1d5c12a9-20230202T131155Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","subject":"[PATCH v4 4/9] versioncmp.c: refactor config reading next commit","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-02T13:27:16Z","receivedAt":"2023-02-02T13:28:00Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the reading of the versionSort.suffix and\nversionSort.prereleaseSuffix configuration variables to stay within\nthe bounds of our CodingGuidelines when it comes to line length, and\nto avoid repeating ourselves.\n\nLet's also split out the names of the config variables into variables\nof our own, so we don't have to repeat ourselves, and refactor the\nnested if/else to avoid indenting it, and the existing bracing style\nissue.\n\nThis all helps with the subsequent commit, where we'll need to start\nchecking different git_config_get_value_multi() return value. See\nc026557a373 (versioncmp: generalize version sort suffix reordering,\n2016-12-08) for the original implementation of most of this.\n\nMoving the \"initialized = 1\" assignment allows us to move some of this\nto the variable declarations in the subsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n versioncmp.c | 19 +++++++++++--------\n 1 file changed, 11 insertions(+), 8 deletions(-)\n\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 069ee94a4d7..323f5d35ea8 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)\n \t}\n \n \tif (!initialized) {\n-\t\tconst struct string_list *deprecated_prereleases;\n+\t\tconst char *const newk = \"versionsort.suffix\";\n+\t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *oldl;\n+\n+\t\tprereleases = git_config_get_value_multi(newk);\n+\t\toldl = git_config_get_value_multi(oldk);\n+\t\tif (prereleases && oldl)\n+\t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n+\t\telse if (!prereleases)\n+\t\t\tprereleases = oldl;\n+\n \t\tinitialized = 1;\n-\t\tprereleases = git_config_get_value_multi(\"versionsort.suffix\");\n-\t\tdeprecated_prereleases = git_config_get_value_multi(\"versionsort.prereleasesuffix\");\n-\t\tif (prereleases) {\n-\t\t\tif (deprecated_prereleases)\n-\t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n-\t\t} else\n-\t\t\tprereleases = deprecated_prereleases;\n \t}\n \tif (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,\n \t\t\t\t\t    &diff))\n-- \n2.39.1.1397.g8c8c074958d\n\n"},{"id":"471355","messageId":"patch-v4-3.9-998b11ae4bc-20230202T131155Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","subject":"[PATCH v4 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-02T13:27:15Z","receivedAt":"2023-02-02T13:28:13Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"We already have the basic \"git_config_get_value()\" function and its\n\"repo_*\" and \"configset\" siblings to get a given \"key\" and assign the\nlast key found to a provided \"value\".\n\nBut some callers don't care about that value, but just want to use the\nreturn value of the \"get_value()\" function to check whether the key\nexist (or another non-zero return value).\n\nThe immediate motivation for this is that a subsequent commit will\nneed to change all callers of the \"*_get_value_multi()\" family of\nfunctions. In two cases here we (ab)used it to check whether we had\nany values for the given key, but didn't care about the return value.\n\nThe rest of the callers here used various other config API functions\nto do the same, all of which resolved to the same underlying functions\nto provide the answer.\n\nSome of these were using either git_config_get_string() or\ngit_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a\nconfigure_added_submodule() leak, 2022-09-01) for a recent example. We\ncan now use a helper function that doesn't require a throwaway\nvariable.\n\nWe could have changed git_configset_get_value_multi() to accept a\n\"NULL\" as a \"dest\" for all callers, but let's avoid changing the\nbehavior of existing API users. Having an \"unused\" value that we throw\naway internal to config.c is cheap.\n\nAnother name for this function could have been\n\"*_config_key_exists()\", as suggested in [1]. That would work for all\nof these callers, and would currently be equivalent to this function,\nas the git_configset_get_value() API normalizes all non-zero return\nvalues to a \"1\".\n\nBut adding that API would set us up to lose information, as e.g. if\ngit_config_parse_key() in the underlying configset_find_element()\nfails we'd like to return -1, not 1.\n\nLet's change the underlying configset_find_element() function to\nsupport this use-case, we'll make further use of it in a subsequent\ncommit where the git_configset_get_value_multi() function itself will\nexpose this new return value.\n\n1. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                |  5 +---\n builtin/submodule--helper.c |  7 +++---\n builtin/worktree.c          |  3 +--\n config.c                    | 50 +++++++++++++++++++++++++++++++------\n config.h                    | 19 +++++++++++++-\n 5 files changed, 66 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 02455fdcd73..e38d1783f30 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1493,7 +1493,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \t};\n \tint found = 0;\n \tconst char *key = \"maintenance.repo\";\n-\tchar *config_value;\n \tchar *maintpath = get_maintpath();\n \tstruct string_list_item *item;\n \tconst struct string_list *list;\n@@ -1508,9 +1507,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tgit_config_set(\"maintenance.auto\", \"false\");\n \n \t/* Set maintenance strategy, if unset */\n-\tif (!git_config_get_string(\"maintenance.strategy\", &config_value))\n-\t\tfree(config_value);\n-\telse\n+\tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n \tlist = git_config_get_value_multi(key);\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 4c173d8b37a..2278e8c91cb 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t * If there are no path args and submodule.active is set then,\n \t * by default, only initialize 'active' modules.\n \t */\n-\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\tif (!argc && !git_config_get(\"submodule.active\"))\n \t\tmodule_list_active(&list);\n \n \tinfo.prefix = prefix;\n@@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \t\t * If there are no path args and submodule.active is set then,\n \t\t * by default, only initialize 'active' modules.\n \t\t */\n-\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\t\tif (!argc && !git_config_get(\"submodule.active\"))\n \t\t\tmodule_list_active(&list);\n \n \t\tinfo.prefix = opt.prefix;\n@@ -3140,7 +3140,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con\n static void configure_added_submodule(struct add_data *add_data)\n {\n \tchar *key;\n-\tconst char *val;\n \tstruct child_process add_submod = CHILD_PROCESS_INIT;\n \tstruct child_process add_gitmodules = CHILD_PROCESS_INIT;\n \n@@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)\n \t * is_submodule_active(), since that function needs to find\n \t * out the value of \"submodule.active\" again anyway.\n \t */\n-\tif (!git_config_get_string_tmp(\"submodule.active\", &val)) {\n+\tif (!git_config_get(\"submodule.active\")) {\n \t\t/*\n \t\t * If the submodule being added isn't already covered by the\n \t\t * current configured pathspec, set the submodule's active flag\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex f51c40f1e1e..6ba42d4ad20 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -319,7 +319,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \n \tif (file_exists(from_file)) {\n \t\tstruct config_set cs = { { 0 } };\n-\t\tconst char *core_worktree;\n \t\tint bare;\n \n \t\tif (safe_create_leading_directories(to_file) ||\n@@ -338,7 +337,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \t\t\t\tto_file, \"core.bare\", NULL, \"true\", 0))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\n \t\t\t\t\"core.bare\", to_file);\n-\t\tif (!git_configset_get_value(&cs, \"core.worktree\", &core_worktree) &&\n+\t\tif (!git_configset_get(&cs, \"core.worktree\") &&\n \t\t\tgit_config_set_in_file_gently(to_file,\n \t\t\t\t\t\t\t\"core.worktree\", NULL))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\ndiff --git a/config.c b/config.c\nindex 00090a32fc3..b88da70c664 100644\n--- a/config.c\n+++ b/config.c\n@@ -2289,23 +2289,28 @@ void read_very_early_config(config_fn_t cb, void *data)\n \tconfig_with_options(cb, data, NULL, &opts);\n }\n \n-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n+static int configset_find_element(struct config_set *cs, const char *key,\n+\t\t\t\t  struct config_set_element **dest)\n {\n \tstruct config_set_element k;\n \tstruct config_set_element *found_entry;\n \tchar *normalized_key;\n+\tint ret;\n+\n \t/*\n \t * `key` may come from the user, so normalize it before using it\n \t * for querying entries from the hashmap.\n \t */\n-\tif (git_config_parse_key(key, &normalized_key, NULL))\n-\t\treturn NULL;\n+\tret = git_config_parse_key(key, &normalized_key, NULL);\n+\tif (ret)\n+\t\treturn ret;\n \n \thashmap_entry_init(&k.ent, strhash(normalized_key));\n \tk.key = normalized_key;\n \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n \tfree(normalized_key);\n-\treturn found_entry;\n+\t*dest = found_entry;\n+\treturn 0;\n }\n \n static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n@@ -2314,8 +2319,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n \tstruct string_list_item *si;\n \tstruct configset_list_item *l_item;\n \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n+\tint ret;\n \n-\te = configset_find_element(cs, key);\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret)\n+\t\treturn ret;\n \t/*\n \t * Since the keys are being fed by git_config*() callback mechanism, they\n \t * are already normalized. So simply add them without any further munging.\n@@ -2425,8 +2433,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *\n \n const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n {\n-\tstruct config_set_element *e = configset_find_element(cs, key);\n-\treturn e ? &e->value_list : NULL;\n+\tstruct config_set_element *e;\n+\n+\tif (configset_find_element(cs, key, &e))\n+\t\treturn NULL;\n+\telse if (!e)\n+\t\treturn NULL;\n+\treturn &e->value_list;\n+}\n+\n+int git_configset_get(struct config_set *cs, const char *key)\n+{\n+\tstruct config_set_element *e;\n+\tint ret;\n+\n+\tif ((ret = configset_find_element(cs, key, &e)))\n+\t\treturn ret;\n+\telse if (!e)\n+\t\treturn 1;\n+\treturn 0;\n }\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n@@ -2565,6 +2590,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)\n \tconfigset_iter(repo->config, fn, data);\n }\n \n+int repo_config_get(struct repository *repo, const char *key)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get(repo->config, key);\n+}\n+\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value)\n {\n@@ -2679,6 +2710,11 @@ void git_config_clear(void)\n \trepo_config_clear(the_repository);\n }\n \n+int git_config_get(const char *key)\n+{\n+\treturn repo_config_get(the_repository, key);\n+}\n+\n int git_config_get_value(const char *key, const char **value)\n {\n \treturn repo_config_get_value(the_repository, key, value);\ndiff --git a/config.h b/config.h\nindex ef9eade6414..04c5e594015 100644\n--- a/config.h\n+++ b/config.h\n@@ -471,9 +471,12 @@ void git_configset_clear(struct config_set *cs);\n \n /*\n  * These functions return 1 if not found, and 0 if found, leaving the found\n- * value in the 'dest' pointer.\n+ * value in the 'dest' pointer (if any).\n  */\n \n+RESULT_MUST_BE_USED\n+int git_configset_get(struct config_set *cs, const char *key);\n+\n /*\n  * Finds the highest-priority value for the configuration variable `key`\n  * and config set `cs`, stores the pointer to it in `value` and returns 0.\n@@ -494,6 +497,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n /* Functions for reading a repository's config */\n struct repository;\n void repo_config(struct repository *repo, config_fn_t fn, void *data);\n+\n+/**\n+ * Run only the discover part of the repo_config_get_*() functions\n+ * below, in addition to 1 if not found, returns negative values on\n+ * error (e.g. if the key itself is invalid).\n+ */\n+RESULT_MUST_BE_USED\n+int repo_config_get(struct repository *repo, const char *key);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n const struct string_list *repo_config_get_value_multi(struct repository *repo,\n@@ -530,8 +541,14 @@ void git_protected_config(config_fn_t fn, void *data);\n  * manner, the config API provides two functions `git_config_get_value`\n  * and `git_config_get_value_multi`. They both read values from an internal\n  * cache generated previously from reading the config files.\n+ *\n+ * For those git_config_get*() functions that aren't documented,\n+ * consult the corresponding repo_config_get*() function's\n+ * documentation.\n  */\n \n+int git_config_get(const char *key);\n+\n /**\n  * Finds the highest-priority value for the configuration variable `key`,\n  * stores the pointer to it in `value` and returns 0. When the\n-- \n2.39.1.1397.g8c8c074958d\n\n"},{"id":"471356","messageId":"patch-v4-6.9-17c1218e74c-20230202T131155Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","subject":"[PATCH v4 6/9] for-each-repo: error on bad --config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-02T13:27:18Z","receivedAt":"2023-02-02T13:28:15Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in 6c62f015520 (for-each-repo: do nothing on empty config,\n2021-01-08) this command wants to ignore a non-existing config key,\nbut let's not conflate that with bad config.\n\nBefore this, all these added tests would pass with an exit code of 0.\n\nWe could preserve the comment added in 6c62f015520, but now that we're\ndirectly using the documented repo_config_get_value_multi() value it's\njust narrating something that should be obvious from the API use, so\nlet's drop it.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  | 11 ++++++-----\n t/t0068-for-each-repo.sh |  6 ++++++\n 2 files changed, 12 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex fd0e7739e6a..224164addb3 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tstatic const char *config_key = NULL;\n \tint i, result = 0;\n \tconst struct string_list *values;\n+\tint err;\n \n \tconst struct option options[] = {\n \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n@@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\t/*\n-\t * Do nothing on an empty list, which is equivalent to the case\n-\t * where the config variable does not exist at all.\n-\t */\n-\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n+\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\tif (err < 0)\n+\t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n+\t\t\t       for_each_repo_usage, options, config_key);\n+\telse if (err)\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 3648d439a87..6b51e00da0e 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '\n \tgit for-each-repo --config=bogus.config -- help --no-such-option\n '\n \n+test_expect_success 'error on bad config keys' '\n+\ttest_expect_code 129 git for-each-repo --config=a &&\n+\ttest_expect_code 129 git for-each-repo --config=a.b. &&\n+\ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n+'\n+\n test_done\n-- \n2.39.1.1397.g8c8c074958d\n\n"},{"id":"471357","messageId":"patch-v4-5.9-23449ff2c4e-20230202T131155Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","subject":"[PATCH v4 5/9] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-02T13:27:17Z","receivedAt":"2023-02-02T13:28:17Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Have the \"git_configset_get_value_multi()\" function and its siblings\nreturn an \"int\" and populate a \"**dest\" parameter like every other\ngit_configset_get_*()\" in the API.\n\nAs we'll see in in subsequent commits this fixes a blind spot in the\nAPI where it wasn't possible to tell whether a list was empty from\nwhether a config key existed. We'll take advantage of that in\nsubsequent commits, but for now we're faithfully converting existing\nAPI callers.\n\nA logical follow-up to this would be to change the various \"*_get_*()\"\nfunctions to ferry the git_configset_get_value() return value to their\nown callers, e.g. git_configset_get_int() returns \"1\" rather than\nferrying up the \"-1\" that \"git_configset_get_value()\" might return,\nbut that's not being done in this series\n\nMost of this is straightforward, commentary on cases that stand out:\n\n- To ensure that we'll properly use the return values of this function\n  in the future we're using the \"RESULT_MUST_BE_USED\" macro introduced\n  in [1].\n\n  As git_die_config() now has to handle this return value let's have\n  it BUG() if it can't find the config entry. As tested for in a\n  preceding commit we can rely on getting the config list in\n  git_die_config().\n\n- The loops after getting the \"list\" value in \"builtin/gc.c\" could\n  also make use of \"unsorted_string_list_has_string()\" instead of using\n  that loop, but let's leave that for now.\n\n- In \"versioncmp.c\" we now use the return value of the functions,\n  instead of checking if the lists are still non-NULL.\n\n1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n   return values, 2022-09-01),\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c |  5 +----\n builtin/gc.c            | 10 ++++------\n builtin/log.c           |  6 +++---\n config.c                | 34 ++++++++++++++++++++--------------\n config.h                | 29 +++++++++++++++++++++--------\n pack-bitmap.c           |  6 +++++-\n submodule.c             |  3 +--\n t/helper/test-config.c  |  6 ++----\n versioncmp.c            | 11 +++++++----\n 9 files changed, 64 insertions(+), 46 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 6aeac371488..fd0e7739e6a 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -45,14 +45,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\tvalues = repo_config_get_value_multi(the_repository,\n-\t\t\t\t\t     config_key);\n-\n \t/*\n \t * Do nothing on an empty list, which is equivalent to the case\n \t * where the config variable does not exist at all.\n \t */\n-\tif (!values)\n+\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e38d1783f30..2b3da377d52 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1510,8 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tlist = git_config_get_value_multi(key);\n-\tif (list) {\n+\tif (!git_config_get_value_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1577,11 +1576,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \tif (config_file) {\n \t\tgit_configset_init(&cs);\n \t\tgit_configset_add_file(&cs, config_file);\n-\t\tlist = git_configset_get_value_multi(&cs, key);\n-\t} else {\n-\t\tlist = git_config_get_value_multi(key);\n \t}\n-\tif (list) {\n+\tif (!(config_file\n+\t      ? git_configset_get_value_multi(&cs, key, &list)\n+\t      : git_config_get_value_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 04412dd9c93..cec8cabd21e 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tint i;\n \tchar *value = NULL;\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n-\tconst struct string_list *config_exclude =\n-\t\t\tgit_config_get_value_multi(\"log.excludeDecoration\");\n+\tconst struct string_list *config_exclude;\n \n-\tif (config_exclude) {\n+\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t&config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex b88da70c664..ce5d50a490c 100644\n--- a/config.c\n+++ b/config.c\n@@ -2417,29 +2417,34 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n+\tint ret;\n+\n \t/*\n \t * Follows \"last one wins\" semantic, i.e., if there are multiple matches for the\n \t * queried key in the files of the configset, the value returned will be the last\n \t * value in the value list for that key.\n \t */\n-\tvalues = git_configset_get_value_multi(cs, key);\n+\tif ((ret = git_configset_get_value_multi(cs, key, &values)))\n+\t\treturn ret;\n \n-\tif (!values)\n-\t\treturn 1;\n \tassert(values->nr > 0);\n \t*value = values->items[values->nr - 1].string;\n \treturn 0;\n }\n \n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest)\n {\n \tstruct config_set_element *e;\n+\tint ret;\n \n-\tif (configset_find_element(cs, key, &e))\n-\t\treturn NULL;\n+\tif ((ret = configset_find_element(cs, key, &e)))\n+\t\treturn ret;\n \telse if (!e)\n-\t\treturn NULL;\n-\treturn &e->value_list;\n+\t\treturn 1;\n+\t*dest = &e->value_list;\n+\n+\treturn 0;\n }\n \n int git_configset_get(struct config_set *cs, const char *key)\n@@ -2603,11 +2608,11 @@ int repo_config_get_value(struct repository *repo,\n \treturn git_configset_get_value(repo->config, key, value);\n }\n \n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key)\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest)\n {\n \tgit_config_check_init(repo);\n-\treturn git_configset_get_value_multi(repo->config, key);\n+\treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n int repo_config_get_string(struct repository *repo,\n@@ -2720,9 +2725,9 @@ int git_config_get_value(const char *key, const char **value)\n \treturn repo_config_get_value(the_repository, key, value);\n }\n \n-const struct string_list *git_config_get_value_multi(const char *key)\n+int git_config_get_value_multi(const char *key, const struct string_list **dest)\n {\n-\treturn repo_config_get_value_multi(the_repository, key);\n+\treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n int git_config_get_string(const char *key, char **dest)\n@@ -2869,7 +2874,8 @@ void git_die_config(const char *key, const char *err, ...)\n \t\terror_fn(err, params);\n \t\tva_end(params);\n \t}\n-\tvalues = git_config_get_value_multi(key);\n+\tif (git_config_get_value_multi(key, &values))\n+\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n \tkv_info = values->items[values->nr - 1].util;\n \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n }\ndiff --git a/config.h b/config.h\nindex 04c5e594015..fbc153cdc96 100644\n--- a/config.h\n+++ b/config.h\n@@ -459,10 +459,18 @@ int git_configset_add_parameters(struct config_set *cs);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n- * configuration variable `key` is not found, returns NULL. The caller\n- * should not free or modify the returned pointer, as it is owned by the cache.\n+ * configuration variable `key` is not found, returns 1 without touching\n+ * `value`.\n+ *\n+ * The key will be parsed for validity with git_config_parse_key(), on\n+ * error a negative value will be returned.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n+RESULT_MUST_BE_USED\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest);\n \n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n@@ -507,8 +515,9 @@ RESULT_MUST_BE_USED\n int repo_config_get(struct repository *repo, const char *key);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key);\n+RESULT_MUST_BE_USED\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -561,10 +570,14 @@ int git_config_get_value(const char *key, const char **value);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key`. When the configuration variable\n- * `key` is not found, returns NULL. The caller should not free or modify\n- * the returned pointer, as it is owned by the cache.\n+ * `key` is not found, returns 1 without touching `value`.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_config_get_value_multi(const char *key);\n+RESULT_MUST_BE_USED\n+int git_config_get_value_multi(const char *key,\n+\t\t\t       const struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex d2a42abf28c..15c5eb507c0 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2314,7 +2314,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)\n \n const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n-\treturn repo_config_get_value_multi(r, \"pack.preferbitmaptips\");\n+\tconst struct string_list *dest;\n+\n+\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\t\treturn dest;\n+\treturn NULL;\n }\n \n int bitmap_is_preferred_refname(struct repository *r, const char *refname)\ndiff --git a/submodule.c b/submodule.c\nindex 3a0dfc417c0..4b6f5223b0c 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tsl = repo_config_get_value_multi(repo, \"submodule.active\");\n-\tif (sl) {\n+\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex 4ba9eb65606..8f70beb6c9d 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -95,8 +95,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\tgoto exit1;\n \t\t}\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_value_multi\")) {\n-\t\tstrptr = git_config_get_value_multi(argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_config_get_value_multi(argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\n@@ -159,8 +158,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\t\tgoto exit2;\n \t\t\t}\n \t\t}\n-\t\tstrptr = git_configset_get_value_multi(&cs, argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 323f5d35ea8..60c3a517122 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)\n \tif (!initialized) {\n \t\tconst char *const newk = \"versionsort.suffix\";\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n+\t\tint new = git_config_get_value_multi(newk, &newl);\n+\t\tint old = git_config_get_value_multi(oldk, &oldl);\n \n-\t\tprereleases = git_config_get_value_multi(newk);\n-\t\toldl = git_config_get_value_multi(oldk);\n-\t\tif (prereleases && oldl)\n+\t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-\t\telse if (!prereleases)\n+\t\tif (!new)\n+\t\t\tprereleases = newl;\n+\t\telse if (!old)\n \t\t\tprereleases = oldl;\n \n \t\tinitialized = 1;\n-- \n2.39.1.1397.g8c8c074958d\n\n"},{"id":"471358","messageId":"patch-v4-7.9-7fc91eaf747-20230202T131155Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","subject":"[PATCH v4 7/9] config API users: test for *_get_value_multi() segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-02T13:27:19Z","receivedAt":"2023-02-02T13:28:19Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As we'll discuss in the subsequent commit these tests all\nshow *_get_value_multi() API users unable to handle there being a\nvalue-less key in the config, which is represented with a \"NULL\" for\nthat entry in the \"string\" member of the returned \"struct\nstring_list\", causing a segfault.\n\nThese added tests exhaustively test for that issue, as we'll see in a\nsubsequent commit we'll need to change all of the API users\nof *_get_value_multi(). These cases were discovered by triggering each\none individually, and then adding these tests.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t4202-log.sh                 | 11 +++++++++++\n t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++\n t/t7004-tag.sh                 | 12 ++++++++++++\n t/t7413-submodule-is-active.sh | 12 ++++++++++++\n t/t7900-maintenance.sh         | 23 +++++++++++++++++++++++\n 5 files changed, 74 insertions(+)\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 2ce2b41174d..e4f02d8208b 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n+test_expect_failure 'parse log.excludeDecoration with no value' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[log]\n+\t\texcludeDecoration\n+\tEOF\n+\tgit log --decorate=short\n+'\n+\n test_expect_success 'decorate-refs with glob' '\n \tcat >expect.decorate <<-\\EOF &&\n \tMerge-tag-reach\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 7d8dee41b0d..0306b399188 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,6 +404,22 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n+\ttest_expect_failure 'pack.preferBitmapTips' '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -rf repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\tgit config pack.writeBitmapLookupTable '\"$writeLookupTable\"' &&\n+\t\t\ttest_commit_bulk --message=\"%s\" 103 &&\n+\n+\t\t\tcat >>.git/config <<-\\EOF &&\n+\t\t\t[pack]\n+\t\t\t\tpreferBitmapTips\n+\t\t\tEOF\n+\t\t\tgit repack -adb\n+\t\t)\n+\t'\n+\n \ttest_expect_success 'complains about multiple pack bitmaps' '\n \t\trm -fr repo &&\n \t\tgit init repo &&\ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex 9aa1660651b..f343551a7d4 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[versionsort]\n+\t\tprereleaseSuffix\n+\t\tsuffix\n+\tEOF\n+\tgit tag -l --sort=version:refname\n+'\n+\n test_expect_success 'version sort with prerelease reordering' '\n \ttest_config versionsort.prereleaseSuffix -rc &&\n \tgit tag foo1.6-rc1 &&\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex 7cdc2637649..bfe27e50732 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n+test_expect_failure 'is-active handles submodule.active config missing a value' '\n+\tcp super/.git/config super/.git/config.orig &&\n+\ttest_when_finished mv super/.git/config.orig super/.git/config &&\n+\n+\tcat >>super/.git/config <<-\\EOF &&\n+\t[submodule]\n+\t\tactive\n+\tEOF\n+\n+\ttest-tool -C super submodule is-active sub1\n+'\n+\n test_expect_success 'is-active works with basic submodule.active config' '\n \ttest_when_finished \"git -C super config submodule.sub1.URL ../sub\" &&\n \ttest_when_finished \"git -C super config --unset-all submodule.active\" &&\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 823331e44a0..d82eac6a471 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,6 +524,29 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n+test_expect_failure 'register with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance register\n+'\n+\n+test_expect_failure 'unregister with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance unregister &&\n+\tgit maintenance unregister --force\n+'\n+\n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \tMETA=\"a+b*c\" &&\n \tgit init \"$META\" &&\n-- \n2.39.1.1397.g8c8c074958d\n\n"},{"id":"471359","messageId":"patch-v4-9.9-c7a5f5b4133-20230202T131155Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","subject":"[PATCH v4 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-02T13:27:21Z","receivedAt":"2023-02-02T13:28:21Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on\nconfigured repos, 2020-09-11). Due to assuming that elements returned\nfrom the repo_config_get_value_multi() call wouldn't be \"NULL\" we'd\nconflate the <path> and <command> part of the argument list when\nrunning commands.\n\nAs noted in the preceding commit the fix is to move to a safer\n\"*_string_multi()\" version of the *_multi() API. This change is\nseparated from the rest because those all segfaulted. In this change\nwe ended up with different behavior.\n\nWhen using the \"--config=<config>\" form we take each element of the\nlist as a path to a repository. E.g. with a configuration like:\n\n\t[repo] list = /some/repo\n\nWe would, with this command:\n\n\tgit for-each-repo --config=repo.list status builtin\n\nRun a \"git status\" in /some/repo, as:\n\n\tgit -C /some/repo status builtin\n\nI.e. ask \"status\" to report on the \"builtin\" directory. But since a\nconfiguration such as this would result in a \"struct string_list *\"\nwith one element, whose \"string\" member is \"NULL\":\n\n\t[repo] list\n\nWe would, when constructing our command-line in\n\"builtin/for-each-repo.c\"...\n\n\tstrvec_pushl(&child.args, \"-C\", path, NULL);\n\tfor (i = 0; i < argc; i++)\n\t\tstrvec_push(&child.args, argv[i]);\n\n...have that \"path\" be \"NULL\", and as strvec_pushl() stops when it\nsees NULL we'd end with the first \"argv\" element as the argument to\nthe \"-C\" option, e.g.:\n\n\tgit -C status builtin\n\nI.e. we'd run the command \"builtin\" in the \"status\" directory.\n\nIn another context this might be an interesting security\nvulnerability, but I think that this amounts to a nothingburger on\nthat front.\n\nA hypothetical attacker would need to be able to write config for the\nvictim to run, if they're able to do that there's more interesting\nattack vectors. See the \"safe.directory\" facility added in\n8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).\n\nAn even more unlikely possibility would be an attacker able to\ngenerate the config used for \"for-each-repo --config=<key>\", but\nnothing else (e.g. an automated system producing that list).\n\nEven in that case the attack vector is limited to the user running\ncommands whose name matches a directory that's interesting to the\nattacker (e.g. a \"log\" directory in a repository). The second\nargument (if any) of the command is likely to make git die without\ndoing anything interesting (e.g. \"-p\" to \"log\", there being no \"-p\"\nbuilt-in command to run).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  |  2 +-\n t/t0068-for-each-repo.sh | 13 +++++++++++++\n 2 files changed, 14 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 224164addb3..ce8f7a99086 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\terr = repo_config_get_string_multi(the_repository, config_key, &values);\n \tif (err < 0)\n \t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n \t\t\t       for_each_repo_usage, options, config_key);\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 6b51e00da0e..4b90b74d5d5 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -46,4 +46,17 @@ test_expect_success 'error on bad config keys' '\n \ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n '\n \n+test_expect_success 'error on NULL value for config keys' '\n+\tcat >>.git/config <<-\\EOF &&\n+\t[empty]\n+\t\tkey\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''empty.key'\\''\n+\tEOF\n+\ttest_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.39.1.1397.g8c8c074958d\n\n"},{"id":"471360","messageId":"patch-v4-8.9-a391ee17617-20230202T131155Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","subject":"[PATCH v4 8/9] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-02T13:27:20Z","receivedAt":"2023-02-02T13:28:22Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix numerous and mostly long-standing segfaults in consumers of\nthe *_config_*value_multi() API. As discussed in the preceding commit\nan empty key in the config syntax yields a \"NULL\" string, which these\nusers would give to strcmp() (or similar), resulting in segfaults.\n\nAs this change shows, most users users of the *_config_*value_multi()\nAPI didn't really want such an an unsafe and low-level API, let's give\nthem something with the safety of git_config_get_string() instead.\n\nThis fix is similar to what the *_string() functions and others\nacquired in[1] and [2]. Namely introducing and using a safer\n\"*_get_string_multi()\" variant of the low-level \"_*value_multi()\"\nfunction.\n\nThis fixes segfaults in code introduced in:\n\n  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)\n  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)\n  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)\n  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)\n  - 92156291ca8 (log: add default decoration filter, 2022-08-05)\n  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)\n\nThere are now three remaining files using the low-level API:\n\n- Two cases in \"builtin/submodule--helper.c\", where it's used safely\n  to see if any config exists.\n\n  We could refactor these away from \"multi\" to some \"does it exist?\"\n  function, as [4] did, but as that's orthogonal to the \"string\"\n  safety we're introducing here let's leave them for now.\n\n- One in \"builtin/for-each-repo.c\", which we'll convert in a\n  subsequent commit.\n\n- The \"t/helper/test-config.c\" code added in [4].\n\nAs seen in the preceding commit we need to give the\n\"t/helper/test-config.c\" caller these \"NULL\" entries.\n\nWe could also alter the underlying git_configset_get_value_multi()\nfunction to be \"string safe\", but doing so would leave no room for\nother variants of \"*_get_value_multi()\" that coerce to other types.\n\nSuch coercion can't be built on the string version, since as we've\nestablished \"NULL\" is a true value in the boolean context, but if we\ncoerced it to \"\" for use in a list of strings it'll be subsequently\ncoerced to \"false\" as a boolean.\n\nThe callback pattern being used here will make it easy to introduce\ne.g. a \"multi\" variant which coerces its values to \"bool\", \"int\",\n\"path\" etc.\n\n1. 40ea4ed9032 (Add config_error_nonbool() helper function,\n   2008-02-11)\n2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,\n   2008-02-11).\n3. https://lore.kernel.org/git/patch-07.10-c01f7d85c94-20221026T151328Z-avarab@gmail.com/\n4. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                   |  6 +++---\n builtin/log.c                  |  4 ++--\n config.c                       | 32 ++++++++++++++++++++++++++++++++\n config.h                       | 19 +++++++++++++++++++\n pack-bitmap.c                  |  2 +-\n submodule.c                    |  2 +-\n t/t4202-log.sh                 |  8 ++++++--\n t/t5310-pack-bitmaps.sh        |  8 ++++++--\n t/t7004-tag.sh                 |  9 +++++++--\n t/t7413-submodule-is-active.sh |  8 ++++++--\n t/t7900-maintenance.sh         | 25 ++++++++++++++++++++-----\n versioncmp.c                   |  4 ++--\n 12 files changed, 105 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 2b3da377d52..9497bdf23e4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1510,7 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tif (!git_config_get_value_multi(key, &list)) {\n+\tif (!git_config_get_string_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1578,8 +1578,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tgit_configset_add_file(&cs, config_file);\n \t}\n \tif (!(config_file\n-\t      ? git_configset_get_value_multi(&cs, key, &list)\n-\t      : git_config_get_value_multi(key, &list))) {\n+\t      ? git_configset_get_string_multi(&cs, key, &list)\n+\t      : git_config_get_string_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex cec8cabd21e..481685d5263 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n \tconst struct string_list *config_exclude;\n \n-\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n-\t\t\t\t\t&config_exclude)) {\n+\tif (!git_config_get_string_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t &config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex ce5d50a490c..30867663997 100644\n--- a/config.c\n+++ b/config.c\n@@ -2447,6 +2447,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \treturn 0;\n }\n \n+static int check_multi_string(struct string_list_item *item, void *util)\n+{\n+\treturn item->string ? 0 : config_error_nonbool(util);\n+}\n+\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest)\n+{\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value_multi(cs, key, dest)))\n+\t\treturn ret;\n+\tif ((ret = for_each_string_list((struct string_list *)*dest,\n+\t\t\t\t\tcheck_multi_string, (void *)key)))\n+\t\treturn ret;\n+\n+\treturn 0;\n+}\n+\n int git_configset_get(struct config_set *cs, const char *key)\n {\n \tstruct config_set_element *e;\n@@ -2615,6 +2634,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,\n \treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_string_multi(repo->config, key, dest);\n+}\n+\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest)\n {\n@@ -2730,6 +2756,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)\n \treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest)\n+{\n+\treturn repo_config_get_string_multi(the_repository, key, dest);\n+}\n+\n int git_config_get_string(const char *key, char **dest)\n {\n \treturn repo_config_get_string(the_repository, key, dest);\ndiff --git a/config.h b/config.h\nindex fbc153cdc96..d98a06352e3 100644\n--- a/config.h\n+++ b/config.h\n@@ -472,6 +472,19 @@ RESULT_MUST_BE_USED\n int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \t\t\t\t  const struct string_list **dest);\n \n+/**\n+ * A validation wrapper for git_configset_get_value_multi() which does\n+ * for it what git_configset_get_string() does for\n+ * git_configset_get_value().\n+ *\n+ * The configuration syntax allows for \"[section] key\", which will\n+ * give us a NULL entry in the \"struct string_list\", as opposed to\n+ * \"[section] key =\" which is the empty string. Most users of the API\n+ * are not prepared to handle NULL in a \"struct string_list\".\n+ */\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest);\n+\n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n  */\n@@ -518,6 +531,9 @@ int repo_config_get_value(struct repository *repo,\n RESULT_MUST_BE_USED\n int repo_config_get_value_multi(struct repository *repo, const char *key,\n \t\t\t\tconst struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -578,6 +594,9 @@ int git_config_get_value(const char *key, const char **value);\n RESULT_MUST_BE_USED\n int git_config_get_value_multi(const char *key,\n \t\t\t       const struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 15c5eb507c0..d003c7e60b4 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2316,7 +2316,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n \tconst struct string_list *dest;\n \n-\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\tif (!repo_config_get_string_multi(r, \"pack.preferbitmaptips\", &dest))\n \t\treturn dest;\n \treturn NULL;\n }\ndiff --git a/submodule.c b/submodule.c\nindex 4b6f5223b0c..30a103246ec 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n+\tif (!repo_config_get_string_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex e4f02d8208b..ae73aef922f 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n-test_expect_failure 'parse log.excludeDecoration with no value' '\n+test_expect_success 'parse log.excludeDecoration with no value' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '\n \t[log]\n \t\texcludeDecoration\n \tEOF\n-\tgit log --decorate=short\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''log.excludeDecoration'\\''\n+\tEOF\n+\tgit log --decorate=short 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'decorate-refs with glob' '\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 0306b399188..526a5a506eb 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,7 +404,7 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n-\ttest_expect_failure 'pack.preferBitmapTips' '\n+\ttest_expect_success 'pack.preferBitmapTips' '\n \t\tgit init repo &&\n \t\ttest_when_finished \"rm -rf repo\" &&\n \t\t(\n@@ -416,7 +416,11 @@ test_bitmap_cases () {\n \t\t\t[pack]\n \t\t\t\tpreferBitmapTips\n \t\t\tEOF\n-\t\t\tgit repack -adb\n+\t\t\tcat >expect <<-\\EOF &&\n+\t\t\terror: missing value for '\\''pack.preferbitmaptips'\\''\n+\t\t\tEOF\n+\t\t\tgit repack -adb 2>actual &&\n+\t\t\ttest_cmp expect actual\n \t\t)\n \t'\n \ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex f343551a7d4..f4a31ada79a 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas\n \t\tprereleaseSuffix\n \t\tsuffix\n \tEOF\n-\tgit tag -l --sort=version:refname\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''versionsort.suffix'\\''\n+\terror: missing value for '\\''versionsort.prereleasesuffix'\\''\n+\tEOF\n+\tgit tag -l --sort=version:refname 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'version sort with prerelease reordering' '\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex bfe27e50732..887d181b72e 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n-test_expect_failure 'is-active handles submodule.active config missing a value' '\n+test_expect_success 'is-active handles submodule.active config missing a value' '\n \tcp super/.git/config super/.git/config.orig &&\n \ttest_when_finished mv super/.git/config.orig super/.git/config &&\n \n@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'\n \t\tactive\n \tEOF\n \n-\ttest-tool -C super submodule is-active sub1\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''submodule.active'\\''\n+\tEOF\n+\ttest-tool -C super submodule is-active sub1 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'is-active works with basic submodule.active config' '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d82eac6a471..487e326b3fa 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n-test_expect_failure 'register with no value for maintenance.repo' '\n+test_expect_success 'register with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance register\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\tgit maintenance register 2>actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n-test_expect_failure 'unregister with no value for maintenance.repo' '\n+test_expect_success 'unregister with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -543,8 +548,18 @@ test_expect_failure 'unregister with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance unregister &&\n-\tgit maintenance unregister --force\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\ttest_expect_code 128 git maintenance unregister 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo &&\n+\n+\tgit maintenance unregister --force 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 60c3a517122..7498da96e0e 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n \t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n-\t\tint new = git_config_get_value_multi(newk, &newl);\n-\t\tint old = git_config_get_value_multi(oldk, &oldl);\n+\t\tint new = git_config_get_string_multi(newk, &newl);\n+\t\tint old = git_config_get_string_multi(oldk, &oldl);\n \n \t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-- \n2.39.1.1397.g8c8c074958d\n\n"},{"id":"471405","messageId":"xmqqtu03r79x.fsf@gitster.g","threadId":"58696","inReplyTo":"patch-v4-2.9-1f0f8bdcde9-20230202T131155Z-avarab@gmail.com","subject":"Re: [PATCH v4 2/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-02-02T23:12:42Z","receivedAt":"2023-02-02T23:12:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> A less well known edge case in the config format is that keys can be\n> value-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\n> are equivalent as far as \"--type=bool\" is concerned:\n>\n> \t[a]key\n> \t[a]key = true\n>\n> But as far as our parser is concerned the values for these two are\n> NULL, and \"true\". I.e. for a sequence like:\n>\n> \t[a]key=x\n> \t[a]key\n> \t[a]key=y\n>\n> We get a \"struct string_list\" with \"string\" members with \".string\"\n> values of:\n>\n> \t{ \"x\", NULL, \"y\" }\n>\n> This behavior goes back to the initial implementation of\n> git_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n> 2005-10-10).\n>\n> When parts of the config_set API were tested for in [1] they didn't\n> add coverage for 3/4 of the \"(NULL)\" cases handled in\n> \"t/helper/test-config.c\". We'd test that case for \"get_value\", but not\n> \"get_value_multi\", \"configset_get_value\" and\n> \"configset_get_value_multi\".\n>\n> We now cover all of those cases, which in turn expose the details of\n> how this part of the config API works.\n\nGood to see a better coverage.\n\nWith the \"last one wins\" semantics for half of these 4 cases, it may\nmake sense to further extend the tests to cover cases where the last\none is a valueless true, in addition to what is used in this patch\n(i.e. a valueless true in the middle of three).\n\nThanks.\n"},{"id":"471409","messageId":"xmqqv8kjpqoe.fsf@gitster.g","threadId":"58696","inReplyTo":"patch-v4-3.9-998b11ae4bc-20230202T131155Z-avarab@gmail.com","subject":"Re: [PATCH v4 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-02-02T23:56:33Z","receivedAt":"2023-02-02T23:56:57Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> We already have the basic \"git_config_get_value()\" function and its\n> \"repo_*\" and \"configset\" siblings to get a given \"key\" and assign the\n> last key found to a provided \"value\".\n>\n> But some callers don't care about that value, but just want to use the\n> return value of the \"get_value()\" function to check whether the key\n> exist (or another non-zero return value).\n>\n> The immediate motivation for this is that a subsequent commit will\n> need to change all callers of the \"*_get_value_multi()\" family of\n> functions. In two cases here we (ab)used it to check whether we had\n> any values for the given key, but didn't care about the return value.\n\nSo, the idea is that \n\n\tif (!git_config_get_string(key, &discard))\n\t\tfree(discard);\n\telse\n\t\t... the key is missing ...\n\nbecomes\n\n\tif (git_config_get(key))\n\t\t... the key is missing ...\n\nIn other words, git_config_get() returns 0 only when the key is\nused, and non-zero return signals that the key is not used?\n\nSimilarly, get_value_multi() was an interface to get to the\nvalue_list associated to the given key, and was abused like\n\n\tif (git_config_get_value_multi(key))\n\t\t... the key exists ...\n\nwhich will become\n\n\tif (!git_config_get(key))\n\t\t... the key exists ...\n\nright?\n\n>  \t/* Set maintenance strategy, if unset */\n> -\tif (!git_config_get_string(\"maintenance.strategy\", &config_value))\n> -\t\tfree(config_value);\n> -\telse\n> +\tif (git_config_get(\"maintenance.strategy\"))\n>  \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n\nOK.  config_get() says \"true\" meaning the key is missing.\n\n> -\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n> +\tif (!argc && !git_config_get(\"submodule.active\"))\n>  \t\tmodule_list_active(&list);\n\nOK.\n\n> @@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n>  \t\t * If there are no path args and submodule.active is set then,\n>  \t\t * by default, only initialize 'active' modules.\n>  \t\t */\n> -\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n> +\t\tif (!argc && !git_config_get(\"submodule.active\"))\n>  \t\t\tmodule_list_active(&list);\n\nOK.\n\n> @@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)\n>  \t * is_submodule_active(), since that function needs to find\n>  \t * out the value of \"submodule.active\" again anyway.\n>  \t */\n> -\tif (!git_config_get_string_tmp(\"submodule.active\", &val)) {\n> +\tif (!git_config_get(\"submodule.active\")) {\n\nstring_tmp() variant is to retrieve borrowed value, and it returns 0\nwhen there is a value.  If it is a valueless true, we get -1 back\nwith an error message.  What does the updated version do in the\nvalueless true case?\n\n> diff --git a/builtin/worktree.c b/builtin/worktree.c\n> index f51c40f1e1e..6ba42d4ad20 100644\n> --- a/builtin/worktree.c\n> +++ b/builtin/worktree.c\n> @@ -338,7 +337,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n>  \t\t\t\tto_file, \"core.bare\", NULL, \"true\", 0))\n>  \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\n>  \t\t\t\t\"core.bare\", to_file);\n> -\t\tif (!git_configset_get_value(&cs, \"core.worktree\", &core_worktree) &&\n> +\t\tif (!git_configset_get(&cs, \"core.worktree\") &&\n\nOK.\n\n> diff --git a/config.c b/config.c\n> index 00090a32fc3..b88da70c664 100644\n> --- a/config.c\n> +++ b/config.c\n> @@ -2289,23 +2289,28 @@ void read_very_early_config(config_fn_t cb, void *data)\n>  \tconfig_with_options(cb, data, NULL, &opts);\n>  }\n>  \n> -static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n> +static int configset_find_element(struct config_set *cs, const char *key,\n> +\t\t\t\t  struct config_set_element **dest)\n>  {\n>  \tstruct config_set_element k;\n>  \tstruct config_set_element *found_entry;\n>  \tchar *normalized_key;\n> +\tint ret;\n> +\n>  \t/*\n>  \t * `key` may come from the user, so normalize it before using it\n>  \t * for querying entries from the hashmap.\n>  \t */\n> -\tif (git_config_parse_key(key, &normalized_key, NULL))\n> -\t\treturn NULL;\n> +\tret = git_config_parse_key(key, &normalized_key, NULL);\n> +\tif (ret)\n> +\t\treturn ret;\n>  \n>  \thashmap_entry_init(&k.ent, strhash(normalized_key));\n>  \tk.key = normalized_key;\n>  \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n>  \tfree(normalized_key);\n> -\treturn found_entry;\n> +\t*dest = found_entry;\n> +\treturn 0;\n\nOK, so we used to return NULL when the key is not parseable, and\notherwise we returned the config_set_element we found for the key,\nor NULL if there is no such element.  Now we return error code as\nthe return value and allow the caller to peek the element via *dest\nparameter.\n\nSo, from the caller's point of view (dest != NULL) is how it checks\nif the key is used.  The function returning 0 is a sign that the key\npassed to it is healthy.  OK.\n\n> @@ -2314,8 +2319,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n>  \tstruct string_list_item *si;\n>  \tstruct configset_list_item *l_item;\n>  \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n> +\tint ret;\n>  \n> -\te = configset_find_element(cs, key);\n> +\tret = configset_find_element(cs, key, &e);\n> +\tif (ret)\n> +\t\treturn ret;\n\nThe function never returned any meaningful error, so the callers may\nnot be prepared to see such an error return.  But now we at least\nnotice an error at this level.\n\n> @@ -2425,8 +2433,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *\n>  \n>  const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n>  {\n> -\tstruct config_set_element *e = configset_find_element(cs, key);\n> -\treturn e ? &e->value_list : NULL;\n> +\tstruct config_set_element *e;\n> +\n> +\tif (configset_find_element(cs, key, &e))\n> +\t\treturn NULL;\n> +\telse if (!e)\n> +\t\treturn NULL;\n> +\treturn &e->value_list;\n> +}\n\nOK.  !e means \"we got a healthy key and peeking into the hash table,\nthere wasn't any entry for the key\", and that is reported with NULL.\nDo we evern return a string list with .nr == 0, I wonder.  Having to\ndeal with such a list would make the caller's job more complex, but\nperhaps we are not allowing the code to shrink value_list.nr to\navoid such a situation?\n\n> +int git_configset_get(struct config_set *cs, const char *key)\n> +{\n> +\tstruct config_set_element *e;\n> +\tint ret;\n> +\n> +\tif ((ret = configset_find_element(cs, key, &e)))\n> +\t\treturn ret;\n> +\telse if (!e)\n> +\t\treturn 1;\n> +\treturn 0;\n>  }\n\nOK.  So 0 return from the function means there is a value (or more)\nfor a given key.  Good.\n\n> diff --git a/config.h b/config.h\n> index ef9eade6414..04c5e594015 100644\n> --- a/config.h\n> +++ b/config.h\n> @@ -471,9 +471,12 @@ void git_configset_clear(struct config_set *cs);\n>  \n>  /*\n>   * These functions return 1 if not found, and 0 if found, leaving the found\n> - * value in the 'dest' pointer.\n> + * value in the 'dest' pointer (if any).\n>   */\n\nNow the returned non-zero values are no longer 1 alone, no?\nWhatever lower-level functions use to signal an error is propagated\nup with the\n\n\tif ((ret = func())\n\t\treturn ret;\n\npattern.\n"},{"id":"471412","messageId":"xmqqwn4zo859.fsf@gitster.g","threadId":"58696","inReplyTo":"patch-v4-1.9-4ae56cab7c7-20230202T131155Z-avarab@gmail.com","subject":"Re: [PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-02-03T01:22:10Z","receivedAt":"2023-02-03T01:22:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> There were no tests checking for the output of the git_die_config()\n> function in the config API, added in 5a80e97c827 (config: add\n> `git_die_config()` to the config-set API, 2014-08-07). We only tested\n> \"test_must_fail\", but didn't assert the output.\n\nIt sort of is expected as git_die_config() is useful only for code\nthat uses new style config parsing (i.e. instead of using the\ngit_config() callback interface to parse what we encounter in the\nconfig file, actively call git_config_get_foo() interface to ask for\nkeys the caller cares about), and dying unconditionally is not\nuseful for the old style ones.\n\nA better coverage is good.\n"},{"id":"471463","messageId":"xmqq1qn6o1ra.fsf@gitster.g","threadId":"58696","inReplyTo":"patch-v4-4.9-aae1d5c12a9-20230202T131155Z-avarab@gmail.com","subject":"Re: [PATCH v4 4/9] versioncmp.c: refactor config reading next commit","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-02-03T21:52:25Z","receivedAt":"2023-02-03T21:52:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> Refactor the reading of the versionSort.suffix and\n> versionSort.prereleaseSuffix configuration variables to stay within\n> the bounds of our CodingGuidelines when it comes to line length, and\n> to avoid repeating ourselves.\n>\n> Let's also split out the names of the config variables into variables\n> of our own, so we don't have to repeat ourselves, \n\nYou do not have to repeat \"we don't have to repeat\" by mentioning it\ntwice in two paragraphs.\n\n> Moving the \"initialized = 1\" assignment allows us to move some of this\n> to the variable declarations in the subsequent commit.\n\nUnclear until looking at these subsequent steps; let's see what\nhappens next ;-).\n\n>  \tif (!initialized) {\n> -\t\tconst struct string_list *deprecated_prereleases;\n> +\t\tconst char *const newk = \"versionsort.suffix\";\n> +\t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n> +\t\tconst struct string_list *oldl;\n\nWith s/oldl/deprecated_prereleases/ the damage would even be\nsmaller.  It's not like a more descriptive name in this small scope\nhurts line length or readability, is it?\n\n> +\t\tprereleases = git_config_get_value_multi(newk);\n> +\t\toldl = git_config_get_value_multi(oldk);\n\n> +\t\tif (prereleases && oldl)\n> +\t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n> +\t\telse if (!prereleases)\n> +\t\t\tprereleases = oldl;\n\nThis makes it more clear than the original what is going on, even\nthough they are equivalent.  If we have both, we ignore the\nfallback, and if we don't have what we need, we replace it with the\nfallback, which could be NULL in which case we end up not having\nany.\n\nIt is a very nice added bonus that we ended up with a shallow\nnesting.\n"},{"id":"471550","messageId":"kl6l357ji4a6.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v4-1.9-4ae56cab7c7-20230202T131155Z-avarab@gmail.com","subject":"Re: [PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-02-06T08:31:13Z","receivedAt":"2023-02-06T08:31:28Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> We need tests for this because a subsequent commit will alter the\n> return value of git_config_get_value_multi(), which is used to get the\n> config values in the git_die_config() function. This test coverage\n> helps to build confidence in that subsequent change.\n\nIn v3, I recall having to read ahead quite far ahead in the series to\nunderstand why we needed this patch. In comparison, this is a lot\nclearer :)\n"},{"id":"471551","messageId":"kl6lzg9rgjqv.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v4-2.9-1f0f8bdcde9-20230202T131155Z-avarab@gmail.com","subject":"Re: [PATCH v4 2/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-02-06T10:40:08Z","receivedAt":"2023-02-06T10:40:57Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> +test_NULL_in_multi () {\n> +\tlocal op=\"$1\" &&\n> +\tlocal file=\"$2\" &&\n> +\n> +\ttest_expect_success \"$op: NULL value in config${file:+ in $file}\" '\n> +\t\tconfig=\"$file\" &&\n> +\t\tif test -z \"$config\"\n> +\t\tthen\n> +\t\t\tconfig=.git/config &&\n> +\t\t\ttest_when_finished \"mv $config.old $config\" &&\n> +\t\t\tmv \"$config\" \"$config\".old\n> +\t\tfi &&\n> +\n> +\t\tcat >\"$config\" <<-\\EOF &&\n> +\t\t[a]key=x\n> +\t\t[a]key\n> +\t\t[a]key=y\n> +\t\tEOF\n> +\t\tcase \"$op\" in\n> +\t\t*_multi)\n> +\t\t\tcat >expect <<-\\EOF\n> +\t\t\tx\n> +\t\t\t(NULL)\n> +\t\t\ty\n> +\t\t\tEOF\n> +\t\t\t;;\n> +\t\t*)\n> +\t\t\tcat >expect <<-\\EOF\n> +\t\t\ty\n> +\t\t\tEOF\n> +\t\t\t;;\n> +\t\tesac &&\n> +\t\ttest-tool config \"$op\" a.key $file >actual &&\n> +\t\ttest_cmp expect actual\n> +\t'\n> +}\n> +\n> +test_NULL_in_multi \"get_value_multi\"\n> +test_NULL_in_multi \"configset_get_value\" \"my.config\"\n> +test_NULL_in_multi \"configset_get_value_multi\" \"my.config\"\n\nI frankly preferred v3's tests over this version. v3 is slightly\nverbose, but at least the lack of logic made it easy to read and\nunderstand. I'd be okay with it if we get a big DRY-ness benefit, but 2\nconditionals for 3 cases seems quite un-DRY to me.\n"},{"id":"471552","messageId":"230206.86edr36ki2.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"kl6lzg9rgjqv.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v4 2/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-06T12:31:36Z","receivedAt":"2023-02-06T12:34:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Feb 06 2023, Glen Choo wrote:\n\n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>\n>> +test_NULL_in_multi () {\n>> +\tlocal op=\"$1\" &&\n>> +\tlocal file=\"$2\" &&\n>> +\n>> +\ttest_expect_success \"$op: NULL value in config${file:+ in $file}\" '\n>> +\t\tconfig=\"$file\" &&\n>> +\t\tif test -z \"$config\"\n>> +\t\tthen\n>> +\t\t\tconfig=.git/config &&\n>> +\t\t\ttest_when_finished \"mv $config.old $config\" &&\n>> +\t\t\tmv \"$config\" \"$config\".old\n>> +\t\tfi &&\n>> +\n>> +\t\tcat >\"$config\" <<-\\EOF &&\n>> +\t\t[a]key=x\n>> +\t\t[a]key\n>> +\t\t[a]key=y\n>> +\t\tEOF\n>> +\t\tcase \"$op\" in\n>> +\t\t*_multi)\n>> +\t\t\tcat >expect <<-\\EOF\n>> +\t\t\tx\n>> +\t\t\t(NULL)\n>> +\t\t\ty\n>> +\t\t\tEOF\n>> +\t\t\t;;\n>> +\t\t*)\n>> +\t\t\tcat >expect <<-\\EOF\n>> +\t\t\ty\n>> +\t\t\tEOF\n>> +\t\t\t;;\n>> +\t\tesac &&\n>> +\t\ttest-tool config \"$op\" a.key $file >actual &&\n>> +\t\ttest_cmp expect actual\n>> +\t'\n>> +}\n>> +\n>> +test_NULL_in_multi \"get_value_multi\"\n>> +test_NULL_in_multi \"configset_get_value\" \"my.config\"\n>> +test_NULL_in_multi \"configset_get_value_multi\" \"my.config\"\n>\n> I frankly preferred v3's tests over this version. v3 is slightly\n> verbose, but at least the lack of logic made it easy to read and\n> understand. I'd be okay with it if we get a big DRY-ness benefit, but 2\n> conditionals for 3 cases seems quite un-DRY to me.\n\nNote that the v3 version didn't test the get_value_multi(), adjusting\nthe v3 version with copy/paste to test that as well is why I made this a\nfunction. From the CL's range-diff:\n\n    -    When the \"t/t1308-config-set.sh\" tests were added in [1] only one of\n    -    the three \"(NULL)\" lines in \"t/helper/test-config.c\" had any test\n    -    coverage. This change adds tests that stress the remaining two.\n    +    When parts of the config_set API were tested for in [1] they didn't\n    +    add coverage for 3/4 of the \"(NULL)\" cases handled in\n    +    \"t/helper/test-config.c\". We'd test that case for \"get_value\", but not\n    +    \"get_value_multi\", \"configset_get_value\" and\n    +    \"configset_get_value_multi\".\n    +\n    +    We now cover all of those cases, which in turn expose the details of\n    +    how this part of the config API works.\n\nOf course that wouldn't address an outstanding point that we should just\ncopy/paste these anyway, but maybe that addresses your feedback...\n"},{"id":"471553","messageId":"kl6lv8kfgedg.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v4-3.9-998b11ae4bc-20230202T131155Z-avarab@gmail.com","subject":"Re: [PATCH v4 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-02-06T12:36:11Z","receivedAt":"2023-02-06T12:36:28Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"I think introducing a new function to replace the *_get_value_multi()\nabuses is a good way forward. Junio has already adequately commented on\nyour implementation, so I'll focus this review on a different approach\nthat this patch could have taken.\n\nÆvar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> We already have the basic \"git_config_get_value()\" function and its\n> \"repo_*\" and \"configset\" siblings to get a given \"key\" and assign the\n> last key found to a provided \"value\".\n>\n> But some callers don't care about that value, but just want to use the\n> return value of the \"get_value()\" function to check whether the key\n> exist (or another non-zero return value).\n\n[...]\n\n> We could have changed git_configset_get_value_multi() to accept a\n> \"NULL\" as a \"dest\" for all callers, but let's avoid changing the\n> behavior of existing API users. Having an \"unused\" value that we throw\n> away internal to config.c is cheap.\n\nThere is yet another option, which is to teach \"git_config_get_value()\"\n(mentioned earlier) to accept NULL to mean \"I just want to know if there\nis a value, I don't care what it is\". That's what the *_get_<type>()\nfunctions use under the hood (i.e. the ones that return either 0 or 1 or\nexit).\n\nThis amounts to implementing the \"*_config_key_exists()\" API you\nmentioned, but I think this is better fit for the current set of\nsemantics. At the very least, that would be an easy 1-1 replacement for\nthe *_get_string[_tmp]() replacements we make here. There's also the\nsmall benefit of saving one function implementation.\n\n> Another name for this function could have been\n> \"*_config_key_exists()\", as suggested in [1]. That would work for all\n> of these callers, and would currently be equivalent to this function,\n> as the git_configset_get_value() API normalizes all non-zero return\n> values to a \"1\".\n>\n> But adding that API would set us up to lose information, as e.g. if\n> git_config_parse_key() in the underlying configset_find_element()\n> fails we'd like to return -1, not 1.\n\nWe were already 'losing' (or rather, not caring about) this information\nwith the *_get_<type>() functions. The only reason we'd care about this\nis if we using git_configset_get_value_multi() or similar.\n\nWe replace two callers of git_configset_get_value_multi() in this patch,\nbut they didn't care about the -1 case anyway...\n\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n>  \t * If there are no path args and submodule.active is set then,\n>  \t * by default, only initialize 'active' modules.\n>  \t */\n> -\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n> +\tif (!argc && !git_config_get(\"submodule.active\"))\n>  \t\tmodule_list_active(&list);\n>  \n>  \tinfo.prefix = prefix;\n> @@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n>  \t\t * If there are no path args and submodule.active is set then,\n>  \t\t * by default, only initialize 'active' modules.\n>  \t\t */\n> -\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n> +\t\tif (!argc && !git_config_get(\"submodule.active\"))\n>  \t\t\tmodule_list_active(&list);\n>  \n>  \t\tinfo.prefix = opt.prefix;\n\nHere they are.\n\n> diff --git a/config.h b/config.h\n> index ef9eade6414..04c5e594015 100644\n> --- a/config.h\n> +++ b/config.h\n> @@ -471,9 +471,12 @@ void git_configset_clear(struct config_set *cs);\n>  \n>  /*\n>   * These functions return 1 if not found, and 0 if found, leaving the found\n> - * value in the 'dest' pointer.\n> + * value in the 'dest' pointer (if any).\n>   */\n>  \n> +RESULT_MUST_BE_USED\n> +int git_configset_get(struct config_set *cs, const char *key);\n> +\n\nAs Junio pointed out, git_configset_get() can now return -1, so this\nisn't so accurate any more. git_configset_get() is really the exception\nhere, since all the other functions in this section are the\ngit_configset_get_*() functions that use git_configset_get_value(). I'd\nprefer returning only 0 or 1 for consistency.\n\n>  /*\n>   * Finds the highest-priority value for the configuration variable `key`\n>   * and config set `cs`, stores the pointer to it in `value` and returns 0.\n> @@ -494,6 +497,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n>  /* Functions for reading a repository's config */\n>  struct repository;\n>  void repo_config(struct repository *repo, config_fn_t fn, void *data);\n> +\n> +/**\n> + * Run only the discover part of the repo_config_get_*() functions\n> + * below, in addition to 1 if not found, returns negative values on\n> + * error (e.g. if the key itself is invalid).\n> + */\n> +RESULT_MUST_BE_USED\n> +int repo_config_get(struct repository *repo, const char *key);\n\nThis comment is quite a welcome addition. I've found myself losing track\nof this information quite often\n"},{"id":"471554","messageId":"kl6lttzzgebw.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v4-3.9-998b11ae4bc-20230202T131155Z-avarab@gmail.com","subject":"Re: [PATCH v4 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-02-06T12:37:07Z","receivedAt":"2023-02-06T12:37:14Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"I think introducing a new function to replace the *_get_value_multi()\nabuses is a good way forward. Junio has already adequately commented on\nyour implementation, so I'll focus this review on a different approach\nthat this patch could have taken.\n\nÆvar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> We already have the basic \"git_config_get_value()\" function and its\n> \"repo_*\" and \"configset\" siblings to get a given \"key\" and assign the\n> last key found to a provided \"value\".\n>\n> But some callers don't care about that value, but just want to use the\n> return value of the \"get_value()\" function to check whether the key\n> exist (or another non-zero return value).\n\n[...]\n\n> We could have changed git_configset_get_value_multi() to accept a\n> \"NULL\" as a \"dest\" for all callers, but let's avoid changing the\n> behavior of existing API users. Having an \"unused\" value that we throw\n> away internal to config.c is cheap.\n\nThere is yet another option, which is to teach \"git_config_get_value()\"\n(mentioned earlier) to accept NULL to mean \"I just want to know if there\nis a value, I don't care what it is\". That's what the *_get_<type>()\nfunctions use under the hood (i.e. the ones that return either 0 or 1 or\nexit).\n\nThis amounts to implementing the \"*_config_key_exists()\" API you\nmentioned, but I think this is better fit for the current set of\nsemantics. At the very least, that would be an easy 1-1 replacement for\nthe *_get_string[_tmp]() replacements we make here. There's also the\nsmall benefit of saving one function implementation.\n\n> Another name for this function could have been\n> \"*_config_key_exists()\", as suggested in [1]. That would work for all\n> of these callers, and would currently be equivalent to this function,\n> as the git_configset_get_value() API normalizes all non-zero return\n> values to a \"1\".\n>\n> But adding that API would set us up to lose information, as e.g. if\n> git_config_parse_key() in the underlying configset_find_element()\n> fails we'd like to return -1, not 1.\n\nWe were already 'losing' (or rather, not caring about) this information\nwith the *_get_<type>() functions. The only reason we'd care about this\nis if we using git_configset_get_value_multi() or similar.\n\nWe replace two callers of git_configset_get_value_multi() in this patch,\nbut they didn't care about the -1 case anyway...\n\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n>  \t * If there are no path args and submodule.active is set then,\n>  \t * by default, only initialize 'active' modules.\n>  \t */\n> -\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n> +\tif (!argc && !git_config_get(\"submodule.active\"))\n>  \t\tmodule_list_active(&list);\n>  \n>  \tinfo.prefix = prefix;\n> @@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n>  \t\t * If there are no path args and submodule.active is set then,\n>  \t\t * by default, only initialize 'active' modules.\n>  \t\t */\n> -\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n> +\t\tif (!argc && !git_config_get(\"submodule.active\"))\n>  \t\t\tmodule_list_active(&list);\n>  \n>  \t\tinfo.prefix = opt.prefix;\n\nHere they are.\n\n> diff --git a/config.h b/config.h\n> index ef9eade6414..04c5e594015 100644\n> --- a/config.h\n> +++ b/config.h\n> @@ -471,9 +471,12 @@ void git_configset_clear(struct config_set *cs);\n>  \n>  /*\n>   * These functions return 1 if not found, and 0 if found, leaving the found\n> - * value in the 'dest' pointer.\n> + * value in the 'dest' pointer (if any).\n>   */\n>  \n> +RESULT_MUST_BE_USED\n> +int git_configset_get(struct config_set *cs, const char *key);\n> +\n\nAs Junio pointed out, git_configset_get() can now return -1, so this\nisn't so accurate any more. git_configset_get() is really the exception\nhere, since all the other functions in this section are the\ngit_configset_get_*() functions that use git_configset_get_value(). I'd\nprefer returning only 0 or 1 for consistency.\n\n>  /*\n>   * Finds the highest-priority value for the configuration variable `key`\n>   * and config set `cs`, stores the pointer to it in `value` and returns 0.\n> @@ -494,6 +497,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n>  /* Functions for reading a repository's config */\n>  struct repository;\n>  void repo_config(struct repository *repo, config_fn_t fn, void *data);\n> +\n> +/**\n> + * Run only the discover part of the repo_config_get_*() functions\n> + * below, in addition to 1 if not found, returns negative values on\n> + * error (e.g. if the key itself is invalid).\n> + */\n> +RESULT_MUST_BE_USED\n> +int repo_config_get(struct repository *repo, const char *key);\n\nThis comment is quite a welcome addition. I've found myself losing track\nof this information quite often\n"},{"id":"471555","messageId":"kl6lpmangdf4.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v4-6.9-17c1218e74c-20230202T131155Z-avarab@gmail.com","subject":"Re: [PATCH v4 6/9] for-each-repo: error on bad --config","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-02-06T12:56:47Z","receivedAt":"2023-02-06T12:56:52Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> Before this, all these added tests would pass with an exit code of 0.\n>\n> We could preserve the comment added in 6c62f015520, but now that we're\n> directly using the documented repo_config_get_value_multi() value it's\n> just narrating something that should be obvious from the API use, so\n> let's drop it.\n\n[...]\n\n> diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\n> index fd0e7739e6a..224164addb3 100644\n> --- a/builtin/for-each-repo.c\n> +++ b/builtin/for-each-repo.c\n> @@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n>  \tstatic const char *config_key = NULL;\n>  \tint i, result = 0;\n>  \tconst struct string_list *values;\n> +\tint err;\n>  \n>  \tconst struct option options[] = {\n>  \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n> @@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n>  \tif (!config_key)\n>  \t\tdie(_(\"missing --config=<config>\"));\n>  \n> -\t/*\n> -\t * Do nothing on an empty list, which is equivalent to the case\n> -\t * where the config variable does not exist at all.\n> -\t */\n> -\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n> +\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n> +\tif (err < 0)\n> +\t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n> +\t\t\t       for_each_repo_usage, options, config_key);\n> +\telse if (err)\n>  \t\treturn 0;\n\nCompared to v3, this change was moved from the previous patch to this\none.\n\n> diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\n> index 3648d439a87..6b51e00da0e 100755\n> --- a/t/t0068-for-each-repo.sh\n> +++ b/t/t0068-for-each-repo.sh\n> @@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '\n>  \tgit for-each-repo --config=bogus.config -- help --no-such-option\n>  '\n>  \n> +test_expect_success 'error on bad config keys' '\n> +\ttest_expect_code 129 git for-each-repo --config=a &&\n> +\ttest_expect_code 129 git for-each-repo --config=a.b. &&\n> +\ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n> +'\n> +\n>  test_done\n\nAnd this was moved from patch 1. Both make a lot of sense in this patch,\nI think this version reads a bit better.\n"},{"id":"471556","messageId":"kl6lmt5rgd2r.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v4-8.9-a391ee17617-20230202T131155Z-avarab@gmail.com","subject":"Re: [PATCH v4 8/9] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-02-06T13:04:12Z","receivedAt":"2023-02-06T13:04:18Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> There are now three remaining files using the low-level API:\n>\n> - Two cases in \"builtin/submodule--helper.c\", where it's used safely\n>   to see if any config exists.\n>\n>   We could refactor these away from \"multi\" to some \"does it exist?\"\n>   function, as [4] did, but as that's orthogonal to the \"string\"\n>   safety we're introducing here let's leave them for now.\n\nMaybe I'm mistaken, but weren't these removed in 3/9? I couldn't find\nthese calls any more.\n"},{"id":"471561","messageId":"kl6lk00uhiep.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"230206.86edr36ki2.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v4 2/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-02-06T16:23:42Z","receivedAt":"2023-02-06T16:23:53Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> On Mon, Feb 06 2023, Glen Choo wrote:\n>\n>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>>\n>>> +test_NULL_in_multi () {\n>>> +\tlocal op=\"$1\" &&\n>>> +\tlocal file=\"$2\" &&\n>>> +\n>>> +\ttest_expect_success \"$op: NULL value in config${file:+ in $file}\" '\n>>> +\t\tconfig=\"$file\" &&\n>>> +\t\tif test -z \"$config\"\n>>> +\t\tthen\n>>> +\t\t\tconfig=.git/config &&\n>>> +\t\t\ttest_when_finished \"mv $config.old $config\" &&\n>>> +\t\t\tmv \"$config\" \"$config\".old\n>>> +\t\tfi &&\n>>> +\n>>> +\t\tcat >\"$config\" <<-\\EOF &&\n>>> +\t\t[a]key=x\n>>> +\t\t[a]key\n>>> +\t\t[a]key=y\n>>> +\t\tEOF\n>>> +\t\tcase \"$op\" in\n>>> +\t\t*_multi)\n>>> +\t\t\tcat >expect <<-\\EOF\n>>> +\t\t\tx\n>>> +\t\t\t(NULL)\n>>> +\t\t\ty\n>>> +\t\t\tEOF\n>>> +\t\t\t;;\n>>> +\t\t*)\n>>> +\t\t\tcat >expect <<-\\EOF\n>>> +\t\t\ty\n>>> +\t\t\tEOF\n>>> +\t\t\t;;\n>>> +\t\tesac &&\n>>> +\t\ttest-tool config \"$op\" a.key $file >actual &&\n>>> +\t\ttest_cmp expect actual\n>>> +\t'\n>>> +}\n>>> +\n>>> +test_NULL_in_multi \"get_value_multi\"\n>>> +test_NULL_in_multi \"configset_get_value\" \"my.config\"\n>>> +test_NULL_in_multi \"configset_get_value_multi\" \"my.config\"\n>>\n>> I frankly preferred v3's tests over this version. v3 is slightly\n>> verbose, but at least the lack of logic made it easy to read and\n>> understand. I'd be okay with it if we get a big DRY-ness benefit, but 2\n>> conditionals for 3 cases seems quite un-DRY to me.\n>\n> Note that the v3 version didn't test the get_value_multi(), adjusting\n> the v3 version with copy/paste to test that as well is why I made this a\n> function. From the CL's range-diff:\n>\n>     -    When the \"t/t1308-config-set.sh\" tests were added in [1] only one of\n>     -    the three \"(NULL)\" lines in \"t/helper/test-config.c\" had any test\n>     -    coverage. This change adds tests that stress the remaining two.\n>     +    When parts of the config_set API were tested for in [1] they didn't\n>     +    add coverage for 3/4 of the \"(NULL)\" cases handled in\n>     +    \"t/helper/test-config.c\". We'd test that case for \"get_value\", but not\n>     +    \"get_value_multi\", \"configset_get_value\" and\n>     +    \"configset_get_value_multi\".\n>     +\n>     +    We now cover all of those cases, which in turn expose the details of\n>     +    how this part of the config API works.\n>\n> Of course that wouldn't address an outstanding point that we should just\n> copy/paste these anyway, but maybe that addresses your feedback...\n\nAh, yes I noticed that (thanks for confirming), and I meant that I think\nit would be better to just copy/paste anyway.\n"},{"id":"471675","messageId":"230207.865ycd4upk.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"xmqqv8kjpqoe.fsf@gitster.g","subject":"Re: [PATCH v4 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T10:29:15Z","receivedAt":"2023-02-07T10:49:09Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Feb 02 2023, Junio C Hamano wrote:\n\n> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n>\n>> We already have the basic \"git_config_get_value()\" function and its\n>> \"repo_*\" and \"configset\" siblings to get a given \"key\" and assign the\n>> last key found to a provided \"value\".\n>>\n>> But some callers don't care about that value, but just want to use the\n>> return value of the \"get_value()\" function to check whether the key\n>> exist (or another non-zero return value).\n>>\n>> The immediate motivation for this is that a subsequent commit will\n>> need to change all callers of the \"*_get_value_multi()\" family of\n>> functions. In two cases here we (ab)used it to check whether we had\n>> any values for the given key, but didn't care about the return value.\n>\n> So, the idea is that \n>\n> \tif (!git_config_get_string(key, &discard))\n> \t\tfree(discard);\n> \telse\n> \t\t... the key is missing ...\n>\n> becomes\n>\n> \tif (git_config_get(key))\n> \t\t... the key is missing ...\n>\n> In other words, git_config_get() returns 0 only when the key is\n> used, and non-zero return signals that the key is not used?\n>\n> Similarly, get_value_multi() was an interface to get to the\n> value_list associated to the given key, and was abused like\n>\n> \tif (git_config_get_value_multi(key))\n> \t\t... the key exists ...\n>\n> which will become\n>\n> \tif (!git_config_get(key))\n> \t\t... the key exists ...\n>\n> right?\n\nYes, I've amended this in a re-roll to add tests to the configset tests,\nso how the new API should be used becomes obvious.\n\n>> @@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)\n>>  \t * is_submodule_active(), since that function needs to find\n>>  \t * out the value of \"submodule.active\" again anyway.\n>>  \t */\n>> -\tif (!git_config_get_string_tmp(\"submodule.active\", &val)) {\n>> +\tif (!git_config_get(\"submodule.active\")) {\n>\n> string_tmp() variant is to retrieve borrowed value, and it returns 0\n> when there is a value.  If it is a valueless true, we get -1 back\n> with an error message.  What does the updated version do in the\n> valueless true case?\n\nNo, we'll get back 0, as a value-less key exists. \n\nThis sort of code would be correct in general, as if we really want to\ncheck if the key exists a value-less is a valid boolean true.\n\nIn this case we happen to segfault later on if we encounter such a key,\nbut that's unrelated to this being correct.\n\nThe segfault is then fixed later in this topic.\n\n>> @@ -2425,8 +2433,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *\n>>  \n>>  const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n>>  {\n>> -\tstruct config_set_element *e = configset_find_element(cs, key);\n>> -\treturn e ? &e->value_list : NULL;\n>> +\tstruct config_set_element *e;\n>> +\n>> +\tif (configset_find_element(cs, key, &e))\n>> +\t\treturn NULL;\n>> +\telse if (!e)\n>> +\t\treturn NULL;\n>> +\treturn &e->value_list;\n>> +}\n>\n> OK.  !e means \"we got a healthy key and peeking into the hash table,\n> there wasn't any entry for the key\", and that is reported with NULL.\n> Do we evern return a string list with .nr == 0, I wonder.  Having to\n> deal with such a list would make the caller's job more complex, but\n> perhaps we are not allowing the code to shrink value_list.nr to\n> avoid such a situation?\n\nNo, we never return a list with .nr == 0. That's why Stolee's earlier\nRFC tried to solve a subset of the problem this topic addresse by\nreturning such a list as a way to indicate \"does not exist\".\n\nThat would work to an extent, but would leave the main problem (which\nStolee wasn't aware of at the time) of having a \".nr > 0\" list with\n\"NULL\" elements in it.\n\nIt also wouldn't be idiomatic with the rest of the API, as this topic\nshows, and means you can't distinguish non-existence from other errors.\n\n>> +int git_configset_get(struct config_set *cs, const char *key)\n>> +{\n>> +\tstruct config_set_element *e;\n>> +\tint ret;\n>> +\n>> +\tif ((ret = configset_find_element(cs, key, &e)))\n>> +\t\treturn ret;\n>> +\telse if (!e)\n>> +\t\treturn 1;\n>> +\treturn 0;\n>>  }\n>\n> OK.  So 0 return from the function means there is a value (or more)\n> for a given key.  Good.\n>\n>> diff --git a/config.h b/config.h\n>> index ef9eade6414..04c5e594015 100644\n>> --- a/config.h\n>> +++ b/config.h\n>> @@ -471,9 +471,12 @@ void git_configset_clear(struct config_set *cs);\n>>  \n>>  /*\n>>   * These functions return 1 if not found, and 0 if found, leaving the found\n>> - * value in the 'dest' pointer.\n>> + * value in the 'dest' pointer (if any).\n>>   */\n>\n> Now the returned non-zero values are no longer 1 alone, no?\n> Whatever lower-level functions use to signal an error is propagated\n> up with the\n>\n> \tif ((ret = func())\n> \t\treturn ret;\n\nThat's still mostly true, but I should have added the new\ngit_configset_get() below this comment, and split it up, i.e. it's still\ntrue for the functions that populate \"dest\".\n\nI have a topic-on-top to fix those (to propagate them up), and it was\nhard to know where to draw the line, in this case I got it wrong. Will\nfix it.\n"},{"id":"471679","messageId":"230207.861qn14rjs.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"kl6lttzzgebw.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v4 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T11:52:07Z","receivedAt":"2023-02-07T11:57:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Feb 06 2023, Glen Choo wrote:\n\n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>\n>> We already have the basic \"git_config_get_value()\" function and its\n>> \"repo_*\" and \"configset\" siblings to get a given \"key\" and assign the\n>> last key found to a provided \"value\".\n>>\n>> But some callers don't care about that value, but just want to use the\n>> return value of the \"get_value()\" function to check whether the key\n>> exist (or another non-zero return value).\n>\n> [...]\n>\n>> We could have changed git_configset_get_value_multi() to accept a\n>> \"NULL\" as a \"dest\" for all callers, but let's avoid changing the\n>> behavior of existing API users. Having an \"unused\" value that we throw\n>> away internal to config.c is cheap.\n>\n> There is yet another option, which is to teach \"git_config_get_value()\"\n> (mentioned earlier) to accept NULL to mean \"I just want to know if there\n> is a value, I don't care what it is\". That's what the *_get_<type>()\n> functions use under the hood (i.e. the ones that return either 0 or 1 or\n> exit).\n\nI've clarified the commit message, but that's the same as what this is\ndescribing. I.e. I meant \"git_config_get_value_multi() and the functions\nthat are wrapping it\", which is \"git_config_get_value()\" etc.\n\n> This amounts to implementing the \"*_config_key_exists()\" API you\n> mentioned, but I think this is better fit for the current set of\n> semantics. At the very least, that would be an easy 1-1 replacement for\n> the *_get_string[_tmp]() replacements we make here. There's also the\n> small benefit of saving one function implementation.\n\nI think this is the wrong approach, and have updated the commit message\nfurther to advocate for this one.\n\n>> Another name for this function could have been\n>> \"*_config_key_exists()\", as suggested in [1]. That would work for all\n>> of these callers, and would currently be equivalent to this function,\n>> as the git_configset_get_value() API normalizes all non-zero return\n>> values to a \"1\".\n>>\n>> But adding that API would set us up to lose information, as e.g. if\n>> git_config_parse_key() in the underlying configset_find_element()\n>> fails we'd like to return -1, not 1.\n>\n> We were already 'losing' (or rather, not caring about) this information\n> with the *_get_<type>() functions. The only reason we'd care about this\n> is if we using git_configset_get_value_multi() or similar.\n>\n> We replace two callers of git_configset_get_value_multi() in this patch,\n> but they didn't care about the -1 case anyway...\n> [...]\n> As Junio pointed out, git_configset_get() can now return -1, so this\n> isn't so accurate any more. git_configset_get() is really the exception\n> here, since all the other functions in this section are the\n> git_configset_get_*() functions that use git_configset_get_value(). I'd\n> prefer returning only 0 or 1 for consistency.\n\nI really prefer not clobbering these return values, but I take your\npoint that the end-state here is inconsistent.\n\nI figured that I could fix it for the APIs added here, and follow-up\n(with a patch I already had mostly ready) after this series to fix the\nremaining config API warts.\n\nI won't fix all of those in the incoming re-roll of this, but I'll fix\nthis issue, i.e. we'll consistently ferry up \"ret\", and stop normalizing\nnon-zero-non-1 to \"return 1\".\n"},{"id":"471685","messageId":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com","subject":"[PATCH v5 00/10] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:40Z","receivedAt":"2023-02-07T16:11:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This series fixes numerous segfaults in config API users, because they\ndidn't expect *_get_multi() to hand them a string_list with a NULL in\nit given config like \"[a] key\" (note, no \"=\"'s).\n\nA larger general overview at v1[1], but note the API changes in\nv2[2]. Changes since v4[3]:\n\n* Added tests for value-less at the end of a list to 2/10, per Junio's\n  request.\n\n* Clarifications in the 3/10 commit message, per v4's discussion.\n\n* Add RESULT_MUST_BE_USED to configset_find_element(), note that\n  configset_add_value() doesn't have it, see the 3/10 commit message\n  update.\n\n* 3/10 now has tests for the \"get\" family of functions, this should\n  clarify any questions about the semantics of the API.\n\n* Junio suggested for 4/10 to skip renaming the\n  \"deprecated_prereleases\" variable, I tried that, and if we keep it\n  we need to wrap a line later in the series, which makes the\n  versioncmp.c code harder to read. So I kept the renaming as it's\n  refactored in 4/10.\n\n* Glen suggested that the new *_get() family should \"return 1\" on\n  non-zero like the rest of *_get_*() (i.e. coerce 'ret < 0' to\n  'return 1').\n\n  That could be done here, but for the later \"for-each-repo.c\" we need\n  to distinguish \"bad key\" v.s. \"does this exist?\", and just having\n  that API return a more meaningful value would make it inconsistent\n  with the rest.\n\n  As the much of the point of this series is to make that API less of\n  a special snowflake a new 6/10 instead finishes up the work of\n  having most of the rest of the API return the un-coerced \"ret\" from\n  the depths of the config API.\n\n  That patch is quite large by line count, but pretty trivial in\n  complexity. All of those functions are copy/pasted versions of one\n  another with very minor variations.\n\n* Updated the 8/10 commit message, which was stale from a previous\n  version of this topic.\n\nBranch & CI for this at:\nhttps://github.com/avar/git/tree/avar/have-git_configset_get_value-use-dest-and-int-pattern-5\n\n1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/\n2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/\n3. https://lore.kernel.org/git/cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com/\n\nÆvar Arnfjörð Bjarmason (10):\n  config tests: cover blind spots in git_die_config() tests\n  config tests: add \"NULL\" tests for *_get_value_multi()\n  config API: add and use a \"git_config_get()\" family of functions\n  versioncmp.c: refactor config reading next commit\n  config API: have *_multi() return an \"int\" and take a \"dest\"\n  config API: don't lose the git_*get*() return values\n  for-each-repo: error on bad --config\n  config API users: test for *_get_value_multi() segfaults\n  config API: add \"string\" version of *_value_multi(), fix segfaults\n  for-each-repo: with bad config, don't conflate <path> and <cmd>\n\n builtin/for-each-repo.c              |  14 +-\n builtin/gc.c                         |  15 +-\n builtin/log.c                        |   6 +-\n builtin/submodule--helper.c          |   7 +-\n builtin/worktree.c                   |   3 +-\n config.c                             | 226 ++++++++++++++++++---------\n config.h                             |  84 ++++++++--\n pack-bitmap.c                        |   6 +-\n submodule.c                          |   3 +-\n t/helper/test-config.c               |  28 +++-\n t/t0068-for-each-repo.sh             |  19 +++\n t/t1308-config-set.sh                | 108 ++++++++++++-\n t/t3309-notes-merge-auto-resolve.sh  |   7 +-\n t/t4202-log.sh                       |  15 ++\n t/t5304-prune.sh                     |  12 +-\n t/t5310-pack-bitmaps.sh              |  20 +++\n t/t5552-skipping-fetch-negotiator.sh |  16 ++\n t/t7004-tag.sh                       |  17 ++\n t/t7413-submodule-is-active.sh       |  16 ++\n t/t7900-maintenance.sh               |  38 +++++\n versioncmp.c                         |  22 ++-\n 21 files changed, 549 insertions(+), 133 deletions(-)\n\nRange-diff against v4:\n 1:  4ae56cab7c7 =  1:  cefc4188984 config tests: cover blind spots in git_die_config() tests\n 2:  1f0f8bdcde9 !  2:  91a44456327 config tests: add \"NULL\" tests for *_get_value_multi()\n    @@ t/t1308-config-set.sh: test_expect_success 'find multiple values' '\n     +\t\t\tmv \"$config\" \"$config\".old\n     +\t\tfi &&\n     +\n    ++\t\t# Value-less in the middle of a list\n     +\t\tcat >\"$config\" <<-\\EOF &&\n     +\t\t[a]key=x\n     +\t\t[a]key\n    @@ t/t1308-config-set.sh: test_expect_success 'find multiple values' '\n     +\t\t\t;;\n     +\t\tesac &&\n     +\t\ttest-tool config \"$op\" a.key $file >actual &&\n    ++\t\ttest_cmp expect actual &&\n    ++\n    ++\t\t# Value-less at the end of a least\n    ++\t\tcat >\"$config\" <<-\\EOF &&\n    ++\t\t[a]key=x\n    ++\t\t[a]key=y\n    ++\t\t[a]key\n    ++\t\tEOF\n    ++\t\tcase \"$op\" in\n    ++\t\t*_multi)\n    ++\t\t\tcat >expect <<-\\EOF\n    ++\t\t\tx\n    ++\t\t\ty\n    ++\t\t\t(NULL)\n    ++\t\t\tEOF\n    ++\t\t\t;;\n    ++\t\t*)\n    ++\t\t\tcat >expect <<-\\EOF\n    ++\t\t\t(NULL)\n    ++\t\t\tEOF\n    ++\t\t\t;;\n    ++\t\tesac &&\n    ++\t\ttest-tool config \"$op\" a.key $file >actual &&\n     +\t\ttest_cmp expect actual\n     +\t'\n     +}\n 3:  998b11ae4bc !  3:  4a73151abde config API: add and use a \"git_config_get()\" family of functions\n    @@ Commit message\n         can now use a helper function that doesn't require a throwaway\n         variable.\n     \n    -    We could have changed git_configset_get_value_multi() to accept a\n    -    \"NULL\" as a \"dest\" for all callers, but let's avoid changing the\n    -    behavior of existing API users. Having an \"unused\" value that we throw\n    -    away internal to config.c is cheap.\n    +    We could have changed git_configset_get_value_multi() (and then\n    +    git_config_get_value() etc.) to accept a \"NULL\" as a \"dest\" for all\n    +    callers, but let's avoid changing the behavior of existing API\n    +    users. Having an \"unused\" value that we throw away internal to\n    +    config.c is cheap.\n    +\n    +    A \"NULL as optional dest\" pattern is also more fragile, as the intent\n    +    of the caller might be misinterpreted if he were to accidentally pass\n    +    \"NULL\", e.g. when \"dest\" is passed in from another function.\n     \n         Another name for this function could have been\n         \"*_config_key_exists()\", as suggested in [1]. That would work for all\n    @@ Commit message\n         commit where the git_configset_get_value_multi() function itself will\n         expose this new return value.\n     \n    -    1. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/\n    +    This still leaves various inconsistencies and clobbering or ignoring\n    +    of the return value in place. E.g here we're modifying\n    +    configset_add_value(), but ever since it was added in [2] we've been\n    +    ignoring its \"int\" return value, but as we're changing the\n    +    configset_find_element() it uses, let's have it faithfully ferry that\n    +    \"ret\" along.\n    +\n    +    Let's also use the \"RESULT_MUST_BE_USED\" macro introduced in [3] to\n    +    assert that we're checking the return value of\n    +    configset_find_element().\n    +\n    +    We're leaving the same change to configset_add_value() for some future\n    +    series. Once we start paying attention to its return value we'd need\n    +    to ferry it up as deep as do_config_from(), and would need to make\n    +    least read_{,very_}early_config() and git_protected_config() return an\n    +    \"int\" instead of \"void\". Let's leave that for now, and focus on\n    +    the *_get_*() functions.\n    +\n    +    In a subsequent commit we'll fix the other *_get_*() functions to so\n    +    that they'll ferry our underlying \"ret\" along, rather than normalizing\n    +    it to a \"return 1\". But as an intermediate step to that we'll need to\n    +    fix git_configset_get_value_multi() to return \"int\", and that change\n    +    itself is smaller because of this change to migrate some callers away\n    +    from the *_value_multi() API.\n    +\n    +    1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)\n    +    2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/\n    +    3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n    +       return values, 2022-09-01),\n     \n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n    @@ config.c: void read_very_early_config(config_fn_t cb, void *data)\n      }\n      \n     -static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n    ++RESULT_MUST_BE_USED\n     +static int configset_find_element(struct config_set *cs, const char *key,\n     +\t\t\t\t  struct config_set_element **dest)\n      {\n    @@ config.c: void git_config_clear(void)\n     \n      ## config.h ##\n     @@ config.h: void git_configset_clear(struct config_set *cs);\n    - \n    - /*\n    -  * These functions return 1 if not found, and 0 if found, leaving the found\n    -- * value in the 'dest' pointer.\n    -+ * value in the 'dest' pointer (if any).\n    +  * value in the 'dest' pointer.\n       */\n      \n     +RESULT_MUST_BE_USED\n    @@ config.h: void git_protected_config(config_fn_t fn, void *data);\n     + * documentation.\n       */\n      \n    ++RESULT_MUST_BE_USED\n     +int git_config_get(const char *key);\n     +\n      /**\n       * Finds the highest-priority value for the configuration variable `key`,\n       * stores the pointer to it in `value` and returns 0. When the\n    +\n    + ## t/helper/test-config.c ##\n    +@@\n    +  * get_value_multi -> prints all values for the entered key in increasing order\n    +  *\t\t     of priority\n    +  *\n    ++ * get -> print return value for the entered key\n    ++ *\n    +  * get_int -> print integer value for the entered key or die\n    +  *\n    +  * get_bool -> print bool value for the entered key or die\n    +@@ t/helper/test-config.c: int cmd__config(int argc, const char **argv)\n    + \t\t\tprintf(\"Value not found for \\\"%s\\\"\\n\", argv[2]);\n    + \t\t\tgoto exit1;\n    + \t\t}\n    ++\t} else if (argc == 3 && !strcmp(argv[1], \"get\")) {\n    ++\t\tint ret;\n    ++\n    ++\t\tif (!(ret = git_config_get(argv[2])))\n    ++\t\t\tgoto exit0;\n    ++\t\telse if (ret == 1)\n    ++\t\t\tprintf(\"Value not found for \\\"%s\\\"\\n\", argv[2]);\n    ++\t\telse if (ret == -CONFIG_INVALID_KEY)\n    ++\t\t\tprintf(\"Key \\\"%s\\\" is invalid\\n\", argv[2]);\n    ++\t\telse if (ret == -CONFIG_NO_SECTION_OR_NAME)\n    ++\t\t\tprintf(\"Key \\\"%s\\\" has no section\\n\", argv[2]);\n    ++\t\telse\n    ++\t\t\t/*\n    ++\t\t\t * A normal caller should just check \"ret <\n    ++\t\t\t * 0\", but for our own tests let's BUG() if\n    ++\t\t\t * our whitelist of git_config_parse_key()\n    ++\t\t\t * return values isn't exhaustive.\n    ++\t\t\t */\n    ++\t\t\tBUG(\"Key \\\"%s\\\" has unknown return %d\", argv[2], ret);\n    ++\t\tgoto exit1;\n    + \t} else if (argc == 3 && !strcmp(argv[1], \"get_int\")) {\n    + \t\tif (!git_config_get_int(argv[2], &val)) {\n    + \t\t\tprintf(\"%d\\n\", val);\n    +\n    + ## t/t1308-config-set.sh ##\n    +@@ t/t1308-config-set.sh: test_expect_success 'setup default config' '\n    + \t\tskin = false\n    + \t\tnose = 1\n    + \t\thorns\n    ++\t[value]\n    ++\t\tless\n    + \tEOF\n    + '\n    + \n    +@@ t/t1308-config-set.sh: test_expect_success 'find value with the highest priority' '\n    + \tcheck_config get_value case.baz \"hask\"\n    + '\n    + \n    ++test_expect_success 'return value for an existing key' '\n    ++\ttest-tool config get lamb.chop >out 2>err &&\n    ++\ttest_must_be_empty out &&\n    ++\ttest_must_be_empty err\n    ++'\n    ++\n    ++test_expect_success 'return value for value-less key' '\n    ++\ttest-tool config get value.less >out 2>err &&\n    ++\ttest_must_be_empty out &&\n    ++\ttest_must_be_empty err\n    ++'\n    ++\n    ++test_expect_success 'return value for a missing key' '\n    ++\tcat >expect <<-\\EOF &&\n    ++\tValue not found for \"missing.key\"\n    ++\tEOF\n    ++\ttest_expect_code 1 test-tool config get missing.key >actual 2>err &&\n    ++\ttest_cmp actual expect &&\n    ++\ttest_must_be_empty err\n    ++'\n    ++\n    ++test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '\n    ++\tcat >expect <<-\\EOF &&\n    ++\tKey \"fails.iskeychar.-\" is invalid\n    ++\tEOF\n    ++\ttest_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&\n    ++\ttest_cmp actual expect &&\n    ++\ttest_must_be_empty out\n    ++'\n    ++\n    ++test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '\n    ++\tcat >expect <<-\\EOF &&\n    ++\tKey \"keynosection\" has no section\n    ++\tEOF\n    ++\ttest_expect_code 1 test-tool config get keynosection >actual 2>err &&\n    ++\ttest_cmp actual expect &&\n    ++\ttest_must_be_empty out\n    ++'\n    ++\n    + test_expect_success 'find integer value for a key' '\n    + \tcheck_config get_int lamb.chop 65\n    + '\n    +@@ t/t1308-config-set.sh: test_expect_success 'proper error on error in default config files' '\n    + \tcp .git/config .git/config.old &&\n    + \ttest_when_finished \"mv .git/config.old .git/config\" &&\n    + \techo \"[\" >>.git/config &&\n    +-\techo \"fatal: bad config line 34 in file .git/config\" >expect &&\n    ++\techo \"fatal: bad config line 36 in file .git/config\" >expect &&\n    + \ttest_expect_code 128 test-tool config get_value foo.bar 2>actual &&\n    + \ttest_cmp expect actual\n    + '\n 4:  aae1d5c12a9 !  4:  382a77ca69e versioncmp.c: refactor config reading next commit\n    @@ Commit message\n         the bounds of our CodingGuidelines when it comes to line length, and\n         to avoid repeating ourselves.\n     \n    +    Renaming \"deprecated_prereleases\" to \"oldl\" doesn't help us to avoid\n    +    line wrapping now, but it will in a subsequent commit.\n    +\n         Let's also split out the names of the config variables into variables\n    -    of our own, so we don't have to repeat ourselves, and refactor the\n    -    nested if/else to avoid indenting it, and the existing bracing style\n    -    issue.\n    +    of our own, and refactor the nested if/else to avoid indenting it, and\n    +    the existing bracing style issue.\n     \n         This all helps with the subsequent commit, where we'll need to start\n         checking different git_config_get_value_multi() return value. See\n 5:  23449ff2c4e !  5:  8f17bf8150c config API: have *_multi() return an \"int\" and take a \"dest\"\n    @@ Commit message\n         return an \"int\" and populate a \"**dest\" parameter like every other\n         git_configset_get_*()\" in the API.\n     \n    -    As we'll see in in subsequent commits this fixes a blind spot in the\n    -    API where it wasn't possible to tell whether a list was empty from\n    -    whether a config key existed. We'll take advantage of that in\n    -    subsequent commits, but for now we're faithfully converting existing\n    -    API callers.\n    -\n    -    A logical follow-up to this would be to change the various \"*_get_*()\"\n    -    functions to ferry the git_configset_get_value() return value to their\n    -    own callers, e.g. git_configset_get_int() returns \"1\" rather than\n    -    ferrying up the \"-1\" that \"git_configset_get_value()\" might return,\n    -    but that's not being done in this series\n    +    As we'll take advantage of in subsequent commits, this fixes a blind\n    +    spot in the API where it wasn't possible to tell whether a list was\n    +    empty from whether a config key existed. For now we don't make use of\n    +    those new return values, but faithfully convert existing API users.\n     \n         Most of this is straightforward, commentary on cases that stand out:\n     \n -:  ----------- >  6:  b515ff13f9b config API: don't lose the git_*get*() return values\n 6:  17c1218e74c =  7:  8a83c30ea78 for-each-repo: error on bad --config\n 7:  7fc91eaf747 =  8:  d9abc78c2be config API users: test for *_get_value_multi() segfaults\n 8:  a391ee17617 !  9:  65fa91e7ce7 config API: add \"string\" version of *_value_multi(), fix segfaults\n    @@ Commit message\n           - 92156291ca8 (log: add default decoration filter, 2022-08-05)\n           - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)\n     \n    -    There are now three remaining files using the low-level API:\n    -\n    -    - Two cases in \"builtin/submodule--helper.c\", where it's used safely\n    -      to see if any config exists.\n    -\n    -      We could refactor these away from \"multi\" to some \"does it exist?\"\n    -      function, as [4] did, but as that's orthogonal to the \"string\"\n    -      safety we're introducing here let's leave them for now.\n    +    There are now two users ofthe low-level API:\n     \n         - One in \"builtin/for-each-repo.c\", which we'll convert in a\n           subsequent commit.\n     \n    -    - The \"t/helper/test-config.c\" code added in [4].\n    +    - The \"t/helper/test-config.c\" code added in [3].\n     \n         As seen in the preceding commit we need to give the\n         \"t/helper/test-config.c\" caller these \"NULL\" entries.\n    @@ Commit message\n            2008-02-11)\n         2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,\n            2008-02-11).\n    -    3. https://lore.kernel.org/git/patch-07.10-c01f7d85c94-20221026T151328Z-avarab@gmail.com/\n    -    4. 4c715ebb96a (test-config: add tests for the config_set API,\n    +    3. 4c715ebb96a (test-config: add tests for the config_set API,\n            2014-07-28)\n     \n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n 9:  c7a5f5b4133 = 10:  4db3c6d0ed9 for-each-repo: with bad config, don't conflate <path> and <cmd>\n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471686","messageId":"patch-v5-01.10-cefc4188984-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v5 01/10] config tests: cover blind spots in git_die_config() tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:41Z","receivedAt":"2023-02-07T16:11:02Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"There were no tests checking for the output of the git_die_config()\nfunction in the config API, added in 5a80e97c827 (config: add\n`git_die_config()` to the config-set API, 2014-08-07). We only tested\n\"test_must_fail\", but didn't assert the output.\n\nWe need tests for this because a subsequent commit will alter the\nreturn value of git_config_get_value_multi(), which is used to get the\nconfig values in the git_die_config() function. This test coverage\nhelps to build confidence in that subsequent change.\n\nThese tests cover different interactions with git_die_config():\n\n- The \"notes.mergeStrategy\" test in\n  \"t/t3309-notes-merge-auto-resolve.sh\" is a case where a function\n  outside of config.c (git_config_get_notes_strategy()) calls\n  git_die_config().\n\n- The \"gc.pruneExpire\" test in \"t5304-prune.sh\" is a case where\n  git_config_get_expiry() calls git_die_config(), covering a different\n  \"type\" than the \"string\" test for \"notes.mergeStrategy\".\n\n- The \"fetch.negotiationAlgorithm\" test in\n  \"t/t5552-skipping-fetch-negotiator.sh\" is a case where\n  git_config_get_string*() calls git_die_config().\n\nWe also cover both the \"from command-line config\" and \"in file..at\nline\" cases here.\n\nThe clobbering of existing \".git/config\" files here is so that we're\nnot implicitly testing the line count of the default config.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-\n t/t5304-prune.sh                     | 12 ++++++++++--\n t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++\n 3 files changed, 32 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh\nindex 141d3e4ca4d..9bd5dbf341f 100755\n--- a/t/t3309-notes-merge-auto-resolve.sh\n+++ b/t/t3309-notes-merge-auto-resolve.sh\n@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '\n \n test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '\n \tgit config core.notesRef refs/notes/y &&\n-\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z &&\n+\tcat >expect <<-\\EOF &&\n+\terror: unknown notes merge strategy foo\n+\tfatal: unable to parse '\\''notes.mergeStrategy'\\'' from command-line config\n+\tEOF\n+\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z 2>actual &&\n+\ttest_cmp expect actual &&\n \t# Verify no changes (y)\n \tverify_notes y y\n '\ndiff --git a/t/t5304-prune.sh b/t/t5304-prune.sh\nindex d65a5f94b4b..5500dd08426 100755\n--- a/t/t5304-prune.sh\n+++ b/t/t5304-prune.sh\n@@ -72,8 +72,16 @@ test_expect_success 'gc: implicit prune --expire' '\n '\n \n test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '\n-\tgit config gc.pruneExpire invalid &&\n-\ttest_must_fail git gc\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t>repo/.git/config &&\n+\tgit -C repo config gc.pruneExpire invalid &&\n+\tcat >expect <<-\\EOF &&\n+\terror: Invalid gc.pruneexpire: '\\''invalid'\\''\n+\tfatal: bad config variable '\\''gc.pruneexpire'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_must_fail git -C repo gc 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'gc: start with ok gc.pruneExpire' '\ndiff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh\nindex 165427d57e5..b55a9f65e6b 100755\n--- a/t/t5552-skipping-fetch-negotiator.sh\n+++ b/t/t5552-skipping-fetch-negotiator.sh\n@@ -3,6 +3,22 @@\n test_description='test skipping fetch negotiator'\n . ./test-lib.sh\n \n+test_expect_success 'fetch.negotiationalgorithm config' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\tcat >repo/.git/config <<-\\EOF &&\n+\t[fetch]\n+\tnegotiationAlgorithm\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''fetch.negotiationalgorithm'\\''\n+\tfatal: bad config variable '\\''fetch.negotiationalgorithm'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_expect_code 128 git -C repo fetch >out 2>actual &&\n+\ttest_must_be_empty out &&\n+\ttest_cmp expect actual\n+'\n+\n have_sent () {\n \twhile test \"$#\" -ne 0\n \tdo\n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471687","messageId":"patch-v5-02.10-91a44456327-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v5 02/10] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:42Z","receivedAt":"2023-02-07T16:11:04Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"A less well known edge case in the config format is that keys can be\nvalue-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\nare equivalent as far as \"--type=bool\" is concerned:\n\n\t[a]key\n\t[a]key = true\n\nBut as far as our parser is concerned the values for these two are\nNULL, and \"true\". I.e. for a sequence like:\n\n\t[a]key=x\n\t[a]key\n\t[a]key=y\n\nWe get a \"struct string_list\" with \"string\" members with \".string\"\nvalues of:\n\n\t{ \"x\", NULL, \"y\" }\n\nThis behavior goes back to the initial implementation of\ngit_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n2005-10-10).\n\nWhen parts of the config_set API were tested for in [1] they didn't\nadd coverage for 3/4 of the \"(NULL)\" cases handled in\n\"t/helper/test-config.c\". We'd test that case for \"get_value\", but not\n\"get_value_multi\", \"configset_get_value\" and\n\"configset_get_value_multi\".\n\nWe now cover all of those cases, which in turn expose the details of\nhow this part of the config API works.\n\n1. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t1308-config-set.sh | 65 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 65 insertions(+)\n\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex b38e158d3b2..4be1ab1147c 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -146,6 +146,71 @@ test_expect_success 'find multiple values' '\n \tcheck_config get_value_multi case.baz sam bat hask\n '\n \n+test_NULL_in_multi () {\n+\tlocal op=\"$1\" &&\n+\tlocal file=\"$2\" &&\n+\n+\ttest_expect_success \"$op: NULL value in config${file:+ in $file}\" '\n+\t\tconfig=\"$file\" &&\n+\t\tif test -z \"$config\"\n+\t\tthen\n+\t\t\tconfig=.git/config &&\n+\t\t\ttest_when_finished \"mv $config.old $config\" &&\n+\t\t\tmv \"$config\" \"$config\".old\n+\t\tfi &&\n+\n+\t\t# Value-less in the middle of a list\n+\t\tcat >\"$config\" <<-\\EOF &&\n+\t\t[a]key=x\n+\t\t[a]key\n+\t\t[a]key=y\n+\t\tEOF\n+\t\tcase \"$op\" in\n+\t\t*_multi)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\tx\n+\t\t\t(NULL)\n+\t\t\ty\n+\t\t\tEOF\n+\t\t\t;;\n+\t\t*)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\ty\n+\t\t\tEOF\n+\t\t\t;;\n+\t\tesac &&\n+\t\ttest-tool config \"$op\" a.key $file >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# Value-less at the end of a least\n+\t\tcat >\"$config\" <<-\\EOF &&\n+\t\t[a]key=x\n+\t\t[a]key=y\n+\t\t[a]key\n+\t\tEOF\n+\t\tcase \"$op\" in\n+\t\t*_multi)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\tx\n+\t\t\ty\n+\t\t\t(NULL)\n+\t\t\tEOF\n+\t\t\t;;\n+\t\t*)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\t(NULL)\n+\t\t\tEOF\n+\t\t\t;;\n+\t\tesac &&\n+\t\ttest-tool config \"$op\" a.key $file >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+}\n+\n+test_NULL_in_multi \"get_value_multi\"\n+test_NULL_in_multi \"configset_get_value\" \"my.config\"\n+test_NULL_in_multi \"configset_get_value_multi\" \"my.config\"\n+\n test_expect_success 'find value from a configset' '\n \tcat >config2 <<-\\EOF &&\n \t[case]\n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471688","messageId":"patch-v5-04.10-382a77ca69e-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v5 04/10] versioncmp.c: refactor config reading next commit","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:44Z","receivedAt":"2023-02-07T16:11:11Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the reading of the versionSort.suffix and\nversionSort.prereleaseSuffix configuration variables to stay within\nthe bounds of our CodingGuidelines when it comes to line length, and\nto avoid repeating ourselves.\n\nRenaming \"deprecated_prereleases\" to \"oldl\" doesn't help us to avoid\nline wrapping now, but it will in a subsequent commit.\n\nLet's also split out the names of the config variables into variables\nof our own, and refactor the nested if/else to avoid indenting it, and\nthe existing bracing style issue.\n\nThis all helps with the subsequent commit, where we'll need to start\nchecking different git_config_get_value_multi() return value. See\nc026557a373 (versioncmp: generalize version sort suffix reordering,\n2016-12-08) for the original implementation of most of this.\n\nMoving the \"initialized = 1\" assignment allows us to move some of this\nto the variable declarations in the subsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n versioncmp.c | 19 +++++++++++--------\n 1 file changed, 11 insertions(+), 8 deletions(-)\n\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 069ee94a4d7..323f5d35ea8 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)\n \t}\n \n \tif (!initialized) {\n-\t\tconst struct string_list *deprecated_prereleases;\n+\t\tconst char *const newk = \"versionsort.suffix\";\n+\t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *oldl;\n+\n+\t\tprereleases = git_config_get_value_multi(newk);\n+\t\toldl = git_config_get_value_multi(oldk);\n+\t\tif (prereleases && oldl)\n+\t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n+\t\telse if (!prereleases)\n+\t\t\tprereleases = oldl;\n+\n \t\tinitialized = 1;\n-\t\tprereleases = git_config_get_value_multi(\"versionsort.suffix\");\n-\t\tdeprecated_prereleases = git_config_get_value_multi(\"versionsort.prereleasesuffix\");\n-\t\tif (prereleases) {\n-\t\t\tif (deprecated_prereleases)\n-\t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n-\t\t} else\n-\t\t\tprereleases = deprecated_prereleases;\n \t}\n \tif (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,\n \t\t\t\t\t    &diff))\n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471689","messageId":"patch-v5-03.10-4a73151abde-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v5 03/10] config API: add and use a \"git_config_get()\" family of functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:43Z","receivedAt":"2023-02-07T16:11:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"We already have the basic \"git_config_get_value()\" function and its\n\"repo_*\" and \"configset\" siblings to get a given \"key\" and assign the\nlast key found to a provided \"value\".\n\nBut some callers don't care about that value, but just want to use the\nreturn value of the \"get_value()\" function to check whether the key\nexist (or another non-zero return value).\n\nThe immediate motivation for this is that a subsequent commit will\nneed to change all callers of the \"*_get_value_multi()\" family of\nfunctions. In two cases here we (ab)used it to check whether we had\nany values for the given key, but didn't care about the return value.\n\nThe rest of the callers here used various other config API functions\nto do the same, all of which resolved to the same underlying functions\nto provide the answer.\n\nSome of these were using either git_config_get_string() or\ngit_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a\nconfigure_added_submodule() leak, 2022-09-01) for a recent example. We\ncan now use a helper function that doesn't require a throwaway\nvariable.\n\nWe could have changed git_configset_get_value_multi() (and then\ngit_config_get_value() etc.) to accept a \"NULL\" as a \"dest\" for all\ncallers, but let's avoid changing the behavior of existing API\nusers. Having an \"unused\" value that we throw away internal to\nconfig.c is cheap.\n\nA \"NULL as optional dest\" pattern is also more fragile, as the intent\nof the caller might be misinterpreted if he were to accidentally pass\n\"NULL\", e.g. when \"dest\" is passed in from another function.\n\nAnother name for this function could have been\n\"*_config_key_exists()\", as suggested in [1]. That would work for all\nof these callers, and would currently be equivalent to this function,\nas the git_configset_get_value() API normalizes all non-zero return\nvalues to a \"1\".\n\nBut adding that API would set us up to lose information, as e.g. if\ngit_config_parse_key() in the underlying configset_find_element()\nfails we'd like to return -1, not 1.\n\nLet's change the underlying configset_find_element() function to\nsupport this use-case, we'll make further use of it in a subsequent\ncommit where the git_configset_get_value_multi() function itself will\nexpose this new return value.\n\nThis still leaves various inconsistencies and clobbering or ignoring\nof the return value in place. E.g here we're modifying\nconfigset_add_value(), but ever since it was added in [2] we've been\nignoring its \"int\" return value, but as we're changing the\nconfigset_find_element() it uses, let's have it faithfully ferry that\n\"ret\" along.\n\nLet's also use the \"RESULT_MUST_BE_USED\" macro introduced in [3] to\nassert that we're checking the return value of\nconfigset_find_element().\n\nWe're leaving the same change to configset_add_value() for some future\nseries. Once we start paying attention to its return value we'd need\nto ferry it up as deep as do_config_from(), and would need to make\nleast read_{,very_}early_config() and git_protected_config() return an\n\"int\" instead of \"void\". Let's leave that for now, and focus on\nthe *_get_*() functions.\n\nIn a subsequent commit we'll fix the other *_get_*() functions to so\nthat they'll ferry our underlying \"ret\" along, rather than normalizing\nit to a \"return 1\". But as an intermediate step to that we'll need to\nfix git_configset_get_value_multi() to return \"int\", and that change\nitself is smaller because of this change to migrate some callers away\nfrom the *_value_multi() API.\n\n1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)\n2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/\n3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n   return values, 2022-09-01),\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                |  5 +---\n builtin/submodule--helper.c |  7 +++--\n builtin/worktree.c          |  3 +--\n config.c                    | 51 ++++++++++++++++++++++++++++++++-----\n config.h                    | 18 +++++++++++++\n t/helper/test-config.c      | 22 ++++++++++++++++\n t/t1308-config-set.sh       | 43 ++++++++++++++++++++++++++++++-\n 7 files changed, 131 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 02455fdcd73..e38d1783f30 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1493,7 +1493,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \t};\n \tint found = 0;\n \tconst char *key = \"maintenance.repo\";\n-\tchar *config_value;\n \tchar *maintpath = get_maintpath();\n \tstruct string_list_item *item;\n \tconst struct string_list *list;\n@@ -1508,9 +1507,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tgit_config_set(\"maintenance.auto\", \"false\");\n \n \t/* Set maintenance strategy, if unset */\n-\tif (!git_config_get_string(\"maintenance.strategy\", &config_value))\n-\t\tfree(config_value);\n-\telse\n+\tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n \tlist = git_config_get_value_multi(key);\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 4c173d8b37a..2278e8c91cb 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t * If there are no path args and submodule.active is set then,\n \t * by default, only initialize 'active' modules.\n \t */\n-\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\tif (!argc && !git_config_get(\"submodule.active\"))\n \t\tmodule_list_active(&list);\n \n \tinfo.prefix = prefix;\n@@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \t\t * If there are no path args and submodule.active is set then,\n \t\t * by default, only initialize 'active' modules.\n \t\t */\n-\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\t\tif (!argc && !git_config_get(\"submodule.active\"))\n \t\t\tmodule_list_active(&list);\n \n \t\tinfo.prefix = opt.prefix;\n@@ -3140,7 +3140,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con\n static void configure_added_submodule(struct add_data *add_data)\n {\n \tchar *key;\n-\tconst char *val;\n \tstruct child_process add_submod = CHILD_PROCESS_INIT;\n \tstruct child_process add_gitmodules = CHILD_PROCESS_INIT;\n \n@@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)\n \t * is_submodule_active(), since that function needs to find\n \t * out the value of \"submodule.active\" again anyway.\n \t */\n-\tif (!git_config_get_string_tmp(\"submodule.active\", &val)) {\n+\tif (!git_config_get(\"submodule.active\")) {\n \t\t/*\n \t\t * If the submodule being added isn't already covered by the\n \t\t * current configured pathspec, set the submodule's active flag\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex f51c40f1e1e..6ba42d4ad20 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -319,7 +319,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \n \tif (file_exists(from_file)) {\n \t\tstruct config_set cs = { { 0 } };\n-\t\tconst char *core_worktree;\n \t\tint bare;\n \n \t\tif (safe_create_leading_directories(to_file) ||\n@@ -338,7 +337,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \t\t\t\tto_file, \"core.bare\", NULL, \"true\", 0))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\n \t\t\t\t\"core.bare\", to_file);\n-\t\tif (!git_configset_get_value(&cs, \"core.worktree\", &core_worktree) &&\n+\t\tif (!git_configset_get(&cs, \"core.worktree\") &&\n \t\t\tgit_config_set_in_file_gently(to_file,\n \t\t\t\t\t\t\t\"core.worktree\", NULL))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\ndiff --git a/config.c b/config.c\nindex 00090a32fc3..d4f0e4fd619 100644\n--- a/config.c\n+++ b/config.c\n@@ -2289,23 +2289,29 @@ void read_very_early_config(config_fn_t cb, void *data)\n \tconfig_with_options(cb, data, NULL, &opts);\n }\n \n-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n+RESULT_MUST_BE_USED\n+static int configset_find_element(struct config_set *cs, const char *key,\n+\t\t\t\t  struct config_set_element **dest)\n {\n \tstruct config_set_element k;\n \tstruct config_set_element *found_entry;\n \tchar *normalized_key;\n+\tint ret;\n+\n \t/*\n \t * `key` may come from the user, so normalize it before using it\n \t * for querying entries from the hashmap.\n \t */\n-\tif (git_config_parse_key(key, &normalized_key, NULL))\n-\t\treturn NULL;\n+\tret = git_config_parse_key(key, &normalized_key, NULL);\n+\tif (ret)\n+\t\treturn ret;\n \n \thashmap_entry_init(&k.ent, strhash(normalized_key));\n \tk.key = normalized_key;\n \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n \tfree(normalized_key);\n-\treturn found_entry;\n+\t*dest = found_entry;\n+\treturn 0;\n }\n \n static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n@@ -2314,8 +2320,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n \tstruct string_list_item *si;\n \tstruct configset_list_item *l_item;\n \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n+\tint ret;\n \n-\te = configset_find_element(cs, key);\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret)\n+\t\treturn ret;\n \t/*\n \t * Since the keys are being fed by git_config*() callback mechanism, they\n \t * are already normalized. So simply add them without any further munging.\n@@ -2425,8 +2434,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *\n \n const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n {\n-\tstruct config_set_element *e = configset_find_element(cs, key);\n-\treturn e ? &e->value_list : NULL;\n+\tstruct config_set_element *e;\n+\n+\tif (configset_find_element(cs, key, &e))\n+\t\treturn NULL;\n+\telse if (!e)\n+\t\treturn NULL;\n+\treturn &e->value_list;\n+}\n+\n+int git_configset_get(struct config_set *cs, const char *key)\n+{\n+\tstruct config_set_element *e;\n+\tint ret;\n+\n+\tif ((ret = configset_find_element(cs, key, &e)))\n+\t\treturn ret;\n+\telse if (!e)\n+\t\treturn 1;\n+\treturn 0;\n }\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n@@ -2565,6 +2591,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)\n \tconfigset_iter(repo->config, fn, data);\n }\n \n+int repo_config_get(struct repository *repo, const char *key)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get(repo->config, key);\n+}\n+\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value)\n {\n@@ -2679,6 +2711,11 @@ void git_config_clear(void)\n \trepo_config_clear(the_repository);\n }\n \n+int git_config_get(const char *key)\n+{\n+\treturn repo_config_get(the_repository, key);\n+}\n+\n int git_config_get_value(const char *key, const char **value)\n {\n \treturn repo_config_get_value(the_repository, key, value);\ndiff --git a/config.h b/config.h\nindex ef9eade6414..d016d05460d 100644\n--- a/config.h\n+++ b/config.h\n@@ -474,6 +474,9 @@ void git_configset_clear(struct config_set *cs);\n  * value in the 'dest' pointer.\n  */\n \n+RESULT_MUST_BE_USED\n+int git_configset_get(struct config_set *cs, const char *key);\n+\n /*\n  * Finds the highest-priority value for the configuration variable `key`\n  * and config set `cs`, stores the pointer to it in `value` and returns 0.\n@@ -494,6 +497,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n /* Functions for reading a repository's config */\n struct repository;\n void repo_config(struct repository *repo, config_fn_t fn, void *data);\n+\n+/**\n+ * Run only the discover part of the repo_config_get_*() functions\n+ * below, in addition to 1 if not found, returns negative values on\n+ * error (e.g. if the key itself is invalid).\n+ */\n+RESULT_MUST_BE_USED\n+int repo_config_get(struct repository *repo, const char *key);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n const struct string_list *repo_config_get_value_multi(struct repository *repo,\n@@ -530,8 +541,15 @@ void git_protected_config(config_fn_t fn, void *data);\n  * manner, the config API provides two functions `git_config_get_value`\n  * and `git_config_get_value_multi`. They both read values from an internal\n  * cache generated previously from reading the config files.\n+ *\n+ * For those git_config_get*() functions that aren't documented,\n+ * consult the corresponding repo_config_get*() function's\n+ * documentation.\n  */\n \n+RESULT_MUST_BE_USED\n+int git_config_get(const char *key);\n+\n /**\n  * Finds the highest-priority value for the configuration variable `key`,\n  * stores the pointer to it in `value` and returns 0. When the\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex 4ba9eb65606..cbb33ae1fff 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -14,6 +14,8 @@\n  * get_value_multi -> prints all values for the entered key in increasing order\n  *\t\t     of priority\n  *\n+ * get -> print return value for the entered key\n+ *\n  * get_int -> print integer value for the entered key or die\n  *\n  * get_bool -> print bool value for the entered key or die\n@@ -109,6 +111,26 @@ int cmd__config(int argc, const char **argv)\n \t\t\tprintf(\"Value not found for \\\"%s\\\"\\n\", argv[2]);\n \t\t\tgoto exit1;\n \t\t}\n+\t} else if (argc == 3 && !strcmp(argv[1], \"get\")) {\n+\t\tint ret;\n+\n+\t\tif (!(ret = git_config_get(argv[2])))\n+\t\t\tgoto exit0;\n+\t\telse if (ret == 1)\n+\t\t\tprintf(\"Value not found for \\\"%s\\\"\\n\", argv[2]);\n+\t\telse if (ret == -CONFIG_INVALID_KEY)\n+\t\t\tprintf(\"Key \\\"%s\\\" is invalid\\n\", argv[2]);\n+\t\telse if (ret == -CONFIG_NO_SECTION_OR_NAME)\n+\t\t\tprintf(\"Key \\\"%s\\\" has no section\\n\", argv[2]);\n+\t\telse\n+\t\t\t/*\n+\t\t\t * A normal caller should just check \"ret <\n+\t\t\t * 0\", but for our own tests let's BUG() if\n+\t\t\t * our whitelist of git_config_parse_key()\n+\t\t\t * return values isn't exhaustive.\n+\t\t\t */\n+\t\t\tBUG(\"Key \\\"%s\\\" has unknown return %d\", argv[2], ret);\n+\t\tgoto exit1;\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_int\")) {\n \t\tif (!git_config_get_int(argv[2], &val)) {\n \t\t\tprintf(\"%d\\n\", val);\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex 4be1ab1147c..7def7053e1c 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -58,6 +58,8 @@ test_expect_success 'setup default config' '\n \t\tskin = false\n \t\tnose = 1\n \t\thorns\n+\t[value]\n+\t\tless\n \tEOF\n '\n \n@@ -116,6 +118,45 @@ test_expect_success 'find value with the highest priority' '\n \tcheck_config get_value case.baz \"hask\"\n '\n \n+test_expect_success 'return value for an existing key' '\n+\ttest-tool config get lamb.chop >out 2>err &&\n+\ttest_must_be_empty out &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for value-less key' '\n+\ttest-tool config get value.less >out 2>err &&\n+\ttest_must_be_empty out &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for a missing key' '\n+\tcat >expect <<-\\EOF &&\n+\tValue not found for \"missing.key\"\n+\tEOF\n+\ttest_expect_code 1 test-tool config get missing.key >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '\n+\tcat >expect <<-\\EOF &&\n+\tKey \"fails.iskeychar.-\" is invalid\n+\tEOF\n+\ttest_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '\n+\tcat >expect <<-\\EOF &&\n+\tKey \"keynosection\" has no section\n+\tEOF\n+\ttest_expect_code 1 test-tool config get keynosection >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty out\n+'\n+\n test_expect_success 'find integer value for a key' '\n \tcheck_config get_int lamb.chop 65\n '\n@@ -272,7 +313,7 @@ test_expect_success 'proper error on error in default config files' '\n \tcp .git/config .git/config.old &&\n \ttest_when_finished \"mv .git/config.old .git/config\" &&\n \techo \"[\" >>.git/config &&\n-\techo \"fatal: bad config line 34 in file .git/config\" >expect &&\n+\techo \"fatal: bad config line 36 in file .git/config\" >expect &&\n \ttest_expect_code 128 test-tool config get_value foo.bar 2>actual &&\n \ttest_cmp expect actual\n '\n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471690","messageId":"patch-v5-05.10-8f17bf8150c-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v5 05/10] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:45Z","receivedAt":"2023-02-07T16:11:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Have the \"git_configset_get_value_multi()\" function and its siblings\nreturn an \"int\" and populate a \"**dest\" parameter like every other\ngit_configset_get_*()\" in the API.\n\nAs we'll take advantage of in subsequent commits, this fixes a blind\nspot in the API where it wasn't possible to tell whether a list was\nempty from whether a config key existed. For now we don't make use of\nthose new return values, but faithfully convert existing API users.\n\nMost of this is straightforward, commentary on cases that stand out:\n\n- To ensure that we'll properly use the return values of this function\n  in the future we're using the \"RESULT_MUST_BE_USED\" macro introduced\n  in [1].\n\n  As git_die_config() now has to handle this return value let's have\n  it BUG() if it can't find the config entry. As tested for in a\n  preceding commit we can rely on getting the config list in\n  git_die_config().\n\n- The loops after getting the \"list\" value in \"builtin/gc.c\" could\n  also make use of \"unsorted_string_list_has_string()\" instead of using\n  that loop, but let's leave that for now.\n\n- In \"versioncmp.c\" we now use the return value of the functions,\n  instead of checking if the lists are still non-NULL.\n\n1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n   return values, 2022-09-01),\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c |  5 +----\n builtin/gc.c            | 10 ++++------\n builtin/log.c           |  6 +++---\n config.c                | 34 ++++++++++++++++++++--------------\n config.h                | 29 +++++++++++++++++++++--------\n pack-bitmap.c           |  6 +++++-\n submodule.c             |  3 +--\n t/helper/test-config.c  |  6 ++----\n versioncmp.c            | 11 +++++++----\n 9 files changed, 64 insertions(+), 46 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 6aeac371488..fd0e7739e6a 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -45,14 +45,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\tvalues = repo_config_get_value_multi(the_repository,\n-\t\t\t\t\t     config_key);\n-\n \t/*\n \t * Do nothing on an empty list, which is equivalent to the case\n \t * where the config variable does not exist at all.\n \t */\n-\tif (!values)\n+\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e38d1783f30..2b3da377d52 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1510,8 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tlist = git_config_get_value_multi(key);\n-\tif (list) {\n+\tif (!git_config_get_value_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1577,11 +1576,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \tif (config_file) {\n \t\tgit_configset_init(&cs);\n \t\tgit_configset_add_file(&cs, config_file);\n-\t\tlist = git_configset_get_value_multi(&cs, key);\n-\t} else {\n-\t\tlist = git_config_get_value_multi(key);\n \t}\n-\tif (list) {\n+\tif (!(config_file\n+\t      ? git_configset_get_value_multi(&cs, key, &list)\n+\t      : git_config_get_value_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 04412dd9c93..cec8cabd21e 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tint i;\n \tchar *value = NULL;\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n-\tconst struct string_list *config_exclude =\n-\t\t\tgit_config_get_value_multi(\"log.excludeDecoration\");\n+\tconst struct string_list *config_exclude;\n \n-\tif (config_exclude) {\n+\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t&config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex d4f0e4fd619..569819b4a1b 100644\n--- a/config.c\n+++ b/config.c\n@@ -2418,29 +2418,34 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n+\tint ret;\n+\n \t/*\n \t * Follows \"last one wins\" semantic, i.e., if there are multiple matches for the\n \t * queried key in the files of the configset, the value returned will be the last\n \t * value in the value list for that key.\n \t */\n-\tvalues = git_configset_get_value_multi(cs, key);\n+\tif ((ret = git_configset_get_value_multi(cs, key, &values)))\n+\t\treturn ret;\n \n-\tif (!values)\n-\t\treturn 1;\n \tassert(values->nr > 0);\n \t*value = values->items[values->nr - 1].string;\n \treturn 0;\n }\n \n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest)\n {\n \tstruct config_set_element *e;\n+\tint ret;\n \n-\tif (configset_find_element(cs, key, &e))\n-\t\treturn NULL;\n+\tif ((ret = configset_find_element(cs, key, &e)))\n+\t\treturn ret;\n \telse if (!e)\n-\t\treturn NULL;\n-\treturn &e->value_list;\n+\t\treturn 1;\n+\t*dest = &e->value_list;\n+\n+\treturn 0;\n }\n \n int git_configset_get(struct config_set *cs, const char *key)\n@@ -2604,11 +2609,11 @@ int repo_config_get_value(struct repository *repo,\n \treturn git_configset_get_value(repo->config, key, value);\n }\n \n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key)\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest)\n {\n \tgit_config_check_init(repo);\n-\treturn git_configset_get_value_multi(repo->config, key);\n+\treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n int repo_config_get_string(struct repository *repo,\n@@ -2721,9 +2726,9 @@ int git_config_get_value(const char *key, const char **value)\n \treturn repo_config_get_value(the_repository, key, value);\n }\n \n-const struct string_list *git_config_get_value_multi(const char *key)\n+int git_config_get_value_multi(const char *key, const struct string_list **dest)\n {\n-\treturn repo_config_get_value_multi(the_repository, key);\n+\treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n int git_config_get_string(const char *key, char **dest)\n@@ -2870,7 +2875,8 @@ void git_die_config(const char *key, const char *err, ...)\n \t\terror_fn(err, params);\n \t\tva_end(params);\n \t}\n-\tvalues = git_config_get_value_multi(key);\n+\tif (git_config_get_value_multi(key, &values))\n+\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n \tkv_info = values->items[values->nr - 1].util;\n \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n }\ndiff --git a/config.h b/config.h\nindex d016d05460d..115259ecb8d 100644\n--- a/config.h\n+++ b/config.h\n@@ -459,10 +459,18 @@ int git_configset_add_parameters(struct config_set *cs);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n- * configuration variable `key` is not found, returns NULL. The caller\n- * should not free or modify the returned pointer, as it is owned by the cache.\n+ * configuration variable `key` is not found, returns 1 without touching\n+ * `value`.\n+ *\n+ * The key will be parsed for validity with git_config_parse_key(), on\n+ * error a negative value will be returned.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n+RESULT_MUST_BE_USED\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest);\n \n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n@@ -507,8 +515,9 @@ RESULT_MUST_BE_USED\n int repo_config_get(struct repository *repo, const char *key);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key);\n+RESULT_MUST_BE_USED\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -562,10 +571,14 @@ int git_config_get_value(const char *key, const char **value);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key`. When the configuration variable\n- * `key` is not found, returns NULL. The caller should not free or modify\n- * the returned pointer, as it is owned by the cache.\n+ * `key` is not found, returns 1 without touching `value`.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_config_get_value_multi(const char *key);\n+RESULT_MUST_BE_USED\n+int git_config_get_value_multi(const char *key,\n+\t\t\t       const struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex d2a42abf28c..15c5eb507c0 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2314,7 +2314,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)\n \n const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n-\treturn repo_config_get_value_multi(r, \"pack.preferbitmaptips\");\n+\tconst struct string_list *dest;\n+\n+\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\t\treturn dest;\n+\treturn NULL;\n }\n \n int bitmap_is_preferred_refname(struct repository *r, const char *refname)\ndiff --git a/submodule.c b/submodule.c\nindex 3a0dfc417c0..4b6f5223b0c 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tsl = repo_config_get_value_multi(repo, \"submodule.active\");\n-\tif (sl) {\n+\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex cbb33ae1fff..6dc4c37444f 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -97,8 +97,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\tgoto exit1;\n \t\t}\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_value_multi\")) {\n-\t\tstrptr = git_config_get_value_multi(argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_config_get_value_multi(argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\n@@ -181,8 +180,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\t\tgoto exit2;\n \t\t\t}\n \t\t}\n-\t\tstrptr = git_configset_get_value_multi(&cs, argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 323f5d35ea8..60c3a517122 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)\n \tif (!initialized) {\n \t\tconst char *const newk = \"versionsort.suffix\";\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n+\t\tint new = git_config_get_value_multi(newk, &newl);\n+\t\tint old = git_config_get_value_multi(oldk, &oldl);\n \n-\t\tprereleases = git_config_get_value_multi(newk);\n-\t\toldl = git_config_get_value_multi(oldk);\n-\t\tif (prereleases && oldl)\n+\t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-\t\telse if (!prereleases)\n+\t\tif (!new)\n+\t\t\tprereleases = newl;\n+\t\telse if (!old)\n \t\t\tprereleases = oldl;\n \n \t\tinitialized = 1;\n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471691","messageId":"patch-v5-06.10-b515ff13f9b-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v5 06/10] config API: don't lose the git_*get*() return values","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:46Z","receivedAt":"2023-02-07T16:11:19Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Since a preceding commit which added the \"git_config_get()\" family of\nfunctions, and the preceding commit where *_multi() started returning\nan \"int\" we've finally been able to ferry up non-zero return values,\nrather than having negative return values normalized to a \"return 1\"\nalong the way.\n\nIn practice this doesn't matter to existing callers. They're either\nignoring these return values and relying on us to only populate \"dest\"\nif we'd return 0, or normalizing non-zero return values with \"!\".\n\nEven if they weren't normalizing them we'll only return non-zero\nnegative values in those cases where the config key itself is bad,\nwhich excludes the vast majority of our callers, as they hardcode a\nvalued configuration key as a fixed string in the C sources.\n\nSo this change is expected to do nothing for now, but is really here\nfor our own sanity. It's much harder to reason about an API that's\nlosing return values in some cases, and coercing them in others. If\nthere isn't a compelling reason to do otherwise we should let the\ncaller decide if they care about the distinction between bad keys and\nnon-existence.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n config.c | 117 ++++++++++++++++++++++++++++++-------------------------\n config.h |  16 ++++----\n 2 files changed, 72 insertions(+), 61 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex 569819b4a1b..8d7e40ac8a4 100644\n--- a/config.c\n+++ b/config.c\n@@ -2463,86 +2463,93 @@ int git_configset_get(struct config_set *cs, const char *key)\n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n {\n \tconst char *value;\n-\tif (!git_configset_get_value(cs, key, &value))\n-\t\treturn git_config_string((const char **)dest, key, value);\n-\telse\n-\t\treturn 1;\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value(cs, key, &value)))\n+\t\treturn ret;\n+\treturn git_config_string((const char **)dest, key, value);\n }\n \n static int git_configset_get_string_tmp(struct config_set *cs, const char *key,\n \t\t\t\t\tconst char **dest)\n {\n \tconst char *value;\n-\tif (!git_configset_get_value(cs, key, &value)) {\n-\t\tif (!value)\n-\t\t\treturn config_error_nonbool(key);\n-\t\t*dest = value;\n-\t\treturn 0;\n-\t} else {\n-\t\treturn 1;\n-\t}\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value(cs, key, &value)))\n+\t\treturn ret;\n+\tif (!value)\n+\t\treturn config_error_nonbool(key);\n+\t*dest = value;\n+\treturn 0;\n }\n \n int git_configset_get_int(struct config_set *cs, const char *key, int *dest)\n {\n \tconst char *value;\n-\tif (!git_configset_get_value(cs, key, &value)) {\n-\t\t*dest = git_config_int(key, value);\n-\t\treturn 0;\n-\t} else\n-\t\treturn 1;\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value(cs, key, &value)))\n+\t\treturn ret;\n+\t*dest = git_config_int(key, value);\n+\treturn 0;\n }\n \n int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest)\n {\n \tconst char *value;\n-\tif (!git_configset_get_value(cs, key, &value)) {\n-\t\t*dest = git_config_ulong(key, value);\n-\t\treturn 0;\n-\t} else\n-\t\treturn 1;\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value(cs, key, &value)))\n+\t\treturn ret;\n+\t*dest = git_config_ulong(key, value);\n+\treturn 0;\n }\n \n int git_configset_get_bool(struct config_set *cs, const char *key, int *dest)\n {\n \tconst char *value;\n-\tif (!git_configset_get_value(cs, key, &value)) {\n-\t\t*dest = git_config_bool(key, value);\n-\t\treturn 0;\n-\t} else\n-\t\treturn 1;\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value(cs, key, &value)))\n+\t\treturn ret;\n+\t*dest = git_config_bool(key, value);\n+\treturn 0;\n }\n \n int git_configset_get_bool_or_int(struct config_set *cs, const char *key,\n \t\t\t\tint *is_bool, int *dest)\n {\n \tconst char *value;\n-\tif (!git_configset_get_value(cs, key, &value)) {\n-\t\t*dest = git_config_bool_or_int(key, value, is_bool);\n-\t\treturn 0;\n-\t} else\n-\t\treturn 1;\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value(cs, key, &value)))\n+\t\treturn ret;\n+\t*dest = git_config_bool_or_int(key, value, is_bool);\n+\treturn 0;\n }\n \n int git_configset_get_maybe_bool(struct config_set *cs, const char *key, int *dest)\n {\n \tconst char *value;\n-\tif (!git_configset_get_value(cs, key, &value)) {\n-\t\t*dest = git_parse_maybe_bool(value);\n-\t\tif (*dest == -1)\n-\t\t\treturn -1;\n-\t\treturn 0;\n-\t} else\n-\t\treturn 1;\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value(cs, key, &value)))\n+\t\treturn ret;\n+\t*dest = git_parse_maybe_bool(value);\n+\tif (*dest == -1)\n+\t\treturn -1;\n+\treturn 0;\n }\n \n int git_configset_get_pathname(struct config_set *cs, const char *key, const char **dest)\n {\n \tconst char *value;\n-\tif (!git_configset_get_value(cs, key, &value))\n-\t\treturn git_config_pathname(dest, key, value);\n-\telse\n-\t\treturn 1;\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value(cs, key, &value)))\n+\t\treturn ret;\n+\treturn git_config_pathname(dest, key, value);\n }\n \n /* Functions use to read configuration from a repository */\n@@ -2789,9 +2796,11 @@ int git_config_get_expiry_in_days(const char *key, timestamp_t *expiry, timestam\n \tconst char *expiry_string;\n \tintmax_t days;\n \ttimestamp_t when;\n+\tint ret;\n \n-\tif (git_config_get_string_tmp(key, &expiry_string))\n-\t\treturn 1; /* no such thing */\n+\tif ((ret = git_config_get_string_tmp(key, &expiry_string)))\n+\t\t/* no such thing, or git_config_parse_key() failure etc. */\n+\t\treturn ret;\n \n \tif (git_parse_signed(expiry_string, &days, maximum_signed_value_of_type(int))) {\n \t\tconst int scale = 86400;\n@@ -2834,6 +2843,7 @@ int git_config_get_max_percent_split_change(void)\n int git_config_get_index_threads(int *dest)\n {\n \tint is_bool, val;\n+\tint ret;\n \n \tval = git_env_ulong(\"GIT_TEST_INDEX_THREADS\", 0);\n \tif (val) {\n@@ -2841,15 +2851,14 @@ int git_config_get_index_threads(int *dest)\n \t\treturn 0;\n \t}\n \n-\tif (!git_config_get_bool_or_int(\"index.threads\", &is_bool, &val)) {\n-\t\tif (is_bool)\n-\t\t\t*dest = val ? 0 : 1;\n-\t\telse\n-\t\t\t*dest = val;\n-\t\treturn 0;\n-\t}\n-\n-\treturn 1;\n+\tif ((ret = git_config_get_bool_or_int(\"index.threads\", &is_bool,\n+\t\t\t\t\t      &val)))\n+\t\treturn ret;\n+\tif (is_bool)\n+\t\t*dest = val ? 0 : 1;\n+\telse\n+\t\t*dest = val;\n+\treturn 0;\n }\n \n NORETURN\ndiff --git a/config.h b/config.h\nindex 115259ecb8d..da5c498d39a 100644\n--- a/config.h\n+++ b/config.h\n@@ -477,20 +477,22 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,\n  */\n void git_configset_clear(struct config_set *cs);\n \n-/*\n+/**\n  * These functions return 1 if not found, and 0 if found, leaving the found\n- * value in the 'dest' pointer.\n+ * value in the 'dest' pointer. On error a negative value is returned.\n+ *\n+ * The functions that return a single value (i.e. not\n+ * *_get_*multi*()) will return the highest-priority value for the\n+ * configuration variable `key`, i.e. in the case where we have\n+ * multiple values the last value found.\n  */\n \n RESULT_MUST_BE_USED\n int git_configset_get(struct config_set *cs, const char *key);\n \n /*\n- * Finds the highest-priority value for the configuration variable `key`\n- * and config set `cs`, stores the pointer to it in `value` and returns 0.\n- * When the configuration variable `key` is not found, returns 1 without\n- * touching `value`. The caller should not free or modify `value`, as it\n- * is owned by the cache.\n+ * The caller should not free or modify `value`, as it is owned by the\n+ * cache.\n  */\n int git_configset_get_value(struct config_set *cs, const char *key, const char **dest);\n \n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471692","messageId":"patch-v5-07.10-8a83c30ea78-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v5 07/10] for-each-repo: error on bad --config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:47Z","receivedAt":"2023-02-07T16:11:20Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in 6c62f015520 (for-each-repo: do nothing on empty config,\n2021-01-08) this command wants to ignore a non-existing config key,\nbut let's not conflate that with bad config.\n\nBefore this, all these added tests would pass with an exit code of 0.\n\nWe could preserve the comment added in 6c62f015520, but now that we're\ndirectly using the documented repo_config_get_value_multi() value it's\njust narrating something that should be obvious from the API use, so\nlet's drop it.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  | 11 ++++++-----\n t/t0068-for-each-repo.sh |  6 ++++++\n 2 files changed, 12 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex fd0e7739e6a..224164addb3 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tstatic const char *config_key = NULL;\n \tint i, result = 0;\n \tconst struct string_list *values;\n+\tint err;\n \n \tconst struct option options[] = {\n \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n@@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\t/*\n-\t * Do nothing on an empty list, which is equivalent to the case\n-\t * where the config variable does not exist at all.\n-\t */\n-\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n+\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\tif (err < 0)\n+\t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n+\t\t\t       for_each_repo_usage, options, config_key);\n+\telse if (err)\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 3648d439a87..6b51e00da0e 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '\n \tgit for-each-repo --config=bogus.config -- help --no-such-option\n '\n \n+test_expect_success 'error on bad config keys' '\n+\ttest_expect_code 129 git for-each-repo --config=a &&\n+\ttest_expect_code 129 git for-each-repo --config=a.b. &&\n+\ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n+'\n+\n test_done\n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471693","messageId":"patch-v5-08.10-d9abc78c2be-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v5 08/10] config API users: test for *_get_value_multi() segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:48Z","receivedAt":"2023-02-07T16:11:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As we'll discuss in the subsequent commit these tests all\nshow *_get_value_multi() API users unable to handle there being a\nvalue-less key in the config, which is represented with a \"NULL\" for\nthat entry in the \"string\" member of the returned \"struct\nstring_list\", causing a segfault.\n\nThese added tests exhaustively test for that issue, as we'll see in a\nsubsequent commit we'll need to change all of the API users\nof *_get_value_multi(). These cases were discovered by triggering each\none individually, and then adding these tests.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t4202-log.sh                 | 11 +++++++++++\n t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++\n t/t7004-tag.sh                 | 12 ++++++++++++\n t/t7413-submodule-is-active.sh | 12 ++++++++++++\n t/t7900-maintenance.sh         | 23 +++++++++++++++++++++++\n 5 files changed, 74 insertions(+)\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 2ce2b41174d..e4f02d8208b 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n+test_expect_failure 'parse log.excludeDecoration with no value' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[log]\n+\t\texcludeDecoration\n+\tEOF\n+\tgit log --decorate=short\n+'\n+\n test_expect_success 'decorate-refs with glob' '\n \tcat >expect.decorate <<-\\EOF &&\n \tMerge-tag-reach\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 7d8dee41b0d..0306b399188 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,6 +404,22 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n+\ttest_expect_failure 'pack.preferBitmapTips' '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -rf repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\tgit config pack.writeBitmapLookupTable '\"$writeLookupTable\"' &&\n+\t\t\ttest_commit_bulk --message=\"%s\" 103 &&\n+\n+\t\t\tcat >>.git/config <<-\\EOF &&\n+\t\t\t[pack]\n+\t\t\t\tpreferBitmapTips\n+\t\t\tEOF\n+\t\t\tgit repack -adb\n+\t\t)\n+\t'\n+\n \ttest_expect_success 'complains about multiple pack bitmaps' '\n \t\trm -fr repo &&\n \t\tgit init repo &&\ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex 9aa1660651b..f343551a7d4 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[versionsort]\n+\t\tprereleaseSuffix\n+\t\tsuffix\n+\tEOF\n+\tgit tag -l --sort=version:refname\n+'\n+\n test_expect_success 'version sort with prerelease reordering' '\n \ttest_config versionsort.prereleaseSuffix -rc &&\n \tgit tag foo1.6-rc1 &&\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex 7cdc2637649..bfe27e50732 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n+test_expect_failure 'is-active handles submodule.active config missing a value' '\n+\tcp super/.git/config super/.git/config.orig &&\n+\ttest_when_finished mv super/.git/config.orig super/.git/config &&\n+\n+\tcat >>super/.git/config <<-\\EOF &&\n+\t[submodule]\n+\t\tactive\n+\tEOF\n+\n+\ttest-tool -C super submodule is-active sub1\n+'\n+\n test_expect_success 'is-active works with basic submodule.active config' '\n \ttest_when_finished \"git -C super config submodule.sub1.URL ../sub\" &&\n \ttest_when_finished \"git -C super config --unset-all submodule.active\" &&\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 823331e44a0..d82eac6a471 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,6 +524,29 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n+test_expect_failure 'register with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance register\n+'\n+\n+test_expect_failure 'unregister with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance unregister &&\n+\tgit maintenance unregister --force\n+'\n+\n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \tMETA=\"a+b*c\" &&\n \tgit init \"$META\" &&\n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471694","messageId":"patch-v5-09.10-65fa91e7ce7-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v5 09/10] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:49Z","receivedAt":"2023-02-07T16:11:39Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix numerous and mostly long-standing segfaults in consumers of\nthe *_config_*value_multi() API. As discussed in the preceding commit\nan empty key in the config syntax yields a \"NULL\" string, which these\nusers would give to strcmp() (or similar), resulting in segfaults.\n\nAs this change shows, most users users of the *_config_*value_multi()\nAPI didn't really want such an an unsafe and low-level API, let's give\nthem something with the safety of git_config_get_string() instead.\n\nThis fix is similar to what the *_string() functions and others\nacquired in[1] and [2]. Namely introducing and using a safer\n\"*_get_string_multi()\" variant of the low-level \"_*value_multi()\"\nfunction.\n\nThis fixes segfaults in code introduced in:\n\n  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)\n  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)\n  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)\n  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)\n  - 92156291ca8 (log: add default decoration filter, 2022-08-05)\n  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)\n\nThere are now two users ofthe low-level API:\n\n- One in \"builtin/for-each-repo.c\", which we'll convert in a\n  subsequent commit.\n\n- The \"t/helper/test-config.c\" code added in [3].\n\nAs seen in the preceding commit we need to give the\n\"t/helper/test-config.c\" caller these \"NULL\" entries.\n\nWe could also alter the underlying git_configset_get_value_multi()\nfunction to be \"string safe\", but doing so would leave no room for\nother variants of \"*_get_value_multi()\" that coerce to other types.\n\nSuch coercion can't be built on the string version, since as we've\nestablished \"NULL\" is a true value in the boolean context, but if we\ncoerced it to \"\" for use in a list of strings it'll be subsequently\ncoerced to \"false\" as a boolean.\n\nThe callback pattern being used here will make it easy to introduce\ne.g. a \"multi\" variant which coerces its values to \"bool\", \"int\",\n\"path\" etc.\n\n1. 40ea4ed9032 (Add config_error_nonbool() helper function,\n   2008-02-11)\n2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,\n   2008-02-11).\n3. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                   |  6 +++---\n builtin/log.c                  |  4 ++--\n config.c                       | 32 ++++++++++++++++++++++++++++++++\n config.h                       | 19 +++++++++++++++++++\n pack-bitmap.c                  |  2 +-\n submodule.c                    |  2 +-\n t/t4202-log.sh                 |  8 ++++++--\n t/t5310-pack-bitmaps.sh        |  8 ++++++--\n t/t7004-tag.sh                 |  9 +++++++--\n t/t7413-submodule-is-active.sh |  8 ++++++--\n t/t7900-maintenance.sh         | 25 ++++++++++++++++++++-----\n versioncmp.c                   |  4 ++--\n 12 files changed, 105 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 2b3da377d52..9497bdf23e4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1510,7 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tif (!git_config_get_value_multi(key, &list)) {\n+\tif (!git_config_get_string_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1578,8 +1578,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tgit_configset_add_file(&cs, config_file);\n \t}\n \tif (!(config_file\n-\t      ? git_configset_get_value_multi(&cs, key, &list)\n-\t      : git_config_get_value_multi(key, &list))) {\n+\t      ? git_configset_get_string_multi(&cs, key, &list)\n+\t      : git_config_get_string_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex cec8cabd21e..481685d5263 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n \tconst struct string_list *config_exclude;\n \n-\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n-\t\t\t\t\t&config_exclude)) {\n+\tif (!git_config_get_string_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t &config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex 8d7e40ac8a4..de92d592e50 100644\n--- a/config.c\n+++ b/config.c\n@@ -2448,6 +2448,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \treturn 0;\n }\n \n+static int check_multi_string(struct string_list_item *item, void *util)\n+{\n+\treturn item->string ? 0 : config_error_nonbool(util);\n+}\n+\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest)\n+{\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value_multi(cs, key, dest)))\n+\t\treturn ret;\n+\tif ((ret = for_each_string_list((struct string_list *)*dest,\n+\t\t\t\t\tcheck_multi_string, (void *)key)))\n+\t\treturn ret;\n+\n+\treturn 0;\n+}\n+\n int git_configset_get(struct config_set *cs, const char *key)\n {\n \tstruct config_set_element *e;\n@@ -2623,6 +2642,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,\n \treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_string_multi(repo->config, key, dest);\n+}\n+\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest)\n {\n@@ -2738,6 +2764,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)\n \treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest)\n+{\n+\treturn repo_config_get_string_multi(the_repository, key, dest);\n+}\n+\n int git_config_get_string(const char *key, char **dest)\n {\n \treturn repo_config_get_string(the_repository, key, dest);\ndiff --git a/config.h b/config.h\nindex da5c498d39a..8577c80213c 100644\n--- a/config.h\n+++ b/config.h\n@@ -472,6 +472,19 @@ RESULT_MUST_BE_USED\n int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \t\t\t\t  const struct string_list **dest);\n \n+/**\n+ * A validation wrapper for git_configset_get_value_multi() which does\n+ * for it what git_configset_get_string() does for\n+ * git_configset_get_value().\n+ *\n+ * The configuration syntax allows for \"[section] key\", which will\n+ * give us a NULL entry in the \"struct string_list\", as opposed to\n+ * \"[section] key =\" which is the empty string. Most users of the API\n+ * are not prepared to handle NULL in a \"struct string_list\".\n+ */\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest);\n+\n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n  */\n@@ -520,6 +533,9 @@ int repo_config_get_value(struct repository *repo,\n RESULT_MUST_BE_USED\n int repo_config_get_value_multi(struct repository *repo, const char *key,\n \t\t\t\tconst struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -581,6 +597,9 @@ int git_config_get_value(const char *key, const char **value);\n RESULT_MUST_BE_USED\n int git_config_get_value_multi(const char *key,\n \t\t\t       const struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 15c5eb507c0..d003c7e60b4 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2316,7 +2316,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n \tconst struct string_list *dest;\n \n-\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\tif (!repo_config_get_string_multi(r, \"pack.preferbitmaptips\", &dest))\n \t\treturn dest;\n \treturn NULL;\n }\ndiff --git a/submodule.c b/submodule.c\nindex 4b6f5223b0c..30a103246ec 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n+\tif (!repo_config_get_string_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex e4f02d8208b..ae73aef922f 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n-test_expect_failure 'parse log.excludeDecoration with no value' '\n+test_expect_success 'parse log.excludeDecoration with no value' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '\n \t[log]\n \t\texcludeDecoration\n \tEOF\n-\tgit log --decorate=short\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''log.excludeDecoration'\\''\n+\tEOF\n+\tgit log --decorate=short 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'decorate-refs with glob' '\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 0306b399188..526a5a506eb 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,7 +404,7 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n-\ttest_expect_failure 'pack.preferBitmapTips' '\n+\ttest_expect_success 'pack.preferBitmapTips' '\n \t\tgit init repo &&\n \t\ttest_when_finished \"rm -rf repo\" &&\n \t\t(\n@@ -416,7 +416,11 @@ test_bitmap_cases () {\n \t\t\t[pack]\n \t\t\t\tpreferBitmapTips\n \t\t\tEOF\n-\t\t\tgit repack -adb\n+\t\t\tcat >expect <<-\\EOF &&\n+\t\t\terror: missing value for '\\''pack.preferbitmaptips'\\''\n+\t\t\tEOF\n+\t\t\tgit repack -adb 2>actual &&\n+\t\t\ttest_cmp expect actual\n \t\t)\n \t'\n \ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex f343551a7d4..f4a31ada79a 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas\n \t\tprereleaseSuffix\n \t\tsuffix\n \tEOF\n-\tgit tag -l --sort=version:refname\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''versionsort.suffix'\\''\n+\terror: missing value for '\\''versionsort.prereleasesuffix'\\''\n+\tEOF\n+\tgit tag -l --sort=version:refname 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'version sort with prerelease reordering' '\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex bfe27e50732..887d181b72e 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n-test_expect_failure 'is-active handles submodule.active config missing a value' '\n+test_expect_success 'is-active handles submodule.active config missing a value' '\n \tcp super/.git/config super/.git/config.orig &&\n \ttest_when_finished mv super/.git/config.orig super/.git/config &&\n \n@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'\n \t\tactive\n \tEOF\n \n-\ttest-tool -C super submodule is-active sub1\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''submodule.active'\\''\n+\tEOF\n+\ttest-tool -C super submodule is-active sub1 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'is-active works with basic submodule.active config' '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d82eac6a471..487e326b3fa 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n-test_expect_failure 'register with no value for maintenance.repo' '\n+test_expect_success 'register with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance register\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\tgit maintenance register 2>actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n-test_expect_failure 'unregister with no value for maintenance.repo' '\n+test_expect_success 'unregister with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -543,8 +548,18 @@ test_expect_failure 'unregister with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance unregister &&\n-\tgit maintenance unregister --force\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\ttest_expect_code 128 git maintenance unregister 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo &&\n+\n+\tgit maintenance unregister --force 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 60c3a517122..7498da96e0e 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n \t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n-\t\tint new = git_config_get_value_multi(newk, &newl);\n-\t\tint old = git_config_get_value_multi(oldk, &oldl);\n+\t\tint new = git_config_get_string_multi(newk, &newl);\n+\t\tint old = git_config_get_string_multi(oldk, &oldl);\n \n \t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471695","messageId":"patch-v5-10.10-4db3c6d0ed9-20230207T154000Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v5 10/10] for-each-repo: with bad config, don't conflate <path> and <cmd>","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-07T16:10:50Z","receivedAt":"2023-02-07T16:11:41Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on\nconfigured repos, 2020-09-11). Due to assuming that elements returned\nfrom the repo_config_get_value_multi() call wouldn't be \"NULL\" we'd\nconflate the <path> and <command> part of the argument list when\nrunning commands.\n\nAs noted in the preceding commit the fix is to move to a safer\n\"*_string_multi()\" version of the *_multi() API. This change is\nseparated from the rest because those all segfaulted. In this change\nwe ended up with different behavior.\n\nWhen using the \"--config=<config>\" form we take each element of the\nlist as a path to a repository. E.g. with a configuration like:\n\n\t[repo] list = /some/repo\n\nWe would, with this command:\n\n\tgit for-each-repo --config=repo.list status builtin\n\nRun a \"git status\" in /some/repo, as:\n\n\tgit -C /some/repo status builtin\n\nI.e. ask \"status\" to report on the \"builtin\" directory. But since a\nconfiguration such as this would result in a \"struct string_list *\"\nwith one element, whose \"string\" member is \"NULL\":\n\n\t[repo] list\n\nWe would, when constructing our command-line in\n\"builtin/for-each-repo.c\"...\n\n\tstrvec_pushl(&child.args, \"-C\", path, NULL);\n\tfor (i = 0; i < argc; i++)\n\t\tstrvec_push(&child.args, argv[i]);\n\n...have that \"path\" be \"NULL\", and as strvec_pushl() stops when it\nsees NULL we'd end with the first \"argv\" element as the argument to\nthe \"-C\" option, e.g.:\n\n\tgit -C status builtin\n\nI.e. we'd run the command \"builtin\" in the \"status\" directory.\n\nIn another context this might be an interesting security\nvulnerability, but I think that this amounts to a nothingburger on\nthat front.\n\nA hypothetical attacker would need to be able to write config for the\nvictim to run, if they're able to do that there's more interesting\nattack vectors. See the \"safe.directory\" facility added in\n8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).\n\nAn even more unlikely possibility would be an attacker able to\ngenerate the config used for \"for-each-repo --config=<key>\", but\nnothing else (e.g. an automated system producing that list).\n\nEven in that case the attack vector is limited to the user running\ncommands whose name matches a directory that's interesting to the\nattacker (e.g. a \"log\" directory in a repository). The second\nargument (if any) of the command is likely to make git die without\ndoing anything interesting (e.g. \"-p\" to \"log\", there being no \"-p\"\nbuilt-in command to run).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  |  2 +-\n t/t0068-for-each-repo.sh | 13 +++++++++++++\n 2 files changed, 14 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 224164addb3..ce8f7a99086 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\terr = repo_config_get_string_multi(the_repository, config_key, &values);\n \tif (err < 0)\n \t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n \t\t\t       for_each_repo_usage, options, config_key);\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 6b51e00da0e..4b90b74d5d5 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -46,4 +46,17 @@ test_expect_success 'error on bad config keys' '\n \ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n '\n \n+test_expect_success 'error on NULL value for config keys' '\n+\tcat >>.git/config <<-\\EOF &&\n+\t[empty]\n+\t\tkey\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''empty.key'\\''\n+\tEOF\n+\ttest_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.39.1.1430.gb2471c0aaf4\n\n"},{"id":"471699","messageId":"xmqqr0v1z87i.fsf@gitster.g","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"Re: [PATCH v5 00/10] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-02-07T17:38:57Z","receivedAt":"2023-02-07T17:39:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> * Added tests for value-less at the end of a list to 2/10, per Junio's\n>   request.\n\nI do not \"request\" anything during my reviews, and I prefer not to\nsee that verb.  If you find what a reviewer suggests is valuable,\nyou take it, otherwise you explain why it is better to go without\nwhat was suggested.\n\n>   As the much of the point of this series is to make that API less of\n>   a special snowflake a new 6/10 instead finishes up the work of\n>   having most of the rest of the API return the un-coerced \"ret\" from\n>   the depths of the config API.\n>\n>   That patch is quite large by line count, but pretty trivial in\n>   complexity. All of those functions are copy/pasted versions of one\n>   another with very minor variations.\n>\n> * Updated the 8/10 commit message, which was stale from a previous\n>   version of this topic.\n\nThis is now 9/10, thanks to the new 6/10 step being added, and it\nreads well.\n\nThanks, will queue.\n"},{"id":"471836","messageId":"kl6llel7trlw.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v5-02.10-91a44456327-20230207T154000Z-avarab@gmail.com","subject":"Re: [PATCH v5 02/10] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-02-09T04:00:59Z","receivedAt":"2023-02-09T04:03:03Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> +\t\t# Value-less in the middle of a list\n> +\t\tcat >\"$config\" <<-\\EOF &&\n> +\t\t[a]key=x\n> +\t\t[a]key\n> +\t\t[a]key=y\n> +\t\tEOF\n> +\t\tcase \"$op\" in\n> +\t\t*_multi)\n> +\t\t\tcat >expect <<-\\EOF\n> +\t\t\tx\n> +\t\t\t(NULL)\n> +\t\t\ty\n> +\t\t\tEOF\n> +\t\t\t;;\n> +\t\t*)\n> +\t\t\tcat >expect <<-\\EOF\n> +\t\t\ty\n> +\t\t\tEOF\n> +\t\t\t;;\n> +\t\tesac &&\n> +\t\ttest-tool config \"$op\" a.key $file >actual &&\n> +\t\ttest_cmp expect actual &&\n\nThis extra test case makes me feel a bit better about making this DRY,\nthough the extra \"case\" statement detracts from the readability a bit.\n\nMaybe if we split the _multi and non-multi cases?\n\n\ttest_expect_success \"multi\" '\n    # tmp config things\n\n\t\t# Value-less in the middle of a list\n\t\tcat >\"$config\" <<-\\EOF &&\n\t\t[a]key=x\n\t\t[a]key\n\t\t[a]key=y\n\t\tEOF\n    cat >expect <<-\\EOF\n    x\n    (NULL)\n    y\n    EOF\n\t\ttest-tool config \"$op\" a.key $file >actual &&\n\t\ttest_cmp expect actual &&\n\n\t\t# Value-less at the end of a least (probable typo)\n\t\tcat >\"$config\" <<-\\EOF &&\n\t\t[a]key=x\n\t\t[a]key=y\n\t\t[a]key\n\t\tEOF\n    cat >expect <<-\\EOF\n    x\n    y\n    (NULL)\n    EOF\n\t\ttest-tool config \"$op\" a.key $file >actual &&\n\t\ttest_cmp expect actual\n\t'\n\n\ttest_expect_success \"single\" '\n    # tmp config things\n\n\t\t# Value-less in the middle of a list\n\t\tcat >\"$config\" <<-\\EOF &&\n\t\t[a]key=x\n\t\t[a]key\n\t\t[a]key=y\n\t\tEOF\n    cat >expect <<-\\EOF\n    y\n    EOF\n\t\ttest-tool config \"$op\" a.key $file >actual &&\n\t\ttest_cmp expect actual &&\n\n\t\t# Value-less at the end of a least (probable typo)\n\t\tcat >\"$config\" <<-\\EOF &&\n\t\t[a]key=x\n\t\t[a]key=y\n\t\t[a]key\n\t\tEOF\n    cat >expect <<-\\EOF\n    (NULL)\n    EOF\n\t\ttest-tool config \"$op\" a.key $file >actual &&\n\t\ttest_cmp expect actual\n\t'\n\nIdk. It does read a bit clearer to me, but I don't feel strongly about\nit.\n\n> +\t\t# Value-less at the end of a least\n\ns/least/list\n"},{"id":"471838","messageId":"kl6lttzvw8k1.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v5-03.10-4a73151abde-20230207T154000Z-avarab@gmail.com","subject":"Re: [PATCH v5 03/10] config API: add and use a \"git_config_get()\" family of functions","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-02-09T08:24:14Z","receivedAt":"2023-02-09T08:24:26Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> We could have changed git_configset_get_value_multi() (and then\n> git_config_get_value() etc.) to accept a \"NULL\" as a \"dest\" for all\n> callers, but let's avoid changing the behavior of existing API\n> users. Having an \"unused\" value that we throw away internal to\n> config.c is cheap.\n>\n> A \"NULL as optional dest\" pattern is also more fragile, as the intent\n> of the caller might be misinterpreted if he were to accidentally pass\n> \"NULL\", e.g. when \"dest\" is passed in from another function.\n\nOkay, I think I can buy this argument. In other words,\ngit_config_get_value() is only used to put the value in \"*dest\", so\n\"dest = NULL\" is an error. This is by design, because it defends against\ncallers who are using it wrongly. If it accepted \"NULL\" to mean 'dest\nwill be ignored', we're creating possible hard-to-spot bugs because we\nno longer error out early.\n\n> This still leaves various inconsistencies and clobbering or ignoring\n> of the return value in place. E.g here we're modifying\n> configset_add_value(), but ever since it was added in [2] we've been\n> ignoring its \"int\" return value, but as we're changing the\n> configset_find_element() it uses, let's have it faithfully ferry that\n> \"ret\" along.\n>\n> Let's also use the \"RESULT_MUST_BE_USED\" macro introduced in [3] to\n> assert that we're checking the return value of\n> configset_find_element().\n>\n> We're leaving the same change to configset_add_value() for some future\n> series. Once we start paying attention to its return value we'd need\n> to ferry it up as deep as do_config_from(), and would need to make\n> least read_{,very_}early_config() and git_protected_config() return an\n> \"int\" instead of \"void\". Let's leave that for now, and focus on\n> the *_get_*() functions.\n>\n> In a subsequent commit we'll fix the other *_get_*() functions to so\n> that they'll ferry our underlying \"ret\" along, rather than normalizing\n> it to a \"return 1\". But as an intermediate step to that we'll need to\n> fix git_configset_get_value_multi() to return \"int\", and that change\n> itself is smaller because of this change to migrate some callers away\n> from the *_value_multi() API.\n\nI haven't read ahead, but on first impression this sounds like it might\nbe too intrusive for a series whose goal is to clean up\n*_get_value_multi().\n\n> diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\n> index 4be1ab1147c..7def7053e1c 100755\n> --- a/t/t1308-config-set.sh\n> +++ b/t/t1308-config-set.sh\n> @@ -58,6 +58,8 @@ test_expect_success 'setup default config' '\n>  \t\tskin = false\n>  \t\tnose = 1\n>  \t\thorns\n> +\t[value]\n> +\t\tless\n>  \tEOF\n>  '\n>  \n> @@ -116,6 +118,45 @@ test_expect_success 'find value with the highest priority' '\n>  \tcheck_config get_value case.baz \"hask\"\n>  '\n>  \n> +test_expect_success 'return value for an existing key' '\n> +\ttest-tool config get lamb.chop >out 2>err &&\n> +\ttest_must_be_empty out &&\n> +\ttest_must_be_empty err\n> +'\n> +\n> +test_expect_success 'return value for value-less key' '\n> +\ttest-tool config get value.less >out 2>err &&\n> +\ttest_must_be_empty out &&\n> +\ttest_must_be_empty err\n> +'\n> +\n> +test_expect_success 'return value for a missing key' '\n> +\tcat >expect <<-\\EOF &&\n> +\tValue not found for \"missing.key\"\n> +\tEOF\n> +\ttest_expect_code 1 test-tool config get missing.key >actual 2>err &&\n> +\ttest_cmp actual expect &&\n> +\ttest_must_be_empty err\n> +'\n> +\n> +test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '\n> +\tcat >expect <<-\\EOF &&\n> +\tKey \"fails.iskeychar.-\" is invalid\n> +\tEOF\n> +\ttest_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&\n> +\ttest_cmp actual expect &&\n> +\ttest_must_be_empty out\n> +'\n> +\n> +test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '\n> +\tcat >expect <<-\\EOF &&\n> +\tKey \"keynosection\" has no section\n> +\tEOF\n> +\ttest_expect_code 1 test-tool config get keynosection >actual 2>err &&\n> +\ttest_cmp actual expect &&\n> +\ttest_must_be_empty out\n> +'\n> +\n\nNo real comments on the changes themselves. The added test coverage in\nthis version is quite nice.\n"},{"id":"471845","messageId":"230209.86h6vvxhq8.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"kl6lttzvw8k1.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v5 03/10] config API: add and use a \"git_config_get()\" family of functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-09T10:11:01Z","receivedAt":"2023-02-09T10:20:56Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Feb 09 2023, Glen Choo wrote:\n\n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>> [...]\n>> This still leaves various inconsistencies and clobbering or ignoring\n>> of the return value in place. E.g here we're modifying\n>> configset_add_value(), but ever since it was added in [2] we've been\n>> ignoring its \"int\" return value, but as we're changing the\n>> configset_find_element() it uses, let's have it faithfully ferry that\n>> \"ret\" along.\n>>\n>> Let's also use the \"RESULT_MUST_BE_USED\" macro introduced in [3] to\n>> assert that we're checking the return value of\n>> configset_find_element().\n>>\n>> We're leaving the same change to configset_add_value() for some future\n>> series. Once we start paying attention to its return value we'd need\n>> to ferry it up as deep as do_config_from(), and would need to make\n>> least read_{,very_}early_config() and git_protected_config() return an\n>> \"int\" instead of \"void\". Let's leave that for now, and focus on\n>> the *_get_*() functions.\n>>\n>> In a subsequent commit we'll fix the other *_get_*() functions to so\n>> that they'll ferry our underlying \"ret\" along, rather than normalizing\n>> it to a \"return 1\". But as an intermediate step to that we'll need to\n>> fix git_configset_get_value_multi() to return \"int\", and that change\n>> itself is smaller because of this change to migrate some callers away\n>> from the *_value_multi() API.\n>\n> I haven't read ahead, but on first impression this sounds like it might\n> be too intrusive for a series whose goal is to clean up\n> *_get_value_multi().\n\nYeah, that was my inclination too :) But Glen seemed to have a strong\nopinion on the end-state of the topic being inconsistent in its API\n(which he's right about, some stuff returning -1 or 1, some only 1).\n\nI wanted to just leave it for a follow-up topic I've got to fix various\nwarts in the API, but cherry-picked & included the new 06/10 here to\naddress that concern.\n\nI'm also confident that we can expose this to current API users, so\npartly I'm playing reviewer flip-flop here and seeing what sticks. If\nyou feel it should be ejected I'm also happy to do that, and re-roll...\n"},{"id":"471848","messageId":"230209.864jrvxfuy.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"230209.86h6vvxhq8.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v5 03/10] config API: add and use a \"git_config_get()\" family of functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-02-09T10:59:43Z","receivedAt":"2023-02-09T11:02:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Feb 09 2023, Ævar Arnfjörð Bjarmason wrote:\n\n> On Thu, Feb 09 2023, Glen Choo wrote:\n>\n>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>>> [...]\n>>> This still leaves various inconsistencies and clobbering or ignoring\n>>> of the return value in place. E.g here we're modifying\n>>> configset_add_value(), but ever since it was added in [2] we've been\n>>> ignoring its \"int\" return value, but as we're changing the\n>>> configset_find_element() it uses, let's have it faithfully ferry that\n>>> \"ret\" along.\n>>>\n>>> Let's also use the \"RESULT_MUST_BE_USED\" macro introduced in [3] to\n>>> assert that we're checking the return value of\n>>> configset_find_element().\n>>>\n>>> We're leaving the same change to configset_add_value() for some future\n>>> series. Once we start paying attention to its return value we'd need\n>>> to ferry it up as deep as do_config_from(), and would need to make\n>>> least read_{,very_}early_config() and git_protected_config() return an\n>>> \"int\" instead of \"void\". Let's leave that for now, and focus on\n>>> the *_get_*() functions.\n>>>\n>>> In a subsequent commit we'll fix the other *_get_*() functions to so\n>>> that they'll ferry our underlying \"ret\" along, rather than normalizing\n>>> it to a \"return 1\". But as an intermediate step to that we'll need to\n>>> fix git_configset_get_value_multi() to return \"int\", and that change\n>>> itself is smaller because of this change to migrate some callers away\n>>> from the *_value_multi() API.\n>>\n>> I haven't read ahead, but on first impression this sounds like it might\n>> be too intrusive for a series whose goal is to clean up\n>> *_get_value_multi().\n>\n> Yeah, that was my inclination too :) But Glen seemed to have a strong\n> opinion on the end-state of the topic being inconsistent in its API\n> (which he's right about, some stuff returning -1 or 1, some only 1).\n\nHrm, so clearly I lost track of who I was replying to there, sorry :)\n\nI thought this was a reply from Junio at the time.\n\nBut the rest of this stands, i.e. I thought I'd integrate this based on\nyour feedback on the previous version.\n\n> I wanted to just leave it for a follow-up topic I've got to fix various\n> warts in the API, but cherry-picked & included the new 06/10 here to\n> address that concern.\n>\n> I'm also confident that we can expose this to current API users, so\n> partly I'm playing reviewer flip-flop here and seeing what sticks. If\n> you feel it should be ejected I'm also happy to do that, and re-roll...\n\n"},{"id":"471870","messageId":"kl6lr0uybx0f.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"230209.864jrvxfuy.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v5 03/10] config API: add and use a \"git_config_get()\" family of functions","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-02-09T16:53:52Z","receivedAt":"2023-02-09T16:53:57Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> On Thu, Feb 09 2023, Ævar Arnfjörð Bjarmason wrote:\n>\n>> On Thu, Feb 09 2023, Glen Choo wrote:\n>>\n>>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>>>> [...]\n>>>> In a subsequent commit we'll fix the other *_get_*() functions to so\n>>>> that they'll ferry our underlying \"ret\" along, rather than normalizing\n>>>> it to a \"return 1\". But as an intermediate step to that we'll need to\n>>>> fix git_configset_get_value_multi() to return \"int\", and that change\n>>>> itself is smaller because of this change to migrate some callers away\n>>>> from the *_value_multi() API.\n>>>\n>>> I haven't read ahead, but on first impression this sounds like it might\n>>> be too intrusive for a series whose goal is to clean up\n>>> *_get_value_multi().\n>>\n>> Yeah, that was my inclination too :) But Glen seemed to have a strong\n>> opinion on the end-state of the topic being inconsistent in its API\n>> (which he's right about, some stuff returning -1 or 1, some only 1).\n>\n> Hrm, so clearly I lost track of who I was replying to there, sorry :)\n>\n> I thought this was a reply from Junio at the time.\n\nHeh. Maybe I do a good Junio impression.\n\n>> I wanted to just leave it for a follow-up topic I've got to fix various\n>> warts in the API, but cherry-picked & included the new 06/10 here to\n>> address that concern.\n>>\n>> I'm also confident that we can expose this to current API users, so\n>> partly I'm playing reviewer flip-flop here and seeing what sticks. If\n>> you feel it should be ejected I'm also happy to do that, and re-roll...\n\nReading ahead, I think that 06/10 should probably be ejected; the series\nis doing too many things. You're probably right that 06/10 is a safe\nchange to make, but it's a big enough change to require some careful\nreview. I don't think it's worth holding up the original *_multi()\nchanges, especially since I think they're pretty much mergeable.\n\nThe change would probably make more sense in the follow up topic. I\nwouldn't mind giving that topic a look.\n\nAnd if we are sending this follow up topic, then perhaps we could be\nconsistent about *_get() only returning 0 or 1 in this series, and the\nfollow up series could make all the functions ferry up the return code.\nThis does introduce some churn, but the consistency will be a good\nproperty to have, especially if, in the follow up topic, we decide to do\nsomething else with the API.\n"},{"id":"473133","messageId":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com","subject":"[PATCH v6 0/9] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-07T18:09:31Z","receivedAt":"2023-03-07T18:15:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This series fixes numerous segfaults in config API users, because they\ndidn't expect *_get_multi() to hand them a string_list with a NULL in\nit given config like \"[a] key\" (note, no \"=\"'s).\n\nA larger general overview at v1[1], but note the API changes in\nv2[2]. Changes since v5[3]:\n\n * Drop the 6th commit, which made existing API functions return\n   -1. We shouldn't coerce errors to \"return 1\", but making the API\n   consistent can wait for a follow-up to this topic.\n\n   This should address the reason for this topic being stalled for a\n   while, see e.g. [4].\n\n1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/\n2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/\n3. https://lore.kernel.org/git/cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com/\n4. https://lore.kernel.org/git/xmqqcz5snyxz.fsf@gitster.g/\n\nÆvar Arnfjörð Bjarmason (9):\n  config tests: cover blind spots in git_die_config() tests\n  config tests: add \"NULL\" tests for *_get_value_multi()\n  config API: add and use a \"git_config_get()\" family of functions\n  versioncmp.c: refactor config reading next commit\n  config API: have *_multi() return an \"int\" and take a \"dest\"\n  for-each-repo: error on bad --config\n  config API users: test for *_get_value_multi() segfaults\n  config API: add \"string\" version of *_value_multi(), fix segfaults\n  for-each-repo: with bad config, don't conflate <path> and <cmd>\n\n builtin/for-each-repo.c              |  14 ++--\n builtin/gc.c                         |  15 ++--\n builtin/log.c                        |   6 +-\n builtin/submodule--helper.c          |   7 +-\n builtin/worktree.c                   |   3 +-\n config.c                             | 109 ++++++++++++++++++++++-----\n config.h                             |  68 ++++++++++++++---\n pack-bitmap.c                        |   6 +-\n submodule.c                          |   3 +-\n t/helper/test-config.c               |  28 ++++++-\n t/t0068-for-each-repo.sh             |  19 +++++\n t/t1308-config-set.sh                | 108 +++++++++++++++++++++++++-\n t/t3309-notes-merge-auto-resolve.sh  |   7 +-\n t/t4202-log.sh                       |  15 ++++\n t/t5304-prune.sh                     |  12 ++-\n t/t5310-pack-bitmaps.sh              |  20 +++++\n t/t5552-skipping-fetch-negotiator.sh |  16 ++++\n t/t7004-tag.sh                       |  17 +++++\n t/t7413-submodule-is-active.sh       |  16 ++++\n t/t7900-maintenance.sh               |  38 ++++++++++\n versioncmp.c                         |  22 ++++--\n 21 files changed, 477 insertions(+), 72 deletions(-)\n\nRange-diff against v5:\n 1:  cefc4188984 =  1:  43fdb0cf50c config tests: cover blind spots in git_die_config() tests\n 2:  91a44456327 =  2:  4b0799090c9 config tests: add \"NULL\" tests for *_get_value_multi()\n 3:  4a73151abde =  3:  62fe2f04e71 config API: add and use a \"git_config_get()\" family of functions\n 4:  382a77ca69e =  4:  e36303f4d3d versioncmp.c: refactor config reading next commit\n 5:  8f17bf8150c !  5:  e38523267e7 config API: have *_multi() return an \"int\" and take a \"dest\"\n    @@ config.c: void git_die_config(const char *key, const char *err, ...)\n      }\n     \n      ## config.h ##\n    -@@ config.h: int git_configset_add_parameters(struct config_set *cs);\n    +@@ config.h: int git_configset_add_file(struct config_set *cs, const char *filename);\n      /**\n       * Finds and returns the value list, sorted in order of increasing priority\n       * for the configuration variable `key` and config set `cs`. When the\n 6:  b515ff13f9b <  -:  ----------- config API: don't lose the git_*get*() return values\n 7:  8a83c30ea78 =  6:  3a87b35e114 for-each-repo: error on bad --config\n 8:  d9abc78c2be =  7:  66b7060f66f config API users: test for *_get_value_multi() segfaults\n 9:  65fa91e7ce7 =  8:  0da4cdb3f6a config API: add \"string\" version of *_value_multi(), fix segfaults\n10:  4db3c6d0ed9 =  9:  627eb15a319 for-each-repo: with bad config, don't conflate <path> and <cmd>\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473134","messageId":"patch-v6-1.9-43fdb0cf50c-20230307T180516Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"[PATCH v6 1/9] config tests: cover blind spots in git_die_config() tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-07T18:09:32Z","receivedAt":"2023-03-07T18:15:04Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"There were no tests checking for the output of the git_die_config()\nfunction in the config API, added in 5a80e97c827 (config: add\n`git_die_config()` to the config-set API, 2014-08-07). We only tested\n\"test_must_fail\", but didn't assert the output.\n\nWe need tests for this because a subsequent commit will alter the\nreturn value of git_config_get_value_multi(), which is used to get the\nconfig values in the git_die_config() function. This test coverage\nhelps to build confidence in that subsequent change.\n\nThese tests cover different interactions with git_die_config():\n\n- The \"notes.mergeStrategy\" test in\n  \"t/t3309-notes-merge-auto-resolve.sh\" is a case where a function\n  outside of config.c (git_config_get_notes_strategy()) calls\n  git_die_config().\n\n- The \"gc.pruneExpire\" test in \"t5304-prune.sh\" is a case where\n  git_config_get_expiry() calls git_die_config(), covering a different\n  \"type\" than the \"string\" test for \"notes.mergeStrategy\".\n\n- The \"fetch.negotiationAlgorithm\" test in\n  \"t/t5552-skipping-fetch-negotiator.sh\" is a case where\n  git_config_get_string*() calls git_die_config().\n\nWe also cover both the \"from command-line config\" and \"in file..at\nline\" cases here.\n\nThe clobbering of existing \".git/config\" files here is so that we're\nnot implicitly testing the line count of the default config.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-\n t/t5304-prune.sh                     | 12 ++++++++++--\n t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++\n 3 files changed, 32 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh\nindex 141d3e4ca4d..9bd5dbf341f 100755\n--- a/t/t3309-notes-merge-auto-resolve.sh\n+++ b/t/t3309-notes-merge-auto-resolve.sh\n@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '\n \n test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '\n \tgit config core.notesRef refs/notes/y &&\n-\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z &&\n+\tcat >expect <<-\\EOF &&\n+\terror: unknown notes merge strategy foo\n+\tfatal: unable to parse '\\''notes.mergeStrategy'\\'' from command-line config\n+\tEOF\n+\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z 2>actual &&\n+\ttest_cmp expect actual &&\n \t# Verify no changes (y)\n \tverify_notes y y\n '\ndiff --git a/t/t5304-prune.sh b/t/t5304-prune.sh\nindex d65a5f94b4b..5500dd08426 100755\n--- a/t/t5304-prune.sh\n+++ b/t/t5304-prune.sh\n@@ -72,8 +72,16 @@ test_expect_success 'gc: implicit prune --expire' '\n '\n \n test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '\n-\tgit config gc.pruneExpire invalid &&\n-\ttest_must_fail git gc\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t>repo/.git/config &&\n+\tgit -C repo config gc.pruneExpire invalid &&\n+\tcat >expect <<-\\EOF &&\n+\terror: Invalid gc.pruneexpire: '\\''invalid'\\''\n+\tfatal: bad config variable '\\''gc.pruneexpire'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_must_fail git -C repo gc 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'gc: start with ok gc.pruneExpire' '\ndiff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh\nindex 165427d57e5..b55a9f65e6b 100755\n--- a/t/t5552-skipping-fetch-negotiator.sh\n+++ b/t/t5552-skipping-fetch-negotiator.sh\n@@ -3,6 +3,22 @@\n test_description='test skipping fetch negotiator'\n . ./test-lib.sh\n \n+test_expect_success 'fetch.negotiationalgorithm config' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\tcat >repo/.git/config <<-\\EOF &&\n+\t[fetch]\n+\tnegotiationAlgorithm\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''fetch.negotiationalgorithm'\\''\n+\tfatal: bad config variable '\\''fetch.negotiationalgorithm'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_expect_code 128 git -C repo fetch >out 2>actual &&\n+\ttest_must_be_empty out &&\n+\ttest_cmp expect actual\n+'\n+\n have_sent () {\n \twhile test \"$#\" -ne 0\n \tdo\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473135","messageId":"patch-v6-4.9-e36303f4d3d-20230307T180516Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"[PATCH v6 4/9] versioncmp.c: refactor config reading next commit","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-07T18:09:35Z","receivedAt":"2023-03-07T18:15:08Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the reading of the versionSort.suffix and\nversionSort.prereleaseSuffix configuration variables to stay within\nthe bounds of our CodingGuidelines when it comes to line length, and\nto avoid repeating ourselves.\n\nRenaming \"deprecated_prereleases\" to \"oldl\" doesn't help us to avoid\nline wrapping now, but it will in a subsequent commit.\n\nLet's also split out the names of the config variables into variables\nof our own, and refactor the nested if/else to avoid indenting it, and\nthe existing bracing style issue.\n\nThis all helps with the subsequent commit, where we'll need to start\nchecking different git_config_get_value_multi() return value. See\nc026557a373 (versioncmp: generalize version sort suffix reordering,\n2016-12-08) for the original implementation of most of this.\n\nMoving the \"initialized = 1\" assignment allows us to move some of this\nto the variable declarations in the subsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n versioncmp.c | 19 +++++++++++--------\n 1 file changed, 11 insertions(+), 8 deletions(-)\n\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 069ee94a4d7..323f5d35ea8 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)\n \t}\n \n \tif (!initialized) {\n-\t\tconst struct string_list *deprecated_prereleases;\n+\t\tconst char *const newk = \"versionsort.suffix\";\n+\t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *oldl;\n+\n+\t\tprereleases = git_config_get_value_multi(newk);\n+\t\toldl = git_config_get_value_multi(oldk);\n+\t\tif (prereleases && oldl)\n+\t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n+\t\telse if (!prereleases)\n+\t\t\tprereleases = oldl;\n+\n \t\tinitialized = 1;\n-\t\tprereleases = git_config_get_value_multi(\"versionsort.suffix\");\n-\t\tdeprecated_prereleases = git_config_get_value_multi(\"versionsort.prereleasesuffix\");\n-\t\tif (prereleases) {\n-\t\t\tif (deprecated_prereleases)\n-\t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n-\t\t} else\n-\t\t\tprereleases = deprecated_prereleases;\n \t}\n \tif (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,\n \t\t\t\t\t    &diff))\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473136","messageId":"patch-v6-6.9-3a87b35e114-20230307T180516Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"[PATCH v6 6/9] for-each-repo: error on bad --config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-07T18:09:37Z","receivedAt":"2023-03-07T18:15:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in 6c62f015520 (for-each-repo: do nothing on empty config,\n2021-01-08) this command wants to ignore a non-existing config key,\nbut let's not conflate that with bad config.\n\nBefore this, all these added tests would pass with an exit code of 0.\n\nWe could preserve the comment added in 6c62f015520, but now that we're\ndirectly using the documented repo_config_get_value_multi() value it's\njust narrating something that should be obvious from the API use, so\nlet's drop it.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  | 11 ++++++-----\n t/t0068-for-each-repo.sh |  6 ++++++\n 2 files changed, 12 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex fd0e7739e6a..224164addb3 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tstatic const char *config_key = NULL;\n \tint i, result = 0;\n \tconst struct string_list *values;\n+\tint err;\n \n \tconst struct option options[] = {\n \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n@@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\t/*\n-\t * Do nothing on an empty list, which is equivalent to the case\n-\t * where the config variable does not exist at all.\n-\t */\n-\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n+\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\tif (err < 0)\n+\t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n+\t\t\t       for_each_repo_usage, options, config_key);\n+\telse if (err)\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 3648d439a87..6b51e00da0e 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '\n \tgit for-each-repo --config=bogus.config -- help --no-such-option\n '\n \n+test_expect_success 'error on bad config keys' '\n+\ttest_expect_code 129 git for-each-repo --config=a &&\n+\ttest_expect_code 129 git for-each-repo --config=a.b. &&\n+\ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n+'\n+\n test_done\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473137","messageId":"patch-v6-3.9-62fe2f04e71-20230307T180516Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"[PATCH v6 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-07T18:09:34Z","receivedAt":"2023-03-07T18:15:13Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"We already have the basic \"git_config_get_value()\" function and its\n\"repo_*\" and \"configset\" siblings to get a given \"key\" and assign the\nlast key found to a provided \"value\".\n\nBut some callers don't care about that value, but just want to use the\nreturn value of the \"get_value()\" function to check whether the key\nexist (or another non-zero return value).\n\nThe immediate motivation for this is that a subsequent commit will\nneed to change all callers of the \"*_get_value_multi()\" family of\nfunctions. In two cases here we (ab)used it to check whether we had\nany values for the given key, but didn't care about the return value.\n\nThe rest of the callers here used various other config API functions\nto do the same, all of which resolved to the same underlying functions\nto provide the answer.\n\nSome of these were using either git_config_get_string() or\ngit_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a\nconfigure_added_submodule() leak, 2022-09-01) for a recent example. We\ncan now use a helper function that doesn't require a throwaway\nvariable.\n\nWe could have changed git_configset_get_value_multi() (and then\ngit_config_get_value() etc.) to accept a \"NULL\" as a \"dest\" for all\ncallers, but let's avoid changing the behavior of existing API\nusers. Having an \"unused\" value that we throw away internal to\nconfig.c is cheap.\n\nA \"NULL as optional dest\" pattern is also more fragile, as the intent\nof the caller might be misinterpreted if he were to accidentally pass\n\"NULL\", e.g. when \"dest\" is passed in from another function.\n\nAnother name for this function could have been\n\"*_config_key_exists()\", as suggested in [1]. That would work for all\nof these callers, and would currently be equivalent to this function,\nas the git_configset_get_value() API normalizes all non-zero return\nvalues to a \"1\".\n\nBut adding that API would set us up to lose information, as e.g. if\ngit_config_parse_key() in the underlying configset_find_element()\nfails we'd like to return -1, not 1.\n\nLet's change the underlying configset_find_element() function to\nsupport this use-case, we'll make further use of it in a subsequent\ncommit where the git_configset_get_value_multi() function itself will\nexpose this new return value.\n\nThis still leaves various inconsistencies and clobbering or ignoring\nof the return value in place. E.g here we're modifying\nconfigset_add_value(), but ever since it was added in [2] we've been\nignoring its \"int\" return value, but as we're changing the\nconfigset_find_element() it uses, let's have it faithfully ferry that\n\"ret\" along.\n\nLet's also use the \"RESULT_MUST_BE_USED\" macro introduced in [3] to\nassert that we're checking the return value of\nconfigset_find_element().\n\nWe're leaving the same change to configset_add_value() for some future\nseries. Once we start paying attention to its return value we'd need\nto ferry it up as deep as do_config_from(), and would need to make\nleast read_{,very_}early_config() and git_protected_config() return an\n\"int\" instead of \"void\". Let's leave that for now, and focus on\nthe *_get_*() functions.\n\nIn a subsequent commit we'll fix the other *_get_*() functions to so\nthat they'll ferry our underlying \"ret\" along, rather than normalizing\nit to a \"return 1\". But as an intermediate step to that we'll need to\nfix git_configset_get_value_multi() to return \"int\", and that change\nitself is smaller because of this change to migrate some callers away\nfrom the *_value_multi() API.\n\n1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)\n2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/\n3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n   return values, 2022-09-01),\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                |  5 +---\n builtin/submodule--helper.c |  7 +++--\n builtin/worktree.c          |  3 +--\n config.c                    | 51 ++++++++++++++++++++++++++++++++-----\n config.h                    | 18 +++++++++++++\n t/helper/test-config.c      | 22 ++++++++++++++++\n t/t1308-config-set.sh       | 43 ++++++++++++++++++++++++++++++-\n 7 files changed, 131 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 02455fdcd73..e38d1783f30 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1493,7 +1493,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \t};\n \tint found = 0;\n \tconst char *key = \"maintenance.repo\";\n-\tchar *config_value;\n \tchar *maintpath = get_maintpath();\n \tstruct string_list_item *item;\n \tconst struct string_list *list;\n@@ -1508,9 +1507,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tgit_config_set(\"maintenance.auto\", \"false\");\n \n \t/* Set maintenance strategy, if unset */\n-\tif (!git_config_get_string(\"maintenance.strategy\", &config_value))\n-\t\tfree(config_value);\n-\telse\n+\tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n \tlist = git_config_get_value_multi(key);\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 4c173d8b37a..2278e8c91cb 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t * If there are no path args and submodule.active is set then,\n \t * by default, only initialize 'active' modules.\n \t */\n-\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\tif (!argc && !git_config_get(\"submodule.active\"))\n \t\tmodule_list_active(&list);\n \n \tinfo.prefix = prefix;\n@@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \t\t * If there are no path args and submodule.active is set then,\n \t\t * by default, only initialize 'active' modules.\n \t\t */\n-\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\t\tif (!argc && !git_config_get(\"submodule.active\"))\n \t\t\tmodule_list_active(&list);\n \n \t\tinfo.prefix = opt.prefix;\n@@ -3140,7 +3140,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con\n static void configure_added_submodule(struct add_data *add_data)\n {\n \tchar *key;\n-\tconst char *val;\n \tstruct child_process add_submod = CHILD_PROCESS_INIT;\n \tstruct child_process add_gitmodules = CHILD_PROCESS_INIT;\n \n@@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)\n \t * is_submodule_active(), since that function needs to find\n \t * out the value of \"submodule.active\" again anyway.\n \t */\n-\tif (!git_config_get_string_tmp(\"submodule.active\", &val)) {\n+\tif (!git_config_get(\"submodule.active\")) {\n \t\t/*\n \t\t * If the submodule being added isn't already covered by the\n \t\t * current configured pathspec, set the submodule's active flag\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 254283aa6f5..2d81965711f 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -319,7 +319,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \n \tif (file_exists(from_file)) {\n \t\tstruct config_set cs = { { 0 } };\n-\t\tconst char *core_worktree;\n \t\tint bare;\n \n \t\tif (safe_create_leading_directories(to_file) ||\n@@ -338,7 +337,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \t\t\t\tto_file, \"core.bare\", NULL, \"true\", 0))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\n \t\t\t\t\"core.bare\", to_file);\n-\t\tif (!git_configset_get_value(&cs, \"core.worktree\", &core_worktree) &&\n+\t\tif (!git_configset_get(&cs, \"core.worktree\") &&\n \t\t\tgit_config_set_in_file_gently(to_file,\n \t\t\t\t\t\t\t\"core.worktree\", NULL))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\ndiff --git a/config.c b/config.c\nindex 00090a32fc3..d4f0e4fd619 100644\n--- a/config.c\n+++ b/config.c\n@@ -2289,23 +2289,29 @@ void read_very_early_config(config_fn_t cb, void *data)\n \tconfig_with_options(cb, data, NULL, &opts);\n }\n \n-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n+RESULT_MUST_BE_USED\n+static int configset_find_element(struct config_set *cs, const char *key,\n+\t\t\t\t  struct config_set_element **dest)\n {\n \tstruct config_set_element k;\n \tstruct config_set_element *found_entry;\n \tchar *normalized_key;\n+\tint ret;\n+\n \t/*\n \t * `key` may come from the user, so normalize it before using it\n \t * for querying entries from the hashmap.\n \t */\n-\tif (git_config_parse_key(key, &normalized_key, NULL))\n-\t\treturn NULL;\n+\tret = git_config_parse_key(key, &normalized_key, NULL);\n+\tif (ret)\n+\t\treturn ret;\n \n \thashmap_entry_init(&k.ent, strhash(normalized_key));\n \tk.key = normalized_key;\n \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n \tfree(normalized_key);\n-\treturn found_entry;\n+\t*dest = found_entry;\n+\treturn 0;\n }\n \n static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n@@ -2314,8 +2320,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n \tstruct string_list_item *si;\n \tstruct configset_list_item *l_item;\n \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n+\tint ret;\n \n-\te = configset_find_element(cs, key);\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret)\n+\t\treturn ret;\n \t/*\n \t * Since the keys are being fed by git_config*() callback mechanism, they\n \t * are already normalized. So simply add them without any further munging.\n@@ -2425,8 +2434,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *\n \n const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n {\n-\tstruct config_set_element *e = configset_find_element(cs, key);\n-\treturn e ? &e->value_list : NULL;\n+\tstruct config_set_element *e;\n+\n+\tif (configset_find_element(cs, key, &e))\n+\t\treturn NULL;\n+\telse if (!e)\n+\t\treturn NULL;\n+\treturn &e->value_list;\n+}\n+\n+int git_configset_get(struct config_set *cs, const char *key)\n+{\n+\tstruct config_set_element *e;\n+\tint ret;\n+\n+\tif ((ret = configset_find_element(cs, key, &e)))\n+\t\treturn ret;\n+\telse if (!e)\n+\t\treturn 1;\n+\treturn 0;\n }\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n@@ -2565,6 +2591,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)\n \tconfigset_iter(repo->config, fn, data);\n }\n \n+int repo_config_get(struct repository *repo, const char *key)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get(repo->config, key);\n+}\n+\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value)\n {\n@@ -2679,6 +2711,11 @@ void git_config_clear(void)\n \trepo_config_clear(the_repository);\n }\n \n+int git_config_get(const char *key)\n+{\n+\treturn repo_config_get(the_repository, key);\n+}\n+\n int git_config_get_value(const char *key, const char **value)\n {\n \treturn repo_config_get_value(the_repository, key, value);\ndiff --git a/config.h b/config.h\nindex 7606246531a..7dd62ca81bf 100644\n--- a/config.h\n+++ b/config.h\n@@ -465,6 +465,9 @@ void git_configset_clear(struct config_set *cs);\n  * value in the 'dest' pointer.\n  */\n \n+RESULT_MUST_BE_USED\n+int git_configset_get(struct config_set *cs, const char *key);\n+\n /*\n  * Finds the highest-priority value for the configuration variable `key`\n  * and config set `cs`, stores the pointer to it in `value` and returns 0.\n@@ -485,6 +488,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n /* Functions for reading a repository's config */\n struct repository;\n void repo_config(struct repository *repo, config_fn_t fn, void *data);\n+\n+/**\n+ * Run only the discover part of the repo_config_get_*() functions\n+ * below, in addition to 1 if not found, returns negative values on\n+ * error (e.g. if the key itself is invalid).\n+ */\n+RESULT_MUST_BE_USED\n+int repo_config_get(struct repository *repo, const char *key);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n const struct string_list *repo_config_get_value_multi(struct repository *repo,\n@@ -521,8 +532,15 @@ void git_protected_config(config_fn_t fn, void *data);\n  * manner, the config API provides two functions `git_config_get_value`\n  * and `git_config_get_value_multi`. They both read values from an internal\n  * cache generated previously from reading the config files.\n+ *\n+ * For those git_config_get*() functions that aren't documented,\n+ * consult the corresponding repo_config_get*() function's\n+ * documentation.\n  */\n \n+RESULT_MUST_BE_USED\n+int git_config_get(const char *key);\n+\n /**\n  * Finds the highest-priority value for the configuration variable `key`,\n  * stores the pointer to it in `value` and returns 0. When the\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex 4ba9eb65606..cbb33ae1fff 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -14,6 +14,8 @@\n  * get_value_multi -> prints all values for the entered key in increasing order\n  *\t\t     of priority\n  *\n+ * get -> print return value for the entered key\n+ *\n  * get_int -> print integer value for the entered key or die\n  *\n  * get_bool -> print bool value for the entered key or die\n@@ -109,6 +111,26 @@ int cmd__config(int argc, const char **argv)\n \t\t\tprintf(\"Value not found for \\\"%s\\\"\\n\", argv[2]);\n \t\t\tgoto exit1;\n \t\t}\n+\t} else if (argc == 3 && !strcmp(argv[1], \"get\")) {\n+\t\tint ret;\n+\n+\t\tif (!(ret = git_config_get(argv[2])))\n+\t\t\tgoto exit0;\n+\t\telse if (ret == 1)\n+\t\t\tprintf(\"Value not found for \\\"%s\\\"\\n\", argv[2]);\n+\t\telse if (ret == -CONFIG_INVALID_KEY)\n+\t\t\tprintf(\"Key \\\"%s\\\" is invalid\\n\", argv[2]);\n+\t\telse if (ret == -CONFIG_NO_SECTION_OR_NAME)\n+\t\t\tprintf(\"Key \\\"%s\\\" has no section\\n\", argv[2]);\n+\t\telse\n+\t\t\t/*\n+\t\t\t * A normal caller should just check \"ret <\n+\t\t\t * 0\", but for our own tests let's BUG() if\n+\t\t\t * our whitelist of git_config_parse_key()\n+\t\t\t * return values isn't exhaustive.\n+\t\t\t */\n+\t\t\tBUG(\"Key \\\"%s\\\" has unknown return %d\", argv[2], ret);\n+\t\tgoto exit1;\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_int\")) {\n \t\tif (!git_config_get_int(argv[2], &val)) {\n \t\t\tprintf(\"%d\\n\", val);\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex 4be1ab1147c..7def7053e1c 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -58,6 +58,8 @@ test_expect_success 'setup default config' '\n \t\tskin = false\n \t\tnose = 1\n \t\thorns\n+\t[value]\n+\t\tless\n \tEOF\n '\n \n@@ -116,6 +118,45 @@ test_expect_success 'find value with the highest priority' '\n \tcheck_config get_value case.baz \"hask\"\n '\n \n+test_expect_success 'return value for an existing key' '\n+\ttest-tool config get lamb.chop >out 2>err &&\n+\ttest_must_be_empty out &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for value-less key' '\n+\ttest-tool config get value.less >out 2>err &&\n+\ttest_must_be_empty out &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for a missing key' '\n+\tcat >expect <<-\\EOF &&\n+\tValue not found for \"missing.key\"\n+\tEOF\n+\ttest_expect_code 1 test-tool config get missing.key >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '\n+\tcat >expect <<-\\EOF &&\n+\tKey \"fails.iskeychar.-\" is invalid\n+\tEOF\n+\ttest_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '\n+\tcat >expect <<-\\EOF &&\n+\tKey \"keynosection\" has no section\n+\tEOF\n+\ttest_expect_code 1 test-tool config get keynosection >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty out\n+'\n+\n test_expect_success 'find integer value for a key' '\n \tcheck_config get_int lamb.chop 65\n '\n@@ -272,7 +313,7 @@ test_expect_success 'proper error on error in default config files' '\n \tcp .git/config .git/config.old &&\n \ttest_when_finished \"mv .git/config.old .git/config\" &&\n \techo \"[\" >>.git/config &&\n-\techo \"fatal: bad config line 34 in file .git/config\" >expect &&\n+\techo \"fatal: bad config line 36 in file .git/config\" >expect &&\n \ttest_expect_code 128 test-tool config get_value foo.bar 2>actual &&\n \ttest_cmp expect actual\n '\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473138","messageId":"patch-v6-5.9-e38523267e7-20230307T180516Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"[PATCH v6 5/9] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-07T18:09:36Z","receivedAt":"2023-03-07T18:15:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Have the \"git_configset_get_value_multi()\" function and its siblings\nreturn an \"int\" and populate a \"**dest\" parameter like every other\ngit_configset_get_*()\" in the API.\n\nAs we'll take advantage of in subsequent commits, this fixes a blind\nspot in the API where it wasn't possible to tell whether a list was\nempty from whether a config key existed. For now we don't make use of\nthose new return values, but faithfully convert existing API users.\n\nMost of this is straightforward, commentary on cases that stand out:\n\n- To ensure that we'll properly use the return values of this function\n  in the future we're using the \"RESULT_MUST_BE_USED\" macro introduced\n  in [1].\n\n  As git_die_config() now has to handle this return value let's have\n  it BUG() if it can't find the config entry. As tested for in a\n  preceding commit we can rely on getting the config list in\n  git_die_config().\n\n- The loops after getting the \"list\" value in \"builtin/gc.c\" could\n  also make use of \"unsorted_string_list_has_string()\" instead of using\n  that loop, but let's leave that for now.\n\n- In \"versioncmp.c\" we now use the return value of the functions,\n  instead of checking if the lists are still non-NULL.\n\n1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n   return values, 2022-09-01),\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c |  5 +----\n builtin/gc.c            | 10 ++++------\n builtin/log.c           |  6 +++---\n config.c                | 34 ++++++++++++++++++++--------------\n config.h                | 29 +++++++++++++++++++++--------\n pack-bitmap.c           |  6 +++++-\n submodule.c             |  3 +--\n t/helper/test-config.c  |  6 ++----\n versioncmp.c            | 11 +++++++----\n 9 files changed, 64 insertions(+), 46 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 6aeac371488..fd0e7739e6a 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -45,14 +45,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\tvalues = repo_config_get_value_multi(the_repository,\n-\t\t\t\t\t     config_key);\n-\n \t/*\n \t * Do nothing on an empty list, which is equivalent to the case\n \t * where the config variable does not exist at all.\n \t */\n-\tif (!values)\n+\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e38d1783f30..2b3da377d52 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1510,8 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tlist = git_config_get_value_multi(key);\n-\tif (list) {\n+\tif (!git_config_get_value_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1577,11 +1576,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \tif (config_file) {\n \t\tgit_configset_init(&cs);\n \t\tgit_configset_add_file(&cs, config_file);\n-\t\tlist = git_configset_get_value_multi(&cs, key);\n-\t} else {\n-\t\tlist = git_config_get_value_multi(key);\n \t}\n-\tif (list) {\n+\tif (!(config_file\n+\t      ? git_configset_get_value_multi(&cs, key, &list)\n+\t      : git_config_get_value_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex a70fba198f9..e43f6f9d8c1 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tint i;\n \tchar *value = NULL;\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n-\tconst struct string_list *config_exclude =\n-\t\t\tgit_config_get_value_multi(\"log.excludeDecoration\");\n+\tconst struct string_list *config_exclude;\n \n-\tif (config_exclude) {\n+\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t&config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex d4f0e4fd619..569819b4a1b 100644\n--- a/config.c\n+++ b/config.c\n@@ -2418,29 +2418,34 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n+\tint ret;\n+\n \t/*\n \t * Follows \"last one wins\" semantic, i.e., if there are multiple matches for the\n \t * queried key in the files of the configset, the value returned will be the last\n \t * value in the value list for that key.\n \t */\n-\tvalues = git_configset_get_value_multi(cs, key);\n+\tif ((ret = git_configset_get_value_multi(cs, key, &values)))\n+\t\treturn ret;\n \n-\tif (!values)\n-\t\treturn 1;\n \tassert(values->nr > 0);\n \t*value = values->items[values->nr - 1].string;\n \treturn 0;\n }\n \n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest)\n {\n \tstruct config_set_element *e;\n+\tint ret;\n \n-\tif (configset_find_element(cs, key, &e))\n-\t\treturn NULL;\n+\tif ((ret = configset_find_element(cs, key, &e)))\n+\t\treturn ret;\n \telse if (!e)\n-\t\treturn NULL;\n-\treturn &e->value_list;\n+\t\treturn 1;\n+\t*dest = &e->value_list;\n+\n+\treturn 0;\n }\n \n int git_configset_get(struct config_set *cs, const char *key)\n@@ -2604,11 +2609,11 @@ int repo_config_get_value(struct repository *repo,\n \treturn git_configset_get_value(repo->config, key, value);\n }\n \n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key)\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest)\n {\n \tgit_config_check_init(repo);\n-\treturn git_configset_get_value_multi(repo->config, key);\n+\treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n int repo_config_get_string(struct repository *repo,\n@@ -2721,9 +2726,9 @@ int git_config_get_value(const char *key, const char **value)\n \treturn repo_config_get_value(the_repository, key, value);\n }\n \n-const struct string_list *git_config_get_value_multi(const char *key)\n+int git_config_get_value_multi(const char *key, const struct string_list **dest)\n {\n-\treturn repo_config_get_value_multi(the_repository, key);\n+\treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n int git_config_get_string(const char *key, char **dest)\n@@ -2870,7 +2875,8 @@ void git_die_config(const char *key, const char *err, ...)\n \t\terror_fn(err, params);\n \t\tva_end(params);\n \t}\n-\tvalues = git_config_get_value_multi(key);\n+\tif (git_config_get_value_multi(key, &values))\n+\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n \tkv_info = values->items[values->nr - 1].util;\n \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n }\ndiff --git a/config.h b/config.h\nindex 7dd62ca81bf..4db6b90ac20 100644\n--- a/config.h\n+++ b/config.h\n@@ -450,10 +450,18 @@ int git_configset_add_file(struct config_set *cs, const char *filename);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n- * configuration variable `key` is not found, returns NULL. The caller\n- * should not free or modify the returned pointer, as it is owned by the cache.\n+ * configuration variable `key` is not found, returns 1 without touching\n+ * `value`.\n+ *\n+ * The key will be parsed for validity with git_config_parse_key(), on\n+ * error a negative value will be returned.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n+RESULT_MUST_BE_USED\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest);\n \n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n@@ -498,8 +506,9 @@ RESULT_MUST_BE_USED\n int repo_config_get(struct repository *repo, const char *key);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key);\n+RESULT_MUST_BE_USED\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -553,10 +562,14 @@ int git_config_get_value(const char *key, const char **value);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key`. When the configuration variable\n- * `key` is not found, returns NULL. The caller should not free or modify\n- * the returned pointer, as it is owned by the cache.\n+ * `key` is not found, returns 1 without touching `value`.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_config_get_value_multi(const char *key);\n+RESULT_MUST_BE_USED\n+int git_config_get_value_multi(const char *key,\n+\t\t\t       const struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex d2a42abf28c..15c5eb507c0 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2314,7 +2314,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)\n \n const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n-\treturn repo_config_get_value_multi(r, \"pack.preferbitmaptips\");\n+\tconst struct string_list *dest;\n+\n+\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\t\treturn dest;\n+\treturn NULL;\n }\n \n int bitmap_is_preferred_refname(struct repository *r, const char *refname)\ndiff --git a/submodule.c b/submodule.c\nindex 3a0dfc417c0..4b6f5223b0c 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tsl = repo_config_get_value_multi(repo, \"submodule.active\");\n-\tif (sl) {\n+\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex cbb33ae1fff..6dc4c37444f 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -97,8 +97,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\tgoto exit1;\n \t\t}\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_value_multi\")) {\n-\t\tstrptr = git_config_get_value_multi(argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_config_get_value_multi(argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\n@@ -181,8 +180,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\t\tgoto exit2;\n \t\t\t}\n \t\t}\n-\t\tstrptr = git_configset_get_value_multi(&cs, argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 323f5d35ea8..60c3a517122 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)\n \tif (!initialized) {\n \t\tconst char *const newk = \"versionsort.suffix\";\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n+\t\tint new = git_config_get_value_multi(newk, &newl);\n+\t\tint old = git_config_get_value_multi(oldk, &oldl);\n \n-\t\tprereleases = git_config_get_value_multi(newk);\n-\t\toldl = git_config_get_value_multi(oldk);\n-\t\tif (prereleases && oldl)\n+\t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-\t\telse if (!prereleases)\n+\t\tif (!new)\n+\t\t\tprereleases = newl;\n+\t\telse if (!old)\n \t\t\tprereleases = oldl;\n \n \t\tinitialized = 1;\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473139","messageId":"patch-v6-7.9-66b7060f66f-20230307T180516Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"[PATCH v6 7/9] config API users: test for *_get_value_multi() segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-07T18:09:38Z","receivedAt":"2023-03-07T18:15:18Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As we'll discuss in the subsequent commit these tests all\nshow *_get_value_multi() API users unable to handle there being a\nvalue-less key in the config, which is represented with a \"NULL\" for\nthat entry in the \"string\" member of the returned \"struct\nstring_list\", causing a segfault.\n\nThese added tests exhaustively test for that issue, as we'll see in a\nsubsequent commit we'll need to change all of the API users\nof *_get_value_multi(). These cases were discovered by triggering each\none individually, and then adding these tests.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t4202-log.sh                 | 11 +++++++++++\n t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++\n t/t7004-tag.sh                 | 12 ++++++++++++\n t/t7413-submodule-is-active.sh | 12 ++++++++++++\n t/t7900-maintenance.sh         | 23 +++++++++++++++++++++++\n 5 files changed, 74 insertions(+)\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 2ce2b41174d..e4f02d8208b 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n+test_expect_failure 'parse log.excludeDecoration with no value' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[log]\n+\t\texcludeDecoration\n+\tEOF\n+\tgit log --decorate=short\n+'\n+\n test_expect_success 'decorate-refs with glob' '\n \tcat >expect.decorate <<-\\EOF &&\n \tMerge-tag-reach\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 7d8dee41b0d..0306b399188 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,6 +404,22 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n+\ttest_expect_failure 'pack.preferBitmapTips' '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -rf repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\tgit config pack.writeBitmapLookupTable '\"$writeLookupTable\"' &&\n+\t\t\ttest_commit_bulk --message=\"%s\" 103 &&\n+\n+\t\t\tcat >>.git/config <<-\\EOF &&\n+\t\t\t[pack]\n+\t\t\t\tpreferBitmapTips\n+\t\t\tEOF\n+\t\t\tgit repack -adb\n+\t\t)\n+\t'\n+\n \ttest_expect_success 'complains about multiple pack bitmaps' '\n \t\trm -fr repo &&\n \t\tgit init repo &&\ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex 9aa1660651b..f343551a7d4 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[versionsort]\n+\t\tprereleaseSuffix\n+\t\tsuffix\n+\tEOF\n+\tgit tag -l --sort=version:refname\n+'\n+\n test_expect_success 'version sort with prerelease reordering' '\n \ttest_config versionsort.prereleaseSuffix -rc &&\n \tgit tag foo1.6-rc1 &&\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex 7cdc2637649..bfe27e50732 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n+test_expect_failure 'is-active handles submodule.active config missing a value' '\n+\tcp super/.git/config super/.git/config.orig &&\n+\ttest_when_finished mv super/.git/config.orig super/.git/config &&\n+\n+\tcat >>super/.git/config <<-\\EOF &&\n+\t[submodule]\n+\t\tactive\n+\tEOF\n+\n+\ttest-tool -C super submodule is-active sub1\n+'\n+\n test_expect_success 'is-active works with basic submodule.active config' '\n \ttest_when_finished \"git -C super config submodule.sub1.URL ../sub\" &&\n \ttest_when_finished \"git -C super config --unset-all submodule.active\" &&\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 823331e44a0..d82eac6a471 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,6 +524,29 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n+test_expect_failure 'register with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance register\n+'\n+\n+test_expect_failure 'unregister with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance unregister &&\n+\tgit maintenance unregister --force\n+'\n+\n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \tMETA=\"a+b*c\" &&\n \tgit init \"$META\" &&\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473140","messageId":"patch-v6-8.9-0da4cdb3f6a-20230307T180516Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"[PATCH v6 8/9] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-07T18:09:39Z","receivedAt":"2023-03-07T18:15:20Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix numerous and mostly long-standing segfaults in consumers of\nthe *_config_*value_multi() API. As discussed in the preceding commit\nan empty key in the config syntax yields a \"NULL\" string, which these\nusers would give to strcmp() (or similar), resulting in segfaults.\n\nAs this change shows, most users users of the *_config_*value_multi()\nAPI didn't really want such an an unsafe and low-level API, let's give\nthem something with the safety of git_config_get_string() instead.\n\nThis fix is similar to what the *_string() functions and others\nacquired in[1] and [2]. Namely introducing and using a safer\n\"*_get_string_multi()\" variant of the low-level \"_*value_multi()\"\nfunction.\n\nThis fixes segfaults in code introduced in:\n\n  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)\n  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)\n  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)\n  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)\n  - 92156291ca8 (log: add default decoration filter, 2022-08-05)\n  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)\n\nThere are now two users ofthe low-level API:\n\n- One in \"builtin/for-each-repo.c\", which we'll convert in a\n  subsequent commit.\n\n- The \"t/helper/test-config.c\" code added in [3].\n\nAs seen in the preceding commit we need to give the\n\"t/helper/test-config.c\" caller these \"NULL\" entries.\n\nWe could also alter the underlying git_configset_get_value_multi()\nfunction to be \"string safe\", but doing so would leave no room for\nother variants of \"*_get_value_multi()\" that coerce to other types.\n\nSuch coercion can't be built on the string version, since as we've\nestablished \"NULL\" is a true value in the boolean context, but if we\ncoerced it to \"\" for use in a list of strings it'll be subsequently\ncoerced to \"false\" as a boolean.\n\nThe callback pattern being used here will make it easy to introduce\ne.g. a \"multi\" variant which coerces its values to \"bool\", \"int\",\n\"path\" etc.\n\n1. 40ea4ed9032 (Add config_error_nonbool() helper function,\n   2008-02-11)\n2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,\n   2008-02-11).\n3. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                   |  6 +++---\n builtin/log.c                  |  4 ++--\n config.c                       | 32 ++++++++++++++++++++++++++++++++\n config.h                       | 19 +++++++++++++++++++\n pack-bitmap.c                  |  2 +-\n submodule.c                    |  2 +-\n t/t4202-log.sh                 |  8 ++++++--\n t/t5310-pack-bitmaps.sh        |  8 ++++++--\n t/t7004-tag.sh                 |  9 +++++++--\n t/t7413-submodule-is-active.sh |  8 ++++++--\n t/t7900-maintenance.sh         | 25 ++++++++++++++++++++-----\n versioncmp.c                   |  4 ++--\n 12 files changed, 105 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 2b3da377d52..9497bdf23e4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1510,7 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tif (!git_config_get_value_multi(key, &list)) {\n+\tif (!git_config_get_string_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1578,8 +1578,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tgit_configset_add_file(&cs, config_file);\n \t}\n \tif (!(config_file\n-\t      ? git_configset_get_value_multi(&cs, key, &list)\n-\t      : git_config_get_value_multi(key, &list))) {\n+\t      ? git_configset_get_string_multi(&cs, key, &list)\n+\t      : git_config_get_string_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex e43f6f9d8c1..ca847524fa4 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n \tconst struct string_list *config_exclude;\n \n-\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n-\t\t\t\t\t&config_exclude)) {\n+\tif (!git_config_get_string_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t &config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex 569819b4a1b..c63034fb78b 100644\n--- a/config.c\n+++ b/config.c\n@@ -2448,6 +2448,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \treturn 0;\n }\n \n+static int check_multi_string(struct string_list_item *item, void *util)\n+{\n+\treturn item->string ? 0 : config_error_nonbool(util);\n+}\n+\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest)\n+{\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value_multi(cs, key, dest)))\n+\t\treturn ret;\n+\tif ((ret = for_each_string_list((struct string_list *)*dest,\n+\t\t\t\t\tcheck_multi_string, (void *)key)))\n+\t\treturn ret;\n+\n+\treturn 0;\n+}\n+\n int git_configset_get(struct config_set *cs, const char *key)\n {\n \tstruct config_set_element *e;\n@@ -2616,6 +2635,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,\n \treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_string_multi(repo->config, key, dest);\n+}\n+\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest)\n {\n@@ -2731,6 +2757,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)\n \treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest)\n+{\n+\treturn repo_config_get_string_multi(the_repository, key, dest);\n+}\n+\n int git_config_get_string(const char *key, char **dest)\n {\n \treturn repo_config_get_string(the_repository, key, dest);\ndiff --git a/config.h b/config.h\nindex 4db6b90ac20..5f258e5b8df 100644\n--- a/config.h\n+++ b/config.h\n@@ -463,6 +463,19 @@ RESULT_MUST_BE_USED\n int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \t\t\t\t  const struct string_list **dest);\n \n+/**\n+ * A validation wrapper for git_configset_get_value_multi() which does\n+ * for it what git_configset_get_string() does for\n+ * git_configset_get_value().\n+ *\n+ * The configuration syntax allows for \"[section] key\", which will\n+ * give us a NULL entry in the \"struct string_list\", as opposed to\n+ * \"[section] key =\" which is the empty string. Most users of the API\n+ * are not prepared to handle NULL in a \"struct string_list\".\n+ */\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest);\n+\n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n  */\n@@ -509,6 +522,9 @@ int repo_config_get_value(struct repository *repo,\n RESULT_MUST_BE_USED\n int repo_config_get_value_multi(struct repository *repo, const char *key,\n \t\t\t\tconst struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -570,6 +586,9 @@ int git_config_get_value(const char *key, const char **value);\n RESULT_MUST_BE_USED\n int git_config_get_value_multi(const char *key,\n \t\t\t       const struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 15c5eb507c0..d003c7e60b4 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2316,7 +2316,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n \tconst struct string_list *dest;\n \n-\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\tif (!repo_config_get_string_multi(r, \"pack.preferbitmaptips\", &dest))\n \t\treturn dest;\n \treturn NULL;\n }\ndiff --git a/submodule.c b/submodule.c\nindex 4b6f5223b0c..30a103246ec 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n+\tif (!repo_config_get_string_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex e4f02d8208b..ae73aef922f 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n-test_expect_failure 'parse log.excludeDecoration with no value' '\n+test_expect_success 'parse log.excludeDecoration with no value' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '\n \t[log]\n \t\texcludeDecoration\n \tEOF\n-\tgit log --decorate=short\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''log.excludeDecoration'\\''\n+\tEOF\n+\tgit log --decorate=short 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'decorate-refs with glob' '\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 0306b399188..526a5a506eb 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,7 +404,7 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n-\ttest_expect_failure 'pack.preferBitmapTips' '\n+\ttest_expect_success 'pack.preferBitmapTips' '\n \t\tgit init repo &&\n \t\ttest_when_finished \"rm -rf repo\" &&\n \t\t(\n@@ -416,7 +416,11 @@ test_bitmap_cases () {\n \t\t\t[pack]\n \t\t\t\tpreferBitmapTips\n \t\t\tEOF\n-\t\t\tgit repack -adb\n+\t\t\tcat >expect <<-\\EOF &&\n+\t\t\terror: missing value for '\\''pack.preferbitmaptips'\\''\n+\t\t\tEOF\n+\t\t\tgit repack -adb 2>actual &&\n+\t\t\ttest_cmp expect actual\n \t\t)\n \t'\n \ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex f343551a7d4..f4a31ada79a 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas\n \t\tprereleaseSuffix\n \t\tsuffix\n \tEOF\n-\tgit tag -l --sort=version:refname\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''versionsort.suffix'\\''\n+\terror: missing value for '\\''versionsort.prereleasesuffix'\\''\n+\tEOF\n+\tgit tag -l --sort=version:refname 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'version sort with prerelease reordering' '\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex bfe27e50732..887d181b72e 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n-test_expect_failure 'is-active handles submodule.active config missing a value' '\n+test_expect_success 'is-active handles submodule.active config missing a value' '\n \tcp super/.git/config super/.git/config.orig &&\n \ttest_when_finished mv super/.git/config.orig super/.git/config &&\n \n@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'\n \t\tactive\n \tEOF\n \n-\ttest-tool -C super submodule is-active sub1\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''submodule.active'\\''\n+\tEOF\n+\ttest-tool -C super submodule is-active sub1 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'is-active works with basic submodule.active config' '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d82eac6a471..487e326b3fa 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n-test_expect_failure 'register with no value for maintenance.repo' '\n+test_expect_success 'register with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance register\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\tgit maintenance register 2>actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n-test_expect_failure 'unregister with no value for maintenance.repo' '\n+test_expect_success 'unregister with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -543,8 +548,18 @@ test_expect_failure 'unregister with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance unregister &&\n-\tgit maintenance unregister --force\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\ttest_expect_code 128 git maintenance unregister 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo &&\n+\n+\tgit maintenance unregister --force 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 60c3a517122..7498da96e0e 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n \t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n-\t\tint new = git_config_get_value_multi(newk, &newl);\n-\t\tint old = git_config_get_value_multi(oldk, &oldl);\n+\t\tint new = git_config_get_string_multi(newk, &newl);\n+\t\tint old = git_config_get_string_multi(oldk, &oldl);\n \n \t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473141","messageId":"patch-v6-9.9-627eb15a319-20230307T180516Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"[PATCH v6 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-07T18:09:40Z","receivedAt":"2023-03-07T18:15:23Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on\nconfigured repos, 2020-09-11). Due to assuming that elements returned\nfrom the repo_config_get_value_multi() call wouldn't be \"NULL\" we'd\nconflate the <path> and <command> part of the argument list when\nrunning commands.\n\nAs noted in the preceding commit the fix is to move to a safer\n\"*_string_multi()\" version of the *_multi() API. This change is\nseparated from the rest because those all segfaulted. In this change\nwe ended up with different behavior.\n\nWhen using the \"--config=<config>\" form we take each element of the\nlist as a path to a repository. E.g. with a configuration like:\n\n\t[repo] list = /some/repo\n\nWe would, with this command:\n\n\tgit for-each-repo --config=repo.list status builtin\n\nRun a \"git status\" in /some/repo, as:\n\n\tgit -C /some/repo status builtin\n\nI.e. ask \"status\" to report on the \"builtin\" directory. But since a\nconfiguration such as this would result in a \"struct string_list *\"\nwith one element, whose \"string\" member is \"NULL\":\n\n\t[repo] list\n\nWe would, when constructing our command-line in\n\"builtin/for-each-repo.c\"...\n\n\tstrvec_pushl(&child.args, \"-C\", path, NULL);\n\tfor (i = 0; i < argc; i++)\n\t\tstrvec_push(&child.args, argv[i]);\n\n...have that \"path\" be \"NULL\", and as strvec_pushl() stops when it\nsees NULL we'd end with the first \"argv\" element as the argument to\nthe \"-C\" option, e.g.:\n\n\tgit -C status builtin\n\nI.e. we'd run the command \"builtin\" in the \"status\" directory.\n\nIn another context this might be an interesting security\nvulnerability, but I think that this amounts to a nothingburger on\nthat front.\n\nA hypothetical attacker would need to be able to write config for the\nvictim to run, if they're able to do that there's more interesting\nattack vectors. See the \"safe.directory\" facility added in\n8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).\n\nAn even more unlikely possibility would be an attacker able to\ngenerate the config used for \"for-each-repo --config=<key>\", but\nnothing else (e.g. an automated system producing that list).\n\nEven in that case the attack vector is limited to the user running\ncommands whose name matches a directory that's interesting to the\nattacker (e.g. a \"log\" directory in a repository). The second\nargument (if any) of the command is likely to make git die without\ndoing anything interesting (e.g. \"-p\" to \"log\", there being no \"-p\"\nbuilt-in command to run).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  |  2 +-\n t/t0068-for-each-repo.sh | 13 +++++++++++++\n 2 files changed, 14 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 224164addb3..ce8f7a99086 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\terr = repo_config_get_string_multi(the_repository, config_key, &values);\n \tif (err < 0)\n \t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n \t\t\t       for_each_repo_usage, options, config_key);\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 6b51e00da0e..4b90b74d5d5 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -46,4 +46,17 @@ test_expect_success 'error on bad config keys' '\n \ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n '\n \n+test_expect_success 'error on NULL value for config keys' '\n+\tcat >>.git/config <<-\\EOF &&\n+\t[empty]\n+\t\tkey\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''empty.key'\\''\n+\tEOF\n+\ttest_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473142","messageId":"patch-v6-2.9-4b0799090c9-20230307T180516Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"[PATCH v6 2/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-07T18:09:33Z","receivedAt":"2023-03-07T18:16:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"A less well known edge case in the config format is that keys can be\nvalue-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\nare equivalent as far as \"--type=bool\" is concerned:\n\n\t[a]key\n\t[a]key = true\n\nBut as far as our parser is concerned the values for these two are\nNULL, and \"true\". I.e. for a sequence like:\n\n\t[a]key=x\n\t[a]key\n\t[a]key=y\n\nWe get a \"struct string_list\" with \"string\" members with \".string\"\nvalues of:\n\n\t{ \"x\", NULL, \"y\" }\n\nThis behavior goes back to the initial implementation of\ngit_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n2005-10-10).\n\nWhen parts of the config_set API were tested for in [1] they didn't\nadd coverage for 3/4 of the \"(NULL)\" cases handled in\n\"t/helper/test-config.c\". We'd test that case for \"get_value\", but not\n\"get_value_multi\", \"configset_get_value\" and\n\"configset_get_value_multi\".\n\nWe now cover all of those cases, which in turn expose the details of\nhow this part of the config API works.\n\n1. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t1308-config-set.sh | 65 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 65 insertions(+)\n\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex b38e158d3b2..4be1ab1147c 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -146,6 +146,71 @@ test_expect_success 'find multiple values' '\n \tcheck_config get_value_multi case.baz sam bat hask\n '\n \n+test_NULL_in_multi () {\n+\tlocal op=\"$1\" &&\n+\tlocal file=\"$2\" &&\n+\n+\ttest_expect_success \"$op: NULL value in config${file:+ in $file}\" '\n+\t\tconfig=\"$file\" &&\n+\t\tif test -z \"$config\"\n+\t\tthen\n+\t\t\tconfig=.git/config &&\n+\t\t\ttest_when_finished \"mv $config.old $config\" &&\n+\t\t\tmv \"$config\" \"$config\".old\n+\t\tfi &&\n+\n+\t\t# Value-less in the middle of a list\n+\t\tcat >\"$config\" <<-\\EOF &&\n+\t\t[a]key=x\n+\t\t[a]key\n+\t\t[a]key=y\n+\t\tEOF\n+\t\tcase \"$op\" in\n+\t\t*_multi)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\tx\n+\t\t\t(NULL)\n+\t\t\ty\n+\t\t\tEOF\n+\t\t\t;;\n+\t\t*)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\ty\n+\t\t\tEOF\n+\t\t\t;;\n+\t\tesac &&\n+\t\ttest-tool config \"$op\" a.key $file >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# Value-less at the end of a least\n+\t\tcat >\"$config\" <<-\\EOF &&\n+\t\t[a]key=x\n+\t\t[a]key=y\n+\t\t[a]key\n+\t\tEOF\n+\t\tcase \"$op\" in\n+\t\t*_multi)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\tx\n+\t\t\ty\n+\t\t\t(NULL)\n+\t\t\tEOF\n+\t\t\t;;\n+\t\t*)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\t(NULL)\n+\t\t\tEOF\n+\t\t\t;;\n+\t\tesac &&\n+\t\ttest-tool config \"$op\" a.key $file >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+}\n+\n+test_NULL_in_multi \"get_value_multi\"\n+test_NULL_in_multi \"configset_get_value\" \"my.config\"\n+test_NULL_in_multi \"configset_get_value_multi\" \"my.config\"\n+\n test_expect_success 'find value from a configset' '\n \tcat >config2 <<-\\EOF &&\n \t[case]\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473164","messageId":"kl6lwn3sgjam.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"Re: [PATCH v6 0/9] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-03-08T00:48:17Z","receivedAt":"2023-03-08T00:48:23Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> Range-diff against v5:\n>  1:  cefc4188984 =  1:  43fdb0cf50c config tests: cover blind spots in git_die_config() tests\n>  2:  91a44456327 =  2:  4b0799090c9 config tests: add \"NULL\" tests for *_get_value_multi()\n>  3:  4a73151abde =  3:  62fe2f04e71 config API: add and use a \"git_config_get()\" family of functions\n>  4:  382a77ca69e =  4:  e36303f4d3d versioncmp.c: refactor config reading next commit\n>  5:  8f17bf8150c !  5:  e38523267e7 config API: have *_multi() return an \"int\" and take a \"dest\"\n>     @@ config.c: void git_die_config(const char *key, const char *err, ...)\n>       }\n>      \n>       ## config.h ##\n>     -@@ config.h: int git_configset_add_parameters(struct config_set *cs);\n>     +@@ config.h: int git_configset_add_file(struct config_set *cs, const char *filename);\n>       /**\n>        * Finds and returns the value list, sorted in order of increasing priority\n>        * for the configuration variable `key` and config set `cs`. When the\n>  6:  b515ff13f9b <  -:  ----------- config API: don't lose the git_*get*() return values\n>  7:  8a83c30ea78 =  6:  3a87b35e114 for-each-repo: error on bad --config\n>  8:  d9abc78c2be =  7:  66b7060f66f config API users: test for *_get_value_multi() segfaults\n>  9:  65fa91e7ce7 =  8:  0da4cdb3f6a config API: add \"string\" version of *_value_multi(), fix segfaults\n> 10:  4db3c6d0ed9 =  9:  627eb15a319 for-each-repo: with bad config, don't conflate <path> and <cmd>\n\nI haven't reread the series in its totality yet (I should get to it in\nthe next few days), but a small-ish thing that jumps out from\nthe range-diff is that this version doesn't revert the commit message\nchanges in the previous version (v5 CL [1]) that referred to the ejected\n06/10. I.e. v5 said that we were changing the return values of the\n*_get_*() functions so that the new function is not a special snowflake,\nand those commit messages haven't been changed.\n\n1. https://lore.kernel.org/git/cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com/\n"},{"id":"473172","messageId":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com","subject":"[PATCH v7 0/9] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-08T09:06:42Z","receivedAt":"2023-03-08T09:07:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This series fixes numerous segfaults in config API users, because they\ndidn't expect *_get_multi() to hand them a string_list with a NULL in\nit given config like \"[a] key\" (note, no \"=\"'s).\n\nA larger general overview at v1[1], but note the API changes in\nv2[2]. Changes since v6[3]:\n\n * Glen pointed out that ejecting a commit in v6 orphaned a\n   corresponding forward-reference in a commit message, fix that.\n\n1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/\n2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/\n3. https://lore.kernel.org/git/cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com/\n4. https://lore.kernel.org/git/kl6lwn3sgjam.fsf@chooglen-macbookpro.roam.corp.google.com/\n\nÆvar Arnfjörð Bjarmason (9):\n  config tests: cover blind spots in git_die_config() tests\n  config tests: add \"NULL\" tests for *_get_value_multi()\n  config API: add and use a \"git_config_get()\" family of functions\n  versioncmp.c: refactor config reading next commit\n  config API: have *_multi() return an \"int\" and take a \"dest\"\n  for-each-repo: error on bad --config\n  config API users: test for *_get_value_multi() segfaults\n  config API: add \"string\" version of *_value_multi(), fix segfaults\n  for-each-repo: with bad config, don't conflate <path> and <cmd>\n\n builtin/for-each-repo.c              |  14 ++--\n builtin/gc.c                         |  15 ++--\n builtin/log.c                        |   6 +-\n builtin/submodule--helper.c          |   7 +-\n builtin/worktree.c                   |   3 +-\n config.c                             | 109 ++++++++++++++++++++++-----\n config.h                             |  68 ++++++++++++++---\n pack-bitmap.c                        |   6 +-\n submodule.c                          |   3 +-\n t/helper/test-config.c               |  28 ++++++-\n t/t0068-for-each-repo.sh             |  19 +++++\n t/t1308-config-set.sh                | 108 +++++++++++++++++++++++++-\n t/t3309-notes-merge-auto-resolve.sh  |   7 +-\n t/t4202-log.sh                       |  15 ++++\n t/t5304-prune.sh                     |  12 ++-\n t/t5310-pack-bitmaps.sh              |  20 +++++\n t/t5552-skipping-fetch-negotiator.sh |  16 ++++\n t/t7004-tag.sh                       |  17 +++++\n t/t7413-submodule-is-active.sh       |  16 ++++\n t/t7900-maintenance.sh               |  38 ++++++++++\n versioncmp.c                         |  22 ++++--\n 21 files changed, 477 insertions(+), 72 deletions(-)\n\nRange-diff against v6:\n 1:  43fdb0cf50c =  1:  9f297a35e14 config tests: cover blind spots in git_die_config() tests\n 2:  4b0799090c9 =  2:  45d483066ef config tests: add \"NULL\" tests for *_get_value_multi()\n 3:  62fe2f04e71 !  3:  a977b7b188f config API: add and use a \"git_config_get()\" family of functions\n    @@ Commit message\n         \"int\" instead of \"void\". Let's leave that for now, and focus on\n         the *_get_*() functions.\n     \n    -    In a subsequent commit we'll fix the other *_get_*() functions to so\n    -    that they'll ferry our underlying \"ret\" along, rather than normalizing\n    -    it to a \"return 1\". But as an intermediate step to that we'll need to\n    -    fix git_configset_get_value_multi() to return \"int\", and that change\n    -    itself is smaller because of this change to migrate some callers away\n    -    from the *_value_multi() API.\n    -\n         1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)\n         2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/\n         3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n 4:  e36303f4d3d =  4:  3a5a323cd91 versioncmp.c: refactor config reading next commit\n 5:  e38523267e7 =  5:  dced12a40d2 config API: have *_multi() return an \"int\" and take a \"dest\"\n 6:  3a87b35e114 =  6:  d910f7e3a27 for-each-repo: error on bad --config\n 7:  66b7060f66f =  7:  57db0fcd91f config API users: test for *_get_value_multi() segfaults\n 8:  0da4cdb3f6a =  8:  b374a716555 config API: add \"string\" version of *_value_multi(), fix segfaults\n 9:  627eb15a319 =  9:  6791e1f6f85 for-each-repo: with bad config, don't conflate <path> and <cmd>\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473173","messageId":"patch-v7-1.9-9f297a35e14-20230308T090513Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"[PATCH v7 1/9] config tests: cover blind spots in git_die_config() tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-08T09:06:43Z","receivedAt":"2023-03-08T09:07:51Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"There were no tests checking for the output of the git_die_config()\nfunction in the config API, added in 5a80e97c827 (config: add\n`git_die_config()` to the config-set API, 2014-08-07). We only tested\n\"test_must_fail\", but didn't assert the output.\n\nWe need tests for this because a subsequent commit will alter the\nreturn value of git_config_get_value_multi(), which is used to get the\nconfig values in the git_die_config() function. This test coverage\nhelps to build confidence in that subsequent change.\n\nThese tests cover different interactions with git_die_config():\n\n- The \"notes.mergeStrategy\" test in\n  \"t/t3309-notes-merge-auto-resolve.sh\" is a case where a function\n  outside of config.c (git_config_get_notes_strategy()) calls\n  git_die_config().\n\n- The \"gc.pruneExpire\" test in \"t5304-prune.sh\" is a case where\n  git_config_get_expiry() calls git_die_config(), covering a different\n  \"type\" than the \"string\" test for \"notes.mergeStrategy\".\n\n- The \"fetch.negotiationAlgorithm\" test in\n  \"t/t5552-skipping-fetch-negotiator.sh\" is a case where\n  git_config_get_string*() calls git_die_config().\n\nWe also cover both the \"from command-line config\" and \"in file..at\nline\" cases here.\n\nThe clobbering of existing \".git/config\" files here is so that we're\nnot implicitly testing the line count of the default config.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-\n t/t5304-prune.sh                     | 12 ++++++++++--\n t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++\n 3 files changed, 32 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh\nindex 141d3e4ca4d..9bd5dbf341f 100755\n--- a/t/t3309-notes-merge-auto-resolve.sh\n+++ b/t/t3309-notes-merge-auto-resolve.sh\n@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '\n \n test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '\n \tgit config core.notesRef refs/notes/y &&\n-\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z &&\n+\tcat >expect <<-\\EOF &&\n+\terror: unknown notes merge strategy foo\n+\tfatal: unable to parse '\\''notes.mergeStrategy'\\'' from command-line config\n+\tEOF\n+\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z 2>actual &&\n+\ttest_cmp expect actual &&\n \t# Verify no changes (y)\n \tverify_notes y y\n '\ndiff --git a/t/t5304-prune.sh b/t/t5304-prune.sh\nindex d65a5f94b4b..5500dd08426 100755\n--- a/t/t5304-prune.sh\n+++ b/t/t5304-prune.sh\n@@ -72,8 +72,16 @@ test_expect_success 'gc: implicit prune --expire' '\n '\n \n test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '\n-\tgit config gc.pruneExpire invalid &&\n-\ttest_must_fail git gc\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t>repo/.git/config &&\n+\tgit -C repo config gc.pruneExpire invalid &&\n+\tcat >expect <<-\\EOF &&\n+\terror: Invalid gc.pruneexpire: '\\''invalid'\\''\n+\tfatal: bad config variable '\\''gc.pruneexpire'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_must_fail git -C repo gc 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'gc: start with ok gc.pruneExpire' '\ndiff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh\nindex 165427d57e5..b55a9f65e6b 100755\n--- a/t/t5552-skipping-fetch-negotiator.sh\n+++ b/t/t5552-skipping-fetch-negotiator.sh\n@@ -3,6 +3,22 @@\n test_description='test skipping fetch negotiator'\n . ./test-lib.sh\n \n+test_expect_success 'fetch.negotiationalgorithm config' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\tcat >repo/.git/config <<-\\EOF &&\n+\t[fetch]\n+\tnegotiationAlgorithm\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''fetch.negotiationalgorithm'\\''\n+\tfatal: bad config variable '\\''fetch.negotiationalgorithm'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_expect_code 128 git -C repo fetch >out 2>actual &&\n+\ttest_must_be_empty out &&\n+\ttest_cmp expect actual\n+'\n+\n have_sent () {\n \twhile test \"$#\" -ne 0\n \tdo\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473174","messageId":"patch-v7-2.9-45d483066ef-20230308T090513Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"[PATCH v7 2/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-08T09:06:44Z","receivedAt":"2023-03-08T09:07:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"A less well known edge case in the config format is that keys can be\nvalue-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\nare equivalent as far as \"--type=bool\" is concerned:\n\n\t[a]key\n\t[a]key = true\n\nBut as far as our parser is concerned the values for these two are\nNULL, and \"true\". I.e. for a sequence like:\n\n\t[a]key=x\n\t[a]key\n\t[a]key=y\n\nWe get a \"struct string_list\" with \"string\" members with \".string\"\nvalues of:\n\n\t{ \"x\", NULL, \"y\" }\n\nThis behavior goes back to the initial implementation of\ngit_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n2005-10-10).\n\nWhen parts of the config_set API were tested for in [1] they didn't\nadd coverage for 3/4 of the \"(NULL)\" cases handled in\n\"t/helper/test-config.c\". We'd test that case for \"get_value\", but not\n\"get_value_multi\", \"configset_get_value\" and\n\"configset_get_value_multi\".\n\nWe now cover all of those cases, which in turn expose the details of\nhow this part of the config API works.\n\n1. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t1308-config-set.sh | 65 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 65 insertions(+)\n\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex b38e158d3b2..4be1ab1147c 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -146,6 +146,71 @@ test_expect_success 'find multiple values' '\n \tcheck_config get_value_multi case.baz sam bat hask\n '\n \n+test_NULL_in_multi () {\n+\tlocal op=\"$1\" &&\n+\tlocal file=\"$2\" &&\n+\n+\ttest_expect_success \"$op: NULL value in config${file:+ in $file}\" '\n+\t\tconfig=\"$file\" &&\n+\t\tif test -z \"$config\"\n+\t\tthen\n+\t\t\tconfig=.git/config &&\n+\t\t\ttest_when_finished \"mv $config.old $config\" &&\n+\t\t\tmv \"$config\" \"$config\".old\n+\t\tfi &&\n+\n+\t\t# Value-less in the middle of a list\n+\t\tcat >\"$config\" <<-\\EOF &&\n+\t\t[a]key=x\n+\t\t[a]key\n+\t\t[a]key=y\n+\t\tEOF\n+\t\tcase \"$op\" in\n+\t\t*_multi)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\tx\n+\t\t\t(NULL)\n+\t\t\ty\n+\t\t\tEOF\n+\t\t\t;;\n+\t\t*)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\ty\n+\t\t\tEOF\n+\t\t\t;;\n+\t\tesac &&\n+\t\ttest-tool config \"$op\" a.key $file >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# Value-less at the end of a least\n+\t\tcat >\"$config\" <<-\\EOF &&\n+\t\t[a]key=x\n+\t\t[a]key=y\n+\t\t[a]key\n+\t\tEOF\n+\t\tcase \"$op\" in\n+\t\t*_multi)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\tx\n+\t\t\ty\n+\t\t\t(NULL)\n+\t\t\tEOF\n+\t\t\t;;\n+\t\t*)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\t(NULL)\n+\t\t\tEOF\n+\t\t\t;;\n+\t\tesac &&\n+\t\ttest-tool config \"$op\" a.key $file >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+}\n+\n+test_NULL_in_multi \"get_value_multi\"\n+test_NULL_in_multi \"configset_get_value\" \"my.config\"\n+test_NULL_in_multi \"configset_get_value_multi\" \"my.config\"\n+\n test_expect_success 'find value from a configset' '\n \tcat >config2 <<-\\EOF &&\n \t[case]\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473175","messageId":"patch-v7-3.9-a977b7b188f-20230308T090513Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"[PATCH v7 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-08T09:06:45Z","receivedAt":"2023-03-08T09:08:05Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"We already have the basic \"git_config_get_value()\" function and its\n\"repo_*\" and \"configset\" siblings to get a given \"key\" and assign the\nlast key found to a provided \"value\".\n\nBut some callers don't care about that value, but just want to use the\nreturn value of the \"get_value()\" function to check whether the key\nexist (or another non-zero return value).\n\nThe immediate motivation for this is that a subsequent commit will\nneed to change all callers of the \"*_get_value_multi()\" family of\nfunctions. In two cases here we (ab)used it to check whether we had\nany values for the given key, but didn't care about the return value.\n\nThe rest of the callers here used various other config API functions\nto do the same, all of which resolved to the same underlying functions\nto provide the answer.\n\nSome of these were using either git_config_get_string() or\ngit_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a\nconfigure_added_submodule() leak, 2022-09-01) for a recent example. We\ncan now use a helper function that doesn't require a throwaway\nvariable.\n\nWe could have changed git_configset_get_value_multi() (and then\ngit_config_get_value() etc.) to accept a \"NULL\" as a \"dest\" for all\ncallers, but let's avoid changing the behavior of existing API\nusers. Having an \"unused\" value that we throw away internal to\nconfig.c is cheap.\n\nA \"NULL as optional dest\" pattern is also more fragile, as the intent\nof the caller might be misinterpreted if he were to accidentally pass\n\"NULL\", e.g. when \"dest\" is passed in from another function.\n\nAnother name for this function could have been\n\"*_config_key_exists()\", as suggested in [1]. That would work for all\nof these callers, and would currently be equivalent to this function,\nas the git_configset_get_value() API normalizes all non-zero return\nvalues to a \"1\".\n\nBut adding that API would set us up to lose information, as e.g. if\ngit_config_parse_key() in the underlying configset_find_element()\nfails we'd like to return -1, not 1.\n\nLet's change the underlying configset_find_element() function to\nsupport this use-case, we'll make further use of it in a subsequent\ncommit where the git_configset_get_value_multi() function itself will\nexpose this new return value.\n\nThis still leaves various inconsistencies and clobbering or ignoring\nof the return value in place. E.g here we're modifying\nconfigset_add_value(), but ever since it was added in [2] we've been\nignoring its \"int\" return value, but as we're changing the\nconfigset_find_element() it uses, let's have it faithfully ferry that\n\"ret\" along.\n\nLet's also use the \"RESULT_MUST_BE_USED\" macro introduced in [3] to\nassert that we're checking the return value of\nconfigset_find_element().\n\nWe're leaving the same change to configset_add_value() for some future\nseries. Once we start paying attention to its return value we'd need\nto ferry it up as deep as do_config_from(), and would need to make\nleast read_{,very_}early_config() and git_protected_config() return an\n\"int\" instead of \"void\". Let's leave that for now, and focus on\nthe *_get_*() functions.\n\n1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)\n2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/\n3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n   return values, 2022-09-01),\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                |  5 +---\n builtin/submodule--helper.c |  7 +++--\n builtin/worktree.c          |  3 +--\n config.c                    | 51 ++++++++++++++++++++++++++++++++-----\n config.h                    | 18 +++++++++++++\n t/helper/test-config.c      | 22 ++++++++++++++++\n t/t1308-config-set.sh       | 43 ++++++++++++++++++++++++++++++-\n 7 files changed, 131 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 02455fdcd73..e38d1783f30 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1493,7 +1493,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \t};\n \tint found = 0;\n \tconst char *key = \"maintenance.repo\";\n-\tchar *config_value;\n \tchar *maintpath = get_maintpath();\n \tstruct string_list_item *item;\n \tconst struct string_list *list;\n@@ -1508,9 +1507,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tgit_config_set(\"maintenance.auto\", \"false\");\n \n \t/* Set maintenance strategy, if unset */\n-\tif (!git_config_get_string(\"maintenance.strategy\", &config_value))\n-\t\tfree(config_value);\n-\telse\n+\tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n \tlist = git_config_get_value_multi(key);\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 4c173d8b37a..2278e8c91cb 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t * If there are no path args and submodule.active is set then,\n \t * by default, only initialize 'active' modules.\n \t */\n-\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\tif (!argc && !git_config_get(\"submodule.active\"))\n \t\tmodule_list_active(&list);\n \n \tinfo.prefix = prefix;\n@@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \t\t * If there are no path args and submodule.active is set then,\n \t\t * by default, only initialize 'active' modules.\n \t\t */\n-\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\t\tif (!argc && !git_config_get(\"submodule.active\"))\n \t\t\tmodule_list_active(&list);\n \n \t\tinfo.prefix = opt.prefix;\n@@ -3140,7 +3140,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con\n static void configure_added_submodule(struct add_data *add_data)\n {\n \tchar *key;\n-\tconst char *val;\n \tstruct child_process add_submod = CHILD_PROCESS_INIT;\n \tstruct child_process add_gitmodules = CHILD_PROCESS_INIT;\n \n@@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)\n \t * is_submodule_active(), since that function needs to find\n \t * out the value of \"submodule.active\" again anyway.\n \t */\n-\tif (!git_config_get_string_tmp(\"submodule.active\", &val)) {\n+\tif (!git_config_get(\"submodule.active\")) {\n \t\t/*\n \t\t * If the submodule being added isn't already covered by the\n \t\t * current configured pathspec, set the submodule's active flag\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 254283aa6f5..2d81965711f 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -319,7 +319,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \n \tif (file_exists(from_file)) {\n \t\tstruct config_set cs = { { 0 } };\n-\t\tconst char *core_worktree;\n \t\tint bare;\n \n \t\tif (safe_create_leading_directories(to_file) ||\n@@ -338,7 +337,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \t\t\t\tto_file, \"core.bare\", NULL, \"true\", 0))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\n \t\t\t\t\"core.bare\", to_file);\n-\t\tif (!git_configset_get_value(&cs, \"core.worktree\", &core_worktree) &&\n+\t\tif (!git_configset_get(&cs, \"core.worktree\") &&\n \t\t\tgit_config_set_in_file_gently(to_file,\n \t\t\t\t\t\t\t\"core.worktree\", NULL))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\ndiff --git a/config.c b/config.c\nindex 00090a32fc3..d4f0e4fd619 100644\n--- a/config.c\n+++ b/config.c\n@@ -2289,23 +2289,29 @@ void read_very_early_config(config_fn_t cb, void *data)\n \tconfig_with_options(cb, data, NULL, &opts);\n }\n \n-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n+RESULT_MUST_BE_USED\n+static int configset_find_element(struct config_set *cs, const char *key,\n+\t\t\t\t  struct config_set_element **dest)\n {\n \tstruct config_set_element k;\n \tstruct config_set_element *found_entry;\n \tchar *normalized_key;\n+\tint ret;\n+\n \t/*\n \t * `key` may come from the user, so normalize it before using it\n \t * for querying entries from the hashmap.\n \t */\n-\tif (git_config_parse_key(key, &normalized_key, NULL))\n-\t\treturn NULL;\n+\tret = git_config_parse_key(key, &normalized_key, NULL);\n+\tif (ret)\n+\t\treturn ret;\n \n \thashmap_entry_init(&k.ent, strhash(normalized_key));\n \tk.key = normalized_key;\n \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n \tfree(normalized_key);\n-\treturn found_entry;\n+\t*dest = found_entry;\n+\treturn 0;\n }\n \n static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n@@ -2314,8 +2320,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n \tstruct string_list_item *si;\n \tstruct configset_list_item *l_item;\n \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n+\tint ret;\n \n-\te = configset_find_element(cs, key);\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret)\n+\t\treturn ret;\n \t/*\n \t * Since the keys are being fed by git_config*() callback mechanism, they\n \t * are already normalized. So simply add them without any further munging.\n@@ -2425,8 +2434,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *\n \n const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n {\n-\tstruct config_set_element *e = configset_find_element(cs, key);\n-\treturn e ? &e->value_list : NULL;\n+\tstruct config_set_element *e;\n+\n+\tif (configset_find_element(cs, key, &e))\n+\t\treturn NULL;\n+\telse if (!e)\n+\t\treturn NULL;\n+\treturn &e->value_list;\n+}\n+\n+int git_configset_get(struct config_set *cs, const char *key)\n+{\n+\tstruct config_set_element *e;\n+\tint ret;\n+\n+\tif ((ret = configset_find_element(cs, key, &e)))\n+\t\treturn ret;\n+\telse if (!e)\n+\t\treturn 1;\n+\treturn 0;\n }\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n@@ -2565,6 +2591,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)\n \tconfigset_iter(repo->config, fn, data);\n }\n \n+int repo_config_get(struct repository *repo, const char *key)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get(repo->config, key);\n+}\n+\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value)\n {\n@@ -2679,6 +2711,11 @@ void git_config_clear(void)\n \trepo_config_clear(the_repository);\n }\n \n+int git_config_get(const char *key)\n+{\n+\treturn repo_config_get(the_repository, key);\n+}\n+\n int git_config_get_value(const char *key, const char **value)\n {\n \treturn repo_config_get_value(the_repository, key, value);\ndiff --git a/config.h b/config.h\nindex 7606246531a..7dd62ca81bf 100644\n--- a/config.h\n+++ b/config.h\n@@ -465,6 +465,9 @@ void git_configset_clear(struct config_set *cs);\n  * value in the 'dest' pointer.\n  */\n \n+RESULT_MUST_BE_USED\n+int git_configset_get(struct config_set *cs, const char *key);\n+\n /*\n  * Finds the highest-priority value for the configuration variable `key`\n  * and config set `cs`, stores the pointer to it in `value` and returns 0.\n@@ -485,6 +488,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n /* Functions for reading a repository's config */\n struct repository;\n void repo_config(struct repository *repo, config_fn_t fn, void *data);\n+\n+/**\n+ * Run only the discover part of the repo_config_get_*() functions\n+ * below, in addition to 1 if not found, returns negative values on\n+ * error (e.g. if the key itself is invalid).\n+ */\n+RESULT_MUST_BE_USED\n+int repo_config_get(struct repository *repo, const char *key);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n const struct string_list *repo_config_get_value_multi(struct repository *repo,\n@@ -521,8 +532,15 @@ void git_protected_config(config_fn_t fn, void *data);\n  * manner, the config API provides two functions `git_config_get_value`\n  * and `git_config_get_value_multi`. They both read values from an internal\n  * cache generated previously from reading the config files.\n+ *\n+ * For those git_config_get*() functions that aren't documented,\n+ * consult the corresponding repo_config_get*() function's\n+ * documentation.\n  */\n \n+RESULT_MUST_BE_USED\n+int git_config_get(const char *key);\n+\n /**\n  * Finds the highest-priority value for the configuration variable `key`,\n  * stores the pointer to it in `value` and returns 0. When the\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex 4ba9eb65606..cbb33ae1fff 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -14,6 +14,8 @@\n  * get_value_multi -> prints all values for the entered key in increasing order\n  *\t\t     of priority\n  *\n+ * get -> print return value for the entered key\n+ *\n  * get_int -> print integer value for the entered key or die\n  *\n  * get_bool -> print bool value for the entered key or die\n@@ -109,6 +111,26 @@ int cmd__config(int argc, const char **argv)\n \t\t\tprintf(\"Value not found for \\\"%s\\\"\\n\", argv[2]);\n \t\t\tgoto exit1;\n \t\t}\n+\t} else if (argc == 3 && !strcmp(argv[1], \"get\")) {\n+\t\tint ret;\n+\n+\t\tif (!(ret = git_config_get(argv[2])))\n+\t\t\tgoto exit0;\n+\t\telse if (ret == 1)\n+\t\t\tprintf(\"Value not found for \\\"%s\\\"\\n\", argv[2]);\n+\t\telse if (ret == -CONFIG_INVALID_KEY)\n+\t\t\tprintf(\"Key \\\"%s\\\" is invalid\\n\", argv[2]);\n+\t\telse if (ret == -CONFIG_NO_SECTION_OR_NAME)\n+\t\t\tprintf(\"Key \\\"%s\\\" has no section\\n\", argv[2]);\n+\t\telse\n+\t\t\t/*\n+\t\t\t * A normal caller should just check \"ret <\n+\t\t\t * 0\", but for our own tests let's BUG() if\n+\t\t\t * our whitelist of git_config_parse_key()\n+\t\t\t * return values isn't exhaustive.\n+\t\t\t */\n+\t\t\tBUG(\"Key \\\"%s\\\" has unknown return %d\", argv[2], ret);\n+\t\tgoto exit1;\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_int\")) {\n \t\tif (!git_config_get_int(argv[2], &val)) {\n \t\t\tprintf(\"%d\\n\", val);\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex 4be1ab1147c..7def7053e1c 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -58,6 +58,8 @@ test_expect_success 'setup default config' '\n \t\tskin = false\n \t\tnose = 1\n \t\thorns\n+\t[value]\n+\t\tless\n \tEOF\n '\n \n@@ -116,6 +118,45 @@ test_expect_success 'find value with the highest priority' '\n \tcheck_config get_value case.baz \"hask\"\n '\n \n+test_expect_success 'return value for an existing key' '\n+\ttest-tool config get lamb.chop >out 2>err &&\n+\ttest_must_be_empty out &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for value-less key' '\n+\ttest-tool config get value.less >out 2>err &&\n+\ttest_must_be_empty out &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for a missing key' '\n+\tcat >expect <<-\\EOF &&\n+\tValue not found for \"missing.key\"\n+\tEOF\n+\ttest_expect_code 1 test-tool config get missing.key >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '\n+\tcat >expect <<-\\EOF &&\n+\tKey \"fails.iskeychar.-\" is invalid\n+\tEOF\n+\ttest_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '\n+\tcat >expect <<-\\EOF &&\n+\tKey \"keynosection\" has no section\n+\tEOF\n+\ttest_expect_code 1 test-tool config get keynosection >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty out\n+'\n+\n test_expect_success 'find integer value for a key' '\n \tcheck_config get_int lamb.chop 65\n '\n@@ -272,7 +313,7 @@ test_expect_success 'proper error on error in default config files' '\n \tcp .git/config .git/config.old &&\n \ttest_when_finished \"mv .git/config.old .git/config\" &&\n \techo \"[\" >>.git/config &&\n-\techo \"fatal: bad config line 34 in file .git/config\" >expect &&\n+\techo \"fatal: bad config line 36 in file .git/config\" >expect &&\n \ttest_expect_code 128 test-tool config get_value foo.bar 2>actual &&\n \ttest_cmp expect actual\n '\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473176","messageId":"patch-v7-4.9-3a5a323cd91-20230308T090513Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"[PATCH v7 4/9] versioncmp.c: refactor config reading next commit","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-08T09:06:46Z","receivedAt":"2023-03-08T09:08:06Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the reading of the versionSort.suffix and\nversionSort.prereleaseSuffix configuration variables to stay within\nthe bounds of our CodingGuidelines when it comes to line length, and\nto avoid repeating ourselves.\n\nRenaming \"deprecated_prereleases\" to \"oldl\" doesn't help us to avoid\nline wrapping now, but it will in a subsequent commit.\n\nLet's also split out the names of the config variables into variables\nof our own, and refactor the nested if/else to avoid indenting it, and\nthe existing bracing style issue.\n\nThis all helps with the subsequent commit, where we'll need to start\nchecking different git_config_get_value_multi() return value. See\nc026557a373 (versioncmp: generalize version sort suffix reordering,\n2016-12-08) for the original implementation of most of this.\n\nMoving the \"initialized = 1\" assignment allows us to move some of this\nto the variable declarations in the subsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n versioncmp.c | 19 +++++++++++--------\n 1 file changed, 11 insertions(+), 8 deletions(-)\n\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 069ee94a4d7..323f5d35ea8 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)\n \t}\n \n \tif (!initialized) {\n-\t\tconst struct string_list *deprecated_prereleases;\n+\t\tconst char *const newk = \"versionsort.suffix\";\n+\t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *oldl;\n+\n+\t\tprereleases = git_config_get_value_multi(newk);\n+\t\toldl = git_config_get_value_multi(oldk);\n+\t\tif (prereleases && oldl)\n+\t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n+\t\telse if (!prereleases)\n+\t\t\tprereleases = oldl;\n+\n \t\tinitialized = 1;\n-\t\tprereleases = git_config_get_value_multi(\"versionsort.suffix\");\n-\t\tdeprecated_prereleases = git_config_get_value_multi(\"versionsort.prereleasesuffix\");\n-\t\tif (prereleases) {\n-\t\t\tif (deprecated_prereleases)\n-\t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n-\t\t} else\n-\t\t\tprereleases = deprecated_prereleases;\n \t}\n \tif (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,\n \t\t\t\t\t    &diff))\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473177","messageId":"patch-v7-7.9-57db0fcd91f-20230308T090513Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"[PATCH v7 7/9] config API users: test for *_get_value_multi() segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-08T09:06:49Z","receivedAt":"2023-03-08T09:08:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As we'll discuss in the subsequent commit these tests all\nshow *_get_value_multi() API users unable to handle there being a\nvalue-less key in the config, which is represented with a \"NULL\" for\nthat entry in the \"string\" member of the returned \"struct\nstring_list\", causing a segfault.\n\nThese added tests exhaustively test for that issue, as we'll see in a\nsubsequent commit we'll need to change all of the API users\nof *_get_value_multi(). These cases were discovered by triggering each\none individually, and then adding these tests.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t4202-log.sh                 | 11 +++++++++++\n t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++\n t/t7004-tag.sh                 | 12 ++++++++++++\n t/t7413-submodule-is-active.sh | 12 ++++++++++++\n t/t7900-maintenance.sh         | 23 +++++++++++++++++++++++\n 5 files changed, 74 insertions(+)\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 2ce2b41174d..e4f02d8208b 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n+test_expect_failure 'parse log.excludeDecoration with no value' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[log]\n+\t\texcludeDecoration\n+\tEOF\n+\tgit log --decorate=short\n+'\n+\n test_expect_success 'decorate-refs with glob' '\n \tcat >expect.decorate <<-\\EOF &&\n \tMerge-tag-reach\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 7d8dee41b0d..0306b399188 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,6 +404,22 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n+\ttest_expect_failure 'pack.preferBitmapTips' '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -rf repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\tgit config pack.writeBitmapLookupTable '\"$writeLookupTable\"' &&\n+\t\t\ttest_commit_bulk --message=\"%s\" 103 &&\n+\n+\t\t\tcat >>.git/config <<-\\EOF &&\n+\t\t\t[pack]\n+\t\t\t\tpreferBitmapTips\n+\t\t\tEOF\n+\t\t\tgit repack -adb\n+\t\t)\n+\t'\n+\n \ttest_expect_success 'complains about multiple pack bitmaps' '\n \t\trm -fr repo &&\n \t\tgit init repo &&\ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex 9aa1660651b..f343551a7d4 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[versionsort]\n+\t\tprereleaseSuffix\n+\t\tsuffix\n+\tEOF\n+\tgit tag -l --sort=version:refname\n+'\n+\n test_expect_success 'version sort with prerelease reordering' '\n \ttest_config versionsort.prereleaseSuffix -rc &&\n \tgit tag foo1.6-rc1 &&\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex 7cdc2637649..bfe27e50732 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n+test_expect_failure 'is-active handles submodule.active config missing a value' '\n+\tcp super/.git/config super/.git/config.orig &&\n+\ttest_when_finished mv super/.git/config.orig super/.git/config &&\n+\n+\tcat >>super/.git/config <<-\\EOF &&\n+\t[submodule]\n+\t\tactive\n+\tEOF\n+\n+\ttest-tool -C super submodule is-active sub1\n+'\n+\n test_expect_success 'is-active works with basic submodule.active config' '\n \ttest_when_finished \"git -C super config submodule.sub1.URL ../sub\" &&\n \ttest_when_finished \"git -C super config --unset-all submodule.active\" &&\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 823331e44a0..d82eac6a471 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,6 +524,29 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n+test_expect_failure 'register with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance register\n+'\n+\n+test_expect_failure 'unregister with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance unregister &&\n+\tgit maintenance unregister --force\n+'\n+\n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \tMETA=\"a+b*c\" &&\n \tgit init \"$META\" &&\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473178","messageId":"patch-v7-6.9-d910f7e3a27-20230308T090513Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"[PATCH v7 6/9] for-each-repo: error on bad --config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-08T09:06:48Z","receivedAt":"2023-03-08T09:08:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in 6c62f015520 (for-each-repo: do nothing on empty config,\n2021-01-08) this command wants to ignore a non-existing config key,\nbut let's not conflate that with bad config.\n\nBefore this, all these added tests would pass with an exit code of 0.\n\nWe could preserve the comment added in 6c62f015520, but now that we're\ndirectly using the documented repo_config_get_value_multi() value it's\njust narrating something that should be obvious from the API use, so\nlet's drop it.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  | 11 ++++++-----\n t/t0068-for-each-repo.sh |  6 ++++++\n 2 files changed, 12 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex fd0e7739e6a..224164addb3 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tstatic const char *config_key = NULL;\n \tint i, result = 0;\n \tconst struct string_list *values;\n+\tint err;\n \n \tconst struct option options[] = {\n \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n@@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\t/*\n-\t * Do nothing on an empty list, which is equivalent to the case\n-\t * where the config variable does not exist at all.\n-\t */\n-\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n+\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\tif (err < 0)\n+\t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n+\t\t\t       for_each_repo_usage, options, config_key);\n+\telse if (err)\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 3648d439a87..6b51e00da0e 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '\n \tgit for-each-repo --config=bogus.config -- help --no-such-option\n '\n \n+test_expect_success 'error on bad config keys' '\n+\ttest_expect_code 129 git for-each-repo --config=a &&\n+\ttest_expect_code 129 git for-each-repo --config=a.b. &&\n+\ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n+'\n+\n test_done\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473179","messageId":"patch-v7-8.9-b374a716555-20230308T090513Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"[PATCH v7 8/9] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-08T09:06:50Z","receivedAt":"2023-03-08T09:08:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix numerous and mostly long-standing segfaults in consumers of\nthe *_config_*value_multi() API. As discussed in the preceding commit\nan empty key in the config syntax yields a \"NULL\" string, which these\nusers would give to strcmp() (or similar), resulting in segfaults.\n\nAs this change shows, most users users of the *_config_*value_multi()\nAPI didn't really want such an an unsafe and low-level API, let's give\nthem something with the safety of git_config_get_string() instead.\n\nThis fix is similar to what the *_string() functions and others\nacquired in[1] and [2]. Namely introducing and using a safer\n\"*_get_string_multi()\" variant of the low-level \"_*value_multi()\"\nfunction.\n\nThis fixes segfaults in code introduced in:\n\n  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)\n  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)\n  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)\n  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)\n  - 92156291ca8 (log: add default decoration filter, 2022-08-05)\n  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)\n\nThere are now two users ofthe low-level API:\n\n- One in \"builtin/for-each-repo.c\", which we'll convert in a\n  subsequent commit.\n\n- The \"t/helper/test-config.c\" code added in [3].\n\nAs seen in the preceding commit we need to give the\n\"t/helper/test-config.c\" caller these \"NULL\" entries.\n\nWe could also alter the underlying git_configset_get_value_multi()\nfunction to be \"string safe\", but doing so would leave no room for\nother variants of \"*_get_value_multi()\" that coerce to other types.\n\nSuch coercion can't be built on the string version, since as we've\nestablished \"NULL\" is a true value in the boolean context, but if we\ncoerced it to \"\" for use in a list of strings it'll be subsequently\ncoerced to \"false\" as a boolean.\n\nThe callback pattern being used here will make it easy to introduce\ne.g. a \"multi\" variant which coerces its values to \"bool\", \"int\",\n\"path\" etc.\n\n1. 40ea4ed9032 (Add config_error_nonbool() helper function,\n   2008-02-11)\n2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,\n   2008-02-11).\n3. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                   |  6 +++---\n builtin/log.c                  |  4 ++--\n config.c                       | 32 ++++++++++++++++++++++++++++++++\n config.h                       | 19 +++++++++++++++++++\n pack-bitmap.c                  |  2 +-\n submodule.c                    |  2 +-\n t/t4202-log.sh                 |  8 ++++++--\n t/t5310-pack-bitmaps.sh        |  8 ++++++--\n t/t7004-tag.sh                 |  9 +++++++--\n t/t7413-submodule-is-active.sh |  8 ++++++--\n t/t7900-maintenance.sh         | 25 ++++++++++++++++++++-----\n versioncmp.c                   |  4 ++--\n 12 files changed, 105 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 2b3da377d52..9497bdf23e4 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1510,7 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tif (!git_config_get_value_multi(key, &list)) {\n+\tif (!git_config_get_string_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1578,8 +1578,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tgit_configset_add_file(&cs, config_file);\n \t}\n \tif (!(config_file\n-\t      ? git_configset_get_value_multi(&cs, key, &list)\n-\t      : git_config_get_value_multi(key, &list))) {\n+\t      ? git_configset_get_string_multi(&cs, key, &list)\n+\t      : git_config_get_string_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex e43f6f9d8c1..ca847524fa4 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n \tconst struct string_list *config_exclude;\n \n-\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n-\t\t\t\t\t&config_exclude)) {\n+\tif (!git_config_get_string_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t &config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex 569819b4a1b..c63034fb78b 100644\n--- a/config.c\n+++ b/config.c\n@@ -2448,6 +2448,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \treturn 0;\n }\n \n+static int check_multi_string(struct string_list_item *item, void *util)\n+{\n+\treturn item->string ? 0 : config_error_nonbool(util);\n+}\n+\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest)\n+{\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value_multi(cs, key, dest)))\n+\t\treturn ret;\n+\tif ((ret = for_each_string_list((struct string_list *)*dest,\n+\t\t\t\t\tcheck_multi_string, (void *)key)))\n+\t\treturn ret;\n+\n+\treturn 0;\n+}\n+\n int git_configset_get(struct config_set *cs, const char *key)\n {\n \tstruct config_set_element *e;\n@@ -2616,6 +2635,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,\n \treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_string_multi(repo->config, key, dest);\n+}\n+\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest)\n {\n@@ -2731,6 +2757,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)\n \treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest)\n+{\n+\treturn repo_config_get_string_multi(the_repository, key, dest);\n+}\n+\n int git_config_get_string(const char *key, char **dest)\n {\n \treturn repo_config_get_string(the_repository, key, dest);\ndiff --git a/config.h b/config.h\nindex 4db6b90ac20..5f258e5b8df 100644\n--- a/config.h\n+++ b/config.h\n@@ -463,6 +463,19 @@ RESULT_MUST_BE_USED\n int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \t\t\t\t  const struct string_list **dest);\n \n+/**\n+ * A validation wrapper for git_configset_get_value_multi() which does\n+ * for it what git_configset_get_string() does for\n+ * git_configset_get_value().\n+ *\n+ * The configuration syntax allows for \"[section] key\", which will\n+ * give us a NULL entry in the \"struct string_list\", as opposed to\n+ * \"[section] key =\" which is the empty string. Most users of the API\n+ * are not prepared to handle NULL in a \"struct string_list\".\n+ */\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest);\n+\n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n  */\n@@ -509,6 +522,9 @@ int repo_config_get_value(struct repository *repo,\n RESULT_MUST_BE_USED\n int repo_config_get_value_multi(struct repository *repo, const char *key,\n \t\t\t\tconst struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -570,6 +586,9 @@ int git_config_get_value(const char *key, const char **value);\n RESULT_MUST_BE_USED\n int git_config_get_value_multi(const char *key,\n \t\t\t       const struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 15c5eb507c0..d003c7e60b4 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2316,7 +2316,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n \tconst struct string_list *dest;\n \n-\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\tif (!repo_config_get_string_multi(r, \"pack.preferbitmaptips\", &dest))\n \t\treturn dest;\n \treturn NULL;\n }\ndiff --git a/submodule.c b/submodule.c\nindex 4b6f5223b0c..30a103246ec 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n+\tif (!repo_config_get_string_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex e4f02d8208b..ae73aef922f 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n-test_expect_failure 'parse log.excludeDecoration with no value' '\n+test_expect_success 'parse log.excludeDecoration with no value' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '\n \t[log]\n \t\texcludeDecoration\n \tEOF\n-\tgit log --decorate=short\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''log.excludeDecoration'\\''\n+\tEOF\n+\tgit log --decorate=short 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'decorate-refs with glob' '\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 0306b399188..526a5a506eb 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,7 +404,7 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n-\ttest_expect_failure 'pack.preferBitmapTips' '\n+\ttest_expect_success 'pack.preferBitmapTips' '\n \t\tgit init repo &&\n \t\ttest_when_finished \"rm -rf repo\" &&\n \t\t(\n@@ -416,7 +416,11 @@ test_bitmap_cases () {\n \t\t\t[pack]\n \t\t\t\tpreferBitmapTips\n \t\t\tEOF\n-\t\t\tgit repack -adb\n+\t\t\tcat >expect <<-\\EOF &&\n+\t\t\terror: missing value for '\\''pack.preferbitmaptips'\\''\n+\t\t\tEOF\n+\t\t\tgit repack -adb 2>actual &&\n+\t\t\ttest_cmp expect actual\n \t\t)\n \t'\n \ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex f343551a7d4..f4a31ada79a 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas\n \t\tprereleaseSuffix\n \t\tsuffix\n \tEOF\n-\tgit tag -l --sort=version:refname\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''versionsort.suffix'\\''\n+\terror: missing value for '\\''versionsort.prereleasesuffix'\\''\n+\tEOF\n+\tgit tag -l --sort=version:refname 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'version sort with prerelease reordering' '\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex bfe27e50732..887d181b72e 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n-test_expect_failure 'is-active handles submodule.active config missing a value' '\n+test_expect_success 'is-active handles submodule.active config missing a value' '\n \tcp super/.git/config super/.git/config.orig &&\n \ttest_when_finished mv super/.git/config.orig super/.git/config &&\n \n@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'\n \t\tactive\n \tEOF\n \n-\ttest-tool -C super submodule is-active sub1\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''submodule.active'\\''\n+\tEOF\n+\ttest-tool -C super submodule is-active sub1 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'is-active works with basic submodule.active config' '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d82eac6a471..487e326b3fa 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n-test_expect_failure 'register with no value for maintenance.repo' '\n+test_expect_success 'register with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance register\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\tgit maintenance register 2>actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n-test_expect_failure 'unregister with no value for maintenance.repo' '\n+test_expect_success 'unregister with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -543,8 +548,18 @@ test_expect_failure 'unregister with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance unregister &&\n-\tgit maintenance unregister --force\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\ttest_expect_code 128 git maintenance unregister 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo &&\n+\n+\tgit maintenance unregister --force 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 60c3a517122..7498da96e0e 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n \t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n-\t\tint new = git_config_get_value_multi(newk, &newl);\n-\t\tint old = git_config_get_value_multi(oldk, &oldl);\n+\t\tint new = git_config_get_string_multi(newk, &newl);\n+\t\tint old = git_config_get_string_multi(oldk, &oldl);\n \n \t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473180","messageId":"patch-v7-9.9-6791e1f6f85-20230308T090513Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"[PATCH v7 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-08T09:06:51Z","receivedAt":"2023-03-08T09:08:21Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on\nconfigured repos, 2020-09-11). Due to assuming that elements returned\nfrom the repo_config_get_value_multi() call wouldn't be \"NULL\" we'd\nconflate the <path> and <command> part of the argument list when\nrunning commands.\n\nAs noted in the preceding commit the fix is to move to a safer\n\"*_string_multi()\" version of the *_multi() API. This change is\nseparated from the rest because those all segfaulted. In this change\nwe ended up with different behavior.\n\nWhen using the \"--config=<config>\" form we take each element of the\nlist as a path to a repository. E.g. with a configuration like:\n\n\t[repo] list = /some/repo\n\nWe would, with this command:\n\n\tgit for-each-repo --config=repo.list status builtin\n\nRun a \"git status\" in /some/repo, as:\n\n\tgit -C /some/repo status builtin\n\nI.e. ask \"status\" to report on the \"builtin\" directory. But since a\nconfiguration such as this would result in a \"struct string_list *\"\nwith one element, whose \"string\" member is \"NULL\":\n\n\t[repo] list\n\nWe would, when constructing our command-line in\n\"builtin/for-each-repo.c\"...\n\n\tstrvec_pushl(&child.args, \"-C\", path, NULL);\n\tfor (i = 0; i < argc; i++)\n\t\tstrvec_push(&child.args, argv[i]);\n\n...have that \"path\" be \"NULL\", and as strvec_pushl() stops when it\nsees NULL we'd end with the first \"argv\" element as the argument to\nthe \"-C\" option, e.g.:\n\n\tgit -C status builtin\n\nI.e. we'd run the command \"builtin\" in the \"status\" directory.\n\nIn another context this might be an interesting security\nvulnerability, but I think that this amounts to a nothingburger on\nthat front.\n\nA hypothetical attacker would need to be able to write config for the\nvictim to run, if they're able to do that there's more interesting\nattack vectors. See the \"safe.directory\" facility added in\n8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).\n\nAn even more unlikely possibility would be an attacker able to\ngenerate the config used for \"for-each-repo --config=<key>\", but\nnothing else (e.g. an automated system producing that list).\n\nEven in that case the attack vector is limited to the user running\ncommands whose name matches a directory that's interesting to the\nattacker (e.g. a \"log\" directory in a repository). The second\nargument (if any) of the command is likely to make git die without\ndoing anything interesting (e.g. \"-p\" to \"log\", there being no \"-p\"\nbuilt-in command to run).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  |  2 +-\n t/t0068-for-each-repo.sh | 13 +++++++++++++\n 2 files changed, 14 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 224164addb3..ce8f7a99086 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\terr = repo_config_get_string_multi(the_repository, config_key, &values);\n \tif (err < 0)\n \t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n \t\t\t       for_each_repo_usage, options, config_key);\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 6b51e00da0e..4b90b74d5d5 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -46,4 +46,17 @@ test_expect_success 'error on bad config keys' '\n \ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n '\n \n+test_expect_success 'error on NULL value for config keys' '\n+\tcat >>.git/config <<-\\EOF &&\n+\t[empty]\n+\t\tkey\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''empty.key'\\''\n+\tEOF\n+\ttest_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473181","messageId":"patch-v7-5.9-dced12a40d2-20230308T090513Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"[PATCH v7 5/9] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-08T09:06:47Z","receivedAt":"2023-03-08T09:08:23Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Have the \"git_configset_get_value_multi()\" function and its siblings\nreturn an \"int\" and populate a \"**dest\" parameter like every other\ngit_configset_get_*()\" in the API.\n\nAs we'll take advantage of in subsequent commits, this fixes a blind\nspot in the API where it wasn't possible to tell whether a list was\nempty from whether a config key existed. For now we don't make use of\nthose new return values, but faithfully convert existing API users.\n\nMost of this is straightforward, commentary on cases that stand out:\n\n- To ensure that we'll properly use the return values of this function\n  in the future we're using the \"RESULT_MUST_BE_USED\" macro introduced\n  in [1].\n\n  As git_die_config() now has to handle this return value let's have\n  it BUG() if it can't find the config entry. As tested for in a\n  preceding commit we can rely on getting the config list in\n  git_die_config().\n\n- The loops after getting the \"list\" value in \"builtin/gc.c\" could\n  also make use of \"unsorted_string_list_has_string()\" instead of using\n  that loop, but let's leave that for now.\n\n- In \"versioncmp.c\" we now use the return value of the functions,\n  instead of checking if the lists are still non-NULL.\n\n1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n   return values, 2022-09-01),\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c |  5 +----\n builtin/gc.c            | 10 ++++------\n builtin/log.c           |  6 +++---\n config.c                | 34 ++++++++++++++++++++--------------\n config.h                | 29 +++++++++++++++++++++--------\n pack-bitmap.c           |  6 +++++-\n submodule.c             |  3 +--\n t/helper/test-config.c  |  6 ++----\n versioncmp.c            | 11 +++++++----\n 9 files changed, 64 insertions(+), 46 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 6aeac371488..fd0e7739e6a 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -45,14 +45,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\tvalues = repo_config_get_value_multi(the_repository,\n-\t\t\t\t\t     config_key);\n-\n \t/*\n \t * Do nothing on an empty list, which is equivalent to the case\n \t * where the config variable does not exist at all.\n \t */\n-\tif (!values)\n+\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e38d1783f30..2b3da377d52 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1510,8 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tlist = git_config_get_value_multi(key);\n-\tif (list) {\n+\tif (!git_config_get_value_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1577,11 +1576,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \tif (config_file) {\n \t\tgit_configset_init(&cs);\n \t\tgit_configset_add_file(&cs, config_file);\n-\t\tlist = git_configset_get_value_multi(&cs, key);\n-\t} else {\n-\t\tlist = git_config_get_value_multi(key);\n \t}\n-\tif (list) {\n+\tif (!(config_file\n+\t      ? git_configset_get_value_multi(&cs, key, &list)\n+\t      : git_config_get_value_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex a70fba198f9..e43f6f9d8c1 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tint i;\n \tchar *value = NULL;\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n-\tconst struct string_list *config_exclude =\n-\t\t\tgit_config_get_value_multi(\"log.excludeDecoration\");\n+\tconst struct string_list *config_exclude;\n \n-\tif (config_exclude) {\n+\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t&config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex d4f0e4fd619..569819b4a1b 100644\n--- a/config.c\n+++ b/config.c\n@@ -2418,29 +2418,34 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n+\tint ret;\n+\n \t/*\n \t * Follows \"last one wins\" semantic, i.e., if there are multiple matches for the\n \t * queried key in the files of the configset, the value returned will be the last\n \t * value in the value list for that key.\n \t */\n-\tvalues = git_configset_get_value_multi(cs, key);\n+\tif ((ret = git_configset_get_value_multi(cs, key, &values)))\n+\t\treturn ret;\n \n-\tif (!values)\n-\t\treturn 1;\n \tassert(values->nr > 0);\n \t*value = values->items[values->nr - 1].string;\n \treturn 0;\n }\n \n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest)\n {\n \tstruct config_set_element *e;\n+\tint ret;\n \n-\tif (configset_find_element(cs, key, &e))\n-\t\treturn NULL;\n+\tif ((ret = configset_find_element(cs, key, &e)))\n+\t\treturn ret;\n \telse if (!e)\n-\t\treturn NULL;\n-\treturn &e->value_list;\n+\t\treturn 1;\n+\t*dest = &e->value_list;\n+\n+\treturn 0;\n }\n \n int git_configset_get(struct config_set *cs, const char *key)\n@@ -2604,11 +2609,11 @@ int repo_config_get_value(struct repository *repo,\n \treturn git_configset_get_value(repo->config, key, value);\n }\n \n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key)\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest)\n {\n \tgit_config_check_init(repo);\n-\treturn git_configset_get_value_multi(repo->config, key);\n+\treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n int repo_config_get_string(struct repository *repo,\n@@ -2721,9 +2726,9 @@ int git_config_get_value(const char *key, const char **value)\n \treturn repo_config_get_value(the_repository, key, value);\n }\n \n-const struct string_list *git_config_get_value_multi(const char *key)\n+int git_config_get_value_multi(const char *key, const struct string_list **dest)\n {\n-\treturn repo_config_get_value_multi(the_repository, key);\n+\treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n int git_config_get_string(const char *key, char **dest)\n@@ -2870,7 +2875,8 @@ void git_die_config(const char *key, const char *err, ...)\n \t\terror_fn(err, params);\n \t\tva_end(params);\n \t}\n-\tvalues = git_config_get_value_multi(key);\n+\tif (git_config_get_value_multi(key, &values))\n+\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n \tkv_info = values->items[values->nr - 1].util;\n \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n }\ndiff --git a/config.h b/config.h\nindex 7dd62ca81bf..4db6b90ac20 100644\n--- a/config.h\n+++ b/config.h\n@@ -450,10 +450,18 @@ int git_configset_add_file(struct config_set *cs, const char *filename);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n- * configuration variable `key` is not found, returns NULL. The caller\n- * should not free or modify the returned pointer, as it is owned by the cache.\n+ * configuration variable `key` is not found, returns 1 without touching\n+ * `value`.\n+ *\n+ * The key will be parsed for validity with git_config_parse_key(), on\n+ * error a negative value will be returned.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n+RESULT_MUST_BE_USED\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest);\n \n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n@@ -498,8 +506,9 @@ RESULT_MUST_BE_USED\n int repo_config_get(struct repository *repo, const char *key);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key);\n+RESULT_MUST_BE_USED\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -553,10 +562,14 @@ int git_config_get_value(const char *key, const char **value);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key`. When the configuration variable\n- * `key` is not found, returns NULL. The caller should not free or modify\n- * the returned pointer, as it is owned by the cache.\n+ * `key` is not found, returns 1 without touching `value`.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_config_get_value_multi(const char *key);\n+RESULT_MUST_BE_USED\n+int git_config_get_value_multi(const char *key,\n+\t\t\t       const struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex d2a42abf28c..15c5eb507c0 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2314,7 +2314,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)\n \n const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n-\treturn repo_config_get_value_multi(r, \"pack.preferbitmaptips\");\n+\tconst struct string_list *dest;\n+\n+\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\t\treturn dest;\n+\treturn NULL;\n }\n \n int bitmap_is_preferred_refname(struct repository *r, const char *refname)\ndiff --git a/submodule.c b/submodule.c\nindex 3a0dfc417c0..4b6f5223b0c 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tsl = repo_config_get_value_multi(repo, \"submodule.active\");\n-\tif (sl) {\n+\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex cbb33ae1fff..6dc4c37444f 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -97,8 +97,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\tgoto exit1;\n \t\t}\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_value_multi\")) {\n-\t\tstrptr = git_config_get_value_multi(argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_config_get_value_multi(argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\n@@ -181,8 +180,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\t\tgoto exit2;\n \t\t\t}\n \t\t}\n-\t\tstrptr = git_configset_get_value_multi(&cs, argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 323f5d35ea8..60c3a517122 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)\n \tif (!initialized) {\n \t\tconst char *const newk = \"versionsort.suffix\";\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n+\t\tint new = git_config_get_value_multi(newk, &newl);\n+\t\tint old = git_config_get_value_multi(oldk, &oldl);\n \n-\t\tprereleases = git_config_get_value_multi(newk);\n-\t\toldl = git_config_get_value_multi(oldk);\n-\t\tif (prereleases && oldl)\n+\t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-\t\telse if (!prereleases)\n+\t\tif (!new)\n+\t\t\tprereleases = newl;\n+\t\telse if (!old)\n \t\t\tprereleases = oldl;\n \n \t\tinitialized = 1;\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"473311","messageId":"kl6ledpxhi3t.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v7-3.9-a977b7b188f-20230308T090513Z-avarab@gmail.com","subject":"Re: [PATCH v7 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-03-09T18:53:10Z","receivedAt":"2023-03-09T18:53:26Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> diff --git a/config.h b/config.h\n> index 7606246531a..7dd62ca81bf 100644\n> --- a/config.h\n> +++ b/config.h\n> @@ -465,6 +465,9 @@ void git_configset_clear(struct config_set *cs);\n>   * value in the 'dest' pointer.\n>   */\n>  \n> +RESULT_MUST_BE_USED\n> +int git_configset_get(struct config_set *cs, const char *key);\n\nIIRC, feedback on v4 [1] mentioned that since git_configset_get() can\nreturn negative values, it probably shouldn't come under this comment:\n\n  /*\n  * These functions return 1 if not found, and 0 if found, leaving the found\n  * value in the 'dest' pointer.\n  */\n\nI think moving it to before the comment would suffice, maybe with a\npointer to the corresponding repo_* or git_*.\n\n1. https://lore.kernel.org/git/xmqqv8kjpqoe.fsf@gitster.g/\n\n\n> @@ -485,6 +488,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n>  /* Functions for reading a repository's config */\n>  struct repository;\n>  void repo_config(struct repository *repo, config_fn_t fn, void *data);\n> +\n> +/**\n> + * Run only the discover part of the repo_config_get_*() functions\n> + * below, in addition to 1 if not found, returns negative values on\n> + * error (e.g. if the key itself is invalid).\n> + */\n> +RESULT_MUST_BE_USED\n> +int repo_config_get(struct repository *repo, const char *key);\n>  int repo_config_get_value(struct repository *repo,\n>  \t\t\t  const char *key, const char **value);\n>  const struct string_list *repo_config_get_value_multi(struct repository *repo,\n> @@ -521,8 +532,15 @@ void git_protected_config(config_fn_t fn, void *data);\n>   * manner, the config API provides two functions `git_config_get_value`\n>   * and `git_config_get_value_multi`. They both read values from an internal\n>   * cache generated previously from reading the config files.\n> + *\n> + * For those git_config_get*() functions that aren't documented,\n> + * consult the corresponding repo_config_get*() function's\n> + * documentation.\n>   */\n\nAfter rereading config.h, I really appreciate comments like this that\ntry to control the documentation load. We have configset*, repo* and\ngit*, _and_ the comments are spread out hapzardly around config.h with\nno pointers to the corresponding comments. I think we're overdue for\nreorganization, and this sort of comment helps a lot with that.\n\nAs a suggestion, it seems like the git_config_get*() functions are\nactually the better documented ones - nearly all of them have comments,\nwhereas the repo_config_get_*() ones typically don't, so maybe adding\nthe comment to git_config_get() instead of repo_config_get() would be\nbetter for this series:\n\n----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n  diff --git a/config.h b/config.h\n  index 7dd62ca81b..aa9bdf8df4 100644\n  --- a/config.h\n  +++ b/config.h\n  @@ -489,10 +489,10 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n  struct repository;\n  void repo_config(struct repository *repo, config_fn_t fn, void *data);\n\n  -/**\n  - * Run only the discover part of the repo_config_get_*() functions\n  - * below, in addition to 1 if not found, returns negative values on\n  - * error (e.g. if the key itself is invalid).\n  +/*\n  + * These repo_config_get*() functions each correspond to to a git_config_get*()\n  + * function. Consult the corresponding git_config_get*() documentation for more\n  + * information.\n    */\n  RESULT_MUST_BE_USED\n  int repo_config_get(struct repository *repo, const char *key);\n  @@ -532,12 +532,13 @@ void git_protected_config(config_fn_t fn, void *data);\n    * manner, the config API provides two functions `git_config_get_value`\n    * and `git_config_get_value_multi`. They both read values from an internal\n    * cache generated previously from reading the config files.\n  - *\n  - * For those git_config_get*() functions that aren't documented,\n  - * consult the corresponding repo_config_get*() function's\n  - * documentation.\n    */\n\n  +/**\n  + * Run only the discover part of the repo_config_get_*() functions\n  + * below, in addition to 1 if not found, returns negative values on\n  + * error (e.g. if the key itself is invalid).\n  + */\n  RESULT_MUST_BE_USED\n  int git_config_get(const char *key);\n----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n\nThough in the long run, I'd prefer having the docs on the more \"general\"\nAPIs (configset_*, repo_*) instead of the more \"specific\" ones (git_*).\nPerhaps you had a similar intent while making this change, but I think\nthis might be better left as a cleanup.\n\nAs an aside, I really appreciate your effort in sticking with the config\ninterface work. I think it's grown quite unruly, and it's worth trying\nto tame it.\n"},{"id":"473312","messageId":"kl6lbkl1hhqo.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"patch-v7-5.9-dced12a40d2-20230308T090513Z-avarab@gmail.com","subject":"Re: [PATCH v7 5/9] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-03-09T19:01:03Z","receivedAt":"2023-03-09T19:01:10Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> Have the \"git_configset_get_value_multi()\" function and its siblings\n> return an \"int\" and populate a \"**dest\" parameter like every other\n> git_configset_get_*()\" in the API.\n>\n> As we'll take advantage of in subsequent commits, this fixes a blind\n> spot in the API where it wasn't possible to tell whether a list was\n> empty from whether a config key existed. For now we don't make use of\n> those new return values, but faithfully convert existing API users.\n\nI think the commit message is fine as-is, but perhaps you intended to\ninclude this paragraph from v4 [1]?\n\n    A logical follow-up to this would be to change the various \"*_get_*()\"\n    functions to ferry the git_configset_get_value() return value to their\n    own callers, e.g. git_configset_get_int() returns \"1\" rather than\n    ferrying up the \"-1\" that \"git_configset_get_value()\" might return,\n    but that's not being done in this series\n\nWhich is nice, but the commit message reads fine without it too.\n\n1. https://lore.kernel.org/git/patch-v4-5.9-23449ff2c4e-20230202T131155Z-avarab@gmail.com/\n\n>\n> Most of this is straightforward, commentary on cases that stand out:\n>\n> - To ensure that we'll properly use the return values of this function\n>   in the future we're using the \"RESULT_MUST_BE_USED\" macro introduced\n>   in [1].\n>\n>   As git_die_config() now has to handle this return value let's have\n>   it BUG() if it can't find the config entry. As tested for in a\n>   preceding commit we can rely on getting the config list in\n>   git_die_config().\n>\n> - The loops after getting the \"list\" value in \"builtin/gc.c\" could\n>   also make use of \"unsorted_string_list_has_string()\" instead of using\n>   that loop, but let's leave that for now.\n>\n> - In \"versioncmp.c\" we now use the return value of the functions,\n>   instead of checking if the lists are still non-NULL.\n>\n> 1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n>    return values, 2022-09-01),\n"},{"id":"473313","messageId":"kl6l8rg5hhen.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"Re: [PATCH v7 0/9] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-03-09T19:08:16Z","receivedAt":"2023-03-09T19:08:34Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> This series fixes numerous segfaults in config API users, because they\n> didn't expect *_get_multi() to hand them a string_list with a NULL in\n> it given config like \"[a] key\" (note, no \"=\"'s).\n>\n> A larger general overview at v1[1], but note the API changes in\n> v2[2]. Changes since v6[3]:\n>\n>  * Glen pointed out that ejecting a commit in v6 orphaned a\n>    corresponding forward-reference in a commit message, fix that.\n\nThanks for your patience with the rerolls :) I only spotted a minor\ncomment issue [1] (which I think was what originally motivated v5?).\nIMO this will be mergeable once we reorder that comment.\n\n1. https://lore.kernel.org/git/kl6ledpxhi3t.fsf@chooglen-macbookpro.roam.corp.google.com/\n"},{"id":"473316","messageId":"xmqq4jqtac0z.fsf@gitster.g","threadId":"58696","inReplyTo":"kl6l8rg5hhen.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v7 0/9] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-03-09T20:46:20Z","receivedAt":"2023-03-09T20:47:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>\n>> This series fixes numerous segfaults in config API users, because they\n>> didn't expect *_get_multi() to hand them a string_list with a NULL in\n>> it given config like \"[a] key\" (note, no \"=\"'s).\n>>\n>> A larger general overview at v1[1], but note the API changes in\n>> v2[2]. Changes since v6[3]:\n>>\n>>  * Glen pointed out that ejecting a commit in v6 orphaned a\n>>    corresponding forward-reference in a commit message, fix that.\n>\n> Thanks for your patience with the rerolls :) I only spotted a minor\n> comment issue [1] (which I think was what originally motivated v5?).\n> IMO this will be mergeable once we reorder that comment.\n>\n> 1. https://lore.kernel.org/git/kl6ledpxhi3t.fsf@chooglen-macbookpro.roam.corp.google.com/\n\nThanks for carefully reading these patches.  I agree that this round\nis in quite a good shape.\n\n"},{"id":"473498","messageId":"230314.86pm9by3oz.gmgdl@evledraar.gmail.com","threadId":"58696","inReplyTo":"kl6ledpxhi3t.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v7 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-14T11:21:47Z","receivedAt":"2023-03-14T11:26:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 09 2023, Glen Choo wrote:\n\n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>\n>> diff --git a/config.h b/config.h\n>> index 7606246531a..7dd62ca81bf 100644\n>> --- a/config.h\n>> +++ b/config.h\n>> @@ -465,6 +465,9 @@ void git_configset_clear(struct config_set *cs);\n>>   * value in the 'dest' pointer.\n>>   */\n>>  \n>> +RESULT_MUST_BE_USED\n>> +int git_configset_get(struct config_set *cs, const char *key);\n>\n> IIRC, feedback on v4 [1] mentioned that since git_configset_get() can\n> return negative values, it probably shouldn't come under this comment:\n>\n>   /*\n>   * These functions return 1 if not found, and 0 if found, leaving the found\n>   * value in the 'dest' pointer.\n>   */\n>\n> I think moving it to before the comment would suffice, maybe with a\n> pointer to the corresponding repo_* or git_*.\n>\n> 1. https://lore.kernel.org/git/xmqqv8kjpqoe.fsf@gitster.g/\n\nI'll fix this, FWIW I was trying to juggle this so that I'd avoid future\nchurn for a subsequent cleanup of the interface & documentation...\n\n>> @@ -485,6 +488,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n>>  /* Functions for reading a repository's config */\n>>  struct repository;\n>>  void repo_config(struct repository *repo, config_fn_t fn, void *data);\n>> +\n>> +/**\n>> + * Run only the discover part of the repo_config_get_*() functions\n>> + * below, in addition to 1 if not found, returns negative values on\n>> + * error (e.g. if the key itself is invalid).\n>> + */\n>> +RESULT_MUST_BE_USED\n>> +int repo_config_get(struct repository *repo, const char *key);\n>>  int repo_config_get_value(struct repository *repo,\n>>  \t\t\t  const char *key, const char **value);\n>>  const struct string_list *repo_config_get_value_multi(struct repository *repo,\n>> @@ -521,8 +532,15 @@ void git_protected_config(config_fn_t fn, void *data);\n>>   * manner, the config API provides two functions `git_config_get_value`\n>>   * and `git_config_get_value_multi`. They both read values from an internal\n>>   * cache generated previously from reading the config files.\n>> + *\n>> + * For those git_config_get*() functions that aren't documented,\n>> + * consult the corresponding repo_config_get*() function's\n>> + * documentation.\n>>   */\n>\n> After rereading config.h, I really appreciate comments like this that\n> try to control the documentation load. We have configset*, repo* and\n> git*, _and_ the comments are spread out hapzardly around config.h with\n> no pointers to the corresponding comments. I think we're overdue for\n> reorganization, and this sort of comment helps a lot with that.\n>\n> As a suggestion, it seems like the git_config_get*() functions are\n> actually the better documented ones - nearly all of them have comments,\n> whereas the repo_config_get_*() ones typically don't, so maybe adding\n> the comment to git_config_get() instead of repo_config_get() would be\n> better for this series:\n>\n> ----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n>   diff --git a/config.h b/config.h\n>   index 7dd62ca81b..aa9bdf8df4 100644\n>   --- a/config.h\n>   +++ b/config.h\n>   @@ -489,10 +489,10 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n>   struct repository;\n>   void repo_config(struct repository *repo, config_fn_t fn, void *data);\n>\n>   -/**\n>   - * Run only the discover part of the repo_config_get_*() functions\n>   - * below, in addition to 1 if not found, returns negative values on\n>   - * error (e.g. if the key itself is invalid).\n>   +/*\n>   + * These repo_config_get*() functions each correspond to to a git_config_get*()\n>   + * function. Consult the corresponding git_config_get*() documentation for more\n>   + * information.\n>     */\n>   RESULT_MUST_BE_USED\n>   int repo_config_get(struct repository *repo, const char *key);\n>   @@ -532,12 +532,13 @@ void git_protected_config(config_fn_t fn, void *data);\n>     * manner, the config API provides two functions `git_config_get_value`\n>     * and `git_config_get_value_multi`. They both read values from an internal\n>     * cache generated previously from reading the config files.\n>   - *\n>   - * For those git_config_get*() functions that aren't documented,\n>   - * consult the corresponding repo_config_get*() function's\n>   - * documentation.\n>     */\n>\n>   +/**\n>   + * Run only the discover part of the repo_config_get_*() functions\n>   + * below, in addition to 1 if not found, returns negative values on\n>   + * error (e.g. if the key itself is invalid).\n>   + */\n>   RESULT_MUST_BE_USED\n>   int git_config_get(const char *key);\n> ----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n>\n> Though in the long run, I'd prefer having the docs on the more \"general\"\n> APIs (configset_*, repo_*) instead of the more \"specific\" ones (git_*).\n> Perhaps you had a similar intent while making this change, but I think\n> this might be better left as a cleanup.\n\n...yes, that's why I put the primary documentation on the repo_*()\nversion here, as with other implicit \"the_repository\" and \"the_index\"\nmigrations I think we should be moving towards using those, and\neventually removing the non-repo_*() ones (in some cases they're almost\nunused, or it's easy enough to migrate the rest).\n\nBut let's leave that for some future cleanup, but for now I think it's\nOK to leave this slight inconsistency in place, with an eye to such\nfuture consolidation.\n\n> As an aside, I really appreciate your effort in sticking with the config\n> interface work. I think it's grown quite unruly, and it's worth trying\n> to tame it.\n\nThanks, hopefully a trivial & upcoming v8 will be the last version...\n"},{"id":"474301","messageId":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com","subject":"[PATCH v8 0/9] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-28T14:04:19Z","receivedAt":"2023-03-28T14:06:13Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This series fixes numerous segfaults in config API users, because they\ndidn't expect *_get_multi() to hand them a string_list with a NULL in\nit given config like \"[a] key\" (note, no \"=\"'s).\n\nA larger general overview at v1[1], but note the API changes in\nv2[2]. Changes since v7[3]:\n\n* A trivial documentation change to 3/9, to clarify which doc in\n  config.h refer to what. As noted in the v7 discussion I think that\n  config.h could use some larger cleanups in this area, but let's\n  leave that for some future topic.\n\nBranch & passing CI for this at:\nhttps://github.com/avar/git/tree/avar/have-git_configset_get_value-use-dest-and-int-pattern-8\n\n1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/\n2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/\n3. https://lore.kernel.org/git/cover-v7-0.9-00000000000-20230308T090513Z-avarab@gmail.com/\n\nÆvar Arnfjörð Bjarmason (9):\n  config tests: cover blind spots in git_die_config() tests\n  config tests: add \"NULL\" tests for *_get_value_multi()\n  config API: add and use a \"git_config_get()\" family of functions\n  versioncmp.c: refactor config reading next commit\n  config API: have *_multi() return an \"int\" and take a \"dest\"\n  for-each-repo: error on bad --config\n  config API users: test for *_get_value_multi() segfaults\n  config API: add \"string\" version of *_value_multi(), fix segfaults\n  for-each-repo: with bad config, don't conflate <path> and <cmd>\n\n builtin/for-each-repo.c              |  14 ++--\n builtin/gc.c                         |  15 ++--\n builtin/log.c                        |   6 +-\n builtin/submodule--helper.c          |   7 +-\n builtin/worktree.c                   |   3 +-\n config.c                             | 109 ++++++++++++++++++++++-----\n config.h                             |  72 +++++++++++++++---\n pack-bitmap.c                        |   6 +-\n submodule.c                          |   3 +-\n t/helper/test-config.c               |  28 ++++++-\n t/t0068-for-each-repo.sh             |  19 +++++\n t/t1308-config-set.sh                | 108 +++++++++++++++++++++++++-\n t/t3309-notes-merge-auto-resolve.sh  |   7 +-\n t/t4202-log.sh                       |  15 ++++\n t/t5304-prune.sh                     |  12 ++-\n t/t5310-pack-bitmaps.sh              |  20 +++++\n t/t5552-skipping-fetch-negotiator.sh |  16 ++++\n t/t7004-tag.sh                       |  17 +++++\n t/t7413-submodule-is-active.sh       |  16 ++++\n t/t7900-maintenance.sh               |  38 ++++++++++\n versioncmp.c                         |  22 ++++--\n 21 files changed, 481 insertions(+), 72 deletions(-)\n\nRange-diff against v7:\n 1:  9f297a35e14 =  1:  b600354c0f6 config tests: cover blind spots in git_die_config() tests\n 2:  45d483066ef =  2:  49908f0bcf3 config tests: add \"NULL\" tests for *_get_value_multi()\n 3:  a977b7b188f !  3:  d163b3d04ff config API: add and use a \"git_config_get()\" family of functions\n    @@ config.h: void git_configset_clear(struct config_set *cs);\n       * value in the 'dest' pointer.\n       */\n      \n    ++/**\n    ++ * git_configset_get() returns negative values on error, see\n    ++ * repo_config_get() below.\n    ++ */\n     +RESULT_MUST_BE_USED\n     +int git_configset_get(struct config_set *cs, const char *key);\n     +\n 4:  3a5a323cd91 =  4:  d7dfedb7225 versioncmp.c: refactor config reading next commit\n 5:  dced12a40d2 =  5:  840fb9d5c74 config API: have *_multi() return an \"int\" and take a \"dest\"\n 6:  d910f7e3a27 =  6:  75a68b14217 for-each-repo: error on bad --config\n 7:  57db0fcd91f =  7:  a78056e2748 config API users: test for *_get_value_multi() segfaults\n 8:  b374a716555 =  8:  686b512c3df config API: add \"string\" version of *_value_multi(), fix segfaults\n 9:  6791e1f6f85 =  9:  6fce633493b for-each-repo: with bad config, don't conflate <path> and <cmd>\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"474302","messageId":"patch-v8-1.9-b600354c0f6-20230328T140127Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"[PATCH v8 1/9] config tests: cover blind spots in git_die_config() tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-28T14:04:20Z","receivedAt":"2023-03-28T14:06:19Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"There were no tests checking for the output of the git_die_config()\nfunction in the config API, added in 5a80e97c827 (config: add\n`git_die_config()` to the config-set API, 2014-08-07). We only tested\n\"test_must_fail\", but didn't assert the output.\n\nWe need tests for this because a subsequent commit will alter the\nreturn value of git_config_get_value_multi(), which is used to get the\nconfig values in the git_die_config() function. This test coverage\nhelps to build confidence in that subsequent change.\n\nThese tests cover different interactions with git_die_config():\n\n- The \"notes.mergeStrategy\" test in\n  \"t/t3309-notes-merge-auto-resolve.sh\" is a case where a function\n  outside of config.c (git_config_get_notes_strategy()) calls\n  git_die_config().\n\n- The \"gc.pruneExpire\" test in \"t5304-prune.sh\" is a case where\n  git_config_get_expiry() calls git_die_config(), covering a different\n  \"type\" than the \"string\" test for \"notes.mergeStrategy\".\n\n- The \"fetch.negotiationAlgorithm\" test in\n  \"t/t5552-skipping-fetch-negotiator.sh\" is a case where\n  git_config_get_string*() calls git_die_config().\n\nWe also cover both the \"from command-line config\" and \"in file..at\nline\" cases here.\n\nThe clobbering of existing \".git/config\" files here is so that we're\nnot implicitly testing the line count of the default config.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-\n t/t5304-prune.sh                     | 12 ++++++++++--\n t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++\n 3 files changed, 32 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh\nindex 141d3e4ca4d..9bd5dbf341f 100755\n--- a/t/t3309-notes-merge-auto-resolve.sh\n+++ b/t/t3309-notes-merge-auto-resolve.sh\n@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '\n \n test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '\n \tgit config core.notesRef refs/notes/y &&\n-\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z &&\n+\tcat >expect <<-\\EOF &&\n+\terror: unknown notes merge strategy foo\n+\tfatal: unable to parse '\\''notes.mergeStrategy'\\'' from command-line config\n+\tEOF\n+\ttest_must_fail git -c notes.mergeStrategy=\"foo\" notes merge z 2>actual &&\n+\ttest_cmp expect actual &&\n \t# Verify no changes (y)\n \tverify_notes y y\n '\ndiff --git a/t/t5304-prune.sh b/t/t5304-prune.sh\nindex d65a5f94b4b..5500dd08426 100755\n--- a/t/t5304-prune.sh\n+++ b/t/t5304-prune.sh\n@@ -72,8 +72,16 @@ test_expect_success 'gc: implicit prune --expire' '\n '\n \n test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '\n-\tgit config gc.pruneExpire invalid &&\n-\ttest_must_fail git gc\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t>repo/.git/config &&\n+\tgit -C repo config gc.pruneExpire invalid &&\n+\tcat >expect <<-\\EOF &&\n+\terror: Invalid gc.pruneexpire: '\\''invalid'\\''\n+\tfatal: bad config variable '\\''gc.pruneexpire'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_must_fail git -C repo gc 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'gc: start with ok gc.pruneExpire' '\ndiff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh\nindex 165427d57e5..b55a9f65e6b 100755\n--- a/t/t5552-skipping-fetch-negotiator.sh\n+++ b/t/t5552-skipping-fetch-negotiator.sh\n@@ -3,6 +3,22 @@\n test_description='test skipping fetch negotiator'\n . ./test-lib.sh\n \n+test_expect_success 'fetch.negotiationalgorithm config' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\tcat >repo/.git/config <<-\\EOF &&\n+\t[fetch]\n+\tnegotiationAlgorithm\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''fetch.negotiationalgorithm'\\''\n+\tfatal: bad config variable '\\''fetch.negotiationalgorithm'\\'' in file '\\''.git/config'\\'' at line 2\n+\tEOF\n+\ttest_expect_code 128 git -C repo fetch >out 2>actual &&\n+\ttest_must_be_empty out &&\n+\ttest_cmp expect actual\n+'\n+\n have_sent () {\n \twhile test \"$#\" -ne 0\n \tdo\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"474303","messageId":"patch-v8-2.9-49908f0bcf3-20230328T140127Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"[PATCH v8 2/9] config tests: add \"NULL\" tests for *_get_value_multi()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-28T14:04:21Z","receivedAt":"2023-03-28T14:06:21Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"A less well known edge case in the config format is that keys can be\nvalue-less, a shorthand syntax for \"true\" boolean keys. I.e. these two\nare equivalent as far as \"--type=bool\" is concerned:\n\n\t[a]key\n\t[a]key = true\n\nBut as far as our parser is concerned the values for these two are\nNULL, and \"true\". I.e. for a sequence like:\n\n\t[a]key=x\n\t[a]key\n\t[a]key=y\n\nWe get a \"struct string_list\" with \"string\" members with \".string\"\nvalues of:\n\n\t{ \"x\", NULL, \"y\" }\n\nThis behavior goes back to the initial implementation of\ngit_config_bool() in 17712991a59 (Add \".git/config\" file parser,\n2005-10-10).\n\nWhen parts of the config_set API were tested for in [1] they didn't\nadd coverage for 3/4 of the \"(NULL)\" cases handled in\n\"t/helper/test-config.c\". We'd test that case for \"get_value\", but not\n\"get_value_multi\", \"configset_get_value\" and\n\"configset_get_value_multi\".\n\nWe now cover all of those cases, which in turn expose the details of\nhow this part of the config API works.\n\n1. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t1308-config-set.sh | 65 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 65 insertions(+)\n\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex b38e158d3b2..4be1ab1147c 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -146,6 +146,71 @@ test_expect_success 'find multiple values' '\n \tcheck_config get_value_multi case.baz sam bat hask\n '\n \n+test_NULL_in_multi () {\n+\tlocal op=\"$1\" &&\n+\tlocal file=\"$2\" &&\n+\n+\ttest_expect_success \"$op: NULL value in config${file:+ in $file}\" '\n+\t\tconfig=\"$file\" &&\n+\t\tif test -z \"$config\"\n+\t\tthen\n+\t\t\tconfig=.git/config &&\n+\t\t\ttest_when_finished \"mv $config.old $config\" &&\n+\t\t\tmv \"$config\" \"$config\".old\n+\t\tfi &&\n+\n+\t\t# Value-less in the middle of a list\n+\t\tcat >\"$config\" <<-\\EOF &&\n+\t\t[a]key=x\n+\t\t[a]key\n+\t\t[a]key=y\n+\t\tEOF\n+\t\tcase \"$op\" in\n+\t\t*_multi)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\tx\n+\t\t\t(NULL)\n+\t\t\ty\n+\t\t\tEOF\n+\t\t\t;;\n+\t\t*)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\ty\n+\t\t\tEOF\n+\t\t\t;;\n+\t\tesac &&\n+\t\ttest-tool config \"$op\" a.key $file >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# Value-less at the end of a least\n+\t\tcat >\"$config\" <<-\\EOF &&\n+\t\t[a]key=x\n+\t\t[a]key=y\n+\t\t[a]key\n+\t\tEOF\n+\t\tcase \"$op\" in\n+\t\t*_multi)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\tx\n+\t\t\ty\n+\t\t\t(NULL)\n+\t\t\tEOF\n+\t\t\t;;\n+\t\t*)\n+\t\t\tcat >expect <<-\\EOF\n+\t\t\t(NULL)\n+\t\t\tEOF\n+\t\t\t;;\n+\t\tesac &&\n+\t\ttest-tool config \"$op\" a.key $file >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+}\n+\n+test_NULL_in_multi \"get_value_multi\"\n+test_NULL_in_multi \"configset_get_value\" \"my.config\"\n+test_NULL_in_multi \"configset_get_value_multi\" \"my.config\"\n+\n test_expect_success 'find value from a configset' '\n \tcat >config2 <<-\\EOF &&\n \t[case]\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"474304","messageId":"patch-v8-3.9-d163b3d04ff-20230328T140127Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"[PATCH v8 3/9] config API: add and use a \"git_config_get()\" family of functions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-28T14:04:22Z","receivedAt":"2023-03-28T14:06:22Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"We already have the basic \"git_config_get_value()\" function and its\n\"repo_*\" and \"configset\" siblings to get a given \"key\" and assign the\nlast key found to a provided \"value\".\n\nBut some callers don't care about that value, but just want to use the\nreturn value of the \"get_value()\" function to check whether the key\nexist (or another non-zero return value).\n\nThe immediate motivation for this is that a subsequent commit will\nneed to change all callers of the \"*_get_value_multi()\" family of\nfunctions. In two cases here we (ab)used it to check whether we had\nany values for the given key, but didn't care about the return value.\n\nThe rest of the callers here used various other config API functions\nto do the same, all of which resolved to the same underlying functions\nto provide the answer.\n\nSome of these were using either git_config_get_string() or\ngit_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a\nconfigure_added_submodule() leak, 2022-09-01) for a recent example. We\ncan now use a helper function that doesn't require a throwaway\nvariable.\n\nWe could have changed git_configset_get_value_multi() (and then\ngit_config_get_value() etc.) to accept a \"NULL\" as a \"dest\" for all\ncallers, but let's avoid changing the behavior of existing API\nusers. Having an \"unused\" value that we throw away internal to\nconfig.c is cheap.\n\nA \"NULL as optional dest\" pattern is also more fragile, as the intent\nof the caller might be misinterpreted if he were to accidentally pass\n\"NULL\", e.g. when \"dest\" is passed in from another function.\n\nAnother name for this function could have been\n\"*_config_key_exists()\", as suggested in [1]. That would work for all\nof these callers, and would currently be equivalent to this function,\nas the git_configset_get_value() API normalizes all non-zero return\nvalues to a \"1\".\n\nBut adding that API would set us up to lose information, as e.g. if\ngit_config_parse_key() in the underlying configset_find_element()\nfails we'd like to return -1, not 1.\n\nLet's change the underlying configset_find_element() function to\nsupport this use-case, we'll make further use of it in a subsequent\ncommit where the git_configset_get_value_multi() function itself will\nexpose this new return value.\n\nThis still leaves various inconsistencies and clobbering or ignoring\nof the return value in place. E.g here we're modifying\nconfigset_add_value(), but ever since it was added in [2] we've been\nignoring its \"int\" return value, but as we're changing the\nconfigset_find_element() it uses, let's have it faithfully ferry that\n\"ret\" along.\n\nLet's also use the \"RESULT_MUST_BE_USED\" macro introduced in [3] to\nassert that we're checking the return value of\nconfigset_find_element().\n\nWe're leaving the same change to configset_add_value() for some future\nseries. Once we start paying attention to its return value we'd need\nto ferry it up as deep as do_config_from(), and would need to make\nleast read_{,very_}early_config() and git_protected_config() return an\n\"int\" instead of \"void\". Let's leave that for now, and focus on\nthe *_get_*() functions.\n\n1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)\n2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/\n3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n   return values, 2022-09-01),\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                |  5 +---\n builtin/submodule--helper.c |  7 +++--\n builtin/worktree.c          |  3 +--\n config.c                    | 51 ++++++++++++++++++++++++++++++++-----\n config.h                    | 22 ++++++++++++++++\n t/helper/test-config.c      | 22 ++++++++++++++++\n t/t1308-config-set.sh       | 43 ++++++++++++++++++++++++++++++-\n 7 files changed, 135 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex c58fe8c936c..b7251840e20 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1494,7 +1494,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \t};\n \tint found = 0;\n \tconst char *key = \"maintenance.repo\";\n-\tchar *config_value;\n \tchar *maintpath = get_maintpath();\n \tstruct string_list_item *item;\n \tconst struct string_list *list;\n@@ -1509,9 +1508,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tgit_config_set(\"maintenance.auto\", \"false\");\n \n \t/* Set maintenance strategy, if unset */\n-\tif (!git_config_get_string(\"maintenance.strategy\", &config_value))\n-\t\tfree(config_value);\n-\telse\n+\tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n \tlist = git_config_get_value_multi(key);\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex d05d1a84623..647dbb932bc 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -559,7 +559,7 @@ static int module_init(int argc, const char **argv, const char *prefix)\n \t * If there are no path args and submodule.active is set then,\n \t * by default, only initialize 'active' modules.\n \t */\n-\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\tif (!argc && !git_config_get(\"submodule.active\"))\n \t\tmodule_list_active(&list);\n \n \tinfo.prefix = prefix;\n@@ -2745,7 +2745,7 @@ static int module_update(int argc, const char **argv, const char *prefix)\n \t\t * If there are no path args and submodule.active is set then,\n \t\t * by default, only initialize 'active' modules.\n \t\t */\n-\t\tif (!argc && git_config_get_value_multi(\"submodule.active\"))\n+\t\tif (!argc && !git_config_get(\"submodule.active\"))\n \t\t\tmodule_list_active(&list);\n \n \t\tinfo.prefix = opt.prefix;\n@@ -3142,7 +3142,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con\n static void configure_added_submodule(struct add_data *add_data)\n {\n \tchar *key;\n-\tconst char *val;\n \tstruct child_process add_submod = CHILD_PROCESS_INIT;\n \tstruct child_process add_gitmodules = CHILD_PROCESS_INIT;\n \n@@ -3187,7 +3186,7 @@ static void configure_added_submodule(struct add_data *add_data)\n \t * is_submodule_active(), since that function needs to find\n \t * out the value of \"submodule.active\" again anyway.\n \t */\n-\tif (!git_config_get_string_tmp(\"submodule.active\", &val)) {\n+\tif (!git_config_get(\"submodule.active\")) {\n \t\t/*\n \t\t * If the submodule being added isn't already covered by the\n \t\t * current configured pathspec, set the submodule's active flag\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 80d05e246d8..476325ef98f 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -320,7 +320,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \n \tif (file_exists(from_file)) {\n \t\tstruct config_set cs = { { 0 } };\n-\t\tconst char *core_worktree;\n \t\tint bare;\n \n \t\tif (safe_create_leading_directories(to_file) ||\n@@ -339,7 +338,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)\n \t\t\t\tto_file, \"core.bare\", NULL, \"true\", 0))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\n \t\t\t\t\"core.bare\", to_file);\n-\t\tif (!git_configset_get_value(&cs, \"core.worktree\", &core_worktree) &&\n+\t\tif (!git_configset_get(&cs, \"core.worktree\") &&\n \t\t\tgit_config_set_in_file_gently(to_file,\n \t\t\t\t\t\t\t\"core.worktree\", NULL))\n \t\t\terror(_(\"failed to unset '%s' in '%s'\"),\ndiff --git a/config.c b/config.c\nindex d0aff55fa66..ba2ec3b54ee 100644\n--- a/config.c\n+++ b/config.c\n@@ -2292,23 +2292,29 @@ void read_very_early_config(config_fn_t cb, void *data)\n \tconfig_with_options(cb, data, NULL, &opts);\n }\n \n-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)\n+RESULT_MUST_BE_USED\n+static int configset_find_element(struct config_set *cs, const char *key,\n+\t\t\t\t  struct config_set_element **dest)\n {\n \tstruct config_set_element k;\n \tstruct config_set_element *found_entry;\n \tchar *normalized_key;\n+\tint ret;\n+\n \t/*\n \t * `key` may come from the user, so normalize it before using it\n \t * for querying entries from the hashmap.\n \t */\n-\tif (git_config_parse_key(key, &normalized_key, NULL))\n-\t\treturn NULL;\n+\tret = git_config_parse_key(key, &normalized_key, NULL);\n+\tif (ret)\n+\t\treturn ret;\n \n \thashmap_entry_init(&k.ent, strhash(normalized_key));\n \tk.key = normalized_key;\n \tfound_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);\n \tfree(normalized_key);\n-\treturn found_entry;\n+\t*dest = found_entry;\n+\treturn 0;\n }\n \n static int configset_add_value(struct config_set *cs, const char *key, const char *value)\n@@ -2317,8 +2323,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha\n \tstruct string_list_item *si;\n \tstruct configset_list_item *l_item;\n \tstruct key_value_info *kv_info = xmalloc(sizeof(*kv_info));\n+\tint ret;\n \n-\te = configset_find_element(cs, key);\n+\tret = configset_find_element(cs, key, &e);\n+\tif (ret)\n+\t\treturn ret;\n \t/*\n \t * Since the keys are being fed by git_config*() callback mechanism, they\n \t * are already normalized. So simply add them without any further munging.\n@@ -2428,8 +2437,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *\n \n const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n {\n-\tstruct config_set_element *e = configset_find_element(cs, key);\n-\treturn e ? &e->value_list : NULL;\n+\tstruct config_set_element *e;\n+\n+\tif (configset_find_element(cs, key, &e))\n+\t\treturn NULL;\n+\telse if (!e)\n+\t\treturn NULL;\n+\treturn &e->value_list;\n+}\n+\n+int git_configset_get(struct config_set *cs, const char *key)\n+{\n+\tstruct config_set_element *e;\n+\tint ret;\n+\n+\tif ((ret = configset_find_element(cs, key, &e)))\n+\t\treturn ret;\n+\telse if (!e)\n+\t\treturn 1;\n+\treturn 0;\n }\n \n int git_configset_get_string(struct config_set *cs, const char *key, char **dest)\n@@ -2568,6 +2594,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)\n \tconfigset_iter(repo->config, fn, data);\n }\n \n+int repo_config_get(struct repository *repo, const char *key)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get(repo->config, key);\n+}\n+\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value)\n {\n@@ -2682,6 +2714,11 @@ void git_config_clear(void)\n \trepo_config_clear(the_repository);\n }\n \n+int git_config_get(const char *key)\n+{\n+\treturn repo_config_get(the_repository, key);\n+}\n+\n int git_config_get_value(const char *key, const char **value)\n {\n \treturn repo_config_get_value(the_repository, key, value);\ndiff --git a/config.h b/config.h\nindex 7606246531a..72d83e21e3d 100644\n--- a/config.h\n+++ b/config.h\n@@ -465,6 +465,13 @@ void git_configset_clear(struct config_set *cs);\n  * value in the 'dest' pointer.\n  */\n \n+/**\n+ * git_configset_get() returns negative values on error, see\n+ * repo_config_get() below.\n+ */\n+RESULT_MUST_BE_USED\n+int git_configset_get(struct config_set *cs, const char *key);\n+\n /*\n  * Finds the highest-priority value for the configuration variable `key`\n  * and config set `cs`, stores the pointer to it in `value` and returns 0.\n@@ -485,6 +492,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha\n /* Functions for reading a repository's config */\n struct repository;\n void repo_config(struct repository *repo, config_fn_t fn, void *data);\n+\n+/**\n+ * Run only the discover part of the repo_config_get_*() functions\n+ * below, in addition to 1 if not found, returns negative values on\n+ * error (e.g. if the key itself is invalid).\n+ */\n+RESULT_MUST_BE_USED\n+int repo_config_get(struct repository *repo, const char *key);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n const struct string_list *repo_config_get_value_multi(struct repository *repo,\n@@ -521,8 +536,15 @@ void git_protected_config(config_fn_t fn, void *data);\n  * manner, the config API provides two functions `git_config_get_value`\n  * and `git_config_get_value_multi`. They both read values from an internal\n  * cache generated previously from reading the config files.\n+ *\n+ * For those git_config_get*() functions that aren't documented,\n+ * consult the corresponding repo_config_get*() function's\n+ * documentation.\n  */\n \n+RESULT_MUST_BE_USED\n+int git_config_get(const char *key);\n+\n /**\n  * Finds the highest-priority value for the configuration variable `key`,\n  * stores the pointer to it in `value` and returns 0. When the\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex 4ba9eb65606..cbb33ae1fff 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -14,6 +14,8 @@\n  * get_value_multi -> prints all values for the entered key in increasing order\n  *\t\t     of priority\n  *\n+ * get -> print return value for the entered key\n+ *\n  * get_int -> print integer value for the entered key or die\n  *\n  * get_bool -> print bool value for the entered key or die\n@@ -109,6 +111,26 @@ int cmd__config(int argc, const char **argv)\n \t\t\tprintf(\"Value not found for \\\"%s\\\"\\n\", argv[2]);\n \t\t\tgoto exit1;\n \t\t}\n+\t} else if (argc == 3 && !strcmp(argv[1], \"get\")) {\n+\t\tint ret;\n+\n+\t\tif (!(ret = git_config_get(argv[2])))\n+\t\t\tgoto exit0;\n+\t\telse if (ret == 1)\n+\t\t\tprintf(\"Value not found for \\\"%s\\\"\\n\", argv[2]);\n+\t\telse if (ret == -CONFIG_INVALID_KEY)\n+\t\t\tprintf(\"Key \\\"%s\\\" is invalid\\n\", argv[2]);\n+\t\telse if (ret == -CONFIG_NO_SECTION_OR_NAME)\n+\t\t\tprintf(\"Key \\\"%s\\\" has no section\\n\", argv[2]);\n+\t\telse\n+\t\t\t/*\n+\t\t\t * A normal caller should just check \"ret <\n+\t\t\t * 0\", but for our own tests let's BUG() if\n+\t\t\t * our whitelist of git_config_parse_key()\n+\t\t\t * return values isn't exhaustive.\n+\t\t\t */\n+\t\t\tBUG(\"Key \\\"%s\\\" has unknown return %d\", argv[2], ret);\n+\t\tgoto exit1;\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_int\")) {\n \t\tif (!git_config_get_int(argv[2], &val)) {\n \t\t\tprintf(\"%d\\n\", val);\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex 4be1ab1147c..7def7053e1c 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -58,6 +58,8 @@ test_expect_success 'setup default config' '\n \t\tskin = false\n \t\tnose = 1\n \t\thorns\n+\t[value]\n+\t\tless\n \tEOF\n '\n \n@@ -116,6 +118,45 @@ test_expect_success 'find value with the highest priority' '\n \tcheck_config get_value case.baz \"hask\"\n '\n \n+test_expect_success 'return value for an existing key' '\n+\ttest-tool config get lamb.chop >out 2>err &&\n+\ttest_must_be_empty out &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for value-less key' '\n+\ttest-tool config get value.less >out 2>err &&\n+\ttest_must_be_empty out &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for a missing key' '\n+\tcat >expect <<-\\EOF &&\n+\tValue not found for \"missing.key\"\n+\tEOF\n+\ttest_expect_code 1 test-tool config get missing.key >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty err\n+'\n+\n+test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '\n+\tcat >expect <<-\\EOF &&\n+\tKey \"fails.iskeychar.-\" is invalid\n+\tEOF\n+\ttest_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '\n+\tcat >expect <<-\\EOF &&\n+\tKey \"keynosection\" has no section\n+\tEOF\n+\ttest_expect_code 1 test-tool config get keynosection >actual 2>err &&\n+\ttest_cmp actual expect &&\n+\ttest_must_be_empty out\n+'\n+\n test_expect_success 'find integer value for a key' '\n \tcheck_config get_int lamb.chop 65\n '\n@@ -272,7 +313,7 @@ test_expect_success 'proper error on error in default config files' '\n \tcp .git/config .git/config.old &&\n \ttest_when_finished \"mv .git/config.old .git/config\" &&\n \techo \"[\" >>.git/config &&\n-\techo \"fatal: bad config line 34 in file .git/config\" >expect &&\n+\techo \"fatal: bad config line 36 in file .git/config\" >expect &&\n \ttest_expect_code 128 test-tool config get_value foo.bar 2>actual &&\n \ttest_cmp expect actual\n '\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"474305","messageId":"patch-v8-4.9-d7dfedb7225-20230328T140127Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"[PATCH v8 4/9] versioncmp.c: refactor config reading next commit","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-28T14:04:23Z","receivedAt":"2023-03-28T14:06:27Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the reading of the versionSort.suffix and\nversionSort.prereleaseSuffix configuration variables to stay within\nthe bounds of our CodingGuidelines when it comes to line length, and\nto avoid repeating ourselves.\n\nRenaming \"deprecated_prereleases\" to \"oldl\" doesn't help us to avoid\nline wrapping now, but it will in a subsequent commit.\n\nLet's also split out the names of the config variables into variables\nof our own, and refactor the nested if/else to avoid indenting it, and\nthe existing bracing style issue.\n\nThis all helps with the subsequent commit, where we'll need to start\nchecking different git_config_get_value_multi() return value. See\nc026557a373 (versioncmp: generalize version sort suffix reordering,\n2016-12-08) for the original implementation of most of this.\n\nMoving the \"initialized = 1\" assignment allows us to move some of this\nto the variable declarations in the subsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n versioncmp.c | 19 +++++++++++--------\n 1 file changed, 11 insertions(+), 8 deletions(-)\n\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 069ee94a4d7..323f5d35ea8 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)\n \t}\n \n \tif (!initialized) {\n-\t\tconst struct string_list *deprecated_prereleases;\n+\t\tconst char *const newk = \"versionsort.suffix\";\n+\t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *oldl;\n+\n+\t\tprereleases = git_config_get_value_multi(newk);\n+\t\toldl = git_config_get_value_multi(oldk);\n+\t\tif (prereleases && oldl)\n+\t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n+\t\telse if (!prereleases)\n+\t\t\tprereleases = oldl;\n+\n \t\tinitialized = 1;\n-\t\tprereleases = git_config_get_value_multi(\"versionsort.suffix\");\n-\t\tdeprecated_prereleases = git_config_get_value_multi(\"versionsort.prereleasesuffix\");\n-\t\tif (prereleases) {\n-\t\t\tif (deprecated_prereleases)\n-\t\t\t\twarning(\"ignoring versionsort.prereleasesuffix because versionsort.suffix is set\");\n-\t\t} else\n-\t\t\tprereleases = deprecated_prereleases;\n \t}\n \tif (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,\n \t\t\t\t\t    &diff))\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"474306","messageId":"patch-v8-5.9-840fb9d5c74-20230328T140127Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"[PATCH v8 5/9] config API: have *_multi() return an \"int\" and take a \"dest\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-28T14:04:24Z","receivedAt":"2023-03-28T14:06:38Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Have the \"git_configset_get_value_multi()\" function and its siblings\nreturn an \"int\" and populate a \"**dest\" parameter like every other\ngit_configset_get_*()\" in the API.\n\nAs we'll take advantage of in subsequent commits, this fixes a blind\nspot in the API where it wasn't possible to tell whether a list was\nempty from whether a config key existed. For now we don't make use of\nthose new return values, but faithfully convert existing API users.\n\nMost of this is straightforward, commentary on cases that stand out:\n\n- To ensure that we'll properly use the return values of this function\n  in the future we're using the \"RESULT_MUST_BE_USED\" macro introduced\n  in [1].\n\n  As git_die_config() now has to handle this return value let's have\n  it BUG() if it can't find the config entry. As tested for in a\n  preceding commit we can rely on getting the config list in\n  git_die_config().\n\n- The loops after getting the \"list\" value in \"builtin/gc.c\" could\n  also make use of \"unsorted_string_list_has_string()\" instead of using\n  that loop, but let's leave that for now.\n\n- In \"versioncmp.c\" we now use the return value of the functions,\n  instead of checking if the lists are still non-NULL.\n\n1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()\n   return values, 2022-09-01),\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c |  5 +----\n builtin/gc.c            | 10 ++++------\n builtin/log.c           |  6 +++---\n config.c                | 34 ++++++++++++++++++++--------------\n config.h                | 29 +++++++++++++++++++++--------\n pack-bitmap.c           |  6 +++++-\n submodule.c             |  3 +--\n t/helper/test-config.c  |  6 ++----\n versioncmp.c            | 11 +++++++----\n 9 files changed, 64 insertions(+), 46 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 6aeac371488..fd0e7739e6a 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -45,14 +45,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\tvalues = repo_config_get_value_multi(the_repository,\n-\t\t\t\t\t     config_key);\n-\n \t/*\n \t * Do nothing on an empty list, which is equivalent to the case\n \t * where the config variable does not exist at all.\n \t */\n-\tif (!values)\n+\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex b7251840e20..b87fb53a215 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1511,8 +1511,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tlist = git_config_get_value_multi(key);\n-\tif (list) {\n+\tif (!git_config_get_value_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1578,11 +1577,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \tif (config_file) {\n \t\tgit_configset_init(&cs);\n \t\tgit_configset_add_file(&cs, config_file);\n-\t\tlist = git_configset_get_value_multi(&cs, key);\n-\t} else {\n-\t\tlist = git_config_get_value_multi(key);\n \t}\n-\tif (list) {\n+\tif (!(config_file\n+\t      ? git_configset_get_value_multi(&cs, key, &list)\n+\t      : git_config_get_value_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 4693385e8ed..4e04efa5a72 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -185,10 +185,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tint i;\n \tchar *value = NULL;\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n-\tconst struct string_list *config_exclude =\n-\t\t\tgit_config_get_value_multi(\"log.excludeDecoration\");\n+\tconst struct string_list *config_exclude;\n \n-\tif (config_exclude) {\n+\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t&config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex ba2ec3b54ee..e8ea4533f94 100644\n--- a/config.c\n+++ b/config.c\n@@ -2421,29 +2421,34 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n+\tint ret;\n+\n \t/*\n \t * Follows \"last one wins\" semantic, i.e., if there are multiple matches for the\n \t * queried key in the files of the configset, the value returned will be the last\n \t * value in the value list for that key.\n \t */\n-\tvalues = git_configset_get_value_multi(cs, key);\n+\tif ((ret = git_configset_get_value_multi(cs, key, &values)))\n+\t\treturn ret;\n \n-\tif (!values)\n-\t\treturn 1;\n \tassert(values->nr > 0);\n \t*value = values->items[values->nr - 1].string;\n \treturn 0;\n }\n \n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest)\n {\n \tstruct config_set_element *e;\n+\tint ret;\n \n-\tif (configset_find_element(cs, key, &e))\n-\t\treturn NULL;\n+\tif ((ret = configset_find_element(cs, key, &e)))\n+\t\treturn ret;\n \telse if (!e)\n-\t\treturn NULL;\n-\treturn &e->value_list;\n+\t\treturn 1;\n+\t*dest = &e->value_list;\n+\n+\treturn 0;\n }\n \n int git_configset_get(struct config_set *cs, const char *key)\n@@ -2607,11 +2612,11 @@ int repo_config_get_value(struct repository *repo,\n \treturn git_configset_get_value(repo->config, key, value);\n }\n \n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key)\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest)\n {\n \tgit_config_check_init(repo);\n-\treturn git_configset_get_value_multi(repo->config, key);\n+\treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n int repo_config_get_string(struct repository *repo,\n@@ -2724,9 +2729,9 @@ int git_config_get_value(const char *key, const char **value)\n \treturn repo_config_get_value(the_repository, key, value);\n }\n \n-const struct string_list *git_config_get_value_multi(const char *key)\n+int git_config_get_value_multi(const char *key, const struct string_list **dest)\n {\n-\treturn repo_config_get_value_multi(the_repository, key);\n+\treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n int git_config_get_string(const char *key, char **dest)\n@@ -2873,7 +2878,8 @@ void git_die_config(const char *key, const char *err, ...)\n \t\terror_fn(err, params);\n \t\tva_end(params);\n \t}\n-\tvalues = git_config_get_value_multi(key);\n+\tif (git_config_get_value_multi(key, &values))\n+\t\tBUG(\"for key '%s' we must have a value to report on\", key);\n \tkv_info = values->items[values->nr - 1].util;\n \tgit_die_config_linenr(key, kv_info->filename, kv_info->linenr);\n }\ndiff --git a/config.h b/config.h\nindex 72d83e21e3d..109c845663d 100644\n--- a/config.h\n+++ b/config.h\n@@ -450,10 +450,18 @@ int git_configset_add_file(struct config_set *cs, const char *filename);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n- * configuration variable `key` is not found, returns NULL. The caller\n- * should not free or modify the returned pointer, as it is owned by the cache.\n+ * configuration variable `key` is not found, returns 1 without touching\n+ * `value`.\n+ *\n+ * The key will be parsed for validity with git_config_parse_key(), on\n+ * error a negative value will be returned.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);\n+RESULT_MUST_BE_USED\n+int git_configset_get_value_multi(struct config_set *cs, const char *key,\n+\t\t\t\t  const struct string_list **dest);\n \n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n@@ -502,8 +510,9 @@ RESULT_MUST_BE_USED\n int repo_config_get(struct repository *repo, const char *key);\n int repo_config_get_value(struct repository *repo,\n \t\t\t  const char *key, const char **value);\n-const struct string_list *repo_config_get_value_multi(struct repository *repo,\n-\t\t\t\t\t\t      const char *key);\n+RESULT_MUST_BE_USED\n+int repo_config_get_value_multi(struct repository *repo, const char *key,\n+\t\t\t\tconst struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -557,10 +566,14 @@ int git_config_get_value(const char *key, const char **value);\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key`. When the configuration variable\n- * `key` is not found, returns NULL. The caller should not free or modify\n- * the returned pointer, as it is owned by the cache.\n+ * `key` is not found, returns 1 without touching `value`.\n+ *\n+ * The caller should not free or modify the returned pointer, as it is\n+ * owned by the cache.\n  */\n-const struct string_list *git_config_get_value_multi(const char *key);\n+RESULT_MUST_BE_USED\n+int git_config_get_value_multi(const char *key,\n+\t\t\t       const struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex ca7c81b5c9f..4c1e6fed631 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2318,7 +2318,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)\n \n const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n-\treturn repo_config_get_value_multi(r, \"pack.preferbitmaptips\");\n+\tconst struct string_list *dest;\n+\n+\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\t\treturn dest;\n+\treturn NULL;\n }\n \n int bitmap_is_preferred_refname(struct repository *r, const char *refname)\ndiff --git a/submodule.c b/submodule.c\nindex 2a057c35b74..85b1ccbf784 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -275,8 +275,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tsl = repo_config_get_value_multi(repo, \"submodule.active\");\n-\tif (sl) {\n+\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/helper/test-config.c b/t/helper/test-config.c\nindex cbb33ae1fff..6dc4c37444f 100644\n--- a/t/helper/test-config.c\n+++ b/t/helper/test-config.c\n@@ -97,8 +97,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\tgoto exit1;\n \t\t}\n \t} else if (argc == 3 && !strcmp(argv[1], \"get_value_multi\")) {\n-\t\tstrptr = git_config_get_value_multi(argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_config_get_value_multi(argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\n@@ -181,8 +180,7 @@ int cmd__config(int argc, const char **argv)\n \t\t\t\tgoto exit2;\n \t\t\t}\n \t\t}\n-\t\tstrptr = git_configset_get_value_multi(&cs, argv[2]);\n-\t\tif (strptr) {\n+\t\tif (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {\n \t\t\tfor (i = 0; i < strptr->nr; i++) {\n \t\t\t\tv = strptr->items[i].string;\n \t\t\t\tif (!v)\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 323f5d35ea8..60c3a517122 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)\n \tif (!initialized) {\n \t\tconst char *const newk = \"versionsort.suffix\";\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n+\t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n+\t\tint new = git_config_get_value_multi(newk, &newl);\n+\t\tint old = git_config_get_value_multi(oldk, &oldl);\n \n-\t\tprereleases = git_config_get_value_multi(newk);\n-\t\toldl = git_config_get_value_multi(oldk);\n-\t\tif (prereleases && oldl)\n+\t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-\t\telse if (!prereleases)\n+\t\tif (!new)\n+\t\t\tprereleases = newl;\n+\t\telse if (!old)\n \t\t\tprereleases = oldl;\n \n \t\tinitialized = 1;\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"474307","messageId":"patch-v8-6.9-75a68b14217-20230328T140127Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"[PATCH v8 6/9] for-each-repo: error on bad --config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-28T14:04:25Z","receivedAt":"2023-03-28T14:06:41Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in 6c62f015520 (for-each-repo: do nothing on empty config,\n2021-01-08) this command wants to ignore a non-existing config key,\nbut let's not conflate that with bad config.\n\nBefore this, all these added tests would pass with an exit code of 0.\n\nWe could preserve the comment added in 6c62f015520, but now that we're\ndirectly using the documented repo_config_get_value_multi() value it's\njust narrating something that should be obvious from the API use, so\nlet's drop it.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  | 11 ++++++-----\n t/t0068-for-each-repo.sh |  6 ++++++\n 2 files changed, 12 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex fd0e7739e6a..224164addb3 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tstatic const char *config_key = NULL;\n \tint i, result = 0;\n \tconst struct string_list *values;\n+\tint err;\n \n \tconst struct option options[] = {\n \t\tOPT_STRING(0, \"config\", &config_key, N_(\"config\"),\n@@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\t/*\n-\t * Do nothing on an empty list, which is equivalent to the case\n-\t * where the config variable does not exist at all.\n-\t */\n-\tif (repo_config_get_value_multi(the_repository, config_key, &values))\n+\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\tif (err < 0)\n+\t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n+\t\t\t       for_each_repo_usage, options, config_key);\n+\telse if (err)\n \t\treturn 0;\n \n \tfor (i = 0; !result && i < values->nr; i++)\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 3648d439a87..6b51e00da0e 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '\n \tgit for-each-repo --config=bogus.config -- help --no-such-option\n '\n \n+test_expect_success 'error on bad config keys' '\n+\ttest_expect_code 129 git for-each-repo --config=a &&\n+\ttest_expect_code 129 git for-each-repo --config=a.b. &&\n+\ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n+'\n+\n test_done\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"474308","messageId":"patch-v8-8.9-686b512c3df-20230328T140127Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"[PATCH v8 8/9] config API: add \"string\" version of *_value_multi(), fix segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-28T14:04:27Z","receivedAt":"2023-03-28T14:06:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix numerous and mostly long-standing segfaults in consumers of\nthe *_config_*value_multi() API. As discussed in the preceding commit\nan empty key in the config syntax yields a \"NULL\" string, which these\nusers would give to strcmp() (or similar), resulting in segfaults.\n\nAs this change shows, most users users of the *_config_*value_multi()\nAPI didn't really want such an an unsafe and low-level API, let's give\nthem something with the safety of git_config_get_string() instead.\n\nThis fix is similar to what the *_string() functions and others\nacquired in[1] and [2]. Namely introducing and using a safer\n\"*_get_string_multi()\" variant of the low-level \"_*value_multi()\"\nfunction.\n\nThis fixes segfaults in code introduced in:\n\n  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)\n  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)\n  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)\n  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)\n  - 92156291ca8 (log: add default decoration filter, 2022-08-05)\n  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)\n\nThere are now two users ofthe low-level API:\n\n- One in \"builtin/for-each-repo.c\", which we'll convert in a\n  subsequent commit.\n\n- The \"t/helper/test-config.c\" code added in [3].\n\nAs seen in the preceding commit we need to give the\n\"t/helper/test-config.c\" caller these \"NULL\" entries.\n\nWe could also alter the underlying git_configset_get_value_multi()\nfunction to be \"string safe\", but doing so would leave no room for\nother variants of \"*_get_value_multi()\" that coerce to other types.\n\nSuch coercion can't be built on the string version, since as we've\nestablished \"NULL\" is a true value in the boolean context, but if we\ncoerced it to \"\" for use in a list of strings it'll be subsequently\ncoerced to \"false\" as a boolean.\n\nThe callback pattern being used here will make it easy to introduce\ne.g. a \"multi\" variant which coerces its values to \"bool\", \"int\",\n\"path\" etc.\n\n1. 40ea4ed9032 (Add config_error_nonbool() helper function,\n   2008-02-11)\n2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,\n   2008-02-11).\n3. 4c715ebb96a (test-config: add tests for the config_set API,\n   2014-07-28)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c                   |  6 +++---\n builtin/log.c                  |  4 ++--\n config.c                       | 32 ++++++++++++++++++++++++++++++++\n config.h                       | 19 +++++++++++++++++++\n pack-bitmap.c                  |  2 +-\n submodule.c                    |  2 +-\n t/t4202-log.sh                 |  8 ++++++--\n t/t5310-pack-bitmaps.sh        |  8 ++++++--\n t/t7004-tag.sh                 |  9 +++++++--\n t/t7413-submodule-is-active.sh |  8 ++++++--\n t/t7900-maintenance.sh         | 25 ++++++++++++++++++++-----\n versioncmp.c                   |  4 ++--\n 12 files changed, 105 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex b87fb53a215..efc1b9a0fda 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -1511,7 +1511,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)\n \tif (git_config_get(\"maintenance.strategy\"))\n \t\tgit_config_set(\"maintenance.strategy\", \"incremental\");\n \n-\tif (!git_config_get_value_multi(key, &list)) {\n+\tif (!git_config_get_string_multi(key, &list)) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\n@@ -1579,8 +1579,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi\n \t\tgit_configset_add_file(&cs, config_file);\n \t}\n \tif (!(config_file\n-\t      ? git_configset_get_value_multi(&cs, key, &list)\n-\t      : git_config_get_value_multi(key, &list))) {\n+\t      ? git_configset_get_string_multi(&cs, key, &list)\n+\t      : git_config_get_string_multi(key, &list))) {\n \t\tfor_each_string_list_item(item, list) {\n \t\t\tif (!strcmp(maintpath, item->string)) {\n \t\t\t\tfound = 1;\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 4e04efa5a72..0c6556b2d78 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -187,8 +187,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f\n \tstruct string_list *include = decoration_filter->include_ref_pattern;\n \tconst struct string_list *config_exclude;\n \n-\tif (!git_config_get_value_multi(\"log.excludeDecoration\",\n-\t\t\t\t\t&config_exclude)) {\n+\tif (!git_config_get_string_multi(\"log.excludeDecoration\",\n+\t\t\t\t\t &config_exclude)) {\n \t\tstruct string_list_item *item;\n \t\tfor_each_string_list_item(item, config_exclude)\n \t\t\tstring_list_append(decoration_filter->exclude_ref_config_pattern,\ndiff --git a/config.c b/config.c\nindex e8ea4533f94..f80a068fcad 100644\n--- a/config.c\n+++ b/config.c\n@@ -2451,6 +2451,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \treturn 0;\n }\n \n+static int check_multi_string(struct string_list_item *item, void *util)\n+{\n+\treturn item->string ? 0 : config_error_nonbool(util);\n+}\n+\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest)\n+{\n+\tint ret;\n+\n+\tif ((ret = git_configset_get_value_multi(cs, key, dest)))\n+\t\treturn ret;\n+\tif ((ret = for_each_string_list((struct string_list *)*dest,\n+\t\t\t\t\tcheck_multi_string, (void *)key)))\n+\t\treturn ret;\n+\n+\treturn 0;\n+}\n+\n int git_configset_get(struct config_set *cs, const char *key)\n {\n \tstruct config_set_element *e;\n@@ -2619,6 +2638,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,\n \treturn git_configset_get_value_multi(repo->config, key, dest);\n }\n \n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest)\n+{\n+\tgit_config_check_init(repo);\n+\treturn git_configset_get_string_multi(repo->config, key, dest);\n+}\n+\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest)\n {\n@@ -2734,6 +2760,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)\n \treturn repo_config_get_value_multi(the_repository, key, dest);\n }\n \n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest)\n+{\n+\treturn repo_config_get_string_multi(the_repository, key, dest);\n+}\n+\n int git_config_get_string(const char *key, char **dest)\n {\n \treturn repo_config_get_string(the_repository, key, dest);\ndiff --git a/config.h b/config.h\nindex 109c845663d..4a6e3f19e5d 100644\n--- a/config.h\n+++ b/config.h\n@@ -463,6 +463,19 @@ RESULT_MUST_BE_USED\n int git_configset_get_value_multi(struct config_set *cs, const char *key,\n \t\t\t\t  const struct string_list **dest);\n \n+/**\n+ * A validation wrapper for git_configset_get_value_multi() which does\n+ * for it what git_configset_get_string() does for\n+ * git_configset_get_value().\n+ *\n+ * The configuration syntax allows for \"[section] key\", which will\n+ * give us a NULL entry in the \"struct string_list\", as opposed to\n+ * \"[section] key =\" which is the empty string. Most users of the API\n+ * are not prepared to handle NULL in a \"struct string_list\".\n+ */\n+int git_configset_get_string_multi(struct config_set *cs, const char *key,\n+\t\t\t\t   const struct string_list **dest);\n+\n /**\n  * Clears `config_set` structure, removes all saved variable-value pairs.\n  */\n@@ -513,6 +526,9 @@ int repo_config_get_value(struct repository *repo,\n RESULT_MUST_BE_USED\n int repo_config_get_value_multi(struct repository *repo, const char *key,\n \t\t\t\tconst struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int repo_config_get_string_multi(struct repository *repo, const char *key,\n+\t\t\t\t const struct string_list **dest);\n int repo_config_get_string(struct repository *repo,\n \t\t\t   const char *key, char **dest);\n int repo_config_get_string_tmp(struct repository *repo,\n@@ -574,6 +590,9 @@ int git_config_get_value(const char *key, const char **value);\n RESULT_MUST_BE_USED\n int git_config_get_value_multi(const char *key,\n \t\t\t       const struct string_list **dest);\n+RESULT_MUST_BE_USED\n+int git_config_get_string_multi(const char *key,\n+\t\t\t\tconst struct string_list **dest);\n \n /**\n  * Resets and invalidates the config cache.\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 4c1e6fed631..f8ab6be411d 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2320,7 +2320,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)\n {\n \tconst struct string_list *dest;\n \n-\tif (!repo_config_get_value_multi(r, \"pack.preferbitmaptips\", &dest))\n+\tif (!repo_config_get_string_multi(r, \"pack.preferbitmaptips\", &dest))\n \t\treturn dest;\n \treturn NULL;\n }\ndiff --git a/submodule.c b/submodule.c\nindex 85b1ccbf784..c9579f9a3f8 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -275,7 +275,7 @@ int is_tree_submodule_active(struct repository *repo,\n \tfree(key);\n \n \t/* submodule.active is set */\n-\tif (!repo_config_get_value_multi(repo, \"submodule.active\", &sl)) {\n+\tif (!repo_config_get_string_multi(repo, \"submodule.active\", &sl)) {\n \t\tstruct pathspec ps;\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tconst struct string_list_item *item;\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex e4f02d8208b..ae73aef922f 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n-test_expect_failure 'parse log.excludeDecoration with no value' '\n+test_expect_success 'parse log.excludeDecoration with no value' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '\n \t[log]\n \t\texcludeDecoration\n \tEOF\n-\tgit log --decorate=short\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''log.excludeDecoration'\\''\n+\tEOF\n+\tgit log --decorate=short 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'decorate-refs with glob' '\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 0306b399188..526a5a506eb 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,7 +404,7 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n-\ttest_expect_failure 'pack.preferBitmapTips' '\n+\ttest_expect_success 'pack.preferBitmapTips' '\n \t\tgit init repo &&\n \t\ttest_when_finished \"rm -rf repo\" &&\n \t\t(\n@@ -416,7 +416,11 @@ test_bitmap_cases () {\n \t\t\t[pack]\n \t\t\t\tpreferBitmapTips\n \t\t\tEOF\n-\t\t\tgit repack -adb\n+\t\t\tcat >expect <<-\\EOF &&\n+\t\t\terror: missing value for '\\''pack.preferbitmaptips'\\''\n+\t\t\tEOF\n+\t\t\tgit repack -adb 2>actual &&\n+\t\t\ttest_cmp expect actual\n \t\t)\n \t'\n \ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex f343551a7d4..f4a31ada79a 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas\n \t\tprereleaseSuffix\n \t\tsuffix\n \tEOF\n-\tgit tag -l --sort=version:refname\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''versionsort.suffix'\\''\n+\terror: missing value for '\\''versionsort.prereleasesuffix'\\''\n+\tEOF\n+\tgit tag -l --sort=version:refname 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'version sort with prerelease reordering' '\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex bfe27e50732..887d181b72e 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n-test_expect_failure 'is-active handles submodule.active config missing a value' '\n+test_expect_success 'is-active handles submodule.active config missing a value' '\n \tcp super/.git/config super/.git/config.orig &&\n \ttest_when_finished mv super/.git/config.orig super/.git/config &&\n \n@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'\n \t\tactive\n \tEOF\n \n-\ttest-tool -C super submodule is-active sub1\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''submodule.active'\\''\n+\tEOF\n+\ttest-tool -C super submodule is-active sub1 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'is-active works with basic submodule.active config' '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d82eac6a471..487e326b3fa 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n-test_expect_failure 'register with no value for maintenance.repo' '\n+test_expect_success 'register with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance register\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\tgit maintenance register 2>actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n-test_expect_failure 'unregister with no value for maintenance.repo' '\n+test_expect_success 'unregister with no value for maintenance.repo' '\n \tcp .git/config .git/config.orig &&\n \ttest_when_finished mv .git/config.orig .git/config &&\n \n@@ -543,8 +548,18 @@ test_expect_failure 'unregister with no value for maintenance.repo' '\n \t[maintenance]\n \t\trepo\n \tEOF\n-\tgit maintenance unregister &&\n-\tgit maintenance unregister --force\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''maintenance.repo'\\''\n+\tEOF\n+\ttest_expect_code 128 git maintenance unregister 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo &&\n+\n+\tgit maintenance unregister --force 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config maintenance.repo\n '\n \n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\ndiff --git a/versioncmp.c b/versioncmp.c\nindex 60c3a517122..7498da96e0e 100644\n--- a/versioncmp.c\n+++ b/versioncmp.c\n@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)\n \t\tconst char *const oldk = \"versionsort.prereleasesuffix\";\n \t\tconst struct string_list *newl;\n \t\tconst struct string_list *oldl;\n-\t\tint new = git_config_get_value_multi(newk, &newl);\n-\t\tint old = git_config_get_value_multi(oldk, &oldl);\n+\t\tint new = git_config_get_string_multi(newk, &newl);\n+\t\tint old = git_config_get_string_multi(oldk, &oldl);\n \n \t\tif (!new && !old)\n \t\t\twarning(\"ignoring %s because %s is set\", oldk, newk);\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"474309","messageId":"patch-v8-7.9-a78056e2748-20230328T140127Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"[PATCH v8 7/9] config API users: test for *_get_value_multi() segfaults","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-28T14:04:26Z","receivedAt":"2023-03-28T14:06:51Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As we'll discuss in the subsequent commit these tests all\nshow *_get_value_multi() API users unable to handle there being a\nvalue-less key in the config, which is represented with a \"NULL\" for\nthat entry in the \"string\" member of the returned \"struct\nstring_list\", causing a segfault.\n\nThese added tests exhaustively test for that issue, as we'll see in a\nsubsequent commit we'll need to change all of the API users\nof *_get_value_multi(). These cases were discovered by triggering each\none individually, and then adding these tests.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t4202-log.sh                 | 11 +++++++++++\n t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++\n t/t7004-tag.sh                 | 12 ++++++++++++\n t/t7413-submodule-is-active.sh | 12 ++++++++++++\n t/t7900-maintenance.sh         | 23 +++++++++++++++++++++++\n 5 files changed, 74 insertions(+)\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 2ce2b41174d..e4f02d8208b 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '\n \n '\n \n+test_expect_failure 'parse log.excludeDecoration with no value' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[log]\n+\t\texcludeDecoration\n+\tEOF\n+\tgit log --decorate=short\n+'\n+\n test_expect_success 'decorate-refs with glob' '\n \tcat >expect.decorate <<-\\EOF &&\n \tMerge-tag-reach\ndiff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh\nindex 7d8dee41b0d..0306b399188 100755\n--- a/t/t5310-pack-bitmaps.sh\n+++ b/t/t5310-pack-bitmaps.sh\n@@ -404,6 +404,22 @@ test_bitmap_cases () {\n \t\t)\n \t'\n \n+\ttest_expect_failure 'pack.preferBitmapTips' '\n+\t\tgit init repo &&\n+\t\ttest_when_finished \"rm -rf repo\" &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\tgit config pack.writeBitmapLookupTable '\"$writeLookupTable\"' &&\n+\t\t\ttest_commit_bulk --message=\"%s\" 103 &&\n+\n+\t\t\tcat >>.git/config <<-\\EOF &&\n+\t\t\t[pack]\n+\t\t\t\tpreferBitmapTips\n+\t\t\tEOF\n+\t\t\tgit repack -adb\n+\t\t)\n+\t'\n+\n \ttest_expect_success 'complains about multiple pack bitmaps' '\n \t\trm -fr repo &&\n \t\tgit init repo &&\ndiff --git a/t/t7004-tag.sh b/t/t7004-tag.sh\nindex 9aa1660651b..f343551a7d4 100755\n--- a/t/t7004-tag.sh\n+++ b/t/t7004-tag.sh\n@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '\n \ttest_must_fail git tag -l \"foo*\"\n '\n \n+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[versionsort]\n+\t\tprereleaseSuffix\n+\t\tsuffix\n+\tEOF\n+\tgit tag -l --sort=version:refname\n+'\n+\n test_expect_success 'version sort with prerelease reordering' '\n \ttest_config versionsort.prereleaseSuffix -rc &&\n \tgit tag foo1.6-rc1 &&\ndiff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh\nindex 7cdc2637649..bfe27e50732 100755\n--- a/t/t7413-submodule-is-active.sh\n+++ b/t/t7413-submodule-is-active.sh\n@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '\n \ttest-tool -C super submodule is-active sub1\n '\n \n+test_expect_failure 'is-active handles submodule.active config missing a value' '\n+\tcp super/.git/config super/.git/config.orig &&\n+\ttest_when_finished mv super/.git/config.orig super/.git/config &&\n+\n+\tcat >>super/.git/config <<-\\EOF &&\n+\t[submodule]\n+\t\tactive\n+\tEOF\n+\n+\ttest-tool -C super submodule is-active sub1\n+'\n+\n test_expect_success 'is-active works with basic submodule.active config' '\n \ttest_when_finished \"git -C super config submodule.sub1.URL ../sub\" &&\n \ttest_when_finished \"git -C super config --unset-all submodule.active\" &&\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex 823331e44a0..d82eac6a471 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -524,6 +524,29 @@ test_expect_success 'register and unregister' '\n \tgit maintenance unregister --config-file ./other --force\n '\n \n+test_expect_failure 'register with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance register\n+'\n+\n+test_expect_failure 'unregister with no value for maintenance.repo' '\n+\tcp .git/config .git/config.orig &&\n+\ttest_when_finished mv .git/config.orig .git/config &&\n+\n+\tcat >>.git/config <<-\\EOF &&\n+\t[maintenance]\n+\t\trepo\n+\tEOF\n+\tgit maintenance unregister &&\n+\tgit maintenance unregister --force\n+'\n+\n test_expect_success !MINGW 'register and unregister with regex metacharacters' '\n \tMETA=\"a+b*c\" &&\n \tgit init \"$META\" &&\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"474310","messageId":"patch-v8-9.9-6fce633493b-20230328T140127Z-avarab@gmail.com","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"[PATCH v8 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2023-03-28T14:04:28Z","receivedAt":"2023-03-28T14:06:54Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on\nconfigured repos, 2020-09-11). Due to assuming that elements returned\nfrom the repo_config_get_value_multi() call wouldn't be \"NULL\" we'd\nconflate the <path> and <command> part of the argument list when\nrunning commands.\n\nAs noted in the preceding commit the fix is to move to a safer\n\"*_string_multi()\" version of the *_multi() API. This change is\nseparated from the rest because those all segfaulted. In this change\nwe ended up with different behavior.\n\nWhen using the \"--config=<config>\" form we take each element of the\nlist as a path to a repository. E.g. with a configuration like:\n\n\t[repo] list = /some/repo\n\nWe would, with this command:\n\n\tgit for-each-repo --config=repo.list status builtin\n\nRun a \"git status\" in /some/repo, as:\n\n\tgit -C /some/repo status builtin\n\nI.e. ask \"status\" to report on the \"builtin\" directory. But since a\nconfiguration such as this would result in a \"struct string_list *\"\nwith one element, whose \"string\" member is \"NULL\":\n\n\t[repo] list\n\nWe would, when constructing our command-line in\n\"builtin/for-each-repo.c\"...\n\n\tstrvec_pushl(&child.args, \"-C\", path, NULL);\n\tfor (i = 0; i < argc; i++)\n\t\tstrvec_push(&child.args, argv[i]);\n\n...have that \"path\" be \"NULL\", and as strvec_pushl() stops when it\nsees NULL we'd end with the first \"argv\" element as the argument to\nthe \"-C\" option, e.g.:\n\n\tgit -C status builtin\n\nI.e. we'd run the command \"builtin\" in the \"status\" directory.\n\nIn another context this might be an interesting security\nvulnerability, but I think that this amounts to a nothingburger on\nthat front.\n\nA hypothetical attacker would need to be able to write config for the\nvictim to run, if they're able to do that there's more interesting\nattack vectors. See the \"safe.directory\" facility added in\n8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).\n\nAn even more unlikely possibility would be an attacker able to\ngenerate the config used for \"for-each-repo --config=<key>\", but\nnothing else (e.g. an automated system producing that list).\n\nEven in that case the attack vector is limited to the user running\ncommands whose name matches a directory that's interesting to the\nattacker (e.g. a \"log\" directory in a repository). The second\nargument (if any) of the command is likely to make git die without\ndoing anything interesting (e.g. \"-p\" to \"log\", there being no \"-p\"\nbuilt-in command to run).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/for-each-repo.c  |  2 +-\n t/t0068-for-each-repo.sh | 13 +++++++++++++\n 2 files changed, 14 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\nindex 224164addb3..ce8f7a99086 100644\n--- a/builtin/for-each-repo.c\n+++ b/builtin/for-each-repo.c\n@@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n \tif (!config_key)\n \t\tdie(_(\"missing --config=<config>\"));\n \n-\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n+\terr = repo_config_get_string_multi(the_repository, config_key, &values);\n \tif (err < 0)\n \t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n \t\t\t       for_each_repo_usage, options, config_key);\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 6b51e00da0e..4b90b74d5d5 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -46,4 +46,17 @@ test_expect_success 'error on bad config keys' '\n \ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n '\n \n+test_expect_success 'error on NULL value for config keys' '\n+\tcat >>.git/config <<-\\EOF &&\n+\t[empty]\n+\t\tkey\n+\tEOF\n+\tcat >expect <<-\\EOF &&\n+\terror: missing value for '\\''empty.key'\\''\n+\tEOF\n+\ttest_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&\n+\tgrep ^error actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.40.0.rc1.1034.g5867a1b10c5\n\n"},{"id":"474324","messageId":"kl6lo7ocdd8w.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"Re: [PATCH v8 0/9] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-03-28T16:58:23Z","receivedAt":"2023-03-28T16:58:29Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> This series fixes numerous segfaults in config API users, because they\n> didn't expect *_get_multi() to hand them a string_list with a NULL in\n> it given config like \"[a] key\" (note, no \"=\"'s).\n>\n> [..]\n>\n> * A trivial documentation change to 3/9, to clarify which doc in\n>   config.h refer to what. As noted in the v7 discussion I think that\n>   config.h could use some larger cleanups in this area, but let's\n>   leave that for some future topic.\n>\n> [...]\n>\n> Range-diff against v7:\n>  1:  9f297a35e14 =  1:  b600354c0f6 config tests: cover blind spots in git_die_config() tests\n>  2:  45d483066ef =  2:  49908f0bcf3 config tests: add \"NULL\" tests for *_get_value_multi()\n>  3:  a977b7b188f !  3:  d163b3d04ff config API: add and use a \"git_config_get()\" family of functions\n>     @@ config.h: void git_configset_clear(struct config_set *cs);\n>        * value in the 'dest' pointer.\n>        */\n>       \n>     ++/**\n>     ++ * git_configset_get() returns negative values on error, see\n>     ++ * repo_config_get() below.\n>     ++ */\n>      +RESULT_MUST_BE_USED\n>      +int git_configset_get(struct config_set *cs, const char *key);\n>      +\n\nThanks! I read through config.h to be sure, and the result looks pretty\nclear to me.\n\n>  4:  3a5a323cd91 =  4:  d7dfedb7225 versioncmp.c: refactor config reading next commit\n>  5:  dced12a40d2 =  5:  840fb9d5c74 config API: have *_multi() return an \"int\" and take a \"dest\"\n>  6:  d910f7e3a27 =  6:  75a68b14217 for-each-repo: error on bad --config\n>  7:  57db0fcd91f =  7:  a78056e2748 config API users: test for *_get_value_multi() segfaults\n>  8:  b374a716555 =  8:  686b512c3df config API: add \"string\" version of *_value_multi(), fix segfaults\n>  9:  6791e1f6f85 =  9:  6fce633493b for-each-repo: with bad config, don't conflate <path> and <cmd>\n\nReviewed-by: Glen Choo <chooglen@google.com>\n"},{"id":"474325","messageId":"xmqqo7ocg665.fsf@gitster.g","threadId":"58696","inReplyTo":"kl6lo7ocdd8w.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v8 0/9] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-03-28T17:02:58Z","receivedAt":"2023-03-28T17:03:07Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n> Thanks! I read through config.h to be sure, and the result looks pretty\n> clear to me.\n\nThanks, both.  Replaced and will mark the topic for 'next' soonish.\n\n"},{"id":"474430","messageId":"xmqqwn2z6w40.fsf@gitster.g","threadId":"58696","inReplyTo":"cover-v8-0.9-00000000000-20230328T140126Z-avarab@gmail.com","subject":"Re: [PATCH v8 0/9] config API: make \"multi\" safe, fix segfaults, propagate \"ret\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-03-29T22:17:19Z","receivedAt":"2023-03-29T22:17:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> A larger general overview at v1[1], but note the API changes in\n> v2[2]. Changes since v7[3]:\n>\n> * A trivial documentation change to 3/9, to clarify which doc in\n>   config.h refer to what. As noted in the v7 discussion I think that\n>   config.h could use some larger cleanups in this area, but let's\n>   leave that for some future topic.\n\nThanks.  Will replace.\n"},{"id":"475003","messageId":"20230407155132.GA3117@szeder.dev","threadId":"58696","inReplyTo":"patch-v8-9.9-6fce633493b-20230328T140127Z-avarab@gmail.com","subject":"Re: [PATCH v8 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2023-04-07T15:51:32Z","receivedAt":"2023-04-07T15:51:39Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Tue, Mar 28, 2023 at 04:04:28PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on\n> configured repos, 2020-09-11). Due to assuming that elements returned\n> from the repo_config_get_value_multi() call wouldn't be \"NULL\" we'd\n> conflate the <path> and <command> part of the argument list when\n> running commands.\n> \n> As noted in the preceding commit the fix is to move to a safer\n> \"*_string_multi()\" version of the *_multi() API. This change is\n> separated from the rest because those all segfaulted. In this change\n> we ended up with different behavior.\n> \n> When using the \"--config=<config>\" form we take each element of the\n> list as a path to a repository. E.g. with a configuration like:\n> \n> \t[repo] list = /some/repo\n> \n> We would, with this command:\n> \n> \tgit for-each-repo --config=repo.list status builtin\n> \n> Run a \"git status\" in /some/repo, as:\n> \n> \tgit -C /some/repo status builtin\n> \n> I.e. ask \"status\" to report on the \"builtin\" directory. But since a\n> configuration such as this would result in a \"struct string_list *\"\n> with one element, whose \"string\" member is \"NULL\":\n> \n> \t[repo] list\n> \n> We would, when constructing our command-line in\n> \"builtin/for-each-repo.c\"...\n> \n> \tstrvec_pushl(&child.args, \"-C\", path, NULL);\n> \tfor (i = 0; i < argc; i++)\n> \t\tstrvec_push(&child.args, argv[i]);\n> \n> ...have that \"path\" be \"NULL\", and as strvec_pushl() stops when it\n> sees NULL we'd end with the first \"argv\" element as the argument to\n> the \"-C\" option, e.g.:\n> \n> \tgit -C status builtin\n> \n> I.e. we'd run the command \"builtin\" in the \"status\" directory.\n> \n> In another context this might be an interesting security\n> vulnerability, but I think that this amounts to a nothingburger on\n> that front.\n> \n> A hypothetical attacker would need to be able to write config for the\n> victim to run, if they're able to do that there's more interesting\n> attack vectors. See the \"safe.directory\" facility added in\n> 8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).\n> \n> An even more unlikely possibility would be an attacker able to\n> generate the config used for \"for-each-repo --config=<key>\", but\n> nothing else (e.g. an automated system producing that list).\n> \n> Even in that case the attack vector is limited to the user running\n> commands whose name matches a directory that's interesting to the\n> attacker (e.g. a \"log\" directory in a repository). The second\n> argument (if any) of the command is likely to make git die without\n> doing anything interesting (e.g. \"-p\" to \"log\", there being no \"-p\"\n> built-in command to run).\n> \n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  builtin/for-each-repo.c  |  2 +-\n>  t/t0068-for-each-repo.sh | 13 +++++++++++++\n>  2 files changed, 14 insertions(+), 1 deletion(-)\n> \n> diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c\n> index 224164addb3..ce8f7a99086 100644\n> --- a/builtin/for-each-repo.c\n> +++ b/builtin/for-each-repo.c\n> @@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)\n>  \tif (!config_key)\n>  \t\tdie(_(\"missing --config=<config>\"));\n>  \n> -\terr = repo_config_get_value_multi(the_repository, config_key, &values);\n> +\terr = repo_config_get_string_multi(the_repository, config_key, &values);\n>  \tif (err < 0)\n>  \t\tusage_msg_optf(_(\"got bad config --config=%s\"),\n>  \t\t\t       for_each_repo_usage, options, config_key);\n> diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\n> index 6b51e00da0e..4b90b74d5d5 100755\n> --- a/t/t0068-for-each-repo.sh\n> +++ b/t/t0068-for-each-repo.sh\n> @@ -46,4 +46,17 @@ test_expect_success 'error on bad config keys' '\n>  \ttest_expect_code 129 git for-each-repo --config=\"'\\''.b\"\n>  '\n>  \n> +test_expect_success 'error on NULL value for config keys' '\n> +\tcat >>.git/config <<-\\EOF &&\n> +\t[empty]\n> +\t\tkey\n> +\tEOF\n> +\tcat >expect <<-\\EOF &&\n> +\terror: missing value for '\\''empty.key'\\''\n> +\tEOF\n> +\ttest_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&\n> +\tgrep ^error actual.raw >actual &&\n> +\ttest_cmp expect actual\n> +'\n\nIn this case the full error message looks like this:\n\n  $ ./git -c empty.key for-each-repo --config=empty.key\n  error: missing value for 'empty.key'\n  fatal: got bad config --config=empty.key\n\n  usage: git for-each-repo --config=<config> [--] <arguments>\n\n      --config <config>     config key storing a list of repository paths\n\nHaving both an \"error:\" and a \"fatal:\" message seems redundant.\n\n\nOn a related note, according to the usage shown above (and the\nsynopsis in the man page), 'git for-each-repo' expects mandatory\n<arguments>, but this doesn't seem to be enforced, and invoking it\nwithout any arguments results in the usage of the main git command:\n\n  $ ./git -c empty.key=. for-each-repo --config=empty.key\n  usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]\n             [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]\n             [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]\n             [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]\n             [--config-env=<name>=<envvar>] <command> [<args>]\n  \n  These are common Git commands used in various situations:\n  \n  start a working area (see also: git help tutorial)\n  [...]\n\nThis is misleading, because without any hints as to what was wrong I\nthought that the problem is with the options of the main git command,\nnot with the (lack of) arguments of the 'for-each-repo' command.\n\n\n"}]}