git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults
@ 2022-10-26 15:35 Ævar Arnfjörð Bjarmason
  2022-10-26 15:35 ` [PATCH 01/10] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
                   ` (11 more replies)
  0 siblings, 12 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

This series is a follow-up to an earlier RFC Stolee sent about making
the *_multi() config API return non-NULL, and instead give you an
empty string list[1].

I also think that part of the config API is a wart, but that we should
go for a different solution. It's the only config function that
doesn't return an "int" indicating whether we found the key.

Code that wants to use the values should then check that return
value. I.e. Stolee's version allows you to do:

	const struct string_list *list = git_config_get_value_multi(key);
	for_each_string_list_item(item, list) { ... found = 1 ... }

Whereas in this proposal we instead do (same as for non-multi):

	if (!git_config_get_const_value_multi(key, &list))
		for_each_string_list_item(item, list) { ... found = 1 ... }

Mid-series that's made nicer by adjusting the string_list API to have
sensible "const"'s (so we don't need catsing), and using utility
functions from there. I.e. the recently added code in builtin/gc.c
becomes (Stolee's at [3]):

	if (!git_config_get_knownkey_value_multi(key, &list))
		found = unsorted_string_list_has_string(list, maintpath);

But anyway.

Once I started poking at this approach I discovered that we have a
much larger issue here than whether the top-level value is NULL or an empty list.

As noted I don't think it's an issue that the *_multi() returns NULL
if we have no key, that's easy to handle.

But *_multi() doesn't have the equivalent of a wrapper that coerces
the values on the list into one of our types (as in "git config
--type=<type>").

A not so well known edge case in our config format (see 8/10) is that
value-less keys are represented as NULL's, and a "struct string_list"
is perfectly happy to have a "char *string" member that's NULL.

I.e.:

	[a]key=x
	[a]key
	[a]key=y

Is represented as:

	{ "x", NULL, "y" }

Not, as existing code apparently expected:

	{ "x", "", "y" }

As a result existing code using *_multi() would segfault when reading
config like that. See 9/10, which fixes segfaults in 6 commits as old
as from 2015, and a couple of recent ones: One in the last release,
and one on "master" but not released yet.

The fix is thoroughly boring, we just start doing for *_multi() what
we've been doing for other config since Junio's 2008 fix (see 9/10) to
fix the same issue for non-multi config variables.

I.e. we provide a safer "I want strings, please" variant of the API,
just for *_multi(). At the culmination of this topic only the
test-helper uses the underlying unsafe API, and only because it needs
to check that we're still parsing the config correctly.

1. https://lore.kernel.org/git/pull.1369.git.1664287711.gitgitgadget@gmail.com/
2. https://lore.kernel.org/git/220928.868rm3w9d4.gmgdl@evledraar.gmail.com
3. https://lore.kernel.org/git/e06cb4df081bc2222731f9185a22ed7ad67e3814.1664287711.git.gitgitgadget@gmail.com/

Ævar Arnfjörð Bjarmason (10):
  config API: have *_multi() return an "int" and take a "dest"
  for-each-repo: error on bad --config
  config API: mark *_multi() with RESULT_MUST_BE_USED
  string-list API: mark "struct_string_list" to "for_each_string_list"
    const
  string-list API: make has_string() and list_lookup() "const"
  builtin/gc.c: use "unsorted_string_list_has_string()" where
    appropriate
  config API: add and use "lookup_value" functions
  config tests: add "NULL" tests for *_get_value_multi()
  config API: add "string" version of *_value_multi(), fix segfaults
  for-each-repo: with bad config, don't conflate <path> and <cmd>

 builtin/for-each-repo.c        |  14 ++-
 builtin/gc.c                   |  29 +-----
 builtin/log.c                  |   6 +-
 builtin/submodule--helper.c    |   7 +-
 builtin/worktree.c             |   3 +-
 config.c                       | 164 +++++++++++++++++++++++++++++----
 config.h                       | 110 ++++++++++++++++++++--
 pack-bitmap.c                  |   7 +-
 string-list.c                  |   6 +-
 string-list.h                  |   6 +-
 submodule.c                    |   3 +-
 t/helper/test-config.c         |   6 +-
 t/t0068-for-each-repo.sh       |  19 ++++
 t/t1308-config-set.sh          |  30 ++++++
 t/t4202-log.sh                 |  15 +++
 t/t5310-pack-bitmaps.sh        |  21 +++++
 t/t7004-tag.sh                 |  17 ++++
 t/t7413-submodule-is-active.sh |  16 ++++
 t/t7900-maintenance.sh         |  38 ++++++++
 versioncmp.c                   |  18 +++-
 20 files changed, 451 insertions(+), 84 deletions(-)

-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH 01/10] config API: have *_multi() return an "int" and take a "dest"
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
@ 2022-10-26 15:35 ` Ævar Arnfjörð Bjarmason
  2022-10-26 18:49   ` SZEDER Gábor
  2022-10-27 19:27   ` Junio C Hamano
  2022-10-26 15:35 ` [PATCH 02/10] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
                   ` (10 subsequent siblings)
  11 siblings, 2 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

The git_configset_get_value_multi() function added in 3c8687a73ee (add
`config_set` API for caching config-like files, 2014-07-28) is a
fundamental part of of the config API, and
e.g. "git_config_get_value()" and others are implemented in terms of
it.

But it has had the limitation that configset_find_element() calls
git_config_parse_key(), but then throws away the distinction between a
"ret < 1" return value from it, and return values that indicate a key
doesn't exist. As a result the git_config_get_value_multi() function
would either return a "const struct string_list *", or NULL.

By changing the *_multi() function to return an "int" for the status
and to write to a "const struct string_list **dest" parameter we can
avoid losing this information. API callers can now do:

	const struct string_list *dest;
	int ret;

	ret = git_config_get_value_multi(key, &dest);
	if (ret < 1)
		die("bad key: %s", key);
	else if (ret)
		; /* key does not exist */
	else
		; /* got key, can use "dest" */

A "get_knownkey_value_multi" variant is also provided, which will
BUG() out in the "ret < 1" case. This is useful in the cases where we
hardcode the keyname in our source code, and therefore use the more
idiomatic pattern of:

	if (!git_config_get_value_multi(key, &dest)
		; /* got key, can use "dest" */
	else
		; /* key does not exist */

The "knownkey" name was picked instead of e.g. "const" to avoid a
repeat of the issues noted in f1de981e8b6 (config: fix leaks from
git_config_get_string_const(), 2020-08-14) and 9a53219f69b (config:
drop git_config_get_string_const(), 2020-08-17). API users might think
that "const" means that the value(s) don't need to be free'd.

As noted in commentary here we treat git_die_config() as a
special-case, i.e. we assume that a value we're complaining about has
already had its key pass the git_config_parse_key() check.

Likewise we consider the keys passed to "t/helper/test-config.c" to be
"knownkey", and will emit a BUG() if they don't pass
git_config_parse_key(). Those will come from our *.sh tests, so
they're also "known keys" coming from our sources.

A logical follow-up to this would be to change the various "*_get_*()"
functions to ferry the git_configset_get_value() return value to their
own callers, e.g.:

	diff --git a/config.c b/config.c
	index 094ad899e0b..7e8ee4cfec1 100644
	--- a/config.c
	+++ b/config.c
	@@ -2479,11 +2479,14 @@ static int git_configset_get_string_tmp(struct config_set *cs, const char *key,
	 int git_configset_get_int(struct config_set *cs, const char *key, int *dest)
	 {
	 	const char *value;
	-	if (!git_configset_get_value(cs, key, &value)) {
	-		*dest = git_config_int(key, value);
	-		return 0;
	-	} else
	-		return 1;
	+	int ret;
	+
	+	if ((ret = git_configset_get_value(cs, key, &value)))
	+		goto done;
	+
	+	*dest = git_config_int(key, value);
	+done:
	+	return ret;
	 }

	 int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest)

Most of those callers don't care, and call those functions as
"if (!func(...))", but if they do they'll be able to tell key
non-existence from errors we encounter. Before this change those API
users would have been unable to tell the two conditions apart, as
git_configset_get_value() hid the difference.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c     |  5 +-
 builtin/gc.c                |  6 +--
 builtin/log.c               |  6 +--
 builtin/submodule--helper.c |  6 ++-
 config.c                    | 94 ++++++++++++++++++++++++++++++-------
 config.h                    | 52 ++++++++++++++++----
 pack-bitmap.c               |  7 ++-
 submodule.c                 |  3 +-
 t/helper/test-config.c      |  6 +--
 versioncmp.c                | 10 ++--
 10 files changed, 148 insertions(+), 47 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index fd86e5a8619..b01721762ef 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -28,7 +28,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 {
 	static const char *config_key = NULL;
 	int i, result = 0;
-	const struct string_list *values;
+	const struct string_list *values = NULL;
 
 	const struct option options[] = {
 		OPT_STRING(0, "config", &config_key, N_("config"),
@@ -42,8 +42,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	values = repo_config_get_value_multi(the_repository,
-					     config_key);
+	repo_config_get_value_multi(the_repository, config_key, &values);
 
 	/*
 	 * Do nothing on an empty list, which is equivalent to the case
diff --git a/builtin/gc.c b/builtin/gc.c
index 243ee85d283..04c48638ef4 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1485,8 +1485,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	else
 		git_config_set("maintenance.strategy", "incremental");
 
-	list = git_config_get_value_multi(key);
-	if (list) {
+	if (!git_config_get_knownkey_value_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1542,8 +1541,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 		usage_with_options(builtin_maintenance_unregister_usage,
 				   options);
 
-	list = git_config_get_value_multi(key);
-	if (list) {
+	if (!git_config_get_knownkey_value_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index ee19dc5d450..75464c96ccf 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	int i;
 	char *value = NULL;
 	struct string_list *include = decoration_filter->include_ref_pattern;
-	const struct string_list *config_exclude =
-			git_config_get_value_multi("log.excludeDecoration");
+	const struct string_list *config_exclude;
 
-	if (config_exclude) {
+	if (!git_config_get_knownkey_value_multi("log.excludeDecoration",
+					      &config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 0b4acb442b2..1f8fe6a8e0d 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -541,6 +541,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 		NULL
 	};
 	int ret = 1;
+	const struct string_list *values;
 
 	argc = parse_options(argc, argv, prefix, module_init_options,
 			     git_submodule_helper_usage, 0);
@@ -552,7 +553,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 	 * If there are no path args and submodule.active is set then,
 	 * by default, only initialize 'active' modules.
 	 */
-	if (!argc && git_config_get_value_multi("submodule.active"))
+	if (!argc && !git_config_get_value_multi("submodule.active", &values))
 		module_list_active(&list);
 
 	info.prefix = prefix;
@@ -2708,6 +2709,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 	if (opt.init) {
 		struct module_list list = MODULE_LIST_INIT;
 		struct init_cb info = INIT_CB_INIT;
+		const struct string_list *values;
 
 		if (module_list_compute(argc, argv, opt.prefix,
 					&pathspec2, &list) < 0) {
@@ -2720,7 +2722,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 		 * If there are no path args and submodule.active is set then,
 		 * by default, only initialize 'active' modules.
 		 */
-		if (!argc && git_config_get_value_multi("submodule.active"))
+		if (!argc && !git_config_get_value_multi("submodule.active", &values))
 			module_list_active(&list);
 
 		info.prefix = opt.prefix;
diff --git a/config.c b/config.c
index cbb5a3bab74..2100b29b689 100644
--- a/config.c
+++ b/config.c
@@ -2275,23 +2275,28 @@ void read_very_early_config(config_fn_t cb, void *data)
 	config_with_options(cb, data, NULL, &opts);
 }
 
-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
+static int configset_find_element(struct config_set *cs, const char *key,
+				  struct config_set_element **dest)
 {
 	struct config_set_element k;
 	struct config_set_element *found_entry;
 	char *normalized_key;
+	int ret;
+
 	/*
 	 * `key` may come from the user, so normalize it before using it
 	 * for querying entries from the hashmap.
 	 */
-	if (git_config_parse_key(key, &normalized_key, NULL))
-		return NULL;
+	ret = git_config_parse_key(key, &normalized_key, NULL);
+	if (ret < 0)
+		return ret;
 
 	hashmap_entry_init(&k.ent, strhash(normalized_key));
 	k.key = normalized_key;
 	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
 	free(normalized_key);
-	return found_entry;
+	*dest = found_entry;
+	return 0;
 }
 
 static int configset_add_value(struct config_set *cs, const char *key, const char *value)
@@ -2300,8 +2305,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha
 	struct string_list_item *si;
 	struct configset_list_item *l_item;
 	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
+	int ret;
 
-	e = configset_find_element(cs, key);
+	ret = configset_find_element(cs, key, &e);
+	if (ret < 0)
+		return ret;
 	/*
 	 * Since the keys are being fed by git_config*() callback mechanism, they
 	 * are already normalized. So simply add them without any further munging.
@@ -2400,24 +2408,54 @@ int git_configset_add_parameters(struct config_set *cs)
 int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
 {
 	const struct string_list *values = NULL;
+	int ret;
+
 	/*
 	 * Follows "last one wins" semantic, i.e., if there are multiple matches for the
 	 * queried key in the files of the configset, the value returned will be the last
 	 * value in the value list for that key.
 	 */
-	values = git_configset_get_value_multi(cs, key);
+	ret = git_configset_get_value_multi(cs, key, &values);
 
-	if (!values)
+	if (ret < 0)
+		return ret;
+	else if (!values)
 		return 1;
 	assert(values->nr > 0);
 	*value = values->items[values->nr - 1].string;
 	return 0;
 }
 
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
+static int git_configset_get_value_multi_1(struct config_set *cs, const char *key,
+					   const struct string_list **dest,
+					   int knownkey)
 {
-	struct config_set_element *e = configset_find_element(cs, key);
-	return e ? &e->value_list : NULL;
+	struct config_set_element *e;
+	int ret;
+
+	ret = configset_find_element(cs, key, &e);
+	if (ret < 0 && knownkey)
+		BUG("*_get_knownkey_*() only accepts known-good (hardcoded) keys, but '%s' is bad!", key);
+	else if (ret < 0)
+		return ret;
+	else if (!e)
+		return 1;
+	*dest = &e->value_list;
+
+	return 0;
+}
+
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest)
+{
+	return git_configset_get_value_multi_1(cs, key, dest, 0);
+}
+
+int git_configset_get_knownkey_value_multi(struct config_set *cs,
+					   const char *const key,
+					   const struct string_list **dest)
+{
+	return git_configset_get_value_multi_1(cs, key, dest, 1);
 }
 
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
@@ -2563,11 +2601,20 @@ int repo_config_get_value(struct repository *repo,
 	return git_configset_get_value(repo->config, key, value);
 }
 
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key)
+int repo_config_get_value_multi(struct repository *repo,
+				const char *key,
+				const struct string_list **dest)
 {
 	git_config_check_init(repo);
-	return git_configset_get_value_multi(repo->config, key);
+	return git_configset_get_value_multi(repo->config, key, dest);
+}
+
+int repo_config_get_knownkey_value_multi(struct repository *repo,
+					 const char *const key,
+					 const struct string_list **dest)
+{
+	git_config_check_init(repo);
+	return git_configset_get_knownkey_value_multi(repo->config, key, dest);
 }
 
 int repo_config_get_string(struct repository *repo,
@@ -2684,9 +2731,15 @@ int git_config_get_value(const char *key, const char **value)
 	return repo_config_get_value(the_repository, key, value);
 }
 
-const struct string_list *git_config_get_value_multi(const char *key)
+int git_config_get_value_multi(const char *key, const struct string_list **dest)
+{
+	return repo_config_get_value_multi(the_repository, key, dest);
+}
+
+int git_config_get_knownkey_value_multi(const char *const key,
+					const struct string_list **dest)
 {
-	return repo_config_get_value_multi(the_repository, key);
+	return repo_config_get_knownkey_value_multi(the_repository, key, dest);
 }
 
 int git_config_get_string(const char *key, char **dest)
@@ -2833,7 +2886,16 @@ void git_die_config(const char *key, const char *err, ...)
 		error_fn(err, params);
 		va_end(params);
 	}
-	values = git_config_get_value_multi(key);
+
+	/*
+	 * We don't have a "const" key here, but we should definitely
+	 * have one that's passed git_config_parse_key() already, if
+	 * we're at the point of complaining about its value. So let's
+	 * use *_knownkey_value_multi() here to get that BUG(...).
+	 */
+	if (git_config_get_knownkey_value_multi(key, &values))
+		BUG("key '%s' does not exist, should not be given to git_die_config()",
+		    key);
 	kv_info = values->items[values->nr - 1].util;
 	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
 }
diff --git a/config.h b/config.h
index ca994d77147..c88619b7dcf 100644
--- a/config.h
+++ b/config.h
@@ -457,11 +457,30 @@ int git_configset_add_parameters(struct config_set *cs);
 
 /**
  * Finds and returns the value list, sorted in order of increasing priority
- * for the configuration variable `key` and config set `cs`. When the
- * configuration variable `key` is not found, returns NULL. The caller
- * should not free or modify the returned pointer, as it is owned by the cache.
+ * for the configuration variable `key` and config set `cs`.
+ *
+ * When the configuration variable `key` is not found, returns 1
+ * without touching `value`.
+ *
+ * The key will be parsed for validity with git_config_parse_key(), on
+ * error a negative value will be returned. See
+ * git_configset_get_knownkey_value_multi() for a version of this which
+ * BUG()s out on negative return values.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
+ */
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest);
+
+/**
+ * Like git_configset_get_value_multi(), but BUG()s out if the return
+ * value is < 0. Use it for keys known to pass git_config_parse_key(),
+ * i.e. those hardcoded in the code, and never user-provided keys.
  */
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
+int git_configset_get_knownkey_value_multi(struct config_set *cs,
+					   const char *const key,
+					   const struct string_list **dest);
 
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
@@ -495,8 +514,12 @@ struct repository;
 void repo_config(struct repository *repo, config_fn_t fn, void *data);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key);
+int repo_config_get_value_multi(struct repository *repo,
+				const char *key,
+				const struct string_list **dest);
+int repo_config_get_knownkey_value_multi(struct repository *repo,
+					 const char *const key,
+					 const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -543,10 +566,21 @@ int git_config_get_value(const char *key, const char **value);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key`. When the configuration variable
- * `key` is not found, returns NULL. The caller should not free or modify
- * the returned pointer, as it is owned by the cache.
+ * `key` is not found, returns 1 without touching `value`.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
+ */
+int git_config_get_value_multi(const char *key,
+			       const struct string_list **dest);
+
+/**
+ * A wrapper for git_config_get_value_multi() which does for it what
+ * git_configset_get_knownkey_value_multi() does for
+ * git_configset_get_value_multi().
  */
-const struct string_list *git_config_get_value_multi(const char *key);
+int git_config_get_knownkey_value_multi(const char *const key,
+					const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index 440407f1be7..0b4e73abbfa 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2301,7 +2301,12 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)
 
 const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
-	return repo_config_get_value_multi(r, "pack.preferbitmaptips");
+	const struct string_list *dest;
+
+	if (!repo_config_get_knownkey_value_multi(r, "pack.preferbitmaptips",
+					       &dest))
+		return dest;
+	return NULL;
 }
 
 int bitmap_is_preferred_refname(struct repository *r, const char *refname)
diff --git a/submodule.c b/submodule.c
index bf7a2c79183..e8c4362743d 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	sl = repo_config_get_value_multi(repo, "submodule.active");
-	if (sl) {
+	if (!repo_config_get_knownkey_value_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/helper/test-config.c b/t/helper/test-config.c
index 4ba9eb65606..f0d476d2376 100644
--- a/t/helper/test-config.c
+++ b/t/helper/test-config.c
@@ -95,8 +95,7 @@ int cmd__config(int argc, const char **argv)
 			goto exit1;
 		}
 	} else if (argc == 3 && !strcmp(argv[1], "get_value_multi")) {
-		strptr = git_config_get_value_multi(argv[2]);
-		if (strptr) {
+		if (!git_config_get_knownkey_value_multi(argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
@@ -159,8 +158,7 @@ int cmd__config(int argc, const char **argv)
 				goto exit2;
 			}
 		}
-		strptr = git_configset_get_value_multi(&cs, argv[2]);
-		if (strptr) {
+		if (!git_configset_get_knownkey_value_multi(&cs, argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
diff --git a/versioncmp.c b/versioncmp.c
index 069ee94a4d7..9064478dc4a 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -160,10 +160,14 @@ int versioncmp(const char *s1, const char *s2)
 	}
 
 	if (!initialized) {
-		const struct string_list *deprecated_prereleases;
+		const struct string_list *deprecated_prereleases = NULL;
+
 		initialized = 1;
-		prereleases = git_config_get_value_multi("versionsort.suffix");
-		deprecated_prereleases = git_config_get_value_multi("versionsort.prereleasesuffix");
+		git_config_get_knownkey_value_multi("versionsort.suffix",
+						 &prereleases);
+		git_config_get_value_multi("versionsort.prereleasesuffix",
+					   &deprecated_prereleases);
+
 		if (prereleases) {
 			if (deprecated_prereleases)
 				warning("ignoring versionsort.prereleasesuffix because versionsort.suffix is set");
-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 02/10] for-each-repo: error on bad --config
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
  2022-10-26 15:35 ` [PATCH 01/10] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
@ 2022-10-26 15:35 ` Ævar Arnfjörð Bjarmason
  2022-10-26 15:35 ` [PATCH 03/10] config API: mark *_multi() with RESULT_MUST_BE_USED Ævar Arnfjörð Bjarmason
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

As noted in 6c62f015520 (for-each-repo: do nothing on empty config,
2021-01-08) this command wants to ignore a non-existing config key,
but let's not conflate that with bad config.

We could preserve the comment added in 6c62f015520, but now that we're
directly using the documented repo_config_get_value_multi() value it's
just narrating something that should be obvious from the API use, so
let's drop it.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  | 15 +++++++--------
 t/t0068-for-each-repo.sh |  6 ++++++
 2 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index b01721762ef..16e9a76d04a 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -28,7 +28,8 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 {
 	static const char *config_key = NULL;
 	int i, result = 0;
-	const struct string_list *values = NULL;
+	const struct string_list *values;
+	int err;
 
 	const struct option options[] = {
 		OPT_STRING(0, "config", &config_key, N_("config"),
@@ -42,13 +43,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	repo_config_get_value_multi(the_repository, config_key, &values);
-
-	/*
-	 * Do nothing on an empty list, which is equivalent to the case
-	 * where the config variable does not exist at all.
-	 */
-	if (!values)
+	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	if (err < 0)
+		usage_msg_optf(_("got bad config --config=%s"),
+			       for_each_repo_usage, options, config_key);
+	else if (err)
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 4675e852517..115221c9ca5 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -33,4 +33,10 @@ test_expect_success 'do nothing on empty config' '
 	git for-each-repo --config=bogus.config -- help --no-such-option
 '
 
+test_expect_success 'error on bad config keys' '
+	test_expect_code 129 git for-each-repo --config=a &&
+	test_expect_code 129 git for-each-repo --config=a.b. &&
+	test_expect_code 129 git for-each-repo --config="'\''.b"
+'
+
 test_done
-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 03/10] config API: mark *_multi() with RESULT_MUST_BE_USED
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
  2022-10-26 15:35 ` [PATCH 01/10] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
  2022-10-26 15:35 ` [PATCH 02/10] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
@ 2022-10-26 15:35 ` Ævar Arnfjörð Bjarmason
  2022-10-26 15:35 ` [PATCH 04/10] string-list API: mark "struct_string_list" to "for_each_string_list" const Ævar Arnfjörð Bjarmason
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Use the RESULT_MUST_BE_USED attribute to assert that all users of
the *_multi() API use the return values, in the preceding commit
"for-each-repo" started using the return value meaningfully.

This requires changing versioncmp() so that we use the "ret" versions
of the return values, and don't implicitly rely on
"deprecated_prereleases" being set to NULL if the key didn't exist.

See 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
return values, 2022-09-01) for the introduction of
RESULT_MUST_BE_USED.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 config.h     |  6 ++++++
 versioncmp.c | 22 +++++++++++++---------
 2 files changed, 19 insertions(+), 9 deletions(-)

diff --git a/config.h b/config.h
index c88619b7dcf..a5710c5856e 100644
--- a/config.h
+++ b/config.h
@@ -470,6 +470,7 @@ int git_configset_add_parameters(struct config_set *cs);
  * The caller should not free or modify the returned pointer, as it is
  * owned by the cache.
  */
+RESULT_MUST_BE_USED
 int git_configset_get_value_multi(struct config_set *cs, const char *key,
 				  const struct string_list **dest);
 
@@ -478,6 +479,7 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,
  * value is < 0. Use it for keys known to pass git_config_parse_key(),
  * i.e. those hardcoded in the code, and never user-provided keys.
  */
+RESULT_MUST_BE_USED
 int git_configset_get_knownkey_value_multi(struct config_set *cs,
 					   const char *const key,
 					   const struct string_list **dest);
@@ -514,9 +516,11 @@ struct repository;
 void repo_config(struct repository *repo, config_fn_t fn, void *data);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
+RESULT_MUST_BE_USED
 int repo_config_get_value_multi(struct repository *repo,
 				const char *key,
 				const struct string_list **dest);
+RESULT_MUST_BE_USED
 int repo_config_get_knownkey_value_multi(struct repository *repo,
 					 const char *const key,
 					 const struct string_list **dest);
@@ -571,6 +575,7 @@ int git_config_get_value(const char *key, const char **value);
  * The caller should not free or modify the returned pointer, as it is
  * owned by the cache.
  */
+RESULT_MUST_BE_USED
 int git_config_get_value_multi(const char *key,
 			       const struct string_list **dest);
 
@@ -579,6 +584,7 @@ int git_config_get_value_multi(const char *key,
  * git_configset_get_knownkey_value_multi() does for
  * git_configset_get_value_multi().
  */
+RESULT_MUST_BE_USED
 int git_config_get_knownkey_value_multi(const char *const key,
 					const struct string_list **dest);
 
diff --git a/versioncmp.c b/versioncmp.c
index 9064478dc4a..effe1a6a6be 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -160,19 +160,23 @@ int versioncmp(const char *s1, const char *s2)
 	}
 
 	if (!initialized) {
-		const struct string_list *deprecated_prereleases = NULL;
+		const struct string_list *deprecated_prereleases;
+		int prereleases_ret, deprecated_prereleases_ret;
 
 		initialized = 1;
-		git_config_get_knownkey_value_multi("versionsort.suffix",
-						 &prereleases);
-		git_config_get_value_multi("versionsort.prereleasesuffix",
-					   &deprecated_prereleases);
-
-		if (prereleases) {
-			if (deprecated_prereleases)
+		prereleases_ret =
+			git_config_get_knownkey_value_multi("versionsort.suffix",
+							    &prereleases);
+		deprecated_prereleases_ret =
+			git_config_get_knownkey_value_multi("versionsort.prereleasesuffix",
+							    &deprecated_prereleases);
+
+		if (!prereleases_ret) {
+			if (!deprecated_prereleases_ret)
 				warning("ignoring versionsort.prereleasesuffix because versionsort.suffix is set");
-		} else
+		} else if (!deprecated_prereleases_ret) {
 			prereleases = deprecated_prereleases;
+		}
 	}
 	if (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,
 					    &diff))
-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 04/10] string-list API: mark "struct_string_list" to "for_each_string_list" const
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
                   ` (2 preceding siblings ...)
  2022-10-26 15:35 ` [PATCH 03/10] config API: mark *_multi() with RESULT_MUST_BE_USED Ævar Arnfjörð Bjarmason
@ 2022-10-26 15:35 ` Ævar Arnfjörð Bjarmason
  2022-10-27 19:32   ` Junio C Hamano
  2022-10-26 15:35 ` [PATCH 05/10] string-list API: make has_string() and list_lookup() "const" Ævar Arnfjörð Bjarmason
                   ` (7 subsequent siblings)
  11 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Add a "const" to the "struct string_list *" passed to
for_each_string_list().

This is arguably abuse of the type system, as the
"string_list_each_func_t fn" take a "struct string_list_item *",
i.e. not one with a "const", and those functions *can* modify those
items.

But as we'll see in a subsequent commit we have other such iteration
functions that could benefit from a "const", i.e. to declare that
we're not altering the list itself, even though we might be calling
functions that alter its values.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 string-list.c | 2 +-
 string-list.h | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/string-list.c b/string-list.c
index 549fc416d68..d8957466d25 100644
--- a/string-list.c
+++ b/string-list.c
@@ -129,7 +129,7 @@ void string_list_remove_duplicates(struct string_list *list, int free_util)
 	}
 }
 
-int for_each_string_list(struct string_list *list,
+int for_each_string_list(const struct string_list *list,
 			 string_list_each_func_t fn, void *cb_data)
 {
 	int i, ret = 0;
diff --git a/string-list.h b/string-list.h
index c7b0d5d0008..7153cb79154 100644
--- a/string-list.h
+++ b/string-list.h
@@ -138,7 +138,7 @@ void string_list_clear_func(struct string_list *list, string_list_clear_func_t c
  * Apply `func` to each item. If `func` returns nonzero, the
  * iteration aborts and the return value is propagated.
  */
-int for_each_string_list(struct string_list *list,
+int for_each_string_list(const struct string_list *list,
 			 string_list_each_func_t func, void *cb_data);
 
 /**
-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 05/10] string-list API: make has_string() and list_lookup() "const"
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
                   ` (3 preceding siblings ...)
  2022-10-26 15:35 ` [PATCH 04/10] string-list API: mark "struct_string_list" to "for_each_string_list" const Ævar Arnfjörð Bjarmason
@ 2022-10-26 15:35 ` Ævar Arnfjörð Bjarmason
  2022-10-26 15:35 ` [PATCH 06/10] builtin/gc.c: use "unsorted_string_list_has_string()" where appropriate Ævar Arnfjörð Bjarmason
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Ever since these were added in the "path_list" predecessor of this API
in 6d297f81373 (Status update on merge-recursive in C, 2006-07-08)
they haven't been "const", but as the compiler validates for us adding
that attribute to them is correct.

Note that they will return a non-const "struct string_list_item *",
but the "struct string_list *" itself that's passed in can be marked
"const".

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 string-list.c | 4 ++--
 string-list.h | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/string-list.c b/string-list.c
index d8957466d25..d97a8f61c02 100644
--- a/string-list.c
+++ b/string-list.c
@@ -245,7 +245,7 @@ void string_list_sort(struct string_list *list)
 	QSORT_S(list->items, list->nr, cmp_items, &sort_ctx);
 }
 
-struct string_list_item *unsorted_string_list_lookup(struct string_list *list,
+struct string_list_item *unsorted_string_list_lookup(const struct string_list *list,
 						     const char *string)
 {
 	struct string_list_item *item;
@@ -257,7 +257,7 @@ struct string_list_item *unsorted_string_list_lookup(struct string_list *list,
 	return NULL;
 }
 
-int unsorted_string_list_has_string(struct string_list *list,
+int unsorted_string_list_has_string(const struct string_list *list,
 				    const char *string)
 {
 	return unsorted_string_list_lookup(list, string) != NULL;
diff --git a/string-list.h b/string-list.h
index 7153cb79154..3589afee2ee 100644
--- a/string-list.h
+++ b/string-list.h
@@ -227,13 +227,13 @@ void string_list_sort(struct string_list *list);
  * Like `string_list_has_string()` but for unsorted lists. Linear in
  * size of the list.
  */
-int unsorted_string_list_has_string(struct string_list *list, const char *string);
+int unsorted_string_list_has_string(const struct string_list *list, const char *string);
 
 /**
  * Like `string_list_lookup()` but for unsorted lists. Linear in size
  * of the list.
  */
-struct string_list_item *unsorted_string_list_lookup(struct string_list *list,
+struct string_list_item *unsorted_string_list_lookup(const struct string_list *list,
 						     const char *string);
 /**
  * Remove an item from a string_list. The `string` pointer of the
-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 06/10] builtin/gc.c: use "unsorted_string_list_has_string()" where appropriate
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
                   ` (4 preceding siblings ...)
  2022-10-26 15:35 ` [PATCH 05/10] string-list API: make has_string() and list_lookup() "const" Ævar Arnfjörð Bjarmason
@ 2022-10-26 15:35 ` Ævar Arnfjörð Bjarmason
  2022-10-27 19:37   ` Junio C Hamano
  2022-10-26 15:35 ` [PATCH 07/10] config API: add and use "lookup_value" functions Ævar Arnfjörð Bjarmason
                   ` (5 subsequent siblings)
  11 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Refactor a "do I have an element like this?" pattern added in [1] and
[2] to use unsorted_string_list_has_string() instead of a
for_each_string_list_item() loop.

A preceding commit added a "const" to the "struct string_list *"
argument of unsorted_string_list_has_string(), it'll thus play nicely
with git_config_get_const_value_multi() without needing a cast here.

1. 1ebe6b02970 (maintenance: add 'unregister --force', 2022-09-27)
2. 50a044f1e40 (gc: replace config subprocesses with API calls,
   2022-09-27)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c | 22 ++++------------------
 1 file changed, 4 insertions(+), 18 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 04c48638ef4..f435eda2e73 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1467,7 +1467,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	const char *key = "maintenance.repo";
 	char *config_value;
 	char *maintpath = get_maintpath();
-	struct string_list_item *item;
 	const struct string_list *list;
 
 	argc = parse_options(argc, argv, prefix, options,
@@ -1485,14 +1484,8 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	else
 		git_config_set("maintenance.strategy", "incremental");
 
-	if (!git_config_get_knownkey_value_multi(key, &list)) {
-		for_each_string_list_item(item, list) {
-			if (!strcmp(maintpath, item->string)) {
-				found = 1;
-				break;
-			}
-		}
-	}
+	if (!git_config_get_knownkey_value_multi(key, &list))
+		found = unsorted_string_list_has_string(list, maintpath);
 
 	if (!found) {
 		int rc;
@@ -1532,7 +1525,6 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 	const char *key = "maintenance.repo";
 	char *maintpath = get_maintpath();
 	int found = 0;
-	struct string_list_item *item;
 	const struct string_list *list;
 
 	argc = parse_options(argc, argv, prefix, options,
@@ -1541,14 +1533,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 		usage_with_options(builtin_maintenance_unregister_usage,
 				   options);
 
-	if (!git_config_get_knownkey_value_multi(key, &list)) {
-		for_each_string_list_item(item, list) {
-			if (!strcmp(maintpath, item->string)) {
-				found = 1;
-				break;
-			}
-		}
-	}
+	if (!git_config_get_knownkey_value_multi(key, &list))
+		found = unsorted_string_list_has_string(list, maintpath);
 
 	if (found) {
 		int rc;
-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 07/10] config API: add and use "lookup_value" functions
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
                   ` (5 preceding siblings ...)
  2022-10-26 15:35 ` [PATCH 06/10] builtin/gc.c: use "unsorted_string_list_has_string()" where appropriate Ævar Arnfjörð Bjarmason
@ 2022-10-26 15:35 ` Ævar Arnfjörð Bjarmason
  2022-10-27 19:42   ` Junio C Hamano
  2022-10-26 15:35 ` [PATCH 08/10] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Change various users of the config API who only wanted to ask if a
configuration key existed to use a new *_config*_lookup_value() family
of functions. Unlike the existing API functions in the API this one
doesn't take a "dest" argument.

Some of these were using either git_config_get_string() or
git_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a
configure_added_submodule() leak, 2022-09-01) for a recent example. We
can now use a helper function that doesn't require a throwaway
variable.

We could have changed git_configset_get_value_multi() to accept a
"NULL" as a "dest" for all callers, but let's avoid changing the
behavior of existing API users. The new "lookup" API and the older API
call our static "git_configset_get_value_multi_1()" helper with a new
"read_only" argument instead.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                |  5 +----
 builtin/submodule--helper.c |  9 +++------
 builtin/worktree.c          |  3 +--
 config.c                    | 25 +++++++++++++++++++++----
 config.h                    | 12 ++++++++++++
 5 files changed, 38 insertions(+), 16 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index f435eda2e73..3e94fa5e20f 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1465,7 +1465,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	};
 	int found = 0;
 	const char *key = "maintenance.repo";
-	char *config_value;
 	char *maintpath = get_maintpath();
 	const struct string_list *list;
 
@@ -1479,9 +1478,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	git_config_set("maintenance.auto", "false");
 
 	/* Set maintenance strategy, if unset */
-	if (!git_config_get_string("maintenance.strategy", &config_value))
-		free(config_value);
-	else
+	if (git_config_lookup_value("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
 	if (!git_config_get_knownkey_value_multi(key, &list))
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 1f8fe6a8e0d..b758255f816 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -541,7 +541,6 @@ static int module_init(int argc, const char **argv, const char *prefix)
 		NULL
 	};
 	int ret = 1;
-	const struct string_list *values;
 
 	argc = parse_options(argc, argv, prefix, module_init_options,
 			     git_submodule_helper_usage, 0);
@@ -553,7 +552,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 	 * If there are no path args and submodule.active is set then,
 	 * by default, only initialize 'active' modules.
 	 */
-	if (!argc && !git_config_get_value_multi("submodule.active", &values))
+	if (!argc && !git_config_lookup_value("submodule.active"))
 		module_list_active(&list);
 
 	info.prefix = prefix;
@@ -2709,7 +2708,6 @@ static int module_update(int argc, const char **argv, const char *prefix)
 	if (opt.init) {
 		struct module_list list = MODULE_LIST_INIT;
 		struct init_cb info = INIT_CB_INIT;
-		const struct string_list *values;
 
 		if (module_list_compute(argc, argv, opt.prefix,
 					&pathspec2, &list) < 0) {
@@ -2722,7 +2720,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 		 * If there are no path args and submodule.active is set then,
 		 * by default, only initialize 'active' modules.
 		 */
-		if (!argc && !git_config_get_value_multi("submodule.active", &values))
+		if (!argc && !git_config_lookup_value("submodule.active"))
 			module_list_active(&list);
 
 		info.prefix = opt.prefix;
@@ -3166,7 +3164,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con
 static void configure_added_submodule(struct add_data *add_data)
 {
 	char *key;
-	const char *val;
 	struct child_process add_submod = CHILD_PROCESS_INIT;
 	struct child_process add_gitmodules = CHILD_PROCESS_INIT;
 
@@ -3211,7 +3208,7 @@ static void configure_added_submodule(struct add_data *add_data)
 	 * is_submodule_active(), since that function needs to find
 	 * out the value of "submodule.active" again anyway.
 	 */
-	if (!git_config_get_string_tmp("submodule.active", &val)) {
+	if (!git_config_lookup_value("submodule.active")) {
 		/*
 		 * If the submodule being added isn't already covered by the
 		 * current configured pathspec, set the submodule's active flag
diff --git a/builtin/worktree.c b/builtin/worktree.c
index c6710b25520..5ab16631dbc 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -260,7 +260,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 
 	if (file_exists(from_file)) {
 		struct config_set cs = { { 0 } };
-		const char *core_worktree;
 		int bare;
 
 		if (safe_create_leading_directories(to_file) ||
@@ -279,7 +278,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 				to_file, "core.bare", NULL, "true", 0))
 			error(_("failed to unset '%s' in '%s'"),
 				"core.bare", to_file);
-		if (!git_configset_get_value(&cs, "core.worktree", &core_worktree) &&
+		if (!git_configset_lookup_value(&cs, "core.worktree") &&
 			git_config_set_in_file_gently(to_file,
 							"core.worktree", NULL))
 			error(_("failed to unset '%s' in '%s'"),
diff --git a/config.c b/config.c
index 2100b29b689..5cd130ddbb9 100644
--- a/config.c
+++ b/config.c
@@ -2428,7 +2428,7 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *
 
 static int git_configset_get_value_multi_1(struct config_set *cs, const char *key,
 					   const struct string_list **dest,
-					   int knownkey)
+					   int read_only, int knownkey)
 {
 	struct config_set_element *e;
 	int ret;
@@ -2440,7 +2440,8 @@ static int git_configset_get_value_multi_1(struct config_set *cs, const char *ke
 		return ret;
 	else if (!e)
 		return 1;
-	*dest = &e->value_list;
+	if (!read_only)
+		*dest = &e->value_list;
 
 	return 0;
 }
@@ -2448,14 +2449,19 @@ static int git_configset_get_value_multi_1(struct config_set *cs, const char *ke
 int git_configset_get_value_multi(struct config_set *cs, const char *key,
 				  const struct string_list **dest)
 {
-	return git_configset_get_value_multi_1(cs, key, dest, 0);
+	return git_configset_get_value_multi_1(cs, key, dest, 0, 0);
 }
 
 int git_configset_get_knownkey_value_multi(struct config_set *cs,
 					   const char *const key,
 					   const struct string_list **dest)
 {
-	return git_configset_get_value_multi_1(cs, key, dest, 1);
+	return git_configset_get_value_multi_1(cs, key, dest, 0, 1);
+}
+
+int git_configset_lookup_value(struct config_set *cs, const char *key)
+{
+	return git_configset_get_value_multi_1(cs, key, NULL, 1, 0);
 }
 
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
@@ -2594,6 +2600,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)
 	configset_iter(repo->config, fn, data);
 }
 
+int repo_config_lookup_value(struct repository *repo, const char *key)
+{
+	git_config_check_init(repo);
+	return git_configset_get_value_multi_1(repo->config, key, NULL, 1, 0);
+}
+
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value)
 {
@@ -2726,6 +2738,11 @@ void git_config_clear(void)
 	repo_config_clear(the_repository);
 }
 
+int git_config_lookup_value(const char *key)
+{
+	return repo_config_lookup_value(the_repository, key);
+}
+
 int git_config_get_value(const char *key, const char **value)
 {
 	return repo_config_get_value(the_repository, key, value);
diff --git a/config.h b/config.h
index a5710c5856e..cf1ae7862a8 100644
--- a/config.h
+++ b/config.h
@@ -502,6 +502,8 @@ void git_configset_clear(struct config_set *cs);
  * is owned by the cache.
  */
 int git_configset_get_value(struct config_set *cs, const char *key, const char **dest);
+RESULT_MUST_BE_USED
+int git_configset_lookup_value(struct config_set *cs, const char *key);
 
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest);
 int git_configset_get_int(struct config_set *cs, const char *key, int *dest);
@@ -524,6 +526,8 @@ RESULT_MUST_BE_USED
 int repo_config_get_knownkey_value_multi(struct repository *repo,
 					 const char *const key,
 					 const struct string_list **dest);
+RESULT_MUST_BE_USED
+int repo_config_lookup_value(struct repository *repo, const char *key);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -588,6 +592,14 @@ RESULT_MUST_BE_USED
 int git_config_get_knownkey_value_multi(const char *const key,
 					const struct string_list **dest);
 
+/**
+ * The same as git_config_value(), except without the extra work to
+ * return the value to the user, used to check if a value for a key
+ * exists.
+ */
+RESULT_MUST_BE_USED
+int git_config_lookup_value(const char *key);
+
 /**
  * Resets and invalidates the config cache.
  */
-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 08/10] config tests: add "NULL" tests for *_get_value_multi()
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
                   ` (6 preceding siblings ...)
  2022-10-26 15:35 ` [PATCH 07/10] config API: add and use "lookup_value" functions Ævar Arnfjörð Bjarmason
@ 2022-10-26 15:35 ` Ævar Arnfjörð Bjarmason
  2022-10-27 19:43   ` Junio C Hamano
  2022-10-26 15:35 ` [PATCH 09/10] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
                   ` (3 subsequent siblings)
  11 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

A less well known edge case in the config format is that keys can be
value-less, a shorthand syntax for "true" boolean keys. I.e. these two
are equivalent as far as "--type=bool" is concerned:

	[a]key
	[a]key = true

But as far as our parser is concerned the values for these two are
NULL, and "true". I.e. for a sequence like:

	[a]key=x
	[a]key
	[a]key=y

We get a "struct string_list" with "string" members with ".string"
values of:

	{ "x", NULL, "y" }

This behavior goes back to the initial implementation of
git_config_bool() in 17712991a59 (Add ".git/config" file parser,
2005-10-10).

When the "t/t1308-config-set.sh" tests were added in [1] only one of
the three "(NULL)" lines in "t/helper/test-config.c" had any test
coverage. This change adds tests that stress the remaining two.

1. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t1308-config-set.sh | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index b38e158d3b2..561e82f1808 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -146,6 +146,36 @@ test_expect_success 'find multiple values' '
 	check_config get_value_multi case.baz sam bat hask
 '
 
+test_expect_success 'emit multi values from configset with NULL entry' '
+	test_when_finished "rm -f my.config" &&
+	cat >my.config <<-\EOF &&
+	[a]key=x
+	[a]key
+	[a]key=y
+	EOF
+	cat >expect <<-\EOF &&
+	x
+	(NULL)
+	y
+	EOF
+	test-tool config configset_get_value_multi a.key my.config >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'multi values from configset with a last NULL entry' '
+	test_when_finished "rm -f my.config" &&
+	cat >my.config <<-\EOF &&
+	[a]key=x
+	[a]key=y
+	[a]key
+	EOF
+	cat >expect <<-\EOF &&
+	(NULL)
+	EOF
+	test-tool config configset_get_value a.key my.config >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'find value from a configset' '
 	cat >config2 <<-\EOF &&
 	[case]
-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 09/10] config API: add "string" version of *_value_multi(), fix segfaults
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
                   ` (7 preceding siblings ...)
  2022-10-26 15:35 ` [PATCH 08/10] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2022-10-26 15:35 ` Ævar Arnfjörð Bjarmason
  2022-10-27 19:49   ` Junio C Hamano
  2022-10-26 15:35 ` [PATCH 10/10] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
                   ` (2 subsequent siblings)
  11 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Fix numerous and mostly long-standing segfaults in consumers of
the *_config_*value_multi() API. As discussed in the preceding commit
an empty key in the config syntax yields a "NULL" string, which these
users would give to strcmp() (or similar), resulting in segfaults.

As this change shows, these non-test users of
the *_config_*value_multi() API didn't really want such an an unsafe
and low-level API, let's give them something with the safety of
git_config_get_string() instead.

This fix is similar to what the *_string() functions and others
acquired in[1] and [2]. Namely introducing and using a
safer *_config_*value_multi_string() variant of the
low-level *_config_*value_multi_string() function.

This fixes segfaults in code introduced in:

  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)
  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)
  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)
  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)
  - 92156291ca8 (log: add default decoration filter, 2022-08-05)
  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)

There are now two remaining user of the low-level API, one is the
"t/helper/test-config.c" code added in [3]. The other we'll address in
a subsequent commit.

As seen in the preceding commit we need to give the
"t/helper/test-config.c" caller these "NULL" entries. We thus cannot
alter the underlying git_configset_get_value_multi_1() function itself
to make it "safe".

Such a thing would also be undesirable, as casting or forbidding NULL
values might only be one potential use-case of the underlying
function. It's better to have a "raw" low-level function, and
corresponding wrapper functions that coerce its values. The callback
pattern being used here will make it easy to introduce e.g. a "multi"
variant which coerces its values to "bool", "int", "path" etc.

1. 40ea4ed9032 (Add config_error_nonbool() helper function,
   2008-02-11)
2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,
   2008-02-11).
3. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                   |  4 +--
 builtin/log.c                  |  2 +-
 config.c                       | 63 +++++++++++++++++++++++++++++++---
 config.h                       | 40 +++++++++++++++++++++
 pack-bitmap.c                  |  2 +-
 submodule.c                    |  2 +-
 t/t4202-log.sh                 | 15 ++++++++
 t/t5310-pack-bitmaps.sh        | 21 ++++++++++++
 t/t7004-tag.sh                 | 17 +++++++++
 t/t7413-submodule-is-active.sh | 16 +++++++++
 t/t7900-maintenance.sh         | 38 ++++++++++++++++++++
 versioncmp.c                   |  8 ++---
 12 files changed, 214 insertions(+), 14 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 3e94fa5e20f..3fc759b1f0c 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1481,7 +1481,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	if (git_config_lookup_value("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
-	if (!git_config_get_knownkey_value_multi(key, &list))
+	if (!git_config_get_knownkey_value_multi_string(key, &list))
 		found = unsorted_string_list_has_string(list, maintpath);
 
 	if (!found) {
@@ -1530,7 +1530,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 		usage_with_options(builtin_maintenance_unregister_usage,
 				   options);
 
-	if (!git_config_get_knownkey_value_multi(key, &list))
+	if (!git_config_get_knownkey_value_multi_string(key, &list))
 		found = unsorted_string_list_has_string(list, maintpath);
 
 	if (found) {
diff --git a/builtin/log.c b/builtin/log.c
index 75464c96ccf..d6b1c75ea2e 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -184,7 +184,7 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	struct string_list *include = decoration_filter->include_ref_pattern;
 	const struct string_list *config_exclude;
 
-	if (!git_config_get_knownkey_value_multi("log.excludeDecoration",
+	if (!git_config_get_knownkey_value_multi_string("log.excludeDecoration",
 					      &config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
diff --git a/config.c b/config.c
index 5cd130ddbb9..25bb6514f81 100644
--- a/config.c
+++ b/config.c
@@ -2428,7 +2428,8 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *
 
 static int git_configset_get_value_multi_1(struct config_set *cs, const char *key,
 					   const struct string_list **dest,
-					   int read_only, int knownkey)
+					   int read_only, int knownkey,
+					   string_list_each_func_t check_fn)
 {
 	struct config_set_element *e;
 	int ret;
@@ -2440,28 +2441,51 @@ static int git_configset_get_value_multi_1(struct config_set *cs, const char *ke
 		return ret;
 	else if (!e)
 		return 1;
+	if (check_fn &&
+	    (ret = for_each_string_list(&e->value_list, check_fn, (void *)key)))
+		return ret;
 	if (!read_only)
 		*dest = &e->value_list;
 
 	return 0;
 }
 
+static int check_multi_string(struct string_list_item *item, void *util)
+{
+	return item->string ? 0 : config_error_nonbool(util);
+}
+
 int git_configset_get_value_multi(struct config_set *cs, const char *key,
 				  const struct string_list **dest)
 {
-	return git_configset_get_value_multi_1(cs, key, dest, 0, 0);
+	return git_configset_get_value_multi_1(cs, key, dest, 0, 0, NULL);
 }
 
 int git_configset_get_knownkey_value_multi(struct config_set *cs,
 					   const char *const key,
 					   const struct string_list **dest)
 {
-	return git_configset_get_value_multi_1(cs, key, dest, 0, 1);
+	return git_configset_get_value_multi_1(cs, key, dest, 0, 1, NULL);
+}
+
+int git_configset_get_value_multi_string(struct config_set *cs, const char *key,
+					 const struct string_list **dest)
+{
+	return git_configset_get_value_multi_1(cs, key, dest, 0, 0,
+					       check_multi_string);
+}
+
+int git_configset_get_knownkey_value_multi_string(struct config_set *cs,
+						  const char *const key,
+						  const struct string_list **dest)
+{
+	return git_configset_get_value_multi_1(cs, key, dest, 0, 1,
+					       check_multi_string);
 }
 
 int git_configset_lookup_value(struct config_set *cs, const char *key)
 {
-	return git_configset_get_value_multi_1(cs, key, NULL, 1, 0);
+	return git_configset_get_value_multi_1(cs, key, NULL, 1, 0, NULL);
 }
 
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
@@ -2603,7 +2627,8 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)
 int repo_config_lookup_value(struct repository *repo, const char *key)
 {
 	git_config_check_init(repo);
-	return git_configset_get_value_multi_1(repo->config, key, NULL, 1, 0);
+	return git_configset_get_value_multi_1(repo->config, key, NULL, 1, 0,
+					       NULL);
 }
 
 int repo_config_get_value(struct repository *repo,
@@ -2629,6 +2654,22 @@ int repo_config_get_knownkey_value_multi(struct repository *repo,
 	return git_configset_get_knownkey_value_multi(repo->config, key, dest);
 }
 
+int repo_config_get_value_multi_string(struct repository *repo,
+				       const char *key,
+				       const struct string_list **dest)
+{
+	git_config_check_init(repo);
+	return git_configset_get_value_multi_string(repo->config, key, dest);
+}
+
+int repo_config_get_knownkey_value_multi_string(struct repository *repo,
+						const char *key,
+						const struct string_list **dest)
+{
+	git_config_check_init(repo);
+	return git_configset_get_knownkey_value_multi_string(repo->config, key, dest);
+}
+
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest)
 {
@@ -2759,6 +2800,18 @@ int git_config_get_knownkey_value_multi(const char *const key,
 	return repo_config_get_knownkey_value_multi(the_repository, key, dest);
 }
 
+int git_config_get_value_multi_string(const char *key,
+				      const struct string_list **dest)
+{
+	return repo_config_get_value_multi_string(the_repository, key, dest);
+}
+
+int git_config_get_knownkey_value_multi_string(const char *key,
+					       const struct string_list **dest)
+{
+	return repo_config_get_knownkey_value_multi_string(the_repository, key, dest);
+}
+
 int git_config_get_string(const char *key, char **dest)
 {
 	return repo_config_get_string(the_repository, key, dest);
diff --git a/config.h b/config.h
index cf1ae7862a8..047ef83afc6 100644
--- a/config.h
+++ b/config.h
@@ -484,6 +484,30 @@ int git_configset_get_knownkey_value_multi(struct config_set *cs,
 					   const char *const key,
 					   const struct string_list **dest);
 
+/**
+ * A validation wrapper for git_configset_get_value_multi() which does
+ * for it what git_configset_get_string() does for
+ * git_configset_get_value().
+ *
+ * The configuration syntax allows for "[section] key", which will
+ * give us a NULL entry in the "struct string_list", as opposed to
+ * "[section] key =" which is the empty string. Most users of the API
+ * are not prepared to handle NULL in a "struct string_list".
+ */
+int git_configset_get_value_multi_string(struct config_set *cs, const char *key,
+					 const struct string_list **dest);
+
+/**
+ * A wrapper for git_configset_get_value_multi_string() which does for
+ * it what git_configset_get_knownkey_value_multi() does for
+ * git_configset_get_value_multi().
+ */
+RESULT_MUST_BE_USED
+int git_configset_get_knownkey_value_multi_string(struct config_set *cs,
+						  const char *const key,
+						  const struct string_list **dest);
+
+
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
  */
@@ -527,6 +551,14 @@ int repo_config_get_knownkey_value_multi(struct repository *repo,
 					 const char *const key,
 					 const struct string_list **dest);
 RESULT_MUST_BE_USED
+int repo_config_get_value_multi_string(struct repository *repo,
+				       const char *key,
+				       const struct string_list **dest);
+RESULT_MUST_BE_USED
+int repo_config_get_knownkey_value_multi_string(struct repository *repo,
+						const char *const key,
+						const struct string_list **dest);
+RESULT_MUST_BE_USED
 int repo_config_lookup_value(struct repository *repo, const char *key);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
@@ -592,6 +624,14 @@ RESULT_MUST_BE_USED
 int git_config_get_knownkey_value_multi(const char *const key,
 					const struct string_list **dest);
 
+RESULT_MUST_BE_USED
+int git_config_get_value_multi_string(const char *key,
+				      const struct string_list **dest);
+
+RESULT_MUST_BE_USED
+int git_config_get_knownkey_value_multi_string(const char *const key,
+					       const struct string_list **dest);
+
 /**
  * The same as git_config_value(), except without the extra work to
  * return the value to the user, used to check if a value for a key
diff --git a/pack-bitmap.c b/pack-bitmap.c
index 0b4e73abbfa..9a61d9ff9a8 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2303,7 +2303,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
 	const struct string_list *dest;
 
-	if (!repo_config_get_knownkey_value_multi(r, "pack.preferbitmaptips",
+	if (!repo_config_get_knownkey_value_multi_string(r, "pack.preferbitmaptips",
 					       &dest))
 		return dest;
 	return NULL;
diff --git a/submodule.c b/submodule.c
index e8c4362743d..f84b253154e 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	if (!repo_config_get_knownkey_value_multi(repo, "submodule.active", &sl)) {
+	if (!repo_config_get_knownkey_value_multi_string(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 2ce2b41174d..ae73aef922f 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,6 +835,21 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
+test_expect_success 'parse log.excludeDecoration with no value' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[log]
+		excludeDecoration
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''log.excludeDecoration'\''
+	EOF
+	git log --decorate=short 2>actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'decorate-refs with glob' '
 	cat >expect.decorate <<-\EOF &&
 	Merge-tag-reach
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 6d693eef82f..68195a1de36 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,6 +404,27 @@ test_bitmap_cases () {
 		)
 	'
 
+	test_expect_success 'pack.preferBitmapTips' '
+		git init repo &&
+		test_when_finished "rm -rf repo" &&
+		(
+			cd repo &&
+			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
+			test_commit_bulk --message="%s" 103 &&
+
+			cat >>.git/config <<-\EOF &&
+			[pack]
+				preferBitmapTips
+			EOF
+
+			cat >expect <<-\EOF &&
+			error: missing value for '\''pack.preferbitmaptips'\''
+			EOF
+			git repack -adb 2>actual &&
+			test_cmp expect actual
+		)
+	'
+
 	test_expect_success 'complains about multiple pack bitmaps' '
 		rm -fr repo &&
 		git init repo &&
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index 9aa1660651b..f4a31ada79a 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,6 +1843,23 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[versionsort]
+		prereleaseSuffix
+		suffix
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''versionsort.suffix'\''
+	error: missing value for '\''versionsort.prereleasesuffix'\''
+	EOF
+	git tag -l --sort=version:refname 2>actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'version sort with prerelease reordering' '
 	test_config versionsort.prereleaseSuffix -rc &&
 	git tag foo1.6-rc1 &&
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index 7cdc2637649..887d181b72e 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,6 +51,22 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
+test_expect_success 'is-active handles submodule.active config missing a value' '
+	cp super/.git/config super/.git/config.orig &&
+	test_when_finished mv super/.git/config.orig super/.git/config &&
+
+	cat >>super/.git/config <<-\EOF &&
+	[submodule]
+		active
+	EOF
+
+	cat >expect <<-\EOF &&
+	error: missing value for '\''submodule.active'\''
+	EOF
+	test-tool -C super submodule is-active sub1 2>actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'is-active works with basic submodule.active config' '
 	test_when_finished "git -C super config submodule.sub1.URL ../sub" &&
 	test_when_finished "git -C super config --unset-all submodule.active" &&
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 96bdd420456..1201866c8d0 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -505,6 +505,44 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --force
 '
 
+test_expect_success 'register with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	git maintenance register 2>actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
+'
+
+test_expect_success 'unregister with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	test_expect_code 128 git maintenance unregister 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo &&
+
+	git maintenance unregister --force 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
+'
+
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
 	META="a+b*c" &&
 	git init "$META" &&
diff --git a/versioncmp.c b/versioncmp.c
index effe1a6a6be..4efb5f9e621 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -165,11 +165,11 @@ int versioncmp(const char *s1, const char *s2)
 
 		initialized = 1;
 		prereleases_ret =
-			git_config_get_knownkey_value_multi("versionsort.suffix",
-							    &prereleases);
+			git_config_get_knownkey_value_multi_string("versionsort.suffix",
+								   &prereleases);
 		deprecated_prereleases_ret =
-			git_config_get_knownkey_value_multi("versionsort.prereleasesuffix",
-							    &deprecated_prereleases);
+			git_config_get_knownkey_value_multi_string("versionsort.prereleasesuffix",
+								   &deprecated_prereleases);
 
 		if (!prereleases_ret) {
 			if (!deprecated_prereleases_ret)
-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 10/10] for-each-repo: with bad config, don't conflate <path> and <cmd>
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
                   ` (8 preceding siblings ...)
  2022-10-26 15:35 ` [PATCH 09/10] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
@ 2022-10-26 15:35 ` Ævar Arnfjörð Bjarmason
  2022-10-27 20:12 ` [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Junio C Hamano
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 15:35 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on
configured repos, 2020-09-11). Due to assuming that elements returned
from the repo_config_get_value_multi() call wouldn't be "NULL" we'd
conflate the <path> and <command> part of the argument list when
running commands.

As noted in the preceding commit the fix is to move to a safer
"*_multi_string()" version of the *__multi() API. This change is
separated from the rest because those all segfaulted. In this change
we ended up with different behavior.

When using the "--config=<config>" form we take each element of the
list as a path to a repository. E.g. with a configuration like:

	[repo] list = /some/repo

We would, with this command:

	git for-each-repo --config=repo.list status builtin

Run a "git status" in /some/repo, as:

	git -C /some/repo status builtin

I.e. ask "status" to report on the "builtin" directory. But since a
configuration such as this would result in a "struct string_list *"
with one element, whose "string" member is "NULL":

	[repo] list

We would, when constructing our command-line in
"builtin/for-each-repo.c"...

	strvec_pushl(&child.args, "-C", path, NULL);
	for (i = 0; i < argc; i++)
		strvec_push(&child.args, argv[i]);

...have that "path" be "NULL", and as strvec_pushl() stops when it
sees NULL we'd end with the first "argv" element as the argument to
the "-C" option, e.g.:

	git -C status builtin

I.e. we'd run the command "builtin" in the "status" directory.

In another context this might be an interesting security
vulnerability, but I think that this amounts to a nothingburger on
that front.

A hypothetical attacker would need to be able to write config for the
victim to run, if they're able to do that there's more interesting
attack vectors. See the "safe.directory" facility added in
8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).

An even more unlikely possibility would be an attacker able to
generate the config used for "for-each-repo --config=<key>", but
nothing else (e.g. an automated system producing that list).

Even in that case the attack vector is limited to the user running
commands whose name matches a directory that's interesting to the
attacker (e.g. a "log" directory in a repository). The second
argument (if any) of the command is likely to make git die without
doing anything interesting (e.g. "-p" to "log", there being no "-p"
built-in command to run).

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  |  2 +-
 t/t0068-for-each-repo.sh | 13 +++++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 16e9a76d04a..125901f2fc0 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -43,7 +43,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	err = repo_config_get_value_multi_string(the_repository, config_key, &values);
 	if (err < 0)
 		usage_msg_optf(_("got bad config --config=%s"),
 			       for_each_repo_usage, options, config_key);
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 115221c9ca5..c27d4dc5f71 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -39,4 +39,17 @@ test_expect_success 'error on bad config keys' '
 	test_expect_code 129 git for-each-repo --config="'\''.b"
 '
 
+test_expect_success 'error on NULL value for config keys' '
+	cat >>.git/config <<-\EOF &&
+	[empty]
+		key
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''empty.key'\''
+	EOF
+	test_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.38.0.1251.g3eefdfb5e7a


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH 01/10] config API: have *_multi() return an "int" and take a "dest"
  2022-10-26 15:35 ` [PATCH 01/10] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
@ 2022-10-26 18:49   ` SZEDER Gábor
  2022-10-26 19:33     ` Ævar Arnfjörð Bjarmason
  2022-10-27 19:27   ` Junio C Hamano
  1 sibling, 1 reply; 134+ messages in thread
From: SZEDER Gábor @ 2022-10-26 18:49 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau

On Wed, Oct 26, 2022 at 05:35:14PM +0200, Ævar Arnfjörð Bjarmason wrote:
> The git_configset_get_value_multi() function added in 3c8687a73ee (add
> `config_set` API for caching config-like files, 2014-07-28) is a
> fundamental part of of the config API, and
> e.g. "git_config_get_value()" and others are implemented in terms of
> it.
> 
> But it has had the limitation that configset_find_element() calls
> git_config_parse_key(), but then throws away the distinction between a
> "ret < 1" return value from it, and return values that indicate a key

Shouldn't that be "ret < 0"?

> doesn't exist. As a result the git_config_get_value_multi() function
> would either return a "const struct string_list *", or NULL.
> 
> By changing the *_multi() function to return an "int" for the status
> and to write to a "const struct string_list **dest" parameter we can
> avoid losing this information. API callers can now do:
> 
> 	const struct string_list *dest;
> 	int ret;
> 
> 	ret = git_config_get_value_multi(key, &dest);
> 	if (ret < 1)

This catches all negative values and zero.

> 		die("bad key: %s", key);
> 	else if (ret)

This catches all non-zero values.

> 		; /* key does not exist */
> 	else

So how could this ever be executed?!

> 		; /* got key, can use "dest" */
> 
> A "get_knownkey_value_multi" variant is also provided, which will
> BUG() out in the "ret < 1" case. This is useful in the cases where we

Shouldn't that be "ret < 0" as well?  The condition in that "knownkey"
variant added in this patch is:

  +	ret = configset_find_element(cs, key, &e);
  +	if (ret < 0 && knownkey)
  +		BUG("*_get_knownkey_*() only accepts known-good (hardcoded) keys, but '%s' is bad!", key);

> hardcode the keyname in our source code, and therefore use the more
> idiomatic pattern of:
> 
> 	if (!git_config_get_value_multi(key, &dest)
> 		; /* got key, can use "dest" */
> 	else
> 		; /* key does not exist */
> 
> The "knownkey" name was picked instead of e.g. "const" to avoid a
> repeat of the issues noted in f1de981e8b6 (config: fix leaks from
> git_config_get_string_const(), 2020-08-14) and 9a53219f69b (config:
> drop git_config_get_string_const(), 2020-08-17). API users might think
> that "const" means that the value(s) don't need to be free'd.
> 
> As noted in commentary here we treat git_die_config() as a
> special-case, i.e. we assume that a value we're complaining about has
> already had its key pass the git_config_parse_key() check.
> 
> Likewise we consider the keys passed to "t/helper/test-config.c" to be
> "knownkey", and will emit a BUG() if they don't pass
> git_config_parse_key(). Those will come from our *.sh tests, so
> they're also "known keys" coming from our sources.
> 
> A logical follow-up to this would be to change the various "*_get_*()"
> functions to ferry the git_configset_get_value() return value to their
> own callers, e.g.:
> 
> 	diff --git a/config.c b/config.c
> 	index 094ad899e0b..7e8ee4cfec1 100644
> 	--- a/config.c
> 	+++ b/config.c
> 	@@ -2479,11 +2479,14 @@ static int git_configset_get_string_tmp(struct config_set *cs, const char *key,
> 	 int git_configset_get_int(struct config_set *cs, const char *key, int *dest)
> 	 {
> 	 	const char *value;
> 	-	if (!git_configset_get_value(cs, key, &value)) {
> 	-		*dest = git_config_int(key, value);
> 	-		return 0;
> 	-	} else
> 	-		return 1;
> 	+	int ret;
> 	+
> 	+	if ((ret = git_configset_get_value(cs, key, &value)))
> 	+		goto done;
> 	+
> 	+	*dest = git_config_int(key, value);
> 	+done:
> 	+	return ret;
> 	 }
> 
> 	 int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest)
> 
> Most of those callers don't care, and call those functions as
> "if (!func(...))", but if they do they'll be able to tell key
> non-existence from errors we encounter. Before this change those API
> users would have been unable to tell the two conditions apart, as
> git_configset_get_value() hid the difference.
> 
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  builtin/for-each-repo.c     |  5 +-
>  builtin/gc.c                |  6 +--
>  builtin/log.c               |  6 +--
>  builtin/submodule--helper.c |  6 ++-
>  config.c                    | 94 ++++++++++++++++++++++++++++++-------
>  config.h                    | 52 ++++++++++++++++----
>  pack-bitmap.c               |  7 ++-
>  submodule.c                 |  3 +-
>  t/helper/test-config.c      |  6 +--
>  versioncmp.c                | 10 ++--
>  10 files changed, 148 insertions(+), 47 deletions(-)
> 
> diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
> index fd86e5a8619..b01721762ef 100644
> --- a/builtin/for-each-repo.c
> +++ b/builtin/for-each-repo.c
> @@ -28,7 +28,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
>  {
>  	static const char *config_key = NULL;
>  	int i, result = 0;
> -	const struct string_list *values;
> +	const struct string_list *values = NULL;
>  
>  	const struct option options[] = {
>  		OPT_STRING(0, "config", &config_key, N_("config"),
> @@ -42,8 +42,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
>  	if (!config_key)
>  		die(_("missing --config=<config>"));
>  
> -	values = repo_config_get_value_multi(the_repository,
> -					     config_key);
> +	repo_config_get_value_multi(the_repository, config_key, &values);
>  
>  	/*
>  	 * Do nothing on an empty list, which is equivalent to the case
> diff --git a/builtin/gc.c b/builtin/gc.c
> index 243ee85d283..04c48638ef4 100644
> --- a/builtin/gc.c
> +++ b/builtin/gc.c
> @@ -1485,8 +1485,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
>  	else
>  		git_config_set("maintenance.strategy", "incremental");
>  
> -	list = git_config_get_value_multi(key);
> -	if (list) {
> +	if (!git_config_get_knownkey_value_multi(key, &list)) {
>  		for_each_string_list_item(item, list) {
>  			if (!strcmp(maintpath, item->string)) {
>  				found = 1;
> @@ -1542,8 +1541,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
>  		usage_with_options(builtin_maintenance_unregister_usage,
>  				   options);
>  
> -	list = git_config_get_value_multi(key);
> -	if (list) {
> +	if (!git_config_get_knownkey_value_multi(key, &list)) {
>  		for_each_string_list_item(item, list) {
>  			if (!strcmp(maintpath, item->string)) {
>  				found = 1;
> diff --git a/builtin/log.c b/builtin/log.c
> index ee19dc5d450..75464c96ccf 100644
> --- a/builtin/log.c
> +++ b/builtin/log.c
> @@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
>  	int i;
>  	char *value = NULL;
>  	struct string_list *include = decoration_filter->include_ref_pattern;
> -	const struct string_list *config_exclude =
> -			git_config_get_value_multi("log.excludeDecoration");
> +	const struct string_list *config_exclude;
>  
> -	if (config_exclude) {
> +	if (!git_config_get_knownkey_value_multi("log.excludeDecoration",
> +					      &config_exclude)) {
>  		struct string_list_item *item;
>  		for_each_string_list_item(item, config_exclude)
>  			string_list_append(decoration_filter->exclude_ref_config_pattern,
> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
> index 0b4acb442b2..1f8fe6a8e0d 100644
> --- a/builtin/submodule--helper.c
> +++ b/builtin/submodule--helper.c
> @@ -541,6 +541,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
>  		NULL
>  	};
>  	int ret = 1;
> +	const struct string_list *values;
>  
>  	argc = parse_options(argc, argv, prefix, module_init_options,
>  			     git_submodule_helper_usage, 0);
> @@ -552,7 +553,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
>  	 * If there are no path args and submodule.active is set then,
>  	 * by default, only initialize 'active' modules.
>  	 */
> -	if (!argc && git_config_get_value_multi("submodule.active"))
> +	if (!argc && !git_config_get_value_multi("submodule.active", &values))
>  		module_list_active(&list);
>  
>  	info.prefix = prefix;
> @@ -2708,6 +2709,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
>  	if (opt.init) {
>  		struct module_list list = MODULE_LIST_INIT;
>  		struct init_cb info = INIT_CB_INIT;
> +		const struct string_list *values;
>  
>  		if (module_list_compute(argc, argv, opt.prefix,
>  					&pathspec2, &list) < 0) {
> @@ -2720,7 +2722,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
>  		 * If there are no path args and submodule.active is set then,
>  		 * by default, only initialize 'active' modules.
>  		 */
> -		if (!argc && git_config_get_value_multi("submodule.active"))
> +		if (!argc && !git_config_get_value_multi("submodule.active", &values))
>  			module_list_active(&list);
>  
>  		info.prefix = opt.prefix;
> diff --git a/config.c b/config.c
> index cbb5a3bab74..2100b29b689 100644
> --- a/config.c
> +++ b/config.c
> @@ -2275,23 +2275,28 @@ void read_very_early_config(config_fn_t cb, void *data)
>  	config_with_options(cb, data, NULL, &opts);
>  }
>  
> -static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
> +static int configset_find_element(struct config_set *cs, const char *key,
> +				  struct config_set_element **dest)
>  {
>  	struct config_set_element k;
>  	struct config_set_element *found_entry;
>  	char *normalized_key;
> +	int ret;
> +
>  	/*
>  	 * `key` may come from the user, so normalize it before using it
>  	 * for querying entries from the hashmap.
>  	 */
> -	if (git_config_parse_key(key, &normalized_key, NULL))
> -		return NULL;
> +	ret = git_config_parse_key(key, &normalized_key, NULL);
> +	if (ret < 0)
> +		return ret;
>  
>  	hashmap_entry_init(&k.ent, strhash(normalized_key));
>  	k.key = normalized_key;
>  	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
>  	free(normalized_key);
> -	return found_entry;
> +	*dest = found_entry;
> +	return 0;
>  }
>  
>  static int configset_add_value(struct config_set *cs, const char *key, const char *value)
> @@ -2300,8 +2305,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha
>  	struct string_list_item *si;
>  	struct configset_list_item *l_item;
>  	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
> +	int ret;
>  
> -	e = configset_find_element(cs, key);
> +	ret = configset_find_element(cs, key, &e);
> +	if (ret < 0)
> +		return ret;
>  	/*
>  	 * Since the keys are being fed by git_config*() callback mechanism, they
>  	 * are already normalized. So simply add them without any further munging.
> @@ -2400,24 +2408,54 @@ int git_configset_add_parameters(struct config_set *cs)
>  int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
>  {
>  	const struct string_list *values = NULL;
> +	int ret;
> +
>  	/*
>  	 * Follows "last one wins" semantic, i.e., if there are multiple matches for the
>  	 * queried key in the files of the configset, the value returned will be the last
>  	 * value in the value list for that key.
>  	 */
> -	values = git_configset_get_value_multi(cs, key);
> +	ret = git_configset_get_value_multi(cs, key, &values);
>  
> -	if (!values)
> +	if (ret < 0)
> +		return ret;
> +	else if (!values)
>  		return 1;
>  	assert(values->nr > 0);
>  	*value = values->items[values->nr - 1].string;
>  	return 0;
>  }
>  
> -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
> +static int git_configset_get_value_multi_1(struct config_set *cs, const char *key,
> +					   const struct string_list **dest,
> +					   int knownkey)
>  {
> -	struct config_set_element *e = configset_find_element(cs, key);
> -	return e ? &e->value_list : NULL;
> +	struct config_set_element *e;
> +	int ret;
> +
> +	ret = configset_find_element(cs, key, &e);
> +	if (ret < 0 && knownkey)
> +		BUG("*_get_knownkey_*() only accepts known-good (hardcoded) keys, but '%s' is bad!", key);
> +	else if (ret < 0)
> +		return ret;
> +	else if (!e)
> +		return 1;
> +	*dest = &e->value_list;
> +
> +	return 0;
> +}
> +
> +int git_configset_get_value_multi(struct config_set *cs, const char *key,
> +				  const struct string_list **dest)
> +{
> +	return git_configset_get_value_multi_1(cs, key, dest, 0);
> +}
> +
> +int git_configset_get_knownkey_value_multi(struct config_set *cs,
> +					   const char *const key,
> +					   const struct string_list **dest)
> +{
> +	return git_configset_get_value_multi_1(cs, key, dest, 1);
>  }
>  
>  int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
> @@ -2563,11 +2601,20 @@ int repo_config_get_value(struct repository *repo,
>  	return git_configset_get_value(repo->config, key, value);
>  }
>  
> -const struct string_list *repo_config_get_value_multi(struct repository *repo,
> -						      const char *key)
> +int repo_config_get_value_multi(struct repository *repo,
> +				const char *key,
> +				const struct string_list **dest)
>  {
>  	git_config_check_init(repo);
> -	return git_configset_get_value_multi(repo->config, key);
> +	return git_configset_get_value_multi(repo->config, key, dest);
> +}
> +
> +int repo_config_get_knownkey_value_multi(struct repository *repo,
> +					 const char *const key,
> +					 const struct string_list **dest)
> +{
> +	git_config_check_init(repo);
> +	return git_configset_get_knownkey_value_multi(repo->config, key, dest);
>  }
>  
>  int repo_config_get_string(struct repository *repo,
> @@ -2684,9 +2731,15 @@ int git_config_get_value(const char *key, const char **value)
>  	return repo_config_get_value(the_repository, key, value);
>  }
>  
> -const struct string_list *git_config_get_value_multi(const char *key)
> +int git_config_get_value_multi(const char *key, const struct string_list **dest)
> +{
> +	return repo_config_get_value_multi(the_repository, key, dest);
> +}
> +
> +int git_config_get_knownkey_value_multi(const char *const key,
> +					const struct string_list **dest)
>  {
> -	return repo_config_get_value_multi(the_repository, key);
> +	return repo_config_get_knownkey_value_multi(the_repository, key, dest);
>  }
>  
>  int git_config_get_string(const char *key, char **dest)
> @@ -2833,7 +2886,16 @@ void git_die_config(const char *key, const char *err, ...)
>  		error_fn(err, params);
>  		va_end(params);
>  	}
> -	values = git_config_get_value_multi(key);
> +
> +	/*
> +	 * We don't have a "const" key here, but we should definitely
> +	 * have one that's passed git_config_parse_key() already, if
> +	 * we're at the point of complaining about its value. So let's
> +	 * use *_knownkey_value_multi() here to get that BUG(...).
> +	 */
> +	if (git_config_get_knownkey_value_multi(key, &values))
> +		BUG("key '%s' does not exist, should not be given to git_die_config()",
> +		    key);
>  	kv_info = values->items[values->nr - 1].util;
>  	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
>  }
> diff --git a/config.h b/config.h
> index ca994d77147..c88619b7dcf 100644
> --- a/config.h
> +++ b/config.h
> @@ -457,11 +457,30 @@ int git_configset_add_parameters(struct config_set *cs);
>  
>  /**
>   * Finds and returns the value list, sorted in order of increasing priority
> - * for the configuration variable `key` and config set `cs`. When the
> - * configuration variable `key` is not found, returns NULL. The caller
> - * should not free or modify the returned pointer, as it is owned by the cache.
> + * for the configuration variable `key` and config set `cs`.
> + *
> + * When the configuration variable `key` is not found, returns 1
> + * without touching `value`.
> + *
> + * The key will be parsed for validity with git_config_parse_key(), on
> + * error a negative value will be returned. See
> + * git_configset_get_knownkey_value_multi() for a version of this which
> + * BUG()s out on negative return values.
> + *
> + * The caller should not free or modify the returned pointer, as it is
> + * owned by the cache.
> + */
> +int git_configset_get_value_multi(struct config_set *cs, const char *key,
> +				  const struct string_list **dest);
> +
> +/**
> + * Like git_configset_get_value_multi(), but BUG()s out if the return
> + * value is < 0. Use it for keys known to pass git_config_parse_key(),
> + * i.e. those hardcoded in the code, and never user-provided keys.
>   */
> -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
> +int git_configset_get_knownkey_value_multi(struct config_set *cs,
> +					   const char *const key,
> +					   const struct string_list **dest);
>  
>  /**
>   * Clears `config_set` structure, removes all saved variable-value pairs.
> @@ -495,8 +514,12 @@ struct repository;
>  void repo_config(struct repository *repo, config_fn_t fn, void *data);
>  int repo_config_get_value(struct repository *repo,
>  			  const char *key, const char **value);
> -const struct string_list *repo_config_get_value_multi(struct repository *repo,
> -						      const char *key);
> +int repo_config_get_value_multi(struct repository *repo,
> +				const char *key,
> +				const struct string_list **dest);
> +int repo_config_get_knownkey_value_multi(struct repository *repo,
> +					 const char *const key,
> +					 const struct string_list **dest);
>  int repo_config_get_string(struct repository *repo,
>  			   const char *key, char **dest);
>  int repo_config_get_string_tmp(struct repository *repo,
> @@ -543,10 +566,21 @@ int git_config_get_value(const char *key, const char **value);
>  /**
>   * Finds and returns the value list, sorted in order of increasing priority
>   * for the configuration variable `key`. When the configuration variable
> - * `key` is not found, returns NULL. The caller should not free or modify
> - * the returned pointer, as it is owned by the cache.
> + * `key` is not found, returns 1 without touching `value`.
> + *
> + * The caller should not free or modify the returned pointer, as it is
> + * owned by the cache.
> + */
> +int git_config_get_value_multi(const char *key,
> +			       const struct string_list **dest);
> +
> +/**
> + * A wrapper for git_config_get_value_multi() which does for it what
> + * git_configset_get_knownkey_value_multi() does for
> + * git_configset_get_value_multi().
>   */
> -const struct string_list *git_config_get_value_multi(const char *key);
> +int git_config_get_knownkey_value_multi(const char *const key,
> +					const struct string_list **dest);
>  
>  /**
>   * Resets and invalidates the config cache.
> diff --git a/pack-bitmap.c b/pack-bitmap.c
> index 440407f1be7..0b4e73abbfa 100644
> --- a/pack-bitmap.c
> +++ b/pack-bitmap.c
> @@ -2301,7 +2301,12 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)
>  
>  const struct string_list *bitmap_preferred_tips(struct repository *r)
>  {
> -	return repo_config_get_value_multi(r, "pack.preferbitmaptips");
> +	const struct string_list *dest;
> +
> +	if (!repo_config_get_knownkey_value_multi(r, "pack.preferbitmaptips",
> +					       &dest))
> +		return dest;
> +	return NULL;
>  }
>  
>  int bitmap_is_preferred_refname(struct repository *r, const char *refname)
> diff --git a/submodule.c b/submodule.c
> index bf7a2c79183..e8c4362743d 100644
> --- a/submodule.c
> +++ b/submodule.c
> @@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
>  	free(key);
>  
>  	/* submodule.active is set */
> -	sl = repo_config_get_value_multi(repo, "submodule.active");
> -	if (sl) {
> +	if (!repo_config_get_knownkey_value_multi(repo, "submodule.active", &sl)) {
>  		struct pathspec ps;
>  		struct strvec args = STRVEC_INIT;
>  		const struct string_list_item *item;
> diff --git a/t/helper/test-config.c b/t/helper/test-config.c
> index 4ba9eb65606..f0d476d2376 100644
> --- a/t/helper/test-config.c
> +++ b/t/helper/test-config.c
> @@ -95,8 +95,7 @@ int cmd__config(int argc, const char **argv)
>  			goto exit1;
>  		}
>  	} else if (argc == 3 && !strcmp(argv[1], "get_value_multi")) {
> -		strptr = git_config_get_value_multi(argv[2]);
> -		if (strptr) {
> +		if (!git_config_get_knownkey_value_multi(argv[2], &strptr)) {
>  			for (i = 0; i < strptr->nr; i++) {
>  				v = strptr->items[i].string;
>  				if (!v)
> @@ -159,8 +158,7 @@ int cmd__config(int argc, const char **argv)
>  				goto exit2;
>  			}
>  		}
> -		strptr = git_configset_get_value_multi(&cs, argv[2]);
> -		if (strptr) {
> +		if (!git_configset_get_knownkey_value_multi(&cs, argv[2], &strptr)) {
>  			for (i = 0; i < strptr->nr; i++) {
>  				v = strptr->items[i].string;
>  				if (!v)
> diff --git a/versioncmp.c b/versioncmp.c
> index 069ee94a4d7..9064478dc4a 100644
> --- a/versioncmp.c
> +++ b/versioncmp.c
> @@ -160,10 +160,14 @@ int versioncmp(const char *s1, const char *s2)
>  	}
>  
>  	if (!initialized) {
> -		const struct string_list *deprecated_prereleases;
> +		const struct string_list *deprecated_prereleases = NULL;
> +
>  		initialized = 1;
> -		prereleases = git_config_get_value_multi("versionsort.suffix");
> -		deprecated_prereleases = git_config_get_value_multi("versionsort.prereleasesuffix");
> +		git_config_get_knownkey_value_multi("versionsort.suffix",
> +						 &prereleases);
> +		git_config_get_value_multi("versionsort.prereleasesuffix",
> +					   &deprecated_prereleases);
> +
>  		if (prereleases) {
>  			if (deprecated_prereleases)
>  				warning("ignoring versionsort.prereleasesuffix because versionsort.suffix is set");
> -- 
> 2.38.0.1251.g3eefdfb5e7a
> 

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 01/10] config API: have *_multi() return an "int" and take a "dest"
  2022-10-26 18:49   ` SZEDER Gábor
@ 2022-10-26 19:33     ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-26 19:33 UTC (permalink / raw)
  To: SZEDER Gábor
  Cc: git, Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau


On Wed, Oct 26 2022, SZEDER Gábor wrote:

> On Wed, Oct 26, 2022 at 05:35:14PM +0200, Ævar Arnfjörð Bjarmason wrote:
>> The git_configset_get_value_multi() function added in 3c8687a73ee (add
>> `config_set` API for caching config-like files, 2014-07-28) is a
>> fundamental part of of the config API, and
>> e.g. "git_config_get_value()" and others are implemented in terms of
>> it.
>> 
>> But it has had the limitation that configset_find_element() calls
>> git_config_parse_key(), but then throws away the distinction between a
>> "ret < 1" return value from it, and return values that indicate a key
>
> Shouldn't that be "ret < 0"?

Yes, sorry, that's just a typo. It's <0 for API errors (e.g. unable to
parse your key bad key), 0 for OK, 1 for key doesn't exist.

>> doesn't exist. As a result the git_config_get_value_multi() function
>> would either return a "const struct string_list *", or NULL.
>> 
>> By changing the *_multi() function to return an "int" for the status
>> and to write to a "const struct string_list **dest" parameter we can
>> avoid losing this information. API callers can now do:
>> 
>> 	const struct string_list *dest;
>> 	int ret;
>> 
>> 	ret = git_config_get_value_multi(key, &dest);
>> 	if (ret < 1)
>
> This catches all negative values and zero.
>
>> 		die("bad key: %s", key);
>> 	else if (ret)
>
> This catches all non-zero values.
>
>> 		; /* key does not exist */
>> 	else
>
> So how could this ever be executed?!

Yes, sorry. It's the same typo/thinko.

>> 		; /* got key, can use "dest" */
>> 
>> A "get_knownkey_value_multi" variant is also provided, which will
>> BUG() out in the "ret < 1" case. This is useful in the cases where we
>
> Shouldn't that be "ret < 0" as well?  The condition in that "knownkey"
> variant added in this patch is:
>
>   +	ret = configset_find_element(cs, key, &e);
>   +	if (ret < 0 && knownkey)
>   +		BUG("*_get_knownkey_*() only accepts known-good (hardcoded) keys, but '%s' is bad!", key);

Yes, FWIW the code isn't incorrect in this regard, I just screwed up the
commit message, sorry.

The canonical example that isn't tricky is in builtin/for-each-repo.c, i.e.:

        err = repo_config_get_value_multi_string(the_repository, config_key, &values);
        if (err < 0)
                usage_msg_optf(_("got bad config --config=%s"),
                               for_each_repo_usage, options, config_key);
        else if (err)
                return 0;

I.e. it wants to ignore non-existing config ("else if"), but now we
distinguish that from errors. The *_multi() API on master doesn't allow
for that.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 01/10] config API: have *_multi() return an "int" and take a "dest"
  2022-10-26 15:35 ` [PATCH 01/10] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
  2022-10-26 18:49   ` SZEDER Gábor
@ 2022-10-27 19:27   ` Junio C Hamano
  1 sibling, 0 replies; 134+ messages in thread
From: Junio C Hamano @ 2022-10-27 19:27 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> By changing the *_multi() function to return an "int" for the status
> and to write to a "const struct string_list **dest" parameter we can
> avoid losing this information. API callers can now do:
>
> 	const struct string_list *dest;
> 	int ret;
>
> 	ret = git_config_get_value_multi(key, &dest);
> 	if (ret < 1)
> 		die("bad key: %s", key);
> 	else if (ret)
> 		; /* key does not exist */
> 	else
> 		; /* got key, can use "dest" */

It is a good thing to allow the callers to tell "no such key-value
pair exists", "key is malformed", and "here are the values for the
key".  And the above if/else if/else cascade is a reasonable
interface to give the callers for that (modulo that "negative is
bad" should be kept to match our API convention).

>
> A "get_knownkey_value_multi" variant is also provided, which will
> BUG() out in the "ret < 1" case. This is useful in the cases where we
> hardcode the keyname in our source code, and therefore use the more
> idiomatic pattern of:
>
> 	if (!git_config_get_value_multi(key, &dest)
> 		; /* got key, can use "dest" */
> 	else
> 		; /* key does not exist */

I doubt it is a good idea to add such a specialized interface begin
with.  Let's not bloat the API for little benefit.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 04/10] string-list API: mark "struct_string_list" to "for_each_string_list" const
  2022-10-26 15:35 ` [PATCH 04/10] string-list API: mark "struct_string_list" to "for_each_string_list" const Ævar Arnfjörð Bjarmason
@ 2022-10-27 19:32   ` Junio C Hamano
  2022-10-27 23:04     ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 134+ messages in thread
From: Junio C Hamano @ 2022-10-27 19:32 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> Add a "const" to the "struct string_list *" passed to
> for_each_string_list().
>
> This is arguably abuse of the type system, as the
> "string_list_each_func_t fn" take a "struct string_list_item *",
> i.e. not one with a "const", and those functions *can* modify those
> items.
>
> But as we'll see in a subsequent commit we have other such iteration
> functions that could benefit from a "const", i.e. to declare that
> we're not altering the list itself, even though we might be calling
> functions that alter its values.

The callback functions are allowed to (by taking a non-const
pointer) modify the items, but are there ones that actually modify
them?


^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 06/10] builtin/gc.c: use "unsorted_string_list_has_string()" where appropriate
  2022-10-26 15:35 ` [PATCH 06/10] builtin/gc.c: use "unsorted_string_list_has_string()" where appropriate Ævar Arnfjörð Bjarmason
@ 2022-10-27 19:37   ` Junio C Hamano
  2022-10-27 23:25     ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 134+ messages in thread
From: Junio C Hamano @ 2022-10-27 19:37 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> Refactor a "do I have an element like this?" pattern added in [1] and
> [2] to use unsorted_string_list_has_string() instead of a
> for_each_string_list_item() loop.

In the longer term, I am not sure if we want to keep such code that
uses string-list as a "database to be looked up with the string as
the key".  I am not sure it is worth our review bandwidth to change
a for-each-string-list that terminates early to its shorthand
unsorted_string_list_has_string().  Surely each such conversation
would allow us to lose 4 to 5 lines, but longer term we should be
discuraging the use of unsorted_string_list_has_string() in the
first place.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 07/10] config API: add and use "lookup_value" functions
  2022-10-26 15:35 ` [PATCH 07/10] config API: add and use "lookup_value" functions Ævar Arnfjörð Bjarmason
@ 2022-10-27 19:42   ` Junio C Hamano
  0 siblings, 0 replies; 134+ messages in thread
From: Junio C Hamano @ 2022-10-27 19:42 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> Change various users of the config API who only wanted to ask if a
> configuration key existed to use a new *_config*_lookup_value() family
> of functions. Unlike the existing API functions in the API this one
> doesn't take a "dest" argument.

When I hear "lookup-value", the first thing I would expect was
"look-up by value" (i.e. the reverse look-up to find key).  That is
not what is going on.

What is presented here is "does the key have corresponding value
defined in the configuration system, yes/no?", isn't it?

I would expect such a function to be named *_config_key_exists().

> diff --git a/config.h b/config.h
> index a5710c5856e..cf1ae7862a8 100644
> --- a/config.h
> +++ b/config.h
> @@ -502,6 +502,8 @@ void git_configset_clear(struct config_set *cs);
>   * is owned by the cache.
>   */
>  int git_configset_get_value(struct config_set *cs, const char *key, const char **dest);
> +RESULT_MUST_BE_USED
> +int git_configset_lookup_value(struct config_set *cs, const char *key);

This must be documented, especially if we give it such a bad name ;-).

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 08/10] config tests: add "NULL" tests for *_get_value_multi()
  2022-10-26 15:35 ` [PATCH 08/10] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2022-10-27 19:43   ` Junio C Hamano
  0 siblings, 0 replies; 134+ messages in thread
From: Junio C Hamano @ 2022-10-27 19:43 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> A less well known edge case in the config format is that keys can be
> value-less, a shorthand syntax for "true" boolean keys. I.e. these two
> are equivalent as far as "--type=bool" is concerned:
>
> 	[a]key
> 	[a]key = true
>
> But as far as our parser is concerned the values for these two are
> NULL, and "true". I.e. for a sequence like:
>
> 	[a]key=x
> 	[a]key
> 	[a]key=y
>
> We get a "struct string_list" with "string" members with ".string"
> values of:
>
> 	{ "x", NULL, "y" }
>
> This behavior goes back to the initial implementation of
> git_config_bool() in 17712991a59 (Add ".git/config" file parser,
> 2005-10-10).
>
> When the "t/t1308-config-set.sh" tests were added in [1] only one of
> the three "(NULL)" lines in "t/helper/test-config.c" had any test
> coverage. This change adds tests that stress the remaining two.

Good.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 09/10] config API: add "string" version of *_value_multi(), fix segfaults
  2022-10-26 15:35 ` [PATCH 09/10] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
@ 2022-10-27 19:49   ` Junio C Hamano
  2022-10-27 19:52     ` Junio C Hamano
  0 siblings, 1 reply; 134+ messages in thread
From: Junio C Hamano @ 2022-10-27 19:49 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> Fix numerous and mostly long-standing segfaults in consumers of
> the *_config_*value_multi() API. As discussed in the preceding commit
> an empty key in the config syntax yields a "NULL" string, which these
> users would give to strcmp() (or similar), resulting in segfaults.

Sounds like a good idea.

I would have called them _nonbool(), not _string(), especially
because we are not going to have other variants like _int(), though.



^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 09/10] config API: add "string" version of *_value_multi(), fix segfaults
  2022-10-27 19:49   ` Junio C Hamano
@ 2022-10-27 19:52     ` Junio C Hamano
  2022-10-27 23:44       ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 134+ messages in thread
From: Junio C Hamano @ 2022-10-27 19:52 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor

Junio C Hamano <gitster@pobox.com> writes:

> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
>
>> Fix numerous and mostly long-standing segfaults in consumers of
>> the *_config_*value_multi() API. As discussed in the preceding commit
>> an empty key in the config syntax yields a "NULL" string, which these
>> users would give to strcmp() (or similar), resulting in segfaults.
>
> Sounds like a good idea.
>
> I would have called them _nonbool(), not _string(), especially
> because we are not going to have other variants like _int(), though.

Actually, I take it back.  Instead of introducing _string(), how
about introducing _bool() and convert those minority callers that do
want to see boolean values to use the new one, while rejecting NULLs
for everybody else that calls the traditional "get_value" family of
functions?  That would "optimize" for the majority of simpler users,
wouldn't it?


^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
                   ` (9 preceding siblings ...)
  2022-10-26 15:35 ` [PATCH 10/10] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
@ 2022-10-27 20:12 ` Junio C Hamano
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
  11 siblings, 0 replies; 134+ messages in thread
From: Junio C Hamano @ 2022-10-27 20:12 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> I also think that part of the config API is a wart, but that we should
> go for a different solution. It's the only config function that
> doesn't return an "int" indicating whether we found the key.

Overall I saw some things to like in the series, but was not
impressed by others.  The _multi() thing in the earliest patch is a
welcome change, giving an option to call nonbool() is a good idea
(but I have doubts about the exectuion), and "does the key exist?"
may be a good thing to have.  Others ranged between "Meh?" to "it
might be good, but why does it have to be done here now?".

Thanks.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 04/10] string-list API: mark "struct_string_list" to "for_each_string_list" const
  2022-10-27 19:32   ` Junio C Hamano
@ 2022-10-27 23:04     ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-27 23:04 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor


On Thu, Oct 27 2022, Junio C Hamano wrote:

> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
>
>> Add a "const" to the "struct string_list *" passed to
>> for_each_string_list().
>>
>> This is arguably abuse of the type system, as the
>> "string_list_each_func_t fn" take a "struct string_list_item *",
>> i.e. not one with a "const", and those functions *can* modify those
>> items.
>>
>> But as we'll see in a subsequent commit we have other such iteration
>> functions that could benefit from a "const", i.e. to declare that
>> we're not altering the list itself, even though we might be calling
>> functions that alter its values.
>
> The callback functions are allowed to (by taking a non-const
> pointer) modify the items, but are there ones that actually modify
> them?

Tree-wide that's:

	 11 files changed, 18 insertions(+), 18 deletions(-)

I.e. a bunch of changes like:

	-static int get_notes_refs(struct string_list_item *item, void *arg)
	+static int get_notes_refs(const struct string_list_item *item, void *arg)

It turns out there's a grand total of one user of that:
	
	setup.c: In function ‘canonicalize_ceiling_entry’:
	setup.c:1102:30: error: assignment of member ‘string’ in read-only object
	 1102 |                 item->string = real_path;
	      |                              ^

But note that that's for the "filter" variant. In any case using the
same function pointer type in eb5f0c7a616 (string_list: add a new
function, filter_string_list(), 2012-09-12) for both was probably a
mistake.

But still, I think it's best not to do anything about *that*. I.e. it
makes sense for such an interface to say that the iterator helper takes
your const list, i.e. unlike filter_string_list() it's not expected to
be changing the list itself.

But you as as the caller are then free to change list items you're
given.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 06/10] builtin/gc.c: use "unsorted_string_list_has_string()" where appropriate
  2022-10-27 19:37   ` Junio C Hamano
@ 2022-10-27 23:25     ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-27 23:25 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor


On Thu, Oct 27 2022, Junio C Hamano wrote:

> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
>
>> Refactor a "do I have an element like this?" pattern added in [1] and
>> [2] to use unsorted_string_list_has_string() instead of a
>> for_each_string_list_item() loop.
>
> In the longer term, I am not sure if we want to keep such code that
> uses string-list as a "database to be looked up with the string as
> the key".  I am not sure it is worth our review bandwidth to change
> a for-each-string-list that terminates early to its shorthand
> unsorted_string_list_has_string().  Surely each such conversation
> would allow us to lose 4 to 5 lines, but longer term we should be
> discuraging the use of unsorted_string_list_has_string() in the
> first place.

I haven't benchmarked, but I'd think on modern computers O(n) for such
short lists would be more performance due to cache locality, i.e. not
worth pre-sorting it, or making it a hash table.

But for such small amounts of data I'd think it would be fine either
way.

As to the change, I'm fine with leaving this out.

The reason it's in here is because this series came out as a reply to
Stolee's earlier RFC.

I think it's a fair summary to say that the reason we started talking
about this at all is because the topic at hand was how to make this
exact code in builtin/gc.c safer and more idiomatic. I.e. see:

	https://lore.kernel.org/git/e06cb4df081bc2222731f9185a22ed7ad67e3814.1664287711.git.gitgitgadget@gmail.com/

And my earlier summary of that, the very beginning showing the API forms
under discussion:

	https://lore.kernel.org/git/220928.868rm3w9d4.gmgdl@evledraar.gmail.com/

So Re this & your "it might be good, but why does it have to be done
here now?" reply to the CL: Yeah I can eject some of this, but having a
series (and a predecessor RFC) whose main reason for existing is making
this API safer & nicer seems incomplete unless we're also converting
callers to use those supposedly nicer patterns.

In general I think this sort of change is exactly the sort of thing
you'd want in such a series. A test of a good API isn't just that it
looks or acts nicely in isolation, but that it's easily combined with
other things you might expect to use with it.

Hence this 04-06/10. I.e. the original contention in the RFC was that we
had to return a dummy string list to make these sort of patterns
safer/nicer/idiomatic. I think these patches serve as a convincing
counter-argument to that.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 09/10] config API: add "string" version of *_value_multi(), fix segfaults
  2022-10-27 19:52     ` Junio C Hamano
@ 2022-10-27 23:44       ` Ævar Arnfjörð Bjarmason
  2022-10-28 19:16         ` Junio C Hamano
  0 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-27 23:44 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor


On Thu, Oct 27 2022, Junio C Hamano wrote:

> Junio C Hamano <gitster@pobox.com> writes:
>
>> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
>>
>>> Fix numerous and mostly long-standing segfaults in consumers of
>>> the *_config_*value_multi() API. As discussed in the preceding commit
>>> an empty key in the config syntax yields a "NULL" string, which these
>>> users would give to strcmp() (or similar), resulting in segfaults.
>>
>> Sounds like a good idea.
>>
>> I would have called them _nonbool(), not _string(), especially
>> because we are not going to have other variants like _int(), though.
>
> Actually, I take it back.  Instead of introducing _string(), how
> about introducing _bool() and convert those minority callers that do
> want to see boolean values to use the new one, while rejecting NULLs
> for everybody else that calls the traditional "get_value" family of
> functions?  That would "optimize" for the majority of simpler users,
> wouldn't it?

I don't think the goal should be just to optimize for those current
users, but to leave the config API in a state where it makes sense
conceptually.

For the scalar (single) values we have a low-level "low-level" function,
and then variants to get it as a bool, path, string, int etc.

I think a "multi" function should just be the logical result of applying
one of those "types" to list. I.e. (pseudocode):

	a_raw = get_config_raw("a.key");
	a_string = stringify(a_raw);

And, as a list:

	list_raw = get_config_raw_multi("a.key");
	list_strings = map { stringify(item) } list_raw;

Now, if we don't supply the equivalent of the "raw, but multi-value"
function we'll make it hard to use the API, because now you can't think
about it as the "multi" just being a list version of what you get with
the scalar version.

E.g. what should we do about "[a]key" in a list API that stringifies by
default? If you then want "stringified bool" we're only left with bad choices:

 - If you die that's bed, because that's a legit true value
 - If you coerce it to "" to help the string use case you get the wrong
   answer, because "[a]key=" (empty string) is false, but "[a]key"
   (value-less) is true.
 - Ditto if you prune it out, as then it won't be seen in the bool list.

Which is why I went for the end-state here. I.e. it's now easy to add
other "multi" variants (we'd need to add coercion, but that's easy
enough).

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 09/10] config API: add "string" version of *_value_multi(), fix segfaults
  2022-10-27 23:44       ` Ævar Arnfjörð Bjarmason
@ 2022-10-28 19:16         ` Junio C Hamano
  2022-10-31 18:22           ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 134+ messages in thread
From: Junio C Hamano @ 2022-10-28 19:16 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

>> Actually, I take it back.  Instead of introducing _string(), how
>> about introducing _bool() and convert those minority callers that do
>> want to see boolean values to use the new one, while rejecting NULLs
>> for everybody else that calls the traditional "get_value" family of
>> functions?  That would "optimize" for the majority of simpler users,
>> wouldn't it?
>
> I don't think the goal should be just to optimize for those current
> users, but to leave the config API in a state where it makes sense
> conceptually.

It is more like guiding a conceptually clean design using the need
of the current users to rein in pursuit of theoretical "elegance".

> Now, if we don't supply the equivalent of the "raw, but multi-value"
> function we'll make it hard to use the API, because now you can't think
> about it as the "multi" just being a list version of what you get with
> the scalar version.

I am not interested in _bool() variant that "stringifies" NULL to
"true" at all.  What I was suggesting was:

 * Reserve the current get and get_multi for those who should have
   been calling config_error_nonbool() themselves (because your
   _string() has not been available to them, they were lazy not to
   bother, leading to NULL dereference given certain end-user data).
   And do the config_error_nonbool() inside the updated get and
   get_multi without introducing _string() variant at all.

 * The above alone WILL break callers who are prepared to handle
   "bool" and "bool plus some other string", because they are fully
   expecting that the get API will give them NULL but the above
   update will instead stop before they see the NULL they are
   prepared to handle themselves.  Introduce _bool variants and make
   them call them.

without any "stringifying" at all.


^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 09/10] config API: add "string" version of *_value_multi(), fix segfaults
  2022-10-28 19:16         ` Junio C Hamano
@ 2022-10-31 18:22           ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-10-31 18:22 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Derrick Stolee, Jeff King, Taylor Blau, SZEDER Gábor


On Fri, Oct 28 2022, Junio C Hamano wrote:

> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>
>>> Actually, I take it back.  Instead of introducing _string(), how
>>> about introducing _bool() and convert those minority callers that do
>>> want to see boolean values to use the new one, while rejecting NULLs
>>> for everybody else that calls the traditional "get_value" family of
>>> functions?  That would "optimize" for the majority of simpler users,
>>> wouldn't it?
>>
>> I don't think the goal should be just to optimize for those current
>> users, but to leave the config API in a state where it makes sense
>> conceptually.
>
> It is more like guiding a conceptually clean design using the need
> of the current users to rein in pursuit of theoretical "elegance".

I agree that the current code users don't care either way, they'll be
getting the same thing.

But being able to readily understand an API is valuable too. The config
API is bad enough with all repetition of:

	{git_configset,repo_config,git_config}_get_value()
	{git_configset,repo_config,git_config}_get_string()
	{git_configset,repo_config,git_config}_get_bool()
        [...]

I think it's worth it to be able to say that:

	{git_configset,repo_config,git_config}_get_value_multi()
	{git_configset,repo_config,git_config}_get_value_string()
        <ditto "bool">

Are "just like the scalar version, but multi". Actually when I summarize
it like that I realize I should really make it:

	{git_configset,repo_config,git_config}_get_string_multi()

I.e. "*_get_string_multi()", not "*_get_value_multi_string()". I don't
know what I was thinking.

But aside from that, the point is I think it's worth it not to have it
instead be:

        # "non-string" doesn't exist, but get it via some use of
        # (currently static) configset_find_element()

        # "get value", but really "get string, for multi"
	{git_configset,repo_config,git_config}_get_value_multi()

        # ???
	{git_configset,repo_config,git_config}_get_bool_multi()

We currently don't need/have a "*_get_bool_multi()", which I think is
besides the point. We might in the future, and should forsee that we're
picking a nonsensical naming convention.

We also have similar gaps in the current API (not all variants of all
functions exist, for the scalar variants), but at least those that we do
have behave consistently.

>> Now, if we don't supply the equivalent of the "raw, but multi-value"
>> function we'll make it hard to use the API, because now you can't think
>> about it as the "multi" just being a list version of what you get with
>> the scalar version.
>
> I am not interested in _bool() variant that "stringifies" NULL to
> "true" at all.  What I was suggesting was:
>
>  * Reserve the current get and get_multi for those who should have
>    been calling config_error_nonbool() themselves (because your
>    _string() has not been available to them, they were lazy not to
>    bother, leading to NULL dereference given certain end-user data).
>    And do the config_error_nonbool() inside the updated get and
>    get_multi without introducing _string() variant at all.

I get what you're saying, I just think it suffers from the problem
outlined above, and that it's worth solving it.

>  * The above alone WILL break callers who are prepared to handle
>    "bool" and "bool plus some other string", because they are fully
>    expecting that the get API will give them NULL but the above
>    update will instead stop before they see the NULL they are
>    prepared to handle themselves.  Introduce _bool variants and make
>    them call them.

Even if it wasn't for the naming question, I think the arrangement in
this series is also better in that I need to go and change each caller
to the new variant, and explain for each one why it's OK.

Whereas if we just sneakconfig_error_nonbool() into the low-level API
we're going to have a smaller change, but also one that's basically
"trust me, I read the code of all the callers, this should be fine...".


^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v2 0/9] config API: make "multi" safe, fix numerous segfaults
  2022-10-26 15:35 [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Ævar Arnfjörð Bjarmason
                   ` (10 preceding siblings ...)
  2022-10-27 20:12 ` [PATCH 00/10] config API: make "multi" safe, fix numerous segfaults Junio C Hamano
@ 2022-11-01 23:05 ` Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 1/9] for-each-repo tests: test bad --config keys Ævar Arnfjörð Bjarmason
                     ` (10 more replies)
  11 siblings, 11 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-01 23:05 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

This series fixes numerous segfaults in config API users, because they
didn't expect *_get_multi() to hand them a string_list with a NULL in
it given config like "[a] key" (note, no "="'s).

A larger general overview at:
https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/

Changes since v1:
 
 * Fixed that "ret < 1" v.s. "ret < 0" thinko in the commit message
   (code was fine).

 * This is now much reduced in scope. The v1 was 10 patches, this is
   9, but as the range-diff shows there's 3x new tests at the
   beginning. So the meat of this is smaller.

 * The previous main fix is now in 7-8/9 instead of one patch, I split
   up the test addition (starting with test_expect_failure) and the
   fix.

 * There's no more "known key" API that'll BUG() out if we get < 0.

 * There's no more "lookup_value". We just leave the API users that
   only care if there is a list in-place.

 * The digression to add "const"-ing to the "struct string_list" is
   gone, and the change to use unsorted_string_list_has_string() in
   builtin/gc.c. I can submit that on top of this.

 * Rewrote/redid some things to make subsequent diffs
   smaller. E.g. 4/9 makes 5/9 and especialy 6/9 smaller.

 * Renamed the new helper from git_config_get_value_multi() to
   git_config_get_string_multi().

 * There's still a low-level git_config_get_value_multi(). The updated
   8/9 commit message makes the case for it, i.e. as opposed to having
   all of *_multi() have the equivalent of "--type=string" semantics
   (although we don't expose that via the "git config" tool...).

Passing CI for this at:
https://github.com/avar/git/tree/avar/have-git_configset_get_value-use-dest-and-int-pattern-2

Ævar Arnfjörð Bjarmason (9):
  for-each-repo tests: test bad --config keys
  config tests: cover blind spots in git_die_config() tests
  config tests: add "NULL" tests for *_get_value_multi()
  versioncmp.c: refactor config reading next commit
  config API: have *_multi() return an "int" and take a "dest"
  for-each-repo: error on bad --config
  config API users: test for *_get_value_multi() segfaults
  config API: add "string" version of *_value_multi(), fix segfaults
  for-each-repo: with bad config, don't conflate <path> and <cmd>

 builtin/for-each-repo.c              | 14 ++---
 builtin/gc.c                         |  6 +-
 builtin/log.c                        |  5 +-
 builtin/submodule--helper.c          |  6 +-
 config.c                             | 88 +++++++++++++++++++++++-----
 config.h                             | 50 +++++++++++++---
 pack-bitmap.c                        |  6 +-
 submodule.c                          |  3 +-
 t/helper/test-config.c               |  6 +-
 t/t0068-for-each-repo.sh             | 19 ++++++
 t/t1308-config-set.sh                | 30 ++++++++++
 t/t3309-notes-merge-auto-resolve.sh  |  7 ++-
 t/t4202-log.sh                       | 15 +++++
 t/t5304-prune.sh                     | 12 +++-
 t/t5310-pack-bitmaps.sh              | 21 +++++++
 t/t5552-skipping-fetch-negotiator.sh | 16 +++++
 t/t7004-tag.sh                       | 17 ++++++
 t/t7413-submodule-is-active.sh       | 16 +++++
 t/t7900-maintenance.sh               | 38 ++++++++++++
 versioncmp.c                         | 22 ++++---
 20 files changed, 337 insertions(+), 60 deletions(-)

Range-diff against v1:
 -:  ----------- >  1:  b8fd3bea4d1 for-each-repo tests: test bad --config keys
 -:  ----------- >  2:  6cd0d6faf3c config tests: cover blind spots in git_die_config() tests
 8:  e7568dbe6fe =  3:  f2a8766a802 config tests: add "NULL" tests for *_get_value_multi()
 -:  ----------- >  4:  42cfc61202d versioncmp.c: refactor config reading next commit
 1:  eefa253ab1f !  5:  48fb7cbf585 config API: have *_multi() return an "int" and take a "dest"
    @@ Metadata
      ## Commit message ##
         config API: have *_multi() return an "int" and take a "dest"
     
    -    The git_configset_get_value_multi() function added in 3c8687a73ee (add
    -    `config_set` API for caching config-like files, 2014-07-28) is a
    -    fundamental part of of the config API, and
    -    e.g. "git_config_get_value()" and others are implemented in terms of
    -    it.
    +    Have the "git_configset_get_value_multi()" function and its siblings
    +    return an "int" and populate a "**dest" parameter like every other
    +    git_configset_get_*()" in the API.
     
    -    But it has had the limitation that configset_find_element() calls
    -    git_config_parse_key(), but then throws away the distinction between a
    -    "ret < 1" return value from it, and return values that indicate a key
    -    doesn't exist. As a result the git_config_get_value_multi() function
    -    would either return a "const struct string_list *", or NULL.
    +    As we'll see in in subsequent commits this fixes a blind spot in the
    +    API where it wasn't possible to tell whether a list was empty from
    +    whether a config key existed. We'll take advantage of that in
    +    subsequent commits, but for now we're faithfully converting existing
    +    API callers.
     
    -    By changing the *_multi() function to return an "int" for the status
    -    and to write to a "const struct string_list **dest" parameter we can
    -    avoid losing this information. API callers can now do:
    +    See [1] for the initial addition of "git_configset_get_value_multi()"
     
    -            const struct string_list *dest;
    -            int ret;
    +    1. 3c8687a73ee (add `config_set` API for caching config-like files,
    +       2014-07-28).
     
    -            ret = git_config_get_value_multi(key, &dest);
    -            if (ret < 1)
    -                    die("bad key: %s", key);
    -            else if (ret)
    -                    ; /* key does not exist */
    -            else
    -                    ; /* got key, can use "dest" */
    -
    -    A "get_knownkey_value_multi" variant is also provided, which will
    -    BUG() out in the "ret < 1" case. This is useful in the cases where we
    -    hardcode the keyname in our source code, and therefore use the more
    -    idiomatic pattern of:
    -
    -            if (!git_config_get_value_multi(key, &dest)
    -                    ; /* got key, can use "dest" */
    -            else
    -                    ; /* key does not exist */
    +    A logical follow-up to this would be to change the various "*_get_*()"
    +    functions to ferry the git_configset_get_value() return value to their
    +    own callers, e.g. git_configset_get_int() returns "1" rather than
    +    ferrying up the "-1" that "git_configset_get_value()" might return,
    +    but that's not being done in this series
     
    -    The "knownkey" name was picked instead of e.g. "const" to avoid a
    -    repeat of the issues noted in f1de981e8b6 (config: fix leaks from
    -    git_config_get_string_const(), 2020-08-14) and 9a53219f69b (config:
    -    drop git_config_get_string_const(), 2020-08-17). API users might think
    -    that "const" means that the value(s) don't need to be free'd.
    +    Most of this is straightforward, commentary on cases that stand out:
     
    -    As noted in commentary here we treat git_die_config() as a
    -    special-case, i.e. we assume that a value we're complaining about has
    -    already had its key pass the git_config_parse_key() check.
    +    - As we've tested for in a preceding commit we can rely on getting the
    +      config list in git_die_config(), and as we need to handle the new
    +      return value let's BUG() out if we can't acquire it.
     
    -    Likewise we consider the keys passed to "t/helper/test-config.c" to be
    -    "knownkey", and will emit a BUG() if they don't pass
    -    git_config_parse_key(). Those will come from our *.sh tests, so
    -    they're also "known keys" coming from our sources.
    +    - In "builtin/for-each-ref.c" we could preserve the comment added in
    +      6c62f015520, but now that we're directly using the documented
    +      repo_config_get_value_multi() value it's just narrating something that
    +      should be obvious from the API use, so let's drop it.
     
    -    A logical follow-up to this would be to change the various "*_get_*()"
    -    functions to ferry the git_configset_get_value() return value to their
    -    own callers, e.g.:
    +    - The loops after getting the "list" value in "builtin/gc.c" could
    +      also make use of "unsorted_string_list_has_string()" instead of using
    +      that loop, but let's leave that for now.
     
    -            diff --git a/config.c b/config.c
    -            index 094ad899e0b..7e8ee4cfec1 100644
    -            --- a/config.c
    -            +++ b/config.c
    -            @@ -2479,11 +2479,14 @@ static int git_configset_get_string_tmp(struct config_set *cs, const char *key,
    -             int git_configset_get_int(struct config_set *cs, const char *key, int *dest)
    -             {
    -                    const char *value;
    -            -       if (!git_configset_get_value(cs, key, &value)) {
    -            -               *dest = git_config_int(key, value);
    -            -               return 0;
    -            -       } else
    -            -               return 1;
    -            +       int ret;
    -            +
    -            +       if ((ret = git_configset_get_value(cs, key, &value)))
    -            +               goto done;
    -            +
    -            +       *dest = git_config_int(key, value);
    -            +done:
    -            +       return ret;
    -             }
    +    - We have code e.g. in "builtin/submodule--helper.c" that only wants
    +      to check if a config key exists, and would be better served with
    +      another API, but let's keep using "git_configset_get_value_multi()"
    +      for now.
     
    -             int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest)
    +    - In "versioncmp.c" we now use the return value of the functions,
    +      instead of checking if the lists are still non-NULL. This is strictly
    +      speaking unnecessary, but makes the API use consistent with the rest,
    +      but more importantly...
     
    -    Most of those callers don't care, and call those functions as
    -    "if (!func(...))", but if they do they'll be able to tell key
    -    non-existence from errors we encounter. Before this change those API
    -    users would have been unable to tell the two conditions apart, as
    -    git_configset_get_value() hid the difference.
    +    - ...because we always check our return values we can assert that with
    +      the RESULT_MUST_BE_USED macro added in 1e8697b5c4e (submodule--helper:
    +      check repo{_submodule,}_init() return values, 2022-09-01)
     
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
      ## builtin/for-each-repo.c ##
     @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
    - {
      	static const char *config_key = NULL;
      	int i, result = 0;
    --	const struct string_list *values;
    -+	const struct string_list *values = NULL;
    + 	const struct string_list *values;
    ++	int err;
      
      	const struct option options[] = {
      		OPT_STRING(0, "config", &config_key, N_("config"),
    @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, cons
      
     -	values = repo_config_get_value_multi(the_repository,
     -					     config_key);
    -+	repo_config_get_value_multi(the_repository, config_key, &values);
    +-
    +-	/*
    +-	 * Do nothing on an empty list, which is equivalent to the case
    +-	 * where the config variable does not exist at all.
    +-	 */
    +-	if (!values)
    ++	err = repo_config_get_value_multi(the_repository, config_key, &values);
    ++	if (err < 0)
    ++		return 0;
    ++	else if (err)
    + 		return 0;
      
    - 	/*
    - 	 * Do nothing on an empty list, which is equivalent to the case
    + 	for (i = 0; !result && i < values->nr; i++)
     
      ## builtin/gc.c ##
     @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const char *prefix)
    @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const
      
     -	list = git_config_get_value_multi(key);
     -	if (list) {
    -+	if (!git_config_get_knownkey_value_multi(key, &list)) {
    ++	if (!git_config_get_value_multi(key, &list)) {
      		for_each_string_list_item(item, list) {
      			if (!strcmp(maintpath, item->string)) {
      				found = 1;
    @@ builtin/gc.c: static int maintenance_unregister(int argc, const char **argv, con
      
     -	list = git_config_get_value_multi(key);
     -	if (list) {
    -+	if (!git_config_get_knownkey_value_multi(key, &list)) {
    ++	if (!git_config_get_value_multi(key, &list)) {
      		for_each_string_list_item(item, list) {
      			if (!strcmp(maintpath, item->string)) {
      				found = 1;
    @@ builtin/log.c: static void set_default_decoration_filter(struct decoration_filte
     +	const struct string_list *config_exclude;
      
     -	if (config_exclude) {
    -+	if (!git_config_get_knownkey_value_multi("log.excludeDecoration",
    -+					      &config_exclude)) {
    ++	if (!git_config_get_value_multi("log.excludeDecoration",
    ++					&config_exclude)) {
      		struct string_list_item *item;
      		for_each_string_list_item(item, config_exclude)
      			string_list_append(decoration_filter->exclude_ref_config_pattern,
    @@ builtin/submodule--helper.c: static int module_update(int argc, const char **arg
      		struct init_cb info = INIT_CB_INIT;
     +		const struct string_list *values;
      
    - 		if (module_list_compute(argc, argv, opt.prefix,
    + 		if (module_list_compute(argv, opt.prefix,
      					&pathspec2, &list) < 0) {
     @@ builtin/submodule--helper.c: static int module_update(int argc, const char **argv, const char *prefix)
      		 * If there are no path args and submodule.active is set then,
    @@ config.c: static int configset_add_value(struct config_set *cs, const char *key,
      	/*
      	 * Since the keys are being fed by git_config*() callback mechanism, they
      	 * are already normalized. So simply add them without any further munging.
    -@@ config.c: int git_configset_add_parameters(struct config_set *cs)
    +@@ config.c: int git_configset_add_file(struct config_set *cs, const char *filename)
      int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
      {
      	const struct string_list *values = NULL;
    @@ config.c: int git_configset_add_parameters(struct config_set *cs)
      }
      
     -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
    -+static int git_configset_get_value_multi_1(struct config_set *cs, const char *key,
    -+					   const struct string_list **dest,
    -+					   int knownkey)
    ++int git_configset_get_value_multi(struct config_set *cs, const char *key,
    ++				  const struct string_list **dest)
      {
     -	struct config_set_element *e = configset_find_element(cs, key);
     -	return e ? &e->value_list : NULL;
    @@ config.c: int git_configset_add_parameters(struct config_set *cs)
     +	int ret;
     +
     +	ret = configset_find_element(cs, key, &e);
    -+	if (ret < 0 && knownkey)
    -+		BUG("*_get_knownkey_*() only accepts known-good (hardcoded) keys, but '%s' is bad!", key);
    -+	else if (ret < 0)
    ++	if (ret < 0)
     +		return ret;
     +	else if (!e)
     +		return 1;
     +	*dest = &e->value_list;
     +
     +	return 0;
    -+}
    -+
    -+int git_configset_get_value_multi(struct config_set *cs, const char *key,
    -+				  const struct string_list **dest)
    -+{
    -+	return git_configset_get_value_multi_1(cs, key, dest, 0);
    -+}
    -+
    -+int git_configset_get_knownkey_value_multi(struct config_set *cs,
    -+					   const char *const key,
    -+					   const struct string_list **dest)
    -+{
    -+	return git_configset_get_value_multi_1(cs, key, dest, 1);
      }
      
      int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
    @@ config.c: int repo_config_get_value(struct repository *repo,
      
     -const struct string_list *repo_config_get_value_multi(struct repository *repo,
     -						      const char *key)
    -+int repo_config_get_value_multi(struct repository *repo,
    -+				const char *key,
    ++int repo_config_get_value_multi(struct repository *repo, const char *key,
     +				const struct string_list **dest)
      {
      	git_config_check_init(repo);
     -	return git_configset_get_value_multi(repo->config, key);
     +	return git_configset_get_value_multi(repo->config, key, dest);
    -+}
    -+
    -+int repo_config_get_knownkey_value_multi(struct repository *repo,
    -+					 const char *const key,
    -+					 const struct string_list **dest)
    -+{
    -+	git_config_check_init(repo);
    -+	return git_configset_get_knownkey_value_multi(repo->config, key, dest);
      }
      
      int repo_config_get_string(struct repository *repo,
    @@ config.c: int git_config_get_value(const char *key, const char **value)
      
     -const struct string_list *git_config_get_value_multi(const char *key)
     +int git_config_get_value_multi(const char *key, const struct string_list **dest)
    -+{
    -+	return repo_config_get_value_multi(the_repository, key, dest);
    -+}
    -+
    -+int git_config_get_knownkey_value_multi(const char *const key,
    -+					const struct string_list **dest)
      {
     -	return repo_config_get_value_multi(the_repository, key);
    -+	return repo_config_get_knownkey_value_multi(the_repository, key, dest);
    ++	return repo_config_get_value_multi(the_repository, key, dest);
      }
      
      int git_config_get_string(const char *key, char **dest)
    @@ config.c: void git_die_config(const char *key, const char *err, ...)
      		va_end(params);
      	}
     -	values = git_config_get_value_multi(key);
    -+
    -+	/*
    -+	 * We don't have a "const" key here, but we should definitely
    -+	 * have one that's passed git_config_parse_key() already, if
    -+	 * we're at the point of complaining about its value. So let's
    -+	 * use *_knownkey_value_multi() here to get that BUG(...).
    -+	 */
    -+	if (git_config_get_knownkey_value_multi(key, &values))
    -+		BUG("key '%s' does not exist, should not be given to git_die_config()",
    -+		    key);
    ++	if (git_config_get_value_multi(key, &values))
    ++		BUG("for key '%s' we must have a value to report on", key);
      	kv_info = values->items[values->nr - 1].util;
      	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
      }
     
      ## config.h ##
     @@ config.h: int git_configset_add_parameters(struct config_set *cs);
    - 
      /**
       * Finds and returns the value list, sorted in order of increasing priority
    -- * for the configuration variable `key` and config set `cs`. When the
    +  * for the configuration variable `key` and config set `cs`. When the
     - * configuration variable `key` is not found, returns NULL. The caller
     - * should not free or modify the returned pointer, as it is owned by the cache.
    -+ * for the configuration variable `key` and config set `cs`.
    -+ *
    -+ * When the configuration variable `key` is not found, returns 1
    -+ * without touching `value`.
    ++ * configuration variable `key` is not found, returns 1 without touching
    ++ * `value`.
     + *
     + * The key will be parsed for validity with git_config_parse_key(), on
    -+ * error a negative value will be returned. See
    -+ * git_configset_get_knownkey_value_multi() for a version of this which
    -+ * BUG()s out on negative return values.
    ++ * error a negative value will be returned.
     + *
     + * The caller should not free or modify the returned pointer, as it is
     + * owned by the cache.
    -+ */
    -+int git_configset_get_value_multi(struct config_set *cs, const char *key,
    -+				  const struct string_list **dest);
    -+
    -+/**
    -+ * Like git_configset_get_value_multi(), but BUG()s out if the return
    -+ * value is < 0. Use it for keys known to pass git_config_parse_key(),
    -+ * i.e. those hardcoded in the code, and never user-provided keys.
       */
     -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
    -+int git_configset_get_knownkey_value_multi(struct config_set *cs,
    -+					   const char *const key,
    -+					   const struct string_list **dest);
    ++RESULT_MUST_BE_USED
    ++int git_configset_get_value_multi(struct config_set *cs, const char *key,
    ++				  const struct string_list **dest);
      
      /**
       * Clears `config_set` structure, removes all saved variable-value pairs.
    @@ config.h: struct repository;
      			  const char *key, const char **value);
     -const struct string_list *repo_config_get_value_multi(struct repository *repo,
     -						      const char *key);
    -+int repo_config_get_value_multi(struct repository *repo,
    -+				const char *key,
    ++RESULT_MUST_BE_USED
    ++int repo_config_get_value_multi(struct repository *repo, const char *key,
     +				const struct string_list **dest);
    -+int repo_config_get_knownkey_value_multi(struct repository *repo,
    -+					 const char *const key,
    -+					 const struct string_list **dest);
      int repo_config_get_string(struct repository *repo,
      			   const char *key, char **dest);
      int repo_config_get_string_tmp(struct repository *repo,
    @@ config.h: int git_config_get_value(const char *key, const char **value);
     + *
     + * The caller should not free or modify the returned pointer, as it is
     + * owned by the cache.
    -+ */
    -+int git_config_get_value_multi(const char *key,
    -+			       const struct string_list **dest);
    -+
    -+/**
    -+ * A wrapper for git_config_get_value_multi() which does for it what
    -+ * git_configset_get_knownkey_value_multi() does for
    -+ * git_configset_get_value_multi().
       */
     -const struct string_list *git_config_get_value_multi(const char *key);
    -+int git_config_get_knownkey_value_multi(const char *const key,
    -+					const struct string_list **dest);
    ++RESULT_MUST_BE_USED
    ++int git_config_get_value_multi(const char *key,
    ++			       const struct string_list **dest);
      
      /**
       * Resets and invalidates the config cache.
    @@ pack-bitmap.c: int bitmap_is_midx(struct bitmap_index *bitmap_git)
     -	return repo_config_get_value_multi(r, "pack.preferbitmaptips");
     +	const struct string_list *dest;
     +
    -+	if (!repo_config_get_knownkey_value_multi(r, "pack.preferbitmaptips",
    -+					       &dest))
    ++	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
     +		return dest;
     +	return NULL;
      }
    @@ submodule.c: int is_tree_submodule_active(struct repository *repo,
      	/* submodule.active is set */
     -	sl = repo_config_get_value_multi(repo, "submodule.active");
     -	if (sl) {
    -+	if (!repo_config_get_knownkey_value_multi(repo, "submodule.active", &sl)) {
    ++	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
      		struct pathspec ps;
      		struct strvec args = STRVEC_INIT;
      		const struct string_list_item *item;
    @@ t/helper/test-config.c: int cmd__config(int argc, const char **argv)
      	} else if (argc == 3 && !strcmp(argv[1], "get_value_multi")) {
     -		strptr = git_config_get_value_multi(argv[2]);
     -		if (strptr) {
    -+		if (!git_config_get_knownkey_value_multi(argv[2], &strptr)) {
    ++		if (!git_config_get_value_multi(argv[2], &strptr)) {
      			for (i = 0; i < strptr->nr; i++) {
      				v = strptr->items[i].string;
      				if (!v)
    @@ t/helper/test-config.c: int cmd__config(int argc, const char **argv)
      		}
     -		strptr = git_configset_get_value_multi(&cs, argv[2]);
     -		if (strptr) {
    -+		if (!git_configset_get_knownkey_value_multi(&cs, argv[2], &strptr)) {
    ++		if (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {
      			for (i = 0; i < strptr->nr; i++) {
      				v = strptr->items[i].string;
      				if (!v)
     
      ## versioncmp.c ##
     @@ versioncmp.c: int versioncmp(const char *s1, const char *s2)
    - 	}
    - 
      	if (!initialized) {
    --		const struct string_list *deprecated_prereleases;
    -+		const struct string_list *deprecated_prereleases = NULL;
    -+
    + 		const char *const newk = "versionsort.suffix";
    + 		const char *const oldk = "versionsort.prereleasesuffix";
    ++		const struct string_list *newl;
    + 		const struct string_list *oldl;
    ++		int new = git_config_get_value_multi(newk, &newl);
    ++		int old = git_config_get_value_multi(oldk, &oldl);
    + 
    +-		prereleases = git_config_get_value_multi(newk);
    +-		oldl = git_config_get_value_multi(oldk);
    +-		if (prereleases && oldl)
    ++		if (!new && !old)
    + 			warning("ignoring %s because %s is set", oldk, newk);
    +-		else if (!prereleases)
    ++		if (!new)
    ++			prereleases = newl;
    ++		else if (!old)
    + 			prereleases = oldl;
    + 
      		initialized = 1;
    --		prereleases = git_config_get_value_multi("versionsort.suffix");
    --		deprecated_prereleases = git_config_get_value_multi("versionsort.prereleasesuffix");
    -+		git_config_get_knownkey_value_multi("versionsort.suffix",
    -+						 &prereleases);
    -+		git_config_get_value_multi("versionsort.prereleasesuffix",
    -+					   &deprecated_prereleases);
    -+
    - 		if (prereleases) {
    - 			if (deprecated_prereleases)
    - 				warning("ignoring versionsort.prereleasesuffix because versionsort.suffix is set");
 2:  e17de2a2664 <  -:  ----------- for-each-repo: error on bad --config
 3:  3519d3de010 <  -:  ----------- config API: mark *_multi() with RESULT_MUST_BE_USED
 4:  40b3cc9b8d4 <  -:  ----------- string-list API: mark "struct_string_list" to "for_each_string_list" const
 5:  b32b2e99aba <  -:  ----------- string-list API: make has_string() and list_lookup() "const"
 6:  9c36f17481b <  -:  ----------- builtin/gc.c: use "unsorted_string_list_has_string()" where appropriate
 7:  c01f7d85c94 <  -:  ----------- config API: add and use "lookup_value" functions
 9:  bda9d504b89 <  -:  ----------- config API: add "string" version of *_value_multi(), fix segfaults
 -:  ----------- >  6:  a0c29d46556 for-each-repo: error on bad --config
 -:  ----------- >  7:  c12805f3d55 config API users: test for *_get_value_multi() segfaults
 -:  ----------- >  8:  6b76f9eac90 config API: add "string" version of *_value_multi(), fix segfaults
10:  b59cbed8f61 !  9:  e2f8f7c52e3 for-each-repo: with bad config, don't conflate <path> and <cmd>
    @@ Commit message
         running commands.
     
         As noted in the preceding commit the fix is to move to a safer
    -    "*_multi_string()" version of the *__multi() API. This change is
    +    "*_string_multi()" version of the *_multi() API. This change is
         separated from the rest because those all segfaulted. In this change
         we ended up with different behavior.
     
    @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, cons
      		die(_("missing --config=<config>"));
      
     -	err = repo_config_get_value_multi(the_repository, config_key, &values);
    -+	err = repo_config_get_value_multi_string(the_repository, config_key, &values);
    ++	err = repo_config_get_string_multi(the_repository, config_key, &values);
      	if (err < 0)
      		usage_msg_optf(_("got bad config --config=%s"),
      			       for_each_repo_usage, options, config_key);
-- 
2.38.0.1280.g8136eb6fab2


^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v2 1/9] for-each-repo tests: test bad --config keys
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
@ 2022-11-01 23:05   ` Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 2/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
                     ` (9 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-01 23:05 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

As noted in 6c62f015520 (for-each-repo: do nothing on empty config,
2021-01-08) this command wants to ignore a non-existing config key,
but it's been conflating that with bad config keys.

A subsequent commit will address that, but for now let's fix the gaps
in test coverage, and show what we're currently doing in these cases.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t0068-for-each-repo.sh | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 4675e852517..6bba0c5f4c2 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -33,4 +33,10 @@ test_expect_success 'do nothing on empty config' '
 	git for-each-repo --config=bogus.config -- help --no-such-option
 '
 
+test_expect_success 'bad config keys' '
+	git for-each-repo --config=a &&
+	git for-each-repo --config=a.b. &&
+	git for-each-repo --config="'\''.b"
+'
+
 test_done
-- 
2.38.0.1280.g8136eb6fab2


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 2/9] config tests: cover blind spots in git_die_config() tests
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 1/9] for-each-repo tests: test bad --config keys Ævar Arnfjörð Bjarmason
@ 2022-11-01 23:05   ` Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 3/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
                     ` (8 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-01 23:05 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

There were no tests checking for the output of the git_die_config()
function in the config API, added in 5a80e97c827 (config: add
`git_die_config()` to the config-set API, 2014-08-07). We only tested
"test_must_fail", but didn't assert the output.

Let's check for that by extending the existing tests, and adding a new
one for "fetch.negotiationAlgorithm" so that we have a test for a user
of git_config_get_string*() calling git_die_config().

The other ones are testing:

- For *-resolve.sh: A custom call to git_die_config(), or via
  git_config_get_notes_strategy()
- For *-prune.sh: A call via git_config_get_expiry().

We also cover both the "from command-line config" and "in file..at
line" cases here.

The clobbering of existing ".git/config" files here is so that we're
not implicitly testing the line count of the default config.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-
 t/t5304-prune.sh                     | 12 ++++++++++--
 t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh
index 141d3e4ca4d..9bd5dbf341f 100755
--- a/t/t3309-notes-merge-auto-resolve.sh
+++ b/t/t3309-notes-merge-auto-resolve.sh
@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '
 
 test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '
 	git config core.notesRef refs/notes/y &&
-	test_must_fail git -c notes.mergeStrategy="foo" notes merge z &&
+	cat >expect <<-\EOF &&
+	error: unknown notes merge strategy foo
+	fatal: unable to parse '\''notes.mergeStrategy'\'' from command-line config
+	EOF
+	test_must_fail git -c notes.mergeStrategy="foo" notes merge z 2>actual &&
+	test_cmp expect actual &&
 	# Verify no changes (y)
 	verify_notes y y
 '
diff --git a/t/t5304-prune.sh b/t/t5304-prune.sh
index 8ae314af585..c8fa962b397 100755
--- a/t/t5304-prune.sh
+++ b/t/t5304-prune.sh
@@ -64,8 +64,16 @@ test_expect_success 'gc: implicit prune --expire' '
 '
 
 test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '
-	git config gc.pruneExpire invalid &&
-	test_must_fail git gc
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	>repo/.git/config &&
+	git -C repo config gc.pruneExpire invalid &&
+	cat >expect <<-\EOF &&
+	error: Invalid gc.pruneexpire: '\''invalid'\''
+	fatal: bad config variable '\''gc.pruneexpire'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_must_fail git -C repo gc 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'gc: start with ok gc.pruneExpire' '
diff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh
index 165427d57e5..b55a9f65e6b 100755
--- a/t/t5552-skipping-fetch-negotiator.sh
+++ b/t/t5552-skipping-fetch-negotiator.sh
@@ -3,6 +3,22 @@
 test_description='test skipping fetch negotiator'
 . ./test-lib.sh
 
+test_expect_success 'fetch.negotiationalgorithm config' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	cat >repo/.git/config <<-\EOF &&
+	[fetch]
+	negotiationAlgorithm
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''fetch.negotiationalgorithm'\''
+	fatal: bad config variable '\''fetch.negotiationalgorithm'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_expect_code 128 git -C repo fetch >out 2>actual &&
+	test_must_be_empty out &&
+	test_cmp expect actual
+'
+
 have_sent () {
 	while test "$#" -ne 0
 	do
-- 
2.38.0.1280.g8136eb6fab2


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 3/9] config tests: add "NULL" tests for *_get_value_multi()
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 1/9] for-each-repo tests: test bad --config keys Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 2/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
@ 2022-11-01 23:05   ` Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
                     ` (7 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-01 23:05 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

A less well known edge case in the config format is that keys can be
value-less, a shorthand syntax for "true" boolean keys. I.e. these two
are equivalent as far as "--type=bool" is concerned:

	[a]key
	[a]key = true

But as far as our parser is concerned the values for these two are
NULL, and "true". I.e. for a sequence like:

	[a]key=x
	[a]key
	[a]key=y

We get a "struct string_list" with "string" members with ".string"
values of:

	{ "x", NULL, "y" }

This behavior goes back to the initial implementation of
git_config_bool() in 17712991a59 (Add ".git/config" file parser,
2005-10-10).

When the "t/t1308-config-set.sh" tests were added in [1] only one of
the three "(NULL)" lines in "t/helper/test-config.c" had any test
coverage. This change adds tests that stress the remaining two.

1. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t1308-config-set.sh | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index b38e158d3b2..561e82f1808 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -146,6 +146,36 @@ test_expect_success 'find multiple values' '
 	check_config get_value_multi case.baz sam bat hask
 '
 
+test_expect_success 'emit multi values from configset with NULL entry' '
+	test_when_finished "rm -f my.config" &&
+	cat >my.config <<-\EOF &&
+	[a]key=x
+	[a]key
+	[a]key=y
+	EOF
+	cat >expect <<-\EOF &&
+	x
+	(NULL)
+	y
+	EOF
+	test-tool config configset_get_value_multi a.key my.config >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'multi values from configset with a last NULL entry' '
+	test_when_finished "rm -f my.config" &&
+	cat >my.config <<-\EOF &&
+	[a]key=x
+	[a]key=y
+	[a]key
+	EOF
+	cat >expect <<-\EOF &&
+	(NULL)
+	EOF
+	test-tool config configset_get_value a.key my.config >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'find value from a configset' '
 	cat >config2 <<-\EOF &&
 	[case]
-- 
2.38.0.1280.g8136eb6fab2


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 4/9] versioncmp.c: refactor config reading next commit
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
                     ` (2 preceding siblings ...)
  2022-11-01 23:05   ` [PATCH v2 3/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2022-11-01 23:05   ` Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
                     ` (6 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-01 23:05 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Refactor the reading of the versionSort.suffix and
versionSort.prereleaseSuffix configuration variables to stay within
the bounds of our CodingGuidelines when it comes to line length, and
ta avoid repeating ourselves.

Let's also split out the names of the config variables into variables
of our own, so we don't have to repeat ourselves, and refactor the
nested if/else to avoid indenting it, and the existing bracing style
issue.

This all helps with the subsequent commit, where we'll need to start
checking different git_config_get_value_multi() return value. See
c026557a373 (versioncmp: generalize version sort suffix reordering,
2016-12-08) for the original implementation of most of this.

Moving the "initialized = 1" assignment allows us to move some of this
to the variable declarations in the subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 versioncmp.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/versioncmp.c b/versioncmp.c
index 069ee94a4d7..323f5d35ea8 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)
 	}
 
 	if (!initialized) {
-		const struct string_list *deprecated_prereleases;
+		const char *const newk = "versionsort.suffix";
+		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *oldl;
+
+		prereleases = git_config_get_value_multi(newk);
+		oldl = git_config_get_value_multi(oldk);
+		if (prereleases && oldl)
+			warning("ignoring %s because %s is set", oldk, newk);
+		else if (!prereleases)
+			prereleases = oldl;
+
 		initialized = 1;
-		prereleases = git_config_get_value_multi("versionsort.suffix");
-		deprecated_prereleases = git_config_get_value_multi("versionsort.prereleasesuffix");
-		if (prereleases) {
-			if (deprecated_prereleases)
-				warning("ignoring versionsort.prereleasesuffix because versionsort.suffix is set");
-		} else
-			prereleases = deprecated_prereleases;
 	}
 	if (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,
 					    &diff))
-- 
2.38.0.1280.g8136eb6fab2


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 5/9] config API: have *_multi() return an "int" and take a "dest"
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
                     ` (3 preceding siblings ...)
  2022-11-01 23:05   ` [PATCH v2 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
@ 2022-11-01 23:05   ` Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
                     ` (5 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-01 23:05 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Have the "git_configset_get_value_multi()" function and its siblings
return an "int" and populate a "**dest" parameter like every other
git_configset_get_*()" in the API.

As we'll see in in subsequent commits this fixes a blind spot in the
API where it wasn't possible to tell whether a list was empty from
whether a config key existed. We'll take advantage of that in
subsequent commits, but for now we're faithfully converting existing
API callers.

See [1] for the initial addition of "git_configset_get_value_multi()"

1. 3c8687a73ee (add `config_set` API for caching config-like files,
   2014-07-28).

A logical follow-up to this would be to change the various "*_get_*()"
functions to ferry the git_configset_get_value() return value to their
own callers, e.g. git_configset_get_int() returns "1" rather than
ferrying up the "-1" that "git_configset_get_value()" might return,
but that's not being done in this series

Most of this is straightforward, commentary on cases that stand out:

- As we've tested for in a preceding commit we can rely on getting the
  config list in git_die_config(), and as we need to handle the new
  return value let's BUG() out if we can't acquire it.

- In "builtin/for-each-ref.c" we could preserve the comment added in
  6c62f015520, but now that we're directly using the documented
  repo_config_get_value_multi() value it's just narrating something that
  should be obvious from the API use, so let's drop it.

- The loops after getting the "list" value in "builtin/gc.c" could
  also make use of "unsorted_string_list_has_string()" instead of using
  that loop, but let's leave that for now.

- We have code e.g. in "builtin/submodule--helper.c" that only wants
  to check if a config key exists, and would be better served with
  another API, but let's keep using "git_configset_get_value_multi()"
  for now.

- In "versioncmp.c" we now use the return value of the functions,
  instead of checking if the lists are still non-NULL. This is strictly
  speaking unnecessary, but makes the API use consistent with the rest,
  but more importantly...

- ...because we always check our return values we can assert that with
  the RESULT_MUST_BE_USED macro added in 1e8697b5c4e (submodule--helper:
  check repo{_submodule,}_init() return values, 2022-09-01)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c     | 13 ++++-----
 builtin/gc.c                |  6 ++--
 builtin/log.c               |  6 ++--
 builtin/submodule--helper.c |  6 ++--
 config.c                    | 55 ++++++++++++++++++++++++++-----------
 config.h                    | 29 +++++++++++++------
 pack-bitmap.c               |  6 +++-
 submodule.c                 |  3 +-
 t/helper/test-config.c      |  6 ++--
 versioncmp.c                | 11 +++++---
 10 files changed, 89 insertions(+), 52 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index d45d873f579..7d7685c8a1a 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -29,6 +29,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	static const char *config_key = NULL;
 	int i, result = 0;
 	const struct string_list *values;
+	int err;
 
 	const struct option options[] = {
 		OPT_STRING(0, "config", &config_key, N_("config"),
@@ -42,14 +43,10 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	values = repo_config_get_value_multi(the_repository,
-					     config_key);
-
-	/*
-	 * Do nothing on an empty list, which is equivalent to the case
-	 * where the config variable does not exist at all.
-	 */
-	if (!values)
+	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	if (err < 0)
+		return 0;
+	else if (err)
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/builtin/gc.c b/builtin/gc.c
index 24ea85c7afd..76cee01e442 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1485,8 +1485,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	else
 		git_config_set("maintenance.strategy", "incremental");
 
-	list = git_config_get_value_multi(key);
-	if (list) {
+	if (!git_config_get_value_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1542,8 +1541,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 		usage_with_options(builtin_maintenance_unregister_usage,
 				   options);
 
-	list = git_config_get_value_multi(key);
-	if (list) {
+	if (!git_config_get_value_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index 5eafcf26b49..cc9d92f95da 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	int i;
 	char *value = NULL;
 	struct string_list *include = decoration_filter->include_ref_pattern;
-	const struct string_list *config_exclude =
-			git_config_get_value_multi("log.excludeDecoration");
+	const struct string_list *config_exclude;
 
-	if (config_exclude) {
+	if (!git_config_get_value_multi("log.excludeDecoration",
+					&config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index a7683d35299..53afc2de4af 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -541,6 +541,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 		NULL
 	};
 	int ret = 1;
+	const struct string_list *values;
 
 	argc = parse_options(argc, argv, prefix, module_init_options,
 			     git_submodule_helper_usage, 0);
@@ -552,7 +553,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 	 * If there are no path args and submodule.active is set then,
 	 * by default, only initialize 'active' modules.
 	 */
-	if (!argc && git_config_get_value_multi("submodule.active"))
+	if (!argc && !git_config_get_value_multi("submodule.active", &values))
 		module_list_active(&list);
 
 	info.prefix = prefix;
@@ -2716,6 +2717,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 	if (opt.init) {
 		struct module_list list = MODULE_LIST_INIT;
 		struct init_cb info = INIT_CB_INIT;
+		const struct string_list *values;
 
 		if (module_list_compute(argv, opt.prefix,
 					&pathspec2, &list) < 0) {
@@ -2728,7 +2730,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 		 * If there are no path args and submodule.active is set then,
 		 * by default, only initialize 'active' modules.
 		 */
-		if (!argc && git_config_get_value_multi("submodule.active"))
+		if (!argc && !git_config_get_value_multi("submodule.active", &values))
 			module_list_active(&list);
 
 		info.prefix = opt.prefix;
diff --git a/config.c b/config.c
index c058b2c70c3..0b07045ed8c 100644
--- a/config.c
+++ b/config.c
@@ -2275,23 +2275,28 @@ void read_very_early_config(config_fn_t cb, void *data)
 	config_with_options(cb, data, NULL, &opts);
 }
 
-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
+static int configset_find_element(struct config_set *cs, const char *key,
+				  struct config_set_element **dest)
 {
 	struct config_set_element k;
 	struct config_set_element *found_entry;
 	char *normalized_key;
+	int ret;
+
 	/*
 	 * `key` may come from the user, so normalize it before using it
 	 * for querying entries from the hashmap.
 	 */
-	if (git_config_parse_key(key, &normalized_key, NULL))
-		return NULL;
+	ret = git_config_parse_key(key, &normalized_key, NULL);
+	if (ret < 0)
+		return ret;
 
 	hashmap_entry_init(&k.ent, strhash(normalized_key));
 	k.key = normalized_key;
 	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
 	free(normalized_key);
-	return found_entry;
+	*dest = found_entry;
+	return 0;
 }
 
 static int configset_add_value(struct config_set *cs, const char *key, const char *value)
@@ -2300,8 +2305,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha
 	struct string_list_item *si;
 	struct configset_list_item *l_item;
 	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
+	int ret;
 
-	e = configset_find_element(cs, key);
+	ret = configset_find_element(cs, key, &e);
+	if (ret < 0)
+		return ret;
 	/*
 	 * Since the keys are being fed by git_config*() callback mechanism, they
 	 * are already normalized. So simply add them without any further munging.
@@ -2395,24 +2403,38 @@ int git_configset_add_file(struct config_set *cs, const char *filename)
 int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
 {
 	const struct string_list *values = NULL;
+	int ret;
+
 	/*
 	 * Follows "last one wins" semantic, i.e., if there are multiple matches for the
 	 * queried key in the files of the configset, the value returned will be the last
 	 * value in the value list for that key.
 	 */
-	values = git_configset_get_value_multi(cs, key);
+	ret = git_configset_get_value_multi(cs, key, &values);
 
-	if (!values)
+	if (ret < 0)
+		return ret;
+	else if (!values)
 		return 1;
 	assert(values->nr > 0);
 	*value = values->items[values->nr - 1].string;
 	return 0;
 }
 
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest)
 {
-	struct config_set_element *e = configset_find_element(cs, key);
-	return e ? &e->value_list : NULL;
+	struct config_set_element *e;
+	int ret;
+
+	ret = configset_find_element(cs, key, &e);
+	if (ret < 0)
+		return ret;
+	else if (!e)
+		return 1;
+	*dest = &e->value_list;
+
+	return 0;
 }
 
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
@@ -2558,11 +2580,11 @@ int repo_config_get_value(struct repository *repo,
 	return git_configset_get_value(repo->config, key, value);
 }
 
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key)
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest)
 {
 	git_config_check_init(repo);
-	return git_configset_get_value_multi(repo->config, key);
+	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
 int repo_config_get_string(struct repository *repo,
@@ -2670,9 +2692,9 @@ int git_config_get_value(const char *key, const char **value)
 	return repo_config_get_value(the_repository, key, value);
 }
 
-const struct string_list *git_config_get_value_multi(const char *key)
+int git_config_get_value_multi(const char *key, const struct string_list **dest)
 {
-	return repo_config_get_value_multi(the_repository, key);
+	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
 int git_config_get_string(const char *key, char **dest)
@@ -2819,7 +2841,8 @@ void git_die_config(const char *key, const char *err, ...)
 		error_fn(err, params);
 		va_end(params);
 	}
-	values = git_config_get_value_multi(key);
+	if (git_config_get_value_multi(key, &values))
+		BUG("for key '%s' we must have a value to report on", key);
 	kv_info = values->items[values->nr - 1].util;
 	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
 }
diff --git a/config.h b/config.h
index ef9eade6414..7f6ce6f2fb5 100644
--- a/config.h
+++ b/config.h
@@ -459,10 +459,18 @@ int git_configset_add_parameters(struct config_set *cs);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key` and config set `cs`. When the
- * configuration variable `key` is not found, returns NULL. The caller
- * should not free or modify the returned pointer, as it is owned by the cache.
+ * configuration variable `key` is not found, returns 1 without touching
+ * `value`.
+ *
+ * The key will be parsed for validity with git_config_parse_key(), on
+ * error a negative value will be returned.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
+RESULT_MUST_BE_USED
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest);
 
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
@@ -496,8 +504,9 @@ struct repository;
 void repo_config(struct repository *repo, config_fn_t fn, void *data);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key);
+RESULT_MUST_BE_USED
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -544,10 +553,14 @@ int git_config_get_value(const char *key, const char **value);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key`. When the configuration variable
- * `key` is not found, returns NULL. The caller should not free or modify
- * the returned pointer, as it is owned by the cache.
+ * `key` is not found, returns 1 without touching `value`.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_config_get_value_multi(const char *key);
+RESULT_MUST_BE_USED
+int git_config_get_value_multi(const char *key,
+			       const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index 440407f1be7..81f0c0e016b 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2301,7 +2301,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)
 
 const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
-	return repo_config_get_value_multi(r, "pack.preferbitmaptips");
+	const struct string_list *dest;
+
+	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+		return dest;
+	return NULL;
 }
 
 int bitmap_is_preferred_refname(struct repository *r, const char *refname)
diff --git a/submodule.c b/submodule.c
index b958162d286..05ebe5cab4c 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	sl = repo_config_get_value_multi(repo, "submodule.active");
-	if (sl) {
+	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/helper/test-config.c b/t/helper/test-config.c
index 4ba9eb65606..8f70beb6c9d 100644
--- a/t/helper/test-config.c
+++ b/t/helper/test-config.c
@@ -95,8 +95,7 @@ int cmd__config(int argc, const char **argv)
 			goto exit1;
 		}
 	} else if (argc == 3 && !strcmp(argv[1], "get_value_multi")) {
-		strptr = git_config_get_value_multi(argv[2]);
-		if (strptr) {
+		if (!git_config_get_value_multi(argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
@@ -159,8 +158,7 @@ int cmd__config(int argc, const char **argv)
 				goto exit2;
 			}
 		}
-		strptr = git_configset_get_value_multi(&cs, argv[2]);
-		if (strptr) {
+		if (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
diff --git a/versioncmp.c b/versioncmp.c
index 323f5d35ea8..60c3a517122 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)
 	if (!initialized) {
 		const char *const newk = "versionsort.suffix";
 		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *newl;
 		const struct string_list *oldl;
+		int new = git_config_get_value_multi(newk, &newl);
+		int old = git_config_get_value_multi(oldk, &oldl);
 
-		prereleases = git_config_get_value_multi(newk);
-		oldl = git_config_get_value_multi(oldk);
-		if (prereleases && oldl)
+		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-		else if (!prereleases)
+		if (!new)
+			prereleases = newl;
+		else if (!old)
 			prereleases = oldl;
 
 		initialized = 1;
-- 
2.38.0.1280.g8136eb6fab2


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 6/9] for-each-repo: error on bad --config
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
                     ` (4 preceding siblings ...)
  2022-11-01 23:05   ` [PATCH v2 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
@ 2022-11-01 23:05   ` Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 7/9] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
                     ` (4 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-01 23:05 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

As noted in 6c62f015520 (for-each-repo: do nothing on empty config,
2021-01-08) this command wants to ignore a non-existing config key,
but let's not conflate that with bad config.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c     | 3 ++-
 builtin/submodule--helper.c | 8 ++++----
 t/t0068-for-each-repo.sh    | 8 ++++----
 3 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 7d7685c8a1a..96caf90139b 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -45,7 +45,8 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 
 	err = repo_config_get_value_multi(the_repository, config_key, &values);
 	if (err < 0)
-		return 0;
+		usage_msg_optf(_("got bad config --config=%s"),
+			       for_each_repo_usage, options, config_key);
 	else if (err)
 		return 0;
 
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 53afc2de4af..ad7ecaafc83 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -541,7 +541,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 		NULL
 	};
 	int ret = 1;
-	const struct string_list *values;
+	const struct string_list *unused;
 
 	argc = parse_options(argc, argv, prefix, module_init_options,
 			     git_submodule_helper_usage, 0);
@@ -553,7 +553,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 	 * If there are no path args and submodule.active is set then,
 	 * by default, only initialize 'active' modules.
 	 */
-	if (!argc && !git_config_get_value_multi("submodule.active", &values))
+	if (!argc && !git_config_get_value_multi("submodule.active", &unused))
 		module_list_active(&list);
 
 	info.prefix = prefix;
@@ -2717,7 +2717,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 	if (opt.init) {
 		struct module_list list = MODULE_LIST_INIT;
 		struct init_cb info = INIT_CB_INIT;
-		const struct string_list *values;
+		const struct string_list *unused;
 
 		if (module_list_compute(argv, opt.prefix,
 					&pathspec2, &list) < 0) {
@@ -2730,7 +2730,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 		 * If there are no path args and submodule.active is set then,
 		 * by default, only initialize 'active' modules.
 		 */
-		if (!argc && !git_config_get_value_multi("submodule.active", &values))
+		if (!argc && !git_config_get_value_multi("submodule.active", &unused))
 			module_list_active(&list);
 
 		info.prefix = opt.prefix;
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 6bba0c5f4c2..115221c9ca5 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -33,10 +33,10 @@ test_expect_success 'do nothing on empty config' '
 	git for-each-repo --config=bogus.config -- help --no-such-option
 '
 
-test_expect_success 'bad config keys' '
-	git for-each-repo --config=a &&
-	git for-each-repo --config=a.b. &&
-	git for-each-repo --config="'\''.b"
+test_expect_success 'error on bad config keys' '
+	test_expect_code 129 git for-each-repo --config=a &&
+	test_expect_code 129 git for-each-repo --config=a.b. &&
+	test_expect_code 129 git for-each-repo --config="'\''.b"
 '
 
 test_done
-- 
2.38.0.1280.g8136eb6fab2


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 7/9] config API users: test for *_get_value_multi() segfaults
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
                     ` (5 preceding siblings ...)
  2022-11-01 23:05   ` [PATCH v2 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
@ 2022-11-01 23:05   ` Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 8/9] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
                     ` (3 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-01 23:05 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

As we'll discus in the subsequent commit these tests all
show *_get_value_multi() API users unable to handle there being a
value-less key in the config, which is represented with a "NULL" for
that entry in the "string" member of the returned "struct
string_list", causing a segfault.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t4202-log.sh                 | 11 +++++++++++
 t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++
 t/t7004-tag.sh                 | 12 ++++++++++++
 t/t7413-submodule-is-active.sh | 12 ++++++++++++
 t/t7900-maintenance.sh         | 26 ++++++++++++++++++++++++++
 5 files changed, 77 insertions(+)

diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 2ce2b41174d..e4f02d8208b 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
+test_expect_failure 'parse log.excludeDecoration with no value' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[log]
+		excludeDecoration
+	EOF
+	git log --decorate=short
+'
+
 test_expect_success 'decorate-refs with glob' '
 	cat >expect.decorate <<-\EOF &&
 	Merge-tag-reach
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 6d693eef82f..2e65c8139c4 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,6 +404,22 @@ test_bitmap_cases () {
 		)
 	'
 
+	test_expect_failure 'pack.preferBitmapTips' '
+		git init repo &&
+		test_when_finished "rm -rf repo" &&
+		(
+			cd repo &&
+			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
+			test_commit_bulk --message="%s" 103 &&
+
+			cat >>.git/config <<-\EOF &&
+			[pack]
+				preferBitmapTips
+			EOF
+			git repack -adb
+		)
+	'
+
 	test_expect_success 'complains about multiple pack bitmaps' '
 		rm -fr repo &&
 		git init repo &&
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index 9aa1660651b..f343551a7d4 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[versionsort]
+		prereleaseSuffix
+		suffix
+	EOF
+	git tag -l --sort=version:refname
+'
+
 test_expect_success 'version sort with prerelease reordering' '
 	test_config versionsort.prereleaseSuffix -rc &&
 	git tag foo1.6-rc1 &&
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index 7cdc2637649..bfe27e50732 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
+test_expect_failure 'is-active handles submodule.active config missing a value' '
+	cp super/.git/config super/.git/config.orig &&
+	test_when_finished mv super/.git/config.orig super/.git/config &&
+
+	cat >>super/.git/config <<-\EOF &&
+	[submodule]
+		active
+	EOF
+
+	test-tool -C super submodule is-active sub1
+'
+
 test_expect_success 'is-active works with basic submodule.active config' '
 	test_when_finished "git -C super config submodule.sub1.URL ../sub" &&
 	test_when_finished "git -C super config --unset-all submodule.active" &&
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 96bdd420456..958d906f245 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -505,6 +505,32 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --force
 '
 
+test_expect_failure 'register with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	git maintenance register
+'
+
+test_expect_failure 'unregister with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	git maintenance unregister &&
+	git maintenance unregister --force
+'
+
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
 	META="a+b*c" &&
 	git init "$META" &&
-- 
2.38.0.1280.g8136eb6fab2


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 8/9] config API: add "string" version of *_value_multi(), fix segfaults
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
                     ` (6 preceding siblings ...)
  2022-11-01 23:05   ` [PATCH v2 7/9] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
@ 2022-11-01 23:05   ` Ævar Arnfjörð Bjarmason
  2022-11-01 23:05   ` [PATCH v2 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
                     ` (2 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-01 23:05 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Fix numerous and mostly long-standing segfaults in consumers of
the *_config_*value_multi() API. As discussed in the preceding commit
an empty key in the config syntax yields a "NULL" string, which these
users would give to strcmp() (or similar), resulting in segfaults.

As this change shows, most users users of the *_config_*value_multi()
API didn't really want such an an unsafe and low-level API, let's give
them something with the safety of git_config_get_string() instead.

This fix is similar to what the *_string() functions and others
acquired in[1] and [2]. Namely introducing and using a safer
"*_get_string_multi()" variant of the low-level "_*value_multi()"
function.

This fixes segfaults in code introduced in:

  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)
  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)
  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)
  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)
  - 92156291ca8 (log: add default decoration filter, 2022-08-05)
  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)

There are now three remaining files using the low-level API:

- Two cases in "builtin/submodule--helper.c", where it's used safely
  to see if any config exists.
- One in "builtin/for-each-repo.c", which we'll convert in a
  subsequent commit.
- The "t/helper/test-config.c" code added in [3].

As seen in the preceding commit we need to give the
"t/helper/test-config.c" caller these "NULL" entries.

We could also alter the underlying git_configset_get_value_multi()
function to be "string safe", but doing so would leave no room for
other variants of "*_get_value_multi()" that coerce to other types.

Such coercion can't be built on the string version, since as we've
established "NULL" is a true value in the boolean context, but if we
coerced it to "" for use in a list of strings it'll be subsequently
coerced to "false" as a boolean.

The callback pattern being used here will make it easy to introduce
e.g. a "multi" variant which coerces its values to "bool", "int",
"path" etc.

1. 40ea4ed9032 (Add config_error_nonbool() helper function,
   2008-02-11)
2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,
   2008-02-11).
3. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                   |  4 ++--
 builtin/log.c                  |  3 +--
 config.c                       | 35 +++++++++++++++++++++++++++++++++-
 config.h                       | 19 ++++++++++++++++++
 pack-bitmap.c                  |  2 +-
 submodule.c                    |  2 +-
 t/t4202-log.sh                 |  8 ++++++--
 t/t5310-pack-bitmaps.sh        |  9 +++++++--
 t/t7004-tag.sh                 |  9 +++++++--
 t/t7413-submodule-is-active.sh |  8 ++++++--
 t/t7900-maintenance.sh         | 22 ++++++++++++++++-----
 versioncmp.c                   |  4 ++--
 12 files changed, 103 insertions(+), 22 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 76cee01e442..f887dc7a3f3 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1485,7 +1485,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	else
 		git_config_set("maintenance.strategy", "incremental");
 
-	if (!git_config_get_value_multi(key, &list)) {
+	if (!git_config_get_string_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1541,7 +1541,7 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 		usage_with_options(builtin_maintenance_unregister_usage,
 				   options);
 
-	if (!git_config_get_value_multi(key, &list)) {
+	if (!git_config_get_string_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index cc9d92f95da..9b19ae0a736 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -184,8 +184,7 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	struct string_list *include = decoration_filter->include_ref_pattern;
 	const struct string_list *config_exclude;
 
-	if (!git_config_get_value_multi("log.excludeDecoration",
-					&config_exclude)) {
+	if (!git_config_get_string_multi("log.excludeDecoration", &config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/config.c b/config.c
index 0b07045ed8c..f656d1cd99d 100644
--- a/config.c
+++ b/config.c
@@ -2437,6 +2437,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,
 	return 0;
 }
 
+static int check_multi_string(struct string_list_item *item, void *util)
+{
+	return item->string ? 0 : config_error_nonbool(util);
+}
+
+int git_configset_get_string_multi(struct config_set *cs, const char *key,
+				   const struct string_list **dest)
+{
+	int ret;
+
+	if ((ret = git_configset_get_value_multi(cs, key, dest)))
+		return ret;
+	if ((ret = for_each_string_list((struct string_list *)*dest,
+					check_multi_string, (void *)key)))
+		return ret;
+
+	return 0;
+}
+
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
 {
 	const char *value;
@@ -2587,6 +2606,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,
 	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
+int repo_config_get_string_multi(struct repository *repo, const char *key,
+				 const struct string_list **dest)
+{
+	git_config_check_init(repo);
+	return git_configset_get_string_multi(repo->config, key, dest);
+}
+
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest)
 {
@@ -2697,6 +2723,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)
 	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
+int git_config_get_string_multi(const char *key,
+				const struct string_list **dest)
+{
+	return repo_config_get_string_multi(the_repository, key, dest);
+}
+
 int git_config_get_string(const char *key, char **dest)
 {
 	return repo_config_get_string(the_repository, key, dest);
@@ -2842,7 +2874,8 @@ void git_die_config(const char *key, const char *err, ...)
 		va_end(params);
 	}
 	if (git_config_get_value_multi(key, &values))
-		BUG("for key '%s' we must have a value to report on", key);
+		BUG("key '%s' does not exist, should not be given to git_die_config()",
+		    key);
 	kv_info = values->items[values->nr - 1].util;
 	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
 }
diff --git a/config.h b/config.h
index 7f6ce6f2fb5..3079d60a860 100644
--- a/config.h
+++ b/config.h
@@ -472,6 +472,19 @@ RESULT_MUST_BE_USED
 int git_configset_get_value_multi(struct config_set *cs, const char *key,
 				  const struct string_list **dest);
 
+/**
+ * A validation wrapper for git_configset_get_value_multi() which does
+ * for it what git_configset_get_string() does for
+ * git_configset_get_value().
+ *
+ * The configuration syntax allows for "[section] key", which will
+ * give us a NULL entry in the "struct string_list", as opposed to
+ * "[section] key =" which is the empty string. Most users of the API
+ * are not prepared to handle NULL in a "struct string_list".
+ */
+int git_configset_get_string_multi(struct config_set *cs, const char *key,
+				   const struct string_list **dest);
+
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
  */
@@ -507,6 +520,9 @@ int repo_config_get_value(struct repository *repo,
 RESULT_MUST_BE_USED
 int repo_config_get_value_multi(struct repository *repo, const char *key,
 				const struct string_list **dest);
+RESULT_MUST_BE_USED
+int repo_config_get_string_multi(struct repository *repo, const char *key,
+				 const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -561,6 +577,9 @@ int git_config_get_value(const char *key, const char **value);
 RESULT_MUST_BE_USED
 int git_config_get_value_multi(const char *key,
 			       const struct string_list **dest);
+RESULT_MUST_BE_USED
+int git_config_get_string_multi(const char *key,
+				const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index 81f0c0e016b..dd05ab03ca0 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2303,7 +2303,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
 	const struct string_list *dest;
 
-	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+	if (!repo_config_get_string_multi(r, "pack.preferbitmaptips", &dest))
 		return dest;
 	return NULL;
 }
diff --git a/submodule.c b/submodule.c
index 05ebe5cab4c..6151e5c67a2 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
+	if (!repo_config_get_string_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index e4f02d8208b..ae73aef922f 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
-test_expect_failure 'parse log.excludeDecoration with no value' '
+test_expect_success 'parse log.excludeDecoration with no value' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '
 	[log]
 		excludeDecoration
 	EOF
-	git log --decorate=short
+	cat >expect <<-\EOF &&
+	error: missing value for '\''log.excludeDecoration'\''
+	EOF
+	git log --decorate=short 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'decorate-refs with glob' '
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 2e65c8139c4..68195a1de36 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,7 +404,7 @@ test_bitmap_cases () {
 		)
 	'
 
-	test_expect_failure 'pack.preferBitmapTips' '
+	test_expect_success 'pack.preferBitmapTips' '
 		git init repo &&
 		test_when_finished "rm -rf repo" &&
 		(
@@ -416,7 +416,12 @@ test_bitmap_cases () {
 			[pack]
 				preferBitmapTips
 			EOF
-			git repack -adb
+
+			cat >expect <<-\EOF &&
+			error: missing value for '\''pack.preferbitmaptips'\''
+			EOF
+			git repack -adb 2>actual &&
+			test_cmp expect actual
 		)
 	'
 
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index f343551a7d4..f4a31ada79a 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas
 		prereleaseSuffix
 		suffix
 	EOF
-	git tag -l --sort=version:refname
+	cat >expect <<-\EOF &&
+	error: missing value for '\''versionsort.suffix'\''
+	error: missing value for '\''versionsort.prereleasesuffix'\''
+	EOF
+	git tag -l --sort=version:refname 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'version sort with prerelease reordering' '
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index bfe27e50732..887d181b72e 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
-test_expect_failure 'is-active handles submodule.active config missing a value' '
+test_expect_success 'is-active handles submodule.active config missing a value' '
 	cp super/.git/config super/.git/config.orig &&
 	test_when_finished mv super/.git/config.orig super/.git/config &&
 
@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'
 		active
 	EOF
 
-	test-tool -C super submodule is-active sub1
+	cat >expect <<-\EOF &&
+	error: missing value for '\''submodule.active'\''
+	EOF
+	test-tool -C super submodule is-active sub1 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'is-active works with basic submodule.active config' '
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 958d906f245..1201866c8d0 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -505,7 +505,7 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --force
 '
 
-test_expect_failure 'register with no value for maintenance.repo' '
+test_expect_success 'register with no value for maintenance.repo' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -513,10 +513,15 @@ test_expect_failure 'register with no value for maintenance.repo' '
 	[maintenance]
 		repo
 	EOF
-	git maintenance register
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	git maintenance register 2>actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
 '
 
-test_expect_failure 'unregister with no value for maintenance.repo' '
+test_expect_success 'unregister with no value for maintenance.repo' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -527,8 +532,15 @@ test_expect_failure 'unregister with no value for maintenance.repo' '
 	cat >expect <<-\EOF &&
 	error: missing value for '\''maintenance.repo'\''
 	EOF
-	git maintenance unregister &&
-	git maintenance unregister --force
+	test_expect_code 128 git maintenance unregister 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo &&
+
+	git maintenance unregister --force 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
 '
 
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
diff --git a/versioncmp.c b/versioncmp.c
index 60c3a517122..7498da96e0e 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)
 		const char *const oldk = "versionsort.prereleasesuffix";
 		const struct string_list *newl;
 		const struct string_list *oldl;
-		int new = git_config_get_value_multi(newk, &newl);
-		int old = git_config_get_value_multi(oldk, &oldl);
+		int new = git_config_get_string_multi(newk, &newl);
+		int old = git_config_get_string_multi(oldk, &oldl);
 
 		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-- 
2.38.0.1280.g8136eb6fab2


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
                     ` (7 preceding siblings ...)
  2022-11-01 23:05   ` [PATCH v2 8/9] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
@ 2022-11-01 23:05   ` Ævar Arnfjörð Bjarmason
  2022-11-02  0:49   ` [PATCH v2 0/9] config API: make "multi" safe, fix numerous segfaults Taylor Blau
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-01 23:05 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on
configured repos, 2020-09-11). Due to assuming that elements returned
from the repo_config_get_value_multi() call wouldn't be "NULL" we'd
conflate the <path> and <command> part of the argument list when
running commands.

As noted in the preceding commit the fix is to move to a safer
"*_string_multi()" version of the *_multi() API. This change is
separated from the rest because those all segfaulted. In this change
we ended up with different behavior.

When using the "--config=<config>" form we take each element of the
list as a path to a repository. E.g. with a configuration like:

	[repo] list = /some/repo

We would, with this command:

	git for-each-repo --config=repo.list status builtin

Run a "git status" in /some/repo, as:

	git -C /some/repo status builtin

I.e. ask "status" to report on the "builtin" directory. But since a
configuration such as this would result in a "struct string_list *"
with one element, whose "string" member is "NULL":

	[repo] list

We would, when constructing our command-line in
"builtin/for-each-repo.c"...

	strvec_pushl(&child.args, "-C", path, NULL);
	for (i = 0; i < argc; i++)
		strvec_push(&child.args, argv[i]);

...have that "path" be "NULL", and as strvec_pushl() stops when it
sees NULL we'd end with the first "argv" element as the argument to
the "-C" option, e.g.:

	git -C status builtin

I.e. we'd run the command "builtin" in the "status" directory.

In another context this might be an interesting security
vulnerability, but I think that this amounts to a nothingburger on
that front.

A hypothetical attacker would need to be able to write config for the
victim to run, if they're able to do that there's more interesting
attack vectors. See the "safe.directory" facility added in
8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).

An even more unlikely possibility would be an attacker able to
generate the config used for "for-each-repo --config=<key>", but
nothing else (e.g. an automated system producing that list).

Even in that case the attack vector is limited to the user running
commands whose name matches a directory that's interesting to the
attacker (e.g. a "log" directory in a repository). The second
argument (if any) of the command is likely to make git die without
doing anything interesting (e.g. "-p" to "log", there being no "-p"
built-in command to run).

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  |  2 +-
 t/t0068-for-each-repo.sh | 13 +++++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 96caf90139b..9f52d6e0568 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -43,7 +43,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	err = repo_config_get_string_multi(the_repository, config_key, &values);
 	if (err < 0)
 		usage_msg_optf(_("got bad config --config=%s"),
 			       for_each_repo_usage, options, config_key);
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 115221c9ca5..c27d4dc5f71 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -39,4 +39,17 @@ test_expect_success 'error on bad config keys' '
 	test_expect_code 129 git for-each-repo --config="'\''.b"
 '
 
+test_expect_success 'error on NULL value for config keys' '
+	cat >>.git/config <<-\EOF &&
+	[empty]
+		key
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''empty.key'\''
+	EOF
+	test_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.38.0.1280.g8136eb6fab2


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 0/9] config API: make "multi" safe, fix numerous segfaults
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
                     ` (8 preceding siblings ...)
  2022-11-01 23:05   ` [PATCH v2 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
@ 2022-11-02  0:49   ` Taylor Blau
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
  10 siblings, 0 replies; 134+ messages in thread
From: Taylor Blau @ 2022-11-02  0:49 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor

On Wed, Nov 02, 2022 at 12:05:11AM +0100, Ævar Arnfjörð Bjarmason wrote:
> Ævar Arnfjörð Bjarmason (9):
>   for-each-repo tests: test bad --config keys
>   config tests: cover blind spots in git_die_config() tests
>   config tests: add "NULL" tests for *_get_value_multi()
>   versioncmp.c: refactor config reading next commit
>   config API: have *_multi() return an "int" and take a "dest"
>   for-each-repo: error on bad --config
>   config API users: test for *_get_value_multi() segfaults
>   config API: add "string" version of *_value_multi(), fix segfaults
>   for-each-repo: with bad config, don't conflate <path> and <cmd>

Thanks. I took the updated round, and will review it more closely when I
have a chance to tomorrow.

Thanks,
Taylor

^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v3 0/9] config API: make "multi" safe, fix numerous segfaults
  2022-11-01 23:05 ` [PATCH v2 0/9] " Ævar Arnfjörð Bjarmason
                     ` (9 preceding siblings ...)
  2022-11-02  0:49   ` [PATCH v2 0/9] config API: make "multi" safe, fix numerous segfaults Taylor Blau
@ 2022-11-25  9:50   ` Ævar Arnfjörð Bjarmason
  2022-11-25  9:50     ` [PATCH v3 1/9] for-each-repo tests: test bad --config keys Ævar Arnfjörð Bjarmason
                       ` (10 more replies)
  10 siblings, 11 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-25  9:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

This series fixes numerous segfaults in config API users, because they
didn't expect *_get_multi() to hand them a string_list with a NULL in
it given config like "[a] key" (note, no "="'s).

A larger general overview at v1[1], but note the API changes in
v2[2]. Changes since v2:

* Rebased on the now-landed "rp/maintenance-qol", which had conflicts
  in builtin/gc.c.
* Re-wrap some of the code properly at 79 characters.
* Drop the stray submodule--helper from "for-each-repo", it was from a
  mistaken earlier rebase in v2.
* Avoid minor whitespace changes in a test.
* Don't change the BUG() message in config.c later in the series
  (another earlier rebase change when slimming down the v2).

Junio: I'm sending this re-roll because I see
"ab/config-multi-and-nonbool" got ejected (presumably due to the
conflicts). Per [3] I think the "mixed bag" note in "What's Cooking"
refers to the state of v1, which I tried to address in v2.

1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/
2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/
3. https://lore.kernel.org/git/221107.86leomx2dg.gmgdl@evledraar.gmail.com/

CI & branch at:
https://github.com/avar/git/tree/avar/have-git_configset_get_value-use-dest-and-int-pattern-3

Ævar Arnfjörð Bjarmason (9):
  for-each-repo tests: test bad --config keys
  config tests: cover blind spots in git_die_config() tests
  config tests: add "NULL" tests for *_get_value_multi()
  versioncmp.c: refactor config reading next commit
  config API: have *_multi() return an "int" and take a "dest"
  for-each-repo: error on bad --config
  config API users: test for *_get_value_multi() segfaults
  config API: add "string" version of *_value_multi(), fix segfaults
  for-each-repo: with bad config, don't conflate <path> and <cmd>

 builtin/for-each-repo.c              | 14 ++---
 builtin/gc.c                         | 10 ++--
 builtin/log.c                        |  6 +-
 builtin/submodule--helper.c          |  7 ++-
 config.c                             | 87 +++++++++++++++++++++++-----
 config.h                             | 50 +++++++++++++---
 pack-bitmap.c                        |  6 +-
 submodule.c                          |  3 +-
 t/helper/test-config.c               |  6 +-
 t/t0068-for-each-repo.sh             | 19 ++++++
 t/t1308-config-set.sh                | 30 ++++++++++
 t/t3309-notes-merge-auto-resolve.sh  |  7 ++-
 t/t4202-log.sh                       | 15 +++++
 t/t5304-prune.sh                     | 12 +++-
 t/t5310-pack-bitmaps.sh              | 20 +++++++
 t/t5552-skipping-fetch-negotiator.sh | 16 +++++
 t/t7004-tag.sh                       | 17 ++++++
 t/t7413-submodule-is-active.sh       | 16 +++++
 t/t7900-maintenance.sh               | 38 ++++++++++++
 versioncmp.c                         | 22 ++++---
 20 files changed, 339 insertions(+), 62 deletions(-)

Range-diff against v2:
 1:  b8fd3bea4d1 =  1:  5c8819ff388 for-each-repo tests: test bad --config keys
 2:  6cd0d6faf3c =  2:  3eb8da6086d config tests: cover blind spots in git_die_config() tests
 3:  f2a8766a802 =  3:  14b08dfc162 config tests: add "NULL" tests for *_get_value_multi()
 4:  42cfc61202d =  4:  cb802b30cd8 versioncmp.c: refactor config reading next commit
 5:  48fb7cbf585 !  5:  e0e6ade3f38 config API: have *_multi() return an "int" and take a "dest"
    @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const
      			if (!strcmp(maintpath, item->string)) {
      				found = 1;
     @@ builtin/gc.c: static int maintenance_unregister(int argc, const char **argv, const char *prefi
    - 		usage_with_options(builtin_maintenance_unregister_usage,
    - 				   options);
    - 
    --	list = git_config_get_value_multi(key);
    + 	if (config_file) {
    + 		git_configset_init(&cs);
    + 		git_configset_add_file(&cs, config_file);
    +-		list = git_configset_get_value_multi(&cs, key);
    +-	} else {
    +-		list = git_config_get_value_multi(key);
    + 	}
     -	if (list) {
    -+	if (!git_config_get_value_multi(key, &list)) {
    ++	if (!(config_file
    ++	      ? git_configset_get_value_multi(&cs, key, &list)
    ++	      : git_config_get_value_multi(key, &list))) {
      		for_each_string_list_item(item, list) {
      			if (!strcmp(maintpath, item->string)) {
      				found = 1;
    @@ builtin/submodule--helper.c: static int module_update(int argc, const char **arg
      		 * by default, only initialize 'active' modules.
      		 */
     -		if (!argc && git_config_get_value_multi("submodule.active"))
    -+		if (!argc && !git_config_get_value_multi("submodule.active", &values))
    ++		if (!argc && !git_config_get_value_multi("submodule.active",
    ++							 &values))
      			module_list_active(&list);
      
      		info.prefix = opt.prefix;
 6:  a0c29d46556 !  6:  06d502bc577 for-each-repo: error on bad --config
    @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, cons
      		return 0;
      
     
    - ## builtin/submodule--helper.c ##
    -@@ builtin/submodule--helper.c: static int module_init(int argc, const char **argv, const char *prefix)
    - 		NULL
    - 	};
    - 	int ret = 1;
    --	const struct string_list *values;
    -+	const struct string_list *unused;
    - 
    - 	argc = parse_options(argc, argv, prefix, module_init_options,
    - 			     git_submodule_helper_usage, 0);
    -@@ builtin/submodule--helper.c: static int module_init(int argc, const char **argv, const char *prefix)
    - 	 * If there are no path args and submodule.active is set then,
    - 	 * by default, only initialize 'active' modules.
    - 	 */
    --	if (!argc && !git_config_get_value_multi("submodule.active", &values))
    -+	if (!argc && !git_config_get_value_multi("submodule.active", &unused))
    - 		module_list_active(&list);
    - 
    - 	info.prefix = prefix;
    -@@ builtin/submodule--helper.c: static int module_update(int argc, const char **argv, const char *prefix)
    - 	if (opt.init) {
    - 		struct module_list list = MODULE_LIST_INIT;
    - 		struct init_cb info = INIT_CB_INIT;
    --		const struct string_list *values;
    -+		const struct string_list *unused;
    - 
    - 		if (module_list_compute(argv, opt.prefix,
    - 					&pathspec2, &list) < 0) {
    -@@ builtin/submodule--helper.c: static int module_update(int argc, const char **argv, const char *prefix)
    - 		 * If there are no path args and submodule.active is set then,
    - 		 * by default, only initialize 'active' modules.
    - 		 */
    --		if (!argc && !git_config_get_value_multi("submodule.active", &values))
    -+		if (!argc && !git_config_get_value_multi("submodule.active", &unused))
    - 			module_list_active(&list);
    - 
    - 		info.prefix = opt.prefix;
    -
      ## t/t0068-for-each-repo.sh ##
     @@ t/t0068-for-each-repo.sh: test_expect_success 'do nothing on empty config' '
      	git for-each-repo --config=bogus.config -- help --no-such-option
 7:  c12805f3d55 !  7:  f35aacef4ca config API users: test for *_get_value_multi() segfaults
    @@ t/t7413-submodule-is-active.sh: test_expect_success 'is-active works with submod
     
      ## t/t7900-maintenance.sh ##
     @@ t/t7900-maintenance.sh: test_expect_success 'register and unregister' '
    - 	git maintenance unregister --force
    + 	git maintenance unregister --config-file ./other --force
      '
      
     +test_expect_failure 'register with no value for maintenance.repo' '
 8:  6b76f9eac90 !  8:  b45189b4624 config API: add "string" version of *_value_multi(), fix segfaults
    @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const
      			if (!strcmp(maintpath, item->string)) {
      				found = 1;
     @@ builtin/gc.c: static int maintenance_unregister(int argc, const char **argv, const char *prefi
    - 		usage_with_options(builtin_maintenance_unregister_usage,
    - 				   options);
    - 
    --	if (!git_config_get_value_multi(key, &list)) {
    -+	if (!git_config_get_string_multi(key, &list)) {
    + 		git_configset_add_file(&cs, config_file);
    + 	}
    + 	if (!(config_file
    +-	      ? git_configset_get_value_multi(&cs, key, &list)
    +-	      : git_config_get_value_multi(key, &list))) {
    ++	      ? git_configset_get_string_multi(&cs, key, &list)
    ++	      : git_config_get_string_multi(key, &list))) {
      		for_each_string_list_item(item, list) {
      			if (!strcmp(maintpath, item->string)) {
      				found = 1;
    @@ builtin/log.c: static void set_default_decoration_filter(struct decoration_filte
      
     -	if (!git_config_get_value_multi("log.excludeDecoration",
     -					&config_exclude)) {
    -+	if (!git_config_get_string_multi("log.excludeDecoration", &config_exclude)) {
    ++	if (!git_config_get_string_multi("log.excludeDecoration",
    ++					 &config_exclude)) {
      		struct string_list_item *item;
      		for_each_string_list_item(item, config_exclude)
      			string_list_append(decoration_filter->exclude_ref_config_pattern,
    @@ config.c: int git_config_get_value_multi(const char *key, const struct string_li
      int git_config_get_string(const char *key, char **dest)
      {
      	return repo_config_get_string(the_repository, key, dest);
    -@@ config.c: void git_die_config(const char *key, const char *err, ...)
    - 		va_end(params);
    - 	}
    - 	if (git_config_get_value_multi(key, &values))
    --		BUG("for key '%s' we must have a value to report on", key);
    -+		BUG("key '%s' does not exist, should not be given to git_die_config()",
    -+		    key);
    - 	kv_info = values->items[values->nr - 1].util;
    - 	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
    - }
     
      ## config.h ##
     @@ config.h: RESULT_MUST_BE_USED
    @@ t/t5310-pack-bitmaps.sh: test_bitmap_cases () {
      				preferBitmapTips
      			EOF
     -			git repack -adb
    -+
     +			cat >expect <<-\EOF &&
     +			error: missing value for '\''pack.preferbitmaptips'\''
     +			EOF
    @@ t/t7413-submodule-is-active.sh: test_expect_failure 'is-active handles submodule
     
      ## t/t7900-maintenance.sh ##
     @@ t/t7900-maintenance.sh: test_expect_success 'register and unregister' '
    - 	git maintenance unregister --force
    + 	git maintenance unregister --config-file ./other --force
      '
      
     -test_expect_failure 'register with no value for maintenance.repo' '
 9:  e2f8f7c52e3 =  9:  58ead3ca555 for-each-repo: with bad config, don't conflate <path> and <cmd>
-- 
2.39.0.rc0.955.ge9b241be664


^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v3 1/9] for-each-repo tests: test bad --config keys
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
@ 2022-11-25  9:50     ` Ævar Arnfjörð Bjarmason
  2022-11-25  9:50     ` [PATCH v3 2/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
                       ` (9 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-25  9:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

As noted in 6c62f015520 (for-each-repo: do nothing on empty config,
2021-01-08) this command wants to ignore a non-existing config key,
but it's been conflating that with bad config keys.

A subsequent commit will address that, but for now let's fix the gaps
in test coverage, and show what we're currently doing in these cases.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t0068-for-each-repo.sh | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index c6e0d655630..a099abc652e 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -39,4 +39,10 @@ test_expect_success 'do nothing on empty config' '
 	git for-each-repo --config=bogus.config -- help --no-such-option
 '
 
+test_expect_success 'bad config keys' '
+	git for-each-repo --config=a &&
+	git for-each-repo --config=a.b. &&
+	git for-each-repo --config="'\''.b"
+'
+
 test_done
-- 
2.39.0.rc0.955.ge9b241be664


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 2/9] config tests: cover blind spots in git_die_config() tests
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
  2022-11-25  9:50     ` [PATCH v3 1/9] for-each-repo tests: test bad --config keys Ævar Arnfjörð Bjarmason
@ 2022-11-25  9:50     ` Ævar Arnfjörð Bjarmason
  2023-01-19  0:15       ` Glen Choo
  2022-11-25  9:50     ` [PATCH v3 3/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
                       ` (8 subsequent siblings)
  10 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-25  9:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

There were no tests checking for the output of the git_die_config()
function in the config API, added in 5a80e97c827 (config: add
`git_die_config()` to the config-set API, 2014-08-07). We only tested
"test_must_fail", but didn't assert the output.

Let's check for that by extending the existing tests, and adding a new
one for "fetch.negotiationAlgorithm" so that we have a test for a user
of git_config_get_string*() calling git_die_config().

The other ones are testing:

- For *-resolve.sh: A custom call to git_die_config(), or via
  git_config_get_notes_strategy()
- For *-prune.sh: A call via git_config_get_expiry().

We also cover both the "from command-line config" and "in file..at
line" cases here.

The clobbering of existing ".git/config" files here is so that we're
not implicitly testing the line count of the default config.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-
 t/t5304-prune.sh                     | 12 ++++++++++--
 t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh
index 141d3e4ca4d..9bd5dbf341f 100755
--- a/t/t3309-notes-merge-auto-resolve.sh
+++ b/t/t3309-notes-merge-auto-resolve.sh
@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '
 
 test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '
 	git config core.notesRef refs/notes/y &&
-	test_must_fail git -c notes.mergeStrategy="foo" notes merge z &&
+	cat >expect <<-\EOF &&
+	error: unknown notes merge strategy foo
+	fatal: unable to parse '\''notes.mergeStrategy'\'' from command-line config
+	EOF
+	test_must_fail git -c notes.mergeStrategy="foo" notes merge z 2>actual &&
+	test_cmp expect actual &&
 	# Verify no changes (y)
 	verify_notes y y
 '
diff --git a/t/t5304-prune.sh b/t/t5304-prune.sh
index 8ae314af585..c8fa962b397 100755
--- a/t/t5304-prune.sh
+++ b/t/t5304-prune.sh
@@ -64,8 +64,16 @@ test_expect_success 'gc: implicit prune --expire' '
 '
 
 test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '
-	git config gc.pruneExpire invalid &&
-	test_must_fail git gc
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	>repo/.git/config &&
+	git -C repo config gc.pruneExpire invalid &&
+	cat >expect <<-\EOF &&
+	error: Invalid gc.pruneexpire: '\''invalid'\''
+	fatal: bad config variable '\''gc.pruneexpire'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_must_fail git -C repo gc 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'gc: start with ok gc.pruneExpire' '
diff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh
index 165427d57e5..b55a9f65e6b 100755
--- a/t/t5552-skipping-fetch-negotiator.sh
+++ b/t/t5552-skipping-fetch-negotiator.sh
@@ -3,6 +3,22 @@
 test_description='test skipping fetch negotiator'
 . ./test-lib.sh
 
+test_expect_success 'fetch.negotiationalgorithm config' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	cat >repo/.git/config <<-\EOF &&
+	[fetch]
+	negotiationAlgorithm
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''fetch.negotiationalgorithm'\''
+	fatal: bad config variable '\''fetch.negotiationalgorithm'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_expect_code 128 git -C repo fetch >out 2>actual &&
+	test_must_be_empty out &&
+	test_cmp expect actual
+'
+
 have_sent () {
 	while test "$#" -ne 0
 	do
-- 
2.39.0.rc0.955.ge9b241be664


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 3/9] config tests: add "NULL" tests for *_get_value_multi()
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
  2022-11-25  9:50     ` [PATCH v3 1/9] for-each-repo tests: test bad --config keys Ævar Arnfjörð Bjarmason
  2022-11-25  9:50     ` [PATCH v3 2/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
@ 2022-11-25  9:50     ` Ævar Arnfjörð Bjarmason
  2023-01-19  0:28       ` Glen Choo
  2022-11-25  9:50     ` [PATCH v3 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
                       ` (7 subsequent siblings)
  10 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-25  9:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

A less well known edge case in the config format is that keys can be
value-less, a shorthand syntax for "true" boolean keys. I.e. these two
are equivalent as far as "--type=bool" is concerned:

	[a]key
	[a]key = true

But as far as our parser is concerned the values for these two are
NULL, and "true". I.e. for a sequence like:

	[a]key=x
	[a]key
	[a]key=y

We get a "struct string_list" with "string" members with ".string"
values of:

	{ "x", NULL, "y" }

This behavior goes back to the initial implementation of
git_config_bool() in 17712991a59 (Add ".git/config" file parser,
2005-10-10).

When the "t/t1308-config-set.sh" tests were added in [1] only one of
the three "(NULL)" lines in "t/helper/test-config.c" had any test
coverage. This change adds tests that stress the remaining two.

1. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t1308-config-set.sh | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index b38e158d3b2..561e82f1808 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -146,6 +146,36 @@ test_expect_success 'find multiple values' '
 	check_config get_value_multi case.baz sam bat hask
 '
 
+test_expect_success 'emit multi values from configset with NULL entry' '
+	test_when_finished "rm -f my.config" &&
+	cat >my.config <<-\EOF &&
+	[a]key=x
+	[a]key
+	[a]key=y
+	EOF
+	cat >expect <<-\EOF &&
+	x
+	(NULL)
+	y
+	EOF
+	test-tool config configset_get_value_multi a.key my.config >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'multi values from configset with a last NULL entry' '
+	test_when_finished "rm -f my.config" &&
+	cat >my.config <<-\EOF &&
+	[a]key=x
+	[a]key=y
+	[a]key
+	EOF
+	cat >expect <<-\EOF &&
+	(NULL)
+	EOF
+	test-tool config configset_get_value a.key my.config >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'find value from a configset' '
 	cat >config2 <<-\EOF &&
 	[case]
-- 
2.39.0.rc0.955.ge9b241be664


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 4/9] versioncmp.c: refactor config reading next commit
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
                       ` (2 preceding siblings ...)
  2022-11-25  9:50     ` [PATCH v3 3/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2022-11-25  9:50     ` Ævar Arnfjörð Bjarmason
  2022-11-25  9:50     ` [PATCH v3 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
                       ` (6 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-25  9:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Refactor the reading of the versionSort.suffix and
versionSort.prereleaseSuffix configuration variables to stay within
the bounds of our CodingGuidelines when it comes to line length, and
ta avoid repeating ourselves.

Let's also split out the names of the config variables into variables
of our own, so we don't have to repeat ourselves, and refactor the
nested if/else to avoid indenting it, and the existing bracing style
issue.

This all helps with the subsequent commit, where we'll need to start
checking different git_config_get_value_multi() return value. See
c026557a373 (versioncmp: generalize version sort suffix reordering,
2016-12-08) for the original implementation of most of this.

Moving the "initialized = 1" assignment allows us to move some of this
to the variable declarations in the subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 versioncmp.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/versioncmp.c b/versioncmp.c
index 069ee94a4d7..323f5d35ea8 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)
 	}
 
 	if (!initialized) {
-		const struct string_list *deprecated_prereleases;
+		const char *const newk = "versionsort.suffix";
+		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *oldl;
+
+		prereleases = git_config_get_value_multi(newk);
+		oldl = git_config_get_value_multi(oldk);
+		if (prereleases && oldl)
+			warning("ignoring %s because %s is set", oldk, newk);
+		else if (!prereleases)
+			prereleases = oldl;
+
 		initialized = 1;
-		prereleases = git_config_get_value_multi("versionsort.suffix");
-		deprecated_prereleases = git_config_get_value_multi("versionsort.prereleasesuffix");
-		if (prereleases) {
-			if (deprecated_prereleases)
-				warning("ignoring versionsort.prereleasesuffix because versionsort.suffix is set");
-		} else
-			prereleases = deprecated_prereleases;
 	}
 	if (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,
 					    &diff))
-- 
2.39.0.rc0.955.ge9b241be664


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 5/9] config API: have *_multi() return an "int" and take a "dest"
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
                       ` (3 preceding siblings ...)
  2022-11-25  9:50     ` [PATCH v3 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
@ 2022-11-25  9:50     ` Ævar Arnfjörð Bjarmason
  2023-01-19  0:50       ` Glen Choo
  2022-11-25  9:50     ` [PATCH v3 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
                       ` (5 subsequent siblings)
  10 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-25  9:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Have the "git_configset_get_value_multi()" function and its siblings
return an "int" and populate a "**dest" parameter like every other
git_configset_get_*()" in the API.

As we'll see in in subsequent commits this fixes a blind spot in the
API where it wasn't possible to tell whether a list was empty from
whether a config key existed. We'll take advantage of that in
subsequent commits, but for now we're faithfully converting existing
API callers.

See [1] for the initial addition of "git_configset_get_value_multi()"

1. 3c8687a73ee (add `config_set` API for caching config-like files,
   2014-07-28).

A logical follow-up to this would be to change the various "*_get_*()"
functions to ferry the git_configset_get_value() return value to their
own callers, e.g. git_configset_get_int() returns "1" rather than
ferrying up the "-1" that "git_configset_get_value()" might return,
but that's not being done in this series

Most of this is straightforward, commentary on cases that stand out:

- As we've tested for in a preceding commit we can rely on getting the
  config list in git_die_config(), and as we need to handle the new
  return value let's BUG() out if we can't acquire it.

- In "builtin/for-each-ref.c" we could preserve the comment added in
  6c62f015520, but now that we're directly using the documented
  repo_config_get_value_multi() value it's just narrating something that
  should be obvious from the API use, so let's drop it.

- The loops after getting the "list" value in "builtin/gc.c" could
  also make use of "unsorted_string_list_has_string()" instead of using
  that loop, but let's leave that for now.

- We have code e.g. in "builtin/submodule--helper.c" that only wants
  to check if a config key exists, and would be better served with
  another API, but let's keep using "git_configset_get_value_multi()"
  for now.

- In "versioncmp.c" we now use the return value of the functions,
  instead of checking if the lists are still non-NULL. This is strictly
  speaking unnecessary, but makes the API use consistent with the rest,
  but more importantly...

- ...because we always check our return values we can assert that with
  the RESULT_MUST_BE_USED macro added in 1e8697b5c4e (submodule--helper:
  check repo{_submodule,}_init() return values, 2022-09-01)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c     | 13 ++++-----
 builtin/gc.c                | 10 +++----
 builtin/log.c               |  6 ++--
 builtin/submodule--helper.c |  7 +++--
 config.c                    | 55 ++++++++++++++++++++++++++-----------
 config.h                    | 29 +++++++++++++------
 pack-bitmap.c               |  6 +++-
 submodule.c                 |  3 +-
 t/helper/test-config.c      |  6 ++--
 versioncmp.c                | 11 +++++---
 10 files changed, 92 insertions(+), 54 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 6aeac371488..7cc41847635 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	static const char *config_key = NULL;
 	int i, result = 0;
 	const struct string_list *values;
+	int err;
 
 	const struct option options[] = {
 		OPT_STRING(0, "config", &config_key, N_("config"),
@@ -45,14 +46,10 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	values = repo_config_get_value_multi(the_repository,
-					     config_key);
-
-	/*
-	 * Do nothing on an empty list, which is equivalent to the case
-	 * where the config variable does not exist at all.
-	 */
-	if (!values)
+	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	if (err < 0)
+		return 0;
+	else if (err)
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/builtin/gc.c b/builtin/gc.c
index 02455fdcd73..69503e0a023 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1513,8 +1513,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	else
 		git_config_set("maintenance.strategy", "incremental");
 
-	list = git_config_get_value_multi(key);
-	if (list) {
+	if (!git_config_get_value_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1580,11 +1579,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 	if (config_file) {
 		git_configset_init(&cs);
 		git_configset_add_file(&cs, config_file);
-		list = git_configset_get_value_multi(&cs, key);
-	} else {
-		list = git_config_get_value_multi(key);
 	}
-	if (list) {
+	if (!(config_file
+	      ? git_configset_get_value_multi(&cs, key, &list)
+	      : git_config_get_value_multi(key, &list))) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index 5eafcf26b49..cc9d92f95da 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	int i;
 	char *value = NULL;
 	struct string_list *include = decoration_filter->include_ref_pattern;
-	const struct string_list *config_exclude =
-			git_config_get_value_multi("log.excludeDecoration");
+	const struct string_list *config_exclude;
 
-	if (config_exclude) {
+	if (!git_config_get_value_multi("log.excludeDecoration",
+					&config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index c75e9e86b06..08c12b25375 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -541,6 +541,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 		NULL
 	};
 	int ret = 1;
+	const struct string_list *values;
 
 	argc = parse_options(argc, argv, prefix, module_init_options,
 			     git_submodule_helper_usage, 0);
@@ -552,7 +553,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 	 * If there are no path args and submodule.active is set then,
 	 * by default, only initialize 'active' modules.
 	 */
-	if (!argc && git_config_get_value_multi("submodule.active"))
+	if (!argc && !git_config_get_value_multi("submodule.active", &values))
 		module_list_active(&list);
 
 	info.prefix = prefix;
@@ -2714,6 +2715,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 	if (opt.init) {
 		struct module_list list = MODULE_LIST_INIT;
 		struct init_cb info = INIT_CB_INIT;
+		const struct string_list *values;
 
 		if (module_list_compute(argv, opt.prefix,
 					&pathspec2, &list) < 0) {
@@ -2726,7 +2728,8 @@ static int module_update(int argc, const char **argv, const char *prefix)
 		 * If there are no path args and submodule.active is set then,
 		 * by default, only initialize 'active' modules.
 		 */
-		if (!argc && git_config_get_value_multi("submodule.active"))
+		if (!argc && !git_config_get_value_multi("submodule.active",
+							 &values))
 			module_list_active(&list);
 
 		info.prefix = opt.prefix;
diff --git a/config.c b/config.c
index c058b2c70c3..0b07045ed8c 100644
--- a/config.c
+++ b/config.c
@@ -2275,23 +2275,28 @@ void read_very_early_config(config_fn_t cb, void *data)
 	config_with_options(cb, data, NULL, &opts);
 }
 
-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
+static int configset_find_element(struct config_set *cs, const char *key,
+				  struct config_set_element **dest)
 {
 	struct config_set_element k;
 	struct config_set_element *found_entry;
 	char *normalized_key;
+	int ret;
+
 	/*
 	 * `key` may come from the user, so normalize it before using it
 	 * for querying entries from the hashmap.
 	 */
-	if (git_config_parse_key(key, &normalized_key, NULL))
-		return NULL;
+	ret = git_config_parse_key(key, &normalized_key, NULL);
+	if (ret < 0)
+		return ret;
 
 	hashmap_entry_init(&k.ent, strhash(normalized_key));
 	k.key = normalized_key;
 	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
 	free(normalized_key);
-	return found_entry;
+	*dest = found_entry;
+	return 0;
 }
 
 static int configset_add_value(struct config_set *cs, const char *key, const char *value)
@@ -2300,8 +2305,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha
 	struct string_list_item *si;
 	struct configset_list_item *l_item;
 	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
+	int ret;
 
-	e = configset_find_element(cs, key);
+	ret = configset_find_element(cs, key, &e);
+	if (ret < 0)
+		return ret;
 	/*
 	 * Since the keys are being fed by git_config*() callback mechanism, they
 	 * are already normalized. So simply add them without any further munging.
@@ -2395,24 +2403,38 @@ int git_configset_add_file(struct config_set *cs, const char *filename)
 int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
 {
 	const struct string_list *values = NULL;
+	int ret;
+
 	/*
 	 * Follows "last one wins" semantic, i.e., if there are multiple matches for the
 	 * queried key in the files of the configset, the value returned will be the last
 	 * value in the value list for that key.
 	 */
-	values = git_configset_get_value_multi(cs, key);
+	ret = git_configset_get_value_multi(cs, key, &values);
 
-	if (!values)
+	if (ret < 0)
+		return ret;
+	else if (!values)
 		return 1;
 	assert(values->nr > 0);
 	*value = values->items[values->nr - 1].string;
 	return 0;
 }
 
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest)
 {
-	struct config_set_element *e = configset_find_element(cs, key);
-	return e ? &e->value_list : NULL;
+	struct config_set_element *e;
+	int ret;
+
+	ret = configset_find_element(cs, key, &e);
+	if (ret < 0)
+		return ret;
+	else if (!e)
+		return 1;
+	*dest = &e->value_list;
+
+	return 0;
 }
 
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
@@ -2558,11 +2580,11 @@ int repo_config_get_value(struct repository *repo,
 	return git_configset_get_value(repo->config, key, value);
 }
 
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key)
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest)
 {
 	git_config_check_init(repo);
-	return git_configset_get_value_multi(repo->config, key);
+	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
 int repo_config_get_string(struct repository *repo,
@@ -2670,9 +2692,9 @@ int git_config_get_value(const char *key, const char **value)
 	return repo_config_get_value(the_repository, key, value);
 }
 
-const struct string_list *git_config_get_value_multi(const char *key)
+int git_config_get_value_multi(const char *key, const struct string_list **dest)
 {
-	return repo_config_get_value_multi(the_repository, key);
+	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
 int git_config_get_string(const char *key, char **dest)
@@ -2819,7 +2841,8 @@ void git_die_config(const char *key, const char *err, ...)
 		error_fn(err, params);
 		va_end(params);
 	}
-	values = git_config_get_value_multi(key);
+	if (git_config_get_value_multi(key, &values))
+		BUG("for key '%s' we must have a value to report on", key);
 	kv_info = values->items[values->nr - 1].util;
 	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
 }
diff --git a/config.h b/config.h
index ef9eade6414..7f6ce6f2fb5 100644
--- a/config.h
+++ b/config.h
@@ -459,10 +459,18 @@ int git_configset_add_parameters(struct config_set *cs);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key` and config set `cs`. When the
- * configuration variable `key` is not found, returns NULL. The caller
- * should not free or modify the returned pointer, as it is owned by the cache.
+ * configuration variable `key` is not found, returns 1 without touching
+ * `value`.
+ *
+ * The key will be parsed for validity with git_config_parse_key(), on
+ * error a negative value will be returned.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
+RESULT_MUST_BE_USED
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest);
 
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
@@ -496,8 +504,9 @@ struct repository;
 void repo_config(struct repository *repo, config_fn_t fn, void *data);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key);
+RESULT_MUST_BE_USED
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -544,10 +553,14 @@ int git_config_get_value(const char *key, const char **value);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key`. When the configuration variable
- * `key` is not found, returns NULL. The caller should not free or modify
- * the returned pointer, as it is owned by the cache.
+ * `key` is not found, returns 1 without touching `value`.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_config_get_value_multi(const char *key);
+RESULT_MUST_BE_USED
+int git_config_get_value_multi(const char *key,
+			       const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index 440407f1be7..81f0c0e016b 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2301,7 +2301,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)
 
 const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
-	return repo_config_get_value_multi(r, "pack.preferbitmaptips");
+	const struct string_list *dest;
+
+	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+		return dest;
+	return NULL;
 }
 
 int bitmap_is_preferred_refname(struct repository *r, const char *refname)
diff --git a/submodule.c b/submodule.c
index 8ac2fca855d..e43c4230ba3 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	sl = repo_config_get_value_multi(repo, "submodule.active");
-	if (sl) {
+	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/helper/test-config.c b/t/helper/test-config.c
index 4ba9eb65606..8f70beb6c9d 100644
--- a/t/helper/test-config.c
+++ b/t/helper/test-config.c
@@ -95,8 +95,7 @@ int cmd__config(int argc, const char **argv)
 			goto exit1;
 		}
 	} else if (argc == 3 && !strcmp(argv[1], "get_value_multi")) {
-		strptr = git_config_get_value_multi(argv[2]);
-		if (strptr) {
+		if (!git_config_get_value_multi(argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
@@ -159,8 +158,7 @@ int cmd__config(int argc, const char **argv)
 				goto exit2;
 			}
 		}
-		strptr = git_configset_get_value_multi(&cs, argv[2]);
-		if (strptr) {
+		if (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
diff --git a/versioncmp.c b/versioncmp.c
index 323f5d35ea8..60c3a517122 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)
 	if (!initialized) {
 		const char *const newk = "versionsort.suffix";
 		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *newl;
 		const struct string_list *oldl;
+		int new = git_config_get_value_multi(newk, &newl);
+		int old = git_config_get_value_multi(oldk, &oldl);
 
-		prereleases = git_config_get_value_multi(newk);
-		oldl = git_config_get_value_multi(oldk);
-		if (prereleases && oldl)
+		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-		else if (!prereleases)
+		if (!new)
+			prereleases = newl;
+		else if (!old)
 			prereleases = oldl;
 
 		initialized = 1;
-- 
2.39.0.rc0.955.ge9b241be664


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 6/9] for-each-repo: error on bad --config
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
                       ` (4 preceding siblings ...)
  2022-11-25  9:50     ` [PATCH v3 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
@ 2022-11-25  9:50     ` Ævar Arnfjörð Bjarmason
  2022-11-25  9:50     ` [PATCH v3 7/9] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
                       ` (4 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-25  9:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

As noted in 6c62f015520 (for-each-repo: do nothing on empty config,
2021-01-08) this command wants to ignore a non-existing config key,
but let's not conflate that with bad config.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  | 3 ++-
 t/t0068-for-each-repo.sh | 8 ++++----
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 7cc41847635..224164addb3 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -48,7 +48,8 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 
 	err = repo_config_get_value_multi(the_repository, config_key, &values);
 	if (err < 0)
-		return 0;
+		usage_msg_optf(_("got bad config --config=%s"),
+			       for_each_repo_usage, options, config_key);
 	else if (err)
 		return 0;
 
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index a099abc652e..19ceaa546ea 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -39,10 +39,10 @@ test_expect_success 'do nothing on empty config' '
 	git for-each-repo --config=bogus.config -- help --no-such-option
 '
 
-test_expect_success 'bad config keys' '
-	git for-each-repo --config=a &&
-	git for-each-repo --config=a.b. &&
-	git for-each-repo --config="'\''.b"
+test_expect_success 'error on bad config keys' '
+	test_expect_code 129 git for-each-repo --config=a &&
+	test_expect_code 129 git for-each-repo --config=a.b. &&
+	test_expect_code 129 git for-each-repo --config="'\''.b"
 '
 
 test_done
-- 
2.39.0.rc0.955.ge9b241be664


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 7/9] config API users: test for *_get_value_multi() segfaults
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
                       ` (5 preceding siblings ...)
  2022-11-25  9:50     ` [PATCH v3 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
@ 2022-11-25  9:50     ` Ævar Arnfjörð Bjarmason
  2023-01-19  0:51       ` Glen Choo
  2022-11-25  9:50     ` [PATCH v3 8/9] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
                       ` (3 subsequent siblings)
  10 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-25  9:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

As we'll discus in the subsequent commit these tests all
show *_get_value_multi() API users unable to handle there being a
value-less key in the config, which is represented with a "NULL" for
that entry in the "string" member of the returned "struct
string_list", causing a segfault.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t4202-log.sh                 | 11 +++++++++++
 t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++
 t/t7004-tag.sh                 | 12 ++++++++++++
 t/t7413-submodule-is-active.sh | 12 ++++++++++++
 t/t7900-maintenance.sh         | 26 ++++++++++++++++++++++++++
 5 files changed, 77 insertions(+)

diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 2ce2b41174d..e4f02d8208b 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
+test_expect_failure 'parse log.excludeDecoration with no value' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[log]
+		excludeDecoration
+	EOF
+	git log --decorate=short
+'
+
 test_expect_success 'decorate-refs with glob' '
 	cat >expect.decorate <<-\EOF &&
 	Merge-tag-reach
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 6d693eef82f..2e65c8139c4 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,6 +404,22 @@ test_bitmap_cases () {
 		)
 	'
 
+	test_expect_failure 'pack.preferBitmapTips' '
+		git init repo &&
+		test_when_finished "rm -rf repo" &&
+		(
+			cd repo &&
+			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
+			test_commit_bulk --message="%s" 103 &&
+
+			cat >>.git/config <<-\EOF &&
+			[pack]
+				preferBitmapTips
+			EOF
+			git repack -adb
+		)
+	'
+
 	test_expect_success 'complains about multiple pack bitmaps' '
 		rm -fr repo &&
 		git init repo &&
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index 9aa1660651b..f343551a7d4 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[versionsort]
+		prereleaseSuffix
+		suffix
+	EOF
+	git tag -l --sort=version:refname
+'
+
 test_expect_success 'version sort with prerelease reordering' '
 	test_config versionsort.prereleaseSuffix -rc &&
 	git tag foo1.6-rc1 &&
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index 7cdc2637649..bfe27e50732 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
+test_expect_failure 'is-active handles submodule.active config missing a value' '
+	cp super/.git/config super/.git/config.orig &&
+	test_when_finished mv super/.git/config.orig super/.git/config &&
+
+	cat >>super/.git/config <<-\EOF &&
+	[submodule]
+		active
+	EOF
+
+	test-tool -C super submodule is-active sub1
+'
+
 test_expect_success 'is-active works with basic submodule.active config' '
 	test_when_finished "git -C super config submodule.sub1.URL ../sub" &&
 	test_when_finished "git -C super config --unset-all submodule.active" &&
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 823331e44a0..2dbac07be83 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -524,6 +524,32 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --config-file ./other --force
 '
 
+test_expect_failure 'register with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	git maintenance register
+'
+
+test_expect_failure 'unregister with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	git maintenance unregister &&
+	git maintenance unregister --force
+'
+
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
 	META="a+b*c" &&
 	git init "$META" &&
-- 
2.39.0.rc0.955.ge9b241be664


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 8/9] config API: add "string" version of *_value_multi(), fix segfaults
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
                       ` (6 preceding siblings ...)
  2022-11-25  9:50     ` [PATCH v3 7/9] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
@ 2022-11-25  9:50     ` Ævar Arnfjörð Bjarmason
  2023-01-19  1:03       ` Glen Choo
  2022-11-25  9:50     ` [PATCH v3 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
                       ` (2 subsequent siblings)
  10 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-25  9:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Fix numerous and mostly long-standing segfaults in consumers of
the *_config_*value_multi() API. As discussed in the preceding commit
an empty key in the config syntax yields a "NULL" string, which these
users would give to strcmp() (or similar), resulting in segfaults.

As this change shows, most users users of the *_config_*value_multi()
API didn't really want such an an unsafe and low-level API, let's give
them something with the safety of git_config_get_string() instead.

This fix is similar to what the *_string() functions and others
acquired in[1] and [2]. Namely introducing and using a safer
"*_get_string_multi()" variant of the low-level "_*value_multi()"
function.

This fixes segfaults in code introduced in:

  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)
  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)
  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)
  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)
  - 92156291ca8 (log: add default decoration filter, 2022-08-05)
  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)

There are now three remaining files using the low-level API:

- Two cases in "builtin/submodule--helper.c", where it's used safely
  to see if any config exists.
- One in "builtin/for-each-repo.c", which we'll convert in a
  subsequent commit.
- The "t/helper/test-config.c" code added in [3].

As seen in the preceding commit we need to give the
"t/helper/test-config.c" caller these "NULL" entries.

We could also alter the underlying git_configset_get_value_multi()
function to be "string safe", but doing so would leave no room for
other variants of "*_get_value_multi()" that coerce to other types.

Such coercion can't be built on the string version, since as we've
established "NULL" is a true value in the boolean context, but if we
coerced it to "" for use in a list of strings it'll be subsequently
coerced to "false" as a boolean.

The callback pattern being used here will make it easy to introduce
e.g. a "multi" variant which coerces its values to "bool", "int",
"path" etc.

1. 40ea4ed9032 (Add config_error_nonbool() helper function,
   2008-02-11)
2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,
   2008-02-11).
3. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                   |  6 +++---
 builtin/log.c                  |  4 ++--
 config.c                       | 32 ++++++++++++++++++++++++++++++++
 config.h                       | 19 +++++++++++++++++++
 pack-bitmap.c                  |  2 +-
 submodule.c                    |  2 +-
 t/t4202-log.sh                 |  8 ++++++--
 t/t5310-pack-bitmaps.sh        |  8 ++++++--
 t/t7004-tag.sh                 |  9 +++++++--
 t/t7413-submodule-is-active.sh |  8 ++++++--
 t/t7900-maintenance.sh         | 22 +++++++++++++++++-----
 versioncmp.c                   |  4 ++--
 12 files changed, 102 insertions(+), 22 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 69503e0a023..2d95c5b29aa 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1513,7 +1513,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	else
 		git_config_set("maintenance.strategy", "incremental");
 
-	if (!git_config_get_value_multi(key, &list)) {
+	if (!git_config_get_string_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1581,8 +1581,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 		git_configset_add_file(&cs, config_file);
 	}
 	if (!(config_file
-	      ? git_configset_get_value_multi(&cs, key, &list)
-	      : git_config_get_value_multi(key, &list))) {
+	      ? git_configset_get_string_multi(&cs, key, &list)
+	      : git_config_get_string_multi(key, &list))) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index cc9d92f95da..cd17f311712 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	struct string_list *include = decoration_filter->include_ref_pattern;
 	const struct string_list *config_exclude;
 
-	if (!git_config_get_value_multi("log.excludeDecoration",
-					&config_exclude)) {
+	if (!git_config_get_string_multi("log.excludeDecoration",
+					 &config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/config.c b/config.c
index 0b07045ed8c..9bd43189c02 100644
--- a/config.c
+++ b/config.c
@@ -2437,6 +2437,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,
 	return 0;
 }
 
+static int check_multi_string(struct string_list_item *item, void *util)
+{
+	return item->string ? 0 : config_error_nonbool(util);
+}
+
+int git_configset_get_string_multi(struct config_set *cs, const char *key,
+				   const struct string_list **dest)
+{
+	int ret;
+
+	if ((ret = git_configset_get_value_multi(cs, key, dest)))
+		return ret;
+	if ((ret = for_each_string_list((struct string_list *)*dest,
+					check_multi_string, (void *)key)))
+		return ret;
+
+	return 0;
+}
+
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
 {
 	const char *value;
@@ -2587,6 +2606,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,
 	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
+int repo_config_get_string_multi(struct repository *repo, const char *key,
+				 const struct string_list **dest)
+{
+	git_config_check_init(repo);
+	return git_configset_get_string_multi(repo->config, key, dest);
+}
+
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest)
 {
@@ -2697,6 +2723,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)
 	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
+int git_config_get_string_multi(const char *key,
+				const struct string_list **dest)
+{
+	return repo_config_get_string_multi(the_repository, key, dest);
+}
+
 int git_config_get_string(const char *key, char **dest)
 {
 	return repo_config_get_string(the_repository, key, dest);
diff --git a/config.h b/config.h
index 7f6ce6f2fb5..3079d60a860 100644
--- a/config.h
+++ b/config.h
@@ -472,6 +472,19 @@ RESULT_MUST_BE_USED
 int git_configset_get_value_multi(struct config_set *cs, const char *key,
 				  const struct string_list **dest);
 
+/**
+ * A validation wrapper for git_configset_get_value_multi() which does
+ * for it what git_configset_get_string() does for
+ * git_configset_get_value().
+ *
+ * The configuration syntax allows for "[section] key", which will
+ * give us a NULL entry in the "struct string_list", as opposed to
+ * "[section] key =" which is the empty string. Most users of the API
+ * are not prepared to handle NULL in a "struct string_list".
+ */
+int git_configset_get_string_multi(struct config_set *cs, const char *key,
+				   const struct string_list **dest);
+
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
  */
@@ -507,6 +520,9 @@ int repo_config_get_value(struct repository *repo,
 RESULT_MUST_BE_USED
 int repo_config_get_value_multi(struct repository *repo, const char *key,
 				const struct string_list **dest);
+RESULT_MUST_BE_USED
+int repo_config_get_string_multi(struct repository *repo, const char *key,
+				 const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -561,6 +577,9 @@ int git_config_get_value(const char *key, const char **value);
 RESULT_MUST_BE_USED
 int git_config_get_value_multi(const char *key,
 			       const struct string_list **dest);
+RESULT_MUST_BE_USED
+int git_config_get_string_multi(const char *key,
+				const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index 81f0c0e016b..dd05ab03ca0 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2303,7 +2303,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
 	const struct string_list *dest;
 
-	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+	if (!repo_config_get_string_multi(r, "pack.preferbitmaptips", &dest))
 		return dest;
 	return NULL;
 }
diff --git a/submodule.c b/submodule.c
index e43c4230ba3..0f6cf864ed9 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
+	if (!repo_config_get_string_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index e4f02d8208b..ae73aef922f 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
-test_expect_failure 'parse log.excludeDecoration with no value' '
+test_expect_success 'parse log.excludeDecoration with no value' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '
 	[log]
 		excludeDecoration
 	EOF
-	git log --decorate=short
+	cat >expect <<-\EOF &&
+	error: missing value for '\''log.excludeDecoration'\''
+	EOF
+	git log --decorate=short 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'decorate-refs with glob' '
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 2e65c8139c4..894c750080c 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,7 +404,7 @@ test_bitmap_cases () {
 		)
 	'
 
-	test_expect_failure 'pack.preferBitmapTips' '
+	test_expect_success 'pack.preferBitmapTips' '
 		git init repo &&
 		test_when_finished "rm -rf repo" &&
 		(
@@ -416,7 +416,11 @@ test_bitmap_cases () {
 			[pack]
 				preferBitmapTips
 			EOF
-			git repack -adb
+			cat >expect <<-\EOF &&
+			error: missing value for '\''pack.preferbitmaptips'\''
+			EOF
+			git repack -adb 2>actual &&
+			test_cmp expect actual
 		)
 	'
 
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index f343551a7d4..f4a31ada79a 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas
 		prereleaseSuffix
 		suffix
 	EOF
-	git tag -l --sort=version:refname
+	cat >expect <<-\EOF &&
+	error: missing value for '\''versionsort.suffix'\''
+	error: missing value for '\''versionsort.prereleasesuffix'\''
+	EOF
+	git tag -l --sort=version:refname 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'version sort with prerelease reordering' '
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index bfe27e50732..887d181b72e 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
-test_expect_failure 'is-active handles submodule.active config missing a value' '
+test_expect_success 'is-active handles submodule.active config missing a value' '
 	cp super/.git/config super/.git/config.orig &&
 	test_when_finished mv super/.git/config.orig super/.git/config &&
 
@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'
 		active
 	EOF
 
-	test-tool -C super submodule is-active sub1
+	cat >expect <<-\EOF &&
+	error: missing value for '\''submodule.active'\''
+	EOF
+	test-tool -C super submodule is-active sub1 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'is-active works with basic submodule.active config' '
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 2dbac07be83..487e326b3fa 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --config-file ./other --force
 '
 
-test_expect_failure 'register with no value for maintenance.repo' '
+test_expect_success 'register with no value for maintenance.repo' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '
 	[maintenance]
 		repo
 	EOF
-	git maintenance register
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	git maintenance register 2>actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
 '
 
-test_expect_failure 'unregister with no value for maintenance.repo' '
+test_expect_success 'unregister with no value for maintenance.repo' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -546,8 +551,15 @@ test_expect_failure 'unregister with no value for maintenance.repo' '
 	cat >expect <<-\EOF &&
 	error: missing value for '\''maintenance.repo'\''
 	EOF
-	git maintenance unregister &&
-	git maintenance unregister --force
+	test_expect_code 128 git maintenance unregister 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo &&
+
+	git maintenance unregister --force 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
 '
 
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
diff --git a/versioncmp.c b/versioncmp.c
index 60c3a517122..7498da96e0e 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)
 		const char *const oldk = "versionsort.prereleasesuffix";
 		const struct string_list *newl;
 		const struct string_list *oldl;
-		int new = git_config_get_value_multi(newk, &newl);
-		int old = git_config_get_value_multi(oldk, &oldl);
+		int new = git_config_get_string_multi(newk, &newl);
+		int old = git_config_get_string_multi(oldk, &oldl);
 
 		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-- 
2.39.0.rc0.955.ge9b241be664


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
                       ` (7 preceding siblings ...)
  2022-11-25  9:50     ` [PATCH v3 8/9] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
@ 2022-11-25  9:50     ` Ævar Arnfjörð Bjarmason
  2023-01-19  0:10     ` [PATCH v3 0/9] config API: make "multi" safe, fix numerous segfaults Glen Choo
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-11-25  9:50 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on
configured repos, 2020-09-11). Due to assuming that elements returned
from the repo_config_get_value_multi() call wouldn't be "NULL" we'd
conflate the <path> and <command> part of the argument list when
running commands.

As noted in the preceding commit the fix is to move to a safer
"*_string_multi()" version of the *_multi() API. This change is
separated from the rest because those all segfaulted. In this change
we ended up with different behavior.

When using the "--config=<config>" form we take each element of the
list as a path to a repository. E.g. with a configuration like:

	[repo] list = /some/repo

We would, with this command:

	git for-each-repo --config=repo.list status builtin

Run a "git status" in /some/repo, as:

	git -C /some/repo status builtin

I.e. ask "status" to report on the "builtin" directory. But since a
configuration such as this would result in a "struct string_list *"
with one element, whose "string" member is "NULL":

	[repo] list

We would, when constructing our command-line in
"builtin/for-each-repo.c"...

	strvec_pushl(&child.args, "-C", path, NULL);
	for (i = 0; i < argc; i++)
		strvec_push(&child.args, argv[i]);

...have that "path" be "NULL", and as strvec_pushl() stops when it
sees NULL we'd end with the first "argv" element as the argument to
the "-C" option, e.g.:

	git -C status builtin

I.e. we'd run the command "builtin" in the "status" directory.

In another context this might be an interesting security
vulnerability, but I think that this amounts to a nothingburger on
that front.

A hypothetical attacker would need to be able to write config for the
victim to run, if they're able to do that there's more interesting
attack vectors. See the "safe.directory" facility added in
8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).

An even more unlikely possibility would be an attacker able to
generate the config used for "for-each-repo --config=<key>", but
nothing else (e.g. an automated system producing that list).

Even in that case the attack vector is limited to the user running
commands whose name matches a directory that's interesting to the
attacker (e.g. a "log" directory in a repository). The second
argument (if any) of the command is likely to make git die without
doing anything interesting (e.g. "-p" to "log", there being no "-p"
built-in command to run).

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  |  2 +-
 t/t0068-for-each-repo.sh | 13 +++++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 224164addb3..ce8f7a99086 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	err = repo_config_get_string_multi(the_repository, config_key, &values);
 	if (err < 0)
 		usage_msg_optf(_("got bad config --config=%s"),
 			       for_each_repo_usage, options, config_key);
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 19ceaa546ea..48187a40d64 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -45,4 +45,17 @@ test_expect_success 'error on bad config keys' '
 	test_expect_code 129 git for-each-repo --config="'\''.b"
 '
 
+test_expect_success 'error on NULL value for config keys' '
+	cat >>.git/config <<-\EOF &&
+	[empty]
+		key
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''empty.key'\''
+	EOF
+	test_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.39.0.rc0.955.ge9b241be664


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v3 0/9] config API: make "multi" safe, fix numerous segfaults
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
                       ` (8 preceding siblings ...)
  2022-11-25  9:50     ` [PATCH v3 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
@ 2023-01-19  0:10     ` Glen Choo
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
  10 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-01-19  0:10 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

We covered this at Review Club this week (thanks for coming, Ævar!). You
can find the notes at:

  https://docs.google.com/document/d/14L8BAumGTpsXpjDY8VzZ4rRtpAjuGrFSRqn3stCuS_w/edit

The overall sentiment from the meeting was that this is a positive
direction for the config API to go in. My personal opinion is that this
series is close to mergeable and I had mostly minor comments.

Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:

> This series fixes numerous segfaults in config API users, because they
> didn't expect *_get_multi() to hand them a string_list with a NULL in
> it given config like "[a] key" (note, no "="'s).

As you mentioned in Review Club, this series also fixes a wart in
config.h where *_get_value_multi() returned a "struct string_list"
instead of an error code like all other getters. So this series is
technically doing two sort-of-different things...

> Ævar Arnfjörð Bjarmason (9):
>   for-each-repo tests: test bad --config keys
>   config tests: cover blind spots in git_die_config() tests
>   config tests: add "NULL" tests for *_get_value_multi()
>   versioncmp.c: refactor config reading next commit
>   config API: have *_multi() return an "int" and take a "dest"
>   for-each-repo: error on bad --config

Fix the wart..

>   config API users: test for *_get_value_multi() segfaults
>   config API: add "string" version of *_value_multi(), fix segfaults
>   for-each-repo: with bad config, don't conflate <path> and <cmd>

and introduce the better API that won't segfault, but I think it's okay
to have the series do both since they're closely related enough and the
latter is quite small anyway.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v3 2/9] config tests: cover blind spots in git_die_config() tests
  2022-11-25  9:50     ` [PATCH v3 2/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
@ 2023-01-19  0:15       ` Glen Choo
  0 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-01-19  0:15 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:

> There were no tests checking for the output of the git_die_config()
> function in the config API, added in 5a80e97c827 (config: add
> `git_die_config()` to the config-set API, 2014-08-07). We only tested
> "test_must_fail", but didn't assert the output.

It wasn't immediately obvious to me why this was relevant to this
series; but reading ahead to 5/9 shows that git_die_config() is a caller
of a *_get_value_multi() function that we are changing, so we want to
assert on the output so that we know that git_die_config() is still
doing the right thing (since test_must_fail alone won't tell us whether
we introduced bugs in git_die_config()).

Might be good to include that extra context, but I don't feel strongly
about it.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v3 3/9] config tests: add "NULL" tests for *_get_value_multi()
  2022-11-25  9:50     ` [PATCH v3 3/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2023-01-19  0:28       ` Glen Choo
  0 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-01-19  0:28 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:

> A less well known edge case in the config format is that keys can be
> value-less, a shorthand syntax for "true" boolean keys. I.e. these two
> are equivalent as far as "--type=bool" is concerned:
>
> 	[a]key
> 	[a]key = true
>
> But as far as our parser is concerned the values for these two are
> NULL, and "true". I.e. for a sequence like:
>
> 	[a]key=x
> 	[a]key
> 	[a]key=y
>
> We get a "struct string_list" with "string" members with ".string"
> values of:
>
> 	{ "x", NULL, "y" }
>
> This behavior goes back to the initial implementation of
> git_config_bool() in 17712991a59 (Add ".git/config" file parser,
> 2005-10-10).

I didn't know about this behavior before, actually. Thanks for the
explanation.

> When the "t/t1308-config-set.sh" tests were added in [1] only one of
> the three "(NULL)" lines in "t/helper/test-config.c" had any test
> coverage. This change adds tests that stress the remaining two.

I initially read this as testing that t/helper/test-config.c is doing
the right thing, which would be the antipattern of writing tests for our
tests.

During Review Club, you mentioned that the motivation was something
else, which IIRC is closer to exercising the internals of the configset
API, which makes sense to me, thought it would be helpful to clarify
that better in the commit message.

> +test_expect_success 'emit multi values from configset with NULL entry' '
> +	test_when_finished "rm -f my.config" &&
> +	cat >my.config <<-\EOF &&
> +	[a]key=x
> +	[a]key
> +	[a]key=y
> +	EOF
> +	cat >expect <<-\EOF &&
> +	x
> +	(NULL)
> +	y
> +	EOF
> +	test-tool config configset_get_value_multi a.key my.config >actual &&
> +	test_cmp expect actual
> +'

So if this meant to exercise configset_get_value_multi(), maybe it would
be even clearer to just say so in the test name, e.g.
'configset_get_value_multi with NULL entry'.

Side comment: by the end of the series, *_get_value_multi() has no
legitimate callers outside of config.c and the test code, and if we
remove it from config.h, this scenario wouldn't ever bother us in actual
`git` usage, but we probably still want to test for it since we want to
exercise the internals.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v3 5/9] config API: have *_multi() return an "int" and take a "dest"
  2022-11-25  9:50     ` [PATCH v3 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
@ 2023-01-19  0:50       ` Glen Choo
  0 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-01-19  0:50 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:

> Have the "git_configset_get_value_multi()" function and its siblings
> return an "int" and populate a "**dest" parameter like every other
> git_configset_get_*()" in the API.

Indeed, this is the only function that's inconsistent. Great to see that
it's being fixed :)

> As we'll see in in subsequent commits this fixes a blind spot in the
> API where it wasn't possible to tell whether a list was empty from
> whether a config key existed. We'll take advantage of that in
> subsequent commits, but for now we're faithfully converting existing
> API callers.

Sounds good.

> Most of this is straightforward, commentary on cases that stand out:

Thanks for this btw, I found it quite helpful for navigating the patch.
>
> - As we've tested for in a preceding commit we can rely on getting the
>   config list in git_die_config(), and as we need to handle the new
>   return value let's BUG() out if we can't acquire it.

This wasn't immediately clear to me; I'll explain more in the code.

> - In "builtin/for-each-ref.c" we could preserve the comment added in

I think you meant for-each-repo.

> @@ -45,14 +46,10 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
>  	if (!config_key)
>  		die(_("missing --config=<config>"));
>  
> -	values = repo_config_get_value_multi(the_repository,
> -					     config_key);
> -
> -	/*
> -	 * Do nothing on an empty list, which is equivalent to the case
> -	 * where the config variable does not exist at all.
> -	 */
> -	if (!values)
> +	err = repo_config_get_value_multi(the_repository, config_key, &values);
> +	if (err < 0)
> +		return 0;
> +	else if (err)
>  		return 0;

This conditional could be collapsed into "if (err)", but it's like this
because the next patch distinguishes between the two cases. Not really
worth the callout in commentary, but FYI for others who might be
wondering the same thing.

> diff --git a/config.c b/config.c
> index c058b2c70c3..0b07045ed8c 100644
> --- a/config.c
> +++ b/config.c
> @@ -2275,23 +2275,28 @@ void read_very_early_config(config_fn_t cb, void *data)
>  	config_with_options(cb, data, NULL, &opts);
>  }
>  
> -static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
> +static int configset_find_element(struct config_set *cs, const char *key,
> +				  struct config_set_element **dest)
>  {
>  	struct config_set_element k;
>  	struct config_set_element *found_entry;
>  	char *normalized_key;
> +	int ret;
> +
>  	/*
>  	 * `key` may come from the user, so normalize it before using it
>  	 * for querying entries from the hashmap.
>  	 */
> -	if (git_config_parse_key(key, &normalized_key, NULL))
> -		return NULL;
> +	ret = git_config_parse_key(key, &normalized_key, NULL);
> +	if (ret < 0)
> +		return ret;
>  
>  	hashmap_entry_init(&k.ent, strhash(normalized_key));
>  	k.key = normalized_key;
>  	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
>  	free(normalized_key);
> -	return found_entry;
> +	*dest = found_entry;
> +	return 0;
>  }
>  
>  static int configset_add_value(struct config_set *cs, const char *key, const char *value)
> @@ -2300,8 +2305,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha
>  	struct string_list_item *si;
>  	struct configset_list_item *l_item;
>  	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
> +	int ret;
>  
> -	e = configset_find_element(cs, key);
> +	ret = configset_find_element(cs, key, &e);
> +	if (ret < 0)
> +		return ret;
>  	/*
>  	 * Since the keys are being fed by git_config*() callback mechanism, they
>  	 * are already normalized. So simply add them without any further munging.
> @@ -2395,24 +2403,38 @@ int git_configset_add_file(struct config_set *cs, const char *filename)
>  int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
>  {
>  	const struct string_list *values = NULL;
> +	int ret;
> +
>  	/*
>  	 * Follows "last one wins" semantic, i.e., if there are multiple matches for the
>  	 * queried key in the files of the configset, the value returned will be the last
>  	 * value in the value list for that key.
>  	 */
> -	values = git_configset_get_value_multi(cs, key);
> +	ret = git_configset_get_value_multi(cs, key, &values);
>  
> -	if (!values)
> +	if (ret < 0)
> +		return ret;
> +	else if (!values)
>  		return 1;
>  	assert(values->nr > 0);
>  	*value = values->items[values->nr - 1].string;
>  	return 0;
>  }
>  
> -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
> +int git_configset_get_value_multi(struct config_set *cs, const char *key,
> +				  const struct string_list **dest)
>  {
> -	struct config_set_element *e = configset_find_element(cs, key);
> -	return e ? &e->value_list : NULL;
> +	struct config_set_element *e;
> +	int ret;
> +
> +	ret = configset_find_element(cs, key, &e);
> +	if (ret < 0)
> +		return ret;
> +	else if (!e)
> +		return 1;
> +	*dest = &e->value_list;
> +
> +	return 0;
>  }

The changes here and the call sites look quite straightforward.

>  int git_config_get_string(const char *key, char **dest)
> @@ -2819,7 +2841,8 @@ void git_die_config(const char *key, const char *err, ...)
>  		error_fn(err, params);
>  		va_end(params);
>  	}
> -	values = git_config_get_value_multi(key);
> +	if (git_config_get_value_multi(key, &values))
> +		BUG("for key '%s' we must have a value to report on", key);
>  	kv_info = values->items[values->nr - 1].util;
>  	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
>  }

Here is the BUG() call that wasn't immediately clear to me. What wasn't
obvious from the commentary is that this was an 'unhandled error'
before (we didn't check if the returned value was NULL). Arguably we
should have had this BUG call before, but we didn't enforce this until
we added RESULT_MUST_BE_USED.

And this should be a BUG(), and not e.g. error(), since git_die_config()
is meant to report bad config values, so git_config_get_value_multi()
should never fail if we've already managed to get a value, looks good.

> diff --git a/config.h b/config.h
> index ef9eade6414..7f6ce6f2fb5 100644
> --- a/config.h
> +++ b/config.h
> @@ -459,10 +459,18 @@ int git_configset_add_parameters(struct config_set *cs);
>  /**
>   * Finds and returns the value list, sorted in order of increasing priority
>   * for the configuration variable `key` and config set `cs`. When the
> - * configuration variable `key` is not found, returns NULL. The caller
> - * should not free or modify the returned pointer, as it is owned by the cache.
> + * configuration variable `key` is not found, returns 1 without touching
> + * `value`.
> + *
> + * The key will be parsed for validity with git_config_parse_key(), on
> + * error a negative value will be returned.
> + *
> + * The caller should not free or modify the returned pointer, as it is
> + * owned by the cache.
>   */
> -const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
> +RESULT_MUST_BE_USED
> +int git_configset_get_value_multi(struct config_set *cs, const char *key,
> +				  const struct string_list **dest);

Updated comments look good too.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v3 7/9] config API users: test for *_get_value_multi() segfaults
  2022-11-25  9:50     ` [PATCH v3 7/9] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
@ 2023-01-19  0:51       ` Glen Choo
  0 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-01-19  0:51 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:

> +test_expect_failure 'unregister with no value for maintenance.repo' '
> +	cp .git/config .git/config.orig &&
> +	test_when_finished mv .git/config.orig .git/config &&
> +
> +	cat >>.git/config <<-\EOF &&
> +	[maintenance]
> +		repo
> +	EOF
> +	cat >expect <<-\EOF &&
> +	error: missing value for '\''maintenance.repo'\''
> +	EOF
> +	git maintenance unregister &&
> +	git maintenance unregister --force
> +'
> +

Mechanical error: This 'expect' was probably meant for the next patch.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v3 8/9] config API: add "string" version of *_value_multi(), fix segfaults
  2022-11-25  9:50     ` [PATCH v3 8/9] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
@ 2023-01-19  1:03       ` Glen Choo
  0 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-01-19  1:03 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Jeff King, Taylor Blau,
	SZEDER Gábor, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:

> Fix numerous and mostly long-standing segfaults in consumers of
> the *_config_*value_multi() API. As discussed in the preceding commit
> an empty key in the config syntax yields a "NULL" string, which these
> users would give to strcmp() (or similar), resulting in segfaults.
>
> As this change shows, most users users of the *_config_*value_multi()
> API didn't really want such an an unsafe and low-level API, let's give
> them something with the safety of git_config_get_string() instead.

I think the low-level API argument makes sense. All of the other
*_get_*() functions perform some kind of validation, e.g.
config_parse_*() for non-string types and config_error_nonbool() for
strings. In effect, *_get_value_multi() was returning raw output from
the config parser without any concern for the caller.

> This fix is similar to what the *_string() functions and others
> acquired in[1] and [2]. Namely introducing and using a safer
> "*_get_string_multi()" variant of the low-level "_*value_multi()"
> function.

This suggests to me that we should really get rid of
*_get_value_multi(), since nobody outside of config.c should need it. I
don't think we'd ever end up in a situation where the caller wants the
raw strings from the config parser (unless we had a config
key which accepted values of different types? but that sounds like a
terrible mistake).

> There are now three remaining files using the low-level API:
>
> - Two cases in "builtin/submodule--helper.c", where it's used safely
>   to see if any config exists.
> - One in "builtin/for-each-repo.c", which we'll convert in a
>   subsequent commit.
> - The "t/helper/test-config.c" code added in [3].

As you noted, the only remaining non-test caller of the low-level API is
builtin/submodule--helper.c, which maybe we could safely convert anyway
and get rid of the API altogether. I'm okay with that being a leftover
bit, but maybe that's worth noting in the CL.

> The callback pattern being used here will make it easy to introduce
> e.g. a "multi" variant which coerces its values to "bool", "int",
> "path" etc.

I like that this is quite easily extensible, e.g.

> diff --git a/config.c b/config.c
> index 0b07045ed8c..9bd43189c02 100644
> --- a/config.c
> +++ b/config.c
> @@ -2437,6 +2437,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,
>  	return 0;
>  }
>  
> +static int check_multi_string(struct string_list_item *item, void *util)
> +{
> +	return item->string ? 0 : config_error_nonbool(util);
> +}
> +
> +int git_configset_get_string_multi(struct config_set *cs, const char *key,
> +				   const struct string_list **dest)
> +{
> +	int ret;
> +
> +	if ((ret = git_configset_get_value_multi(cs, key, dest)))
> +		return ret;
> +	if ((ret = for_each_string_list((struct string_list *)*dest,
> +					check_multi_string, (void *)key)))
> +		return ret;
> +
> +	return 0;
> +}

we could just use config_parse_<typename>() if we want to add, e.g.
*_get_bool_multi().

And as a reasonableness check, config_error_nonbool() is what we use to
validate the *_get_string() functions, so it makes sense to reuse it for
the string list version.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v4 0/9] config API: make "multi" safe, fix numerous segfaults
  2022-11-25  9:50   ` [PATCH v3 " Ævar Arnfjörð Bjarmason
                       ` (9 preceding siblings ...)
  2023-01-19  0:10     ` [PATCH v3 0/9] config API: make "multi" safe, fix numerous segfaults Glen Choo
@ 2023-02-02 13:27     ` Ævar Arnfjörð Bjarmason
  2023-02-02 13:27       ` [PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
                         ` (9 more replies)
  10 siblings, 10 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-02 13:27 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

This series fixes numerous segfaults in config API users, because they
didn't expect *_get_multi() to hand them a string_list with a NULL in
it given config like "[a] key" (note, no "="'s).

A larger general overview at v1[1], but note the API changes in
v2[2]. Changes since v3[3] (in particular thanks to this series being
featured in the Review Club[4]):

* It wasn't clear from the early test commit messages why certain
  things were being tested, if the test were exhaustive etc. Covered
  that.

* Rewrote 2/9 to use a test helper function, which gives us better
  coverage.

* The v1 included a "lookup_value" family of functions, as some of the
  "multi" users are only using the API to check key existence.

  The feedback on that was that the API naming din't make sense[6],
  which I agree with. Rather than having e.g. a git_config_exists() we
  introduce a git_config_get(), this is just like
  git_config_get_{value,string,int,...}(), except that we don't have a
  "dest" argument.

  Other than that it works the same way, i.e. the return value
  indicates existence (or other errors).

  This helps to make subsequent changes smaller, as our "real" API
  conversion no longer needs to deal with these callers.

* Various other small tidbits, see the range-diff below.

CI & branch for this topic at:
https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/

1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/
2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/
3. https://lore.kernel.org/git/cover-v3-0.9-00000000000-20221125T093158Z-avarab@gmail.com/
4. https://docs.google.com/document/d/14L8BAumGTpsXpjDY8VzZ4rRtpAjuGrFSRqn3stCuS_w
5. https://lore.kernel.org/git/patch-07.10-c01f7d85c94-20221026T151328Z-avarab@gmail.com/
6. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/

Ævar Arnfjörð Bjarmason (9):
  config tests: cover blind spots in git_die_config() tests
  config tests: add "NULL" tests for *_get_value_multi()
  config API: add and use a "git_config_get()" family of functions
  versioncmp.c: refactor config reading next commit
  config API: have *_multi() return an "int" and take a "dest"
  for-each-repo: error on bad --config
  config API users: test for *_get_value_multi() segfaults
  config API: add "string" version of *_value_multi(), fix segfaults
  for-each-repo: with bad config, don't conflate <path> and <cmd>

 builtin/for-each-repo.c              |  14 ++--
 builtin/gc.c                         |  15 ++--
 builtin/log.c                        |   6 +-
 builtin/submodule--helper.c          |   7 +-
 builtin/worktree.c                   |   3 +-
 config.c                             | 108 ++++++++++++++++++++++-----
 config.h                             |  69 ++++++++++++++---
 pack-bitmap.c                        |   6 +-
 submodule.c                          |   3 +-
 t/helper/test-config.c               |   6 +-
 t/t0068-for-each-repo.sh             |  19 +++++
 t/t1308-config-set.sh                |  41 ++++++++++
 t/t3309-notes-merge-auto-resolve.sh  |   7 +-
 t/t4202-log.sh                       |  15 ++++
 t/t5304-prune.sh                     |  12 ++-
 t/t5310-pack-bitmaps.sh              |  20 +++++
 t/t5552-skipping-fetch-negotiator.sh |  16 ++++
 t/t7004-tag.sh                       |  17 +++++
 t/t7413-submodule-is-active.sh       |  16 ++++
 t/t7900-maintenance.sh               |  38 ++++++++++
 versioncmp.c                         |  22 ++++--
 21 files changed, 388 insertions(+), 72 deletions(-)

Range-diff against v3:
 1:  5c8819ff388 <  -:  ----------- for-each-repo tests: test bad --config keys
 2:  3eb8da6086d !  1:  4ae56cab7c7 config tests: cover blind spots in git_die_config() tests
    @@ Commit message
         `git_die_config()` to the config-set API, 2014-08-07). We only tested
         "test_must_fail", but didn't assert the output.
     
    -    Let's check for that by extending the existing tests, and adding a new
    -    one for "fetch.negotiationAlgorithm" so that we have a test for a user
    -    of git_config_get_string*() calling git_die_config().
    +    We need tests for this because a subsequent commit will alter the
    +    return value of git_config_get_value_multi(), which is used to get the
    +    config values in the git_die_config() function. This test coverage
    +    helps to build confidence in that subsequent change.
     
    -    The other ones are testing:
    +    These tests cover different interactions with git_die_config():
     
    -    - For *-resolve.sh: A custom call to git_die_config(), or via
    -      git_config_get_notes_strategy()
    -    - For *-prune.sh: A call via git_config_get_expiry().
    +    - The "notes.mergeStrategy" test in
    +      "t/t3309-notes-merge-auto-resolve.sh" is a case where a function
    +      outside of config.c (git_config_get_notes_strategy()) calls
    +      git_die_config().
    +
    +    - The "gc.pruneExpire" test in "t5304-prune.sh" is a case where
    +      git_config_get_expiry() calls git_die_config(), covering a different
    +      "type" than the "string" test for "notes.mergeStrategy".
    +
    +    - The "fetch.negotiationAlgorithm" test in
    +      "t/t5552-skipping-fetch-negotiator.sh" is a case where
    +      git_config_get_string*() calls git_die_config().
     
         We also cover both the "from command-line config" and "in file..at
         line" cases here.
 3:  14b08dfc162 !  2:  1f0f8bdcde9 config tests: add "NULL" tests for *_get_value_multi()
    @@ Commit message
         git_config_bool() in 17712991a59 (Add ".git/config" file parser,
         2005-10-10).
     
    -    When the "t/t1308-config-set.sh" tests were added in [1] only one of
    -    the three "(NULL)" lines in "t/helper/test-config.c" had any test
    -    coverage. This change adds tests that stress the remaining two.
    +    When parts of the config_set API were tested for in [1] they didn't
    +    add coverage for 3/4 of the "(NULL)" cases handled in
    +    "t/helper/test-config.c". We'd test that case for "get_value", but not
    +    "get_value_multi", "configset_get_value" and
    +    "configset_get_value_multi".
    +
    +    We now cover all of those cases, which in turn expose the details of
    +    how this part of the config API works.
     
         1. 4c715ebb96a (test-config: add tests for the config_set API,
            2014-07-28)
    @@ t/t1308-config-set.sh: test_expect_success 'find multiple values' '
      	check_config get_value_multi case.baz sam bat hask
      '
      
    -+test_expect_success 'emit multi values from configset with NULL entry' '
    -+	test_when_finished "rm -f my.config" &&
    -+	cat >my.config <<-\EOF &&
    -+	[a]key=x
    -+	[a]key
    -+	[a]key=y
    -+	EOF
    -+	cat >expect <<-\EOF &&
    -+	x
    -+	(NULL)
    -+	y
    -+	EOF
    -+	test-tool config configset_get_value_multi a.key my.config >actual &&
    -+	test_cmp expect actual
    -+'
    ++test_NULL_in_multi () {
    ++	local op="$1" &&
    ++	local file="$2" &&
    ++
    ++	test_expect_success "$op: NULL value in config${file:+ in $file}" '
    ++		config="$file" &&
    ++		if test -z "$config"
    ++		then
    ++			config=.git/config &&
    ++			test_when_finished "mv $config.old $config" &&
    ++			mv "$config" "$config".old
    ++		fi &&
    ++
    ++		cat >"$config" <<-\EOF &&
    ++		[a]key=x
    ++		[a]key
    ++		[a]key=y
    ++		EOF
    ++		case "$op" in
    ++		*_multi)
    ++			cat >expect <<-\EOF
    ++			x
    ++			(NULL)
    ++			y
    ++			EOF
    ++			;;
    ++		*)
    ++			cat >expect <<-\EOF
    ++			y
    ++			EOF
    ++			;;
    ++		esac &&
    ++		test-tool config "$op" a.key $file >actual &&
    ++		test_cmp expect actual
    ++	'
    ++}
     +
    -+test_expect_success 'multi values from configset with a last NULL entry' '
    -+	test_when_finished "rm -f my.config" &&
    -+	cat >my.config <<-\EOF &&
    -+	[a]key=x
    -+	[a]key=y
    -+	[a]key
    -+	EOF
    -+	cat >expect <<-\EOF &&
    -+	(NULL)
    -+	EOF
    -+	test-tool config configset_get_value a.key my.config >actual &&
    -+	test_cmp expect actual
    -+'
    ++test_NULL_in_multi "get_value_multi"
    ++test_NULL_in_multi "configset_get_value" "my.config"
    ++test_NULL_in_multi "configset_get_value_multi" "my.config"
     +
      test_expect_success 'find value from a configset' '
      	cat >config2 <<-\EOF &&
 -:  ----------- >  3:  998b11ae4bc config API: add and use a "git_config_get()" family of functions
 4:  cb802b30cd8 !  4:  aae1d5c12a9 versioncmp.c: refactor config reading next commit
    @@ Commit message
         Refactor the reading of the versionSort.suffix and
         versionSort.prereleaseSuffix configuration variables to stay within
         the bounds of our CodingGuidelines when it comes to line length, and
    -    ta avoid repeating ourselves.
    +    to avoid repeating ourselves.
     
         Let's also split out the names of the config variables into variables
         of our own, so we don't have to repeat ourselves, and refactor the
 5:  e0e6ade3f38 !  5:  23449ff2c4e config API: have *_multi() return an "int" and take a "dest"
    @@ Commit message
         subsequent commits, but for now we're faithfully converting existing
         API callers.
     
    -    See [1] for the initial addition of "git_configset_get_value_multi()"
    -
    -    1. 3c8687a73ee (add `config_set` API for caching config-like files,
    -       2014-07-28).
    -
         A logical follow-up to this would be to change the various "*_get_*()"
         functions to ferry the git_configset_get_value() return value to their
         own callers, e.g. git_configset_get_int() returns "1" rather than
    @@ Commit message
     
         Most of this is straightforward, commentary on cases that stand out:
     
    -    - As we've tested for in a preceding commit we can rely on getting the
    -      config list in git_die_config(), and as we need to handle the new
    -      return value let's BUG() out if we can't acquire it.
    +    - To ensure that we'll properly use the return values of this function
    +      in the future we're using the "RESULT_MUST_BE_USED" macro introduced
    +      in [1].
     
    -    - In "builtin/for-each-ref.c" we could preserve the comment added in
    -      6c62f015520, but now that we're directly using the documented
    -      repo_config_get_value_multi() value it's just narrating something that
    -      should be obvious from the API use, so let's drop it.
    +      As git_die_config() now has to handle this return value let's have
    +      it BUG() if it can't find the config entry. As tested for in a
    +      preceding commit we can rely on getting the config list in
    +      git_die_config().
     
         - The loops after getting the "list" value in "builtin/gc.c" could
           also make use of "unsorted_string_list_has_string()" instead of using
           that loop, but let's leave that for now.
     
    -    - We have code e.g. in "builtin/submodule--helper.c" that only wants
    -      to check if a config key exists, and would be better served with
    -      another API, but let's keep using "git_configset_get_value_multi()"
    -      for now.
    -
         - In "versioncmp.c" we now use the return value of the functions,
    -      instead of checking if the lists are still non-NULL. This is strictly
    -      speaking unnecessary, but makes the API use consistent with the rest,
    -      but more importantly...
    +      instead of checking if the lists are still non-NULL.
     
    -    - ...because we always check our return values we can assert that with
    -      the RESULT_MUST_BE_USED macro added in 1e8697b5c4e (submodule--helper:
    -      check repo{_submodule,}_init() return values, 2022-09-01)
    +    1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
    +       return values, 2022-09-01),
     
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
      ## builtin/for-each-repo.c ##
    -@@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
    - 	static const char *config_key = NULL;
    - 	int i, result = 0;
    - 	const struct string_list *values;
    -+	int err;
    - 
    - 	const struct option options[] = {
    - 		OPT_STRING(0, "config", &config_key, N_("config"),
     @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
      	if (!config_key)
      		die(_("missing --config=<config>"));
    @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, cons
     -	values = repo_config_get_value_multi(the_repository,
     -					     config_key);
     -
    --	/*
    --	 * Do nothing on an empty list, which is equivalent to the case
    --	 * where the config variable does not exist at all.
    --	 */
    + 	/*
    + 	 * Do nothing on an empty list, which is equivalent to the case
    + 	 * where the config variable does not exist at all.
    + 	 */
     -	if (!values)
    -+	err = repo_config_get_value_multi(the_repository, config_key, &values);
    -+	if (err < 0)
    -+		return 0;
    -+	else if (err)
    ++	if (repo_config_get_value_multi(the_repository, config_key, &values))
      		return 0;
      
      	for (i = 0; !result && i < values->nr; i++)
     
      ## builtin/gc.c ##
     @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const char *prefix)
    - 	else
    + 	if (git_config_get("maintenance.strategy"))
      		git_config_set("maintenance.strategy", "incremental");
      
     -	list = git_config_get_value_multi(key);
    @@ builtin/log.c: static void set_default_decoration_filter(struct decoration_filte
      		for_each_string_list_item(item, config_exclude)
      			string_list_append(decoration_filter->exclude_ref_config_pattern,
     
    - ## builtin/submodule--helper.c ##
    -@@ builtin/submodule--helper.c: static int module_init(int argc, const char **argv, const char *prefix)
    - 		NULL
    - 	};
    - 	int ret = 1;
    -+	const struct string_list *values;
    - 
    - 	argc = parse_options(argc, argv, prefix, module_init_options,
    - 			     git_submodule_helper_usage, 0);
    -@@ builtin/submodule--helper.c: static int module_init(int argc, const char **argv, const char *prefix)
    - 	 * If there are no path args and submodule.active is set then,
    - 	 * by default, only initialize 'active' modules.
    - 	 */
    --	if (!argc && git_config_get_value_multi("submodule.active"))
    -+	if (!argc && !git_config_get_value_multi("submodule.active", &values))
    - 		module_list_active(&list);
    - 
    - 	info.prefix = prefix;
    -@@ builtin/submodule--helper.c: static int module_update(int argc, const char **argv, const char *prefix)
    - 	if (opt.init) {
    - 		struct module_list list = MODULE_LIST_INIT;
    - 		struct init_cb info = INIT_CB_INIT;
    -+		const struct string_list *values;
    - 
    - 		if (module_list_compute(argv, opt.prefix,
    - 					&pathspec2, &list) < 0) {
    -@@ builtin/submodule--helper.c: static int module_update(int argc, const char **argv, const char *prefix)
    - 		 * If there are no path args and submodule.active is set then,
    - 		 * by default, only initialize 'active' modules.
    - 		 */
    --		if (!argc && git_config_get_value_multi("submodule.active"))
    -+		if (!argc && !git_config_get_value_multi("submodule.active",
    -+							 &values))
    - 			module_list_active(&list);
    - 
    - 		info.prefix = opt.prefix;
    -
      ## config.c ##
    -@@ config.c: void read_very_early_config(config_fn_t cb, void *data)
    - 	config_with_options(cb, data, NULL, &opts);
    - }
    - 
    --static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
    -+static int configset_find_element(struct config_set *cs, const char *key,
    -+				  struct config_set_element **dest)
    - {
    - 	struct config_set_element k;
    - 	struct config_set_element *found_entry;
    - 	char *normalized_key;
    -+	int ret;
    -+
    - 	/*
    - 	 * `key` may come from the user, so normalize it before using it
    - 	 * for querying entries from the hashmap.
    - 	 */
    --	if (git_config_parse_key(key, &normalized_key, NULL))
    --		return NULL;
    -+	ret = git_config_parse_key(key, &normalized_key, NULL);
    -+	if (ret < 0)
    -+		return ret;
    - 
    - 	hashmap_entry_init(&k.ent, strhash(normalized_key));
    - 	k.key = normalized_key;
    - 	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
    - 	free(normalized_key);
    --	return found_entry;
    -+	*dest = found_entry;
    -+	return 0;
    - }
    - 
    - static int configset_add_value(struct config_set *cs, const char *key, const char *value)
    -@@ config.c: static int configset_add_value(struct config_set *cs, const char *key, const cha
    - 	struct string_list_item *si;
    - 	struct configset_list_item *l_item;
    - 	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
    -+	int ret;
    - 
    --	e = configset_find_element(cs, key);
    -+	ret = configset_find_element(cs, key, &e);
    -+	if (ret < 0)
    -+		return ret;
    - 	/*
    - 	 * Since the keys are being fed by git_config*() callback mechanism, they
    - 	 * are already normalized. So simply add them without any further munging.
     @@ config.c: int git_configset_add_file(struct config_set *cs, const char *filename)
      int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
      {
    @@ config.c: int git_configset_add_file(struct config_set *cs, const char *filename
      	 * value in the value list for that key.
      	 */
     -	values = git_configset_get_value_multi(cs, key);
    -+	ret = git_configset_get_value_multi(cs, key, &values);
    ++	if ((ret = git_configset_get_value_multi(cs, key, &values)))
    ++		return ret;
      
     -	if (!values)
    -+	if (ret < 0)
    -+		return ret;
    -+	else if (!values)
    - 		return 1;
    +-		return 1;
      	assert(values->nr > 0);
      	*value = values->items[values->nr - 1].string;
      	return 0;
    @@ config.c: int git_configset_add_file(struct config_set *cs, const char *filename
     +int git_configset_get_value_multi(struct config_set *cs, const char *key,
     +				  const struct string_list **dest)
      {
    --	struct config_set_element *e = configset_find_element(cs, key);
    --	return e ? &e->value_list : NULL;
    -+	struct config_set_element *e;
    + 	struct config_set_element *e;
     +	int ret;
    -+
    -+	ret = configset_find_element(cs, key, &e);
    -+	if (ret < 0)
    + 
    +-	if (configset_find_element(cs, key, &e))
    +-		return NULL;
    ++	if ((ret = configset_find_element(cs, key, &e)))
     +		return ret;
    -+	else if (!e)
    + 	else if (!e)
    +-		return NULL;
    +-	return &e->value_list;
     +		return 1;
     +	*dest = &e->value_list;
     +
     +	return 0;
      }
      
    - int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
    + int git_configset_get(struct config_set *cs, const char *key)
     @@ config.c: int repo_config_get_value(struct repository *repo,
      	return git_configset_get_value(repo->config, key, value);
      }
    @@ config.h: int git_configset_add_parameters(struct config_set *cs);
      
      /**
       * Clears `config_set` structure, removes all saved variable-value pairs.
    -@@ config.h: struct repository;
    - void repo_config(struct repository *repo, config_fn_t fn, void *data);
    +@@ config.h: RESULT_MUST_BE_USED
    + int repo_config_get(struct repository *repo, const char *key);
      int repo_config_get_value(struct repository *repo,
      			  const char *key, const char **value);
     -const struct string_list *repo_config_get_value_multi(struct repository *repo,
 6:  06d502bc577 !  6:  17c1218e74c for-each-repo: error on bad --config
    @@ Commit message
         2021-01-08) this command wants to ignore a non-existing config key,
         but let's not conflate that with bad config.
     
    +    Before this, all these added tests would pass with an exit code of 0.
    +
    +    We could preserve the comment added in 6c62f015520, but now that we're
    +    directly using the documented repo_config_get_value_multi() value it's
    +    just narrating something that should be obvious from the API use, so
    +    let's drop it.
    +
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
      ## builtin/for-each-repo.c ##
     @@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
    + 	static const char *config_key = NULL;
    + 	int i, result = 0;
    + 	const struct string_list *values;
    ++	int err;
    + 
    + 	const struct option options[] = {
    + 		OPT_STRING(0, "config", &config_key, N_("config"),
    +@@ builtin/for-each-repo.c: int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
    + 	if (!config_key)
    + 		die(_("missing --config=<config>"));
      
    - 	err = repo_config_get_value_multi(the_repository, config_key, &values);
    - 	if (err < 0)
    --		return 0;
    +-	/*
    +-	 * Do nothing on an empty list, which is equivalent to the case
    +-	 * where the config variable does not exist at all.
    +-	 */
    +-	if (repo_config_get_value_multi(the_repository, config_key, &values))
    ++	err = repo_config_get_value_multi(the_repository, config_key, &values);
    ++	if (err < 0)
     +		usage_msg_optf(_("got bad config --config=%s"),
     +			       for_each_repo_usage, options, config_key);
    - 	else if (err)
    ++	else if (err)
      		return 0;
      
    + 	for (i = 0; !result && i < values->nr; i++)
     
      ## t/t0068-for-each-repo.sh ##
     @@ t/t0068-for-each-repo.sh: test_expect_success 'do nothing on empty config' '
      	git for-each-repo --config=bogus.config -- help --no-such-option
      '
      
    --test_expect_success 'bad config keys' '
    --	git for-each-repo --config=a &&
    --	git for-each-repo --config=a.b. &&
    --	git for-each-repo --config="'\''.b"
     +test_expect_success 'error on bad config keys' '
     +	test_expect_code 129 git for-each-repo --config=a &&
     +	test_expect_code 129 git for-each-repo --config=a.b. &&
     +	test_expect_code 129 git for-each-repo --config="'\''.b"
    - '
    - 
    ++'
    ++
      test_done
 7:  f35aacef4ca !  7:  7fc91eaf747 config API users: test for *_get_value_multi() segfaults
    @@ Metadata
      ## Commit message ##
         config API users: test for *_get_value_multi() segfaults
     
    -    As we'll discus in the subsequent commit these tests all
    +    As we'll discuss in the subsequent commit these tests all
         show *_get_value_multi() API users unable to handle there being a
         value-less key in the config, which is represented with a "NULL" for
         that entry in the "string" member of the returned "struct
         string_list", causing a segfault.
     
    +    These added tests exhaustively test for that issue, as we'll see in a
    +    subsequent commit we'll need to change all of the API users
    +    of *_get_value_multi(). These cases were discovered by triggering each
    +    one individually, and then adding these tests.
    +
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
      ## t/t4202-log.sh ##
    @@ t/t7900-maintenance.sh: test_expect_success 'register and unregister' '
     +	[maintenance]
     +		repo
     +	EOF
    -+	cat >expect <<-\EOF &&
    -+	error: missing value for '\''maintenance.repo'\''
    -+	EOF
     +	git maintenance unregister &&
     +	git maintenance unregister --force
     +'
 8:  b45189b4624 !  8:  a391ee17617 config API: add "string" version of *_value_multi(), fix segfaults
    @@ Commit message
     
         - Two cases in "builtin/submodule--helper.c", where it's used safely
           to see if any config exists.
    +
    +      We could refactor these away from "multi" to some "does it exist?"
    +      function, as [4] did, but as that's orthogonal to the "string"
    +      safety we're introducing here let's leave them for now.
    +
         - One in "builtin/for-each-repo.c", which we'll convert in a
           subsequent commit.
    -    - The "t/helper/test-config.c" code added in [3].
    +
    +    - The "t/helper/test-config.c" code added in [4].
     
         As seen in the preceding commit we need to give the
         "t/helper/test-config.c" caller these "NULL" entries.
    @@ Commit message
            2008-02-11)
         2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,
            2008-02-11).
    -    3. 4c715ebb96a (test-config: add tests for the config_set API,
    +    3. https://lore.kernel.org/git/patch-07.10-c01f7d85c94-20221026T151328Z-avarab@gmail.com/
    +    4. 4c715ebb96a (test-config: add tests for the config_set API,
            2014-07-28)
     
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
      ## builtin/gc.c ##
     @@ builtin/gc.c: static int maintenance_register(int argc, const char **argv, const char *prefix)
    - 	else
    + 	if (git_config_get("maintenance.strategy"))
      		git_config_set("maintenance.strategy", "incremental");
      
     -	if (!git_config_get_value_multi(key, &list)) {
    @@ config.c: int git_configset_get_value_multi(struct config_set *cs, const char *k
     +	return 0;
     +}
     +
    - int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
    + int git_configset_get(struct config_set *cs, const char *key)
      {
    - 	const char *value;
    + 	struct config_set_element *e;
     @@ config.c: int repo_config_get_value_multi(struct repository *repo, const char *key,
      	return git_configset_get_value_multi(repo->config, key, dest);
      }
    @@ t/t7900-maintenance.sh: test_expect_failure 'register with no value for maintena
      	test_when_finished mv .git/config.orig .git/config &&
      
     @@ t/t7900-maintenance.sh: test_expect_failure 'unregister with no value for maintenance.repo' '
    - 	cat >expect <<-\EOF &&
    - 	error: missing value for '\''maintenance.repo'\''
    + 	[maintenance]
    + 		repo
      	EOF
     -	git maintenance unregister &&
     -	git maintenance unregister --force
    ++	cat >expect <<-\EOF &&
    ++	error: missing value for '\''maintenance.repo'\''
    ++	EOF
     +	test_expect_code 128 git maintenance unregister 2>actual.raw &&
     +	grep ^error actual.raw >actual &&
     +	test_cmp expect actual &&
 9:  58ead3ca555 =  9:  c7a5f5b4133 for-each-repo: with bad config, don't conflate <path> and <cmd>
-- 
2.39.1.1397.g8c8c074958d


^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
@ 2023-02-02 13:27       ` Ævar Arnfjörð Bjarmason
  2023-02-03  1:22         ` Junio C Hamano
  2023-02-06  8:31         ` Glen Choo
  2023-02-02 13:27       ` [PATCH v4 2/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
                         ` (8 subsequent siblings)
  9 siblings, 2 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-02 13:27 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

There were no tests checking for the output of the git_die_config()
function in the config API, added in 5a80e97c827 (config: add
`git_die_config()` to the config-set API, 2014-08-07). We only tested
"test_must_fail", but didn't assert the output.

We need tests for this because a subsequent commit will alter the
return value of git_config_get_value_multi(), which is used to get the
config values in the git_die_config() function. This test coverage
helps to build confidence in that subsequent change.

These tests cover different interactions with git_die_config():

- The "notes.mergeStrategy" test in
  "t/t3309-notes-merge-auto-resolve.sh" is a case where a function
  outside of config.c (git_config_get_notes_strategy()) calls
  git_die_config().

- The "gc.pruneExpire" test in "t5304-prune.sh" is a case where
  git_config_get_expiry() calls git_die_config(), covering a different
  "type" than the "string" test for "notes.mergeStrategy".

- The "fetch.negotiationAlgorithm" test in
  "t/t5552-skipping-fetch-negotiator.sh" is a case where
  git_config_get_string*() calls git_die_config().

We also cover both the "from command-line config" and "in file..at
line" cases here.

The clobbering of existing ".git/config" files here is so that we're
not implicitly testing the line count of the default config.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-
 t/t5304-prune.sh                     | 12 ++++++++++--
 t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh
index 141d3e4ca4d..9bd5dbf341f 100755
--- a/t/t3309-notes-merge-auto-resolve.sh
+++ b/t/t3309-notes-merge-auto-resolve.sh
@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '
 
 test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '
 	git config core.notesRef refs/notes/y &&
-	test_must_fail git -c notes.mergeStrategy="foo" notes merge z &&
+	cat >expect <<-\EOF &&
+	error: unknown notes merge strategy foo
+	fatal: unable to parse '\''notes.mergeStrategy'\'' from command-line config
+	EOF
+	test_must_fail git -c notes.mergeStrategy="foo" notes merge z 2>actual &&
+	test_cmp expect actual &&
 	# Verify no changes (y)
 	verify_notes y y
 '
diff --git a/t/t5304-prune.sh b/t/t5304-prune.sh
index d65a5f94b4b..5500dd08426 100755
--- a/t/t5304-prune.sh
+++ b/t/t5304-prune.sh
@@ -72,8 +72,16 @@ test_expect_success 'gc: implicit prune --expire' '
 '
 
 test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '
-	git config gc.pruneExpire invalid &&
-	test_must_fail git gc
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	>repo/.git/config &&
+	git -C repo config gc.pruneExpire invalid &&
+	cat >expect <<-\EOF &&
+	error: Invalid gc.pruneexpire: '\''invalid'\''
+	fatal: bad config variable '\''gc.pruneexpire'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_must_fail git -C repo gc 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'gc: start with ok gc.pruneExpire' '
diff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh
index 165427d57e5..b55a9f65e6b 100755
--- a/t/t5552-skipping-fetch-negotiator.sh
+++ b/t/t5552-skipping-fetch-negotiator.sh
@@ -3,6 +3,22 @@
 test_description='test skipping fetch negotiator'
 . ./test-lib.sh
 
+test_expect_success 'fetch.negotiationalgorithm config' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	cat >repo/.git/config <<-\EOF &&
+	[fetch]
+	negotiationAlgorithm
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''fetch.negotiationalgorithm'\''
+	fatal: bad config variable '\''fetch.negotiationalgorithm'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_expect_code 128 git -C repo fetch >out 2>actual &&
+	test_must_be_empty out &&
+	test_cmp expect actual
+'
+
 have_sent () {
 	while test "$#" -ne 0
 	do
-- 
2.39.1.1397.g8c8c074958d


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v4 2/9] config tests: add "NULL" tests for *_get_value_multi()
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
  2023-02-02 13:27       ` [PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
@ 2023-02-02 13:27       ` Ævar Arnfjörð Bjarmason
  2023-02-02 23:12         ` Junio C Hamano
  2023-02-06 10:40         ` Glen Choo
  2023-02-02 13:27       ` [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
                         ` (7 subsequent siblings)
  9 siblings, 2 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-02 13:27 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

A less well known edge case in the config format is that keys can be
value-less, a shorthand syntax for "true" boolean keys. I.e. these two
are equivalent as far as "--type=bool" is concerned:

	[a]key
	[a]key = true

But as far as our parser is concerned the values for these two are
NULL, and "true". I.e. for a sequence like:

	[a]key=x
	[a]key
	[a]key=y

We get a "struct string_list" with "string" members with ".string"
values of:

	{ "x", NULL, "y" }

This behavior goes back to the initial implementation of
git_config_bool() in 17712991a59 (Add ".git/config" file parser,
2005-10-10).

When parts of the config_set API were tested for in [1] they didn't
add coverage for 3/4 of the "(NULL)" cases handled in
"t/helper/test-config.c". We'd test that case for "get_value", but not
"get_value_multi", "configset_get_value" and
"configset_get_value_multi".

We now cover all of those cases, which in turn expose the details of
how this part of the config API works.

1. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t1308-config-set.sh | 41 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index b38e158d3b2..b172565f92a 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -146,6 +146,47 @@ test_expect_success 'find multiple values' '
 	check_config get_value_multi case.baz sam bat hask
 '
 
+test_NULL_in_multi () {
+	local op="$1" &&
+	local file="$2" &&
+
+	test_expect_success "$op: NULL value in config${file:+ in $file}" '
+		config="$file" &&
+		if test -z "$config"
+		then
+			config=.git/config &&
+			test_when_finished "mv $config.old $config" &&
+			mv "$config" "$config".old
+		fi &&
+
+		cat >"$config" <<-\EOF &&
+		[a]key=x
+		[a]key
+		[a]key=y
+		EOF
+		case "$op" in
+		*_multi)
+			cat >expect <<-\EOF
+			x
+			(NULL)
+			y
+			EOF
+			;;
+		*)
+			cat >expect <<-\EOF
+			y
+			EOF
+			;;
+		esac &&
+		test-tool config "$op" a.key $file >actual &&
+		test_cmp expect actual
+	'
+}
+
+test_NULL_in_multi "get_value_multi"
+test_NULL_in_multi "configset_get_value" "my.config"
+test_NULL_in_multi "configset_get_value_multi" "my.config"
+
 test_expect_success 'find value from a configset' '
 	cat >config2 <<-\EOF &&
 	[case]
-- 
2.39.1.1397.g8c8c074958d


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
  2023-02-02 13:27       ` [PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
  2023-02-02 13:27       ` [PATCH v4 2/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2023-02-02 13:27       ` Ævar Arnfjörð Bjarmason
  2023-02-02 23:56         ` Junio C Hamano
                           ` (2 more replies)
  2023-02-02 13:27       ` [PATCH v4 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
                         ` (6 subsequent siblings)
  9 siblings, 3 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-02 13:27 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

We already have the basic "git_config_get_value()" function and its
"repo_*" and "configset" siblings to get a given "key" and assign the
last key found to a provided "value".

But some callers don't care about that value, but just want to use the
return value of the "get_value()" function to check whether the key
exist (or another non-zero return value).

The immediate motivation for this is that a subsequent commit will
need to change all callers of the "*_get_value_multi()" family of
functions. In two cases here we (ab)used it to check whether we had
any values for the given key, but didn't care about the return value.

The rest of the callers here used various other config API functions
to do the same, all of which resolved to the same underlying functions
to provide the answer.

Some of these were using either git_config_get_string() or
git_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a
configure_added_submodule() leak, 2022-09-01) for a recent example. We
can now use a helper function that doesn't require a throwaway
variable.

We could have changed git_configset_get_value_multi() to accept a
"NULL" as a "dest" for all callers, but let's avoid changing the
behavior of existing API users. Having an "unused" value that we throw
away internal to config.c is cheap.

Another name for this function could have been
"*_config_key_exists()", as suggested in [1]. That would work for all
of these callers, and would currently be equivalent to this function,
as the git_configset_get_value() API normalizes all non-zero return
values to a "1".

But adding that API would set us up to lose information, as e.g. if
git_config_parse_key() in the underlying configset_find_element()
fails we'd like to return -1, not 1.

Let's change the underlying configset_find_element() function to
support this use-case, we'll make further use of it in a subsequent
commit where the git_configset_get_value_multi() function itself will
expose this new return value.

1. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                |  5 +---
 builtin/submodule--helper.c |  7 +++---
 builtin/worktree.c          |  3 +--
 config.c                    | 50 +++++++++++++++++++++++++++++++------
 config.h                    | 19 +++++++++++++-
 5 files changed, 66 insertions(+), 18 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 02455fdcd73..e38d1783f30 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1493,7 +1493,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	};
 	int found = 0;
 	const char *key = "maintenance.repo";
-	char *config_value;
 	char *maintpath = get_maintpath();
 	struct string_list_item *item;
 	const struct string_list *list;
@@ -1508,9 +1507,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	git_config_set("maintenance.auto", "false");
 
 	/* Set maintenance strategy, if unset */
-	if (!git_config_get_string("maintenance.strategy", &config_value))
-		free(config_value);
-	else
+	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
 	list = git_config_get_value_multi(key);
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 4c173d8b37a..2278e8c91cb 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 	 * If there are no path args and submodule.active is set then,
 	 * by default, only initialize 'active' modules.
 	 */
-	if (!argc && git_config_get_value_multi("submodule.active"))
+	if (!argc && !git_config_get("submodule.active"))
 		module_list_active(&list);
 
 	info.prefix = prefix;
@@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 		 * If there are no path args and submodule.active is set then,
 		 * by default, only initialize 'active' modules.
 		 */
-		if (!argc && git_config_get_value_multi("submodule.active"))
+		if (!argc && !git_config_get("submodule.active"))
 			module_list_active(&list);
 
 		info.prefix = opt.prefix;
@@ -3140,7 +3140,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con
 static void configure_added_submodule(struct add_data *add_data)
 {
 	char *key;
-	const char *val;
 	struct child_process add_submod = CHILD_PROCESS_INIT;
 	struct child_process add_gitmodules = CHILD_PROCESS_INIT;
 
@@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)
 	 * is_submodule_active(), since that function needs to find
 	 * out the value of "submodule.active" again anyway.
 	 */
-	if (!git_config_get_string_tmp("submodule.active", &val)) {
+	if (!git_config_get("submodule.active")) {
 		/*
 		 * If the submodule being added isn't already covered by the
 		 * current configured pathspec, set the submodule's active flag
diff --git a/builtin/worktree.c b/builtin/worktree.c
index f51c40f1e1e..6ba42d4ad20 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -319,7 +319,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 
 	if (file_exists(from_file)) {
 		struct config_set cs = { { 0 } };
-		const char *core_worktree;
 		int bare;
 
 		if (safe_create_leading_directories(to_file) ||
@@ -338,7 +337,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 				to_file, "core.bare", NULL, "true", 0))
 			error(_("failed to unset '%s' in '%s'"),
 				"core.bare", to_file);
-		if (!git_configset_get_value(&cs, "core.worktree", &core_worktree) &&
+		if (!git_configset_get(&cs, "core.worktree") &&
 			git_config_set_in_file_gently(to_file,
 							"core.worktree", NULL))
 			error(_("failed to unset '%s' in '%s'"),
diff --git a/config.c b/config.c
index 00090a32fc3..b88da70c664 100644
--- a/config.c
+++ b/config.c
@@ -2289,23 +2289,28 @@ void read_very_early_config(config_fn_t cb, void *data)
 	config_with_options(cb, data, NULL, &opts);
 }
 
-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
+static int configset_find_element(struct config_set *cs, const char *key,
+				  struct config_set_element **dest)
 {
 	struct config_set_element k;
 	struct config_set_element *found_entry;
 	char *normalized_key;
+	int ret;
+
 	/*
 	 * `key` may come from the user, so normalize it before using it
 	 * for querying entries from the hashmap.
 	 */
-	if (git_config_parse_key(key, &normalized_key, NULL))
-		return NULL;
+	ret = git_config_parse_key(key, &normalized_key, NULL);
+	if (ret)
+		return ret;
 
 	hashmap_entry_init(&k.ent, strhash(normalized_key));
 	k.key = normalized_key;
 	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
 	free(normalized_key);
-	return found_entry;
+	*dest = found_entry;
+	return 0;
 }
 
 static int configset_add_value(struct config_set *cs, const char *key, const char *value)
@@ -2314,8 +2319,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha
 	struct string_list_item *si;
 	struct configset_list_item *l_item;
 	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
+	int ret;
 
-	e = configset_find_element(cs, key);
+	ret = configset_find_element(cs, key, &e);
+	if (ret)
+		return ret;
 	/*
 	 * Since the keys are being fed by git_config*() callback mechanism, they
 	 * are already normalized. So simply add them without any further munging.
@@ -2425,8 +2433,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *
 
 const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
 {
-	struct config_set_element *e = configset_find_element(cs, key);
-	return e ? &e->value_list : NULL;
+	struct config_set_element *e;
+
+	if (configset_find_element(cs, key, &e))
+		return NULL;
+	else if (!e)
+		return NULL;
+	return &e->value_list;
+}
+
+int git_configset_get(struct config_set *cs, const char *key)
+{
+	struct config_set_element *e;
+	int ret;
+
+	if ((ret = configset_find_element(cs, key, &e)))
+		return ret;
+	else if (!e)
+		return 1;
+	return 0;
 }
 
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
@@ -2565,6 +2590,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)
 	configset_iter(repo->config, fn, data);
 }
 
+int repo_config_get(struct repository *repo, const char *key)
+{
+	git_config_check_init(repo);
+	return git_configset_get(repo->config, key);
+}
+
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value)
 {
@@ -2679,6 +2710,11 @@ void git_config_clear(void)
 	repo_config_clear(the_repository);
 }
 
+int git_config_get(const char *key)
+{
+	return repo_config_get(the_repository, key);
+}
+
 int git_config_get_value(const char *key, const char **value)
 {
 	return repo_config_get_value(the_repository, key, value);
diff --git a/config.h b/config.h
index ef9eade6414..04c5e594015 100644
--- a/config.h
+++ b/config.h
@@ -471,9 +471,12 @@ void git_configset_clear(struct config_set *cs);
 
 /*
  * These functions return 1 if not found, and 0 if found, leaving the found
- * value in the 'dest' pointer.
+ * value in the 'dest' pointer (if any).
  */
 
+RESULT_MUST_BE_USED
+int git_configset_get(struct config_set *cs, const char *key);
+
 /*
  * Finds the highest-priority value for the configuration variable `key`
  * and config set `cs`, stores the pointer to it in `value` and returns 0.
@@ -494,6 +497,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha
 /* Functions for reading a repository's config */
 struct repository;
 void repo_config(struct repository *repo, config_fn_t fn, void *data);
+
+/**
+ * Run only the discover part of the repo_config_get_*() functions
+ * below, in addition to 1 if not found, returns negative values on
+ * error (e.g. if the key itself is invalid).
+ */
+RESULT_MUST_BE_USED
+int repo_config_get(struct repository *repo, const char *key);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
 const struct string_list *repo_config_get_value_multi(struct repository *repo,
@@ -530,8 +541,14 @@ void git_protected_config(config_fn_t fn, void *data);
  * manner, the config API provides two functions `git_config_get_value`
  * and `git_config_get_value_multi`. They both read values from an internal
  * cache generated previously from reading the config files.
+ *
+ * For those git_config_get*() functions that aren't documented,
+ * consult the corresponding repo_config_get*() function's
+ * documentation.
  */
 
+int git_config_get(const char *key);
+
 /**
  * Finds the highest-priority value for the configuration variable `key`,
  * stores the pointer to it in `value` and returns 0. When the
-- 
2.39.1.1397.g8c8c074958d


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v4 4/9] versioncmp.c: refactor config reading next commit
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
                         ` (2 preceding siblings ...)
  2023-02-02 13:27       ` [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
@ 2023-02-02 13:27       ` Ævar Arnfjörð Bjarmason
  2023-02-03 21:52         ` Junio C Hamano
  2023-02-02 13:27       ` [PATCH v4 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
                         ` (5 subsequent siblings)
  9 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-02 13:27 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Refactor the reading of the versionSort.suffix and
versionSort.prereleaseSuffix configuration variables to stay within
the bounds of our CodingGuidelines when it comes to line length, and
to avoid repeating ourselves.

Let's also split out the names of the config variables into variables
of our own, so we don't have to repeat ourselves, and refactor the
nested if/else to avoid indenting it, and the existing bracing style
issue.

This all helps with the subsequent commit, where we'll need to start
checking different git_config_get_value_multi() return value. See
c026557a373 (versioncmp: generalize version sort suffix reordering,
2016-12-08) for the original implementation of most of this.

Moving the "initialized = 1" assignment allows us to move some of this
to the variable declarations in the subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 versioncmp.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/versioncmp.c b/versioncmp.c
index 069ee94a4d7..323f5d35ea8 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)
 	}
 
 	if (!initialized) {
-		const struct string_list *deprecated_prereleases;
+		const char *const newk = "versionsort.suffix";
+		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *oldl;
+
+		prereleases = git_config_get_value_multi(newk);
+		oldl = git_config_get_value_multi(oldk);
+		if (prereleases && oldl)
+			warning("ignoring %s because %s is set", oldk, newk);
+		else if (!prereleases)
+			prereleases = oldl;
+
 		initialized = 1;
-		prereleases = git_config_get_value_multi("versionsort.suffix");
-		deprecated_prereleases = git_config_get_value_multi("versionsort.prereleasesuffix");
-		if (prereleases) {
-			if (deprecated_prereleases)
-				warning("ignoring versionsort.prereleasesuffix because versionsort.suffix is set");
-		} else
-			prereleases = deprecated_prereleases;
 	}
 	if (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,
 					    &diff))
-- 
2.39.1.1397.g8c8c074958d


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v4 5/9] config API: have *_multi() return an "int" and take a "dest"
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
                         ` (3 preceding siblings ...)
  2023-02-02 13:27       ` [PATCH v4 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
@ 2023-02-02 13:27       ` Ævar Arnfjörð Bjarmason
  2023-02-02 13:27       ` [PATCH v4 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
                         ` (4 subsequent siblings)
  9 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-02 13:27 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Have the "git_configset_get_value_multi()" function and its siblings
return an "int" and populate a "**dest" parameter like every other
git_configset_get_*()" in the API.

As we'll see in in subsequent commits this fixes a blind spot in the
API where it wasn't possible to tell whether a list was empty from
whether a config key existed. We'll take advantage of that in
subsequent commits, but for now we're faithfully converting existing
API callers.

A logical follow-up to this would be to change the various "*_get_*()"
functions to ferry the git_configset_get_value() return value to their
own callers, e.g. git_configset_get_int() returns "1" rather than
ferrying up the "-1" that "git_configset_get_value()" might return,
but that's not being done in this series

Most of this is straightforward, commentary on cases that stand out:

- To ensure that we'll properly use the return values of this function
  in the future we're using the "RESULT_MUST_BE_USED" macro introduced
  in [1].

  As git_die_config() now has to handle this return value let's have
  it BUG() if it can't find the config entry. As tested for in a
  preceding commit we can rely on getting the config list in
  git_die_config().

- The loops after getting the "list" value in "builtin/gc.c" could
  also make use of "unsorted_string_list_has_string()" instead of using
  that loop, but let's leave that for now.

- In "versioncmp.c" we now use the return value of the functions,
  instead of checking if the lists are still non-NULL.

1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
   return values, 2022-09-01),

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c |  5 +----
 builtin/gc.c            | 10 ++++------
 builtin/log.c           |  6 +++---
 config.c                | 34 ++++++++++++++++++++--------------
 config.h                | 29 +++++++++++++++++++++--------
 pack-bitmap.c           |  6 +++++-
 submodule.c             |  3 +--
 t/helper/test-config.c  |  6 ++----
 versioncmp.c            | 11 +++++++----
 9 files changed, 64 insertions(+), 46 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 6aeac371488..fd0e7739e6a 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -45,14 +45,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	values = repo_config_get_value_multi(the_repository,
-					     config_key);
-
 	/*
 	 * Do nothing on an empty list, which is equivalent to the case
 	 * where the config variable does not exist at all.
 	 */
-	if (!values)
+	if (repo_config_get_value_multi(the_repository, config_key, &values))
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/builtin/gc.c b/builtin/gc.c
index e38d1783f30..2b3da377d52 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1510,8 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
-	list = git_config_get_value_multi(key);
-	if (list) {
+	if (!git_config_get_value_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1577,11 +1576,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 	if (config_file) {
 		git_configset_init(&cs);
 		git_configset_add_file(&cs, config_file);
-		list = git_configset_get_value_multi(&cs, key);
-	} else {
-		list = git_config_get_value_multi(key);
 	}
-	if (list) {
+	if (!(config_file
+	      ? git_configset_get_value_multi(&cs, key, &list)
+	      : git_config_get_value_multi(key, &list))) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index 04412dd9c93..cec8cabd21e 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	int i;
 	char *value = NULL;
 	struct string_list *include = decoration_filter->include_ref_pattern;
-	const struct string_list *config_exclude =
-			git_config_get_value_multi("log.excludeDecoration");
+	const struct string_list *config_exclude;
 
-	if (config_exclude) {
+	if (!git_config_get_value_multi("log.excludeDecoration",
+					&config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/config.c b/config.c
index b88da70c664..ce5d50a490c 100644
--- a/config.c
+++ b/config.c
@@ -2417,29 +2417,34 @@ int git_configset_add_file(struct config_set *cs, const char *filename)
 int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
 {
 	const struct string_list *values = NULL;
+	int ret;
+
 	/*
 	 * Follows "last one wins" semantic, i.e., if there are multiple matches for the
 	 * queried key in the files of the configset, the value returned will be the last
 	 * value in the value list for that key.
 	 */
-	values = git_configset_get_value_multi(cs, key);
+	if ((ret = git_configset_get_value_multi(cs, key, &values)))
+		return ret;
 
-	if (!values)
-		return 1;
 	assert(values->nr > 0);
 	*value = values->items[values->nr - 1].string;
 	return 0;
 }
 
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest)
 {
 	struct config_set_element *e;
+	int ret;
 
-	if (configset_find_element(cs, key, &e))
-		return NULL;
+	if ((ret = configset_find_element(cs, key, &e)))
+		return ret;
 	else if (!e)
-		return NULL;
-	return &e->value_list;
+		return 1;
+	*dest = &e->value_list;
+
+	return 0;
 }
 
 int git_configset_get(struct config_set *cs, const char *key)
@@ -2603,11 +2608,11 @@ int repo_config_get_value(struct repository *repo,
 	return git_configset_get_value(repo->config, key, value);
 }
 
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key)
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest)
 {
 	git_config_check_init(repo);
-	return git_configset_get_value_multi(repo->config, key);
+	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
 int repo_config_get_string(struct repository *repo,
@@ -2720,9 +2725,9 @@ int git_config_get_value(const char *key, const char **value)
 	return repo_config_get_value(the_repository, key, value);
 }
 
-const struct string_list *git_config_get_value_multi(const char *key)
+int git_config_get_value_multi(const char *key, const struct string_list **dest)
 {
-	return repo_config_get_value_multi(the_repository, key);
+	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
 int git_config_get_string(const char *key, char **dest)
@@ -2869,7 +2874,8 @@ void git_die_config(const char *key, const char *err, ...)
 		error_fn(err, params);
 		va_end(params);
 	}
-	values = git_config_get_value_multi(key);
+	if (git_config_get_value_multi(key, &values))
+		BUG("for key '%s' we must have a value to report on", key);
 	kv_info = values->items[values->nr - 1].util;
 	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
 }
diff --git a/config.h b/config.h
index 04c5e594015..fbc153cdc96 100644
--- a/config.h
+++ b/config.h
@@ -459,10 +459,18 @@ int git_configset_add_parameters(struct config_set *cs);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key` and config set `cs`. When the
- * configuration variable `key` is not found, returns NULL. The caller
- * should not free or modify the returned pointer, as it is owned by the cache.
+ * configuration variable `key` is not found, returns 1 without touching
+ * `value`.
+ *
+ * The key will be parsed for validity with git_config_parse_key(), on
+ * error a negative value will be returned.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
+RESULT_MUST_BE_USED
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest);
 
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
@@ -507,8 +515,9 @@ RESULT_MUST_BE_USED
 int repo_config_get(struct repository *repo, const char *key);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key);
+RESULT_MUST_BE_USED
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -561,10 +570,14 @@ int git_config_get_value(const char *key, const char **value);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key`. When the configuration variable
- * `key` is not found, returns NULL. The caller should not free or modify
- * the returned pointer, as it is owned by the cache.
+ * `key` is not found, returns 1 without touching `value`.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_config_get_value_multi(const char *key);
+RESULT_MUST_BE_USED
+int git_config_get_value_multi(const char *key,
+			       const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index d2a42abf28c..15c5eb507c0 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2314,7 +2314,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)
 
 const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
-	return repo_config_get_value_multi(r, "pack.preferbitmaptips");
+	const struct string_list *dest;
+
+	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+		return dest;
+	return NULL;
 }
 
 int bitmap_is_preferred_refname(struct repository *r, const char *refname)
diff --git a/submodule.c b/submodule.c
index 3a0dfc417c0..4b6f5223b0c 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	sl = repo_config_get_value_multi(repo, "submodule.active");
-	if (sl) {
+	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/helper/test-config.c b/t/helper/test-config.c
index 4ba9eb65606..8f70beb6c9d 100644
--- a/t/helper/test-config.c
+++ b/t/helper/test-config.c
@@ -95,8 +95,7 @@ int cmd__config(int argc, const char **argv)
 			goto exit1;
 		}
 	} else if (argc == 3 && !strcmp(argv[1], "get_value_multi")) {
-		strptr = git_config_get_value_multi(argv[2]);
-		if (strptr) {
+		if (!git_config_get_value_multi(argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
@@ -159,8 +158,7 @@ int cmd__config(int argc, const char **argv)
 				goto exit2;
 			}
 		}
-		strptr = git_configset_get_value_multi(&cs, argv[2]);
-		if (strptr) {
+		if (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
diff --git a/versioncmp.c b/versioncmp.c
index 323f5d35ea8..60c3a517122 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)
 	if (!initialized) {
 		const char *const newk = "versionsort.suffix";
 		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *newl;
 		const struct string_list *oldl;
+		int new = git_config_get_value_multi(newk, &newl);
+		int old = git_config_get_value_multi(oldk, &oldl);
 
-		prereleases = git_config_get_value_multi(newk);
-		oldl = git_config_get_value_multi(oldk);
-		if (prereleases && oldl)
+		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-		else if (!prereleases)
+		if (!new)
+			prereleases = newl;
+		else if (!old)
 			prereleases = oldl;
 
 		initialized = 1;
-- 
2.39.1.1397.g8c8c074958d


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v4 6/9] for-each-repo: error on bad --config
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
                         ` (4 preceding siblings ...)
  2023-02-02 13:27       ` [PATCH v4 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
@ 2023-02-02 13:27       ` Ævar Arnfjörð Bjarmason
  2023-02-06 12:56         ` Glen Choo
  2023-02-02 13:27       ` [PATCH v4 7/9] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
                         ` (3 subsequent siblings)
  9 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-02 13:27 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

As noted in 6c62f015520 (for-each-repo: do nothing on empty config,
2021-01-08) this command wants to ignore a non-existing config key,
but let's not conflate that with bad config.

Before this, all these added tests would pass with an exit code of 0.

We could preserve the comment added in 6c62f015520, but now that we're
directly using the documented repo_config_get_value_multi() value it's
just narrating something that should be obvious from the API use, so
let's drop it.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  | 11 ++++++-----
 t/t0068-for-each-repo.sh |  6 ++++++
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index fd0e7739e6a..224164addb3 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	static const char *config_key = NULL;
 	int i, result = 0;
 	const struct string_list *values;
+	int err;
 
 	const struct option options[] = {
 		OPT_STRING(0, "config", &config_key, N_("config"),
@@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	/*
-	 * Do nothing on an empty list, which is equivalent to the case
-	 * where the config variable does not exist at all.
-	 */
-	if (repo_config_get_value_multi(the_repository, config_key, &values))
+	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	if (err < 0)
+		usage_msg_optf(_("got bad config --config=%s"),
+			       for_each_repo_usage, options, config_key);
+	else if (err)
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 3648d439a87..6b51e00da0e 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '
 	git for-each-repo --config=bogus.config -- help --no-such-option
 '
 
+test_expect_success 'error on bad config keys' '
+	test_expect_code 129 git for-each-repo --config=a &&
+	test_expect_code 129 git for-each-repo --config=a.b. &&
+	test_expect_code 129 git for-each-repo --config="'\''.b"
+'
+
 test_done
-- 
2.39.1.1397.g8c8c074958d


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v4 7/9] config API users: test for *_get_value_multi() segfaults
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
                         ` (5 preceding siblings ...)
  2023-02-02 13:27       ` [PATCH v4 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
@ 2023-02-02 13:27       ` Ævar Arnfjörð Bjarmason
  2023-02-02 13:27       ` [PATCH v4 8/9] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
                         ` (2 subsequent siblings)
  9 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-02 13:27 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

As we'll discuss in the subsequent commit these tests all
show *_get_value_multi() API users unable to handle there being a
value-less key in the config, which is represented with a "NULL" for
that entry in the "string" member of the returned "struct
string_list", causing a segfault.

These added tests exhaustively test for that issue, as we'll see in a
subsequent commit we'll need to change all of the API users
of *_get_value_multi(). These cases were discovered by triggering each
one individually, and then adding these tests.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t4202-log.sh                 | 11 +++++++++++
 t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++
 t/t7004-tag.sh                 | 12 ++++++++++++
 t/t7413-submodule-is-active.sh | 12 ++++++++++++
 t/t7900-maintenance.sh         | 23 +++++++++++++++++++++++
 5 files changed, 74 insertions(+)

diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 2ce2b41174d..e4f02d8208b 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
+test_expect_failure 'parse log.excludeDecoration with no value' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[log]
+		excludeDecoration
+	EOF
+	git log --decorate=short
+'
+
 test_expect_success 'decorate-refs with glob' '
 	cat >expect.decorate <<-\EOF &&
 	Merge-tag-reach
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 7d8dee41b0d..0306b399188 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,6 +404,22 @@ test_bitmap_cases () {
 		)
 	'
 
+	test_expect_failure 'pack.preferBitmapTips' '
+		git init repo &&
+		test_when_finished "rm -rf repo" &&
+		(
+			cd repo &&
+			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
+			test_commit_bulk --message="%s" 103 &&
+
+			cat >>.git/config <<-\EOF &&
+			[pack]
+				preferBitmapTips
+			EOF
+			git repack -adb
+		)
+	'
+
 	test_expect_success 'complains about multiple pack bitmaps' '
 		rm -fr repo &&
 		git init repo &&
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index 9aa1660651b..f343551a7d4 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[versionsort]
+		prereleaseSuffix
+		suffix
+	EOF
+	git tag -l --sort=version:refname
+'
+
 test_expect_success 'version sort with prerelease reordering' '
 	test_config versionsort.prereleaseSuffix -rc &&
 	git tag foo1.6-rc1 &&
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index 7cdc2637649..bfe27e50732 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
+test_expect_failure 'is-active handles submodule.active config missing a value' '
+	cp super/.git/config super/.git/config.orig &&
+	test_when_finished mv super/.git/config.orig super/.git/config &&
+
+	cat >>super/.git/config <<-\EOF &&
+	[submodule]
+		active
+	EOF
+
+	test-tool -C super submodule is-active sub1
+'
+
 test_expect_success 'is-active works with basic submodule.active config' '
 	test_when_finished "git -C super config submodule.sub1.URL ../sub" &&
 	test_when_finished "git -C super config --unset-all submodule.active" &&
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 823331e44a0..d82eac6a471 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -524,6 +524,29 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --config-file ./other --force
 '
 
+test_expect_failure 'register with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	git maintenance register
+'
+
+test_expect_failure 'unregister with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	git maintenance unregister &&
+	git maintenance unregister --force
+'
+
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
 	META="a+b*c" &&
 	git init "$META" &&
-- 
2.39.1.1397.g8c8c074958d


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v4 8/9] config API: add "string" version of *_value_multi(), fix segfaults
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
                         ` (6 preceding siblings ...)
  2023-02-02 13:27       ` [PATCH v4 7/9] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
@ 2023-02-02 13:27       ` Ævar Arnfjörð Bjarmason
  2023-02-06 13:04         ` Glen Choo
  2023-02-02 13:27       ` [PATCH v4 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
  9 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-02 13:27 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Fix numerous and mostly long-standing segfaults in consumers of
the *_config_*value_multi() API. As discussed in the preceding commit
an empty key in the config syntax yields a "NULL" string, which these
users would give to strcmp() (or similar), resulting in segfaults.

As this change shows, most users users of the *_config_*value_multi()
API didn't really want such an an unsafe and low-level API, let's give
them something with the safety of git_config_get_string() instead.

This fix is similar to what the *_string() functions and others
acquired in[1] and [2]. Namely introducing and using a safer
"*_get_string_multi()" variant of the low-level "_*value_multi()"
function.

This fixes segfaults in code introduced in:

  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)
  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)
  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)
  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)
  - 92156291ca8 (log: add default decoration filter, 2022-08-05)
  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)

There are now three remaining files using the low-level API:

- Two cases in "builtin/submodule--helper.c", where it's used safely
  to see if any config exists.

  We could refactor these away from "multi" to some "does it exist?"
  function, as [4] did, but as that's orthogonal to the "string"
  safety we're introducing here let's leave them for now.

- One in "builtin/for-each-repo.c", which we'll convert in a
  subsequent commit.

- The "t/helper/test-config.c" code added in [4].

As seen in the preceding commit we need to give the
"t/helper/test-config.c" caller these "NULL" entries.

We could also alter the underlying git_configset_get_value_multi()
function to be "string safe", but doing so would leave no room for
other variants of "*_get_value_multi()" that coerce to other types.

Such coercion can't be built on the string version, since as we've
established "NULL" is a true value in the boolean context, but if we
coerced it to "" for use in a list of strings it'll be subsequently
coerced to "false" as a boolean.

The callback pattern being used here will make it easy to introduce
e.g. a "multi" variant which coerces its values to "bool", "int",
"path" etc.

1. 40ea4ed9032 (Add config_error_nonbool() helper function,
   2008-02-11)
2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,
   2008-02-11).
3. https://lore.kernel.org/git/patch-07.10-c01f7d85c94-20221026T151328Z-avarab@gmail.com/
4. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                   |  6 +++---
 builtin/log.c                  |  4 ++--
 config.c                       | 32 ++++++++++++++++++++++++++++++++
 config.h                       | 19 +++++++++++++++++++
 pack-bitmap.c                  |  2 +-
 submodule.c                    |  2 +-
 t/t4202-log.sh                 |  8 ++++++--
 t/t5310-pack-bitmaps.sh        |  8 ++++++--
 t/t7004-tag.sh                 |  9 +++++++--
 t/t7413-submodule-is-active.sh |  8 ++++++--
 t/t7900-maintenance.sh         | 25 ++++++++++++++++++++-----
 versioncmp.c                   |  4 ++--
 12 files changed, 105 insertions(+), 22 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 2b3da377d52..9497bdf23e4 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1510,7 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
-	if (!git_config_get_value_multi(key, &list)) {
+	if (!git_config_get_string_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1578,8 +1578,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 		git_configset_add_file(&cs, config_file);
 	}
 	if (!(config_file
-	      ? git_configset_get_value_multi(&cs, key, &list)
-	      : git_config_get_value_multi(key, &list))) {
+	      ? git_configset_get_string_multi(&cs, key, &list)
+	      : git_config_get_string_multi(key, &list))) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index cec8cabd21e..481685d5263 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	struct string_list *include = decoration_filter->include_ref_pattern;
 	const struct string_list *config_exclude;
 
-	if (!git_config_get_value_multi("log.excludeDecoration",
-					&config_exclude)) {
+	if (!git_config_get_string_multi("log.excludeDecoration",
+					 &config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/config.c b/config.c
index ce5d50a490c..30867663997 100644
--- a/config.c
+++ b/config.c
@@ -2447,6 +2447,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,
 	return 0;
 }
 
+static int check_multi_string(struct string_list_item *item, void *util)
+{
+	return item->string ? 0 : config_error_nonbool(util);
+}
+
+int git_configset_get_string_multi(struct config_set *cs, const char *key,
+				   const struct string_list **dest)
+{
+	int ret;
+
+	if ((ret = git_configset_get_value_multi(cs, key, dest)))
+		return ret;
+	if ((ret = for_each_string_list((struct string_list *)*dest,
+					check_multi_string, (void *)key)))
+		return ret;
+
+	return 0;
+}
+
 int git_configset_get(struct config_set *cs, const char *key)
 {
 	struct config_set_element *e;
@@ -2615,6 +2634,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,
 	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
+int repo_config_get_string_multi(struct repository *repo, const char *key,
+				 const struct string_list **dest)
+{
+	git_config_check_init(repo);
+	return git_configset_get_string_multi(repo->config, key, dest);
+}
+
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest)
 {
@@ -2730,6 +2756,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)
 	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
+int git_config_get_string_multi(const char *key,
+				const struct string_list **dest)
+{
+	return repo_config_get_string_multi(the_repository, key, dest);
+}
+
 int git_config_get_string(const char *key, char **dest)
 {
 	return repo_config_get_string(the_repository, key, dest);
diff --git a/config.h b/config.h
index fbc153cdc96..d98a06352e3 100644
--- a/config.h
+++ b/config.h
@@ -472,6 +472,19 @@ RESULT_MUST_BE_USED
 int git_configset_get_value_multi(struct config_set *cs, const char *key,
 				  const struct string_list **dest);
 
+/**
+ * A validation wrapper for git_configset_get_value_multi() which does
+ * for it what git_configset_get_string() does for
+ * git_configset_get_value().
+ *
+ * The configuration syntax allows for "[section] key", which will
+ * give us a NULL entry in the "struct string_list", as opposed to
+ * "[section] key =" which is the empty string. Most users of the API
+ * are not prepared to handle NULL in a "struct string_list".
+ */
+int git_configset_get_string_multi(struct config_set *cs, const char *key,
+				   const struct string_list **dest);
+
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
  */
@@ -518,6 +531,9 @@ int repo_config_get_value(struct repository *repo,
 RESULT_MUST_BE_USED
 int repo_config_get_value_multi(struct repository *repo, const char *key,
 				const struct string_list **dest);
+RESULT_MUST_BE_USED
+int repo_config_get_string_multi(struct repository *repo, const char *key,
+				 const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -578,6 +594,9 @@ int git_config_get_value(const char *key, const char **value);
 RESULT_MUST_BE_USED
 int git_config_get_value_multi(const char *key,
 			       const struct string_list **dest);
+RESULT_MUST_BE_USED
+int git_config_get_string_multi(const char *key,
+				const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index 15c5eb507c0..d003c7e60b4 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2316,7 +2316,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
 	const struct string_list *dest;
 
-	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+	if (!repo_config_get_string_multi(r, "pack.preferbitmaptips", &dest))
 		return dest;
 	return NULL;
 }
diff --git a/submodule.c b/submodule.c
index 4b6f5223b0c..30a103246ec 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
+	if (!repo_config_get_string_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index e4f02d8208b..ae73aef922f 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
-test_expect_failure 'parse log.excludeDecoration with no value' '
+test_expect_success 'parse log.excludeDecoration with no value' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '
 	[log]
 		excludeDecoration
 	EOF
-	git log --decorate=short
+	cat >expect <<-\EOF &&
+	error: missing value for '\''log.excludeDecoration'\''
+	EOF
+	git log --decorate=short 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'decorate-refs with glob' '
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 0306b399188..526a5a506eb 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,7 +404,7 @@ test_bitmap_cases () {
 		)
 	'
 
-	test_expect_failure 'pack.preferBitmapTips' '
+	test_expect_success 'pack.preferBitmapTips' '
 		git init repo &&
 		test_when_finished "rm -rf repo" &&
 		(
@@ -416,7 +416,11 @@ test_bitmap_cases () {
 			[pack]
 				preferBitmapTips
 			EOF
-			git repack -adb
+			cat >expect <<-\EOF &&
+			error: missing value for '\''pack.preferbitmaptips'\''
+			EOF
+			git repack -adb 2>actual &&
+			test_cmp expect actual
 		)
 	'
 
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index f343551a7d4..f4a31ada79a 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas
 		prereleaseSuffix
 		suffix
 	EOF
-	git tag -l --sort=version:refname
+	cat >expect <<-\EOF &&
+	error: missing value for '\''versionsort.suffix'\''
+	error: missing value for '\''versionsort.prereleasesuffix'\''
+	EOF
+	git tag -l --sort=version:refname 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'version sort with prerelease reordering' '
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index bfe27e50732..887d181b72e 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
-test_expect_failure 'is-active handles submodule.active config missing a value' '
+test_expect_success 'is-active handles submodule.active config missing a value' '
 	cp super/.git/config super/.git/config.orig &&
 	test_when_finished mv super/.git/config.orig super/.git/config &&
 
@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'
 		active
 	EOF
 
-	test-tool -C super submodule is-active sub1
+	cat >expect <<-\EOF &&
+	error: missing value for '\''submodule.active'\''
+	EOF
+	test-tool -C super submodule is-active sub1 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'is-active works with basic submodule.active config' '
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index d82eac6a471..487e326b3fa 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --config-file ./other --force
 '
 
-test_expect_failure 'register with no value for maintenance.repo' '
+test_expect_success 'register with no value for maintenance.repo' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '
 	[maintenance]
 		repo
 	EOF
-	git maintenance register
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	git maintenance register 2>actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
 '
 
-test_expect_failure 'unregister with no value for maintenance.repo' '
+test_expect_success 'unregister with no value for maintenance.repo' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -543,8 +548,18 @@ test_expect_failure 'unregister with no value for maintenance.repo' '
 	[maintenance]
 		repo
 	EOF
-	git maintenance unregister &&
-	git maintenance unregister --force
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	test_expect_code 128 git maintenance unregister 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo &&
+
+	git maintenance unregister --force 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
 '
 
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
diff --git a/versioncmp.c b/versioncmp.c
index 60c3a517122..7498da96e0e 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)
 		const char *const oldk = "versionsort.prereleasesuffix";
 		const struct string_list *newl;
 		const struct string_list *oldl;
-		int new = git_config_get_value_multi(newk, &newl);
-		int old = git_config_get_value_multi(oldk, &oldl);
+		int new = git_config_get_string_multi(newk, &newl);
+		int old = git_config_get_string_multi(oldk, &oldl);
 
 		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-- 
2.39.1.1397.g8c8c074958d


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v4 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
                         ` (7 preceding siblings ...)
  2023-02-02 13:27       ` [PATCH v4 8/9] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
@ 2023-02-02 13:27       ` Ævar Arnfjörð Bjarmason
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
  9 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-02 13:27 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on
configured repos, 2020-09-11). Due to assuming that elements returned
from the repo_config_get_value_multi() call wouldn't be "NULL" we'd
conflate the <path> and <command> part of the argument list when
running commands.

As noted in the preceding commit the fix is to move to a safer
"*_string_multi()" version of the *_multi() API. This change is
separated from the rest because those all segfaulted. In this change
we ended up with different behavior.

When using the "--config=<config>" form we take each element of the
list as a path to a repository. E.g. with a configuration like:

	[repo] list = /some/repo

We would, with this command:

	git for-each-repo --config=repo.list status builtin

Run a "git status" in /some/repo, as:

	git -C /some/repo status builtin

I.e. ask "status" to report on the "builtin" directory. But since a
configuration such as this would result in a "struct string_list *"
with one element, whose "string" member is "NULL":

	[repo] list

We would, when constructing our command-line in
"builtin/for-each-repo.c"...

	strvec_pushl(&child.args, "-C", path, NULL);
	for (i = 0; i < argc; i++)
		strvec_push(&child.args, argv[i]);

...have that "path" be "NULL", and as strvec_pushl() stops when it
sees NULL we'd end with the first "argv" element as the argument to
the "-C" option, e.g.:

	git -C status builtin

I.e. we'd run the command "builtin" in the "status" directory.

In another context this might be an interesting security
vulnerability, but I think that this amounts to a nothingburger on
that front.

A hypothetical attacker would need to be able to write config for the
victim to run, if they're able to do that there's more interesting
attack vectors. See the "safe.directory" facility added in
8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).

An even more unlikely possibility would be an attacker able to
generate the config used for "for-each-repo --config=<key>", but
nothing else (e.g. an automated system producing that list).

Even in that case the attack vector is limited to the user running
commands whose name matches a directory that's interesting to the
attacker (e.g. a "log" directory in a repository). The second
argument (if any) of the command is likely to make git die without
doing anything interesting (e.g. "-p" to "log", there being no "-p"
built-in command to run).

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  |  2 +-
 t/t0068-for-each-repo.sh | 13 +++++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 224164addb3..ce8f7a99086 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	err = repo_config_get_string_multi(the_repository, config_key, &values);
 	if (err < 0)
 		usage_msg_optf(_("got bad config --config=%s"),
 			       for_each_repo_usage, options, config_key);
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 6b51e00da0e..4b90b74d5d5 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -46,4 +46,17 @@ test_expect_success 'error on bad config keys' '
 	test_expect_code 129 git for-each-repo --config="'\''.b"
 '
 
+test_expect_success 'error on NULL value for config keys' '
+	cat >>.git/config <<-\EOF &&
+	[empty]
+		key
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''empty.key'\''
+	EOF
+	test_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.39.1.1397.g8c8c074958d


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 2/9] config tests: add "NULL" tests for *_get_value_multi()
  2023-02-02 13:27       ` [PATCH v4 2/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2023-02-02 23:12         ` Junio C Hamano
  2023-02-06 10:40         ` Glen Choo
  1 sibling, 0 replies; 134+ messages in thread
From: Junio C Hamano @ 2023-02-02 23:12 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Elijah Newren, Jeff King, Taylor Blau,
	SZEDER Gábor, Glen Choo, Calvin Wan, Emily Shaffer, raymond,
	zweiss

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> A less well known edge case in the config format is that keys can be
> value-less, a shorthand syntax for "true" boolean keys. I.e. these two
> are equivalent as far as "--type=bool" is concerned:
>
> 	[a]key
> 	[a]key = true
>
> But as far as our parser is concerned the values for these two are
> NULL, and "true". I.e. for a sequence like:
>
> 	[a]key=x
> 	[a]key
> 	[a]key=y
>
> We get a "struct string_list" with "string" members with ".string"
> values of:
>
> 	{ "x", NULL, "y" }
>
> This behavior goes back to the initial implementation of
> git_config_bool() in 17712991a59 (Add ".git/config" file parser,
> 2005-10-10).
>
> When parts of the config_set API were tested for in [1] they didn't
> add coverage for 3/4 of the "(NULL)" cases handled in
> "t/helper/test-config.c". We'd test that case for "get_value", but not
> "get_value_multi", "configset_get_value" and
> "configset_get_value_multi".
>
> We now cover all of those cases, which in turn expose the details of
> how this part of the config API works.

Good to see a better coverage.

With the "last one wins" semantics for half of these 4 cases, it may
make sense to further extend the tests to cover cases where the last
one is a valueless true, in addition to what is used in this patch
(i.e. a valueless true in the middle of three).

Thanks.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions
  2023-02-02 13:27       ` [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
@ 2023-02-02 23:56         ` Junio C Hamano
  2023-02-07 10:29           ` Ævar Arnfjörð Bjarmason
  2023-02-06 12:36         ` Glen Choo
  2023-02-06 12:37         ` Glen Choo
  2 siblings, 1 reply; 134+ messages in thread
From: Junio C Hamano @ 2023-02-02 23:56 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Elijah Newren, Jeff King, Taylor Blau,
	SZEDER Gábor, Glen Choo, Calvin Wan, Emily Shaffer, raymond,
	zweiss

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> We already have the basic "git_config_get_value()" function and its
> "repo_*" and "configset" siblings to get a given "key" and assign the
> last key found to a provided "value".
>
> But some callers don't care about that value, but just want to use the
> return value of the "get_value()" function to check whether the key
> exist (or another non-zero return value).
>
> The immediate motivation for this is that a subsequent commit will
> need to change all callers of the "*_get_value_multi()" family of
> functions. In two cases here we (ab)used it to check whether we had
> any values for the given key, but didn't care about the return value.

So, the idea is that 

	if (!git_config_get_string(key, &discard))
		free(discard);
	else
		... the key is missing ...

becomes

	if (git_config_get(key))
		... the key is missing ...

In other words, git_config_get() returns 0 only when the key is
used, and non-zero return signals that the key is not used?

Similarly, get_value_multi() was an interface to get to the
value_list associated to the given key, and was abused like

	if (git_config_get_value_multi(key))
		... the key exists ...

which will become

	if (!git_config_get(key))
		... the key exists ...

right?

>  	/* Set maintenance strategy, if unset */
> -	if (!git_config_get_string("maintenance.strategy", &config_value))
> -		free(config_value);
> -	else
> +	if (git_config_get("maintenance.strategy"))
>  		git_config_set("maintenance.strategy", "incremental");

OK.  config_get() says "true" meaning the key is missing.

> -	if (!argc && git_config_get_value_multi("submodule.active"))
> +	if (!argc && !git_config_get("submodule.active"))
>  		module_list_active(&list);

OK.

> @@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
>  		 * If there are no path args and submodule.active is set then,
>  		 * by default, only initialize 'active' modules.
>  		 */
> -		if (!argc && git_config_get_value_multi("submodule.active"))
> +		if (!argc && !git_config_get("submodule.active"))
>  			module_list_active(&list);

OK.

> @@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)
>  	 * is_submodule_active(), since that function needs to find
>  	 * out the value of "submodule.active" again anyway.
>  	 */
> -	if (!git_config_get_string_tmp("submodule.active", &val)) {
> +	if (!git_config_get("submodule.active")) {

string_tmp() variant is to retrieve borrowed value, and it returns 0
when there is a value.  If it is a valueless true, we get -1 back
with an error message.  What does the updated version do in the
valueless true case?

> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index f51c40f1e1e..6ba42d4ad20 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -338,7 +337,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
>  				to_file, "core.bare", NULL, "true", 0))
>  			error(_("failed to unset '%s' in '%s'"),
>  				"core.bare", to_file);
> -		if (!git_configset_get_value(&cs, "core.worktree", &core_worktree) &&
> +		if (!git_configset_get(&cs, "core.worktree") &&

OK.

> diff --git a/config.c b/config.c
> index 00090a32fc3..b88da70c664 100644
> --- a/config.c
> +++ b/config.c
> @@ -2289,23 +2289,28 @@ void read_very_early_config(config_fn_t cb, void *data)
>  	config_with_options(cb, data, NULL, &opts);
>  }
>  
> -static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
> +static int configset_find_element(struct config_set *cs, const char *key,
> +				  struct config_set_element **dest)
>  {
>  	struct config_set_element k;
>  	struct config_set_element *found_entry;
>  	char *normalized_key;
> +	int ret;
> +
>  	/*
>  	 * `key` may come from the user, so normalize it before using it
>  	 * for querying entries from the hashmap.
>  	 */
> -	if (git_config_parse_key(key, &normalized_key, NULL))
> -		return NULL;
> +	ret = git_config_parse_key(key, &normalized_key, NULL);
> +	if (ret)
> +		return ret;
>  
>  	hashmap_entry_init(&k.ent, strhash(normalized_key));
>  	k.key = normalized_key;
>  	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
>  	free(normalized_key);
> -	return found_entry;
> +	*dest = found_entry;
> +	return 0;

OK, so we used to return NULL when the key is not parseable, and
otherwise we returned the config_set_element we found for the key,
or NULL if there is no such element.  Now we return error code as
the return value and allow the caller to peek the element via *dest
parameter.

So, from the caller's point of view (dest != NULL) is how it checks
if the key is used.  The function returning 0 is a sign that the key
passed to it is healthy.  OK.

> @@ -2314,8 +2319,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha
>  	struct string_list_item *si;
>  	struct configset_list_item *l_item;
>  	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
> +	int ret;
>  
> -	e = configset_find_element(cs, key);
> +	ret = configset_find_element(cs, key, &e);
> +	if (ret)
> +		return ret;

The function never returned any meaningful error, so the callers may
not be prepared to see such an error return.  But now we at least
notice an error at this level.

> @@ -2425,8 +2433,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *
>  
>  const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
>  {
> -	struct config_set_element *e = configset_find_element(cs, key);
> -	return e ? &e->value_list : NULL;
> +	struct config_set_element *e;
> +
> +	if (configset_find_element(cs, key, &e))
> +		return NULL;
> +	else if (!e)
> +		return NULL;
> +	return &e->value_list;
> +}

OK.  !e means "we got a healthy key and peeking into the hash table,
there wasn't any entry for the key", and that is reported with NULL.
Do we evern return a string list with .nr == 0, I wonder.  Having to
deal with such a list would make the caller's job more complex, but
perhaps we are not allowing the code to shrink value_list.nr to
avoid such a situation?

> +int git_configset_get(struct config_set *cs, const char *key)
> +{
> +	struct config_set_element *e;
> +	int ret;
> +
> +	if ((ret = configset_find_element(cs, key, &e)))
> +		return ret;
> +	else if (!e)
> +		return 1;
> +	return 0;
>  }

OK.  So 0 return from the function means there is a value (or more)
for a given key.  Good.

> diff --git a/config.h b/config.h
> index ef9eade6414..04c5e594015 100644
> --- a/config.h
> +++ b/config.h
> @@ -471,9 +471,12 @@ void git_configset_clear(struct config_set *cs);
>  
>  /*
>   * These functions return 1 if not found, and 0 if found, leaving the found
> - * value in the 'dest' pointer.
> + * value in the 'dest' pointer (if any).
>   */

Now the returned non-zero values are no longer 1 alone, no?
Whatever lower-level functions use to signal an error is propagated
up with the

	if ((ret = func())
		return ret;

pattern.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests
  2023-02-02 13:27       ` [PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
@ 2023-02-03  1:22         ` Junio C Hamano
  2023-02-06  8:31         ` Glen Choo
  1 sibling, 0 replies; 134+ messages in thread
From: Junio C Hamano @ 2023-02-03  1:22 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Elijah Newren, Jeff King, Taylor Blau,
	SZEDER Gábor, Glen Choo, Calvin Wan, Emily Shaffer, raymond,
	zweiss

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> There were no tests checking for the output of the git_die_config()
> function in the config API, added in 5a80e97c827 (config: add
> `git_die_config()` to the config-set API, 2014-08-07). We only tested
> "test_must_fail", but didn't assert the output.

It sort of is expected as git_die_config() is useful only for code
that uses new style config parsing (i.e. instead of using the
git_config() callback interface to parse what we encounter in the
config file, actively call git_config_get_foo() interface to ask for
keys the caller cares about), and dying unconditionally is not
useful for the old style ones.

A better coverage is good.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 4/9] versioncmp.c: refactor config reading next commit
  2023-02-02 13:27       ` [PATCH v4 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
@ 2023-02-03 21:52         ` Junio C Hamano
  0 siblings, 0 replies; 134+ messages in thread
From: Junio C Hamano @ 2023-02-03 21:52 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Elijah Newren, Jeff King, Taylor Blau,
	SZEDER Gábor, Glen Choo, Calvin Wan, Emily Shaffer, raymond,
	zweiss

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> Refactor the reading of the versionSort.suffix and
> versionSort.prereleaseSuffix configuration variables to stay within
> the bounds of our CodingGuidelines when it comes to line length, and
> to avoid repeating ourselves.
>
> Let's also split out the names of the config variables into variables
> of our own, so we don't have to repeat ourselves, 

You do not have to repeat "we don't have to repeat" by mentioning it
twice in two paragraphs.

> Moving the "initialized = 1" assignment allows us to move some of this
> to the variable declarations in the subsequent commit.

Unclear until looking at these subsequent steps; let's see what
happens next ;-).

>  	if (!initialized) {
> -		const struct string_list *deprecated_prereleases;
> +		const char *const newk = "versionsort.suffix";
> +		const char *const oldk = "versionsort.prereleasesuffix";
> +		const struct string_list *oldl;

With s/oldl/deprecated_prereleases/ the damage would even be
smaller.  It's not like a more descriptive name in this small scope
hurts line length or readability, is it?

> +		prereleases = git_config_get_value_multi(newk);
> +		oldl = git_config_get_value_multi(oldk);

> +		if (prereleases && oldl)
> +			warning("ignoring %s because %s is set", oldk, newk);
> +		else if (!prereleases)
> +			prereleases = oldl;

This makes it more clear than the original what is going on, even
though they are equivalent.  If we have both, we ignore the
fallback, and if we don't have what we need, we replace it with the
fallback, which could be NULL in which case we end up not having
any.

It is a very nice added bonus that we ended up with a shallow
nesting.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests
  2023-02-02 13:27       ` [PATCH v4 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
  2023-02-03  1:22         ` Junio C Hamano
@ 2023-02-06  8:31         ` Glen Choo
  1 sibling, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-02-06  8:31 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> We need tests for this because a subsequent commit will alter the
> return value of git_config_get_value_multi(), which is used to get the
> config values in the git_die_config() function. This test coverage
> helps to build confidence in that subsequent change.

In v3, I recall having to read ahead quite far ahead in the series to
understand why we needed this patch. In comparison, this is a lot
clearer :)

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 2/9] config tests: add "NULL" tests for *_get_value_multi()
  2023-02-02 13:27       ` [PATCH v4 2/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
  2023-02-02 23:12         ` Junio C Hamano
@ 2023-02-06 10:40         ` Glen Choo
  2023-02-06 12:31           ` Ævar Arnfjörð Bjarmason
  1 sibling, 1 reply; 134+ messages in thread
From: Glen Choo @ 2023-02-06 10:40 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> +test_NULL_in_multi () {
> +	local op="$1" &&
> +	local file="$2" &&
> +
> +	test_expect_success "$op: NULL value in config${file:+ in $file}" '
> +		config="$file" &&
> +		if test -z "$config"
> +		then
> +			config=.git/config &&
> +			test_when_finished "mv $config.old $config" &&
> +			mv "$config" "$config".old
> +		fi &&
> +
> +		cat >"$config" <<-\EOF &&
> +		[a]key=x
> +		[a]key
> +		[a]key=y
> +		EOF
> +		case "$op" in
> +		*_multi)
> +			cat >expect <<-\EOF
> +			x
> +			(NULL)
> +			y
> +			EOF
> +			;;
> +		*)
> +			cat >expect <<-\EOF
> +			y
> +			EOF
> +			;;
> +		esac &&
> +		test-tool config "$op" a.key $file >actual &&
> +		test_cmp expect actual
> +	'
> +}
> +
> +test_NULL_in_multi "get_value_multi"
> +test_NULL_in_multi "configset_get_value" "my.config"
> +test_NULL_in_multi "configset_get_value_multi" "my.config"

I frankly preferred v3's tests over this version. v3 is slightly
verbose, but at least the lack of logic made it easy to read and
understand. I'd be okay with it if we get a big DRY-ness benefit, but 2
conditionals for 3 cases seems quite un-DRY to me.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 2/9] config tests: add "NULL" tests for *_get_value_multi()
  2023-02-06 10:40         ` Glen Choo
@ 2023-02-06 12:31           ` Ævar Arnfjörð Bjarmason
  2023-02-06 16:23             ` Glen Choo
  0 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-06 12:31 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss


On Mon, Feb 06 2023, Glen Choo wrote:

> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>
>> +test_NULL_in_multi () {
>> +	local op="$1" &&
>> +	local file="$2" &&
>> +
>> +	test_expect_success "$op: NULL value in config${file:+ in $file}" '
>> +		config="$file" &&
>> +		if test -z "$config"
>> +		then
>> +			config=.git/config &&
>> +			test_when_finished "mv $config.old $config" &&
>> +			mv "$config" "$config".old
>> +		fi &&
>> +
>> +		cat >"$config" <<-\EOF &&
>> +		[a]key=x
>> +		[a]key
>> +		[a]key=y
>> +		EOF
>> +		case "$op" in
>> +		*_multi)
>> +			cat >expect <<-\EOF
>> +			x
>> +			(NULL)
>> +			y
>> +			EOF
>> +			;;
>> +		*)
>> +			cat >expect <<-\EOF
>> +			y
>> +			EOF
>> +			;;
>> +		esac &&
>> +		test-tool config "$op" a.key $file >actual &&
>> +		test_cmp expect actual
>> +	'
>> +}
>> +
>> +test_NULL_in_multi "get_value_multi"
>> +test_NULL_in_multi "configset_get_value" "my.config"
>> +test_NULL_in_multi "configset_get_value_multi" "my.config"
>
> I frankly preferred v3's tests over this version. v3 is slightly
> verbose, but at least the lack of logic made it easy to read and
> understand. I'd be okay with it if we get a big DRY-ness benefit, but 2
> conditionals for 3 cases seems quite un-DRY to me.

Note that the v3 version didn't test the get_value_multi(), adjusting
the v3 version with copy/paste to test that as well is why I made this a
function. From the CL's range-diff:

    -    When the "t/t1308-config-set.sh" tests were added in [1] only one of
    -    the three "(NULL)" lines in "t/helper/test-config.c" had any test
    -    coverage. This change adds tests that stress the remaining two.
    +    When parts of the config_set API were tested for in [1] they didn't
    +    add coverage for 3/4 of the "(NULL)" cases handled in
    +    "t/helper/test-config.c". We'd test that case for "get_value", but not
    +    "get_value_multi", "configset_get_value" and
    +    "configset_get_value_multi".
    +
    +    We now cover all of those cases, which in turn expose the details of
    +    how this part of the config API works.

Of course that wouldn't address an outstanding point that we should just
copy/paste these anyway, but maybe that addresses your feedback...

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions
  2023-02-02 13:27       ` [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
  2023-02-02 23:56         ` Junio C Hamano
@ 2023-02-06 12:36         ` Glen Choo
  2023-02-06 12:37         ` Glen Choo
  2 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-02-06 12:36 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss, Ævar Arnfjörð Bjarmason

I think introducing a new function to replace the *_get_value_multi()
abuses is a good way forward. Junio has already adequately commented on
your implementation, so I'll focus this review on a different approach
that this patch could have taken.

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> We already have the basic "git_config_get_value()" function and its
> "repo_*" and "configset" siblings to get a given "key" and assign the
> last key found to a provided "value".
>
> But some callers don't care about that value, but just want to use the
> return value of the "get_value()" function to check whether the key
> exist (or another non-zero return value).

[...]

> We could have changed git_configset_get_value_multi() to accept a
> "NULL" as a "dest" for all callers, but let's avoid changing the
> behavior of existing API users. Having an "unused" value that we throw
> away internal to config.c is cheap.

There is yet another option, which is to teach "git_config_get_value()"
(mentioned earlier) to accept NULL to mean "I just want to know if there
is a value, I don't care what it is". That's what the *_get_<type>()
functions use under the hood (i.e. the ones that return either 0 or 1 or
exit).

This amounts to implementing the "*_config_key_exists()" API you
mentioned, but I think this is better fit for the current set of
semantics. At the very least, that would be an easy 1-1 replacement for
the *_get_string[_tmp]() replacements we make here. There's also the
small benefit of saving one function implementation.

> Another name for this function could have been
> "*_config_key_exists()", as suggested in [1]. That would work for all
> of these callers, and would currently be equivalent to this function,
> as the git_configset_get_value() API normalizes all non-zero return
> values to a "1".
>
> But adding that API would set us up to lose information, as e.g. if
> git_config_parse_key() in the underlying configset_find_element()
> fails we'd like to return -1, not 1.

We were already 'losing' (or rather, not caring about) this information
with the *_get_<type>() functions. The only reason we'd care about this
is if we using git_configset_get_value_multi() or similar.

We replace two callers of git_configset_get_value_multi() in this patch,
but they didn't care about the -1 case anyway...

> --- a/builtin/submodule--helper.c
> +++ b/builtin/submodule--helper.c
> @@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
>  	 * If there are no path args and submodule.active is set then,
>  	 * by default, only initialize 'active' modules.
>  	 */
> -	if (!argc && git_config_get_value_multi("submodule.active"))
> +	if (!argc && !git_config_get("submodule.active"))
>  		module_list_active(&list);
>  
>  	info.prefix = prefix;
> @@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
>  		 * If there are no path args and submodule.active is set then,
>  		 * by default, only initialize 'active' modules.
>  		 */
> -		if (!argc && git_config_get_value_multi("submodule.active"))
> +		if (!argc && !git_config_get("submodule.active"))
>  			module_list_active(&list);
>  
>  		info.prefix = opt.prefix;

Here they are.

> diff --git a/config.h b/config.h
> index ef9eade6414..04c5e594015 100644
> --- a/config.h
> +++ b/config.h
> @@ -471,9 +471,12 @@ void git_configset_clear(struct config_set *cs);
>  
>  /*
>   * These functions return 1 if not found, and 0 if found, leaving the found
> - * value in the 'dest' pointer.
> + * value in the 'dest' pointer (if any).
>   */
>  
> +RESULT_MUST_BE_USED
> +int git_configset_get(struct config_set *cs, const char *key);
> +

As Junio pointed out, git_configset_get() can now return -1, so this
isn't so accurate any more. git_configset_get() is really the exception
here, since all the other functions in this section are the
git_configset_get_*() functions that use git_configset_get_value(). I'd
prefer returning only 0 or 1 for consistency.

>  /*
>   * Finds the highest-priority value for the configuration variable `key`
>   * and config set `cs`, stores the pointer to it in `value` and returns 0.
> @@ -494,6 +497,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha
>  /* Functions for reading a repository's config */
>  struct repository;
>  void repo_config(struct repository *repo, config_fn_t fn, void *data);
> +
> +/**
> + * Run only the discover part of the repo_config_get_*() functions
> + * below, in addition to 1 if not found, returns negative values on
> + * error (e.g. if the key itself is invalid).
> + */
> +RESULT_MUST_BE_USED
> +int repo_config_get(struct repository *repo, const char *key);

This comment is quite a welcome addition. I've found myself losing track
of this information quite often

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions
  2023-02-02 13:27       ` [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
  2023-02-02 23:56         ` Junio C Hamano
  2023-02-06 12:36         ` Glen Choo
@ 2023-02-06 12:37         ` Glen Choo
  2023-02-07 11:52           ` Ævar Arnfjörð Bjarmason
  2 siblings, 1 reply; 134+ messages in thread
From: Glen Choo @ 2023-02-06 12:37 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss, Ævar Arnfjörð Bjarmason

I think introducing a new function to replace the *_get_value_multi()
abuses is a good way forward. Junio has already adequately commented on
your implementation, so I'll focus this review on a different approach
that this patch could have taken.

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> We already have the basic "git_config_get_value()" function and its
> "repo_*" and "configset" siblings to get a given "key" and assign the
> last key found to a provided "value".
>
> But some callers don't care about that value, but just want to use the
> return value of the "get_value()" function to check whether the key
> exist (or another non-zero return value).

[...]

> We could have changed git_configset_get_value_multi() to accept a
> "NULL" as a "dest" for all callers, but let's avoid changing the
> behavior of existing API users. Having an "unused" value that we throw
> away internal to config.c is cheap.

There is yet another option, which is to teach "git_config_get_value()"
(mentioned earlier) to accept NULL to mean "I just want to know if there
is a value, I don't care what it is". That's what the *_get_<type>()
functions use under the hood (i.e. the ones that return either 0 or 1 or
exit).

This amounts to implementing the "*_config_key_exists()" API you
mentioned, but I think this is better fit for the current set of
semantics. At the very least, that would be an easy 1-1 replacement for
the *_get_string[_tmp]() replacements we make here. There's also the
small benefit of saving one function implementation.

> Another name for this function could have been
> "*_config_key_exists()", as suggested in [1]. That would work for all
> of these callers, and would currently be equivalent to this function,
> as the git_configset_get_value() API normalizes all non-zero return
> values to a "1".
>
> But adding that API would set us up to lose information, as e.g. if
> git_config_parse_key() in the underlying configset_find_element()
> fails we'd like to return -1, not 1.

We were already 'losing' (or rather, not caring about) this information
with the *_get_<type>() functions. The only reason we'd care about this
is if we using git_configset_get_value_multi() or similar.

We replace two callers of git_configset_get_value_multi() in this patch,
but they didn't care about the -1 case anyway...

> --- a/builtin/submodule--helper.c
> +++ b/builtin/submodule--helper.c
> @@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
>  	 * If there are no path args and submodule.active is set then,
>  	 * by default, only initialize 'active' modules.
>  	 */
> -	if (!argc && git_config_get_value_multi("submodule.active"))
> +	if (!argc && !git_config_get("submodule.active"))
>  		module_list_active(&list);
>  
>  	info.prefix = prefix;
> @@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
>  		 * If there are no path args and submodule.active is set then,
>  		 * by default, only initialize 'active' modules.
>  		 */
> -		if (!argc && git_config_get_value_multi("submodule.active"))
> +		if (!argc && !git_config_get("submodule.active"))
>  			module_list_active(&list);
>  
>  		info.prefix = opt.prefix;

Here they are.

> diff --git a/config.h b/config.h
> index ef9eade6414..04c5e594015 100644
> --- a/config.h
> +++ b/config.h
> @@ -471,9 +471,12 @@ void git_configset_clear(struct config_set *cs);
>  
>  /*
>   * These functions return 1 if not found, and 0 if found, leaving the found
> - * value in the 'dest' pointer.
> + * value in the 'dest' pointer (if any).
>   */
>  
> +RESULT_MUST_BE_USED
> +int git_configset_get(struct config_set *cs, const char *key);
> +

As Junio pointed out, git_configset_get() can now return -1, so this
isn't so accurate any more. git_configset_get() is really the exception
here, since all the other functions in this section are the
git_configset_get_*() functions that use git_configset_get_value(). I'd
prefer returning only 0 or 1 for consistency.

>  /*
>   * Finds the highest-priority value for the configuration variable `key`
>   * and config set `cs`, stores the pointer to it in `value` and returns 0.
> @@ -494,6 +497,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha
>  /* Functions for reading a repository's config */
>  struct repository;
>  void repo_config(struct repository *repo, config_fn_t fn, void *data);
> +
> +/**
> + * Run only the discover part of the repo_config_get_*() functions
> + * below, in addition to 1 if not found, returns negative values on
> + * error (e.g. if the key itself is invalid).
> + */
> +RESULT_MUST_BE_USED
> +int repo_config_get(struct repository *repo, const char *key);

This comment is quite a welcome addition. I've found myself losing track
of this information quite often

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 6/9] for-each-repo: error on bad --config
  2023-02-02 13:27       ` [PATCH v4 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
@ 2023-02-06 12:56         ` Glen Choo
  0 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-02-06 12:56 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> Before this, all these added tests would pass with an exit code of 0.
>
> We could preserve the comment added in 6c62f015520, but now that we're
> directly using the documented repo_config_get_value_multi() value it's
> just narrating something that should be obvious from the API use, so
> let's drop it.

[...]

> diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
> index fd0e7739e6a..224164addb3 100644
> --- a/builtin/for-each-repo.c
> +++ b/builtin/for-each-repo.c
> @@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
>  	static const char *config_key = NULL;
>  	int i, result = 0;
>  	const struct string_list *values;
> +	int err;
>  
>  	const struct option options[] = {
>  		OPT_STRING(0, "config", &config_key, N_("config"),
> @@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
>  	if (!config_key)
>  		die(_("missing --config=<config>"));
>  
> -	/*
> -	 * Do nothing on an empty list, which is equivalent to the case
> -	 * where the config variable does not exist at all.
> -	 */
> -	if (repo_config_get_value_multi(the_repository, config_key, &values))
> +	err = repo_config_get_value_multi(the_repository, config_key, &values);
> +	if (err < 0)
> +		usage_msg_optf(_("got bad config --config=%s"),
> +			       for_each_repo_usage, options, config_key);
> +	else if (err)
>  		return 0;

Compared to v3, this change was moved from the previous patch to this
one.

> diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
> index 3648d439a87..6b51e00da0e 100755
> --- a/t/t0068-for-each-repo.sh
> +++ b/t/t0068-for-each-repo.sh
> @@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '
>  	git for-each-repo --config=bogus.config -- help --no-such-option
>  '
>  
> +test_expect_success 'error on bad config keys' '
> +	test_expect_code 129 git for-each-repo --config=a &&
> +	test_expect_code 129 git for-each-repo --config=a.b. &&
> +	test_expect_code 129 git for-each-repo --config="'\''.b"
> +'
> +
>  test_done

And this was moved from patch 1. Both make a lot of sense in this patch,
I think this version reads a bit better.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 8/9] config API: add "string" version of *_value_multi(), fix segfaults
  2023-02-02 13:27       ` [PATCH v4 8/9] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
@ 2023-02-06 13:04         ` Glen Choo
  0 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-02-06 13:04 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> There are now three remaining files using the low-level API:
>
> - Two cases in "builtin/submodule--helper.c", where it's used safely
>   to see if any config exists.
>
>   We could refactor these away from "multi" to some "does it exist?"
>   function, as [4] did, but as that's orthogonal to the "string"
>   safety we're introducing here let's leave them for now.

Maybe I'm mistaken, but weren't these removed in 3/9? I couldn't find
these calls any more.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 2/9] config tests: add "NULL" tests for *_get_value_multi()
  2023-02-06 12:31           ` Ævar Arnfjörð Bjarmason
@ 2023-02-06 16:23             ` Glen Choo
  0 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-02-06 16:23 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> On Mon, Feb 06 2023, Glen Choo wrote:
>
>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>>
>>> +test_NULL_in_multi () {
>>> +	local op="$1" &&
>>> +	local file="$2" &&
>>> +
>>> +	test_expect_success "$op: NULL value in config${file:+ in $file}" '
>>> +		config="$file" &&
>>> +		if test -z "$config"
>>> +		then
>>> +			config=.git/config &&
>>> +			test_when_finished "mv $config.old $config" &&
>>> +			mv "$config" "$config".old
>>> +		fi &&
>>> +
>>> +		cat >"$config" <<-\EOF &&
>>> +		[a]key=x
>>> +		[a]key
>>> +		[a]key=y
>>> +		EOF
>>> +		case "$op" in
>>> +		*_multi)
>>> +			cat >expect <<-\EOF
>>> +			x
>>> +			(NULL)
>>> +			y
>>> +			EOF
>>> +			;;
>>> +		*)
>>> +			cat >expect <<-\EOF
>>> +			y
>>> +			EOF
>>> +			;;
>>> +		esac &&
>>> +		test-tool config "$op" a.key $file >actual &&
>>> +		test_cmp expect actual
>>> +	'
>>> +}
>>> +
>>> +test_NULL_in_multi "get_value_multi"
>>> +test_NULL_in_multi "configset_get_value" "my.config"
>>> +test_NULL_in_multi "configset_get_value_multi" "my.config"
>>
>> I frankly preferred v3's tests over this version. v3 is slightly
>> verbose, but at least the lack of logic made it easy to read and
>> understand. I'd be okay with it if we get a big DRY-ness benefit, but 2
>> conditionals for 3 cases seems quite un-DRY to me.
>
> Note that the v3 version didn't test the get_value_multi(), adjusting
> the v3 version with copy/paste to test that as well is why I made this a
> function. From the CL's range-diff:
>
>     -    When the "t/t1308-config-set.sh" tests were added in [1] only one of
>     -    the three "(NULL)" lines in "t/helper/test-config.c" had any test
>     -    coverage. This change adds tests that stress the remaining two.
>     +    When parts of the config_set API were tested for in [1] they didn't
>     +    add coverage for 3/4 of the "(NULL)" cases handled in
>     +    "t/helper/test-config.c". We'd test that case for "get_value", but not
>     +    "get_value_multi", "configset_get_value" and
>     +    "configset_get_value_multi".
>     +
>     +    We now cover all of those cases, which in turn expose the details of
>     +    how this part of the config API works.
>
> Of course that wouldn't address an outstanding point that we should just
> copy/paste these anyway, but maybe that addresses your feedback...

Ah, yes I noticed that (thanks for confirming), and I meant that I think
it would be better to just copy/paste anyway.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions
  2023-02-02 23:56         ` Junio C Hamano
@ 2023-02-07 10:29           ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 10:29 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Derrick Stolee, Elijah Newren, Jeff King, Taylor Blau,
	SZEDER Gábor, Glen Choo, Calvin Wan, Emily Shaffer, raymond,
	zweiss


On Thu, Feb 02 2023, Junio C Hamano wrote:

> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
>
>> We already have the basic "git_config_get_value()" function and its
>> "repo_*" and "configset" siblings to get a given "key" and assign the
>> last key found to a provided "value".
>>
>> But some callers don't care about that value, but just want to use the
>> return value of the "get_value()" function to check whether the key
>> exist (or another non-zero return value).
>>
>> The immediate motivation for this is that a subsequent commit will
>> need to change all callers of the "*_get_value_multi()" family of
>> functions. In two cases here we (ab)used it to check whether we had
>> any values for the given key, but didn't care about the return value.
>
> So, the idea is that 
>
> 	if (!git_config_get_string(key, &discard))
> 		free(discard);
> 	else
> 		... the key is missing ...
>
> becomes
>
> 	if (git_config_get(key))
> 		... the key is missing ...
>
> In other words, git_config_get() returns 0 only when the key is
> used, and non-zero return signals that the key is not used?
>
> Similarly, get_value_multi() was an interface to get to the
> value_list associated to the given key, and was abused like
>
> 	if (git_config_get_value_multi(key))
> 		... the key exists ...
>
> which will become
>
> 	if (!git_config_get(key))
> 		... the key exists ...
>
> right?

Yes, I've amended this in a re-roll to add tests to the configset tests,
so how the new API should be used becomes obvious.

>> @@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)
>>  	 * is_submodule_active(), since that function needs to find
>>  	 * out the value of "submodule.active" again anyway.
>>  	 */
>> -	if (!git_config_get_string_tmp("submodule.active", &val)) {
>> +	if (!git_config_get("submodule.active")) {
>
> string_tmp() variant is to retrieve borrowed value, and it returns 0
> when there is a value.  If it is a valueless true, we get -1 back
> with an error message.  What does the updated version do in the
> valueless true case?

No, we'll get back 0, as a value-less key exists. 

This sort of code would be correct in general, as if we really want to
check if the key exists a value-less is a valid boolean true.

In this case we happen to segfault later on if we encounter such a key,
but that's unrelated to this being correct.

The segfault is then fixed later in this topic.

>> @@ -2425,8 +2433,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *
>>  
>>  const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
>>  {
>> -	struct config_set_element *e = configset_find_element(cs, key);
>> -	return e ? &e->value_list : NULL;
>> +	struct config_set_element *e;
>> +
>> +	if (configset_find_element(cs, key, &e))
>> +		return NULL;
>> +	else if (!e)
>> +		return NULL;
>> +	return &e->value_list;
>> +}
>
> OK.  !e means "we got a healthy key and peeking into the hash table,
> there wasn't any entry for the key", and that is reported with NULL.
> Do we evern return a string list with .nr == 0, I wonder.  Having to
> deal with such a list would make the caller's job more complex, but
> perhaps we are not allowing the code to shrink value_list.nr to
> avoid such a situation?

No, we never return a list with .nr == 0. That's why Stolee's earlier
RFC tried to solve a subset of the problem this topic addresse by
returning such a list as a way to indicate "does not exist".

That would work to an extent, but would leave the main problem (which
Stolee wasn't aware of at the time) of having a ".nr > 0" list with
"NULL" elements in it.

It also wouldn't be idiomatic with the rest of the API, as this topic
shows, and means you can't distinguish non-existence from other errors.

>> +int git_configset_get(struct config_set *cs, const char *key)
>> +{
>> +	struct config_set_element *e;
>> +	int ret;
>> +
>> +	if ((ret = configset_find_element(cs, key, &e)))
>> +		return ret;
>> +	else if (!e)
>> +		return 1;
>> +	return 0;
>>  }
>
> OK.  So 0 return from the function means there is a value (or more)
> for a given key.  Good.
>
>> diff --git a/config.h b/config.h
>> index ef9eade6414..04c5e594015 100644
>> --- a/config.h
>> +++ b/config.h
>> @@ -471,9 +471,12 @@ void git_configset_clear(struct config_set *cs);
>>  
>>  /*
>>   * These functions return 1 if not found, and 0 if found, leaving the found
>> - * value in the 'dest' pointer.
>> + * value in the 'dest' pointer (if any).
>>   */
>
> Now the returned non-zero values are no longer 1 alone, no?
> Whatever lower-level functions use to signal an error is propagated
> up with the
>
> 	if ((ret = func())
> 		return ret;

That's still mostly true, but I should have added the new
git_configset_get() below this comment, and split it up, i.e. it's still
true for the functions that populate "dest".

I have a topic-on-top to fix those (to propagate them up), and it was
hard to know where to draw the line, in this case I got it wrong. Will
fix it.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v4 3/9] config API: add and use a "git_config_get()" family of functions
  2023-02-06 12:37         ` Glen Choo
@ 2023-02-07 11:52           ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 11:52 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss


On Mon, Feb 06 2023, Glen Choo wrote:

> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>
>> We already have the basic "git_config_get_value()" function and its
>> "repo_*" and "configset" siblings to get a given "key" and assign the
>> last key found to a provided "value".
>>
>> But some callers don't care about that value, but just want to use the
>> return value of the "get_value()" function to check whether the key
>> exist (or another non-zero return value).
>
> [...]
>
>> We could have changed git_configset_get_value_multi() to accept a
>> "NULL" as a "dest" for all callers, but let's avoid changing the
>> behavior of existing API users. Having an "unused" value that we throw
>> away internal to config.c is cheap.
>
> There is yet another option, which is to teach "git_config_get_value()"
> (mentioned earlier) to accept NULL to mean "I just want to know if there
> is a value, I don't care what it is". That's what the *_get_<type>()
> functions use under the hood (i.e. the ones that return either 0 or 1 or
> exit).

I've clarified the commit message, but that's the same as what this is
describing. I.e. I meant "git_config_get_value_multi() and the functions
that are wrapping it", which is "git_config_get_value()" etc.

> This amounts to implementing the "*_config_key_exists()" API you
> mentioned, but I think this is better fit for the current set of
> semantics. At the very least, that would be an easy 1-1 replacement for
> the *_get_string[_tmp]() replacements we make here. There's also the
> small benefit of saving one function implementation.

I think this is the wrong approach, and have updated the commit message
further to advocate for this one.

>> Another name for this function could have been
>> "*_config_key_exists()", as suggested in [1]. That would work for all
>> of these callers, and would currently be equivalent to this function,
>> as the git_configset_get_value() API normalizes all non-zero return
>> values to a "1".
>>
>> But adding that API would set us up to lose information, as e.g. if
>> git_config_parse_key() in the underlying configset_find_element()
>> fails we'd like to return -1, not 1.
>
> We were already 'losing' (or rather, not caring about) this information
> with the *_get_<type>() functions. The only reason we'd care about this
> is if we using git_configset_get_value_multi() or similar.
>
> We replace two callers of git_configset_get_value_multi() in this patch,
> but they didn't care about the -1 case anyway...
> [...]
> As Junio pointed out, git_configset_get() can now return -1, so this
> isn't so accurate any more. git_configset_get() is really the exception
> here, since all the other functions in this section are the
> git_configset_get_*() functions that use git_configset_get_value(). I'd
> prefer returning only 0 or 1 for consistency.

I really prefer not clobbering these return values, but I take your
point that the end-state here is inconsistent.

I figured that I could fix it for the APIs added here, and follow-up
(with a patch I already had mostly ready) after this series to fix the
remaining config API warts.

I won't fix all of those in the incoming re-roll of this, but I'll fix
this issue, i.e. we'll consistently ferry up "ret", and stop normalizing
non-zero-non-1 to "return 1".

^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret"
  2023-02-02 13:27     ` [PATCH v4 " Ævar Arnfjörð Bjarmason
                         ` (8 preceding siblings ...)
  2023-02-02 13:27       ` [PATCH v4 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10       ` Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 01/10] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
                           ` (11 more replies)
  9 siblings, 12 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

This series fixes numerous segfaults in config API users, because they
didn't expect *_get_multi() to hand them a string_list with a NULL in
it given config like "[a] key" (note, no "="'s).

A larger general overview at v1[1], but note the API changes in
v2[2]. Changes since v4[3]:

* Added tests for value-less at the end of a list to 2/10, per Junio's
  request.

* Clarifications in the 3/10 commit message, per v4's discussion.

* Add RESULT_MUST_BE_USED to configset_find_element(), note that
  configset_add_value() doesn't have it, see the 3/10 commit message
  update.

* 3/10 now has tests for the "get" family of functions, this should
  clarify any questions about the semantics of the API.

* Junio suggested for 4/10 to skip renaming the
  "deprecated_prereleases" variable, I tried that, and if we keep it
  we need to wrap a line later in the series, which makes the
  versioncmp.c code harder to read. So I kept the renaming as it's
  refactored in 4/10.

* Glen suggested that the new *_get() family should "return 1" on
  non-zero like the rest of *_get_*() (i.e. coerce 'ret < 0' to
  'return 1').

  That could be done here, but for the later "for-each-repo.c" we need
  to distinguish "bad key" v.s. "does this exist?", and just having
  that API return a more meaningful value would make it inconsistent
  with the rest.

  As the much of the point of this series is to make that API less of
  a special snowflake a new 6/10 instead finishes up the work of
  having most of the rest of the API return the un-coerced "ret" from
  the depths of the config API.

  That patch is quite large by line count, but pretty trivial in
  complexity. All of those functions are copy/pasted versions of one
  another with very minor variations.

* Updated the 8/10 commit message, which was stale from a previous
  version of this topic.

Branch & CI for this at:
https://github.com/avar/git/tree/avar/have-git_configset_get_value-use-dest-and-int-pattern-5

1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/
2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/
3. https://lore.kernel.org/git/cover-v4-0.9-00000000000-20230202T131155Z-avarab@gmail.com/

Ævar Arnfjörð Bjarmason (10):
  config tests: cover blind spots in git_die_config() tests
  config tests: add "NULL" tests for *_get_value_multi()
  config API: add and use a "git_config_get()" family of functions
  versioncmp.c: refactor config reading next commit
  config API: have *_multi() return an "int" and take a "dest"
  config API: don't lose the git_*get*() return values
  for-each-repo: error on bad --config
  config API users: test for *_get_value_multi() segfaults
  config API: add "string" version of *_value_multi(), fix segfaults
  for-each-repo: with bad config, don't conflate <path> and <cmd>

 builtin/for-each-repo.c              |  14 +-
 builtin/gc.c                         |  15 +-
 builtin/log.c                        |   6 +-
 builtin/submodule--helper.c          |   7 +-
 builtin/worktree.c                   |   3 +-
 config.c                             | 226 ++++++++++++++++++---------
 config.h                             |  84 ++++++++--
 pack-bitmap.c                        |   6 +-
 submodule.c                          |   3 +-
 t/helper/test-config.c               |  28 +++-
 t/t0068-for-each-repo.sh             |  19 +++
 t/t1308-config-set.sh                | 108 ++++++++++++-
 t/t3309-notes-merge-auto-resolve.sh  |   7 +-
 t/t4202-log.sh                       |  15 ++
 t/t5304-prune.sh                     |  12 +-
 t/t5310-pack-bitmaps.sh              |  20 +++
 t/t5552-skipping-fetch-negotiator.sh |  16 ++
 t/t7004-tag.sh                       |  17 ++
 t/t7413-submodule-is-active.sh       |  16 ++
 t/t7900-maintenance.sh               |  38 +++++
 versioncmp.c                         |  22 ++-
 21 files changed, 549 insertions(+), 133 deletions(-)

Range-diff against v4:
 1:  4ae56cab7c7 =  1:  cefc4188984 config tests: cover blind spots in git_die_config() tests
 2:  1f0f8bdcde9 !  2:  91a44456327 config tests: add "NULL" tests for *_get_value_multi()
    @@ t/t1308-config-set.sh: test_expect_success 'find multiple values' '
     +			mv "$config" "$config".old
     +		fi &&
     +
    ++		# Value-less in the middle of a list
     +		cat >"$config" <<-\EOF &&
     +		[a]key=x
     +		[a]key
    @@ t/t1308-config-set.sh: test_expect_success 'find multiple values' '
     +			;;
     +		esac &&
     +		test-tool config "$op" a.key $file >actual &&
    ++		test_cmp expect actual &&
    ++
    ++		# Value-less at the end of a least
    ++		cat >"$config" <<-\EOF &&
    ++		[a]key=x
    ++		[a]key=y
    ++		[a]key
    ++		EOF
    ++		case "$op" in
    ++		*_multi)
    ++			cat >expect <<-\EOF
    ++			x
    ++			y
    ++			(NULL)
    ++			EOF
    ++			;;
    ++		*)
    ++			cat >expect <<-\EOF
    ++			(NULL)
    ++			EOF
    ++			;;
    ++		esac &&
    ++		test-tool config "$op" a.key $file >actual &&
     +		test_cmp expect actual
     +	'
     +}
 3:  998b11ae4bc !  3:  4a73151abde config API: add and use a "git_config_get()" family of functions
    @@ Commit message
         can now use a helper function that doesn't require a throwaway
         variable.
     
    -    We could have changed git_configset_get_value_multi() to accept a
    -    "NULL" as a "dest" for all callers, but let's avoid changing the
    -    behavior of existing API users. Having an "unused" value that we throw
    -    away internal to config.c is cheap.
    +    We could have changed git_configset_get_value_multi() (and then
    +    git_config_get_value() etc.) to accept a "NULL" as a "dest" for all
    +    callers, but let's avoid changing the behavior of existing API
    +    users. Having an "unused" value that we throw away internal to
    +    config.c is cheap.
    +
    +    A "NULL as optional dest" pattern is also more fragile, as the intent
    +    of the caller might be misinterpreted if he were to accidentally pass
    +    "NULL", e.g. when "dest" is passed in from another function.
     
         Another name for this function could have been
         "*_config_key_exists()", as suggested in [1]. That would work for all
    @@ Commit message
         commit where the git_configset_get_value_multi() function itself will
         expose this new return value.
     
    -    1. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/
    +    This still leaves various inconsistencies and clobbering or ignoring
    +    of the return value in place. E.g here we're modifying
    +    configset_add_value(), but ever since it was added in [2] we've been
    +    ignoring its "int" return value, but as we're changing the
    +    configset_find_element() it uses, let's have it faithfully ferry that
    +    "ret" along.
    +
    +    Let's also use the "RESULT_MUST_BE_USED" macro introduced in [3] to
    +    assert that we're checking the return value of
    +    configset_find_element().
    +
    +    We're leaving the same change to configset_add_value() for some future
    +    series. Once we start paying attention to its return value we'd need
    +    to ferry it up as deep as do_config_from(), and would need to make
    +    least read_{,very_}early_config() and git_protected_config() return an
    +    "int" instead of "void". Let's leave that for now, and focus on
    +    the *_get_*() functions.
    +
    +    In a subsequent commit we'll fix the other *_get_*() functions to so
    +    that they'll ferry our underlying "ret" along, rather than normalizing
    +    it to a "return 1". But as an intermediate step to that we'll need to
    +    fix git_configset_get_value_multi() to return "int", and that change
    +    itself is smaller because of this change to migrate some callers away
    +    from the *_value_multi() API.
    +
    +    1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)
    +    2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/
    +    3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
    +       return values, 2022-09-01),
     
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
    @@ config.c: void read_very_early_config(config_fn_t cb, void *data)
      }
      
     -static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
    ++RESULT_MUST_BE_USED
     +static int configset_find_element(struct config_set *cs, const char *key,
     +				  struct config_set_element **dest)
      {
    @@ config.c: void git_config_clear(void)
     
      ## config.h ##
     @@ config.h: void git_configset_clear(struct config_set *cs);
    - 
    - /*
    -  * These functions return 1 if not found, and 0 if found, leaving the found
    -- * value in the 'dest' pointer.
    -+ * value in the 'dest' pointer (if any).
    +  * value in the 'dest' pointer.
       */
      
     +RESULT_MUST_BE_USED
    @@ config.h: void git_protected_config(config_fn_t fn, void *data);
     + * documentation.
       */
      
    ++RESULT_MUST_BE_USED
     +int git_config_get(const char *key);
     +
      /**
       * Finds the highest-priority value for the configuration variable `key`,
       * stores the pointer to it in `value` and returns 0. When the
    +
    + ## t/helper/test-config.c ##
    +@@
    +  * get_value_multi -> prints all values for the entered key in increasing order
    +  *		     of priority
    +  *
    ++ * get -> print return value for the entered key
    ++ *
    +  * get_int -> print integer value for the entered key or die
    +  *
    +  * get_bool -> print bool value for the entered key or die
    +@@ t/helper/test-config.c: int cmd__config(int argc, const char **argv)
    + 			printf("Value not found for \"%s\"\n", argv[2]);
    + 			goto exit1;
    + 		}
    ++	} else if (argc == 3 && !strcmp(argv[1], "get")) {
    ++		int ret;
    ++
    ++		if (!(ret = git_config_get(argv[2])))
    ++			goto exit0;
    ++		else if (ret == 1)
    ++			printf("Value not found for \"%s\"\n", argv[2]);
    ++		else if (ret == -CONFIG_INVALID_KEY)
    ++			printf("Key \"%s\" is invalid\n", argv[2]);
    ++		else if (ret == -CONFIG_NO_SECTION_OR_NAME)
    ++			printf("Key \"%s\" has no section\n", argv[2]);
    ++		else
    ++			/*
    ++			 * A normal caller should just check "ret <
    ++			 * 0", but for our own tests let's BUG() if
    ++			 * our whitelist of git_config_parse_key()
    ++			 * return values isn't exhaustive.
    ++			 */
    ++			BUG("Key \"%s\" has unknown return %d", argv[2], ret);
    ++		goto exit1;
    + 	} else if (argc == 3 && !strcmp(argv[1], "get_int")) {
    + 		if (!git_config_get_int(argv[2], &val)) {
    + 			printf("%d\n", val);
    +
    + ## t/t1308-config-set.sh ##
    +@@ t/t1308-config-set.sh: test_expect_success 'setup default config' '
    + 		skin = false
    + 		nose = 1
    + 		horns
    ++	[value]
    ++		less
    + 	EOF
    + '
    + 
    +@@ t/t1308-config-set.sh: test_expect_success 'find value with the highest priority' '
    + 	check_config get_value case.baz "hask"
    + '
    + 
    ++test_expect_success 'return value for an existing key' '
    ++	test-tool config get lamb.chop >out 2>err &&
    ++	test_must_be_empty out &&
    ++	test_must_be_empty err
    ++'
    ++
    ++test_expect_success 'return value for value-less key' '
    ++	test-tool config get value.less >out 2>err &&
    ++	test_must_be_empty out &&
    ++	test_must_be_empty err
    ++'
    ++
    ++test_expect_success 'return value for a missing key' '
    ++	cat >expect <<-\EOF &&
    ++	Value not found for "missing.key"
    ++	EOF
    ++	test_expect_code 1 test-tool config get missing.key >actual 2>err &&
    ++	test_cmp actual expect &&
    ++	test_must_be_empty err
    ++'
    ++
    ++test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '
    ++	cat >expect <<-\EOF &&
    ++	Key "fails.iskeychar.-" is invalid
    ++	EOF
    ++	test_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&
    ++	test_cmp actual expect &&
    ++	test_must_be_empty out
    ++'
    ++
    ++test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '
    ++	cat >expect <<-\EOF &&
    ++	Key "keynosection" has no section
    ++	EOF
    ++	test_expect_code 1 test-tool config get keynosection >actual 2>err &&
    ++	test_cmp actual expect &&
    ++	test_must_be_empty out
    ++'
    ++
    + test_expect_success 'find integer value for a key' '
    + 	check_config get_int lamb.chop 65
    + '
    +@@ t/t1308-config-set.sh: test_expect_success 'proper error on error in default config files' '
    + 	cp .git/config .git/config.old &&
    + 	test_when_finished "mv .git/config.old .git/config" &&
    + 	echo "[" >>.git/config &&
    +-	echo "fatal: bad config line 34 in file .git/config" >expect &&
    ++	echo "fatal: bad config line 36 in file .git/config" >expect &&
    + 	test_expect_code 128 test-tool config get_value foo.bar 2>actual &&
    + 	test_cmp expect actual
    + '
 4:  aae1d5c12a9 !  4:  382a77ca69e versioncmp.c: refactor config reading next commit
    @@ Commit message
         the bounds of our CodingGuidelines when it comes to line length, and
         to avoid repeating ourselves.
     
    +    Renaming "deprecated_prereleases" to "oldl" doesn't help us to avoid
    +    line wrapping now, but it will in a subsequent commit.
    +
         Let's also split out the names of the config variables into variables
    -    of our own, so we don't have to repeat ourselves, and refactor the
    -    nested if/else to avoid indenting it, and the existing bracing style
    -    issue.
    +    of our own, and refactor the nested if/else to avoid indenting it, and
    +    the existing bracing style issue.
     
         This all helps with the subsequent commit, where we'll need to start
         checking different git_config_get_value_multi() return value. See
 5:  23449ff2c4e !  5:  8f17bf8150c config API: have *_multi() return an "int" and take a "dest"
    @@ Commit message
         return an "int" and populate a "**dest" parameter like every other
         git_configset_get_*()" in the API.
     
    -    As we'll see in in subsequent commits this fixes a blind spot in the
    -    API where it wasn't possible to tell whether a list was empty from
    -    whether a config key existed. We'll take advantage of that in
    -    subsequent commits, but for now we're faithfully converting existing
    -    API callers.
    -
    -    A logical follow-up to this would be to change the various "*_get_*()"
    -    functions to ferry the git_configset_get_value() return value to their
    -    own callers, e.g. git_configset_get_int() returns "1" rather than
    -    ferrying up the "-1" that "git_configset_get_value()" might return,
    -    but that's not being done in this series
    +    As we'll take advantage of in subsequent commits, this fixes a blind
    +    spot in the API where it wasn't possible to tell whether a list was
    +    empty from whether a config key existed. For now we don't make use of
    +    those new return values, but faithfully convert existing API users.
     
         Most of this is straightforward, commentary on cases that stand out:
     
 -:  ----------- >  6:  b515ff13f9b config API: don't lose the git_*get*() return values
 6:  17c1218e74c =  7:  8a83c30ea78 for-each-repo: error on bad --config
 7:  7fc91eaf747 =  8:  d9abc78c2be config API users: test for *_get_value_multi() segfaults
 8:  a391ee17617 !  9:  65fa91e7ce7 config API: add "string" version of *_value_multi(), fix segfaults
    @@ Commit message
           - 92156291ca8 (log: add default decoration filter, 2022-08-05)
           - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)
     
    -    There are now three remaining files using the low-level API:
    -
    -    - Two cases in "builtin/submodule--helper.c", where it's used safely
    -      to see if any config exists.
    -
    -      We could refactor these away from "multi" to some "does it exist?"
    -      function, as [4] did, but as that's orthogonal to the "string"
    -      safety we're introducing here let's leave them for now.
    +    There are now two users ofthe low-level API:
     
         - One in "builtin/for-each-repo.c", which we'll convert in a
           subsequent commit.
     
    -    - The "t/helper/test-config.c" code added in [4].
    +    - The "t/helper/test-config.c" code added in [3].
     
         As seen in the preceding commit we need to give the
         "t/helper/test-config.c" caller these "NULL" entries.
    @@ Commit message
            2008-02-11)
         2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,
            2008-02-11).
    -    3. https://lore.kernel.org/git/patch-07.10-c01f7d85c94-20221026T151328Z-avarab@gmail.com/
    -    4. 4c715ebb96a (test-config: add tests for the config_set API,
    +    3. 4c715ebb96a (test-config: add tests for the config_set API,
            2014-07-28)
     
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
 9:  c7a5f5b4133 = 10:  4db3c6d0ed9 for-each-repo: with bad config, don't conflate <path> and <cmd>
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v5 01/10] config tests: cover blind spots in git_die_config() tests
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10         ` Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 02/10] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
                           ` (10 subsequent siblings)
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

There were no tests checking for the output of the git_die_config()
function in the config API, added in 5a80e97c827 (config: add
`git_die_config()` to the config-set API, 2014-08-07). We only tested
"test_must_fail", but didn't assert the output.

We need tests for this because a subsequent commit will alter the
return value of git_config_get_value_multi(), which is used to get the
config values in the git_die_config() function. This test coverage
helps to build confidence in that subsequent change.

These tests cover different interactions with git_die_config():

- The "notes.mergeStrategy" test in
  "t/t3309-notes-merge-auto-resolve.sh" is a case where a function
  outside of config.c (git_config_get_notes_strategy()) calls
  git_die_config().

- The "gc.pruneExpire" test in "t5304-prune.sh" is a case where
  git_config_get_expiry() calls git_die_config(), covering a different
  "type" than the "string" test for "notes.mergeStrategy".

- The "fetch.negotiationAlgorithm" test in
  "t/t5552-skipping-fetch-negotiator.sh" is a case where
  git_config_get_string*() calls git_die_config().

We also cover both the "from command-line config" and "in file..at
line" cases here.

The clobbering of existing ".git/config" files here is so that we're
not implicitly testing the line count of the default config.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-
 t/t5304-prune.sh                     | 12 ++++++++++--
 t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh
index 141d3e4ca4d..9bd5dbf341f 100755
--- a/t/t3309-notes-merge-auto-resolve.sh
+++ b/t/t3309-notes-merge-auto-resolve.sh
@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '
 
 test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '
 	git config core.notesRef refs/notes/y &&
-	test_must_fail git -c notes.mergeStrategy="foo" notes merge z &&
+	cat >expect <<-\EOF &&
+	error: unknown notes merge strategy foo
+	fatal: unable to parse '\''notes.mergeStrategy'\'' from command-line config
+	EOF
+	test_must_fail git -c notes.mergeStrategy="foo" notes merge z 2>actual &&
+	test_cmp expect actual &&
 	# Verify no changes (y)
 	verify_notes y y
 '
diff --git a/t/t5304-prune.sh b/t/t5304-prune.sh
index d65a5f94b4b..5500dd08426 100755
--- a/t/t5304-prune.sh
+++ b/t/t5304-prune.sh
@@ -72,8 +72,16 @@ test_expect_success 'gc: implicit prune --expire' '
 '
 
 test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '
-	git config gc.pruneExpire invalid &&
-	test_must_fail git gc
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	>repo/.git/config &&
+	git -C repo config gc.pruneExpire invalid &&
+	cat >expect <<-\EOF &&
+	error: Invalid gc.pruneexpire: '\''invalid'\''
+	fatal: bad config variable '\''gc.pruneexpire'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_must_fail git -C repo gc 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'gc: start with ok gc.pruneExpire' '
diff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh
index 165427d57e5..b55a9f65e6b 100755
--- a/t/t5552-skipping-fetch-negotiator.sh
+++ b/t/t5552-skipping-fetch-negotiator.sh
@@ -3,6 +3,22 @@
 test_description='test skipping fetch negotiator'
 . ./test-lib.sh
 
+test_expect_success 'fetch.negotiationalgorithm config' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	cat >repo/.git/config <<-\EOF &&
+	[fetch]
+	negotiationAlgorithm
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''fetch.negotiationalgorithm'\''
+	fatal: bad config variable '\''fetch.negotiationalgorithm'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_expect_code 128 git -C repo fetch >out 2>actual &&
+	test_must_be_empty out &&
+	test_cmp expect actual
+'
+
 have_sent () {
 	while test "$#" -ne 0
 	do
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v5 02/10] config tests: add "NULL" tests for *_get_value_multi()
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 01/10] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10         ` Ævar Arnfjörð Bjarmason
  2023-02-09  4:00           ` Glen Choo
  2023-02-07 16:10         ` [PATCH v5 03/10] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
                           ` (9 subsequent siblings)
  11 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

A less well known edge case in the config format is that keys can be
value-less, a shorthand syntax for "true" boolean keys. I.e. these two
are equivalent as far as "--type=bool" is concerned:

	[a]key
	[a]key = true

But as far as our parser is concerned the values for these two are
NULL, and "true". I.e. for a sequence like:

	[a]key=x
	[a]key
	[a]key=y

We get a "struct string_list" with "string" members with ".string"
values of:

	{ "x", NULL, "y" }

This behavior goes back to the initial implementation of
git_config_bool() in 17712991a59 (Add ".git/config" file parser,
2005-10-10).

When parts of the config_set API were tested for in [1] they didn't
add coverage for 3/4 of the "(NULL)" cases handled in
"t/helper/test-config.c". We'd test that case for "get_value", but not
"get_value_multi", "configset_get_value" and
"configset_get_value_multi".

We now cover all of those cases, which in turn expose the details of
how this part of the config API works.

1. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t1308-config-set.sh | 65 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 65 insertions(+)

diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index b38e158d3b2..4be1ab1147c 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -146,6 +146,71 @@ test_expect_success 'find multiple values' '
 	check_config get_value_multi case.baz sam bat hask
 '
 
+test_NULL_in_multi () {
+	local op="$1" &&
+	local file="$2" &&
+
+	test_expect_success "$op: NULL value in config${file:+ in $file}" '
+		config="$file" &&
+		if test -z "$config"
+		then
+			config=.git/config &&
+			test_when_finished "mv $config.old $config" &&
+			mv "$config" "$config".old
+		fi &&
+
+		# Value-less in the middle of a list
+		cat >"$config" <<-\EOF &&
+		[a]key=x
+		[a]key
+		[a]key=y
+		EOF
+		case "$op" in
+		*_multi)
+			cat >expect <<-\EOF
+			x
+			(NULL)
+			y
+			EOF
+			;;
+		*)
+			cat >expect <<-\EOF
+			y
+			EOF
+			;;
+		esac &&
+		test-tool config "$op" a.key $file >actual &&
+		test_cmp expect actual &&
+
+		# Value-less at the end of a least
+		cat >"$config" <<-\EOF &&
+		[a]key=x
+		[a]key=y
+		[a]key
+		EOF
+		case "$op" in
+		*_multi)
+			cat >expect <<-\EOF
+			x
+			y
+			(NULL)
+			EOF
+			;;
+		*)
+			cat >expect <<-\EOF
+			(NULL)
+			EOF
+			;;
+		esac &&
+		test-tool config "$op" a.key $file >actual &&
+		test_cmp expect actual
+	'
+}
+
+test_NULL_in_multi "get_value_multi"
+test_NULL_in_multi "configset_get_value" "my.config"
+test_NULL_in_multi "configset_get_value_multi" "my.config"
+
 test_expect_success 'find value from a configset' '
 	cat >config2 <<-\EOF &&
 	[case]
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v5 03/10] config API: add and use a "git_config_get()" family of functions
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 01/10] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 02/10] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10         ` Ævar Arnfjörð Bjarmason
  2023-02-09  8:24           ` Glen Choo
  2023-02-07 16:10         ` [PATCH v5 04/10] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
                           ` (8 subsequent siblings)
  11 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

We already have the basic "git_config_get_value()" function and its
"repo_*" and "configset" siblings to get a given "key" and assign the
last key found to a provided "value".

But some callers don't care about that value, but just want to use the
return value of the "get_value()" function to check whether the key
exist (or another non-zero return value).

The immediate motivation for this is that a subsequent commit will
need to change all callers of the "*_get_value_multi()" family of
functions. In two cases here we (ab)used it to check whether we had
any values for the given key, but didn't care about the return value.

The rest of the callers here used various other config API functions
to do the same, all of which resolved to the same underlying functions
to provide the answer.

Some of these were using either git_config_get_string() or
git_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a
configure_added_submodule() leak, 2022-09-01) for a recent example. We
can now use a helper function that doesn't require a throwaway
variable.

We could have changed git_configset_get_value_multi() (and then
git_config_get_value() etc.) to accept a "NULL" as a "dest" for all
callers, but let's avoid changing the behavior of existing API
users. Having an "unused" value that we throw away internal to
config.c is cheap.

A "NULL as optional dest" pattern is also more fragile, as the intent
of the caller might be misinterpreted if he were to accidentally pass
"NULL", e.g. when "dest" is passed in from another function.

Another name for this function could have been
"*_config_key_exists()", as suggested in [1]. That would work for all
of these callers, and would currently be equivalent to this function,
as the git_configset_get_value() API normalizes all non-zero return
values to a "1".

But adding that API would set us up to lose information, as e.g. if
git_config_parse_key() in the underlying configset_find_element()
fails we'd like to return -1, not 1.

Let's change the underlying configset_find_element() function to
support this use-case, we'll make further use of it in a subsequent
commit where the git_configset_get_value_multi() function itself will
expose this new return value.

This still leaves various inconsistencies and clobbering or ignoring
of the return value in place. E.g here we're modifying
configset_add_value(), but ever since it was added in [2] we've been
ignoring its "int" return value, but as we're changing the
configset_find_element() it uses, let's have it faithfully ferry that
"ret" along.

Let's also use the "RESULT_MUST_BE_USED" macro introduced in [3] to
assert that we're checking the return value of
configset_find_element().

We're leaving the same change to configset_add_value() for some future
series. Once we start paying attention to its return value we'd need
to ferry it up as deep as do_config_from(), and would need to make
least read_{,very_}early_config() and git_protected_config() return an
"int" instead of "void". Let's leave that for now, and focus on
the *_get_*() functions.

In a subsequent commit we'll fix the other *_get_*() functions to so
that they'll ferry our underlying "ret" along, rather than normalizing
it to a "return 1". But as an intermediate step to that we'll need to
fix git_configset_get_value_multi() to return "int", and that change
itself is smaller because of this change to migrate some callers away
from the *_value_multi() API.

1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)
2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/
3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
   return values, 2022-09-01),

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                |  5 +---
 builtin/submodule--helper.c |  7 +++--
 builtin/worktree.c          |  3 +--
 config.c                    | 51 ++++++++++++++++++++++++++++++++-----
 config.h                    | 18 +++++++++++++
 t/helper/test-config.c      | 22 ++++++++++++++++
 t/t1308-config-set.sh       | 43 ++++++++++++++++++++++++++++++-
 7 files changed, 131 insertions(+), 18 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 02455fdcd73..e38d1783f30 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1493,7 +1493,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	};
 	int found = 0;
 	const char *key = "maintenance.repo";
-	char *config_value;
 	char *maintpath = get_maintpath();
 	struct string_list_item *item;
 	const struct string_list *list;
@@ -1508,9 +1507,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	git_config_set("maintenance.auto", "false");
 
 	/* Set maintenance strategy, if unset */
-	if (!git_config_get_string("maintenance.strategy", &config_value))
-		free(config_value);
-	else
+	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
 	list = git_config_get_value_multi(key);
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 4c173d8b37a..2278e8c91cb 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 	 * If there are no path args and submodule.active is set then,
 	 * by default, only initialize 'active' modules.
 	 */
-	if (!argc && git_config_get_value_multi("submodule.active"))
+	if (!argc && !git_config_get("submodule.active"))
 		module_list_active(&list);
 
 	info.prefix = prefix;
@@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 		 * If there are no path args and submodule.active is set then,
 		 * by default, only initialize 'active' modules.
 		 */
-		if (!argc && git_config_get_value_multi("submodule.active"))
+		if (!argc && !git_config_get("submodule.active"))
 			module_list_active(&list);
 
 		info.prefix = opt.prefix;
@@ -3140,7 +3140,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con
 static void configure_added_submodule(struct add_data *add_data)
 {
 	char *key;
-	const char *val;
 	struct child_process add_submod = CHILD_PROCESS_INIT;
 	struct child_process add_gitmodules = CHILD_PROCESS_INIT;
 
@@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)
 	 * is_submodule_active(), since that function needs to find
 	 * out the value of "submodule.active" again anyway.
 	 */
-	if (!git_config_get_string_tmp("submodule.active", &val)) {
+	if (!git_config_get("submodule.active")) {
 		/*
 		 * If the submodule being added isn't already covered by the
 		 * current configured pathspec, set the submodule's active flag
diff --git a/builtin/worktree.c b/builtin/worktree.c
index f51c40f1e1e..6ba42d4ad20 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -319,7 +319,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 
 	if (file_exists(from_file)) {
 		struct config_set cs = { { 0 } };
-		const char *core_worktree;
 		int bare;
 
 		if (safe_create_leading_directories(to_file) ||
@@ -338,7 +337,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 				to_file, "core.bare", NULL, "true", 0))
 			error(_("failed to unset '%s' in '%s'"),
 				"core.bare", to_file);
-		if (!git_configset_get_value(&cs, "core.worktree", &core_worktree) &&
+		if (!git_configset_get(&cs, "core.worktree") &&
 			git_config_set_in_file_gently(to_file,
 							"core.worktree", NULL))
 			error(_("failed to unset '%s' in '%s'"),
diff --git a/config.c b/config.c
index 00090a32fc3..d4f0e4fd619 100644
--- a/config.c
+++ b/config.c
@@ -2289,23 +2289,29 @@ void read_very_early_config(config_fn_t cb, void *data)
 	config_with_options(cb, data, NULL, &opts);
 }
 
-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
+RESULT_MUST_BE_USED
+static int configset_find_element(struct config_set *cs, const char *key,
+				  struct config_set_element **dest)
 {
 	struct config_set_element k;
 	struct config_set_element *found_entry;
 	char *normalized_key;
+	int ret;
+
 	/*
 	 * `key` may come from the user, so normalize it before using it
 	 * for querying entries from the hashmap.
 	 */
-	if (git_config_parse_key(key, &normalized_key, NULL))
-		return NULL;
+	ret = git_config_parse_key(key, &normalized_key, NULL);
+	if (ret)
+		return ret;
 
 	hashmap_entry_init(&k.ent, strhash(normalized_key));
 	k.key = normalized_key;
 	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
 	free(normalized_key);
-	return found_entry;
+	*dest = found_entry;
+	return 0;
 }
 
 static int configset_add_value(struct config_set *cs, const char *key, const char *value)
@@ -2314,8 +2320,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha
 	struct string_list_item *si;
 	struct configset_list_item *l_item;
 	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
+	int ret;
 
-	e = configset_find_element(cs, key);
+	ret = configset_find_element(cs, key, &e);
+	if (ret)
+		return ret;
 	/*
 	 * Since the keys are being fed by git_config*() callback mechanism, they
 	 * are already normalized. So simply add them without any further munging.
@@ -2425,8 +2434,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *
 
 const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
 {
-	struct config_set_element *e = configset_find_element(cs, key);
-	return e ? &e->value_list : NULL;
+	struct config_set_element *e;
+
+	if (configset_find_element(cs, key, &e))
+		return NULL;
+	else if (!e)
+		return NULL;
+	return &e->value_list;
+}
+
+int git_configset_get(struct config_set *cs, const char *key)
+{
+	struct config_set_element *e;
+	int ret;
+
+	if ((ret = configset_find_element(cs, key, &e)))
+		return ret;
+	else if (!e)
+		return 1;
+	return 0;
 }
 
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
@@ -2565,6 +2591,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)
 	configset_iter(repo->config, fn, data);
 }
 
+int repo_config_get(struct repository *repo, const char *key)
+{
+	git_config_check_init(repo);
+	return git_configset_get(repo->config, key);
+}
+
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value)
 {
@@ -2679,6 +2711,11 @@ void git_config_clear(void)
 	repo_config_clear(the_repository);
 }
 
+int git_config_get(const char *key)
+{
+	return repo_config_get(the_repository, key);
+}
+
 int git_config_get_value(const char *key, const char **value)
 {
 	return repo_config_get_value(the_repository, key, value);
diff --git a/config.h b/config.h
index ef9eade6414..d016d05460d 100644
--- a/config.h
+++ b/config.h
@@ -474,6 +474,9 @@ void git_configset_clear(struct config_set *cs);
  * value in the 'dest' pointer.
  */
 
+RESULT_MUST_BE_USED
+int git_configset_get(struct config_set *cs, const char *key);
+
 /*
  * Finds the highest-priority value for the configuration variable `key`
  * and config set `cs`, stores the pointer to it in `value` and returns 0.
@@ -494,6 +497,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha
 /* Functions for reading a repository's config */
 struct repository;
 void repo_config(struct repository *repo, config_fn_t fn, void *data);
+
+/**
+ * Run only the discover part of the repo_config_get_*() functions
+ * below, in addition to 1 if not found, returns negative values on
+ * error (e.g. if the key itself is invalid).
+ */
+RESULT_MUST_BE_USED
+int repo_config_get(struct repository *repo, const char *key);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
 const struct string_list *repo_config_get_value_multi(struct repository *repo,
@@ -530,8 +541,15 @@ void git_protected_config(config_fn_t fn, void *data);
  * manner, the config API provides two functions `git_config_get_value`
  * and `git_config_get_value_multi`. They both read values from an internal
  * cache generated previously from reading the config files.
+ *
+ * For those git_config_get*() functions that aren't documented,
+ * consult the corresponding repo_config_get*() function's
+ * documentation.
  */
 
+RESULT_MUST_BE_USED
+int git_config_get(const char *key);
+
 /**
  * Finds the highest-priority value for the configuration variable `key`,
  * stores the pointer to it in `value` and returns 0. When the
diff --git a/t/helper/test-config.c b/t/helper/test-config.c
index 4ba9eb65606..cbb33ae1fff 100644
--- a/t/helper/test-config.c
+++ b/t/helper/test-config.c
@@ -14,6 +14,8 @@
  * get_value_multi -> prints all values for the entered key in increasing order
  *		     of priority
  *
+ * get -> print return value for the entered key
+ *
  * get_int -> print integer value for the entered key or die
  *
  * get_bool -> print bool value for the entered key or die
@@ -109,6 +111,26 @@ int cmd__config(int argc, const char **argv)
 			printf("Value not found for \"%s\"\n", argv[2]);
 			goto exit1;
 		}
+	} else if (argc == 3 && !strcmp(argv[1], "get")) {
+		int ret;
+
+		if (!(ret = git_config_get(argv[2])))
+			goto exit0;
+		else if (ret == 1)
+			printf("Value not found for \"%s\"\n", argv[2]);
+		else if (ret == -CONFIG_INVALID_KEY)
+			printf("Key \"%s\" is invalid\n", argv[2]);
+		else if (ret == -CONFIG_NO_SECTION_OR_NAME)
+			printf("Key \"%s\" has no section\n", argv[2]);
+		else
+			/*
+			 * A normal caller should just check "ret <
+			 * 0", but for our own tests let's BUG() if
+			 * our whitelist of git_config_parse_key()
+			 * return values isn't exhaustive.
+			 */
+			BUG("Key \"%s\" has unknown return %d", argv[2], ret);
+		goto exit1;
 	} else if (argc == 3 && !strcmp(argv[1], "get_int")) {
 		if (!git_config_get_int(argv[2], &val)) {
 			printf("%d\n", val);
diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index 4be1ab1147c..7def7053e1c 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -58,6 +58,8 @@ test_expect_success 'setup default config' '
 		skin = false
 		nose = 1
 		horns
+	[value]
+		less
 	EOF
 '
 
@@ -116,6 +118,45 @@ test_expect_success 'find value with the highest priority' '
 	check_config get_value case.baz "hask"
 '
 
+test_expect_success 'return value for an existing key' '
+	test-tool config get lamb.chop >out 2>err &&
+	test_must_be_empty out &&
+	test_must_be_empty err
+'
+
+test_expect_success 'return value for value-less key' '
+	test-tool config get value.less >out 2>err &&
+	test_must_be_empty out &&
+	test_must_be_empty err
+'
+
+test_expect_success 'return value for a missing key' '
+	cat >expect <<-\EOF &&
+	Value not found for "missing.key"
+	EOF
+	test_expect_code 1 test-tool config get missing.key >actual 2>err &&
+	test_cmp actual expect &&
+	test_must_be_empty err
+'
+
+test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '
+	cat >expect <<-\EOF &&
+	Key "fails.iskeychar.-" is invalid
+	EOF
+	test_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&
+	test_cmp actual expect &&
+	test_must_be_empty out
+'
+
+test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '
+	cat >expect <<-\EOF &&
+	Key "keynosection" has no section
+	EOF
+	test_expect_code 1 test-tool config get keynosection >actual 2>err &&
+	test_cmp actual expect &&
+	test_must_be_empty out
+'
+
 test_expect_success 'find integer value for a key' '
 	check_config get_int lamb.chop 65
 '
@@ -272,7 +313,7 @@ test_expect_success 'proper error on error in default config files' '
 	cp .git/config .git/config.old &&
 	test_when_finished "mv .git/config.old .git/config" &&
 	echo "[" >>.git/config &&
-	echo "fatal: bad config line 34 in file .git/config" >expect &&
+	echo "fatal: bad config line 36 in file .git/config" >expect &&
 	test_expect_code 128 test-tool config get_value foo.bar 2>actual &&
 	test_cmp expect actual
 '
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v5 04/10] versioncmp.c: refactor config reading next commit
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
                           ` (2 preceding siblings ...)
  2023-02-07 16:10         ` [PATCH v5 03/10] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10         ` Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 05/10] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
                           ` (7 subsequent siblings)
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Refactor the reading of the versionSort.suffix and
versionSort.prereleaseSuffix configuration variables to stay within
the bounds of our CodingGuidelines when it comes to line length, and
to avoid repeating ourselves.

Renaming "deprecated_prereleases" to "oldl" doesn't help us to avoid
line wrapping now, but it will in a subsequent commit.

Let's also split out the names of the config variables into variables
of our own, and refactor the nested if/else to avoid indenting it, and
the existing bracing style issue.

This all helps with the subsequent commit, where we'll need to start
checking different git_config_get_value_multi() return value. See
c026557a373 (versioncmp: generalize version sort suffix reordering,
2016-12-08) for the original implementation of most of this.

Moving the "initialized = 1" assignment allows us to move some of this
to the variable declarations in the subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 versioncmp.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/versioncmp.c b/versioncmp.c
index 069ee94a4d7..323f5d35ea8 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)
 	}
 
 	if (!initialized) {
-		const struct string_list *deprecated_prereleases;
+		const char *const newk = "versionsort.suffix";
+		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *oldl;
+
+		prereleases = git_config_get_value_multi(newk);
+		oldl = git_config_get_value_multi(oldk);
+		if (prereleases && oldl)
+			warning("ignoring %s because %s is set", oldk, newk);
+		else if (!prereleases)
+			prereleases = oldl;
+
 		initialized = 1;
-		prereleases = git_config_get_value_multi("versionsort.suffix");
-		deprecated_prereleases = git_config_get_value_multi("versionsort.prereleasesuffix");
-		if (prereleases) {
-			if (deprecated_prereleases)
-				warning("ignoring versionsort.prereleasesuffix because versionsort.suffix is set");
-		} else
-			prereleases = deprecated_prereleases;
 	}
 	if (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,
 					    &diff))
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v5 05/10] config API: have *_multi() return an "int" and take a "dest"
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
                           ` (3 preceding siblings ...)
  2023-02-07 16:10         ` [PATCH v5 04/10] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10         ` Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 06/10] config API: don't lose the git_*get*() return values Ævar Arnfjörð Bjarmason
                           ` (6 subsequent siblings)
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Have the "git_configset_get_value_multi()" function and its siblings
return an "int" and populate a "**dest" parameter like every other
git_configset_get_*()" in the API.

As we'll take advantage of in subsequent commits, this fixes a blind
spot in the API where it wasn't possible to tell whether a list was
empty from whether a config key existed. For now we don't make use of
those new return values, but faithfully convert existing API users.

Most of this is straightforward, commentary on cases that stand out:

- To ensure that we'll properly use the return values of this function
  in the future we're using the "RESULT_MUST_BE_USED" macro introduced
  in [1].

  As git_die_config() now has to handle this return value let's have
  it BUG() if it can't find the config entry. As tested for in a
  preceding commit we can rely on getting the config list in
  git_die_config().

- The loops after getting the "list" value in "builtin/gc.c" could
  also make use of "unsorted_string_list_has_string()" instead of using
  that loop, but let's leave that for now.

- In "versioncmp.c" we now use the return value of the functions,
  instead of checking if the lists are still non-NULL.

1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
   return values, 2022-09-01),

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c |  5 +----
 builtin/gc.c            | 10 ++++------
 builtin/log.c           |  6 +++---
 config.c                | 34 ++++++++++++++++++++--------------
 config.h                | 29 +++++++++++++++++++++--------
 pack-bitmap.c           |  6 +++++-
 submodule.c             |  3 +--
 t/helper/test-config.c  |  6 ++----
 versioncmp.c            | 11 +++++++----
 9 files changed, 64 insertions(+), 46 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 6aeac371488..fd0e7739e6a 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -45,14 +45,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	values = repo_config_get_value_multi(the_repository,
-					     config_key);
-
 	/*
 	 * Do nothing on an empty list, which is equivalent to the case
 	 * where the config variable does not exist at all.
 	 */
-	if (!values)
+	if (repo_config_get_value_multi(the_repository, config_key, &values))
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/builtin/gc.c b/builtin/gc.c
index e38d1783f30..2b3da377d52 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1510,8 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
-	list = git_config_get_value_multi(key);
-	if (list) {
+	if (!git_config_get_value_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1577,11 +1576,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 	if (config_file) {
 		git_configset_init(&cs);
 		git_configset_add_file(&cs, config_file);
-		list = git_configset_get_value_multi(&cs, key);
-	} else {
-		list = git_config_get_value_multi(key);
 	}
-	if (list) {
+	if (!(config_file
+	      ? git_configset_get_value_multi(&cs, key, &list)
+	      : git_config_get_value_multi(key, &list))) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index 04412dd9c93..cec8cabd21e 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	int i;
 	char *value = NULL;
 	struct string_list *include = decoration_filter->include_ref_pattern;
-	const struct string_list *config_exclude =
-			git_config_get_value_multi("log.excludeDecoration");
+	const struct string_list *config_exclude;
 
-	if (config_exclude) {
+	if (!git_config_get_value_multi("log.excludeDecoration",
+					&config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/config.c b/config.c
index d4f0e4fd619..569819b4a1b 100644
--- a/config.c
+++ b/config.c
@@ -2418,29 +2418,34 @@ int git_configset_add_file(struct config_set *cs, const char *filename)
 int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
 {
 	const struct string_list *values = NULL;
+	int ret;
+
 	/*
 	 * Follows "last one wins" semantic, i.e., if there are multiple matches for the
 	 * queried key in the files of the configset, the value returned will be the last
 	 * value in the value list for that key.
 	 */
-	values = git_configset_get_value_multi(cs, key);
+	if ((ret = git_configset_get_value_multi(cs, key, &values)))
+		return ret;
 
-	if (!values)
-		return 1;
 	assert(values->nr > 0);
 	*value = values->items[values->nr - 1].string;
 	return 0;
 }
 
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest)
 {
 	struct config_set_element *e;
+	int ret;
 
-	if (configset_find_element(cs, key, &e))
-		return NULL;
+	if ((ret = configset_find_element(cs, key, &e)))
+		return ret;
 	else if (!e)
-		return NULL;
-	return &e->value_list;
+		return 1;
+	*dest = &e->value_list;
+
+	return 0;
 }
 
 int git_configset_get(struct config_set *cs, const char *key)
@@ -2604,11 +2609,11 @@ int repo_config_get_value(struct repository *repo,
 	return git_configset_get_value(repo->config, key, value);
 }
 
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key)
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest)
 {
 	git_config_check_init(repo);
-	return git_configset_get_value_multi(repo->config, key);
+	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
 int repo_config_get_string(struct repository *repo,
@@ -2721,9 +2726,9 @@ int git_config_get_value(const char *key, const char **value)
 	return repo_config_get_value(the_repository, key, value);
 }
 
-const struct string_list *git_config_get_value_multi(const char *key)
+int git_config_get_value_multi(const char *key, const struct string_list **dest)
 {
-	return repo_config_get_value_multi(the_repository, key);
+	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
 int git_config_get_string(const char *key, char **dest)
@@ -2870,7 +2875,8 @@ void git_die_config(const char *key, const char *err, ...)
 		error_fn(err, params);
 		va_end(params);
 	}
-	values = git_config_get_value_multi(key);
+	if (git_config_get_value_multi(key, &values))
+		BUG("for key '%s' we must have a value to report on", key);
 	kv_info = values->items[values->nr - 1].util;
 	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
 }
diff --git a/config.h b/config.h
index d016d05460d..115259ecb8d 100644
--- a/config.h
+++ b/config.h
@@ -459,10 +459,18 @@ int git_configset_add_parameters(struct config_set *cs);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key` and config set `cs`. When the
- * configuration variable `key` is not found, returns NULL. The caller
- * should not free or modify the returned pointer, as it is owned by the cache.
+ * configuration variable `key` is not found, returns 1 without touching
+ * `value`.
+ *
+ * The key will be parsed for validity with git_config_parse_key(), on
+ * error a negative value will be returned.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
+RESULT_MUST_BE_USED
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest);
 
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
@@ -507,8 +515,9 @@ RESULT_MUST_BE_USED
 int repo_config_get(struct repository *repo, const char *key);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key);
+RESULT_MUST_BE_USED
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -562,10 +571,14 @@ int git_config_get_value(const char *key, const char **value);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key`. When the configuration variable
- * `key` is not found, returns NULL. The caller should not free or modify
- * the returned pointer, as it is owned by the cache.
+ * `key` is not found, returns 1 without touching `value`.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_config_get_value_multi(const char *key);
+RESULT_MUST_BE_USED
+int git_config_get_value_multi(const char *key,
+			       const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index d2a42abf28c..15c5eb507c0 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2314,7 +2314,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)
 
 const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
-	return repo_config_get_value_multi(r, "pack.preferbitmaptips");
+	const struct string_list *dest;
+
+	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+		return dest;
+	return NULL;
 }
 
 int bitmap_is_preferred_refname(struct repository *r, const char *refname)
diff --git a/submodule.c b/submodule.c
index 3a0dfc417c0..4b6f5223b0c 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	sl = repo_config_get_value_multi(repo, "submodule.active");
-	if (sl) {
+	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/helper/test-config.c b/t/helper/test-config.c
index cbb33ae1fff..6dc4c37444f 100644
--- a/t/helper/test-config.c
+++ b/t/helper/test-config.c
@@ -97,8 +97,7 @@ int cmd__config(int argc, const char **argv)
 			goto exit1;
 		}
 	} else if (argc == 3 && !strcmp(argv[1], "get_value_multi")) {
-		strptr = git_config_get_value_multi(argv[2]);
-		if (strptr) {
+		if (!git_config_get_value_multi(argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
@@ -181,8 +180,7 @@ int cmd__config(int argc, const char **argv)
 				goto exit2;
 			}
 		}
-		strptr = git_configset_get_value_multi(&cs, argv[2]);
-		if (strptr) {
+		if (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
diff --git a/versioncmp.c b/versioncmp.c
index 323f5d35ea8..60c3a517122 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)
 	if (!initialized) {
 		const char *const newk = "versionsort.suffix";
 		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *newl;
 		const struct string_list *oldl;
+		int new = git_config_get_value_multi(newk, &newl);
+		int old = git_config_get_value_multi(oldk, &oldl);
 
-		prereleases = git_config_get_value_multi(newk);
-		oldl = git_config_get_value_multi(oldk);
-		if (prereleases && oldl)
+		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-		else if (!prereleases)
+		if (!new)
+			prereleases = newl;
+		else if (!old)
 			prereleases = oldl;
 
 		initialized = 1;
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v5 06/10] config API: don't lose the git_*get*() return values
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
                           ` (4 preceding siblings ...)
  2023-02-07 16:10         ` [PATCH v5 05/10] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10         ` Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 07/10] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
                           ` (5 subsequent siblings)
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Since a preceding commit which added the "git_config_get()" family of
functions, and the preceding commit where *_multi() started returning
an "int" we've finally been able to ferry up non-zero return values,
rather than having negative return values normalized to a "return 1"
along the way.

In practice this doesn't matter to existing callers. They're either
ignoring these return values and relying on us to only populate "dest"
if we'd return 0, or normalizing non-zero return values with "!".

Even if they weren't normalizing them we'll only return non-zero
negative values in those cases where the config key itself is bad,
which excludes the vast majority of our callers, as they hardcode a
valued configuration key as a fixed string in the C sources.

So this change is expected to do nothing for now, but is really here
for our own sanity. It's much harder to reason about an API that's
losing return values in some cases, and coercing them in others. If
there isn't a compelling reason to do otherwise we should let the
caller decide if they care about the distinction between bad keys and
non-existence.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 config.c | 117 ++++++++++++++++++++++++++++++-------------------------
 config.h |  16 ++++----
 2 files changed, 72 insertions(+), 61 deletions(-)

diff --git a/config.c b/config.c
index 569819b4a1b..8d7e40ac8a4 100644
--- a/config.c
+++ b/config.c
@@ -2463,86 +2463,93 @@ int git_configset_get(struct config_set *cs, const char *key)
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
 {
 	const char *value;
-	if (!git_configset_get_value(cs, key, &value))
-		return git_config_string((const char **)dest, key, value);
-	else
-		return 1;
+	int ret;
+
+	if ((ret = git_configset_get_value(cs, key, &value)))
+		return ret;
+	return git_config_string((const char **)dest, key, value);
 }
 
 static int git_configset_get_string_tmp(struct config_set *cs, const char *key,
 					const char **dest)
 {
 	const char *value;
-	if (!git_configset_get_value(cs, key, &value)) {
-		if (!value)
-			return config_error_nonbool(key);
-		*dest = value;
-		return 0;
-	} else {
-		return 1;
-	}
+	int ret;
+
+	if ((ret = git_configset_get_value(cs, key, &value)))
+		return ret;
+	if (!value)
+		return config_error_nonbool(key);
+	*dest = value;
+	return 0;
 }
 
 int git_configset_get_int(struct config_set *cs, const char *key, int *dest)
 {
 	const char *value;
-	if (!git_configset_get_value(cs, key, &value)) {
-		*dest = git_config_int(key, value);
-		return 0;
-	} else
-		return 1;
+	int ret;
+
+	if ((ret = git_configset_get_value(cs, key, &value)))
+		return ret;
+	*dest = git_config_int(key, value);
+	return 0;
 }
 
 int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest)
 {
 	const char *value;
-	if (!git_configset_get_value(cs, key, &value)) {
-		*dest = git_config_ulong(key, value);
-		return 0;
-	} else
-		return 1;
+	int ret;
+
+	if ((ret = git_configset_get_value(cs, key, &value)))
+		return ret;
+	*dest = git_config_ulong(key, value);
+	return 0;
 }
 
 int git_configset_get_bool(struct config_set *cs, const char *key, int *dest)
 {
 	const char *value;
-	if (!git_configset_get_value(cs, key, &value)) {
-		*dest = git_config_bool(key, value);
-		return 0;
-	} else
-		return 1;
+	int ret;
+
+	if ((ret = git_configset_get_value(cs, key, &value)))
+		return ret;
+	*dest = git_config_bool(key, value);
+	return 0;
 }
 
 int git_configset_get_bool_or_int(struct config_set *cs, const char *key,
 				int *is_bool, int *dest)
 {
 	const char *value;
-	if (!git_configset_get_value(cs, key, &value)) {
-		*dest = git_config_bool_or_int(key, value, is_bool);
-		return 0;
-	} else
-		return 1;
+	int ret;
+
+	if ((ret = git_configset_get_value(cs, key, &value)))
+		return ret;
+	*dest = git_config_bool_or_int(key, value, is_bool);
+	return 0;
 }
 
 int git_configset_get_maybe_bool(struct config_set *cs, const char *key, int *dest)
 {
 	const char *value;
-	if (!git_configset_get_value(cs, key, &value)) {
-		*dest = git_parse_maybe_bool(value);
-		if (*dest == -1)
-			return -1;
-		return 0;
-	} else
-		return 1;
+	int ret;
+
+	if ((ret = git_configset_get_value(cs, key, &value)))
+		return ret;
+	*dest = git_parse_maybe_bool(value);
+	if (*dest == -1)
+		return -1;
+	return 0;
 }
 
 int git_configset_get_pathname(struct config_set *cs, const char *key, const char **dest)
 {
 	const char *value;
-	if (!git_configset_get_value(cs, key, &value))
-		return git_config_pathname(dest, key, value);
-	else
-		return 1;
+	int ret;
+
+	if ((ret = git_configset_get_value(cs, key, &value)))
+		return ret;
+	return git_config_pathname(dest, key, value);
 }
 
 /* Functions use to read configuration from a repository */
@@ -2789,9 +2796,11 @@ int git_config_get_expiry_in_days(const char *key, timestamp_t *expiry, timestam
 	const char *expiry_string;
 	intmax_t days;
 	timestamp_t when;
+	int ret;
 
-	if (git_config_get_string_tmp(key, &expiry_string))
-		return 1; /* no such thing */
+	if ((ret = git_config_get_string_tmp(key, &expiry_string)))
+		/* no such thing, or git_config_parse_key() failure etc. */
+		return ret;
 
 	if (git_parse_signed(expiry_string, &days, maximum_signed_value_of_type(int))) {
 		const int scale = 86400;
@@ -2834,6 +2843,7 @@ int git_config_get_max_percent_split_change(void)
 int git_config_get_index_threads(int *dest)
 {
 	int is_bool, val;
+	int ret;
 
 	val = git_env_ulong("GIT_TEST_INDEX_THREADS", 0);
 	if (val) {
@@ -2841,15 +2851,14 @@ int git_config_get_index_threads(int *dest)
 		return 0;
 	}
 
-	if (!git_config_get_bool_or_int("index.threads", &is_bool, &val)) {
-		if (is_bool)
-			*dest = val ? 0 : 1;
-		else
-			*dest = val;
-		return 0;
-	}
-
-	return 1;
+	if ((ret = git_config_get_bool_or_int("index.threads", &is_bool,
+					      &val)))
+		return ret;
+	if (is_bool)
+		*dest = val ? 0 : 1;
+	else
+		*dest = val;
+	return 0;
 }
 
 NORETURN
diff --git a/config.h b/config.h
index 115259ecb8d..da5c498d39a 100644
--- a/config.h
+++ b/config.h
@@ -477,20 +477,22 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,
  */
 void git_configset_clear(struct config_set *cs);
 
-/*
+/**
  * These functions return 1 if not found, and 0 if found, leaving the found
- * value in the 'dest' pointer.
+ * value in the 'dest' pointer. On error a negative value is returned.
+ *
+ * The functions that return a single value (i.e. not
+ * *_get_*multi*()) will return the highest-priority value for the
+ * configuration variable `key`, i.e. in the case where we have
+ * multiple values the last value found.
  */
 
 RESULT_MUST_BE_USED
 int git_configset_get(struct config_set *cs, const char *key);
 
 /*
- * Finds the highest-priority value for the configuration variable `key`
- * and config set `cs`, stores the pointer to it in `value` and returns 0.
- * When the configuration variable `key` is not found, returns 1 without
- * touching `value`. The caller should not free or modify `value`, as it
- * is owned by the cache.
+ * The caller should not free or modify `value`, as it is owned by the
+ * cache.
  */
 int git_configset_get_value(struct config_set *cs, const char *key, const char **dest);
 
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v5 07/10] for-each-repo: error on bad --config
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
                           ` (5 preceding siblings ...)
  2023-02-07 16:10         ` [PATCH v5 06/10] config API: don't lose the git_*get*() return values Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10         ` Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 08/10] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
                           ` (4 subsequent siblings)
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

As noted in 6c62f015520 (for-each-repo: do nothing on empty config,
2021-01-08) this command wants to ignore a non-existing config key,
but let's not conflate that with bad config.

Before this, all these added tests would pass with an exit code of 0.

We could preserve the comment added in 6c62f015520, but now that we're
directly using the documented repo_config_get_value_multi() value it's
just narrating something that should be obvious from the API use, so
let's drop it.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  | 11 ++++++-----
 t/t0068-for-each-repo.sh |  6 ++++++
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index fd0e7739e6a..224164addb3 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	static const char *config_key = NULL;
 	int i, result = 0;
 	const struct string_list *values;
+	int err;
 
 	const struct option options[] = {
 		OPT_STRING(0, "config", &config_key, N_("config"),
@@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	/*
-	 * Do nothing on an empty list, which is equivalent to the case
-	 * where the config variable does not exist at all.
-	 */
-	if (repo_config_get_value_multi(the_repository, config_key, &values))
+	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	if (err < 0)
+		usage_msg_optf(_("got bad config --config=%s"),
+			       for_each_repo_usage, options, config_key);
+	else if (err)
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 3648d439a87..6b51e00da0e 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '
 	git for-each-repo --config=bogus.config -- help --no-such-option
 '
 
+test_expect_success 'error on bad config keys' '
+	test_expect_code 129 git for-each-repo --config=a &&
+	test_expect_code 129 git for-each-repo --config=a.b. &&
+	test_expect_code 129 git for-each-repo --config="'\''.b"
+'
+
 test_done
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v5 08/10] config API users: test for *_get_value_multi() segfaults
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
                           ` (6 preceding siblings ...)
  2023-02-07 16:10         ` [PATCH v5 07/10] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10         ` Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 09/10] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
                           ` (3 subsequent siblings)
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

As we'll discuss in the subsequent commit these tests all
show *_get_value_multi() API users unable to handle there being a
value-less key in the config, which is represented with a "NULL" for
that entry in the "string" member of the returned "struct
string_list", causing a segfault.

These added tests exhaustively test for that issue, as we'll see in a
subsequent commit we'll need to change all of the API users
of *_get_value_multi(). These cases were discovered by triggering each
one individually, and then adding these tests.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t4202-log.sh                 | 11 +++++++++++
 t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++
 t/t7004-tag.sh                 | 12 ++++++++++++
 t/t7413-submodule-is-active.sh | 12 ++++++++++++
 t/t7900-maintenance.sh         | 23 +++++++++++++++++++++++
 5 files changed, 74 insertions(+)

diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 2ce2b41174d..e4f02d8208b 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
+test_expect_failure 'parse log.excludeDecoration with no value' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[log]
+		excludeDecoration
+	EOF
+	git log --decorate=short
+'
+
 test_expect_success 'decorate-refs with glob' '
 	cat >expect.decorate <<-\EOF &&
 	Merge-tag-reach
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 7d8dee41b0d..0306b399188 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,6 +404,22 @@ test_bitmap_cases () {
 		)
 	'
 
+	test_expect_failure 'pack.preferBitmapTips' '
+		git init repo &&
+		test_when_finished "rm -rf repo" &&
+		(
+			cd repo &&
+			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
+			test_commit_bulk --message="%s" 103 &&
+
+			cat >>.git/config <<-\EOF &&
+			[pack]
+				preferBitmapTips
+			EOF
+			git repack -adb
+		)
+	'
+
 	test_expect_success 'complains about multiple pack bitmaps' '
 		rm -fr repo &&
 		git init repo &&
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index 9aa1660651b..f343551a7d4 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[versionsort]
+		prereleaseSuffix
+		suffix
+	EOF
+	git tag -l --sort=version:refname
+'
+
 test_expect_success 'version sort with prerelease reordering' '
 	test_config versionsort.prereleaseSuffix -rc &&
 	git tag foo1.6-rc1 &&
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index 7cdc2637649..bfe27e50732 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
+test_expect_failure 'is-active handles submodule.active config missing a value' '
+	cp super/.git/config super/.git/config.orig &&
+	test_when_finished mv super/.git/config.orig super/.git/config &&
+
+	cat >>super/.git/config <<-\EOF &&
+	[submodule]
+		active
+	EOF
+
+	test-tool -C super submodule is-active sub1
+'
+
 test_expect_success 'is-active works with basic submodule.active config' '
 	test_when_finished "git -C super config submodule.sub1.URL ../sub" &&
 	test_when_finished "git -C super config --unset-all submodule.active" &&
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 823331e44a0..d82eac6a471 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -524,6 +524,29 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --config-file ./other --force
 '
 
+test_expect_failure 'register with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	git maintenance register
+'
+
+test_expect_failure 'unregister with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	git maintenance unregister &&
+	git maintenance unregister --force
+'
+
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
 	META="a+b*c" &&
 	git init "$META" &&
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v5 09/10] config API: add "string" version of *_value_multi(), fix segfaults
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
                           ` (7 preceding siblings ...)
  2023-02-07 16:10         ` [PATCH v5 08/10] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10         ` Ævar Arnfjörð Bjarmason
  2023-02-07 16:10         ` [PATCH v5 10/10] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
                           ` (2 subsequent siblings)
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Fix numerous and mostly long-standing segfaults in consumers of
the *_config_*value_multi() API. As discussed in the preceding commit
an empty key in the config syntax yields a "NULL" string, which these
users would give to strcmp() (or similar), resulting in segfaults.

As this change shows, most users users of the *_config_*value_multi()
API didn't really want such an an unsafe and low-level API, let's give
them something with the safety of git_config_get_string() instead.

This fix is similar to what the *_string() functions and others
acquired in[1] and [2]. Namely introducing and using a safer
"*_get_string_multi()" variant of the low-level "_*value_multi()"
function.

This fixes segfaults in code introduced in:

  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)
  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)
  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)
  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)
  - 92156291ca8 (log: add default decoration filter, 2022-08-05)
  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)

There are now two users ofthe low-level API:

- One in "builtin/for-each-repo.c", which we'll convert in a
  subsequent commit.

- The "t/helper/test-config.c" code added in [3].

As seen in the preceding commit we need to give the
"t/helper/test-config.c" caller these "NULL" entries.

We could also alter the underlying git_configset_get_value_multi()
function to be "string safe", but doing so would leave no room for
other variants of "*_get_value_multi()" that coerce to other types.

Such coercion can't be built on the string version, since as we've
established "NULL" is a true value in the boolean context, but if we
coerced it to "" for use in a list of strings it'll be subsequently
coerced to "false" as a boolean.

The callback pattern being used here will make it easy to introduce
e.g. a "multi" variant which coerces its values to "bool", "int",
"path" etc.

1. 40ea4ed9032 (Add config_error_nonbool() helper function,
   2008-02-11)
2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,
   2008-02-11).
3. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                   |  6 +++---
 builtin/log.c                  |  4 ++--
 config.c                       | 32 ++++++++++++++++++++++++++++++++
 config.h                       | 19 +++++++++++++++++++
 pack-bitmap.c                  |  2 +-
 submodule.c                    |  2 +-
 t/t4202-log.sh                 |  8 ++++++--
 t/t5310-pack-bitmaps.sh        |  8 ++++++--
 t/t7004-tag.sh                 |  9 +++++++--
 t/t7413-submodule-is-active.sh |  8 ++++++--
 t/t7900-maintenance.sh         | 25 ++++++++++++++++++++-----
 versioncmp.c                   |  4 ++--
 12 files changed, 105 insertions(+), 22 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 2b3da377d52..9497bdf23e4 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1510,7 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
-	if (!git_config_get_value_multi(key, &list)) {
+	if (!git_config_get_string_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1578,8 +1578,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 		git_configset_add_file(&cs, config_file);
 	}
 	if (!(config_file
-	      ? git_configset_get_value_multi(&cs, key, &list)
-	      : git_config_get_value_multi(key, &list))) {
+	      ? git_configset_get_string_multi(&cs, key, &list)
+	      : git_config_get_string_multi(key, &list))) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index cec8cabd21e..481685d5263 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	struct string_list *include = decoration_filter->include_ref_pattern;
 	const struct string_list *config_exclude;
 
-	if (!git_config_get_value_multi("log.excludeDecoration",
-					&config_exclude)) {
+	if (!git_config_get_string_multi("log.excludeDecoration",
+					 &config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/config.c b/config.c
index 8d7e40ac8a4..de92d592e50 100644
--- a/config.c
+++ b/config.c
@@ -2448,6 +2448,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,
 	return 0;
 }
 
+static int check_multi_string(struct string_list_item *item, void *util)
+{
+	return item->string ? 0 : config_error_nonbool(util);
+}
+
+int git_configset_get_string_multi(struct config_set *cs, const char *key,
+				   const struct string_list **dest)
+{
+	int ret;
+
+	if ((ret = git_configset_get_value_multi(cs, key, dest)))
+		return ret;
+	if ((ret = for_each_string_list((struct string_list *)*dest,
+					check_multi_string, (void *)key)))
+		return ret;
+
+	return 0;
+}
+
 int git_configset_get(struct config_set *cs, const char *key)
 {
 	struct config_set_element *e;
@@ -2623,6 +2642,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,
 	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
+int repo_config_get_string_multi(struct repository *repo, const char *key,
+				 const struct string_list **dest)
+{
+	git_config_check_init(repo);
+	return git_configset_get_string_multi(repo->config, key, dest);
+}
+
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest)
 {
@@ -2738,6 +2764,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)
 	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
+int git_config_get_string_multi(const char *key,
+				const struct string_list **dest)
+{
+	return repo_config_get_string_multi(the_repository, key, dest);
+}
+
 int git_config_get_string(const char *key, char **dest)
 {
 	return repo_config_get_string(the_repository, key, dest);
diff --git a/config.h b/config.h
index da5c498d39a..8577c80213c 100644
--- a/config.h
+++ b/config.h
@@ -472,6 +472,19 @@ RESULT_MUST_BE_USED
 int git_configset_get_value_multi(struct config_set *cs, const char *key,
 				  const struct string_list **dest);
 
+/**
+ * A validation wrapper for git_configset_get_value_multi() which does
+ * for it what git_configset_get_string() does for
+ * git_configset_get_value().
+ *
+ * The configuration syntax allows for "[section] key", which will
+ * give us a NULL entry in the "struct string_list", as opposed to
+ * "[section] key =" which is the empty string. Most users of the API
+ * are not prepared to handle NULL in a "struct string_list".
+ */
+int git_configset_get_string_multi(struct config_set *cs, const char *key,
+				   const struct string_list **dest);
+
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
  */
@@ -520,6 +533,9 @@ int repo_config_get_value(struct repository *repo,
 RESULT_MUST_BE_USED
 int repo_config_get_value_multi(struct repository *repo, const char *key,
 				const struct string_list **dest);
+RESULT_MUST_BE_USED
+int repo_config_get_string_multi(struct repository *repo, const char *key,
+				 const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -581,6 +597,9 @@ int git_config_get_value(const char *key, const char **value);
 RESULT_MUST_BE_USED
 int git_config_get_value_multi(const char *key,
 			       const struct string_list **dest);
+RESULT_MUST_BE_USED
+int git_config_get_string_multi(const char *key,
+				const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index 15c5eb507c0..d003c7e60b4 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2316,7 +2316,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
 	const struct string_list *dest;
 
-	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+	if (!repo_config_get_string_multi(r, "pack.preferbitmaptips", &dest))
 		return dest;
 	return NULL;
 }
diff --git a/submodule.c b/submodule.c
index 4b6f5223b0c..30a103246ec 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
+	if (!repo_config_get_string_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index e4f02d8208b..ae73aef922f 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
-test_expect_failure 'parse log.excludeDecoration with no value' '
+test_expect_success 'parse log.excludeDecoration with no value' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '
 	[log]
 		excludeDecoration
 	EOF
-	git log --decorate=short
+	cat >expect <<-\EOF &&
+	error: missing value for '\''log.excludeDecoration'\''
+	EOF
+	git log --decorate=short 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'decorate-refs with glob' '
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 0306b399188..526a5a506eb 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,7 +404,7 @@ test_bitmap_cases () {
 		)
 	'
 
-	test_expect_failure 'pack.preferBitmapTips' '
+	test_expect_success 'pack.preferBitmapTips' '
 		git init repo &&
 		test_when_finished "rm -rf repo" &&
 		(
@@ -416,7 +416,11 @@ test_bitmap_cases () {
 			[pack]
 				preferBitmapTips
 			EOF
-			git repack -adb
+			cat >expect <<-\EOF &&
+			error: missing value for '\''pack.preferbitmaptips'\''
+			EOF
+			git repack -adb 2>actual &&
+			test_cmp expect actual
 		)
 	'
 
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index f343551a7d4..f4a31ada79a 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas
 		prereleaseSuffix
 		suffix
 	EOF
-	git tag -l --sort=version:refname
+	cat >expect <<-\EOF &&
+	error: missing value for '\''versionsort.suffix'\''
+	error: missing value for '\''versionsort.prereleasesuffix'\''
+	EOF
+	git tag -l --sort=version:refname 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'version sort with prerelease reordering' '
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index bfe27e50732..887d181b72e 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
-test_expect_failure 'is-active handles submodule.active config missing a value' '
+test_expect_success 'is-active handles submodule.active config missing a value' '
 	cp super/.git/config super/.git/config.orig &&
 	test_when_finished mv super/.git/config.orig super/.git/config &&
 
@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'
 		active
 	EOF
 
-	test-tool -C super submodule is-active sub1
+	cat >expect <<-\EOF &&
+	error: missing value for '\''submodule.active'\''
+	EOF
+	test-tool -C super submodule is-active sub1 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'is-active works with basic submodule.active config' '
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index d82eac6a471..487e326b3fa 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --config-file ./other --force
 '
 
-test_expect_failure 'register with no value for maintenance.repo' '
+test_expect_success 'register with no value for maintenance.repo' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '
 	[maintenance]
 		repo
 	EOF
-	git maintenance register
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	git maintenance register 2>actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
 '
 
-test_expect_failure 'unregister with no value for maintenance.repo' '
+test_expect_success 'unregister with no value for maintenance.repo' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -543,8 +548,18 @@ test_expect_failure 'unregister with no value for maintenance.repo' '
 	[maintenance]
 		repo
 	EOF
-	git maintenance unregister &&
-	git maintenance unregister --force
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	test_expect_code 128 git maintenance unregister 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo &&
+
+	git maintenance unregister --force 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
 '
 
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
diff --git a/versioncmp.c b/versioncmp.c
index 60c3a517122..7498da96e0e 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)
 		const char *const oldk = "versionsort.prereleasesuffix";
 		const struct string_list *newl;
 		const struct string_list *oldl;
-		int new = git_config_get_value_multi(newk, &newl);
-		int old = git_config_get_value_multi(oldk, &oldl);
+		int new = git_config_get_string_multi(newk, &newl);
+		int old = git_config_get_string_multi(oldk, &oldl);
 
 		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v5 10/10] for-each-repo: with bad config, don't conflate <path> and <cmd>
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
                           ` (8 preceding siblings ...)
  2023-02-07 16:10         ` [PATCH v5 09/10] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
@ 2023-02-07 16:10         ` Ævar Arnfjörð Bjarmason
  2023-02-07 17:38         ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Junio C Hamano
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
  11 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-07 16:10 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on
configured repos, 2020-09-11). Due to assuming that elements returned
from the repo_config_get_value_multi() call wouldn't be "NULL" we'd
conflate the <path> and <command> part of the argument list when
running commands.

As noted in the preceding commit the fix is to move to a safer
"*_string_multi()" version of the *_multi() API. This change is
separated from the rest because those all segfaulted. In this change
we ended up with different behavior.

When using the "--config=<config>" form we take each element of the
list as a path to a repository. E.g. with a configuration like:

	[repo] list = /some/repo

We would, with this command:

	git for-each-repo --config=repo.list status builtin

Run a "git status" in /some/repo, as:

	git -C /some/repo status builtin

I.e. ask "status" to report on the "builtin" directory. But since a
configuration such as this would result in a "struct string_list *"
with one element, whose "string" member is "NULL":

	[repo] list

We would, when constructing our command-line in
"builtin/for-each-repo.c"...

	strvec_pushl(&child.args, "-C", path, NULL);
	for (i = 0; i < argc; i++)
		strvec_push(&child.args, argv[i]);

...have that "path" be "NULL", and as strvec_pushl() stops when it
sees NULL we'd end with the first "argv" element as the argument to
the "-C" option, e.g.:

	git -C status builtin

I.e. we'd run the command "builtin" in the "status" directory.

In another context this might be an interesting security
vulnerability, but I think that this amounts to a nothingburger on
that front.

A hypothetical attacker would need to be able to write config for the
victim to run, if they're able to do that there's more interesting
attack vectors. See the "safe.directory" facility added in
8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).

An even more unlikely possibility would be an attacker able to
generate the config used for "for-each-repo --config=<key>", but
nothing else (e.g. an automated system producing that list).

Even in that case the attack vector is limited to the user running
commands whose name matches a directory that's interesting to the
attacker (e.g. a "log" directory in a repository). The second
argument (if any) of the command is likely to make git die without
doing anything interesting (e.g. "-p" to "log", there being no "-p"
built-in command to run).

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  |  2 +-
 t/t0068-for-each-repo.sh | 13 +++++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 224164addb3..ce8f7a99086 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	err = repo_config_get_string_multi(the_repository, config_key, &values);
 	if (err < 0)
 		usage_msg_optf(_("got bad config --config=%s"),
 			       for_each_repo_usage, options, config_key);
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 6b51e00da0e..4b90b74d5d5 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -46,4 +46,17 @@ test_expect_success 'error on bad config keys' '
 	test_expect_code 129 git for-each-repo --config="'\''.b"
 '
 
+test_expect_success 'error on NULL value for config keys' '
+	cat >>.git/config <<-\EOF &&
+	[empty]
+		key
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''empty.key'\''
+	EOF
+	test_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.39.1.1430.gb2471c0aaf4


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret"
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
                           ` (9 preceding siblings ...)
  2023-02-07 16:10         ` [PATCH v5 10/10] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
@ 2023-02-07 17:38         ` Junio C Hamano
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
  11 siblings, 0 replies; 134+ messages in thread
From: Junio C Hamano @ 2023-02-07 17:38 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Derrick Stolee, Elijah Newren, Jeff King, Taylor Blau,
	SZEDER Gábor, Glen Choo, Calvin Wan, Emily Shaffer, raymond,
	zweiss

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> * Added tests for value-less at the end of a list to 2/10, per Junio's
>   request.

I do not "request" anything during my reviews, and I prefer not to
see that verb.  If you find what a reviewer suggests is valuable,
you take it, otherwise you explain why it is better to go without
what was suggested.

>   As the much of the point of this series is to make that API less of
>   a special snowflake a new 6/10 instead finishes up the work of
>   having most of the rest of the API return the un-coerced "ret" from
>   the depths of the config API.
>
>   That patch is quite large by line count, but pretty trivial in
>   complexity. All of those functions are copy/pasted versions of one
>   another with very minor variations.
>
> * Updated the 8/10 commit message, which was stale from a previous
>   version of this topic.

This is now 9/10, thanks to the new 6/10 step being added, and it
reads well.

Thanks, will queue.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v5 02/10] config tests: add "NULL" tests for *_get_value_multi()
  2023-02-07 16:10         ` [PATCH v5 02/10] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2023-02-09  4:00           ` Glen Choo
  0 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-02-09  4:00 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> +		# Value-less in the middle of a list
> +		cat >"$config" <<-\EOF &&
> +		[a]key=x
> +		[a]key
> +		[a]key=y
> +		EOF
> +		case "$op" in
> +		*_multi)
> +			cat >expect <<-\EOF
> +			x
> +			(NULL)
> +			y
> +			EOF
> +			;;
> +		*)
> +			cat >expect <<-\EOF
> +			y
> +			EOF
> +			;;
> +		esac &&
> +		test-tool config "$op" a.key $file >actual &&
> +		test_cmp expect actual &&

This extra test case makes me feel a bit better about making this DRY,
though the extra "case" statement detracts from the readability a bit.

Maybe if we split the _multi and non-multi cases?

	test_expect_success "multi" '
    # tmp config things

		# Value-less in the middle of a list
		cat >"$config" <<-\EOF &&
		[a]key=x
		[a]key
		[a]key=y
		EOF
    cat >expect <<-\EOF
    x
    (NULL)
    y
    EOF
		test-tool config "$op" a.key $file >actual &&
		test_cmp expect actual &&

		# Value-less at the end of a least (probable typo)
		cat >"$config" <<-\EOF &&
		[a]key=x
		[a]key=y
		[a]key
		EOF
    cat >expect <<-\EOF
    x
    y
    (NULL)
    EOF
		test-tool config "$op" a.key $file >actual &&
		test_cmp expect actual
	'

	test_expect_success "single" '
    # tmp config things

		# Value-less in the middle of a list
		cat >"$config" <<-\EOF &&
		[a]key=x
		[a]key
		[a]key=y
		EOF
    cat >expect <<-\EOF
    y
    EOF
		test-tool config "$op" a.key $file >actual &&
		test_cmp expect actual &&

		# Value-less at the end of a least (probable typo)
		cat >"$config" <<-\EOF &&
		[a]key=x
		[a]key=y
		[a]key
		EOF
    cat >expect <<-\EOF
    (NULL)
    EOF
		test-tool config "$op" a.key $file >actual &&
		test_cmp expect actual
	'

Idk. It does read a bit clearer to me, but I don't feel strongly about
it.

> +		# Value-less at the end of a least

s/least/list

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v5 03/10] config API: add and use a "git_config_get()" family of functions
  2023-02-07 16:10         ` [PATCH v5 03/10] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
@ 2023-02-09  8:24           ` Glen Choo
  2023-02-09 10:11             ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 134+ messages in thread
From: Glen Choo @ 2023-02-09  8:24 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> We could have changed git_configset_get_value_multi() (and then
> git_config_get_value() etc.) to accept a "NULL" as a "dest" for all
> callers, but let's avoid changing the behavior of existing API
> users. Having an "unused" value that we throw away internal to
> config.c is cheap.
>
> A "NULL as optional dest" pattern is also more fragile, as the intent
> of the caller might be misinterpreted if he were to accidentally pass
> "NULL", e.g. when "dest" is passed in from another function.

Okay, I think I can buy this argument. In other words,
git_config_get_value() is only used to put the value in "*dest", so
"dest = NULL" is an error. This is by design, because it defends against
callers who are using it wrongly. If it accepted "NULL" to mean 'dest
will be ignored', we're creating possible hard-to-spot bugs because we
no longer error out early.

> This still leaves various inconsistencies and clobbering or ignoring
> of the return value in place. E.g here we're modifying
> configset_add_value(), but ever since it was added in [2] we've been
> ignoring its "int" return value, but as we're changing the
> configset_find_element() it uses, let's have it faithfully ferry that
> "ret" along.
>
> Let's also use the "RESULT_MUST_BE_USED" macro introduced in [3] to
> assert that we're checking the return value of
> configset_find_element().
>
> We're leaving the same change to configset_add_value() for some future
> series. Once we start paying attention to its return value we'd need
> to ferry it up as deep as do_config_from(), and would need to make
> least read_{,very_}early_config() and git_protected_config() return an
> "int" instead of "void". Let's leave that for now, and focus on
> the *_get_*() functions.
>
> In a subsequent commit we'll fix the other *_get_*() functions to so
> that they'll ferry our underlying "ret" along, rather than normalizing
> it to a "return 1". But as an intermediate step to that we'll need to
> fix git_configset_get_value_multi() to return "int", and that change
> itself is smaller because of this change to migrate some callers away
> from the *_value_multi() API.

I haven't read ahead, but on first impression this sounds like it might
be too intrusive for a series whose goal is to clean up
*_get_value_multi().

> diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
> index 4be1ab1147c..7def7053e1c 100755
> --- a/t/t1308-config-set.sh
> +++ b/t/t1308-config-set.sh
> @@ -58,6 +58,8 @@ test_expect_success 'setup default config' '
>  		skin = false
>  		nose = 1
>  		horns
> +	[value]
> +		less
>  	EOF
>  '
>  
> @@ -116,6 +118,45 @@ test_expect_success 'find value with the highest priority' '
>  	check_config get_value case.baz "hask"
>  '
>  
> +test_expect_success 'return value for an existing key' '
> +	test-tool config get lamb.chop >out 2>err &&
> +	test_must_be_empty out &&
> +	test_must_be_empty err
> +'
> +
> +test_expect_success 'return value for value-less key' '
> +	test-tool config get value.less >out 2>err &&
> +	test_must_be_empty out &&
> +	test_must_be_empty err
> +'
> +
> +test_expect_success 'return value for a missing key' '
> +	cat >expect <<-\EOF &&
> +	Value not found for "missing.key"
> +	EOF
> +	test_expect_code 1 test-tool config get missing.key >actual 2>err &&
> +	test_cmp actual expect &&
> +	test_must_be_empty err
> +'
> +
> +test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '
> +	cat >expect <<-\EOF &&
> +	Key "fails.iskeychar.-" is invalid
> +	EOF
> +	test_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&
> +	test_cmp actual expect &&
> +	test_must_be_empty out
> +'
> +
> +test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '
> +	cat >expect <<-\EOF &&
> +	Key "keynosection" has no section
> +	EOF
> +	test_expect_code 1 test-tool config get keynosection >actual 2>err &&
> +	test_cmp actual expect &&
> +	test_must_be_empty out
> +'
> +

No real comments on the changes themselves. The added test coverage in
this version is quite nice.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v5 03/10] config API: add and use a "git_config_get()" family of functions
  2023-02-09  8:24           ` Glen Choo
@ 2023-02-09 10:11             ` Ævar Arnfjörð Bjarmason
  2023-02-09 10:59               ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-09 10:11 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss


On Thu, Feb 09 2023, Glen Choo wrote:

> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>> [...]
>> This still leaves various inconsistencies and clobbering or ignoring
>> of the return value in place. E.g here we're modifying
>> configset_add_value(), but ever since it was added in [2] we've been
>> ignoring its "int" return value, but as we're changing the
>> configset_find_element() it uses, let's have it faithfully ferry that
>> "ret" along.
>>
>> Let's also use the "RESULT_MUST_BE_USED" macro introduced in [3] to
>> assert that we're checking the return value of
>> configset_find_element().
>>
>> We're leaving the same change to configset_add_value() for some future
>> series. Once we start paying attention to its return value we'd need
>> to ferry it up as deep as do_config_from(), and would need to make
>> least read_{,very_}early_config() and git_protected_config() return an
>> "int" instead of "void". Let's leave that for now, and focus on
>> the *_get_*() functions.
>>
>> In a subsequent commit we'll fix the other *_get_*() functions to so
>> that they'll ferry our underlying "ret" along, rather than normalizing
>> it to a "return 1". But as an intermediate step to that we'll need to
>> fix git_configset_get_value_multi() to return "int", and that change
>> itself is smaller because of this change to migrate some callers away
>> from the *_value_multi() API.
>
> I haven't read ahead, but on first impression this sounds like it might
> be too intrusive for a series whose goal is to clean up
> *_get_value_multi().

Yeah, that was my inclination too :) But Glen seemed to have a strong
opinion on the end-state of the topic being inconsistent in its API
(which he's right about, some stuff returning -1 or 1, some only 1).

I wanted to just leave it for a follow-up topic I've got to fix various
warts in the API, but cherry-picked & included the new 06/10 here to
address that concern.

I'm also confident that we can expose this to current API users, so
partly I'm playing reviewer flip-flop here and seeing what sticks. If
you feel it should be ejected I'm also happy to do that, and re-roll...

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v5 03/10] config API: add and use a "git_config_get()" family of functions
  2023-02-09 10:11             ` Ævar Arnfjörð Bjarmason
@ 2023-02-09 10:59               ` Ævar Arnfjörð Bjarmason
  2023-02-09 16:53                 ` Glen Choo
  0 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-02-09 10:59 UTC (permalink / raw)
  To: Glen Choo
  Cc: git, Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss


On Thu, Feb 09 2023, Ævar Arnfjörð Bjarmason wrote:

> On Thu, Feb 09 2023, Glen Choo wrote:
>
>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>>> [...]
>>> This still leaves various inconsistencies and clobbering or ignoring
>>> of the return value in place. E.g here we're modifying
>>> configset_add_value(), but ever since it was added in [2] we've been
>>> ignoring its "int" return value, but as we're changing the
>>> configset_find_element() it uses, let's have it faithfully ferry that
>>> "ret" along.
>>>
>>> Let's also use the "RESULT_MUST_BE_USED" macro introduced in [3] to
>>> assert that we're checking the return value of
>>> configset_find_element().
>>>
>>> We're leaving the same change to configset_add_value() for some future
>>> series. Once we start paying attention to its return value we'd need
>>> to ferry it up as deep as do_config_from(), and would need to make
>>> least read_{,very_}early_config() and git_protected_config() return an
>>> "int" instead of "void". Let's leave that for now, and focus on
>>> the *_get_*() functions.
>>>
>>> In a subsequent commit we'll fix the other *_get_*() functions to so
>>> that they'll ferry our underlying "ret" along, rather than normalizing
>>> it to a "return 1". But as an intermediate step to that we'll need to
>>> fix git_configset_get_value_multi() to return "int", and that change
>>> itself is smaller because of this change to migrate some callers away
>>> from the *_value_multi() API.
>>
>> I haven't read ahead, but on first impression this sounds like it might
>> be too intrusive for a series whose goal is to clean up
>> *_get_value_multi().
>
> Yeah, that was my inclination too :) But Glen seemed to have a strong
> opinion on the end-state of the topic being inconsistent in its API
> (which he's right about, some stuff returning -1 or 1, some only 1).

Hrm, so clearly I lost track of who I was replying to there, sorry :)

I thought this was a reply from Junio at the time.

But the rest of this stands, i.e. I thought I'd integrate this based on
your feedback on the previous version.

> I wanted to just leave it for a follow-up topic I've got to fix various
> warts in the API, but cherry-picked & included the new 06/10 here to
> address that concern.
>
> I'm also confident that we can expose this to current API users, so
> partly I'm playing reviewer flip-flop here and seeing what sticks. If
> you feel it should be ejected I'm also happy to do that, and re-roll...


^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v5 03/10] config API: add and use a "git_config_get()" family of functions
  2023-02-09 10:59               ` Ævar Arnfjörð Bjarmason
@ 2023-02-09 16:53                 ` Glen Choo
  0 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-02-09 16:53 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> On Thu, Feb 09 2023, Ævar Arnfjörð Bjarmason wrote:
>
>> On Thu, Feb 09 2023, Glen Choo wrote:
>>
>>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>>>> [...]
>>>> In a subsequent commit we'll fix the other *_get_*() functions to so
>>>> that they'll ferry our underlying "ret" along, rather than normalizing
>>>> it to a "return 1". But as an intermediate step to that we'll need to
>>>> fix git_configset_get_value_multi() to return "int", and that change
>>>> itself is smaller because of this change to migrate some callers away
>>>> from the *_value_multi() API.
>>>
>>> I haven't read ahead, but on first impression this sounds like it might
>>> be too intrusive for a series whose goal is to clean up
>>> *_get_value_multi().
>>
>> Yeah, that was my inclination too :) But Glen seemed to have a strong
>> opinion on the end-state of the topic being inconsistent in its API
>> (which he's right about, some stuff returning -1 or 1, some only 1).
>
> Hrm, so clearly I lost track of who I was replying to there, sorry :)
>
> I thought this was a reply from Junio at the time.

Heh. Maybe I do a good Junio impression.

>> I wanted to just leave it for a follow-up topic I've got to fix various
>> warts in the API, but cherry-picked & included the new 06/10 here to
>> address that concern.
>>
>> I'm also confident that we can expose this to current API users, so
>> partly I'm playing reviewer flip-flop here and seeing what sticks. If
>> you feel it should be ejected I'm also happy to do that, and re-roll...

Reading ahead, I think that 06/10 should probably be ejected; the series
is doing too many things. You're probably right that 06/10 is a safe
change to make, but it's a big enough change to require some careful
review. I don't think it's worth holding up the original *_multi()
changes, especially since I think they're pretty much mergeable.

The change would probably make more sense in the follow up topic. I
wouldn't mind giving that topic a look.

And if we are sending this follow up topic, then perhaps we could be
consistent about *_get() only returning 0 or 1 in this series, and the
follow up series could make all the functions ferry up the return code.
This does introduce some churn, but the consistency will be a good
property to have, especially if, in the follow up topic, we decide to do
something else with the API.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v6 0/9] config API: make "multi" safe, fix segfaults, propagate "ret"
  2023-02-07 16:10       ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Ævar Arnfjörð Bjarmason
                           ` (10 preceding siblings ...)
  2023-02-07 17:38         ` [PATCH v5 00/10] config API: make "multi" safe, fix segfaults, propagate "ret" Junio C Hamano
@ 2023-03-07 18:09         ` Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
                             ` (10 more replies)
  11 siblings, 11 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-07 18:09 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

This series fixes numerous segfaults in config API users, because they
didn't expect *_get_multi() to hand them a string_list with a NULL in
it given config like "[a] key" (note, no "="'s).

A larger general overview at v1[1], but note the API changes in
v2[2]. Changes since v5[3]:

 * Drop the 6th commit, which made existing API functions return
   -1. We shouldn't coerce errors to "return 1", but making the API
   consistent can wait for a follow-up to this topic.

   This should address the reason for this topic being stalled for a
   while, see e.g. [4].

1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/
2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/
3. https://lore.kernel.org/git/cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com/
4. https://lore.kernel.org/git/xmqqcz5snyxz.fsf@gitster.g/

Ævar Arnfjörð Bjarmason (9):
  config tests: cover blind spots in git_die_config() tests
  config tests: add "NULL" tests for *_get_value_multi()
  config API: add and use a "git_config_get()" family of functions
  versioncmp.c: refactor config reading next commit
  config API: have *_multi() return an "int" and take a "dest"
  for-each-repo: error on bad --config
  config API users: test for *_get_value_multi() segfaults
  config API: add "string" version of *_value_multi(), fix segfaults
  for-each-repo: with bad config, don't conflate <path> and <cmd>

 builtin/for-each-repo.c              |  14 ++--
 builtin/gc.c                         |  15 ++--
 builtin/log.c                        |   6 +-
 builtin/submodule--helper.c          |   7 +-
 builtin/worktree.c                   |   3 +-
 config.c                             | 109 ++++++++++++++++++++++-----
 config.h                             |  68 ++++++++++++++---
 pack-bitmap.c                        |   6 +-
 submodule.c                          |   3 +-
 t/helper/test-config.c               |  28 ++++++-
 t/t0068-for-each-repo.sh             |  19 +++++
 t/t1308-config-set.sh                | 108 +++++++++++++++++++++++++-
 t/t3309-notes-merge-auto-resolve.sh  |   7 +-
 t/t4202-log.sh                       |  15 ++++
 t/t5304-prune.sh                     |  12 ++-
 t/t5310-pack-bitmaps.sh              |  20 +++++
 t/t5552-skipping-fetch-negotiator.sh |  16 ++++
 t/t7004-tag.sh                       |  17 +++++
 t/t7413-submodule-is-active.sh       |  16 ++++
 t/t7900-maintenance.sh               |  38 ++++++++++
 versioncmp.c                         |  22 ++++--
 21 files changed, 477 insertions(+), 72 deletions(-)

Range-diff against v5:
 1:  cefc4188984 =  1:  43fdb0cf50c config tests: cover blind spots in git_die_config() tests
 2:  91a44456327 =  2:  4b0799090c9 config tests: add "NULL" tests for *_get_value_multi()
 3:  4a73151abde =  3:  62fe2f04e71 config API: add and use a "git_config_get()" family of functions
 4:  382a77ca69e =  4:  e36303f4d3d versioncmp.c: refactor config reading next commit
 5:  8f17bf8150c !  5:  e38523267e7 config API: have *_multi() return an "int" and take a "dest"
    @@ config.c: void git_die_config(const char *key, const char *err, ...)
      }
     
      ## config.h ##
    -@@ config.h: int git_configset_add_parameters(struct config_set *cs);
    +@@ config.h: int git_configset_add_file(struct config_set *cs, const char *filename);
      /**
       * Finds and returns the value list, sorted in order of increasing priority
       * for the configuration variable `key` and config set `cs`. When the
 6:  b515ff13f9b <  -:  ----------- config API: don't lose the git_*get*() return values
 7:  8a83c30ea78 =  6:  3a87b35e114 for-each-repo: error on bad --config
 8:  d9abc78c2be =  7:  66b7060f66f config API users: test for *_get_value_multi() segfaults
 9:  65fa91e7ce7 =  8:  0da4cdb3f6a config API: add "string" version of *_value_multi(), fix segfaults
10:  4db3c6d0ed9 =  9:  627eb15a319 for-each-repo: with bad config, don't conflate <path> and <cmd>
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v6 1/9] config tests: cover blind spots in git_die_config() tests
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
@ 2023-03-07 18:09           ` Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 2/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
                             ` (9 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-07 18:09 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

There were no tests checking for the output of the git_die_config()
function in the config API, added in 5a80e97c827 (config: add
`git_die_config()` to the config-set API, 2014-08-07). We only tested
"test_must_fail", but didn't assert the output.

We need tests for this because a subsequent commit will alter the
return value of git_config_get_value_multi(), which is used to get the
config values in the git_die_config() function. This test coverage
helps to build confidence in that subsequent change.

These tests cover different interactions with git_die_config():

- The "notes.mergeStrategy" test in
  "t/t3309-notes-merge-auto-resolve.sh" is a case where a function
  outside of config.c (git_config_get_notes_strategy()) calls
  git_die_config().

- The "gc.pruneExpire" test in "t5304-prune.sh" is a case where
  git_config_get_expiry() calls git_die_config(), covering a different
  "type" than the "string" test for "notes.mergeStrategy".

- The "fetch.negotiationAlgorithm" test in
  "t/t5552-skipping-fetch-negotiator.sh" is a case where
  git_config_get_string*() calls git_die_config().

We also cover both the "from command-line config" and "in file..at
line" cases here.

The clobbering of existing ".git/config" files here is so that we're
not implicitly testing the line count of the default config.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-
 t/t5304-prune.sh                     | 12 ++++++++++--
 t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh
index 141d3e4ca4d..9bd5dbf341f 100755
--- a/t/t3309-notes-merge-auto-resolve.sh
+++ b/t/t3309-notes-merge-auto-resolve.sh
@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '
 
 test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '
 	git config core.notesRef refs/notes/y &&
-	test_must_fail git -c notes.mergeStrategy="foo" notes merge z &&
+	cat >expect <<-\EOF &&
+	error: unknown notes merge strategy foo
+	fatal: unable to parse '\''notes.mergeStrategy'\'' from command-line config
+	EOF
+	test_must_fail git -c notes.mergeStrategy="foo" notes merge z 2>actual &&
+	test_cmp expect actual &&
 	# Verify no changes (y)
 	verify_notes y y
 '
diff --git a/t/t5304-prune.sh b/t/t5304-prune.sh
index d65a5f94b4b..5500dd08426 100755
--- a/t/t5304-prune.sh
+++ b/t/t5304-prune.sh
@@ -72,8 +72,16 @@ test_expect_success 'gc: implicit prune --expire' '
 '
 
 test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '
-	git config gc.pruneExpire invalid &&
-	test_must_fail git gc
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	>repo/.git/config &&
+	git -C repo config gc.pruneExpire invalid &&
+	cat >expect <<-\EOF &&
+	error: Invalid gc.pruneexpire: '\''invalid'\''
+	fatal: bad config variable '\''gc.pruneexpire'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_must_fail git -C repo gc 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'gc: start with ok gc.pruneExpire' '
diff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh
index 165427d57e5..b55a9f65e6b 100755
--- a/t/t5552-skipping-fetch-negotiator.sh
+++ b/t/t5552-skipping-fetch-negotiator.sh
@@ -3,6 +3,22 @@
 test_description='test skipping fetch negotiator'
 . ./test-lib.sh
 
+test_expect_success 'fetch.negotiationalgorithm config' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	cat >repo/.git/config <<-\EOF &&
+	[fetch]
+	negotiationAlgorithm
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''fetch.negotiationalgorithm'\''
+	fatal: bad config variable '\''fetch.negotiationalgorithm'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_expect_code 128 git -C repo fetch >out 2>actual &&
+	test_must_be_empty out &&
+	test_cmp expect actual
+'
+
 have_sent () {
 	while test "$#" -ne 0
 	do
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v6 2/9] config tests: add "NULL" tests for *_get_value_multi()
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
@ 2023-03-07 18:09           ` Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 3/9] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
                             ` (8 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-07 18:09 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

A less well known edge case in the config format is that keys can be
value-less, a shorthand syntax for "true" boolean keys. I.e. these two
are equivalent as far as "--type=bool" is concerned:

	[a]key
	[a]key = true

But as far as our parser is concerned the values for these two are
NULL, and "true". I.e. for a sequence like:

	[a]key=x
	[a]key
	[a]key=y

We get a "struct string_list" with "string" members with ".string"
values of:

	{ "x", NULL, "y" }

This behavior goes back to the initial implementation of
git_config_bool() in 17712991a59 (Add ".git/config" file parser,
2005-10-10).

When parts of the config_set API were tested for in [1] they didn't
add coverage for 3/4 of the "(NULL)" cases handled in
"t/helper/test-config.c". We'd test that case for "get_value", but not
"get_value_multi", "configset_get_value" and
"configset_get_value_multi".

We now cover all of those cases, which in turn expose the details of
how this part of the config API works.

1. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t1308-config-set.sh | 65 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 65 insertions(+)

diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index b38e158d3b2..4be1ab1147c 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -146,6 +146,71 @@ test_expect_success 'find multiple values' '
 	check_config get_value_multi case.baz sam bat hask
 '
 
+test_NULL_in_multi () {
+	local op="$1" &&
+	local file="$2" &&
+
+	test_expect_success "$op: NULL value in config${file:+ in $file}" '
+		config="$file" &&
+		if test -z "$config"
+		then
+			config=.git/config &&
+			test_when_finished "mv $config.old $config" &&
+			mv "$config" "$config".old
+		fi &&
+
+		# Value-less in the middle of a list
+		cat >"$config" <<-\EOF &&
+		[a]key=x
+		[a]key
+		[a]key=y
+		EOF
+		case "$op" in
+		*_multi)
+			cat >expect <<-\EOF
+			x
+			(NULL)
+			y
+			EOF
+			;;
+		*)
+			cat >expect <<-\EOF
+			y
+			EOF
+			;;
+		esac &&
+		test-tool config "$op" a.key $file >actual &&
+		test_cmp expect actual &&
+
+		# Value-less at the end of a least
+		cat >"$config" <<-\EOF &&
+		[a]key=x
+		[a]key=y
+		[a]key
+		EOF
+		case "$op" in
+		*_multi)
+			cat >expect <<-\EOF
+			x
+			y
+			(NULL)
+			EOF
+			;;
+		*)
+			cat >expect <<-\EOF
+			(NULL)
+			EOF
+			;;
+		esac &&
+		test-tool config "$op" a.key $file >actual &&
+		test_cmp expect actual
+	'
+}
+
+test_NULL_in_multi "get_value_multi"
+test_NULL_in_multi "configset_get_value" "my.config"
+test_NULL_in_multi "configset_get_value_multi" "my.config"
+
 test_expect_success 'find value from a configset' '
 	cat >config2 <<-\EOF &&
 	[case]
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v6 3/9] config API: add and use a "git_config_get()" family of functions
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 2/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2023-03-07 18:09           ` Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
                             ` (7 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-07 18:09 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

We already have the basic "git_config_get_value()" function and its
"repo_*" and "configset" siblings to get a given "key" and assign the
last key found to a provided "value".

But some callers don't care about that value, but just want to use the
return value of the "get_value()" function to check whether the key
exist (or another non-zero return value).

The immediate motivation for this is that a subsequent commit will
need to change all callers of the "*_get_value_multi()" family of
functions. In two cases here we (ab)used it to check whether we had
any values for the given key, but didn't care about the return value.

The rest of the callers here used various other config API functions
to do the same, all of which resolved to the same underlying functions
to provide the answer.

Some of these were using either git_config_get_string() or
git_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a
configure_added_submodule() leak, 2022-09-01) for a recent example. We
can now use a helper function that doesn't require a throwaway
variable.

We could have changed git_configset_get_value_multi() (and then
git_config_get_value() etc.) to accept a "NULL" as a "dest" for all
callers, but let's avoid changing the behavior of existing API
users. Having an "unused" value that we throw away internal to
config.c is cheap.

A "NULL as optional dest" pattern is also more fragile, as the intent
of the caller might be misinterpreted if he were to accidentally pass
"NULL", e.g. when "dest" is passed in from another function.

Another name for this function could have been
"*_config_key_exists()", as suggested in [1]. That would work for all
of these callers, and would currently be equivalent to this function,
as the git_configset_get_value() API normalizes all non-zero return
values to a "1".

But adding that API would set us up to lose information, as e.g. if
git_config_parse_key() in the underlying configset_find_element()
fails we'd like to return -1, not 1.

Let's change the underlying configset_find_element() function to
support this use-case, we'll make further use of it in a subsequent
commit where the git_configset_get_value_multi() function itself will
expose this new return value.

This still leaves various inconsistencies and clobbering or ignoring
of the return value in place. E.g here we're modifying
configset_add_value(), but ever since it was added in [2] we've been
ignoring its "int" return value, but as we're changing the
configset_find_element() it uses, let's have it faithfully ferry that
"ret" along.

Let's also use the "RESULT_MUST_BE_USED" macro introduced in [3] to
assert that we're checking the return value of
configset_find_element().

We're leaving the same change to configset_add_value() for some future
series. Once we start paying attention to its return value we'd need
to ferry it up as deep as do_config_from(), and would need to make
least read_{,very_}early_config() and git_protected_config() return an
"int" instead of "void". Let's leave that for now, and focus on
the *_get_*() functions.

In a subsequent commit we'll fix the other *_get_*() functions to so
that they'll ferry our underlying "ret" along, rather than normalizing
it to a "return 1". But as an intermediate step to that we'll need to
fix git_configset_get_value_multi() to return "int", and that change
itself is smaller because of this change to migrate some callers away
from the *_value_multi() API.

1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)
2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/
3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
   return values, 2022-09-01),

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                |  5 +---
 builtin/submodule--helper.c |  7 +++--
 builtin/worktree.c          |  3 +--
 config.c                    | 51 ++++++++++++++++++++++++++++++++-----
 config.h                    | 18 +++++++++++++
 t/helper/test-config.c      | 22 ++++++++++++++++
 t/t1308-config-set.sh       | 43 ++++++++++++++++++++++++++++++-
 7 files changed, 131 insertions(+), 18 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 02455fdcd73..e38d1783f30 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1493,7 +1493,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	};
 	int found = 0;
 	const char *key = "maintenance.repo";
-	char *config_value;
 	char *maintpath = get_maintpath();
 	struct string_list_item *item;
 	const struct string_list *list;
@@ -1508,9 +1507,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	git_config_set("maintenance.auto", "false");
 
 	/* Set maintenance strategy, if unset */
-	if (!git_config_get_string("maintenance.strategy", &config_value))
-		free(config_value);
-	else
+	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
 	list = git_config_get_value_multi(key);
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 4c173d8b37a..2278e8c91cb 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 	 * If there are no path args and submodule.active is set then,
 	 * by default, only initialize 'active' modules.
 	 */
-	if (!argc && git_config_get_value_multi("submodule.active"))
+	if (!argc && !git_config_get("submodule.active"))
 		module_list_active(&list);
 
 	info.prefix = prefix;
@@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 		 * If there are no path args and submodule.active is set then,
 		 * by default, only initialize 'active' modules.
 		 */
-		if (!argc && git_config_get_value_multi("submodule.active"))
+		if (!argc && !git_config_get("submodule.active"))
 			module_list_active(&list);
 
 		info.prefix = opt.prefix;
@@ -3140,7 +3140,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con
 static void configure_added_submodule(struct add_data *add_data)
 {
 	char *key;
-	const char *val;
 	struct child_process add_submod = CHILD_PROCESS_INIT;
 	struct child_process add_gitmodules = CHILD_PROCESS_INIT;
 
@@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)
 	 * is_submodule_active(), since that function needs to find
 	 * out the value of "submodule.active" again anyway.
 	 */
-	if (!git_config_get_string_tmp("submodule.active", &val)) {
+	if (!git_config_get("submodule.active")) {
 		/*
 		 * If the submodule being added isn't already covered by the
 		 * current configured pathspec, set the submodule's active flag
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 254283aa6f5..2d81965711f 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -319,7 +319,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 
 	if (file_exists(from_file)) {
 		struct config_set cs = { { 0 } };
-		const char *core_worktree;
 		int bare;
 
 		if (safe_create_leading_directories(to_file) ||
@@ -338,7 +337,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 				to_file, "core.bare", NULL, "true", 0))
 			error(_("failed to unset '%s' in '%s'"),
 				"core.bare", to_file);
-		if (!git_configset_get_value(&cs, "core.worktree", &core_worktree) &&
+		if (!git_configset_get(&cs, "core.worktree") &&
 			git_config_set_in_file_gently(to_file,
 							"core.worktree", NULL))
 			error(_("failed to unset '%s' in '%s'"),
diff --git a/config.c b/config.c
index 00090a32fc3..d4f0e4fd619 100644
--- a/config.c
+++ b/config.c
@@ -2289,23 +2289,29 @@ void read_very_early_config(config_fn_t cb, void *data)
 	config_with_options(cb, data, NULL, &opts);
 }
 
-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
+RESULT_MUST_BE_USED
+static int configset_find_element(struct config_set *cs, const char *key,
+				  struct config_set_element **dest)
 {
 	struct config_set_element k;
 	struct config_set_element *found_entry;
 	char *normalized_key;
+	int ret;
+
 	/*
 	 * `key` may come from the user, so normalize it before using it
 	 * for querying entries from the hashmap.
 	 */
-	if (git_config_parse_key(key, &normalized_key, NULL))
-		return NULL;
+	ret = git_config_parse_key(key, &normalized_key, NULL);
+	if (ret)
+		return ret;
 
 	hashmap_entry_init(&k.ent, strhash(normalized_key));
 	k.key = normalized_key;
 	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
 	free(normalized_key);
-	return found_entry;
+	*dest = found_entry;
+	return 0;
 }
 
 static int configset_add_value(struct config_set *cs, const char *key, const char *value)
@@ -2314,8 +2320,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha
 	struct string_list_item *si;
 	struct configset_list_item *l_item;
 	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
+	int ret;
 
-	e = configset_find_element(cs, key);
+	ret = configset_find_element(cs, key, &e);
+	if (ret)
+		return ret;
 	/*
 	 * Since the keys are being fed by git_config*() callback mechanism, they
 	 * are already normalized. So simply add them without any further munging.
@@ -2425,8 +2434,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *
 
 const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
 {
-	struct config_set_element *e = configset_find_element(cs, key);
-	return e ? &e->value_list : NULL;
+	struct config_set_element *e;
+
+	if (configset_find_element(cs, key, &e))
+		return NULL;
+	else if (!e)
+		return NULL;
+	return &e->value_list;
+}
+
+int git_configset_get(struct config_set *cs, const char *key)
+{
+	struct config_set_element *e;
+	int ret;
+
+	if ((ret = configset_find_element(cs, key, &e)))
+		return ret;
+	else if (!e)
+		return 1;
+	return 0;
 }
 
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
@@ -2565,6 +2591,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)
 	configset_iter(repo->config, fn, data);
 }
 
+int repo_config_get(struct repository *repo, const char *key)
+{
+	git_config_check_init(repo);
+	return git_configset_get(repo->config, key);
+}
+
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value)
 {
@@ -2679,6 +2711,11 @@ void git_config_clear(void)
 	repo_config_clear(the_repository);
 }
 
+int git_config_get(const char *key)
+{
+	return repo_config_get(the_repository, key);
+}
+
 int git_config_get_value(const char *key, const char **value)
 {
 	return repo_config_get_value(the_repository, key, value);
diff --git a/config.h b/config.h
index 7606246531a..7dd62ca81bf 100644
--- a/config.h
+++ b/config.h
@@ -465,6 +465,9 @@ void git_configset_clear(struct config_set *cs);
  * value in the 'dest' pointer.
  */
 
+RESULT_MUST_BE_USED
+int git_configset_get(struct config_set *cs, const char *key);
+
 /*
  * Finds the highest-priority value for the configuration variable `key`
  * and config set `cs`, stores the pointer to it in `value` and returns 0.
@@ -485,6 +488,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha
 /* Functions for reading a repository's config */
 struct repository;
 void repo_config(struct repository *repo, config_fn_t fn, void *data);
+
+/**
+ * Run only the discover part of the repo_config_get_*() functions
+ * below, in addition to 1 if not found, returns negative values on
+ * error (e.g. if the key itself is invalid).
+ */
+RESULT_MUST_BE_USED
+int repo_config_get(struct repository *repo, const char *key);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
 const struct string_list *repo_config_get_value_multi(struct repository *repo,
@@ -521,8 +532,15 @@ void git_protected_config(config_fn_t fn, void *data);
  * manner, the config API provides two functions `git_config_get_value`
  * and `git_config_get_value_multi`. They both read values from an internal
  * cache generated previously from reading the config files.
+ *
+ * For those git_config_get*() functions that aren't documented,
+ * consult the corresponding repo_config_get*() function's
+ * documentation.
  */
 
+RESULT_MUST_BE_USED
+int git_config_get(const char *key);
+
 /**
  * Finds the highest-priority value for the configuration variable `key`,
  * stores the pointer to it in `value` and returns 0. When the
diff --git a/t/helper/test-config.c b/t/helper/test-config.c
index 4ba9eb65606..cbb33ae1fff 100644
--- a/t/helper/test-config.c
+++ b/t/helper/test-config.c
@@ -14,6 +14,8 @@
  * get_value_multi -> prints all values for the entered key in increasing order
  *		     of priority
  *
+ * get -> print return value for the entered key
+ *
  * get_int -> print integer value for the entered key or die
  *
  * get_bool -> print bool value for the entered key or die
@@ -109,6 +111,26 @@ int cmd__config(int argc, const char **argv)
 			printf("Value not found for \"%s\"\n", argv[2]);
 			goto exit1;
 		}
+	} else if (argc == 3 && !strcmp(argv[1], "get")) {
+		int ret;
+
+		if (!(ret = git_config_get(argv[2])))
+			goto exit0;
+		else if (ret == 1)
+			printf("Value not found for \"%s\"\n", argv[2]);
+		else if (ret == -CONFIG_INVALID_KEY)
+			printf("Key \"%s\" is invalid\n", argv[2]);
+		else if (ret == -CONFIG_NO_SECTION_OR_NAME)
+			printf("Key \"%s\" has no section\n", argv[2]);
+		else
+			/*
+			 * A normal caller should just check "ret <
+			 * 0", but for our own tests let's BUG() if
+			 * our whitelist of git_config_parse_key()
+			 * return values isn't exhaustive.
+			 */
+			BUG("Key \"%s\" has unknown return %d", argv[2], ret);
+		goto exit1;
 	} else if (argc == 3 && !strcmp(argv[1], "get_int")) {
 		if (!git_config_get_int(argv[2], &val)) {
 			printf("%d\n", val);
diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index 4be1ab1147c..7def7053e1c 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -58,6 +58,8 @@ test_expect_success 'setup default config' '
 		skin = false
 		nose = 1
 		horns
+	[value]
+		less
 	EOF
 '
 
@@ -116,6 +118,45 @@ test_expect_success 'find value with the highest priority' '
 	check_config get_value case.baz "hask"
 '
 
+test_expect_success 'return value for an existing key' '
+	test-tool config get lamb.chop >out 2>err &&
+	test_must_be_empty out &&
+	test_must_be_empty err
+'
+
+test_expect_success 'return value for value-less key' '
+	test-tool config get value.less >out 2>err &&
+	test_must_be_empty out &&
+	test_must_be_empty err
+'
+
+test_expect_success 'return value for a missing key' '
+	cat >expect <<-\EOF &&
+	Value not found for "missing.key"
+	EOF
+	test_expect_code 1 test-tool config get missing.key >actual 2>err &&
+	test_cmp actual expect &&
+	test_must_be_empty err
+'
+
+test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '
+	cat >expect <<-\EOF &&
+	Key "fails.iskeychar.-" is invalid
+	EOF
+	test_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&
+	test_cmp actual expect &&
+	test_must_be_empty out
+'
+
+test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '
+	cat >expect <<-\EOF &&
+	Key "keynosection" has no section
+	EOF
+	test_expect_code 1 test-tool config get keynosection >actual 2>err &&
+	test_cmp actual expect &&
+	test_must_be_empty out
+'
+
 test_expect_success 'find integer value for a key' '
 	check_config get_int lamb.chop 65
 '
@@ -272,7 +313,7 @@ test_expect_success 'proper error on error in default config files' '
 	cp .git/config .git/config.old &&
 	test_when_finished "mv .git/config.old .git/config" &&
 	echo "[" >>.git/config &&
-	echo "fatal: bad config line 34 in file .git/config" >expect &&
+	echo "fatal: bad config line 36 in file .git/config" >expect &&
 	test_expect_code 128 test-tool config get_value foo.bar 2>actual &&
 	test_cmp expect actual
 '
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v6 4/9] versioncmp.c: refactor config reading next commit
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
                             ` (2 preceding siblings ...)
  2023-03-07 18:09           ` [PATCH v6 3/9] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
@ 2023-03-07 18:09           ` Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
                             ` (6 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-07 18:09 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Refactor the reading of the versionSort.suffix and
versionSort.prereleaseSuffix configuration variables to stay within
the bounds of our CodingGuidelines when it comes to line length, and
to avoid repeating ourselves.

Renaming "deprecated_prereleases" to "oldl" doesn't help us to avoid
line wrapping now, but it will in a subsequent commit.

Let's also split out the names of the config variables into variables
of our own, and refactor the nested if/else to avoid indenting it, and
the existing bracing style issue.

This all helps with the subsequent commit, where we'll need to start
checking different git_config_get_value_multi() return value. See
c026557a373 (versioncmp: generalize version sort suffix reordering,
2016-12-08) for the original implementation of most of this.

Moving the "initialized = 1" assignment allows us to move some of this
to the variable declarations in the subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 versioncmp.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/versioncmp.c b/versioncmp.c
index 069ee94a4d7..323f5d35ea8 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)
 	}
 
 	if (!initialized) {
-		const struct string_list *deprecated_prereleases;
+		const char *const newk = "versionsort.suffix";
+		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *oldl;
+
+		prereleases = git_config_get_value_multi(newk);
+		oldl = git_config_get_value_multi(oldk);
+		if (prereleases && oldl)
+			warning("ignoring %s because %s is set", oldk, newk);
+		else if (!prereleases)
+			prereleases = oldl;
+
 		initialized = 1;
-		prereleases = git_config_get_value_multi("versionsort.suffix");
-		deprecated_prereleases = git_config_get_value_multi("versionsort.prereleasesuffix");
-		if (prereleases) {
-			if (deprecated_prereleases)
-				warning("ignoring versionsort.prereleasesuffix because versionsort.suffix is set");
-		} else
-			prereleases = deprecated_prereleases;
 	}
 	if (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,
 					    &diff))
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v6 5/9] config API: have *_multi() return an "int" and take a "dest"
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
                             ` (3 preceding siblings ...)
  2023-03-07 18:09           ` [PATCH v6 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
@ 2023-03-07 18:09           ` Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
                             ` (5 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-07 18:09 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Have the "git_configset_get_value_multi()" function and its siblings
return an "int" and populate a "**dest" parameter like every other
git_configset_get_*()" in the API.

As we'll take advantage of in subsequent commits, this fixes a blind
spot in the API where it wasn't possible to tell whether a list was
empty from whether a config key existed. For now we don't make use of
those new return values, but faithfully convert existing API users.

Most of this is straightforward, commentary on cases that stand out:

- To ensure that we'll properly use the return values of this function
  in the future we're using the "RESULT_MUST_BE_USED" macro introduced
  in [1].

  As git_die_config() now has to handle this return value let's have
  it BUG() if it can't find the config entry. As tested for in a
  preceding commit we can rely on getting the config list in
  git_die_config().

- The loops after getting the "list" value in "builtin/gc.c" could
  also make use of "unsorted_string_list_has_string()" instead of using
  that loop, but let's leave that for now.

- In "versioncmp.c" we now use the return value of the functions,
  instead of checking if the lists are still non-NULL.

1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
   return values, 2022-09-01),

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c |  5 +----
 builtin/gc.c            | 10 ++++------
 builtin/log.c           |  6 +++---
 config.c                | 34 ++++++++++++++++++++--------------
 config.h                | 29 +++++++++++++++++++++--------
 pack-bitmap.c           |  6 +++++-
 submodule.c             |  3 +--
 t/helper/test-config.c  |  6 ++----
 versioncmp.c            | 11 +++++++----
 9 files changed, 64 insertions(+), 46 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 6aeac371488..fd0e7739e6a 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -45,14 +45,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	values = repo_config_get_value_multi(the_repository,
-					     config_key);
-
 	/*
 	 * Do nothing on an empty list, which is equivalent to the case
 	 * where the config variable does not exist at all.
 	 */
-	if (!values)
+	if (repo_config_get_value_multi(the_repository, config_key, &values))
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/builtin/gc.c b/builtin/gc.c
index e38d1783f30..2b3da377d52 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1510,8 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
-	list = git_config_get_value_multi(key);
-	if (list) {
+	if (!git_config_get_value_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1577,11 +1576,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 	if (config_file) {
 		git_configset_init(&cs);
 		git_configset_add_file(&cs, config_file);
-		list = git_configset_get_value_multi(&cs, key);
-	} else {
-		list = git_config_get_value_multi(key);
 	}
-	if (list) {
+	if (!(config_file
+	      ? git_configset_get_value_multi(&cs, key, &list)
+	      : git_config_get_value_multi(key, &list))) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index a70fba198f9..e43f6f9d8c1 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	int i;
 	char *value = NULL;
 	struct string_list *include = decoration_filter->include_ref_pattern;
-	const struct string_list *config_exclude =
-			git_config_get_value_multi("log.excludeDecoration");
+	const struct string_list *config_exclude;
 
-	if (config_exclude) {
+	if (!git_config_get_value_multi("log.excludeDecoration",
+					&config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/config.c b/config.c
index d4f0e4fd619..569819b4a1b 100644
--- a/config.c
+++ b/config.c
@@ -2418,29 +2418,34 @@ int git_configset_add_file(struct config_set *cs, const char *filename)
 int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
 {
 	const struct string_list *values = NULL;
+	int ret;
+
 	/*
 	 * Follows "last one wins" semantic, i.e., if there are multiple matches for the
 	 * queried key in the files of the configset, the value returned will be the last
 	 * value in the value list for that key.
 	 */
-	values = git_configset_get_value_multi(cs, key);
+	if ((ret = git_configset_get_value_multi(cs, key, &values)))
+		return ret;
 
-	if (!values)
-		return 1;
 	assert(values->nr > 0);
 	*value = values->items[values->nr - 1].string;
 	return 0;
 }
 
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest)
 {
 	struct config_set_element *e;
+	int ret;
 
-	if (configset_find_element(cs, key, &e))
-		return NULL;
+	if ((ret = configset_find_element(cs, key, &e)))
+		return ret;
 	else if (!e)
-		return NULL;
-	return &e->value_list;
+		return 1;
+	*dest = &e->value_list;
+
+	return 0;
 }
 
 int git_configset_get(struct config_set *cs, const char *key)
@@ -2604,11 +2609,11 @@ int repo_config_get_value(struct repository *repo,
 	return git_configset_get_value(repo->config, key, value);
 }
 
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key)
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest)
 {
 	git_config_check_init(repo);
-	return git_configset_get_value_multi(repo->config, key);
+	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
 int repo_config_get_string(struct repository *repo,
@@ -2721,9 +2726,9 @@ int git_config_get_value(const char *key, const char **value)
 	return repo_config_get_value(the_repository, key, value);
 }
 
-const struct string_list *git_config_get_value_multi(const char *key)
+int git_config_get_value_multi(const char *key, const struct string_list **dest)
 {
-	return repo_config_get_value_multi(the_repository, key);
+	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
 int git_config_get_string(const char *key, char **dest)
@@ -2870,7 +2875,8 @@ void git_die_config(const char *key, const char *err, ...)
 		error_fn(err, params);
 		va_end(params);
 	}
-	values = git_config_get_value_multi(key);
+	if (git_config_get_value_multi(key, &values))
+		BUG("for key '%s' we must have a value to report on", key);
 	kv_info = values->items[values->nr - 1].util;
 	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
 }
diff --git a/config.h b/config.h
index 7dd62ca81bf..4db6b90ac20 100644
--- a/config.h
+++ b/config.h
@@ -450,10 +450,18 @@ int git_configset_add_file(struct config_set *cs, const char *filename);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key` and config set `cs`. When the
- * configuration variable `key` is not found, returns NULL. The caller
- * should not free or modify the returned pointer, as it is owned by the cache.
+ * configuration variable `key` is not found, returns 1 without touching
+ * `value`.
+ *
+ * The key will be parsed for validity with git_config_parse_key(), on
+ * error a negative value will be returned.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
+RESULT_MUST_BE_USED
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest);
 
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
@@ -498,8 +506,9 @@ RESULT_MUST_BE_USED
 int repo_config_get(struct repository *repo, const char *key);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key);
+RESULT_MUST_BE_USED
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -553,10 +562,14 @@ int git_config_get_value(const char *key, const char **value);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key`. When the configuration variable
- * `key` is not found, returns NULL. The caller should not free or modify
- * the returned pointer, as it is owned by the cache.
+ * `key` is not found, returns 1 without touching `value`.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_config_get_value_multi(const char *key);
+RESULT_MUST_BE_USED
+int git_config_get_value_multi(const char *key,
+			       const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index d2a42abf28c..15c5eb507c0 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2314,7 +2314,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)
 
 const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
-	return repo_config_get_value_multi(r, "pack.preferbitmaptips");
+	const struct string_list *dest;
+
+	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+		return dest;
+	return NULL;
 }
 
 int bitmap_is_preferred_refname(struct repository *r, const char *refname)
diff --git a/submodule.c b/submodule.c
index 3a0dfc417c0..4b6f5223b0c 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	sl = repo_config_get_value_multi(repo, "submodule.active");
-	if (sl) {
+	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/helper/test-config.c b/t/helper/test-config.c
index cbb33ae1fff..6dc4c37444f 100644
--- a/t/helper/test-config.c
+++ b/t/helper/test-config.c
@@ -97,8 +97,7 @@ int cmd__config(int argc, const char **argv)
 			goto exit1;
 		}
 	} else if (argc == 3 && !strcmp(argv[1], "get_value_multi")) {
-		strptr = git_config_get_value_multi(argv[2]);
-		if (strptr) {
+		if (!git_config_get_value_multi(argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
@@ -181,8 +180,7 @@ int cmd__config(int argc, const char **argv)
 				goto exit2;
 			}
 		}
-		strptr = git_configset_get_value_multi(&cs, argv[2]);
-		if (strptr) {
+		if (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
diff --git a/versioncmp.c b/versioncmp.c
index 323f5d35ea8..60c3a517122 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)
 	if (!initialized) {
 		const char *const newk = "versionsort.suffix";
 		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *newl;
 		const struct string_list *oldl;
+		int new = git_config_get_value_multi(newk, &newl);
+		int old = git_config_get_value_multi(oldk, &oldl);
 
-		prereleases = git_config_get_value_multi(newk);
-		oldl = git_config_get_value_multi(oldk);
-		if (prereleases && oldl)
+		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-		else if (!prereleases)
+		if (!new)
+			prereleases = newl;
+		else if (!old)
 			prereleases = oldl;
 
 		initialized = 1;
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v6 6/9] for-each-repo: error on bad --config
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
                             ` (4 preceding siblings ...)
  2023-03-07 18:09           ` [PATCH v6 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
@ 2023-03-07 18:09           ` Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 7/9] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
                             ` (4 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-07 18:09 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

As noted in 6c62f015520 (for-each-repo: do nothing on empty config,
2021-01-08) this command wants to ignore a non-existing config key,
but let's not conflate that with bad config.

Before this, all these added tests would pass with an exit code of 0.

We could preserve the comment added in 6c62f015520, but now that we're
directly using the documented repo_config_get_value_multi() value it's
just narrating something that should be obvious from the API use, so
let's drop it.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  | 11 ++++++-----
 t/t0068-for-each-repo.sh |  6 ++++++
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index fd0e7739e6a..224164addb3 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	static const char *config_key = NULL;
 	int i, result = 0;
 	const struct string_list *values;
+	int err;
 
 	const struct option options[] = {
 		OPT_STRING(0, "config", &config_key, N_("config"),
@@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	/*
-	 * Do nothing on an empty list, which is equivalent to the case
-	 * where the config variable does not exist at all.
-	 */
-	if (repo_config_get_value_multi(the_repository, config_key, &values))
+	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	if (err < 0)
+		usage_msg_optf(_("got bad config --config=%s"),
+			       for_each_repo_usage, options, config_key);
+	else if (err)
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 3648d439a87..6b51e00da0e 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '
 	git for-each-repo --config=bogus.config -- help --no-such-option
 '
 
+test_expect_success 'error on bad config keys' '
+	test_expect_code 129 git for-each-repo --config=a &&
+	test_expect_code 129 git for-each-repo --config=a.b. &&
+	test_expect_code 129 git for-each-repo --config="'\''.b"
+'
+
 test_done
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v6 7/9] config API users: test for *_get_value_multi() segfaults
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
                             ` (5 preceding siblings ...)
  2023-03-07 18:09           ` [PATCH v6 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
@ 2023-03-07 18:09           ` Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 8/9] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
                             ` (3 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-07 18:09 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

As we'll discuss in the subsequent commit these tests all
show *_get_value_multi() API users unable to handle there being a
value-less key in the config, which is represented with a "NULL" for
that entry in the "string" member of the returned "struct
string_list", causing a segfault.

These added tests exhaustively test for that issue, as we'll see in a
subsequent commit we'll need to change all of the API users
of *_get_value_multi(). These cases were discovered by triggering each
one individually, and then adding these tests.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t4202-log.sh                 | 11 +++++++++++
 t/t5310-pack-bitmaps.sh        | 16 ++++++++++++++++
 t/t7004-tag.sh                 | 12 ++++++++++++
 t/t7413-submodule-is-active.sh | 12 ++++++++++++
 t/t7900-maintenance.sh         | 23 +++++++++++++++++++++++
 5 files changed, 74 insertions(+)

diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 2ce2b41174d..e4f02d8208b 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,6 +835,17 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
+test_expect_failure 'parse log.excludeDecoration with no value' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[log]
+		excludeDecoration
+	EOF
+	git log --decorate=short
+'
+
 test_expect_success 'decorate-refs with glob' '
 	cat >expect.decorate <<-\EOF &&
 	Merge-tag-reach
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 7d8dee41b0d..0306b399188 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,6 +404,22 @@ test_bitmap_cases () {
 		)
 	'
 
+	test_expect_failure 'pack.preferBitmapTips' '
+		git init repo &&
+		test_when_finished "rm -rf repo" &&
+		(
+			cd repo &&
+			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
+			test_commit_bulk --message="%s" 103 &&
+
+			cat >>.git/config <<-\EOF &&
+			[pack]
+				preferBitmapTips
+			EOF
+			git repack -adb
+		)
+	'
+
 	test_expect_success 'complains about multiple pack bitmaps' '
 		rm -fr repo &&
 		git init repo &&
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index 9aa1660651b..f343551a7d4 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,6 +1843,18 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
+test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[versionsort]
+		prereleaseSuffix
+		suffix
+	EOF
+	git tag -l --sort=version:refname
+'
+
 test_expect_success 'version sort with prerelease reordering' '
 	test_config versionsort.prereleaseSuffix -rc &&
 	git tag foo1.6-rc1 &&
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index 7cdc2637649..bfe27e50732 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,6 +51,18 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
+test_expect_failure 'is-active handles submodule.active config missing a value' '
+	cp super/.git/config super/.git/config.orig &&
+	test_when_finished mv super/.git/config.orig super/.git/config &&
+
+	cat >>super/.git/config <<-\EOF &&
+	[submodule]
+		active
+	EOF
+
+	test-tool -C super submodule is-active sub1
+'
+
 test_expect_success 'is-active works with basic submodule.active config' '
 	test_when_finished "git -C super config submodule.sub1.URL ../sub" &&
 	test_when_finished "git -C super config --unset-all submodule.active" &&
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 823331e44a0..d82eac6a471 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -524,6 +524,29 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --config-file ./other --force
 '
 
+test_expect_failure 'register with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	git maintenance register
+'
+
+test_expect_failure 'unregister with no value for maintenance.repo' '
+	cp .git/config .git/config.orig &&
+	test_when_finished mv .git/config.orig .git/config &&
+
+	cat >>.git/config <<-\EOF &&
+	[maintenance]
+		repo
+	EOF
+	git maintenance unregister &&
+	git maintenance unregister --force
+'
+
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
 	META="a+b*c" &&
 	git init "$META" &&
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v6 8/9] config API: add "string" version of *_value_multi(), fix segfaults
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
                             ` (6 preceding siblings ...)
  2023-03-07 18:09           ` [PATCH v6 7/9] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
@ 2023-03-07 18:09           ` Ævar Arnfjörð Bjarmason
  2023-03-07 18:09           ` [PATCH v6 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
                             ` (2 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-07 18:09 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Fix numerous and mostly long-standing segfaults in consumers of
the *_config_*value_multi() API. As discussed in the preceding commit
an empty key in the config syntax yields a "NULL" string, which these
users would give to strcmp() (or similar), resulting in segfaults.

As this change shows, most users users of the *_config_*value_multi()
API didn't really want such an an unsafe and low-level API, let's give
them something with the safety of git_config_get_string() instead.

This fix is similar to what the *_string() functions and others
acquired in[1] and [2]. Namely introducing and using a safer
"*_get_string_multi()" variant of the low-level "_*value_multi()"
function.

This fixes segfaults in code introduced in:

  - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26)
  - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08)
  - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17)
  - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16)
  - 92156291ca8 (log: add default decoration filter, 2022-08-05)
  - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27)

There are now two users ofthe low-level API:

- One in "builtin/for-each-repo.c", which we'll convert in a
  subsequent commit.

- The "t/helper/test-config.c" code added in [3].

As seen in the preceding commit we need to give the
"t/helper/test-config.c" caller these "NULL" entries.

We could also alter the underlying git_configset_get_value_multi()
function to be "string safe", but doing so would leave no room for
other variants of "*_get_value_multi()" that coerce to other types.

Such coercion can't be built on the string version, since as we've
established "NULL" is a true value in the boolean context, but if we
coerced it to "" for use in a list of strings it'll be subsequently
coerced to "false" as a boolean.

The callback pattern being used here will make it easy to introduce
e.g. a "multi" variant which coerces its values to "bool", "int",
"path" etc.

1. 40ea4ed9032 (Add config_error_nonbool() helper function,
   2008-02-11)
2. 6c47d0e8f39 (config.c: guard config parser from value=NULL,
   2008-02-11).
3. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                   |  6 +++---
 builtin/log.c                  |  4 ++--
 config.c                       | 32 ++++++++++++++++++++++++++++++++
 config.h                       | 19 +++++++++++++++++++
 pack-bitmap.c                  |  2 +-
 submodule.c                    |  2 +-
 t/t4202-log.sh                 |  8 ++++++--
 t/t5310-pack-bitmaps.sh        |  8 ++++++--
 t/t7004-tag.sh                 |  9 +++++++--
 t/t7413-submodule-is-active.sh |  8 ++++++--
 t/t7900-maintenance.sh         | 25 ++++++++++++++++++++-----
 versioncmp.c                   |  4 ++--
 12 files changed, 105 insertions(+), 22 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 2b3da377d52..9497bdf23e4 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1510,7 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
-	if (!git_config_get_value_multi(key, &list)) {
+	if (!git_config_get_string_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1578,8 +1578,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 		git_configset_add_file(&cs, config_file);
 	}
 	if (!(config_file
-	      ? git_configset_get_value_multi(&cs, key, &list)
-	      : git_config_get_value_multi(key, &list))) {
+	      ? git_configset_get_string_multi(&cs, key, &list)
+	      : git_config_get_string_multi(key, &list))) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index e43f6f9d8c1..ca847524fa4 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	struct string_list *include = decoration_filter->include_ref_pattern;
 	const struct string_list *config_exclude;
 
-	if (!git_config_get_value_multi("log.excludeDecoration",
-					&config_exclude)) {
+	if (!git_config_get_string_multi("log.excludeDecoration",
+					 &config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/config.c b/config.c
index 569819b4a1b..c63034fb78b 100644
--- a/config.c
+++ b/config.c
@@ -2448,6 +2448,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,
 	return 0;
 }
 
+static int check_multi_string(struct string_list_item *item, void *util)
+{
+	return item->string ? 0 : config_error_nonbool(util);
+}
+
+int git_configset_get_string_multi(struct config_set *cs, const char *key,
+				   const struct string_list **dest)
+{
+	int ret;
+
+	if ((ret = git_configset_get_value_multi(cs, key, dest)))
+		return ret;
+	if ((ret = for_each_string_list((struct string_list *)*dest,
+					check_multi_string, (void *)key)))
+		return ret;
+
+	return 0;
+}
+
 int git_configset_get(struct config_set *cs, const char *key)
 {
 	struct config_set_element *e;
@@ -2616,6 +2635,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,
 	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
+int repo_config_get_string_multi(struct repository *repo, const char *key,
+				 const struct string_list **dest)
+{
+	git_config_check_init(repo);
+	return git_configset_get_string_multi(repo->config, key, dest);
+}
+
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest)
 {
@@ -2731,6 +2757,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)
 	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
+int git_config_get_string_multi(const char *key,
+				const struct string_list **dest)
+{
+	return repo_config_get_string_multi(the_repository, key, dest);
+}
+
 int git_config_get_string(const char *key, char **dest)
 {
 	return repo_config_get_string(the_repository, key, dest);
diff --git a/config.h b/config.h
index 4db6b90ac20..5f258e5b8df 100644
--- a/config.h
+++ b/config.h
@@ -463,6 +463,19 @@ RESULT_MUST_BE_USED
 int git_configset_get_value_multi(struct config_set *cs, const char *key,
 				  const struct string_list **dest);
 
+/**
+ * A validation wrapper for git_configset_get_value_multi() which does
+ * for it what git_configset_get_string() does for
+ * git_configset_get_value().
+ *
+ * The configuration syntax allows for "[section] key", which will
+ * give us a NULL entry in the "struct string_list", as opposed to
+ * "[section] key =" which is the empty string. Most users of the API
+ * are not prepared to handle NULL in a "struct string_list".
+ */
+int git_configset_get_string_multi(struct config_set *cs, const char *key,
+				   const struct string_list **dest);
+
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
  */
@@ -509,6 +522,9 @@ int repo_config_get_value(struct repository *repo,
 RESULT_MUST_BE_USED
 int repo_config_get_value_multi(struct repository *repo, const char *key,
 				const struct string_list **dest);
+RESULT_MUST_BE_USED
+int repo_config_get_string_multi(struct repository *repo, const char *key,
+				 const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -570,6 +586,9 @@ int git_config_get_value(const char *key, const char **value);
 RESULT_MUST_BE_USED
 int git_config_get_value_multi(const char *key,
 			       const struct string_list **dest);
+RESULT_MUST_BE_USED
+int git_config_get_string_multi(const char *key,
+				const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index 15c5eb507c0..d003c7e60b4 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2316,7 +2316,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
 	const struct string_list *dest;
 
-	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+	if (!repo_config_get_string_multi(r, "pack.preferbitmaptips", &dest))
 		return dest;
 	return NULL;
 }
diff --git a/submodule.c b/submodule.c
index 4b6f5223b0c..30a103246ec 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
+	if (!repo_config_get_string_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index e4f02d8208b..ae73aef922f 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '
 
 '
 
-test_expect_failure 'parse log.excludeDecoration with no value' '
+test_expect_success 'parse log.excludeDecoration with no value' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '
 	[log]
 		excludeDecoration
 	EOF
-	git log --decorate=short
+	cat >expect <<-\EOF &&
+	error: missing value for '\''log.excludeDecoration'\''
+	EOF
+	git log --decorate=short 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'decorate-refs with glob' '
diff --git a/t/t5310-pack-bitmaps.sh b/t/t5310-pack-bitmaps.sh
index 0306b399188..526a5a506eb 100755
--- a/t/t5310-pack-bitmaps.sh
+++ b/t/t5310-pack-bitmaps.sh
@@ -404,7 +404,7 @@ test_bitmap_cases () {
 		)
 	'
 
-	test_expect_failure 'pack.preferBitmapTips' '
+	test_expect_success 'pack.preferBitmapTips' '
 		git init repo &&
 		test_when_finished "rm -rf repo" &&
 		(
@@ -416,7 +416,11 @@ test_bitmap_cases () {
 			[pack]
 				preferBitmapTips
 			EOF
-			git repack -adb
+			cat >expect <<-\EOF &&
+			error: missing value for '\''pack.preferbitmaptips'\''
+			EOF
+			git repack -adb 2>actual &&
+			test_cmp expect actual
 		)
 	'
 
diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh
index f343551a7d4..f4a31ada79a 100755
--- a/t/t7004-tag.sh
+++ b/t/t7004-tag.sh
@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '
 	test_must_fail git tag -l "foo*"
 '
 
-test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
+test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas
 		prereleaseSuffix
 		suffix
 	EOF
-	git tag -l --sort=version:refname
+	cat >expect <<-\EOF &&
+	error: missing value for '\''versionsort.suffix'\''
+	error: missing value for '\''versionsort.prereleasesuffix'\''
+	EOF
+	git tag -l --sort=version:refname 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'version sort with prerelease reordering' '
diff --git a/t/t7413-submodule-is-active.sh b/t/t7413-submodule-is-active.sh
index bfe27e50732..887d181b72e 100755
--- a/t/t7413-submodule-is-active.sh
+++ b/t/t7413-submodule-is-active.sh
@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
 	test-tool -C super submodule is-active sub1
 '
 
-test_expect_failure 'is-active handles submodule.active config missing a value' '
+test_expect_success 'is-active handles submodule.active config missing a value' '
 	cp super/.git/config super/.git/config.orig &&
 	test_when_finished mv super/.git/config.orig super/.git/config &&
 
@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'
 		active
 	EOF
 
-	test-tool -C super submodule is-active sub1
+	cat >expect <<-\EOF &&
+	error: missing value for '\''submodule.active'\''
+	EOF
+	test-tool -C super submodule is-active sub1 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'is-active works with basic submodule.active config' '
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index d82eac6a471..487e326b3fa 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '
 	git maintenance unregister --config-file ./other --force
 '
 
-test_expect_failure 'register with no value for maintenance.repo' '
+test_expect_success 'register with no value for maintenance.repo' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '
 	[maintenance]
 		repo
 	EOF
-	git maintenance register
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	git maintenance register 2>actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
 '
 
-test_expect_failure 'unregister with no value for maintenance.repo' '
+test_expect_success 'unregister with no value for maintenance.repo' '
 	cp .git/config .git/config.orig &&
 	test_when_finished mv .git/config.orig .git/config &&
 
@@ -543,8 +548,18 @@ test_expect_failure 'unregister with no value for maintenance.repo' '
 	[maintenance]
 		repo
 	EOF
-	git maintenance unregister &&
-	git maintenance unregister --force
+	cat >expect <<-\EOF &&
+	error: missing value for '\''maintenance.repo'\''
+	EOF
+	test_expect_code 128 git maintenance unregister 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo &&
+
+	git maintenance unregister --force 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual &&
+	git config maintenance.repo
 '
 
 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
diff --git a/versioncmp.c b/versioncmp.c
index 60c3a517122..7498da96e0e 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)
 		const char *const oldk = "versionsort.prereleasesuffix";
 		const struct string_list *newl;
 		const struct string_list *oldl;
-		int new = git_config_get_value_multi(newk, &newl);
-		int old = git_config_get_value_multi(oldk, &oldl);
+		int new = git_config_get_string_multi(newk, &newl);
+		int old = git_config_get_string_multi(oldk, &oldl);
 
 		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v6 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd>
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
                             ` (7 preceding siblings ...)
  2023-03-07 18:09           ` [PATCH v6 8/9] config API: add "string" version of *_value_multi(), fix segfaults Ævar Arnfjörð Bjarmason
@ 2023-03-07 18:09           ` Ævar Arnfjörð Bjarmason
  2023-03-08  0:48           ` [PATCH v6 0/9] config API: make "multi" safe, fix segfaults, propagate "ret" Glen Choo
  2023-03-08  9:06           ` [PATCH v7 " Ævar Arnfjörð Bjarmason
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-07 18:09 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Fix a logic error in 4950b2a2b5c (for-each-repo: run subcommands on
configured repos, 2020-09-11). Due to assuming that elements returned
from the repo_config_get_value_multi() call wouldn't be "NULL" we'd
conflate the <path> and <command> part of the argument list when
running commands.

As noted in the preceding commit the fix is to move to a safer
"*_string_multi()" version of the *_multi() API. This change is
separated from the rest because those all segfaulted. In this change
we ended up with different behavior.

When using the "--config=<config>" form we take each element of the
list as a path to a repository. E.g. with a configuration like:

	[repo] list = /some/repo

We would, with this command:

	git for-each-repo --config=repo.list status builtin

Run a "git status" in /some/repo, as:

	git -C /some/repo status builtin

I.e. ask "status" to report on the "builtin" directory. But since a
configuration such as this would result in a "struct string_list *"
with one element, whose "string" member is "NULL":

	[repo] list

We would, when constructing our command-line in
"builtin/for-each-repo.c"...

	strvec_pushl(&child.args, "-C", path, NULL);
	for (i = 0; i < argc; i++)
		strvec_push(&child.args, argv[i]);

...have that "path" be "NULL", and as strvec_pushl() stops when it
sees NULL we'd end with the first "argv" element as the argument to
the "-C" option, e.g.:

	git -C status builtin

I.e. we'd run the command "builtin" in the "status" directory.

In another context this might be an interesting security
vulnerability, but I think that this amounts to a nothingburger on
that front.

A hypothetical attacker would need to be able to write config for the
victim to run, if they're able to do that there's more interesting
attack vectors. See the "safe.directory" facility added in
8d1a7448206 (setup.c: create `safe.bareRepository`, 2022-07-14).

An even more unlikely possibility would be an attacker able to
generate the config used for "for-each-repo --config=<key>", but
nothing else (e.g. an automated system producing that list).

Even in that case the attack vector is limited to the user running
commands whose name matches a directory that's interesting to the
attacker (e.g. a "log" directory in a repository). The second
argument (if any) of the command is likely to make git die without
doing anything interesting (e.g. "-p" to "log", there being no "-p"
built-in command to run).

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  |  2 +-
 t/t0068-for-each-repo.sh | 13 +++++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 224164addb3..ce8f7a99086 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -46,7 +46,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	err = repo_config_get_string_multi(the_repository, config_key, &values);
 	if (err < 0)
 		usage_msg_optf(_("got bad config --config=%s"),
 			       for_each_repo_usage, options, config_key);
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 6b51e00da0e..4b90b74d5d5 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -46,4 +46,17 @@ test_expect_success 'error on bad config keys' '
 	test_expect_code 129 git for-each-repo --config="'\''.b"
 '
 
+test_expect_success 'error on NULL value for config keys' '
+	cat >>.git/config <<-\EOF &&
+	[empty]
+		key
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''empty.key'\''
+	EOF
+	test_expect_code 129 git for-each-repo --config=empty.key 2>actual.raw &&
+	grep ^error actual.raw >actual &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v6 0/9] config API: make "multi" safe, fix segfaults, propagate "ret"
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
                             ` (8 preceding siblings ...)
  2023-03-07 18:09           ` [PATCH v6 9/9] for-each-repo: with bad config, don't conflate <path> and <cmd> Ævar Arnfjörð Bjarmason
@ 2023-03-08  0:48           ` Glen Choo
  2023-03-08  9:06           ` [PATCH v7 " Ævar Arnfjörð Bjarmason
  10 siblings, 0 replies; 134+ messages in thread
From: Glen Choo @ 2023-03-08  0:48 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Calvin Wan, Emily Shaffer,
	raymond, zweiss, Ævar Arnfjörð Bjarmason

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> Range-diff against v5:
>  1:  cefc4188984 =  1:  43fdb0cf50c config tests: cover blind spots in git_die_config() tests
>  2:  91a44456327 =  2:  4b0799090c9 config tests: add "NULL" tests for *_get_value_multi()
>  3:  4a73151abde =  3:  62fe2f04e71 config API: add and use a "git_config_get()" family of functions
>  4:  382a77ca69e =  4:  e36303f4d3d versioncmp.c: refactor config reading next commit
>  5:  8f17bf8150c !  5:  e38523267e7 config API: have *_multi() return an "int" and take a "dest"
>     @@ config.c: void git_die_config(const char *key, const char *err, ...)
>       }
>      
>       ## config.h ##
>     -@@ config.h: int git_configset_add_parameters(struct config_set *cs);
>     +@@ config.h: int git_configset_add_file(struct config_set *cs, const char *filename);
>       /**
>        * Finds and returns the value list, sorted in order of increasing priority
>        * for the configuration variable `key` and config set `cs`. When the
>  6:  b515ff13f9b <  -:  ----------- config API: don't lose the git_*get*() return values
>  7:  8a83c30ea78 =  6:  3a87b35e114 for-each-repo: error on bad --config
>  8:  d9abc78c2be =  7:  66b7060f66f config API users: test for *_get_value_multi() segfaults
>  9:  65fa91e7ce7 =  8:  0da4cdb3f6a config API: add "string" version of *_value_multi(), fix segfaults
> 10:  4db3c6d0ed9 =  9:  627eb15a319 for-each-repo: with bad config, don't conflate <path> and <cmd>

I haven't reread the series in its totality yet (I should get to it in
the next few days), but a small-ish thing that jumps out from
the range-diff is that this version doesn't revert the commit message
changes in the previous version (v5 CL [1]) that referred to the ejected
06/10. I.e. v5 said that we were changing the return values of the
*_get_*() functions so that the new function is not a special snowflake,
and those commit messages haven't been changed.

1. https://lore.kernel.org/git/cover-v5-00.10-00000000000-20230207T154000Z-avarab@gmail.com/

^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v7 0/9] config API: make "multi" safe, fix segfaults, propagate "ret"
  2023-03-07 18:09         ` [PATCH v6 0/9] " Ævar Arnfjörð Bjarmason
                             ` (9 preceding siblings ...)
  2023-03-08  0:48           ` [PATCH v6 0/9] config API: make "multi" safe, fix segfaults, propagate "ret" Glen Choo
@ 2023-03-08  9:06           ` Ævar Arnfjörð Bjarmason
  2023-03-08  9:06             ` [PATCH v7 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
                               ` (10 more replies)
  10 siblings, 11 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-08  9:06 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

This series fixes numerous segfaults in config API users, because they
didn't expect *_get_multi() to hand them a string_list with a NULL in
it given config like "[a] key" (note, no "="'s).

A larger general overview at v1[1], but note the API changes in
v2[2]. Changes since v6[3]:

 * Glen pointed out that ejecting a commit in v6 orphaned a
   corresponding forward-reference in a commit message, fix that.

1. https://lore.kernel.org/git/cover-00.10-00000000000-20221026T151328Z-avarab@gmail.com/
2. https://lore.kernel.org/git/cover-v2-0.9-00000000000-20221101T225822Z-avarab@gmail.com/
3. https://lore.kernel.org/git/cover-v6-0.9-00000000000-20230307T180516Z-avarab@gmail.com/
4. https://lore.kernel.org/git/kl6lwn3sgjam.fsf@chooglen-macbookpro.roam.corp.google.com/

Ævar Arnfjörð Bjarmason (9):
  config tests: cover blind spots in git_die_config() tests
  config tests: add "NULL" tests for *_get_value_multi()
  config API: add and use a "git_config_get()" family of functions
  versioncmp.c: refactor config reading next commit
  config API: have *_multi() return an "int" and take a "dest"
  for-each-repo: error on bad --config
  config API users: test for *_get_value_multi() segfaults
  config API: add "string" version of *_value_multi(), fix segfaults
  for-each-repo: with bad config, don't conflate <path> and <cmd>

 builtin/for-each-repo.c              |  14 ++--
 builtin/gc.c                         |  15 ++--
 builtin/log.c                        |   6 +-
 builtin/submodule--helper.c          |   7 +-
 builtin/worktree.c                   |   3 +-
 config.c                             | 109 ++++++++++++++++++++++-----
 config.h                             |  68 ++++++++++++++---
 pack-bitmap.c                        |   6 +-
 submodule.c                          |   3 +-
 t/helper/test-config.c               |  28 ++++++-
 t/t0068-for-each-repo.sh             |  19 +++++
 t/t1308-config-set.sh                | 108 +++++++++++++++++++++++++-
 t/t3309-notes-merge-auto-resolve.sh  |   7 +-
 t/t4202-log.sh                       |  15 ++++
 t/t5304-prune.sh                     |  12 ++-
 t/t5310-pack-bitmaps.sh              |  20 +++++
 t/t5552-skipping-fetch-negotiator.sh |  16 ++++
 t/t7004-tag.sh                       |  17 +++++
 t/t7413-submodule-is-active.sh       |  16 ++++
 t/t7900-maintenance.sh               |  38 ++++++++++
 versioncmp.c                         |  22 ++++--
 21 files changed, 477 insertions(+), 72 deletions(-)

Range-diff against v6:
 1:  43fdb0cf50c =  1:  9f297a35e14 config tests: cover blind spots in git_die_config() tests
 2:  4b0799090c9 =  2:  45d483066ef config tests: add "NULL" tests for *_get_value_multi()
 3:  62fe2f04e71 !  3:  a977b7b188f config API: add and use a "git_config_get()" family of functions
    @@ Commit message
         "int" instead of "void". Let's leave that for now, and focus on
         the *_get_*() functions.
     
    -    In a subsequent commit we'll fix the other *_get_*() functions to so
    -    that they'll ferry our underlying "ret" along, rather than normalizing
    -    it to a "return 1". But as an intermediate step to that we'll need to
    -    fix git_configset_get_value_multi() to return "int", and that change
    -    itself is smaller because of this change to migrate some callers away
    -    from the *_value_multi() API.
    -
         1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)
         2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/
         3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
 4:  e36303f4d3d =  4:  3a5a323cd91 versioncmp.c: refactor config reading next commit
 5:  e38523267e7 =  5:  dced12a40d2 config API: have *_multi() return an "int" and take a "dest"
 6:  3a87b35e114 =  6:  d910f7e3a27 for-each-repo: error on bad --config
 7:  66b7060f66f =  7:  57db0fcd91f config API users: test for *_get_value_multi() segfaults
 8:  0da4cdb3f6a =  8:  b374a716555 config API: add "string" version of *_value_multi(), fix segfaults
 9:  627eb15a319 =  9:  6791e1f6f85 for-each-repo: with bad config, don't conflate <path> and <cmd>
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v7 1/9] config tests: cover blind spots in git_die_config() tests
  2023-03-08  9:06           ` [PATCH v7 " Ævar Arnfjörð Bjarmason
@ 2023-03-08  9:06             ` Ævar Arnfjörð Bjarmason
  2023-03-08  9:06             ` [PATCH v7 2/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
                               ` (9 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-08  9:06 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

There were no tests checking for the output of the git_die_config()
function in the config API, added in 5a80e97c827 (config: add
`git_die_config()` to the config-set API, 2014-08-07). We only tested
"test_must_fail", but didn't assert the output.

We need tests for this because a subsequent commit will alter the
return value of git_config_get_value_multi(), which is used to get the
config values in the git_die_config() function. This test coverage
helps to build confidence in that subsequent change.

These tests cover different interactions with git_die_config():

- The "notes.mergeStrategy" test in
  "t/t3309-notes-merge-auto-resolve.sh" is a case where a function
  outside of config.c (git_config_get_notes_strategy()) calls
  git_die_config().

- The "gc.pruneExpire" test in "t5304-prune.sh" is a case where
  git_config_get_expiry() calls git_die_config(), covering a different
  "type" than the "string" test for "notes.mergeStrategy".

- The "fetch.negotiationAlgorithm" test in
  "t/t5552-skipping-fetch-negotiator.sh" is a case where
  git_config_get_string*() calls git_die_config().

We also cover both the "from command-line config" and "in file..at
line" cases here.

The clobbering of existing ".git/config" files here is so that we're
not implicitly testing the line count of the default config.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t3309-notes-merge-auto-resolve.sh  |  7 ++++++-
 t/t5304-prune.sh                     | 12 ++++++++++--
 t/t5552-skipping-fetch-negotiator.sh | 16 ++++++++++++++++
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh
index 141d3e4ca4d..9bd5dbf341f 100755
--- a/t/t3309-notes-merge-auto-resolve.sh
+++ b/t/t3309-notes-merge-auto-resolve.sh
@@ -360,7 +360,12 @@ test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '
 
 test_expect_success 'merge z into y with invalid configuration option => Fail/No changes' '
 	git config core.notesRef refs/notes/y &&
-	test_must_fail git -c notes.mergeStrategy="foo" notes merge z &&
+	cat >expect <<-\EOF &&
+	error: unknown notes merge strategy foo
+	fatal: unable to parse '\''notes.mergeStrategy'\'' from command-line config
+	EOF
+	test_must_fail git -c notes.mergeStrategy="foo" notes merge z 2>actual &&
+	test_cmp expect actual &&
 	# Verify no changes (y)
 	verify_notes y y
 '
diff --git a/t/t5304-prune.sh b/t/t5304-prune.sh
index d65a5f94b4b..5500dd08426 100755
--- a/t/t5304-prune.sh
+++ b/t/t5304-prune.sh
@@ -72,8 +72,16 @@ test_expect_success 'gc: implicit prune --expire' '
 '
 
 test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '
-	git config gc.pruneExpire invalid &&
-	test_must_fail git gc
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	>repo/.git/config &&
+	git -C repo config gc.pruneExpire invalid &&
+	cat >expect <<-\EOF &&
+	error: Invalid gc.pruneexpire: '\''invalid'\''
+	fatal: bad config variable '\''gc.pruneexpire'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_must_fail git -C repo gc 2>actual &&
+	test_cmp expect actual
 '
 
 test_expect_success 'gc: start with ok gc.pruneExpire' '
diff --git a/t/t5552-skipping-fetch-negotiator.sh b/t/t5552-skipping-fetch-negotiator.sh
index 165427d57e5..b55a9f65e6b 100755
--- a/t/t5552-skipping-fetch-negotiator.sh
+++ b/t/t5552-skipping-fetch-negotiator.sh
@@ -3,6 +3,22 @@
 test_description='test skipping fetch negotiator'
 . ./test-lib.sh
 
+test_expect_success 'fetch.negotiationalgorithm config' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	cat >repo/.git/config <<-\EOF &&
+	[fetch]
+	negotiationAlgorithm
+	EOF
+	cat >expect <<-\EOF &&
+	error: missing value for '\''fetch.negotiationalgorithm'\''
+	fatal: bad config variable '\''fetch.negotiationalgorithm'\'' in file '\''.git/config'\'' at line 2
+	EOF
+	test_expect_code 128 git -C repo fetch >out 2>actual &&
+	test_must_be_empty out &&
+	test_cmp expect actual
+'
+
 have_sent () {
 	while test "$#" -ne 0
 	do
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v7 2/9] config tests: add "NULL" tests for *_get_value_multi()
  2023-03-08  9:06           ` [PATCH v7 " Ævar Arnfjörð Bjarmason
  2023-03-08  9:06             ` [PATCH v7 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
@ 2023-03-08  9:06             ` Ævar Arnfjörð Bjarmason
  2023-03-08  9:06             ` [PATCH v7 3/9] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
                               ` (8 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-08  9:06 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

A less well known edge case in the config format is that keys can be
value-less, a shorthand syntax for "true" boolean keys. I.e. these two
are equivalent as far as "--type=bool" is concerned:

	[a]key
	[a]key = true

But as far as our parser is concerned the values for these two are
NULL, and "true". I.e. for a sequence like:

	[a]key=x
	[a]key
	[a]key=y

We get a "struct string_list" with "string" members with ".string"
values of:

	{ "x", NULL, "y" }

This behavior goes back to the initial implementation of
git_config_bool() in 17712991a59 (Add ".git/config" file parser,
2005-10-10).

When parts of the config_set API were tested for in [1] they didn't
add coverage for 3/4 of the "(NULL)" cases handled in
"t/helper/test-config.c". We'd test that case for "get_value", but not
"get_value_multi", "configset_get_value" and
"configset_get_value_multi".

We now cover all of those cases, which in turn expose the details of
how this part of the config API works.

1. 4c715ebb96a (test-config: add tests for the config_set API,
   2014-07-28)

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t1308-config-set.sh | 65 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 65 insertions(+)

diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index b38e158d3b2..4be1ab1147c 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -146,6 +146,71 @@ test_expect_success 'find multiple values' '
 	check_config get_value_multi case.baz sam bat hask
 '
 
+test_NULL_in_multi () {
+	local op="$1" &&
+	local file="$2" &&
+
+	test_expect_success "$op: NULL value in config${file:+ in $file}" '
+		config="$file" &&
+		if test -z "$config"
+		then
+			config=.git/config &&
+			test_when_finished "mv $config.old $config" &&
+			mv "$config" "$config".old
+		fi &&
+
+		# Value-less in the middle of a list
+		cat >"$config" <<-\EOF &&
+		[a]key=x
+		[a]key
+		[a]key=y
+		EOF
+		case "$op" in
+		*_multi)
+			cat >expect <<-\EOF
+			x
+			(NULL)
+			y
+			EOF
+			;;
+		*)
+			cat >expect <<-\EOF
+			y
+			EOF
+			;;
+		esac &&
+		test-tool config "$op" a.key $file >actual &&
+		test_cmp expect actual &&
+
+		# Value-less at the end of a least
+		cat >"$config" <<-\EOF &&
+		[a]key=x
+		[a]key=y
+		[a]key
+		EOF
+		case "$op" in
+		*_multi)
+			cat >expect <<-\EOF
+			x
+			y
+			(NULL)
+			EOF
+			;;
+		*)
+			cat >expect <<-\EOF
+			(NULL)
+			EOF
+			;;
+		esac &&
+		test-tool config "$op" a.key $file >actual &&
+		test_cmp expect actual
+	'
+}
+
+test_NULL_in_multi "get_value_multi"
+test_NULL_in_multi "configset_get_value" "my.config"
+test_NULL_in_multi "configset_get_value_multi" "my.config"
+
 test_expect_success 'find value from a configset' '
 	cat >config2 <<-\EOF &&
 	[case]
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v7 3/9] config API: add and use a "git_config_get()" family of functions
  2023-03-08  9:06           ` [PATCH v7 " Ævar Arnfjörð Bjarmason
  2023-03-08  9:06             ` [PATCH v7 1/9] config tests: cover blind spots in git_die_config() tests Ævar Arnfjörð Bjarmason
  2023-03-08  9:06             ` [PATCH v7 2/9] config tests: add "NULL" tests for *_get_value_multi() Ævar Arnfjörð Bjarmason
@ 2023-03-08  9:06             ` Ævar Arnfjörð Bjarmason
  2023-03-09 18:53               ` Glen Choo
  2023-03-08  9:06             ` [PATCH v7 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
                               ` (7 subsequent siblings)
  10 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-08  9:06 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

We already have the basic "git_config_get_value()" function and its
"repo_*" and "configset" siblings to get a given "key" and assign the
last key found to a provided "value".

But some callers don't care about that value, but just want to use the
return value of the "get_value()" function to check whether the key
exist (or another non-zero return value).

The immediate motivation for this is that a subsequent commit will
need to change all callers of the "*_get_value_multi()" family of
functions. In two cases here we (ab)used it to check whether we had
any values for the given key, but didn't care about the return value.

The rest of the callers here used various other config API functions
to do the same, all of which resolved to the same underlying functions
to provide the answer.

Some of these were using either git_config_get_string() or
git_config_get_string_tmp(), see fe4c750fb13 (submodule--helper: fix a
configure_added_submodule() leak, 2022-09-01) for a recent example. We
can now use a helper function that doesn't require a throwaway
variable.

We could have changed git_configset_get_value_multi() (and then
git_config_get_value() etc.) to accept a "NULL" as a "dest" for all
callers, but let's avoid changing the behavior of existing API
users. Having an "unused" value that we throw away internal to
config.c is cheap.

A "NULL as optional dest" pattern is also more fragile, as the intent
of the caller might be misinterpreted if he were to accidentally pass
"NULL", e.g. when "dest" is passed in from another function.

Another name for this function could have been
"*_config_key_exists()", as suggested in [1]. That would work for all
of these callers, and would currently be equivalent to this function,
as the git_configset_get_value() API normalizes all non-zero return
values to a "1".

But adding that API would set us up to lose information, as e.g. if
git_config_parse_key() in the underlying configset_find_element()
fails we'd like to return -1, not 1.

Let's change the underlying configset_find_element() function to
support this use-case, we'll make further use of it in a subsequent
commit where the git_configset_get_value_multi() function itself will
expose this new return value.

This still leaves various inconsistencies and clobbering or ignoring
of the return value in place. E.g here we're modifying
configset_add_value(), but ever since it was added in [2] we've been
ignoring its "int" return value, but as we're changing the
configset_find_element() it uses, let's have it faithfully ferry that
"ret" along.

Let's also use the "RESULT_MUST_BE_USED" macro introduced in [3] to
assert that we're checking the return value of
configset_find_element().

We're leaving the same change to configset_add_value() for some future
series. Once we start paying attention to its return value we'd need
to ferry it up as deep as do_config_from(), and would need to make
least read_{,very_}early_config() and git_protected_config() return an
"int" instead of "void". Let's leave that for now, and focus on
the *_get_*() functions.

1. 3c8687a73ee (add `config_set` API for caching config-like files, 2014-07-28)
2. https://lore.kernel.org/git/xmqqczadkq9f.fsf@gitster.g/
3. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
   return values, 2022-09-01),

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c                |  5 +---
 builtin/submodule--helper.c |  7 +++--
 builtin/worktree.c          |  3 +--
 config.c                    | 51 ++++++++++++++++++++++++++++++++-----
 config.h                    | 18 +++++++++++++
 t/helper/test-config.c      | 22 ++++++++++++++++
 t/t1308-config-set.sh       | 43 ++++++++++++++++++++++++++++++-
 7 files changed, 131 insertions(+), 18 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 02455fdcd73..e38d1783f30 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1493,7 +1493,6 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	};
 	int found = 0;
 	const char *key = "maintenance.repo";
-	char *config_value;
 	char *maintpath = get_maintpath();
 	struct string_list_item *item;
 	const struct string_list *list;
@@ -1508,9 +1507,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	git_config_set("maintenance.auto", "false");
 
 	/* Set maintenance strategy, if unset */
-	if (!git_config_get_string("maintenance.strategy", &config_value))
-		free(config_value);
-	else
+	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
 	list = git_config_get_value_multi(key);
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 4c173d8b37a..2278e8c91cb 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -557,7 +557,7 @@ static int module_init(int argc, const char **argv, const char *prefix)
 	 * If there are no path args and submodule.active is set then,
 	 * by default, only initialize 'active' modules.
 	 */
-	if (!argc && git_config_get_value_multi("submodule.active"))
+	if (!argc && !git_config_get("submodule.active"))
 		module_list_active(&list);
 
 	info.prefix = prefix;
@@ -2743,7 +2743,7 @@ static int module_update(int argc, const char **argv, const char *prefix)
 		 * If there are no path args and submodule.active is set then,
 		 * by default, only initialize 'active' modules.
 		 */
-		if (!argc && git_config_get_value_multi("submodule.active"))
+		if (!argc && !git_config_get("submodule.active"))
 			module_list_active(&list);
 
 		info.prefix = opt.prefix;
@@ -3140,7 +3140,6 @@ static int config_submodule_in_gitmodules(const char *name, const char *var, con
 static void configure_added_submodule(struct add_data *add_data)
 {
 	char *key;
-	const char *val;
 	struct child_process add_submod = CHILD_PROCESS_INIT;
 	struct child_process add_gitmodules = CHILD_PROCESS_INIT;
 
@@ -3185,7 +3184,7 @@ static void configure_added_submodule(struct add_data *add_data)
 	 * is_submodule_active(), since that function needs to find
 	 * out the value of "submodule.active" again anyway.
 	 */
-	if (!git_config_get_string_tmp("submodule.active", &val)) {
+	if (!git_config_get("submodule.active")) {
 		/*
 		 * If the submodule being added isn't already covered by the
 		 * current configured pathspec, set the submodule's active flag
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 254283aa6f5..2d81965711f 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -319,7 +319,6 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 
 	if (file_exists(from_file)) {
 		struct config_set cs = { { 0 } };
-		const char *core_worktree;
 		int bare;
 
 		if (safe_create_leading_directories(to_file) ||
@@ -338,7 +337,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
 				to_file, "core.bare", NULL, "true", 0))
 			error(_("failed to unset '%s' in '%s'"),
 				"core.bare", to_file);
-		if (!git_configset_get_value(&cs, "core.worktree", &core_worktree) &&
+		if (!git_configset_get(&cs, "core.worktree") &&
 			git_config_set_in_file_gently(to_file,
 							"core.worktree", NULL))
 			error(_("failed to unset '%s' in '%s'"),
diff --git a/config.c b/config.c
index 00090a32fc3..d4f0e4fd619 100644
--- a/config.c
+++ b/config.c
@@ -2289,23 +2289,29 @@ void read_very_early_config(config_fn_t cb, void *data)
 	config_with_options(cb, data, NULL, &opts);
 }
 
-static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
+RESULT_MUST_BE_USED
+static int configset_find_element(struct config_set *cs, const char *key,
+				  struct config_set_element **dest)
 {
 	struct config_set_element k;
 	struct config_set_element *found_entry;
 	char *normalized_key;
+	int ret;
+
 	/*
 	 * `key` may come from the user, so normalize it before using it
 	 * for querying entries from the hashmap.
 	 */
-	if (git_config_parse_key(key, &normalized_key, NULL))
-		return NULL;
+	ret = git_config_parse_key(key, &normalized_key, NULL);
+	if (ret)
+		return ret;
 
 	hashmap_entry_init(&k.ent, strhash(normalized_key));
 	k.key = normalized_key;
 	found_entry = hashmap_get_entry(&cs->config_hash, &k, ent, NULL);
 	free(normalized_key);
-	return found_entry;
+	*dest = found_entry;
+	return 0;
 }
 
 static int configset_add_value(struct config_set *cs, const char *key, const char *value)
@@ -2314,8 +2320,11 @@ static int configset_add_value(struct config_set *cs, const char *key, const cha
 	struct string_list_item *si;
 	struct configset_list_item *l_item;
 	struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
+	int ret;
 
-	e = configset_find_element(cs, key);
+	ret = configset_find_element(cs, key, &e);
+	if (ret)
+		return ret;
 	/*
 	 * Since the keys are being fed by git_config*() callback mechanism, they
 	 * are already normalized. So simply add them without any further munging.
@@ -2425,8 +2434,25 @@ int git_configset_get_value(struct config_set *cs, const char *key, const char *
 
 const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
 {
-	struct config_set_element *e = configset_find_element(cs, key);
-	return e ? &e->value_list : NULL;
+	struct config_set_element *e;
+
+	if (configset_find_element(cs, key, &e))
+		return NULL;
+	else if (!e)
+		return NULL;
+	return &e->value_list;
+}
+
+int git_configset_get(struct config_set *cs, const char *key)
+{
+	struct config_set_element *e;
+	int ret;
+
+	if ((ret = configset_find_element(cs, key, &e)))
+		return ret;
+	else if (!e)
+		return 1;
+	return 0;
 }
 
 int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
@@ -2565,6 +2591,12 @@ void repo_config(struct repository *repo, config_fn_t fn, void *data)
 	configset_iter(repo->config, fn, data);
 }
 
+int repo_config_get(struct repository *repo, const char *key)
+{
+	git_config_check_init(repo);
+	return git_configset_get(repo->config, key);
+}
+
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value)
 {
@@ -2679,6 +2711,11 @@ void git_config_clear(void)
 	repo_config_clear(the_repository);
 }
 
+int git_config_get(const char *key)
+{
+	return repo_config_get(the_repository, key);
+}
+
 int git_config_get_value(const char *key, const char **value)
 {
 	return repo_config_get_value(the_repository, key, value);
diff --git a/config.h b/config.h
index 7606246531a..7dd62ca81bf 100644
--- a/config.h
+++ b/config.h
@@ -465,6 +465,9 @@ void git_configset_clear(struct config_set *cs);
  * value in the 'dest' pointer.
  */
 
+RESULT_MUST_BE_USED
+int git_configset_get(struct config_set *cs, const char *key);
+
 /*
  * Finds the highest-priority value for the configuration variable `key`
  * and config set `cs`, stores the pointer to it in `value` and returns 0.
@@ -485,6 +488,14 @@ int git_configset_get_pathname(struct config_set *cs, const char *key, const cha
 /* Functions for reading a repository's config */
 struct repository;
 void repo_config(struct repository *repo, config_fn_t fn, void *data);
+
+/**
+ * Run only the discover part of the repo_config_get_*() functions
+ * below, in addition to 1 if not found, returns negative values on
+ * error (e.g. if the key itself is invalid).
+ */
+RESULT_MUST_BE_USED
+int repo_config_get(struct repository *repo, const char *key);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
 const struct string_list *repo_config_get_value_multi(struct repository *repo,
@@ -521,8 +532,15 @@ void git_protected_config(config_fn_t fn, void *data);
  * manner, the config API provides two functions `git_config_get_value`
  * and `git_config_get_value_multi`. They both read values from an internal
  * cache generated previously from reading the config files.
+ *
+ * For those git_config_get*() functions that aren't documented,
+ * consult the corresponding repo_config_get*() function's
+ * documentation.
  */
 
+RESULT_MUST_BE_USED
+int git_config_get(const char *key);
+
 /**
  * Finds the highest-priority value for the configuration variable `key`,
  * stores the pointer to it in `value` and returns 0. When the
diff --git a/t/helper/test-config.c b/t/helper/test-config.c
index 4ba9eb65606..cbb33ae1fff 100644
--- a/t/helper/test-config.c
+++ b/t/helper/test-config.c
@@ -14,6 +14,8 @@
  * get_value_multi -> prints all values for the entered key in increasing order
  *		     of priority
  *
+ * get -> print return value for the entered key
+ *
  * get_int -> print integer value for the entered key or die
  *
  * get_bool -> print bool value for the entered key or die
@@ -109,6 +111,26 @@ int cmd__config(int argc, const char **argv)
 			printf("Value not found for \"%s\"\n", argv[2]);
 			goto exit1;
 		}
+	} else if (argc == 3 && !strcmp(argv[1], "get")) {
+		int ret;
+
+		if (!(ret = git_config_get(argv[2])))
+			goto exit0;
+		else if (ret == 1)
+			printf("Value not found for \"%s\"\n", argv[2]);
+		else if (ret == -CONFIG_INVALID_KEY)
+			printf("Key \"%s\" is invalid\n", argv[2]);
+		else if (ret == -CONFIG_NO_SECTION_OR_NAME)
+			printf("Key \"%s\" has no section\n", argv[2]);
+		else
+			/*
+			 * A normal caller should just check "ret <
+			 * 0", but for our own tests let's BUG() if
+			 * our whitelist of git_config_parse_key()
+			 * return values isn't exhaustive.
+			 */
+			BUG("Key \"%s\" has unknown return %d", argv[2], ret);
+		goto exit1;
 	} else if (argc == 3 && !strcmp(argv[1], "get_int")) {
 		if (!git_config_get_int(argv[2], &val)) {
 			printf("%d\n", val);
diff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh
index 4be1ab1147c..7def7053e1c 100755
--- a/t/t1308-config-set.sh
+++ b/t/t1308-config-set.sh
@@ -58,6 +58,8 @@ test_expect_success 'setup default config' '
 		skin = false
 		nose = 1
 		horns
+	[value]
+		less
 	EOF
 '
 
@@ -116,6 +118,45 @@ test_expect_success 'find value with the highest priority' '
 	check_config get_value case.baz "hask"
 '
 
+test_expect_success 'return value for an existing key' '
+	test-tool config get lamb.chop >out 2>err &&
+	test_must_be_empty out &&
+	test_must_be_empty err
+'
+
+test_expect_success 'return value for value-less key' '
+	test-tool config get value.less >out 2>err &&
+	test_must_be_empty out &&
+	test_must_be_empty err
+'
+
+test_expect_success 'return value for a missing key' '
+	cat >expect <<-\EOF &&
+	Value not found for "missing.key"
+	EOF
+	test_expect_code 1 test-tool config get missing.key >actual 2>err &&
+	test_cmp actual expect &&
+	test_must_be_empty err
+'
+
+test_expect_success 'return value for a bad key: CONFIG_INVALID_KEY' '
+	cat >expect <<-\EOF &&
+	Key "fails.iskeychar.-" is invalid
+	EOF
+	test_expect_code 1 test-tool config get fails.iskeychar.- >actual 2>err &&
+	test_cmp actual expect &&
+	test_must_be_empty out
+'
+
+test_expect_success 'return value for a bad key: CONFIG_NO_SECTION_OR_NAME' '
+	cat >expect <<-\EOF &&
+	Key "keynosection" has no section
+	EOF
+	test_expect_code 1 test-tool config get keynosection >actual 2>err &&
+	test_cmp actual expect &&
+	test_must_be_empty out
+'
+
 test_expect_success 'find integer value for a key' '
 	check_config get_int lamb.chop 65
 '
@@ -272,7 +313,7 @@ test_expect_success 'proper error on error in default config files' '
 	cp .git/config .git/config.old &&
 	test_when_finished "mv .git/config.old .git/config" &&
 	echo "[" >>.git/config &&
-	echo "fatal: bad config line 34 in file .git/config" >expect &&
+	echo "fatal: bad config line 36 in file .git/config" >expect &&
 	test_expect_code 128 test-tool config get_value foo.bar 2>actual &&
 	test_cmp expect actual
 '
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v7 4/9] versioncmp.c: refactor config reading next commit
  2023-03-08  9:06           ` [PATCH v7 " Ævar Arnfjörð Bjarmason
                               ` (2 preceding siblings ...)
  2023-03-08  9:06             ` [PATCH v7 3/9] config API: add and use a "git_config_get()" family of functions Ævar Arnfjörð Bjarmason
@ 2023-03-08  9:06             ` Ævar Arnfjörð Bjarmason
  2023-03-08  9:06             ` [PATCH v7 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
                               ` (6 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-08  9:06 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Refactor the reading of the versionSort.suffix and
versionSort.prereleaseSuffix configuration variables to stay within
the bounds of our CodingGuidelines when it comes to line length, and
to avoid repeating ourselves.

Renaming "deprecated_prereleases" to "oldl" doesn't help us to avoid
line wrapping now, but it will in a subsequent commit.

Let's also split out the names of the config variables into variables
of our own, and refactor the nested if/else to avoid indenting it, and
the existing bracing style issue.

This all helps with the subsequent commit, where we'll need to start
checking different git_config_get_value_multi() return value. See
c026557a373 (versioncmp: generalize version sort suffix reordering,
2016-12-08) for the original implementation of most of this.

Moving the "initialized = 1" assignment allows us to move some of this
to the variable declarations in the subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 versioncmp.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/versioncmp.c b/versioncmp.c
index 069ee94a4d7..323f5d35ea8 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -160,15 +160,18 @@ int versioncmp(const char *s1, const char *s2)
 	}
 
 	if (!initialized) {
-		const struct string_list *deprecated_prereleases;
+		const char *const newk = "versionsort.suffix";
+		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *oldl;
+
+		prereleases = git_config_get_value_multi(newk);
+		oldl = git_config_get_value_multi(oldk);
+		if (prereleases && oldl)
+			warning("ignoring %s because %s is set", oldk, newk);
+		else if (!prereleases)
+			prereleases = oldl;
+
 		initialized = 1;
-		prereleases = git_config_get_value_multi("versionsort.suffix");
-		deprecated_prereleases = git_config_get_value_multi("versionsort.prereleasesuffix");
-		if (prereleases) {
-			if (deprecated_prereleases)
-				warning("ignoring versionsort.prereleasesuffix because versionsort.suffix is set");
-		} else
-			prereleases = deprecated_prereleases;
 	}
 	if (prereleases && swap_prereleases(s1, s2, (const char *) p1 - s1 - 1,
 					    &diff))
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v7 5/9] config API: have *_multi() return an "int" and take a "dest"
  2023-03-08  9:06           ` [PATCH v7 " Ævar Arnfjörð Bjarmason
                               ` (3 preceding siblings ...)
  2023-03-08  9:06             ` [PATCH v7 4/9] versioncmp.c: refactor config reading next commit Ævar Arnfjörð Bjarmason
@ 2023-03-08  9:06             ` Ævar Arnfjörð Bjarmason
  2023-03-09 19:01               ` Glen Choo
  2023-03-08  9:06             ` [PATCH v7 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
                               ` (5 subsequent siblings)
  10 siblings, 1 reply; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-08  9:06 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

Have the "git_configset_get_value_multi()" function and its siblings
return an "int" and populate a "**dest" parameter like every other
git_configset_get_*()" in the API.

As we'll take advantage of in subsequent commits, this fixes a blind
spot in the API where it wasn't possible to tell whether a list was
empty from whether a config key existed. For now we don't make use of
those new return values, but faithfully convert existing API users.

Most of this is straightforward, commentary on cases that stand out:

- To ensure that we'll properly use the return values of this function
  in the future we're using the "RESULT_MUST_BE_USED" macro introduced
  in [1].

  As git_die_config() now has to handle this return value let's have
  it BUG() if it can't find the config entry. As tested for in a
  preceding commit we can rely on getting the config list in
  git_die_config().

- The loops after getting the "list" value in "builtin/gc.c" could
  also make use of "unsorted_string_list_has_string()" instead of using
  that loop, but let's leave that for now.

- In "versioncmp.c" we now use the return value of the functions,
  instead of checking if the lists are still non-NULL.

1. 1e8697b5c4e (submodule--helper: check repo{_submodule,}_init()
   return values, 2022-09-01),

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c |  5 +----
 builtin/gc.c            | 10 ++++------
 builtin/log.c           |  6 +++---
 config.c                | 34 ++++++++++++++++++++--------------
 config.h                | 29 +++++++++++++++++++++--------
 pack-bitmap.c           |  6 +++++-
 submodule.c             |  3 +--
 t/helper/test-config.c  |  6 ++----
 versioncmp.c            | 11 +++++++----
 9 files changed, 64 insertions(+), 46 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 6aeac371488..fd0e7739e6a 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -45,14 +45,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	values = repo_config_get_value_multi(the_repository,
-					     config_key);
-
 	/*
 	 * Do nothing on an empty list, which is equivalent to the case
 	 * where the config variable does not exist at all.
 	 */
-	if (!values)
+	if (repo_config_get_value_multi(the_repository, config_key, &values))
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/builtin/gc.c b/builtin/gc.c
index e38d1783f30..2b3da377d52 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1510,8 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
 	if (git_config_get("maintenance.strategy"))
 		git_config_set("maintenance.strategy", "incremental");
 
-	list = git_config_get_value_multi(key);
-	if (list) {
+	if (!git_config_get_value_multi(key, &list)) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
@@ -1577,11 +1576,10 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
 	if (config_file) {
 		git_configset_init(&cs);
 		git_configset_add_file(&cs, config_file);
-		list = git_configset_get_value_multi(&cs, key);
-	} else {
-		list = git_config_get_value_multi(key);
 	}
-	if (list) {
+	if (!(config_file
+	      ? git_configset_get_value_multi(&cs, key, &list)
+	      : git_config_get_value_multi(key, &list))) {
 		for_each_string_list_item(item, list) {
 			if (!strcmp(maintpath, item->string)) {
 				found = 1;
diff --git a/builtin/log.c b/builtin/log.c
index a70fba198f9..e43f6f9d8c1 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -182,10 +182,10 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
 	int i;
 	char *value = NULL;
 	struct string_list *include = decoration_filter->include_ref_pattern;
-	const struct string_list *config_exclude =
-			git_config_get_value_multi("log.excludeDecoration");
+	const struct string_list *config_exclude;
 
-	if (config_exclude) {
+	if (!git_config_get_value_multi("log.excludeDecoration",
+					&config_exclude)) {
 		struct string_list_item *item;
 		for_each_string_list_item(item, config_exclude)
 			string_list_append(decoration_filter->exclude_ref_config_pattern,
diff --git a/config.c b/config.c
index d4f0e4fd619..569819b4a1b 100644
--- a/config.c
+++ b/config.c
@@ -2418,29 +2418,34 @@ int git_configset_add_file(struct config_set *cs, const char *filename)
 int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
 {
 	const struct string_list *values = NULL;
+	int ret;
+
 	/*
 	 * Follows "last one wins" semantic, i.e., if there are multiple matches for the
 	 * queried key in the files of the configset, the value returned will be the last
 	 * value in the value list for that key.
 	 */
-	values = git_configset_get_value_multi(cs, key);
+	if ((ret = git_configset_get_value_multi(cs, key, &values)))
+		return ret;
 
-	if (!values)
-		return 1;
 	assert(values->nr > 0);
 	*value = values->items[values->nr - 1].string;
 	return 0;
 }
 
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest)
 {
 	struct config_set_element *e;
+	int ret;
 
-	if (configset_find_element(cs, key, &e))
-		return NULL;
+	if ((ret = configset_find_element(cs, key, &e)))
+		return ret;
 	else if (!e)
-		return NULL;
-	return &e->value_list;
+		return 1;
+	*dest = &e->value_list;
+
+	return 0;
 }
 
 int git_configset_get(struct config_set *cs, const char *key)
@@ -2604,11 +2609,11 @@ int repo_config_get_value(struct repository *repo,
 	return git_configset_get_value(repo->config, key, value);
 }
 
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key)
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest)
 {
 	git_config_check_init(repo);
-	return git_configset_get_value_multi(repo->config, key);
+	return git_configset_get_value_multi(repo->config, key, dest);
 }
 
 int repo_config_get_string(struct repository *repo,
@@ -2721,9 +2726,9 @@ int git_config_get_value(const char *key, const char **value)
 	return repo_config_get_value(the_repository, key, value);
 }
 
-const struct string_list *git_config_get_value_multi(const char *key)
+int git_config_get_value_multi(const char *key, const struct string_list **dest)
 {
-	return repo_config_get_value_multi(the_repository, key);
+	return repo_config_get_value_multi(the_repository, key, dest);
 }
 
 int git_config_get_string(const char *key, char **dest)
@@ -2870,7 +2875,8 @@ void git_die_config(const char *key, const char *err, ...)
 		error_fn(err, params);
 		va_end(params);
 	}
-	values = git_config_get_value_multi(key);
+	if (git_config_get_value_multi(key, &values))
+		BUG("for key '%s' we must have a value to report on", key);
 	kv_info = values->items[values->nr - 1].util;
 	git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
 }
diff --git a/config.h b/config.h
index 7dd62ca81bf..4db6b90ac20 100644
--- a/config.h
+++ b/config.h
@@ -450,10 +450,18 @@ int git_configset_add_file(struct config_set *cs, const char *filename);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key` and config set `cs`. When the
- * configuration variable `key` is not found, returns NULL. The caller
- * should not free or modify the returned pointer, as it is owned by the cache.
+ * configuration variable `key` is not found, returns 1 without touching
+ * `value`.
+ *
+ * The key will be parsed for validity with git_config_parse_key(), on
+ * error a negative value will be returned.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
+RESULT_MUST_BE_USED
+int git_configset_get_value_multi(struct config_set *cs, const char *key,
+				  const struct string_list **dest);
 
 /**
  * Clears `config_set` structure, removes all saved variable-value pairs.
@@ -498,8 +506,9 @@ RESULT_MUST_BE_USED
 int repo_config_get(struct repository *repo, const char *key);
 int repo_config_get_value(struct repository *repo,
 			  const char *key, const char **value);
-const struct string_list *repo_config_get_value_multi(struct repository *repo,
-						      const char *key);
+RESULT_MUST_BE_USED
+int repo_config_get_value_multi(struct repository *repo, const char *key,
+				const struct string_list **dest);
 int repo_config_get_string(struct repository *repo,
 			   const char *key, char **dest);
 int repo_config_get_string_tmp(struct repository *repo,
@@ -553,10 +562,14 @@ int git_config_get_value(const char *key, const char **value);
 /**
  * Finds and returns the value list, sorted in order of increasing priority
  * for the configuration variable `key`. When the configuration variable
- * `key` is not found, returns NULL. The caller should not free or modify
- * the returned pointer, as it is owned by the cache.
+ * `key` is not found, returns 1 without touching `value`.
+ *
+ * The caller should not free or modify the returned pointer, as it is
+ * owned by the cache.
  */
-const struct string_list *git_config_get_value_multi(const char *key);
+RESULT_MUST_BE_USED
+int git_config_get_value_multi(const char *key,
+			       const struct string_list **dest);
 
 /**
  * Resets and invalidates the config cache.
diff --git a/pack-bitmap.c b/pack-bitmap.c
index d2a42abf28c..15c5eb507c0 100644
--- a/pack-bitmap.c
+++ b/pack-bitmap.c
@@ -2314,7 +2314,11 @@ int bitmap_is_midx(struct bitmap_index *bitmap_git)
 
 const struct string_list *bitmap_preferred_tips(struct repository *r)
 {
-	return repo_config_get_value_multi(r, "pack.preferbitmaptips");
+	const struct string_list *dest;
+
+	if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
+		return dest;
+	return NULL;
 }
 
 int bitmap_is_preferred_refname(struct repository *r, const char *refname)
diff --git a/submodule.c b/submodule.c
index 3a0dfc417c0..4b6f5223b0c 100644
--- a/submodule.c
+++ b/submodule.c
@@ -274,8 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
 	free(key);
 
 	/* submodule.active is set */
-	sl = repo_config_get_value_multi(repo, "submodule.active");
-	if (sl) {
+	if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
 		struct pathspec ps;
 		struct strvec args = STRVEC_INIT;
 		const struct string_list_item *item;
diff --git a/t/helper/test-config.c b/t/helper/test-config.c
index cbb33ae1fff..6dc4c37444f 100644
--- a/t/helper/test-config.c
+++ b/t/helper/test-config.c
@@ -97,8 +97,7 @@ int cmd__config(int argc, const char **argv)
 			goto exit1;
 		}
 	} else if (argc == 3 && !strcmp(argv[1], "get_value_multi")) {
-		strptr = git_config_get_value_multi(argv[2]);
-		if (strptr) {
+		if (!git_config_get_value_multi(argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
@@ -181,8 +180,7 @@ int cmd__config(int argc, const char **argv)
 				goto exit2;
 			}
 		}
-		strptr = git_configset_get_value_multi(&cs, argv[2]);
-		if (strptr) {
+		if (!git_configset_get_value_multi(&cs, argv[2], &strptr)) {
 			for (i = 0; i < strptr->nr; i++) {
 				v = strptr->items[i].string;
 				if (!v)
diff --git a/versioncmp.c b/versioncmp.c
index 323f5d35ea8..60c3a517122 100644
--- a/versioncmp.c
+++ b/versioncmp.c
@@ -162,13 +162,16 @@ int versioncmp(const char *s1, const char *s2)
 	if (!initialized) {
 		const char *const newk = "versionsort.suffix";
 		const char *const oldk = "versionsort.prereleasesuffix";
+		const struct string_list *newl;
 		const struct string_list *oldl;
+		int new = git_config_get_value_multi(newk, &newl);
+		int old = git_config_get_value_multi(oldk, &oldl);
 
-		prereleases = git_config_get_value_multi(newk);
-		oldl = git_config_get_value_multi(oldk);
-		if (prereleases && oldl)
+		if (!new && !old)
 			warning("ignoring %s because %s is set", oldk, newk);
-		else if (!prereleases)
+		if (!new)
+			prereleases = newl;
+		else if (!old)
 			prereleases = oldl;
 
 		initialized = 1;
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v7 6/9] for-each-repo: error on bad --config
  2023-03-08  9:06           ` [PATCH v7 " Ævar Arnfjörð Bjarmason
                               ` (4 preceding siblings ...)
  2023-03-08  9:06             ` [PATCH v7 5/9] config API: have *_multi() return an "int" and take a "dest" Ævar Arnfjörð Bjarmason
@ 2023-03-08  9:06             ` Ævar Arnfjörð Bjarmason
  2023-03-08  9:06             ` [PATCH v7 7/9] config API users: test for *_get_value_multi() segfaults Ævar Arnfjörð Bjarmason
                               ` (4 subsequent siblings)
  10 siblings, 0 replies; 134+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2023-03-08  9:06 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Derrick Stolee, Elijah Newren, Jeff King,
	Taylor Blau, SZEDER Gábor, Glen Choo, Calvin Wan,
	Emily Shaffer, raymond, zweiss,
	Ævar Arnfjörð Bjarmason

As noted in 6c62f015520 (for-each-repo: do nothing on empty config,
2021-01-08) this command wants to ignore a non-existing config key,
but let's not conflate that with bad config.

Before this, all these added tests would pass with an exit code of 0.

We could preserve the comment added in 6c62f015520, but now that we're
directly using the documented repo_config_get_value_multi() value it's
just narrating something that should be obvious from the API use, so
let's drop it.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/for-each-repo.c  | 11 ++++++-----
 t/t0068-for-each-repo.sh |  6 ++++++
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index fd0e7739e6a..224164addb3 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -32,6 +32,7 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	static const char *config_key = NULL;
 	int i, result = 0;
 	const struct string_list *values;
+	int err;
 
 	const struct option options[] = {
 		OPT_STRING(0, "config", &config_key, N_("config"),
@@ -45,11 +46,11 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	if (!config_key)
 		die(_("missing --config=<config>"));
 
-	/*
-	 * Do nothing on an empty list, which is equivalent to the case
-	 * where the config variable does not exist at all.
-	 */
-	if (repo_config_get_value_multi(the_repository, config_key, &values))
+	err = repo_config_get_value_multi(the_repository, config_key, &values);
+	if (err < 0)
+		usage_msg_optf(_("got bad config --config=%s"),
+			       for_each_repo_usage, options, config_key);
+	else if (err)
 		return 0;
 
 	for (i = 0; !result && i < values->nr; i++)
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 3648d439a87..6b51e00da0e 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -40,4 +40,10 @@ test_expect_success 'do nothing on empty config' '
 	git for-each-repo --config=bogus.config -- help --no-such-option
 '
 
+test_expect_success 'error on bad config keys' '
+	test_expect_code 129 git for-each-repo --config=a &&
+	test_expect_code 129 git for-each-repo --config=a.b. &&
+	test_expect_code 129 git for-each-repo --config="'\''.b"
+'
+
 test_done
-- 
2.40.0.rc1.1034.g5867a1b10c5


^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v7 7/9] config API users: test for *_get_value_multi() segfaults
  2023-03-08  9:06           ` [PATCH v7 " Ævar Arnfjörð Bjarmason
                               ` (5 preceding siblings ...)
  2023-03-08  9:06             ` [PATCH v7 6/9] for-each-repo: error on bad --config Ævar Arnfjörð Bjarmason
@ 2023-03-08  9:06             ` Ævar Arnfjörð Bjarmason
  2023-03-08  9:06             `