All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/8] builtin/config: introduce subcommands
@ 2024-03-06 11:31 Patrick Steinhardt
  2024-03-06 11:31 ` [PATCH 1/8] builtin/config: move option array around Patrick Steinhardt
                   ` (13 more replies)
  0 siblings, 14 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-06 11:31 UTC (permalink / raw)
  To: git

[-- Attachment #1: Type: text/plain, Size: 3106 bytes --]

Hi,

the UI of git-config(1) is quite arcane and does not really conform to
the more modern UIs that we have nowadays in Git:

  - While it does have modes, those modes come in the form of switches.
    E.g. you have to say git config --get-all to execute the "get-all"
    mode.

  - Its interface depends on the number of args. Given one arg it will
    print the value of the corresponding config key, given two args it
    will set that key. Did you know you can even give it three args? I
    didn't. Now guess what this mode does.

This patch series overhauls git-config(1) by introducing subcommands.
This results in the following UI that matches more closely what we have
in other Git commands which are more modern:

  - `git config foo.bar` -> `git config get foo.bar`

  - `git config foo.bar value` -> `git config set foo.bar value`

  - `git config foo.bar value value-pattern` -> `git config set-all
    foo.bar value value-pattern`

  - `git config --get-urlmatch` -> `git config get-urlmatch`.

Most importantly, this should help discoverability quite a lot by now
also having a proper synopsis in both the manpage, but also in `git
config -h`.

Of course, backwards compatibility is a big concern. We don't want to
just switch over to the new syntax and break all existing scripts and
muscle memory. This patch series thus abuses the fact that the implicit
modes (`git config foo.bar`, `git config foo.bar value` and `git config
foo.bar value value-pattern`) all require a key as first argument. As
keys _must_ have a dot, this allows us to unambiguously discern those
from actual subcommands.

Thus, git-config(1) now supports both old and new style arguments in a
completely backwards compatible way, which is also demonstrated by the
adapted tests. Eventually, I think we should consider dropping the old
style syntax with e.g. Git v3.0.

We could of course iterate further from here and keep on improving the
UI of the new subcommands, e.g. by merging closely related subcommands.
But for the time being I think it's easier to stop at this point and
revisit the result at a later point in time.

Also, note that I see this as kind of an experiment for how to modernize
our UI slowly over time so that things become more consistent and thus
hopefully easier to use in the long term.

Patrick

Patrick Steinhardt (8):
  builtin/config: move option array around
  builtin/config: move "fixed-value" option to correct group
  builtin/config: use `OPT_CMDMODE()` to specify modes
  builtin/config: move modes into separate functions
  builtin/config: track subcommands by action
  builtin/config: introduce subcommands
  t1300: exercise both old- and new-style modes
  Documentation/git-config: update to new-style syntax

 Documentation/git-config.txt | 204 +++++-----
 builtin/config.c             | 671 +++++++++++++++++++-------------
 t/t0450/txt-help-mismatches  |   1 -
 t/t1300-config.sh            | 734 ++++++++++++++++++-----------------
 4 files changed, 900 insertions(+), 710 deletions(-)

-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH 1/8] builtin/config: move option array around
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
@ 2024-03-06 11:31 ` Patrick Steinhardt
  2024-03-06 11:31 ` [PATCH 2/8] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
                   ` (12 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-06 11:31 UTC (permalink / raw)
  To: git

[-- Attachment #1: Type: text/plain, Size: 8066 bytes --]

Move around the option array. This will help us with a follow-up commit
that introduces subcommands to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 94 ++++++++++++++++++++++++------------------------
 1 file changed, 47 insertions(+), 47 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index b55bfae7d6..6eb6aff917 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -134,53 +134,6 @@ static int option_parse_type(const struct option *opt, const char *arg,
 	return 0;
 }
 
-static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
-	OPT_GROUP(N_("Action")),
-	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
-	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
-	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
-	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_END(),
-};
-
-static NORETURN void usage_builtin_config(void)
-{
-	usage_with_options(builtin_config_usage, builtin_config_options);
-}
-
 static void check_argc(int argc, int min, int max)
 {
 	if (argc >= min && argc <= max)
@@ -669,6 +622,53 @@ static char *default_user_config(void)
 	return strbuf_detach(&buf, NULL);
 }
 
+static struct option builtin_config_options[] = {
+	OPT_GROUP(N_("Config file location")),
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	OPT_GROUP(N_("Action")),
+	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
+	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
+	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
+	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
+	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
+	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
+	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
+	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_GROUP(N_("Type")),
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	OPT_GROUP(N_("Other")),
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
+	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_END(),
+};
+
+static NORETURN void usage_builtin_config(void)
+{
+	usage_with_options(builtin_config_usage, builtin_config_options);
+}
+
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	int nongit = !startup_info->have_repository;
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH 2/8] builtin/config: move "fixed-value" option to correct group
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
  2024-03-06 11:31 ` [PATCH 1/8] builtin/config: move option array around Patrick Steinhardt
@ 2024-03-06 11:31 ` Patrick Steinhardt
  2024-03-06 11:31 ` [PATCH 3/8] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
                   ` (11 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-06 11:31 UTC (permalink / raw)
  To: git

[-- Attachment #1: Type: text/plain, Size: 1922 bytes --]

The `--fixed-value` option can be used to alter how the value-pattern
parameter is interpreted for the various submodes of git-config(1). But
while it is an option, it is currently listed as part of the submodes
group the command, which is wrong.

Move the option to the "Other" group, which hosts the various options
known to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/config.c b/builtin/config.c
index 6eb6aff917..fcd6190f12 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -642,7 +642,6 @@ static struct option builtin_config_options[] = {
 	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
 	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
 	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
 	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
 	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
 	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
@@ -661,6 +660,7 @@ static struct option builtin_config_options[] = {
 	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
 	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
 	OPT_END(),
 };
 
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH 3/8] builtin/config: use `OPT_CMDMODE()` to specify modes
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
  2024-03-06 11:31 ` [PATCH 1/8] builtin/config: move option array around Patrick Steinhardt
  2024-03-06 11:31 ` [PATCH 2/8] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
@ 2024-03-06 11:31 ` Patrick Steinhardt
  2024-03-06 23:52   ` Taylor Blau
  2024-03-06 11:31 ` [PATCH 4/8] builtin/config: move modes into separate functions Patrick Steinhardt
                   ` (10 subsequent siblings)
  13 siblings, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-06 11:31 UTC (permalink / raw)
  To: git

[-- Attachment #1: Type: text/plain, Size: 5386 bytes --]

The git-config(1) command has various different modes which are
accessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are
declared with `OPT_BIT()`, which causes two minor issues:

  - The respective modes also have a negated form `--no-get-urlmatch`,
    which is unintended.

  - We have to manually handle exclusiveness of the modes.

Switch these options to instead use `OPT_CMDMODE()`, which is made
exactly for this usecase. Remove the now-unneeded check that only a
single mode is given, which is now handled by the parse-options
interface.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c  | 32 ++++++++++++++------------------
 t/t1300-config.sh | 13 +++++++++++++
 2 files changed, 27 insertions(+), 18 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index fcd6190f12..8a2d1a5de7 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -631,20 +631,20 @@ static struct option builtin_config_options[] = {
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
 	OPT_GROUP(N_("Action")),
-	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_CMDMODE(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
+	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
+	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
+	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
+	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
+	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
+	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
+	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
 	OPT_GROUP(N_("Type")),
 	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
 	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
@@ -767,10 +767,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (HAS_MULTI_BITS(actions)) {
-		error(_("only one action at a time"));
-		usage_builtin_config();
-	}
 	if (actions == 0)
 		switch (argc) {
 		case 1: actions = ACTION_GET; break;
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 31c3878687..2d1bc1e27e 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -2612,4 +2612,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such
 	grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
 '
 
+test_expect_success 'negated mode causes failure' '
+	test_must_fail git config --no-get 2>err &&
+	grep "unknown option \`no-get${SQ}" err
+'
+
+test_expect_success 'specifying multiple modes causes failure' '
+	cat >expect <<-EOF &&
+	error: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together
+	EOF
+	test_must_fail git config --get --get-all 2>err &&
+	test_cmp expect err
+'
+
 test_done
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH 4/8] builtin/config: move modes into separate functions
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (2 preceding siblings ...)
  2024-03-06 11:31 ` [PATCH 3/8] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
@ 2024-03-06 11:31 ` Patrick Steinhardt
  2024-03-06 11:31 ` [PATCH 5/8] builtin/config: track subcommands by action Patrick Steinhardt
                   ` (9 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-06 11:31 UTC (permalink / raw)
  To: git

[-- Attachment #1: Type: text/plain, Size: 15126 bytes --]

The git-config(1) command has several different modes which cause it to
do different things. The logic for each of these modes is hosted in a
giant switch in `cmd_config()` itself. For one, this is hard to read.
But second, we're about to introduce proper subcommands to git-config(1)
that will require separate functions for each of the modes.

Refactor the code and move each mode into its own function to prepare
for this.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 410 +++++++++++++++++++++++++++++------------------
 1 file changed, 255 insertions(+), 155 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 8a2d1a5de7..a6ab9b8204 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -44,6 +44,7 @@ static struct config_options config_options;
 static int show_origin;
 static int show_scope;
 static int fixed_value;
+static int config_flags;
 
 #define ACTION_GET (1<<0)
 #define ACTION_GET_ALL (1<<1)
@@ -622,6 +623,225 @@ static char *default_user_config(void)
 	return strbuf_detach(&buf, NULL);
 }
 
+static int cmd_config_list(int argc, const char **argv, const char *prefix)
+{
+	check_argc(argc, 0, 0);
+	if (config_with_options(show_all_config, NULL,
+				&given_config_source, the_repository,
+				&config_options) < 0) {
+		if (given_config_source.file)
+			die_errno(_("unable to read config file '%s'"),
+				  given_config_source.file);
+		else
+			die(_("error processing config file(s)"));
+	}
+
+	return 0;
+}
+
+static int cmd_config_edit(int argc, const char **argv, const char *prefix)
+{
+	char *config_file;
+
+	check_argc(argc, 0, 0);
+	if (!given_config_source.file && !startup_info->have_repository)
+		die(_("not in a git directory"));
+	if (given_config_source.use_stdin)
+		die(_("editing stdin is not supported"));
+	if (given_config_source.blob)
+		die(_("editing blobs is not supported"));
+	git_config(git_default_config, NULL);
+	config_file = given_config_source.file ?
+			xstrdup(given_config_source.file) :
+			git_pathdup("config");
+	if (use_global_config) {
+		int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);
+		if (fd >= 0) {
+			char *content = default_user_config();
+			write_str_in_full(fd, content);
+			free(content);
+			close(fd);
+		}
+		else if (errno != EEXIST)
+			die_errno(_("cannot create configuration file %s"), config_file);
+	}
+	launch_editor(config_file, NULL, NULL);
+	free(config_file);
+
+	return 0;
+}
+
+static int cmd_config_set(int argc, const char **argv, const char *prefix)
+{
+	struct key_value_info default_kvi = KVI_INIT;
+	char *value = NULL;
+	int ret;
+
+	check_write();
+	check_argc(argc, 2, 2);
+	value = normalize_value(argv[0], argv[1], &default_kvi);
+	ret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);
+	if (ret == CONFIG_NOTHING_SET)
+		error(_("cannot overwrite multiple values with a single value\n"
+		"       Use a regexp, --add or --replace-all to change %s."), argv[0]);
+
+	free(value);
+	return ret;
+}
+
+static int cmd_config_set_all(int argc, const char **argv, const char *prefix)
+{
+	struct key_value_info default_kvi = KVI_INIT;
+	char *value = NULL;
+	int ret;
+
+	check_write();
+	check_argc(argc, 2, 3);
+	value = normalize_value(argv[0], argv[1], &default_kvi);
+	ret = git_config_set_multivar_in_file_gently(given_config_source.file,
+						     argv[0], value, argv[2],
+						     config_flags);
+
+	free(value);
+	return ret;
+}
+
+static int cmd_config_add(int argc, const char **argv, const char *prefix)
+{
+	struct key_value_info default_kvi = KVI_INIT;
+	char *value = NULL;
+	int ret;
+
+	check_write();
+	check_argc(argc, 2, 2);
+	value = normalize_value(argv[0], argv[1], &default_kvi);
+	ret = git_config_set_multivar_in_file_gently(given_config_source.file,
+						     argv[0], value,
+						     CONFIG_REGEX_NONE,
+						     config_flags);
+
+	free(value);
+	return ret;
+}
+
+static int cmd_config_replace_all(int argc, const char **argv, const char *prefix)
+{
+	struct key_value_info default_kvi = KVI_INIT;
+	char *value = NULL;
+	int ret;
+
+	check_write();
+	check_argc(argc, 2, 3);
+	value = normalize_value(argv[0], argv[1], &default_kvi);
+	ret = git_config_set_multivar_in_file_gently(given_config_source.file,
+						     argv[0], value, argv[2],
+						     config_flags | CONFIG_FLAGS_MULTI_REPLACE);
+
+	free(value);
+	return ret;
+}
+
+static int cmd_config_get(int argc, const char **argv, const char *prefix)
+{
+	check_argc(argc, 1, 2);
+	return get_value(argv[0], argv[1], config_flags);
+}
+
+static int cmd_config_get_all(int argc, const char **argv, const char *prefix)
+{
+	do_all = 1;
+	check_argc(argc, 1, 2);
+	return get_value(argv[0], argv[1], config_flags);
+}
+
+static int cmd_config_get_regexp(int argc, const char **argv, const char *prefix)
+{
+	show_keys = 1;
+	use_key_regexp = 1;
+	do_all = 1;
+	check_argc(argc, 1, 2);
+	return get_value(argv[0], argv[1], config_flags);
+}
+
+static int cmd_config_get_urlmatch(int argc, const char **argv, const char *prefix)
+{
+	check_argc(argc, 2, 2);
+	return get_urlmatch(argv[0], argv[1]);
+}
+
+static int cmd_config_unset(int argc, const char **argv, const char *prefix)
+{
+	check_write();
+	check_argc(argc, 1, 2);
+	if (argc == 2)
+		return git_config_set_multivar_in_file_gently(given_config_source.file,
+							      argv[0], NULL, argv[1],
+							      config_flags);
+	else
+		return git_config_set_in_file_gently(given_config_source.file,
+						     argv[0], NULL);
+}
+
+static int cmd_config_unset_all(int argc, const char **argv, const char *prefix)
+{
+	check_write();
+	check_argc(argc, 1, 2);
+	return git_config_set_multivar_in_file_gently(given_config_source.file,
+						      argv[0], NULL, argv[1],
+						      config_flags | CONFIG_FLAGS_MULTI_REPLACE);
+}
+
+static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)
+{
+	int ret;
+
+	check_write();
+	check_argc(argc, 2, 2);
+	ret = git_config_rename_section_in_file(given_config_source.file,
+						argv[0], argv[1]);
+	if (ret < 0)
+		return ret;
+	else if (!ret)
+		die(_("no such section: %s"), argv[0]);
+	else
+		ret = 0;
+
+	return ret;
+}
+
+static int cmd_config_remove_section(int argc, const char **argv, const char *prefix)
+{
+	int ret;
+
+	check_write();
+	check_argc(argc, 1, 1);
+	ret = git_config_rename_section_in_file(given_config_source.file,
+						argv[0], NULL);
+	if (ret < 0)
+		return ret;
+	else if (!ret)
+		die(_("no such section: %s"), argv[0]);
+	else
+		ret = 0;
+
+	return ret;
+}
+
+static int cmd_config_get_color(int argc, const char **argv, const char *prefix)
+{
+	check_argc(argc, 1, 2);
+	get_color(argv[0], argv[1]);
+	return 0;
+}
+
+static int cmd_config_get_colorbool(int argc, const char **argv, const char *prefix)
+{
+	check_argc(argc, 1, 2);
+	if (argc == 2)
+		color_stdout_is_tty = git_config_bool("command line", argv[1]);
+	return get_colorbool(argv[0], argc == 2);
+}
+
 static struct option builtin_config_options[] = {
 	OPT_GROUP(N_("Config file location")),
 	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
@@ -671,12 +891,6 @@ static NORETURN void usage_builtin_config(void)
 
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
-	int nongit = !startup_info->have_repository;
-	char *value = NULL;
-	int flags = 0;
-	int ret = 0;
-	struct key_value_info default_kvi = KVI_INIT;
-
 	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
 
 	argc = parse_options(argc, argv, prefix, builtin_config_options,
@@ -690,7 +904,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (nongit) {
+	if (!startup_info->have_repository) {
 		if (use_local_config)
 			die(_("--local can only be used inside a git repository"));
 		if (given_config_source.blob)
@@ -751,7 +965,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		config_options.respect_includes = !given_config_source.file;
 	else
 		config_options.respect_includes = respect_includes_opt;
-	if (!nongit) {
+	if (startup_info->have_repository) {
 		config_options.commondir = get_git_common_dir();
 		config_options.git_dir = get_git_dir();
 	}
@@ -826,159 +1040,45 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 			usage_builtin_config();
 		}
 
-		flags |= CONFIG_FLAGS_FIXED_VALUE;
+		config_flags |= CONFIG_FLAGS_FIXED_VALUE;
 	}
 
 	if (actions & PAGING_ACTIONS)
 		setup_auto_pager("config", 1);
 
 	if (actions == ACTION_LIST) {
-		check_argc(argc, 0, 0);
-		if (config_with_options(show_all_config, NULL,
-					&given_config_source, the_repository,
-					&config_options) < 0) {
-			if (given_config_source.file)
-				die_errno(_("unable to read config file '%s'"),
-					  given_config_source.file);
-			else
-				die(_("error processing config file(s)"));
-		}
+		return cmd_config_list(argc, argv, prefix);
+	} else if (actions == ACTION_EDIT) {
+		return cmd_config_edit(argc, argv, prefix);
+	} else if (actions == ACTION_SET) {
+		return cmd_config_set(argc, argv, prefix);
+	} else if (actions == ACTION_SET_ALL) {
+		return cmd_config_set_all(argc, argv, prefix);
+	} else if (actions == ACTION_ADD) {
+		return cmd_config_add(argc, argv, prefix);
+	} else if (actions == ACTION_REPLACE_ALL) {
+		return cmd_config_replace_all(argc, argv, prefix);
+	} else if (actions == ACTION_GET) {
+		return cmd_config_get(argc, argv, prefix);
+	} else if (actions == ACTION_GET_ALL) {
+		return cmd_config_get_all(argc, argv, prefix);
+	} else if (actions == ACTION_GET_REGEXP) {
+		return cmd_config_get_regexp(argc, argv, prefix);
+	} else if (actions == ACTION_GET_URLMATCH) {
+		return cmd_config_get_urlmatch(argc, argv, prefix);
+	} else if (actions == ACTION_UNSET) {
+		return cmd_config_unset(argc, argv, prefix);
+	} else if (actions == ACTION_UNSET_ALL) {
+		return cmd_config_unset_all(argc, argv, prefix);
+	} else if (actions == ACTION_RENAME_SECTION) {
+		return cmd_config_rename_section(argc, argv, prefix);
+	} else if (actions == ACTION_REMOVE_SECTION) {
+		return cmd_config_remove_section(argc, argv, prefix);
+	} else if (actions == ACTION_GET_COLOR) {
+		return cmd_config_get_color(argc, argv, prefix);
+	} else if (actions == ACTION_GET_COLORBOOL) {
+		return cmd_config_get_colorbool(argc, argv, prefix);
 	}
-	else if (actions == ACTION_EDIT) {
-		char *config_file;
 
-		check_argc(argc, 0, 0);
-		if (!given_config_source.file && nongit)
-			die(_("not in a git directory"));
-		if (given_config_source.use_stdin)
-			die(_("editing stdin is not supported"));
-		if (given_config_source.blob)
-			die(_("editing blobs is not supported"));
-		git_config(git_default_config, NULL);
-		config_file = given_config_source.file ?
-				xstrdup(given_config_source.file) :
-				git_pathdup("config");
-		if (use_global_config) {
-			int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);
-			if (fd >= 0) {
-				char *content = default_user_config();
-				write_str_in_full(fd, content);
-				free(content);
-				close(fd);
-			}
-			else if (errno != EEXIST)
-				die_errno(_("cannot create configuration file %s"), config_file);
-		}
-		launch_editor(config_file, NULL, NULL);
-		free(config_file);
-	}
-	else if (actions == ACTION_SET) {
-		check_write();
-		check_argc(argc, 2, 2);
-		value = normalize_value(argv[0], argv[1], &default_kvi);
-		ret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);
-		if (ret == CONFIG_NOTHING_SET)
-			error(_("cannot overwrite multiple values with a single value\n"
-			"       Use a regexp, --add or --replace-all to change %s."), argv[0]);
-	}
-	else if (actions == ACTION_SET_ALL) {
-		check_write();
-		check_argc(argc, 2, 3);
-		value = normalize_value(argv[0], argv[1], &default_kvi);
-		ret = git_config_set_multivar_in_file_gently(given_config_source.file,
-							     argv[0], value, argv[2],
-							     flags);
-	}
-	else if (actions == ACTION_ADD) {
-		check_write();
-		check_argc(argc, 2, 2);
-		value = normalize_value(argv[0], argv[1], &default_kvi);
-		ret = git_config_set_multivar_in_file_gently(given_config_source.file,
-							     argv[0], value,
-							     CONFIG_REGEX_NONE,
-							     flags);
-	}
-	else if (actions == ACTION_REPLACE_ALL) {
-		check_write();
-		check_argc(argc, 2, 3);
-		value = normalize_value(argv[0], argv[1], &default_kvi);
-		ret = git_config_set_multivar_in_file_gently(given_config_source.file,
-							     argv[0], value, argv[2],
-							     flags | CONFIG_FLAGS_MULTI_REPLACE);
-	}
-	else if (actions == ACTION_GET) {
-		check_argc(argc, 1, 2);
-		return get_value(argv[0], argv[1], flags);
-	}
-	else if (actions == ACTION_GET_ALL) {
-		do_all = 1;
-		check_argc(argc, 1, 2);
-		return get_value(argv[0], argv[1], flags);
-	}
-	else if (actions == ACTION_GET_REGEXP) {
-		show_keys = 1;
-		use_key_regexp = 1;
-		do_all = 1;
-		check_argc(argc, 1, 2);
-		return get_value(argv[0], argv[1], flags);
-	}
-	else if (actions == ACTION_GET_URLMATCH) {
-		check_argc(argc, 2, 2);
-		return get_urlmatch(argv[0], argv[1]);
-	}
-	else if (actions == ACTION_UNSET) {
-		check_write();
-		check_argc(argc, 1, 2);
-		if (argc == 2)
-			return git_config_set_multivar_in_file_gently(given_config_source.file,
-								      argv[0], NULL, argv[1],
-								      flags);
-		else
-			return git_config_set_in_file_gently(given_config_source.file,
-							     argv[0], NULL);
-	}
-	else if (actions == ACTION_UNSET_ALL) {
-		check_write();
-		check_argc(argc, 1, 2);
-		return git_config_set_multivar_in_file_gently(given_config_source.file,
-							      argv[0], NULL, argv[1],
-							      flags | CONFIG_FLAGS_MULTI_REPLACE);
-	}
-	else if (actions == ACTION_RENAME_SECTION) {
-		check_write();
-		check_argc(argc, 2, 2);
-		ret = git_config_rename_section_in_file(given_config_source.file,
-							argv[0], argv[1]);
-		if (ret < 0)
-			return ret;
-		else if (!ret)
-			die(_("no such section: %s"), argv[0]);
-		else
-			ret = 0;
-	}
-	else if (actions == ACTION_REMOVE_SECTION) {
-		check_write();
-		check_argc(argc, 1, 1);
-		ret = git_config_rename_section_in_file(given_config_source.file,
-							argv[0], NULL);
-		if (ret < 0)
-			return ret;
-		else if (!ret)
-			die(_("no such section: %s"), argv[0]);
-		else
-			ret = 0;
-	}
-	else if (actions == ACTION_GET_COLOR) {
-		check_argc(argc, 1, 2);
-		get_color(argv[0], argv[1]);
-	}
-	else if (actions == ACTION_GET_COLORBOOL) {
-		check_argc(argc, 1, 2);
-		if (argc == 2)
-			color_stdout_is_tty = git_config_bool("command line", argv[1]);
-		return get_colorbool(argv[0], argc == 2);
-	}
-
-	free(value);
-	return ret;
+	BUG("invalid action");
 }
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH 5/8] builtin/config: track subcommands by action
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (3 preceding siblings ...)
  2024-03-06 11:31 ` [PATCH 4/8] builtin/config: move modes into separate functions Patrick Steinhardt
@ 2024-03-06 11:31 ` Patrick Steinhardt
  2024-03-06 21:54   ` Jean-Noël AVILA
  2024-03-07  0:10   ` Taylor Blau
  2024-03-06 11:31 ` [PATCH 6/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (8 subsequent siblings)
  13 siblings, 2 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-06 11:31 UTC (permalink / raw)
  To: git

[-- Attachment #1: Type: text/plain, Size: 13026 bytes --]

Part of `cmd_config()` is a rather unwieldy switch statement that
invokes the correct subcommand function based on which action has been
requested by the user. Now that we have converted actions to be tracked
via a `OPT_CMDMODE()`, we know that the `actions` variable will
only ever have at most one bit set. This allows us to convert the
variable to use an `enum` instead, and thus to create an array that maps
from this newly introduced `enum` to the corresponding subcommand
function.

Refactor the code to do so. Besides allowing us to get rid of the giant
switch statement, this refactoring will also make it easier to introduce
proper subcommands to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 207 +++++++++++++++++++++++------------------------
 1 file changed, 99 insertions(+), 108 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index a6ab9b8204..0d58397ef5 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -20,6 +20,26 @@ static const char *const builtin_config_usage[] = {
 	NULL
 };
 
+enum config_action {
+	ACTION_NONE,
+	ACTION_GET,
+	ACTION_GET_ALL,
+	ACTION_GET_REGEXP,
+	ACTION_REPLACE_ALL,
+	ACTION_ADD,
+	ACTION_UNSET,
+	ACTION_UNSET_ALL,
+	ACTION_RENAME_SECTION,
+	ACTION_REMOVE_SECTION,
+	ACTION_LIST,
+	ACTION_EDIT,
+	ACTION_SET,
+	ACTION_SET_ALL,
+	ACTION_GET_COLOR,
+	ACTION_GET_COLORBOOL,
+	ACTION_GET_URLMATCH,
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -33,10 +53,12 @@ static char delim = '=';
 static char key_delim = ' ';
 static char term = '\n';
 
+static parse_opt_subcommand_fn *subcommand;
+static enum config_action action = ACTION_NONE;
 static int use_global_config, use_system_config, use_local_config;
 static int use_worktree_config;
 static struct git_config_source given_config_source;
-static int actions, type;
+static int type;
 static char *default_value;
 static int end_nul;
 static int respect_includes_opt = -1;
@@ -46,30 +68,6 @@ static int show_scope;
 static int fixed_value;
 static int config_flags;
 
-#define ACTION_GET (1<<0)
-#define ACTION_GET_ALL (1<<1)
-#define ACTION_GET_REGEXP (1<<2)
-#define ACTION_REPLACE_ALL (1<<3)
-#define ACTION_ADD (1<<4)
-#define ACTION_UNSET (1<<5)
-#define ACTION_UNSET_ALL (1<<6)
-#define ACTION_RENAME_SECTION (1<<7)
-#define ACTION_REMOVE_SECTION (1<<8)
-#define ACTION_LIST (1<<9)
-#define ACTION_EDIT (1<<10)
-#define ACTION_SET (1<<11)
-#define ACTION_SET_ALL (1<<12)
-#define ACTION_GET_COLOR (1<<13)
-#define ACTION_GET_COLORBOOL (1<<14)
-#define ACTION_GET_URLMATCH (1<<15)
-
-/*
- * The actions "ACTION_LIST | ACTION_GET_*" which may produce more than
- * one line of output and which should therefore be paged.
- */
-#define PAGING_ACTIONS (ACTION_LIST | ACTION_GET_ALL | \
-			ACTION_GET_REGEXP | ACTION_GET_URLMATCH)
-
 #define TYPE_BOOL		1
 #define TYPE_INT		2
 #define TYPE_BOOL_OR_INT	3
@@ -842,6 +840,25 @@ static int cmd_config_get_colorbool(int argc, const char **argv, const char *pre
 	return get_colorbool(argv[0], argc == 2);
 }
 
+static parse_opt_subcommand_fn *subcommands_by_action[] = {
+	[ACTION_LIST] = cmd_config_list,
+	[ACTION_EDIT] = cmd_config_edit,
+	[ACTION_SET] = cmd_config_set,
+	[ACTION_SET_ALL] = cmd_config_set_all,
+	[ACTION_ADD] = cmd_config_add,
+	[ACTION_REPLACE_ALL] = cmd_config_replace_all,
+	[ACTION_GET] = cmd_config_get,
+	[ACTION_GET_ALL] = cmd_config_get_all,
+	[ACTION_GET_REGEXP] = cmd_config_get_regexp,
+	[ACTION_GET_URLMATCH] = cmd_config_get_urlmatch,
+	[ACTION_UNSET] = cmd_config_unset,
+	[ACTION_UNSET_ALL] = cmd_config_unset_all,
+	[ACTION_RENAME_SECTION] = cmd_config_rename_section,
+	[ACTION_REMOVE_SECTION] = cmd_config_remove_section,
+	[ACTION_GET_COLOR] = cmd_config_get_color,
+	[ACTION_GET_COLORBOOL] = cmd_config_get_colorbool,
+};
+
 static struct option builtin_config_options[] = {
 	OPT_GROUP(N_("Config file location")),
 	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
@@ -851,20 +868,20 @@ static struct option builtin_config_options[] = {
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
 	OPT_GROUP(N_("Action")),
-	OPT_CMDMODE(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_CMDMODE(0, "get", &action, N_("get value: name [value-pattern]"), ACTION_GET),
+	OPT_CMDMODE(0, "get-all", &action, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
+	OPT_CMDMODE(0, "get-regexp", &action, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
+	OPT_CMDMODE(0, "get-urlmatch", &action, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_CMDMODE(0, "replace-all", &action, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
+	OPT_CMDMODE(0, "add", &action, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_CMDMODE(0, "unset", &action, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
+	OPT_CMDMODE(0, "unset-all", &action, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
+	OPT_CMDMODE(0, "rename-section", &action, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_CMDMODE(0, "remove-section", &action, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_CMDMODE('l', "list", &action, N_("list all"), ACTION_LIST),
+	OPT_CMDMODE('e', "edit", &action, N_("open an editor"), ACTION_EDIT),
+	OPT_CMDMODE(0, "get-color", &action, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
+	OPT_CMDMODE(0, "get-colorbool", &action, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
 	OPT_GROUP(N_("Type")),
 	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
 	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
@@ -976,33 +993,43 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		key_delim = '\n';
 	}
 
-	if ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {
-		error(_("--get-color and variable type are incoherent"));
-		usage_builtin_config();
-	}
-
-	if (actions == 0)
+	if (action == ACTION_NONE) {
 		switch (argc) {
-		case 1: actions = ACTION_GET; break;
-		case 2: actions = ACTION_SET; break;
-		case 3: actions = ACTION_SET_ALL; break;
+		case 1: action = ACTION_GET; break;
+		case 2: action = ACTION_SET; break;
+		case 3: action = ACTION_SET_ALL; break;
 		default:
 			usage_builtin_config();
 		}
+	}
+	if (action <= ACTION_NONE || action >= ARRAY_SIZE(subcommands_by_action))
+		BUG("invalid action %d", action);
+	subcommand = subcommands_by_action[action];
+
+	if (type && (subcommand == cmd_config_get_color ||
+		     subcommand == cmd_config_get_colorbool)) {
+		error(_("--get-color and variable type are incoherent"));
+		usage_builtin_config();
+	}
+
 	if (omit_values &&
-	    !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {
+	    subcommand != cmd_config_list &&
+	    subcommand != cmd_config_get_regexp) {
 		error(_("--name-only is only applicable to --list or --get-regexp"));
 		usage_builtin_config();
 	}
 
-	if (show_origin && !(actions &
-		(ACTION_GET|ACTION_GET_ALL|ACTION_GET_REGEXP|ACTION_LIST))) {
+	if (show_origin &&
+	    subcommand != cmd_config_get &&
+	    subcommand != cmd_config_get_all &&
+	    subcommand != cmd_config_get_regexp &&
+	    subcommand != cmd_config_list) {
 		error(_("--show-origin is only applicable to --get, --get-all, "
 			"--get-regexp, and --list"));
 		usage_builtin_config();
 	}
 
-	if (default_value && !(actions & ACTION_GET)) {
+	if (default_value && subcommand != cmd_config_get) {
 		error(_("--default is only applicable to --get"));
 		usage_builtin_config();
 	}
@@ -1011,28 +1038,19 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 	if (fixed_value) {
 		int allowed_usage = 0;
 
-		switch (actions) {
-		/* git config --get <name> <value-pattern> */
-		case ACTION_GET:
-		/* git config --get-all <name> <value-pattern> */
-		case ACTION_GET_ALL:
-		/* git config --get-regexp <name-pattern> <value-pattern> */
-		case ACTION_GET_REGEXP:
-		/* git config --unset <name> <value-pattern> */
-		case ACTION_UNSET:
-		/* git config --unset-all <name> <value-pattern> */
-		case ACTION_UNSET_ALL:
+		if (subcommand == cmd_config_get ||
+		    subcommand == cmd_config_get_all ||
+		    subcommand == cmd_config_get_regexp ||
+		    subcommand == cmd_config_unset ||
+		    subcommand == cmd_config_unset_all) {
+			/* git config --<action> <name> <value-pattern> */
 			allowed_usage = argc > 1 && !!argv[1];
-			break;
-
-		/* git config <name> <value> <value-pattern> */
-		case ACTION_SET_ALL:
-		/* git config --replace-all <name> <value> <value-pattern> */
-		case ACTION_REPLACE_ALL:
+		} else if (subcommand == cmd_config_set_all ||
+			   subcommand == cmd_config_replace_all) {
+			/* git config --<action> <name> <value> <value-pattern> */
 			allowed_usage = argc > 2 && !!argv[2];
-			break;
-
-		/* other options don't allow --fixed-value */
+		} else {
+			/* other options don't allow --fixed-value */
 		}
 
 		if (!allowed_usage) {
@@ -1043,42 +1061,15 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		config_flags |= CONFIG_FLAGS_FIXED_VALUE;
 	}
 
-	if (actions & PAGING_ACTIONS)
+	/*
+	 * The actions "ACTION_LIST | ACTION_GET_*" which may produce more than
+	 * one line of output and which should therefore be paged.
+	 */
+	if (subcommand == cmd_config_list ||
+	    subcommand == cmd_config_get_all ||
+	    subcommand == cmd_config_get_regexp ||
+	    subcommand == cmd_config_get_urlmatch)
 		setup_auto_pager("config", 1);
 
-	if (actions == ACTION_LIST) {
-		return cmd_config_list(argc, argv, prefix);
-	} else if (actions == ACTION_EDIT) {
-		return cmd_config_edit(argc, argv, prefix);
-	} else if (actions == ACTION_SET) {
-		return cmd_config_set(argc, argv, prefix);
-	} else if (actions == ACTION_SET_ALL) {
-		return cmd_config_set_all(argc, argv, prefix);
-	} else if (actions == ACTION_ADD) {
-		return cmd_config_add(argc, argv, prefix);
-	} else if (actions == ACTION_REPLACE_ALL) {
-		return cmd_config_replace_all(argc, argv, prefix);
-	} else if (actions == ACTION_GET) {
-		return cmd_config_get(argc, argv, prefix);
-	} else if (actions == ACTION_GET_ALL) {
-		return cmd_config_get_all(argc, argv, prefix);
-	} else if (actions == ACTION_GET_REGEXP) {
-		return cmd_config_get_regexp(argc, argv, prefix);
-	} else if (actions == ACTION_GET_URLMATCH) {
-		return cmd_config_get_urlmatch(argc, argv, prefix);
-	} else if (actions == ACTION_UNSET) {
-		return cmd_config_unset(argc, argv, prefix);
-	} else if (actions == ACTION_UNSET_ALL) {
-		return cmd_config_unset_all(argc, argv, prefix);
-	} else if (actions == ACTION_RENAME_SECTION) {
-		return cmd_config_rename_section(argc, argv, prefix);
-	} else if (actions == ACTION_REMOVE_SECTION) {
-		return cmd_config_remove_section(argc, argv, prefix);
-	} else if (actions == ACTION_GET_COLOR) {
-		return cmd_config_get_color(argc, argv, prefix);
-	} else if (actions == ACTION_GET_COLORBOOL) {
-		return cmd_config_get_colorbool(argc, argv, prefix);
-	}
-
-	BUG("invalid action");
+	return subcommand(argc, argv, prefix);
 }
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH 6/8] builtin/config: introduce subcommands
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (4 preceding siblings ...)
  2024-03-06 11:31 ` [PATCH 5/8] builtin/config: track subcommands by action Patrick Steinhardt
@ 2024-03-06 11:31 ` Patrick Steinhardt
  2024-03-06 21:38   ` Karthik Nayak
  2024-03-06 11:31 ` [PATCH 7/8] t1300: exercise both old- and new-style modes Patrick Steinhardt
                   ` (7 subsequent siblings)
  13 siblings, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-06 11:31 UTC (permalink / raw)
  To: git

[-- Attachment #1: Type: text/plain, Size: 5749 bytes --]

While git-config(1) has several modes, those modes are not exposed with
subcommands but instead by specifying e.g. `--unset` or `--list`. This
user interface is not really in line with how our more modern commands
work, where it is a lot more customary to say e.g. `git remote list`.
Furthermore, to add to the confusion, git-config(1) also allows the user
to request modes implicitly by just specifying the correct number of
arguments. Thus, `git config foo.bar` will retrieve the value of
"foo.bar" while `git config foo.bar baz` will set it to "baz".

Overall, this makes for a confusing interface that could really use a
makeover. It hurts discoverability of what you can do with git-config(1)
and is comparatively easy to get wrong.

Modernize git-config(1) so that it understands proper subcommands like
"list" or "unset". Like this, a user can say "git config get foo.bar" to
retrieve the config key's value and "git config set foo.bar baz" to set
it.

One concern in this context is backwards compatibility. Luckily, we can
introduce subcommands without breaking backwards compatibility at all.
This is because all the implicit modes of git-config(1) require that the
first argument is a properly formatted config key. And as config keys
_must_ have a dot in their name, any value without a dot would have been
discarded by git-config(1) previous to this change. Thus, given that
none of the subcommands do have a dot, they are unambiguous.

Consequently, we introduce subcommands in such a way that git-config(1)
understands both the old and the new syntax at the same time. This
should help to transition to the new-style syntax until we eventually
deprecate and remove the old-style syntax.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 52 +++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 45 insertions(+), 7 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 0d58397ef5..10fa933931 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -859,6 +859,26 @@ static parse_opt_subcommand_fn *subcommands_by_action[] = {
 	[ACTION_GET_COLORBOOL] = cmd_config_get_colorbool,
 };
 
+static struct option builtin_subcommand_options[] = {
+	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
+	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
+	OPT_SUBCOMMAND("get-all", &subcommand, cmd_config_get_all),
+	OPT_SUBCOMMAND("get-color", &subcommand, cmd_config_get_color),
+	OPT_SUBCOMMAND("get-colorbool", &subcommand, cmd_config_get_colorbool),
+	OPT_SUBCOMMAND("get-regexp", &subcommand, cmd_config_get_regexp),
+	OPT_SUBCOMMAND("get-urlmatch", &subcommand, cmd_config_get_urlmatch),
+	OPT_SUBCOMMAND("add", &subcommand, cmd_config_add),
+	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
+	OPT_SUBCOMMAND("set-all", &subcommand, cmd_config_set_all),
+	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
+	OPT_SUBCOMMAND("unset-all", &subcommand, cmd_config_unset_all),
+	OPT_SUBCOMMAND("replace-all", &subcommand, cmd_config_replace_all),
+	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
+	OPT_SUBCOMMAND("remove-section", &subcommand, cmd_config_remove_section),
+	OPT_SUBCOMMAND("edit", &subcommand, cmd_config_edit),
+	OPT_END(),
+};
+
 static struct option builtin_config_options[] = {
 	OPT_GROUP(N_("Config file location")),
 	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
@@ -910,6 +930,20 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
 
+	/*
+	 * This is somewhat hacky: we first parse the command line while
+	 * keeping all args intact in order to determine whether a subcommand
+	 * has been specified. If so, we re-parse it a second time, but this
+	 * time we drop KEEP_ARGV0. This is so that we don't munge the command
+	 * line in case no subcommand was given, which would otherwise confuse
+	 * us when parsing the implicit modes.
+	 */
+	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
+	if (subcommand)
+		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
+				     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);
+
 	argc = parse_options(argc, argv, prefix, builtin_config_options,
 			     builtin_config_usage,
 			     PARSE_OPT_STOP_AT_NON_OPTION);
@@ -993,18 +1027,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		key_delim = '\n';
 	}
 
-	if (action == ACTION_NONE) {
+	if (action != ACTION_NONE && subcommand) {
+		error(_("subcommand and action modes are incompatible"));
+		usage_builtin_config();
+	} else if (action == ACTION_NONE && !subcommand) {
 		switch (argc) {
-		case 1: action = ACTION_GET; break;
-		case 2: action = ACTION_SET; break;
-		case 3: action = ACTION_SET_ALL; break;
+		case 1: subcommand = cmd_config_get; break;
+		case 2: subcommand = cmd_config_set; break;
+		case 3: subcommand = cmd_config_set_all; break;
 		default:
 			usage_builtin_config();
 		}
+	} else if (action != ACTION_NONE) {
+		if (action < ACTION_NONE || action >= ARRAY_SIZE(subcommands_by_action))
+			BUG("invalid action %d", action);
+		subcommand = subcommands_by_action[action];
 	}
-	if (action <= ACTION_NONE || action >= ARRAY_SIZE(subcommands_by_action))
-		BUG("invalid action %d", action);
-	subcommand = subcommands_by_action[action];
 
 	if (type && (subcommand == cmd_config_get_color ||
 		     subcommand == cmd_config_get_colorbool)) {
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH 7/8] t1300: exercise both old- and new-style modes
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (5 preceding siblings ...)
  2024-03-06 11:31 ` [PATCH 6/8] builtin/config: introduce subcommands Patrick Steinhardt
@ 2024-03-06 11:31 ` Patrick Steinhardt
  2024-03-06 11:32 ` [PATCH 8/8] Documentation/git-config: update to new-style syntax Patrick Steinhardt
                   ` (6 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-06 11:31 UTC (permalink / raw)
  To: git

[-- Attachment #1: Type: text/plain, Size: 80697 bytes --]

Convert t1300 so that tests exercise both the old and new-style way to
specify the mode.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/t1300-config.sh | 721 ++++++++++++++++++++++++----------------------
 1 file changed, 375 insertions(+), 346 deletions(-)

diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 2d1bc1e27e..705c17a1fb 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -11,6 +11,24 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
+for style in old new
+do
+
+case "$style" in
+old)
+	mode_prefix="--"
+	mode_get=""
+	mode_set=""
+	mode_set_all=""
+	;;
+new)
+	mode_prefix=""
+	mode_get="get"
+	mode_set="set"
+	mode_set_all="set-all"
+	;;
+esac
+
 test_expect_success 'clear default config' '
 	rm -f .git/config
 '
@@ -20,7 +38,7 @@ cat > expect << EOF
 	penguin = little blue
 EOF
 test_expect_success 'initial' '
-	git config section.penguin "little blue" &&
+	git config ${mode_set} section.penguin "little blue" &&
 	test_cmp expect .git/config
 '
 
@@ -30,7 +48,7 @@ cat > expect << EOF
 	Movie = BadPhysics
 EOF
 test_expect_success 'mixed case' '
-	git config Section.Movie BadPhysics &&
+	git config ${mode_set} Section.Movie BadPhysics &&
 	test_cmp expect .git/config
 '
 
@@ -42,7 +60,7 @@ cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'similar section' '
-	git config Sections.WhatEver Second &&
+	git config ${mode_set} Sections.WhatEver Second &&
 	test_cmp expect .git/config
 '
 
@@ -55,16 +73,16 @@ cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'uppercase section' '
-	git config SECTION.UPPERCASE true &&
+	git config ${mode_set} SECTION.UPPERCASE true &&
 	test_cmp expect .git/config
 '
 
 test_expect_success 'replace with non-match' '
-	git config section.penguin kingpin !blue
+	git config ${mode_set_all} section.penguin kingpin !blue
 '
 
 test_expect_success 'replace with non-match (actually matching)' '
-	git config section.penguin "very blue" !kingpin
+	git config ${mode_set_all} section.penguin "very blue" !kingpin
 '
 
 cat > expect << EOF
@@ -102,7 +120,7 @@ test_missing_key () {
 	local key="$1" &&
 	local title="$2" &&
 	test_expect_success "value for $title is not printed" '
-		test_must_fail git config "$key" >out 2>err &&
+		test_must_fail git config ${mode_get} "$key" >out 2>err &&
 		test_must_be_empty out &&
 		test_must_be_empty err
 	'
@@ -125,7 +143,7 @@ foo = bar
 EOF
 
 test_expect_success 'unset with cont. lines' '
-	git config --unset beta.baz
+	git config ${mode_prefix}unset beta.baz
 '
 
 cat > expect <<\EOF
@@ -152,7 +170,7 @@ EOF
 cp .git/config .git/config2
 
 test_expect_success 'multiple unset' '
-	git config --unset-all beta.haha
+	git config ${mode_prefix}unset-all beta.haha
 '
 
 cat > expect << EOF
@@ -178,7 +196,7 @@ test_expect_success '--replace-all missing value' '
 rm .git/config2
 
 test_expect_success '--replace-all' '
-	git config --replace-all beta.haha gamma
+	git config ${mode_prefix}replace-all beta.haha gamma
 '
 
 cat > expect << EOF
@@ -220,7 +238,7 @@ noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'really really mean test' '
-	git config nextsection.nonewline wow &&
+	git config ${mode_set} nextsection.nonewline wow &&
 	test_cmp expect .git/config
 '
 
@@ -238,7 +256,7 @@ noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'unset' '
-	git config --unset beta.haha &&
+	git config ${mode_prefix}unset beta.haha &&
 	test_cmp expect .git/config
 '
 
@@ -258,15 +276,15 @@ test_expect_success 'multivar' '
 '
 
 test_expect_success 'non-match' '
-	git config --get nextsection.nonewline !for
+	git config ${mode_prefix}get nextsection.nonewline !for
 '
 
 test_expect_success 'non-match value' '
-	test_cmp_config wow --get nextsection.nonewline !for
+	test_cmp_config wow ${mode_prefix}get nextsection.nonewline !for
 '
 
 test_expect_success 'multi-valued get returns final one' '
-	test_cmp_config "wow2 for me" --get nextsection.nonewline
+	test_cmp_config "wow2 for me" ${mode_prefix}get nextsection.nonewline
 '
 
 test_expect_success 'multi-valued get-all returns all' '
@@ -274,7 +292,7 @@ test_expect_success 'multi-valued get-all returns all' '
 	wow
 	wow2 for me
 	EOF
-	git config --get-all nextsection.nonewline >actual &&
+	git config ${mode_prefix}get-all nextsection.nonewline >actual &&
 	test_cmp expect actual
 '
 
@@ -289,16 +307,16 @@ noIndent= sillyValue ; 'nother silly comment
 	NoNewLine = wow2 for me
 EOF
 test_expect_success 'multivar replace' '
-	git config nextsection.nonewline "wow3" "wow$" &&
+	git config ${mode_set_all} nextsection.nonewline "wow3" "wow$" &&
 	test_cmp expect .git/config
 '
 
 test_expect_success 'ambiguous unset' '
-	test_must_fail git config --unset nextsection.nonewline
+	test_must_fail git config ${mode_prefix}unset nextsection.nonewline
 '
 
 test_expect_success 'invalid unset' '
-	test_must_fail git config --unset somesection.nonewline
+	test_must_fail git config ${mode_prefix}unset somesection.nonewline
 '
 
 cat > expect << EOF
@@ -312,16 +330,16 @@ noIndent= sillyValue ; 'nother silly comment
 EOF
 
 test_expect_success 'multivar unset' '
-	git config --unset nextsection.nonewline "wow3$" &&
+	git config ${mode_prefix}unset nextsection.nonewline "wow3$" &&
 	test_cmp expect .git/config
 '
 
-test_expect_success 'invalid key' 'test_must_fail git config inval.2key blabla'
+test_expect_success 'invalid key' 'test_must_fail git config ${mode_set} inval.2key blabla'
 
-test_expect_success 'correct key' 'git config 123456.a123 987'
+test_expect_success 'correct key' 'git config ${mode_set} 123456.a123 987'
 
 test_expect_success 'hierarchical section' '
-	git config Version.1.2.3eX.Alpha beta
+	git config ${mode_set} Version.1.2.3eX.Alpha beta
 '
 
 cat > expect << EOF
@@ -350,11 +368,11 @@ version.1.2.3eX.alpha=beta
 EOF
 
 test_expect_success 'working --list' '
-	git config --list > output &&
+	git config ${mode_prefix}list > output &&
 	test_cmp expect output
 '
 test_expect_success '--list without repo produces empty output' '
-	git --git-dir=nonexistent config --list >output &&
+	git --git-dir=nonexistent config ${mode_prefix}list >output &&
 	test_must_be_empty output
 '
 
@@ -366,7 +384,7 @@ version.1.2.3eX.alpha
 EOF
 
 test_expect_success '--name-only --list' '
-	git config --name-only --list >output &&
+	git config ${mode_prefix}list --name-only >output &&
 	test_cmp expect output
 '
 
@@ -376,7 +394,7 @@ nextsection.nonewline wow2 for me
 EOF
 
 test_expect_success '--get-regexp' '
-	git config --get-regexp in >output &&
+	git config ${mode_prefix}get-regexp in >output &&
 	test_cmp expect output
 '
 
@@ -386,7 +404,7 @@ nextsection.nonewline
 EOF
 
 test_expect_success '--name-only --get-regexp' '
-	git config --name-only --get-regexp in >output &&
+	git config ${mode_prefix}get-regexp --name-only in >output &&
 	test_cmp expect output
 '
 
@@ -396,8 +414,8 @@ wow4 for you
 EOF
 
 test_expect_success '--add' '
-	git config --add nextsection.nonewline "wow4 for you" &&
-	git config --get-all nextsection.nonewline > output &&
+	git config ${mode_prefix}add nextsection.nonewline "wow4 for you" &&
+	git config ${mode_prefix}get-all nextsection.nonewline > output &&
 	test_cmp expect output
 '
 
@@ -409,45 +427,45 @@ cat > .git/config << EOF
 EOF
 
 test_expect_success 'get variable with no value' '
-	git config --get novalue.variable ^$
+	git config ${mode_prefix}get novalue.variable ^$
 '
 
 test_expect_success 'get variable with empty value' '
-	git config --get emptyvalue.variable ^$
+	git config ${mode_prefix}get emptyvalue.variable ^$
 '
 
 echo novalue.variable > expect
 
 test_expect_success 'get-regexp variable with no value' '
-	git config --get-regexp novalue > output &&
+	git config ${mode_prefix}get-regexp novalue > output &&
 	test_cmp expect output
 '
 
 echo 'novalue.variable true' > expect
 
 test_expect_success 'get-regexp --bool variable with no value' '
-	git config --bool --get-regexp novalue > output &&
+	git config ${mode_prefix}get-regexp --bool novalue > output &&
 	test_cmp expect output
 '
 
 echo 'emptyvalue.variable ' > expect
 
 test_expect_success 'get-regexp variable with empty value' '
-	git config --get-regexp emptyvalue > output &&
+	git config ${mode_prefix}get-regexp emptyvalue > output &&
 	test_cmp expect output
 '
 
 echo true > expect
 
 test_expect_success 'get bool variable with no value' '
-	git config --bool novalue.variable > output &&
+	git config ${mode_get} --bool novalue.variable > output &&
 	test_cmp expect output
 '
 
 echo false > expect
 
 test_expect_success 'get bool variable with empty value' '
-	git config --bool emptyvalue.variable > output &&
+	git config ${mode_get} --bool emptyvalue.variable > output &&
 	test_cmp expect output
 '
 
@@ -469,7 +487,7 @@ cat > expect << EOF
 EOF
 
 test_expect_success 'new section is partial match of another' '
-	git config a.x y &&
+	git config ${mode_set} a.x y &&
 	test_cmp expect .git/config
 '
 
@@ -484,14 +502,14 @@ cat > expect << EOF
 EOF
 
 test_expect_success 'new variable inserts into proper section' '
-	git config b.x y &&
-	git config a.b c &&
+	git config ${mode_set} b.x y &&
+	git config ${mode_set} a.b c &&
 	test_cmp expect .git/config
 '
 
 test_expect_success 'alternative --file (non-existing file should fail)' '
-	test_must_fail git config --file non-existing-config -l &&
-	test_must_fail git config --file non-existing-config test.xyzzy
+	test_must_fail git config ${mode_prefix}list --file non-existing-config &&
+	test_must_fail git config ${mode_get} --file non-existing-config test.xyzzy
 '
 
 cat > other-config << EOF
@@ -504,29 +522,30 @@ ein.bahn=strasse
 EOF
 
 test_expect_success 'alternative GIT_CONFIG' '
-	GIT_CONFIG=other-config git config --list >output &&
+	GIT_CONFIG=other-config git config ${mode_prefix}list >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'alternative GIT_CONFIG (--file)' '
-	git config --file other-config --list >output &&
+	git config ${mode_prefix}list --file other-config >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'alternative GIT_CONFIG (--file=-)' '
-	git config --file - --list <other-config >output &&
+	git config ${mode_prefix}list --file - <other-config >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'setting a value in stdin is an error' '
-	test_must_fail git config --file - some.value foo
+	test_must_fail git config ${mode_set} --file - some.value foo
 '
 
 test_expect_success 'editing stdin is an error' '
-	test_must_fail git config --file - --edit
+	test_must_fail git config ${mode_prefix}edit --file -
 '
 
 test_expect_success 'refer config from subdirectory' '
+	test_when_finished "rm -r x" &&
 	mkdir x &&
 	test_cmp_config -C x strasse --file=../other-config --get ein.bahn
 '
@@ -539,7 +558,7 @@ cat > expect << EOF
 EOF
 
 test_expect_success '--set in alternative file' '
-	git config --file=other-config anwohner.park ausweis &&
+	git config ${mode_set} --file=other-config anwohner.park ausweis &&
 	test_cmp expect other-config
 '
 
@@ -555,7 +574,7 @@ weird
 EOF
 
 test_expect_success 'rename section' '
-	git config --rename-section branch.eins branch.zwei
+	git config ${mode_prefix}rename-section branch.eins branch.zwei
 '
 
 cat > expect << EOF
@@ -574,7 +593,7 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'rename non-existing section' '
-	test_must_fail git config --rename-section \
+	test_must_fail git config ${mode_prefix}rename-section \
 		branch."world domination" branch.drei
 '
 
@@ -583,7 +602,7 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'rename another section' '
-	git config --rename-section branch."1 234 blabl/a" branch.drei
+	git config ${mode_prefix}rename-section branch."1 234 blabl/a" branch.drei
 '
 
 cat > expect << EOF
@@ -606,7 +625,7 @@ cat >> .git/config << EOF
 EOF
 
 test_expect_success 'rename a section with a var on the same line' '
-	git config --rename-section branch.vier branch.zwei
+	git config ${mode_prefix}rename-section branch.vier branch.zwei
 '
 
 cat > expect << EOF
@@ -627,11 +646,11 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'renaming empty section name is rejected' '
-	test_must_fail git config --rename-section branch.zwei ""
+	test_must_fail git config ${mode_prefix}rename-section branch.zwei ""
 '
 
 test_expect_success 'renaming to bogus section is rejected' '
-	test_must_fail git config --rename-section branch.zwei "bogus name"
+	test_must_fail git config ${mode_prefix}rename-section branch.zwei "bogus name"
 '
 
 test_expect_success 'renaming a section with a long line' '
@@ -640,7 +659,7 @@ test_expect_success 'renaming a section with a long line' '
 		printf "  c = d %1024s [a] e = f\\n" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	git config -f y --rename-section a xyz &&
+	git config ${mode_prefix}rename-section -f y a xyz &&
 	test_must_fail git config -f y b.e
 '
 
@@ -650,7 +669,7 @@ test_expect_success 'renaming an embedded section with a long line' '
 		printf "  c = d %1024s [a] [foo] e = f\\n" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	git config -f y --rename-section a xyz &&
+	git config ${mode_prefix}rename-section -f y a xyz &&
 	test_must_fail git config -f y foo.e
 '
 
@@ -660,7 +679,7 @@ test_expect_success 'renaming a section with an overly-long line' '
 		printf "  c = d %525000s e" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	test_must_fail git config -f y --rename-section a xyz 2>err &&
+	test_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&
 	grep "refusing to work with overly long line in .y. on line 2" err
 '
 
@@ -669,7 +688,7 @@ cat >> .git/config << EOF
 EOF
 
 test_expect_success 'remove section' '
-	git config --remove-section branch.zwei
+	git config ${mode_prefix}remove-section branch.zwei
 '
 
 cat > expect << EOF
@@ -693,20 +712,20 @@ EOF
 
 test_expect_success 'section ending' '
 	rm -f .git/config &&
-	git config gitcvs.enabled true &&
-	git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
-	git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.enabled true &&
+	git config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
 	test_cmp expect .git/config
 
 '
 
 test_expect_success numbers '
-	git config kilo.gram 1k &&
-	git config mega.ton 1m &&
+	git config ${mode_set} kilo.gram 1k &&
+	git config ${mode_set} mega.ton 1m &&
 	echo 1024 >expect &&
 	echo 1048576 >>expect &&
-	git config --int --get kilo.gram >actual &&
-	git config --int --get mega.ton >>actual &&
+	git config ${mode_prefix}get --int kilo.gram >actual &&
+	git config ${mode_prefix}get --int mega.ton >>actual &&
 	test_cmp expect actual
 '
 
@@ -719,25 +738,25 @@ test_expect_success '--int is at least 64 bits' '
 test_expect_success 'invalid unit' '
 	git config aninvalid.unit "1auto" &&
 	test_cmp_config 1auto aninvalid.unit &&
-	test_must_fail git config --int --get aninvalid.unit 2>actual &&
+	test_must_fail git config ${mode_prefix}get --int aninvalid.unit 2>actual &&
 	test_grep "bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit" actual
 '
 
 test_expect_success 'invalid unit boolean' '
 	git config commit.gpgsign "1true" &&
 	test_cmp_config 1true commit.gpgsign &&
-	test_must_fail git config --bool --get commit.gpgsign 2>actual &&
+	test_must_fail git config ${mode_prefix}get --bool commit.gpgsign 2>actual &&
 	test_grep "bad boolean config value .1true. for .commit.gpgsign." actual
 '
 
 test_expect_success 'line number is reported correctly' '
 	printf "[bool]\n\tvar\n" >invalid &&
-	test_must_fail git config -f invalid --path bool.var 2>actual &&
+	test_must_fail git config ${mode_get} -f invalid --path bool.var 2>actual &&
 	test_grep "line 2" actual
 '
 
 test_expect_success 'invalid stdin config' '
-	echo "[broken" | test_must_fail git config --list --file - >output 2>&1 &&
+	echo "[broken" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&
 	test_grep "bad config line 1 in standard input" output
 '
 
@@ -765,19 +784,19 @@ test_expect_success bool '
 	rm -f result &&
 	for i in 1 2 3 4
 	do
-	    git config --bool --get bool.true$i >>result &&
-	    git config --bool --get bool.false$i >>result || return 1
+	    git config ${mode_prefix}get --bool bool.true$i >>result &&
+	    git config ${mode_prefix}get --bool bool.false$i >>result || return 1
 	done &&
 	test_cmp expect result'
 
 test_expect_success 'invalid bool (--get)' '
 
 	git config bool.nobool foobar &&
-	test_must_fail git config --bool --get bool.nobool'
+	test_must_fail git config ${mode_prefix}get --bool bool.nobool'
 
 test_expect_success 'invalid bool (set)' '
 
-	test_must_fail git config --bool bool.nobool foobar'
+	test_must_fail git config ${mode_set} --bool bool.nobool foobar'
 
 cat > expect <<\EOF
 [bool]
@@ -794,14 +813,14 @@ EOF
 test_expect_success 'set --bool' '
 
 	rm -f .git/config &&
-	git config --bool bool.true1 01 &&
-	git config --bool bool.true2 -1 &&
-	git config --bool bool.true3 YeS &&
-	git config --bool bool.true4 true &&
-	git config --bool bool.false1 000 &&
-	git config --bool bool.false2 "" &&
-	git config --bool bool.false3 nO &&
-	git config --bool bool.false4 FALSE &&
+	git config ${mode_set} --bool bool.true1 01 &&
+	git config ${mode_set} --bool bool.true2 -1 &&
+	git config ${mode_set} --bool bool.true3 YeS &&
+	git config ${mode_set} --bool bool.true4 true &&
+	git config ${mode_set} --bool bool.false1 000 &&
+	git config ${mode_set} --bool bool.false2 "" &&
+	git config ${mode_set} --bool bool.false3 nO &&
+	git config ${mode_set} --bool bool.false4 FALSE &&
 	test_cmp expect .git/config'
 
 cat > expect <<\EOF
@@ -814,9 +833,9 @@ EOF
 test_expect_success 'set --int' '
 
 	rm -f .git/config &&
-	git config --int int.val1 01 &&
-	git config --int int.val2 -1 &&
-	git config --int int.val3 5m &&
+	git config ${mode_set} --int int.val1 01 &&
+	git config ${mode_set} --int int.val2 -1 &&
+	git config ${mode_set} --int int.val3 5m &&
 	test_cmp expect .git/config
 '
 
@@ -840,12 +859,12 @@ test_expect_success 'get --bool-or-int' '
 	-1
 	EOF
 	{
-		git config --bool-or-int bool.true1 &&
-		git config --bool-or-int bool.true2 &&
-		git config --bool-or-int bool.false &&
-		git config --bool-or-int int.int1 &&
-		git config --bool-or-int int.int2 &&
-		git config --bool-or-int int.int3
+		git config ${mode_get} --bool-or-int bool.true1 &&
+		git config ${mode_get} --bool-or-int bool.true2 &&
+		git config ${mode_get} --bool-or-int bool.false &&
+		git config ${mode_get} --bool-or-int int.int1 &&
+		git config ${mode_get} --bool-or-int int.int2 &&
+		git config ${mode_get} --bool-or-int int.int3
 	} >actual &&
 	test_cmp expect actual
 '
@@ -864,13 +883,13 @@ EOF
 
 test_expect_success 'set --bool-or-int' '
 	rm -f .git/config &&
-	git config --bool-or-int bool.true1 true &&
-	git config --bool-or-int bool.false1 false &&
-	git config --bool-or-int bool.true2 yes &&
-	git config --bool-or-int bool.false2 no &&
-	git config --bool-or-int int.int1 0 &&
-	git config --bool-or-int int.int2 1 &&
-	git config --bool-or-int int.int3 -1 &&
+	git config ${mode_set} --bool-or-int bool.true1 true &&
+	git config ${mode_set} --bool-or-int bool.false1 false &&
+	git config ${mode_set} --bool-or-int bool.true2 yes &&
+	git config ${mode_set} --bool-or-int bool.false2 no &&
+	git config ${mode_set} --bool-or-int int.int1 0 &&
+	git config ${mode_set} --bool-or-int int.int2 1 &&
+	git config ${mode_set} --bool-or-int int.int3 -1 &&
 	test_cmp expect .git/config
 '
 
@@ -883,9 +902,9 @@ EOF
 
 test_expect_success !MINGW 'set --path' '
 	rm -f .git/config &&
-	git config --path path.home "~/" &&
-	git config --path path.normal "/dev/null" &&
-	git config --path path.trailingtilde "foo~" &&
+	git config ${mode_set} --path path.home "~/" &&
+	git config ${mode_set} --path path.normal "/dev/null" &&
+	git config ${mode_set} --path path.trailingtilde "foo~" &&
 	test_cmp expect .git/config'
 
 if test_have_prereq !MINGW && test "${HOME+set}"
@@ -900,9 +919,9 @@ foo~
 EOF
 
 test_expect_success HOMEVAR 'get --path' '
-	git config --get --path path.home > result &&
-	git config --get --path path.normal >> result &&
-	git config --get --path path.trailingtilde >> result &&
+	git config ${mode_prefix}get --path path.home > result &&
+	git config ${mode_prefix}get --path path.normal >> result &&
+	git config ${mode_prefix}get --path path.trailingtilde >> result &&
 	test_cmp expect result
 '
 
@@ -914,10 +933,10 @@ EOF
 test_expect_success !MINGW 'get --path copes with unset $HOME' '
 	(
 		sane_unset HOME &&
-		test_must_fail git config --get --path path.home \
+		test_must_fail git config ${mode_prefix}get --path path.home \
 			>result 2>msg &&
-		git config --get --path path.normal >>result &&
-		git config --get --path path.trailingtilde >>result
+		git config ${mode_prefix}get --path path.normal >>result &&
+		git config ${mode_prefix}get --path path.trailingtilde >>result
 	) &&
 	test_grep "[Ff]ailed to expand.*~/" msg &&
 	test_cmp expect result
@@ -925,7 +944,7 @@ test_expect_success !MINGW 'get --path copes with unset $HOME' '
 
 test_expect_success 'get --path barfs on boolean variable' '
 	echo "[path]bool" >.git/config &&
-	test_must_fail git config --get --path path.bool
+	test_must_fail git config ${mode_prefix}get --path path.bool
 '
 
 test_expect_success 'get --expiry-date' '
@@ -949,20 +968,20 @@ test_expect_success 'get --expiry-date' '
 	EOF
 	: "work around heredoc parsing bug fixed in dash 0.5.7 (in ec2c84d)" &&
 	{
-		echo "$rel_out $(git config --expiry-date date.valid1)" &&
-		git config --expiry-date date.valid2 &&
-		git config --expiry-date date.valid3 &&
-		git config --expiry-date date.valid4 &&
-		git config --expiry-date date.valid5
+		echo "$rel_out $(git config ${mode_get} --expiry-date date.valid1)" &&
+		git config ${mode_get} --expiry-date date.valid2 &&
+		git config ${mode_get} --expiry-date date.valid3 &&
+		git config ${mode_get} --expiry-date date.valid4 &&
+		git config ${mode_get} --expiry-date date.valid5
 	} >actual &&
 	test_cmp expect actual &&
-	test_must_fail git config --expiry-date date.invalid1
+	test_must_fail git config ${mode_get} --expiry-date date.invalid1
 '
 
 test_expect_success 'get --type=color' '
 	rm .git/config &&
 	git config foo.color "red" &&
-	git config --get --type=color foo.color >actual.raw &&
+	git config ${mode_prefix}get --type=color foo.color >actual.raw &&
 	test_decode_color <actual.raw >actual &&
 	echo "<RED>" >expect &&
 	test_cmp expect actual
@@ -975,17 +994,17 @@ EOF
 
 test_expect_success 'set --type=color' '
 	rm .git/config &&
-	git config --type=color foo.color "red" &&
+	git config ${mode_set} --type=color foo.color "red" &&
 	test_cmp expect .git/config
 '
 
 test_expect_success 'get --type=color barfs on non-color' '
 	echo "[foo]bar=not-a-color" >.git/config &&
-	test_must_fail git config --get --type=color foo.bar
+	test_must_fail git config ${mode_prefix}get --type=color foo.bar
 '
 
 test_expect_success 'set --type=color barfs on non-color' '
-	test_must_fail git config --type=color foo.color "not-a-color" 2>error &&
+	test_must_fail git config ${mode_set} --type=color foo.color "not-a-color" 2>error &&
 	test_grep "cannot parse color" error
 '
 
@@ -998,18 +1017,18 @@ cat > expect << EOF
 EOF
 test_expect_success 'quoting' '
 	rm -f .git/config &&
-	git config quote.leading " test" &&
-	git config quote.ending "test " &&
-	git config quote.semicolon "test;test" &&
-	git config quote.hash "test#test" &&
+	git config ${mode_set} quote.leading " test" &&
+	git config ${mode_set} quote.ending "test " &&
+	git config ${mode_set} quote.semicolon "test;test" &&
+	git config ${mode_set} quote.hash "test#test" &&
 	test_cmp expect .git/config
 '
 
 test_expect_success 'key with newline' '
-	test_must_fail git config "key.with
+	test_must_fail git config ${mode_set} "key.with
 newline" 123'
 
-test_expect_success 'value with newline' 'git config key.sub value.with\\\
+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\
 newline'
 
 cat > .git/config <<\EOF
@@ -1029,7 +1048,7 @@ section.quotecont=cont;inued
 EOF
 
 test_expect_success 'value continued on next line' '
-	git config --list > result &&
+	git config ${mode_prefix}list > result &&
 	test_cmp expect result
 '
 
@@ -1053,14 +1072,14 @@ Qsection.sub=section.val4
 Qsection.sub=section.val5Q
 EOF
 test_expect_success '--null --list' '
-	git config --null --list >result.raw &&
+	git config ${mode_prefix}list --null >result.raw &&
 	nul_to_q <result.raw >result &&
 	echo >>result &&
 	test_cmp expect result
 '
 
 test_expect_success '--null --get-regexp' '
-	git config --null --get-regexp "val[0-9]" >result.raw &&
+	git config ${mode_prefix}get-regexp --null "val[0-9]" >result.raw &&
 	nul_to_q <result.raw >result &&
 	echo >>result &&
 	test_cmp expect result
@@ -1072,12 +1091,13 @@ test_expect_success 'inner whitespace kept verbatim' '
 '
 
 test_expect_success SYMLINKS 'symlinked configuration' '
+	test_when_finished "rm myconfig" &&
 	ln -s notyet myconfig &&
-	git config --file=myconfig test.frotz nitfol &&
+	git config ${mode_set} --file=myconfig test.frotz nitfol &&
 	test -h myconfig &&
 	test -f notyet &&
-	test "z$(git config --file=notyet test.frotz)" = znitfol &&
-	git config --file=myconfig test.xyzzy rezrov &&
+	test "z$(git config ${mode_get} --file=notyet test.frotz)" = znitfol &&
+	git config ${mode_set} --file=myconfig test.xyzzy rezrov &&
 	test -h myconfig &&
 	test -f notyet &&
 	cat >expect <<-\EOF &&
@@ -1085,17 +1105,18 @@ test_expect_success SYMLINKS 'symlinked configuration' '
 	rezrov
 	EOF
 	{
-		git config --file=notyet test.frotz &&
-		git config --file=notyet test.xyzzy
+		git config ${mode_get} --file=notyet test.frotz &&
+		git config ${mode_get} --file=notyet test.xyzzy
 	} >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success SYMLINKS 'symlink to nonexistent configuration' '
+	test_when_finished "rm linktonada linktolinktonada" &&
 	ln -s doesnotexist linktonada &&
 	ln -s linktonada linktolinktonada &&
-	test_must_fail git config --file=linktonada --list &&
-	test_must_fail git config --file=linktolinktonada --list
+	test_must_fail git config ${mode_prefix}list --file=linktonada &&
+	test_must_fail git config ${mode_prefix}list --file=linktolinktonada
 '
 
 test_expect_success 'check split_cmdline return' '
@@ -1103,12 +1124,12 @@ test_expect_success 'check split_cmdline return' '
 	git init repo &&
 	(
 		cd repo &&
-		git config alias.split-cmdline-fix "echo \"" &&
+		git config ${mode_set} alias.split-cmdline-fix "echo \"" &&
 		test_must_fail git split-cmdline-fix &&
 		echo foo >foo &&
 		git add foo &&
 		git commit -m "initial commit" &&
-		git config branch.main.mergeoptions "echo \"" &&
+		git config ${mode_set} branch.main.mergeoptions "echo \"" &&
 		test_must_fail git merge main
 	)
 '
@@ -1122,10 +1143,10 @@ test_expect_success 'git -c "key=value" support' '
 	{
 		git -c section.name=value config section.name &&
 		git -c foo.CamelCase=value config foo.camelcase &&
-		git -c foo.flag config --bool foo.flag
+		git -c foo.flag config ${mode_get} --bool foo.flag
 	} >actual &&
 	test_cmp expect actual &&
-	test_must_fail git -c name=value config section.name
+	test_must_fail git -c name=value config ${mode_get} section.name
 '
 
 # We just need a type-specifier here that cares about the
@@ -1135,27 +1156,27 @@ test_expect_success 'git -c "key=value" support' '
 # its semantics.
 test_expect_success 'git -c can represent empty string' '
 	echo >expect &&
-	git -c foo.empty= config --path foo.empty >actual &&
+	git -c foo.empty= config ${mode_get} --path foo.empty >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'key sanity-checking' '
-	test_must_fail git config foo=bar &&
-	test_must_fail git config foo=.bar &&
-	test_must_fail git config foo.ba=r &&
-	test_must_fail git config foo.1bar &&
-	test_must_fail git config foo."ba
+	test_must_fail git config ${mode_get} foo=bar &&
+	test_must_fail git config ${mode_get} foo=.bar &&
+	test_must_fail git config ${mode_get} foo.ba=r &&
+	test_must_fail git config ${mode_get} foo.1bar &&
+	test_must_fail git config ${mode_get} foo."ba
 				z".bar &&
-	test_must_fail git config . false &&
-	test_must_fail git config .foo false &&
-	test_must_fail git config foo. false &&
-	test_must_fail git config .foo. false &&
-	git config foo.bar true &&
-	git config foo."ba =z".bar false
+	test_must_fail git config ${mode_set} . false &&
+	test_must_fail git config ${mode_set} .foo false &&
+	test_must_fail git config ${mode_set} foo. false &&
+	test_must_fail git config ${mode_set} .foo. false &&
+	git config ${mode_set} foo.bar true &&
+	git config ${mode_set} foo."ba =z".bar false
 '
 
 test_expect_success 'git -c works with aliases of builtins' '
-	git config alias.checkconfig "-c foo.check=bar config foo.check" &&
+	git config ${mode_set} alias.checkconfig "-c foo.check=bar config foo.check" &&
 	echo bar >expect &&
 	git checkconfig >actual &&
 	test_cmp expect actual
@@ -1167,7 +1188,7 @@ test_expect_success 'aliases can be CamelCased' '
 	(
 		cd repo &&
 		test_commit A &&
-		git config alias.CamelCased "rev-parse HEAD" &&
+		git config ${mode_set} alias.CamelCased "rev-parse HEAD" &&
 		git CamelCased >out &&
 		git rev-parse HEAD >expect &&
 		test_cmp expect out
@@ -1176,7 +1197,7 @@ test_expect_success 'aliases can be CamelCased' '
 
 test_expect_success 'git -c does not split values on equals' '
 	echo "value with = in it" >expect &&
-	git -c section.foo="value with = in it" config section.foo >actual &&
+	git -c section.foo="value with = in it" config ${mode_get} section.foo >actual &&
 	test_cmp expect actual
 '
 
@@ -1193,7 +1214,7 @@ test_expect_success 'git -c complains about empty key and value' '
 '
 
 test_expect_success 'multiple git -c appends config' '
-	test_config alias.x "!git -c x.two=2 config --get-regexp ^x\.*" &&
+	test_config alias.x "!git -c x.two=2 config ${mode_prefix}get-regexp ^x\.*" &&
 	cat >expect <<-\EOF &&
 	x.one 1
 	x.two 2
@@ -1210,14 +1231,14 @@ test_expect_success 'last one wins: two level vars' '
 
 	echo VAL >expect &&
 
-	git -c sec.var=val -c sec.VAR=VAL config --get sec.var >actual &&
+	git -c sec.var=val -c sec.VAR=VAL config ${mode_prefix}get sec.var >actual &&
 	test_cmp expect actual &&
-	git -c SEC.var=val -c sec.var=VAL config --get sec.var >actual &&
+	git -c SEC.var=val -c sec.var=VAL config ${mode_prefix}get sec.var >actual &&
 	test_cmp expect actual &&
 
-	git -c sec.var=val -c sec.VAR=VAL config --get SEC.var >actual &&
+	git -c sec.var=val -c sec.VAR=VAL config ${mode_prefix}get SEC.var >actual &&
 	test_cmp expect actual &&
-	git -c SEC.var=val -c sec.var=VAL config --get sec.VAR >actual &&
+	git -c SEC.var=val -c sec.var=VAL config ${mode_prefix}get sec.VAR >actual &&
 	test_cmp expect actual
 '
 
@@ -1228,9 +1249,9 @@ test_expect_success 'last one wins: three level vars' '
 	# case sensitive.
 
 	echo val >expect &&
-	git -c v.a.r=val -c v.A.r=VAL config --get v.a.r >actual &&
+	git -c v.a.r=val -c v.A.r=VAL config ${mode_prefix}get v.a.r >actual &&
 	test_cmp expect actual &&
-	git -c v.a.r=val -c v.A.r=VAL config --get V.a.R >actual &&
+	git -c v.a.r=val -c v.A.r=VAL config ${mode_prefix}get V.a.R >actual &&
 	test_cmp expect actual &&
 
 	# v.a.r and V.a.R are the same variable, as the first
@@ -1238,13 +1259,13 @@ test_expect_success 'last one wins: three level vars' '
 	# case insensitive.
 
 	echo VAL >expect &&
-	git -c v.a.r=val -c v.a.R=VAL config --get v.a.r >actual &&
+	git -c v.a.r=val -c v.a.R=VAL config ${mode_prefix}get v.a.r >actual &&
 	test_cmp expect actual &&
-	git -c v.a.r=val -c V.a.r=VAL config --get v.a.r >actual &&
+	git -c v.a.r=val -c V.a.r=VAL config ${mode_prefix}get v.a.r >actual &&
 	test_cmp expect actual &&
-	git -c v.a.r=val -c v.a.R=VAL config --get V.a.R >actual &&
+	git -c v.a.r=val -c v.a.R=VAL config ${mode_prefix}get V.a.R >actual &&
 	test_cmp expect actual &&
-	git -c v.a.r=val -c V.a.r=VAL config --get V.a.R >actual &&
+	git -c v.a.r=val -c V.a.r=VAL config ${mode_prefix}get V.a.R >actual &&
 	test_cmp expect actual
 '
 
@@ -1259,7 +1280,7 @@ test_expect_success 'old-fashioned settings are case insensitive' '
 	[V.A]
 	Qr = value2
 	EOF
-	git config -f testConfig_actual "v.a.r" value2 &&
+	git config ${mode_set} -f testConfig_actual "v.a.r" value2 &&
 	test_cmp testConfig_expect testConfig_actual &&
 
 	cat >testConfig_actual <<-EOF &&
@@ -1270,7 +1291,7 @@ test_expect_success 'old-fashioned settings are case insensitive' '
 	[V.A]
 	QR = value2
 	EOF
-	git config -f testConfig_actual "V.a.R" value2 &&
+	git config ${mode_set} -f testConfig_actual "V.a.R" value2 &&
 	test_cmp testConfig_expect testConfig_actual &&
 
 	cat >testConfig_actual <<-EOF &&
@@ -1282,7 +1303,7 @@ test_expect_success 'old-fashioned settings are case insensitive' '
 	r = value1
 	Qr = value2
 	EOF
-	git config -f testConfig_actual "V.A.r" value2 &&
+	git config ${mode_set} -f testConfig_actual "V.A.r" value2 &&
 	test_cmp testConfig_expect testConfig_actual &&
 
 	cat >testConfig_actual <<-EOF &&
@@ -1294,7 +1315,7 @@ test_expect_success 'old-fashioned settings are case insensitive' '
 	r = value1
 	Qr = value2
 	EOF
-	git config -f testConfig_actual "v.A.r" value2 &&
+	git config ${mode_set} -f testConfig_actual "v.A.r" value2 &&
 	test_cmp testConfig_expect testConfig_actual
 '
 
@@ -1324,21 +1345,21 @@ test_expect_success 'setting different case sensitive subsections ' '
 	Qf = v2
 	EOF
 	# exact match
-	git config -f testConfig_actual a.b.c v2 &&
+	git config ${mode_set} -f testConfig_actual a.b.c v2 &&
 	# match section and subsection, key is cased differently.
-	git config -f testConfig_actual K.E.y v2 &&
+	git config ${mode_set} -f testConfig_actual K.E.y v2 &&
 	# section and key are matched case insensitive, but subsection needs
 	# to match; When writing out new values only the key is adjusted
-	git config -f testConfig_actual v.A.r v2 &&
+	git config ${mode_set} -f testConfig_actual v.A.r v2 &&
 	# subsection is not matched:
-	git config -f testConfig_actual d.E.f v2 &&
+	git config ${mode_set} -f testConfig_actual d.E.f v2 &&
 	test_cmp testConfig_expect testConfig_actual
 '
 
 for VAR in a .a a. a.0b a."b c". a."b c".0d
 do
 	test_expect_success "git -c $VAR=VAL rejects invalid '$VAR'" '
-		test_must_fail git -c "$VAR=VAL" config -l
+		test_must_fail git -c "$VAR=VAL" config ${mode_prefix}list
 	'
 done
 
@@ -1346,20 +1367,20 @@ for VAR in a.b a."b c".d
 do
 	test_expect_success "git -c $VAR=VAL works with valid '$VAR'" '
 		echo VAL >expect &&
-		git -c "$VAR=VAL" config --get "$VAR" >actual &&
+		git -c "$VAR=VAL" config ${mode_prefix}get "$VAR" >actual &&
 		test_cmp expect actual
 	'
 done
 
 test_expect_success 'git -c is not confused by empty environment' '
-	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config --list
+	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config ${mode_prefix}list
 '
 
 test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
 	v="${SQ}key.one=foo${SQ}" &&
 	v="$v  ${SQ}key.two=bar${SQ}" &&
 	v="$v ${SQ}key.ambiguous=section.whatever=value${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_prefix}get-regexp "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.one foo
 	key.two bar
@@ -1372,7 +1393,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '
 	v="${SQ}key.one${SQ}=${SQ}foo${SQ}" &&
 	v="$v  ${SQ}key.two${SQ}=${SQ}bar${SQ}" &&
 	v="$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_prefix}get-regexp "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.one foo
 	key.two bar
@@ -1386,7 +1407,7 @@ test_expect_success 'old and new-style entries can mix' '
 	v="$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}" &&
 	v="$v ${SQ}key.oldtwo=oldbar${SQ}" &&
 	v="$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_prefix}get-regexp "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.oldone oldfoo
 	key.newone newfoo
@@ -1399,7 +1420,7 @@ test_expect_success 'old and new-style entries can mix' '
 test_expect_success 'old and new bools with ambiguous subsection' '
 	v="${SQ}key.with=equals.oldbool${SQ}" &&
 	v="$v ${SQ}key.with=equals.newbool${SQ}=" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_prefix}get-regexp "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.with equals.oldbool
 	key.with=equals.newbool
@@ -1413,7 +1434,7 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
 	env.two two
 	EOF
 	GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ} ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*" >actual &&
+		git config ${mode_prefix}get-regexp "env.*" >actual &&
 	test_cmp expect actual &&
 
 	cat >expect <<-EOF &&
@@ -1421,12 +1442,12 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
 	env.two two
 	EOF
 	GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ}\\$SQ$SQ$SQ ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*" >actual &&
+		git config ${mode_prefix}get-regexp "env.*" >actual &&
 	test_cmp expect actual &&
 
 	test_must_fail env \
 		GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ}\\$SQ ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*"
+		git config ${mode_prefix}get-regexp "env.*"
 '
 
 test_expect_success 'git --config-env=key=envvar support' '
@@ -1439,12 +1460,12 @@ test_expect_success 'git --config-env=key=envvar support' '
 	false
 	EOF
 	{
-		ENVVAR=value git --config-env=core.name=ENVVAR config core.name &&
-		ENVVAR=value git --config-env core.name=ENVVAR config core.name &&
-		ENVVAR=value git --config-env=foo.CamelCase=ENVVAR config foo.camelcase &&
-		ENVVAR=value git --config-env foo.CamelCase=ENVVAR config foo.camelcase &&
-		ENVVAR= git --config-env=foo.flag=ENVVAR config --bool foo.flag &&
-		ENVVAR= git --config-env foo.flag=ENVVAR config --bool foo.flag
+		ENVVAR=value git --config-env=core.name=ENVVAR config ${mode_get} core.name &&
+		ENVVAR=value git --config-env core.name=ENVVAR config ${mode_get} core.name &&
+		ENVVAR=value git --config-env=foo.CamelCase=ENVVAR config ${mode_get} foo.camelcase &&
+		ENVVAR=value git --config-env foo.CamelCase=ENVVAR config ${mode_get} foo.camelcase &&
+		ENVVAR= git --config-env=foo.flag=ENVVAR config ${mode_get} --bool foo.flag &&
+		ENVVAR= git --config-env foo.flag=ENVVAR config ${mode_get} --bool foo.flag
 	} >actual &&
 	test_cmp expect actual
 '
@@ -1452,17 +1473,17 @@ test_expect_success 'git --config-env=key=envvar support' '
 test_expect_success 'git --config-env with missing value' '
 	test_must_fail env ENVVAR=value git --config-env 2>error &&
 	grep "no config key given for --config-env" error &&
-	test_must_fail env ENVVAR=value git --config-env config core.name 2>error &&
+	test_must_fail env ENVVAR=value git --config-env config ${mode_get} core.name 2>error &&
 	grep "invalid config format: config" error
 '
 
 test_expect_success 'git --config-env fails with invalid parameters' '
-	test_must_fail git --config-env=foo.flag config --bool foo.flag 2>error &&
+	test_must_fail git --config-env=foo.flag config ${mode_get} --bool foo.flag 2>error &&
 	test_grep "invalid config format: foo.flag" error &&
-	test_must_fail git --config-env=foo.flag= config --bool foo.flag 2>error &&
+	test_must_fail git --config-env=foo.flag= config ${mode_get} --bool foo.flag 2>error &&
 	test_grep "missing environment variable name for configuration ${SQ}foo.flag${SQ}" error &&
 	sane_unset NONEXISTENT &&
-	test_must_fail git --config-env=foo.flag=NONEXISTENT config --bool foo.flag 2>error &&
+	test_must_fail git --config-env=foo.flag=NONEXISTENT config ${mode_get} --bool foo.flag 2>error &&
 	test_grep "missing environment variable ${SQ}NONEXISTENT${SQ} for configuration ${SQ}foo.flag${SQ}" error
 '
 
@@ -1474,7 +1495,7 @@ test_expect_success 'git -c and --config-env work together' '
 	ENVVAR=env-value git \
 		-c bar.cmd=cmd-value \
 		--config-env=bar.env=ENVVAR \
-		config --get-regexp "^bar.*" >actual &&
+		config ${mode_prefix}get-regexp "^bar.*" >actual &&
 	test_cmp expect actual
 '
 
@@ -1484,8 +1505,8 @@ test_expect_success 'git -c and --config-env override each other' '
 	cmd
 	EOF
 	{
-		ENVVAR=env git -c bar.bar=cmd --config-env=bar.bar=ENVVAR config bar.bar &&
-		ENVVAR=env git --config-env=bar.bar=ENVVAR -c bar.bar=cmd config bar.bar
+		ENVVAR=env git -c bar.bar=cmd --config-env=bar.bar=ENVVAR config ${config_get} bar.bar &&
+		ENVVAR=env git --config-env=bar.bar=ENVVAR -c bar.bar=cmd config ${config_get} bar.bar
 	} >actual &&
 	test_cmp expect actual
 '
@@ -1494,7 +1515,7 @@ test_expect_success '--config-env handles keys with equals' '
 	echo value=with=equals >expect &&
 	ENVVAR=value=with=equals git \
 		--config-env=section.subsection=with=equals.key=ENVVAR \
-		config section.subsection=with=equals.key >actual &&
+		config ${config_get} section.subsection=with=equals.key >actual &&
 	test_cmp expect actual
 '
 
@@ -1502,7 +1523,7 @@ test_expect_success 'git config handles environment config pairs' '
 	GIT_CONFIG_COUNT=2 \
 		GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="foo" \
 		GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="bar" \
-		git config --get-regexp "pair.*" >actual &&
+		git config ${mode_prefix}get-regexp "pair.*" >actual &&
 	cat >expect <<-EOF &&
 	pair.one foo
 	pair.two bar
@@ -1520,7 +1541,7 @@ test_expect_success 'git config ignores pairs exceeding count' '
 	GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
 		GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="value" \
-		git config --get-regexp "pair.*" >actual 2>error &&
+		git config ${mode_prefix}get-regexp "pair.*" >actual 2>error &&
 	cat >expect <<-EOF &&
 	pair.one value
 	EOF
@@ -1531,43 +1552,43 @@ test_expect_success 'git config ignores pairs exceeding count' '
 test_expect_success 'git config ignores pairs with zero count' '
 	test_must_fail env \
 		GIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${config_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
 test_expect_success 'git config ignores pairs with empty count' '
 	test_must_fail env \
 		GIT_CONFIG_COUNT= GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${config_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
 test_expect_success 'git config fails with invalid count' '
-	test_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&
+	test_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&
 	test_grep "bogus count" error &&
-	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&
+	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&
 	test_grep "too many entries" error
 '
 
 test_expect_success 'git config fails with missing config key' '
 	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0="value" \
-		git config --list 2>error &&
+		git config ${mode_prefix}list 2>error &&
 	test_grep "missing config key" error
 '
 
 test_expect_success 'git config fails with missing config value' '
 	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="pair.one" \
-		git config --list 2>error &&
+		git config ${mode_prefix}list 2>error &&
 	test_grep "missing config value" error
 '
 
 test_expect_success 'git config fails with invalid config pair key' '
 	test_must_fail env GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \
-		git config --list &&
+		git config ${mode_prefix}list &&
 	test_must_fail env GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \
-		git config --list
+		git config ${mode_prefix}list
 '
 
 test_expect_success 'environment overrides config file' '
@@ -1577,7 +1598,7 @@ test_expect_success 'environment overrides config file' '
 	one = value
 	EOF
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \
-		git config pair.one >actual &&
+		git config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1587,7 +1608,7 @@ test_expect_success 'environment overrides config file' '
 test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \
 		GIT_CONFIG_PARAMETERS="${SQ}pair.one=override${SQ}" \
-		git config pair.one >actual &&
+		git config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1596,7 +1617,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '
 
 test_expect_success 'command line overrides environment config' '
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \
-		git -c pair.one=override config pair.one >actual &&
+		git -c pair.one=override config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1606,8 +1627,8 @@ test_expect_success 'command line overrides environment config' '
 test_expect_success 'git config --edit works' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
-	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
-	git config -f tmp --list >actual &&
+	GIT_EDITOR="echo [test]value=yes >" git config ${mode_prefix}edit -f tmp &&
+	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
 
@@ -1615,8 +1636,8 @@ test_expect_success 'git config --edit respects core.editor' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
 	test_config core.editor "echo [test]value=yes >" &&
-	git config -f tmp --edit &&
-	git config -f tmp --list >actual &&
+	git config ${mode_prefix}edit -f tmp &&
+	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
 
@@ -1627,7 +1648,7 @@ test_expect_success 'barf on syntax error' '
 	[section]
 	key garbage
 	EOF
-	test_must_fail git config --get section.key 2>error &&
+	test_must_fail git config ${mode_prefix}get section.key 2>error &&
 	test_grep " line 3 " error
 '
 
@@ -1637,7 +1658,7 @@ test_expect_success 'barf on incomplete section header' '
 	[section
 	key = value
 	EOF
-	test_must_fail git config --get section.key 2>error &&
+	test_must_fail git config ${mode_prefix}get section.key 2>error &&
 	test_grep " line 2 " error
 '
 
@@ -1647,7 +1668,7 @@ test_expect_success 'barf on incomplete string' '
 	[section]
 	key = "value string
 	EOF
-	test_must_fail git config --get section.key 2>error &&
+	test_must_fail git config ${mode_prefix}get section.key 2>error &&
 	test_grep " line 3 " error
 '
 
@@ -1660,22 +1681,22 @@ test_expect_success 'urlmatch' '
 		cookieFile = /tmp/cookie.txt
 	EOF
 
-	test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&
+	test_expect_code 1 git config ${mode_prefix}get-urlmatch --bool doesnt.exist https://good.example.com >actual &&
 	test_must_be_empty actual &&
 
 	echo true >expect &&
-	git config --bool --get-urlmatch http.SSLverify https://good.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch --bool http.SSLverify https://good.example.com >actual &&
 	test_cmp expect actual &&
 
 	echo false >expect &&
-	git config --bool --get-urlmatch http.sslverify https://weak.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch --bool http.sslverify https://weak.example.com >actual &&
 	test_cmp expect actual &&
 
 	{
 		echo http.cookiefile /tmp/cookie.txt &&
 		echo http.sslverify false
 	} >expect &&
-	git config --get-urlmatch HTTP https://weak.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://weak.example.com >actual &&
 	test_cmp expect actual
 '
 
@@ -1690,7 +1711,7 @@ test_expect_success 'urlmatch with --show-scope' '
 	local	http.cookiefile /tmp/cookie.txt
 	local	http.sslverify false
 	EOF
-	git config --get-urlmatch --show-scope HTTP https://weak.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch --show-scope HTTP https://weak.example.com >actual &&
 	test_cmp expect actual
 '
 
@@ -1721,47 +1742,47 @@ test_expect_success 'urlmatch favors more specific URLs' '
 	EOF
 
 	echo http.cookiefile /tmp/root.txt >expect &&
-	git config --get-urlmatch HTTP https://example.com >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://example.com >actual &&
 	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
-	git config --get-urlmatch HTTP https://example.com/subdirectory >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://example.com/subdirectory >actual &&
 	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
-	git config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&
 	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/user.txt >expect &&
-	git config --get-urlmatch HTTP https://user@example.com/ >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://user@example.com/ >actual &&
 	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
-	git config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&
 	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/preceding.txt >expect &&
-	git config --get-urlmatch HTTP https://preceding.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://preceding.example.com >actual &&
 	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/wildcard.txt >expect &&
-	git config --get-urlmatch HTTP https://wildcard.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://wildcard.example.com >actual &&
 	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/sub.txt >expect &&
-	git config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&
 	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/trailing.txt >expect &&
-	git config --get-urlmatch HTTP https://trailing.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://trailing.example.com >actual &&
 	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/sub.txt >expect &&
-	git config --get-urlmatch HTTP https://user@sub.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://user@sub.example.com >actual &&
 	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/multiwildcard.txt >expect &&
-	git config --get-urlmatch HTTP https://wildcard.example.org >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://wildcard.example.org >actual &&
 	test_cmp expect actual
 '
 
@@ -1774,34 +1795,34 @@ test_expect_success 'urlmatch with wildcard' '
 		cookieFile = /tmp/cookie.txt
 	EOF
 
-	test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&
+	test_expect_code 1 git config ${mode_prefix}get-urlmatch --bool doesnt.exist https://good.example.com >actual &&
 	test_must_be_empty actual &&
 
 	echo true >expect &&
-	git config --bool --get-urlmatch http.SSLverify https://example.com >actual &&
+	git config ${mode_prefix}get-urlmatch --bool http.SSLverify https://example.com >actual &&
 	test_cmp expect actual &&
 
 	echo true >expect &&
-	git config --bool --get-urlmatch http.SSLverify https://good-example.com >actual &&
+	git config ${mode_prefix}get-urlmatch --bool http.SSLverify https://good-example.com >actual &&
 	test_cmp expect actual &&
 
 	echo true >expect &&
-	git config --bool --get-urlmatch http.sslverify https://deep.nested.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch --bool http.sslverify https://deep.nested.example.com >actual &&
 	test_cmp expect actual &&
 
 	echo false >expect &&
-	git config --bool --get-urlmatch http.sslverify https://good.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch --bool http.sslverify https://good.example.com >actual &&
 	test_cmp expect actual &&
 
 	{
 		echo http.cookiefile /tmp/cookie.txt &&
 		echo http.sslverify false
 	} >expect &&
-	git config --get-urlmatch HTTP https://good.example.com >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://good.example.com >actual &&
 	test_cmp expect actual &&
 
 	echo http.sslverify >expect &&
-	git config --get-urlmatch HTTP https://more.example.com.au >actual &&
+	git config ${mode_prefix}get-urlmatch HTTP https://more.example.com.au >actual &&
 	test_cmp expect actual
 '
 
@@ -1828,7 +1849,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	# please be careful when you update the above variable
 	EOF
 
-	git config --unset section.key &&
+	git config ${mode_prefix}unset section.key &&
 	test_cmp expect .git/config &&
 
 	cat >.git/config <<-\EOF &&
@@ -1841,7 +1862,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[next-section]
 	EOF
 
-	git config --unset section.key &&
+	git config ${mode_prefix}unset section.key &&
 	test_cmp expect .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1851,7 +1872,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[two]
 	key = true
 	EOF
-	git config --unset two.key &&
+	git config ${mode_prefix}unset two.key &&
 	! grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1861,7 +1882,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[one]
 	key = true
 	EOF
-	git config --unset-all one.key &&
+	git config ${mode_prefix}unset-all one.key &&
 	test_line_count = 0 .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1871,7 +1892,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[two]
 	Qkey = true
 	EOF
-	git config --unset two.key &&
+	git config ${mode_prefix}unset two.key &&
 	grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1883,7 +1904,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[TWO "subsection"]
 	[one]
 	EOF
-	git config --unset two.subsection.key &&
+	git config ${mode_prefix}unset two.subsection.key &&
 	test "not [two subsection]" = "$(git config one.key)" &&
 	test_line_count = 3 .git/config
 '
@@ -1895,7 +1916,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' '
 	key = value2
 	EOF
 
-	git config --unset-all section.key &&
+	git config ${mode_prefix}unset-all section.key &&
 	test_line_count = 0 .git/config
 '
 
@@ -1909,7 +1930,7 @@ test_expect_success 'adding a key into an empty section reuses header' '
 	Qkey = value
 	EOF
 
-	git config section.key value &&
+	git config ${mode_set} section.key value &&
 	test_cmp expect .git/config
 '
 
@@ -1918,7 +1939,7 @@ test_expect_success POSIXPERM,PERL 'preserves existing permissions' '
 	git config imap.pass Hunter2 &&
 	perl -e \
 	  "die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600" &&
-	git config --rename-section imap pop &&
+	git config ${mode_prefix}rename-section imap pop &&
 	perl -e \
 	  "die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600"
 '
@@ -1967,7 +1988,7 @@ test_expect_success '--show-origin with --list' '
 	command line:	user.cmdline=true
 	EOF
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\
-		git -c user.cmdline=true config --list --show-origin >output &&
+		git -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -1984,7 +2005,7 @@ test_expect_success '--show-origin with --list --null' '
 	includeQcommand line:Quser.cmdline
 	trueQ
 	EOF
-	git -c user.cmdline=true config --null --list --show-origin >output.raw &&
+	git -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&
 	nul_to_q <output.raw >output &&
 	# The here-doc above adds a newline that the --null output would not
 	# include. Add it here to make the two comparable.
@@ -1998,7 +2019,7 @@ test_expect_success '--show-origin with single file' '
 	file:.git/config	user.override=local
 	file:.git/config	include.path=../include/relative.include
 	EOF
-	git config --local --list --show-origin >output &&
+	git config ${mode_prefix}list --local --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2007,7 +2028,7 @@ test_expect_success '--show-origin with --get-regexp' '
 	file:$HOME/.gitconfig	user.global true
 	file:.git/config	user.local true
 	EOF
-	git config --show-origin --get-regexp "user\.[g|l].*" >output &&
+	git config ${mode_prefix}get-regexp --show-origin "user\.[g|l].*" >output &&
 	test_cmp expect output
 '
 
@@ -2015,7 +2036,7 @@ test_expect_success '--show-origin getting a single key' '
 	cat >expect <<-\EOF &&
 	file:.git/config	local
 	EOF
-	git config --show-origin user.override >output &&
+	git config ${mode_get} --show-origin user.override >output &&
 	test_cmp expect output
 '
 
@@ -2036,7 +2057,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '
 	cat >expect <<-\EOF &&
 	file:"file\" (dq) and spaces.conf"	user.custom=true
 	EOF
-	git config --file "$WEIRDLY_NAMED_FILE" --show-origin --list >output &&
+	git config ${mode_prefix}list --file "$WEIRDLY_NAMED_FILE" --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2044,7 +2065,7 @@ test_expect_success '--show-origin stdin' '
 	cat >expect <<-\EOF &&
 	standard input:	user.custom=true
 	EOF
-	git config --file - --show-origin --list <"$CUSTOM_CONFIG_FILE" >output &&
+	git config ${mode_prefix}list --file - --show-origin <"$CUSTOM_CONFIG_FILE" >output &&
 	test_cmp expect output
 '
 
@@ -2057,7 +2078,7 @@ test_expect_success '--show-origin stdin with file include' '
 	file:$INCLUDE_DIR/stdin.include	include
 	EOF
 	echo "[include]path=\"$INCLUDE_DIR\"/stdin.include" |
-	git config --show-origin --includes --file - user.stdin >output &&
+	git config ${mode_get} --show-origin --includes --file - user.stdin >output &&
 
 	test_cmp expect output
 '
@@ -2071,7 +2092,7 @@ test_expect_success '--show-origin blob' '
 		cat >expect <<-EOF &&
 		blob:$blob	user.custom=true
 		EOF
-		git config --blob=$blob --show-origin --list >output &&
+		git config ${mode_prefix}list --blob=$blob --show-origin >output &&
 		test_cmp expect output
 	)
 '
@@ -2087,13 +2108,13 @@ test_expect_success '--show-origin blob ref' '
 		cp "$CUSTOM_CONFIG_FILE" custom.conf &&
 		git add custom.conf &&
 		git commit -m "new config file" &&
-		git config --blob=main:custom.conf --show-origin --list >output &&
+		git config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&
 		test_cmp expect output
 	)
 '
 
 test_expect_success '--show-origin with --default' '
-	git config --show-origin --default foo some.key >actual &&
+	git config ${mode_get} --show-origin --default foo some.key >actual &&
 	echo "command line:	foo" >expect &&
 	test_cmp expect actual
 '
@@ -2113,13 +2134,14 @@ test_expect_success '--show-scope with --list' '
 	worktree	user.worktree=true
 	command	user.cmdline=true
 	EOF
+	test_when_finished "git worktree remove wt1" &&
 	git worktree add wt1 &&
 	# We need these to test for worktree scope, but outside of this
 	# test, this is just noise
 	test_config core.repositoryformatversion 1 &&
 	test_config extensions.worktreeConfig true &&
-	git config --worktree user.worktree true &&
-	git -c user.cmdline=true config --list --show-scope >output &&
+	git config ${mode_get} --worktree user.worktree true &&
+	git -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2128,7 +2150,7 @@ test_expect_success !MINGW '--show-scope with --blob' '
 	cat >expect <<-EOF &&
 	command	user.custom=true
 	EOF
-	git config --blob=$blob --show-scope --list >output &&
+	git config ${mode_prefix}list --blob=$blob --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2138,7 +2160,7 @@ test_expect_success '--show-scope with --local' '
 	local	user.override=local
 	local	include.path=../include/relative.include
 	EOF
-	git config --local --list --show-scope >output &&
+	git config ${mode_prefix}list --local --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2146,7 +2168,7 @@ test_expect_success '--show-scope getting a single value' '
 	cat >expect <<-\EOF &&
 	local	true
 	EOF
-	git config --show-scope --get user.local >output &&
+	git config ${mode_prefix}get --show-scope user.local >output &&
 	test_cmp expect output
 '
 
@@ -2162,12 +2184,12 @@ test_expect_success '--show-scope with --show-origin' '
 	local	file:.git/../include/relative.include	user.relative=include
 	command	command line:	user.cmdline=true
 	EOF
-	git -c user.cmdline=true config --list --show-origin --show-scope >output &&
+	git -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&
 	test_cmp expect output
 '
 
 test_expect_success '--show-scope with --default' '
-	git config --show-scope --default foo some.key >actual &&
+	git config ${mode_get} --show-scope --default foo some.key >actual &&
 	echo "command	foo" >expect &&
 	test_cmp expect actual
 '
@@ -2203,7 +2225,7 @@ test_expect_success 'override global and system config' '
 	global	home.config=true
 	local	local.config=true
 	EOF
-	git config --show-scope --list >output &&
+	git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output &&
 
 	cat >expect <<-EOF &&
@@ -2212,20 +2234,20 @@ test_expect_success 'override global and system config' '
 	local	local.config=true
 	EOF
 	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \
-		git config --show-scope --list >output &&
+		git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output &&
 
 	cat >expect <<-EOF &&
 	local	local.config=true
 	EOF
 	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \
-		git config --show-scope --list >output &&
+		git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'override global and system config with missing file' '
-	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&
-	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&
+	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&
+	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&
 	GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version
 '
 
@@ -2251,10 +2273,10 @@ test_expect_success 'write to overridden global and system config' '
 	key = value
 EOF
 
-	GIT_CONFIG_GLOBAL=write-to-global git config --global config.key value &&
+	GIT_CONFIG_GLOBAL=write-to-global git config ${mode_set} --global config.key value &&
 	test_cmp expect write-to-global &&
 
-	GIT_CONFIG_SYSTEM=write-to-system git config --system config.key value &&
+	GIT_CONFIG_SYSTEM=write-to-system git config ${mode_set} --system config.key value &&
 	test_cmp expect write-to-system
 '
 
@@ -2263,7 +2285,7 @@ do
 	test_expect_success "$opt requires a repo" '
 		# we expect 128 to ensure that we do not simply
 		# fail to find anything and return code "1"
-		test_expect_code 128 nongit git config $opt foo.bar
+		test_expect_code 128 nongit git config ${mode_get} $opt foo.bar
 	'
 done
 
@@ -2287,17 +2309,17 @@ test_expect_success 'identical mixed --type specifiers are allowed' '
 '
 
 test_expect_success 'non-identical modern --type specifiers are not allowed' '
-	test_must_fail git config --type=int --type=bool section.big 2>error &&
+	test_must_fail git config ${mode_get} --type=int --type=bool section.big 2>error &&
 	test_grep "only one type at a time" error
 '
 
 test_expect_success 'non-identical legacy --type specifiers are not allowed' '
-	test_must_fail git config --int --bool section.big 2>error &&
+	test_must_fail git config ${mode_get} --int --bool section.big 2>error &&
 	test_grep "only one type at a time" error
 '
 
 test_expect_success 'non-identical mixed --type specifiers are not allowed' '
-	test_must_fail git config --type=int --bool section.big 2>error &&
+	test_must_fail git config ${mode_get} --type=int --bool section.big 2>error &&
 	test_grep "only one type at a time" error
 '
 
@@ -2314,12 +2336,12 @@ test_expect_success 'unset type specifiers may be reset to conflicting ones' '
 '
 
 test_expect_success '--type rejects unknown specifiers' '
-	test_must_fail git config --type=nonsense section.foo 2>error &&
+	test_must_fail git config ${mode_get} --type=nonsense section.foo 2>error &&
 	test_grep "unrecognized --type argument" error
 '
 
 test_expect_success '--type=int requires at least one digit' '
-	test_must_fail git config --type int --default m some.key >out 2>error &&
+	test_must_fail git config ${mode_get} --type int --default m some.key >out 2>error &&
 	grep "bad numeric config value" error &&
 	test_must_be_empty out
 '
@@ -2341,18 +2363,18 @@ test_expect_success '--replace-all does not invent newlines' '
 	[abc]
 	Qkey = b
 	EOF
-	git config --replace-all abc.key b &&
+	git config ${mode_prefix}replace-all abc.key b &&
 	test_cmp expect .git/config
 '
 
 test_expect_success 'set all config with value-pattern' '
 	test_when_finished rm -f config initial &&
-	git config --file=initial abc.key one &&
+	git config ${mode_set} --file=initial abc.key one &&
 
 	# no match => add new entry
 	cp initial config &&
-	git config --file=config abc.key two a+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_set_all} --file=config abc.key two a+ &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2360,12 +2382,12 @@ test_expect_success 'set all config with value-pattern' '
 	test_cmp expect actual &&
 
 	# multiple matches => failure
-	test_must_fail git config --file=config abc.key three o+ 2>err &&
+	test_must_fail git config ${mode_set_all} --file=config abc.key three o+ 2>err &&
 	test_grep "has multiple values" err &&
 
 	# multiple values, no match => add
-	git config --file=config abc.key three a+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_set_all} --file=config abc.key three a+ &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2374,8 +2396,8 @@ test_expect_success 'set all config with value-pattern' '
 	test_cmp expect actual &&
 
 	# single match => replace
-	git config --file=config abc.key four h+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_set_all} --file=config abc.key four h+ &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2386,11 +2408,11 @@ test_expect_success 'set all config with value-pattern' '
 
 test_expect_success '--replace-all and value-pattern' '
 	test_when_finished rm -f config &&
-	git config --file=config --add abc.key one &&
-	git config --file=config --add abc.key two &&
-	git config --file=config --add abc.key three &&
-	git config --file=config --replace-all abc.key four "o+" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}add --file=config abc.key one &&
+	git config ${mode_prefix}add --file=config abc.key two &&
+	git config ${mode_prefix}add --file=config abc.key three &&
+	git config ${mode_prefix}replace-all --file=config abc.key four "o+" &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=four
 	abc.key=three
@@ -2403,33 +2425,33 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	git config --file=config dev.null bogus &&
 
 	# These modes do not allow --fixed-value at all
-	test_must_fail git config --file=config --fixed-value --add dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --rename-section dev null &&
-	test_must_fail git config --file=config --fixed-value --remove-section dev &&
-	test_must_fail git config --file=config --fixed-value --list &&
-	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
+	test_must_fail git config ${mode_prefix}add --file=config --fixed-value dev.null bogus &&
+	test_must_fail git config ${mode_prefix}get-urlmatch --file=config --fixed-value dev.null bogus &&
+	test_must_fail git config ${mode_prefix}get-urlmatch --file=config --fixed-value dev.null bogus &&
+	test_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&
+	test_must_fail git config ${mode_prefix}remove-section --file=config --fixed-value dev &&
+	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
+	test_must_fail git config ${mode_prefix}get-color --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_prefix}get-colorbool --file=config --fixed-value dev.null &&
 
 	# These modes complain when --fixed-value has no value-pattern
-	test_must_fail git config --file=config --fixed-value dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --get dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-all dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-regexp "dev.*" &&
-	test_must_fail git config --file=config --fixed-value --unset dev.null &&
-	test_must_fail git config --file=config --fixed-value --unset-all dev.null
+	test_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&
+	test_must_fail git config ${mode_prefix}replace-all --file=config --fixed-value dev.null bogus &&
+	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_prefix}get-all --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_prefix}get-regexp --file=config --fixed-value "dev.*" &&
+	test_must_fail git config ${mode_prefix}unset --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_prefix}unset-all --file=config --fixed-value dev.null
 '
 
 test_expect_success '--fixed-value uses exact string matching' '
 	test_when_finished rm -f config initial &&
 	META="a+b*c?d[e]f.g" &&
-	git config --file=initial fixed.test "$META" &&
+	git config ${mode_set} --file=initial fixed.test "$META" &&
 
 	cp initial config &&
-	git config --file=config fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_set_all} --file=config fixed.test bogus "$META" &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
 	fixed.test=bogus
@@ -2437,34 +2459,34 @@ test_expect_success '--fixed-value uses exact string matching' '
 	test_cmp expect actual &&
 
 	cp initial config &&
-	git config --file=config --fixed-value fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_set_all} --file=config --fixed-value fixed.test bogus "$META" &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	fixed.test=bogus
 	EOF
 	test_cmp expect actual &&
 
 	cp initial config &&
-	test_must_fail git config --file=config --unset fixed.test "$META" &&
-	git config --file=config --fixed-value --unset fixed.test "$META" &&
-	test_must_fail git config --file=config fixed.test &&
+	test_must_fail git config ${mode_prefix}unset --file=config fixed.test "$META" &&
+	git config ${mode_prefix}unset --file=config --fixed-value fixed.test "$META" &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
-	test_must_fail git config --file=config --unset-all fixed.test "$META" &&
-	git config --file=config --fixed-value --unset-all fixed.test "$META" &&
-	test_must_fail git config --file=config fixed.test &&
+	test_must_fail git config ${mode_prefix}unset-all --file=config fixed.test "$META" &&
+	git config ${mode_prefix}unset-all --file=config --fixed-value fixed.test "$META" &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
-	git config --file=config --replace-all fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}replace-all --file=config fixed.test bogus "$META" &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
 	fixed.test=bogus
 	EOF
 	test_cmp expect actual &&
 
-	git config --file=config --fixed-value --replace-all fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}replace-all --file=config --fixed-value fixed.test bogus "$META" &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=bogus
 	fixed.test=bogus
@@ -2475,23 +2497,23 @@ test_expect_success '--fixed-value uses exact string matching' '
 test_expect_success '--get and --get-all with --fixed-value' '
 	test_when_finished rm -f config &&
 	META="a+b*c?d[e]f.g" &&
-	git config --file=config fixed.test bogus &&
-	git config --file=config --add fixed.test "$META" &&
+	git config ${mode_set} --file=config fixed.test bogus &&
+	git config ${mode_prefix}add --file=config fixed.test "$META" &&
 
-	git config --file=config --get fixed.test bogus &&
-	test_must_fail git config --file=config --get fixed.test "$META" &&
-	git config --file=config --get --fixed-value fixed.test "$META" &&
-	test_must_fail git config --file=config --get --fixed-value fixed.test non-existent &&
+	git config ${mode_prefix}get --file=config fixed.test bogus &&
+	test_must_fail git config ${mode_prefix}get --file=config fixed.test "$META" &&
+	git config ${mode_prefix}get --file=config --fixed-value fixed.test "$META" &&
+	test_must_fail git config ${mode_prefix}get --file=config --fixed-value fixed.test non-existent &&
 
-	git config --file=config --get-all fixed.test bogus &&
-	test_must_fail git config --file=config --get-all fixed.test "$META" &&
-	git config --file=config --get-all --fixed-value fixed.test "$META" &&
-	test_must_fail git config --file=config --get-all --fixed-value fixed.test non-existent &&
+	git config ${mode_prefix}get-all --file=config fixed.test bogus &&
+	test_must_fail git config ${mode_prefix}get-all --file=config fixed.test "$META" &&
+	git config ${mode_prefix}get-all --file=config --fixed-value fixed.test "$META" &&
+	test_must_fail git config ${mode_prefix}get-all --file=config --fixed-value fixed.test non-existent &&
 
-	git config --file=config --get-regexp fixed+ bogus &&
-	test_must_fail git config --file=config --get-regexp fixed+ "$META" &&
-	git config --file=config --get-regexp --fixed-value fixed+ "$META" &&
-	test_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent
+	git config ${mode_prefix}get-regexp --file=config fixed+ bogus &&
+	test_must_fail git config ${mode_prefix}get-regexp --file=config fixed+ "$META" &&
+	git config ${mode_prefix}get-regexp --file=config --fixed-value fixed+ "$META" &&
+	test_must_fail git config ${mode_prefix}get-regexp --file=config --fixed-value fixed+ non-existent
 '
 
 test_expect_success 'includeIf.hasconfig:remote.*.url' '
@@ -2516,10 +2538,10 @@ test_expect_success 'includeIf.hasconfig:remote.*.url' '
 	EOF
 
 	echo this-is-included >expect-this &&
-	git -C hasremoteurlTest config --get user.this >actual-this &&
+	git -C hasremoteurlTest config ${mode_prefix}get user.this >actual-this &&
 	test_cmp expect-this actual-this &&
 
-	test_must_fail git -C hasremoteurlTest config --get user.that
+	test_must_fail git -C hasremoteurlTest config ${mode_prefix}get user.that
 '
 
 test_expect_success 'includeIf.hasconfig:remote.*.url respects last-config-wins' '
@@ -2546,13 +2568,13 @@ test_expect_success 'includeIf.hasconfig:remote.*.url respects last-config-wins'
 	echo main-config >expect-main-config &&
 	echo included-config >expect-included-config &&
 
-	git -C hasremoteurlTest config --get user.one >actual &&
+	git -C hasremoteurlTest config ${mode_prefix}get user.one >actual &&
 	test_cmp expect-main-config actual &&
 
-	git -C hasremoteurlTest config --get user.two >actual &&
+	git -C hasremoteurlTest config ${mode_prefix}get user.two >actual &&
 	test_cmp expect-included-config actual &&
 
-	git -C hasremoteurlTest config --get user.three >actual &&
+	git -C hasremoteurlTest config ${mode_prefix}get user.three >actual &&
 	test_cmp expect-main-config actual
 '
 
@@ -2587,11 +2609,11 @@ test_expect_success 'includeIf.hasconfig:remote.*.url globs' '
 		path = "$(pwd)/no"
 	EOF
 
-	git -C hasremoteurlTest config --get user.dss &&
-	git -C hasremoteurlTest config --get user.dse &&
-	git -C hasremoteurlTest config --get user.dsm &&
-	git -C hasremoteurlTest config --get user.ssm &&
-	test_must_fail git -C hasremoteurlTest config --get user.no
+	git -C hasremoteurlTest config ${mode_prefix}get user.dss &&
+	git -C hasremoteurlTest config ${mode_prefix}get user.dse &&
+	git -C hasremoteurlTest config ${mode_prefix}get user.dsm &&
+	git -C hasremoteurlTest config ${mode_prefix}get user.ssm &&
+	test_must_fail git -C hasremoteurlTest config ${mode_prefix}get user.no
 '
 
 test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such included files' '
@@ -2625,4 +2647,11 @@ test_expect_success 'specifying multiple modes causes failure' '
 	test_cmp expect err
 '
 
+test_expect_success 'specifying mode and subcommand causes failure' '
+	test_must_fail git config get --get 2>err &&
+	grep "subcommand and action modes are incompatible" err
+'
+
+done
+
 test_done
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH 8/8] Documentation/git-config: update to new-style syntax
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (6 preceding siblings ...)
  2024-03-06 11:31 ` [PATCH 7/8] t1300: exercise both old- and new-style modes Patrick Steinhardt
@ 2024-03-06 11:32 ` Patrick Steinhardt
  2024-03-07  6:57   ` Eric Sunshine
  2024-03-06 17:06 ` [PATCH 0/8] builtin/config: introduce subcommands Junio C Hamano
                   ` (5 subsequent siblings)
  13 siblings, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-06 11:32 UTC (permalink / raw)
  To: git

[-- Attachment #1: Type: text/plain, Size: 21396 bytes --]

Update our documentation of git-config(1) to stop mentioning the old
syntax while starting to mention the new syntax. Remove the help
mismatch in t0450 so that we start to ensure that the manpage and
builtin synopsis match.

Note that there will still be lots of mentions of the old style syntax
of git-config(1) throughout our documentation. This documentation will
be updated over time.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 204 ++++++++++++++++++-----------------
 builtin/config.c             |  46 +++++---
 t/t0450/txt-help-mismatches  |   1 -
 3 files changed, 137 insertions(+), 114 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index dff39093b5..edf67c85f7 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -9,21 +9,22 @@ git-config - Get and set repository or global options
 SYNOPSIS
 --------
 [verse]
-'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
-'git config' [<file-option>] [--type=<type>] --add <name> <value>
-'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL>
-'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
-'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
-'git config' [<file-option>] --rename-section <old-name> <new-name>
-'git config' [<file-option>] --remove-section <name>
-'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list
-'git config' [<file-option>] --get-color <name> [<default>]
-'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
-'git config' [<file-option>] -e | --edit
+'git config list' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only]
+'git config get' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] <name> [<value-pattern>]
+'git config get-all' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] <name> [<value-pattern>]
+'git config get-color' [<file-option>] <name> [<default>]
+'git config get-colorbool' [<file-option>] <name> [<stdout-is-tty>]
+'git config get-regexp' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] <name-regex> [<value-pattern>]
+'git config get-urlmatch' [<file-option>] [--type=<type>] [-z|--null] <name> <URL>
+'git config add' [<file-option>] [--type=<type>] <name> <value>
+'git config set' [<file-option>] [--type=<type>] <name> <value>
+'git config set-all' [<file-option>] [--type=<type>] [--fixed-value] <name> <value> [<value-pattern>]
+'git config unset' [<file-option>] [--fixed-value] <name> [<value-pattern>]
+'git config unset-all' [<file-option>] [--fixed-value] <name> [<value-pattern>]
+'git config replace-all' [<file-option>] [--type=<type>] [--fixed-value] <name> <value> [<value-pattern>]
+'git config rename-section' [<file-option>] <old-name> <new-name>
+'git config remove-section' [<file-option>] <name>
+'git config edit' [<file-option>]
 
 DESCRIPTION
 -----------
@@ -74,35 +75,50 @@ On success, the command returns the exit code 0.
 A list of all available configuration variables can be obtained using the
 `git help --config` command.
 
-[[OPTIONS]]
-OPTIONS
--------
-
---replace-all::
-	Default behavior is to replace at most one line. This replaces
-	all lines matching the key (and optionally the `value-pattern`).
+COMMANDS
+--------
 
---add::
-	Adds a new line to the option without altering any existing
-	values.  This is the same as providing '^$' as the `value-pattern`
-	in `--replace-all`.
+list::
+	List all variables set in config file, along with their values.
 
---get::
+get <name> [<value-pattern>]::
 	Get the value for a given key (optionally filtered by a regex
 	matching the value). Returns error code 1 if the key was not
 	found and the last value if multiple key values were found.
 
---get-all::
+get-all <name> [<value-pattern>]::
 	Like get, but returns all values for a multi-valued key.
 
---get-regexp::
-	Like --get-all, but interprets the name as a regular expression and
+get-color <name> [<default>]::
+
+	Find the color configured for `name` (e.g. `color.diff.new`) and
+	output it as the ANSI color escape sequence to the standard
+	output.  The optional `default` parameter is used instead, if
+	there is no color configured for `name`.
++
+`--type=color [--default=<default>]` is preferred over `get-color`
+(but note that `get-color` will omit the trailing newline printed by
+`--type=color`).
+
+get-colorbool <name> [<stdout-is-tty>]::
+
+	Find the color setting for `<name>` (e.g. `color.diff`) and output
+	"true" or "false".  `<stdout-is-tty>` should be either "true" or
+	"false", and is taken into account when configuration says
+	"auto".  If `<stdout-is-tty>` is missing, then checks the standard
+	output of the command itself, and exits with status 0 if color
+	is to be used, or exits with status 1 otherwise.
+	When the color setting for `name` is undefined, the command uses
+	`color.ui` as fallback.
+
+get-regex <name-regex> [<value-pattern>]::
+	Like `get-all`, but interprets the name as a regular expression and
 	writes out the key names.  Regular expression matching is currently
 	case-sensitive and done against a canonicalized version of the key
 	in which section and variable names are lowercased, but subsection
 	names are not.
 
---get-urlmatch <name> <URL>::
+get-urlmatch <name> <URL>::
 	When given a two-part <name> as <section>.<key>, the value for
 	<section>.<URL>.<key> whose <URL> part matches the best to the
 	given URL is returned (if no such key exists, the value for
@@ -110,6 +126,43 @@ OPTIONS
 	<section> as name, do so for all the keys in the section and
 	list them.  Returns error code 1 if no value is found.
 
+add <name> <value>::
+	Adds a new line to the option without altering any existing
+	values.  This is the same as providing '^$' as the `value-pattern`
+	in `--replace-all`.
+
+set <name> <value>::
+	Set the config key to the given value. Adds a new key to the
+	configuration in case it does not yet exist. This subcommand fails when
+	the key has multiple values.
+
+set-all <name> <value> [<value-pattern>]::
+	Set all config keys to the given value.
+
+unset <name> [<value-pattern>]::
+	Remove the line matching the key from config file.
+
+unset-all <name> [<value-pattern>]::
+	Remove all lines matching the key from config file.
+
+replace-all <name> <value> [<value-pattern>]::
+	Default behavior is to replace at most one line. This replaces
+	all lines matching the key (and optionally the `value-pattern`).
+
+remove-section <name>::
+	Remove the given section from the configuration file.
+
+rename-section <old-name> <new-name>::
+	Rename the given section to a new name.
+
+edit::
+	Opens an editor to modify the specified config file; either
+	`--system`, `--global`, or repository (default).
+
+[[OPTIONS]]
+OPTIONS
+-------
+
 --global::
 	For writing options: write to global `~/.gitconfig` file
 	rather than the repository `.git/config`, write to
@@ -166,22 +219,6 @@ See also <<FILES>>.
 	section in linkgit:gitrevisions[7] for a more complete list of
 	ways to spell blob names.
 
---remove-section::
-	Remove the given section from the configuration file.
-
---rename-section::
-	Rename the given section to a new name.
-
---unset::
-	Remove the line matching the key from config file.
-
---unset-all::
-	Remove all lines matching the key from config file.
-
--l::
---list::
-	List all variables set in config file, along with their values.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -236,8 +273,8 @@ Valid `<type>`'s include:
 	contain line breaks.
 
 --name-only::
-	Output only the names of config variables for `--list` or
-	`--get-regexp`.
+	Output only the names of config variables for `list` or
+	`get-regexp`.
 
 --show-origin::
 	Augment the output of all queried config options with the
@@ -250,33 +287,6 @@ Valid `<type>`'s include:
 	all queried config options with the scope of that value
 	(worktree, local, global, system, command).
 
---get-colorbool <name> [<stdout-is-tty>]::
-
-	Find the color setting for `<name>` (e.g. `color.diff`) and output
-	"true" or "false".  `<stdout-is-tty>` should be either "true" or
-	"false", and is taken into account when configuration says
-	"auto".  If `<stdout-is-tty>` is missing, then checks the standard
-	output of the command itself, and exits with status 0 if color
-	is to be used, or exits with status 1 otherwise.
-	When the color setting for `name` is undefined, the command uses
-	`color.ui` as fallback.
-
---get-color <name> [<default>]::
-
-	Find the color configured for `name` (e.g. `color.diff.new`) and
-	output it as the ANSI color escape sequence to the standard
-	output.  The optional `default` parameter is used instead, if
-	there is no color configured for `name`.
-+
-`--type=color [--default=<default>]` is preferred over `--get-color`
-(but note that `--get-color` will omit the trailing newline printed by
-`--type=color`).
-
--e::
---edit::
-	Opens an editor to modify the specified config file; either
-	`--system`, `--global`, or repository (default).
-
 --[no-]includes::
 	Respect `include.*` directives in config files when looking up
 	values. Defaults to `off` when a specific file is given (e.g.,
@@ -284,13 +294,13 @@ Valid `<type>`'s include:
 	config files.
 
 --default <value>::
-  When using `--get`, and the requested variable is not found, behave as if
+  When using `get`, and the requested variable is not found, behave as if
   <value> were the value assigned to the that variable.
 
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
-using `--list` or any of the `--get-*` which may return multiple results.
+using `list` or any of the `get-*` subcommands which may return multiple results.
 The default is to use a pager.
 
 [[FILES]]
@@ -333,8 +343,8 @@ precedence over values read earlier.  When multiple values are taken then all
 values of a key from all files will be used.
 
 By default, options are only written to the repository specific
-configuration file. Note that this also affects options like `--replace-all`
-and `--unset`. *'git config' will only ever change one file at a time*.
+configuration file. Note that this also affects subcommands like `replace-all`
+and `unset`. *'git config' will only ever change one file at a time*.
 
 You can limit which configuration sources are read from or written to by
 specifying the path of a file with the `--file` option, or by specifying a
@@ -469,7 +479,7 @@ Given a .git/config like this:
 you can set the filemode to true with
 
 ------------
-% git config core.filemode true
+% git config set core.filemode true
 ------------
 
 The hypothetical proxy command entries actually have a postfix to discern
@@ -477,7 +487,7 @@ what URL they apply to. Here is how to change the entry for kernel.org
 to "ssh".
 
 ------------
-% git config core.gitproxy '"ssh" for kernel.org' 'for kernel.org$'
+% git config set core.gitproxy '"ssh" for kernel.org' 'for kernel.org$'
 ------------
 
 This makes sure that only the key/value pair for kernel.org is replaced.
@@ -485,7 +495,7 @@ This makes sure that only the key/value pair for kernel.org is replaced.
 To delete the entry for renames, do
 
 ------------
-% git config --unset diff.renames
+% git config unset diff.renames
 ------------
 
 If you want to delete an entry for a multivar (like core.gitproxy above),
@@ -494,51 +504,51 @@ you have to provide a regex matching the value of exactly one line.
 To query the value for a given key, do
 
 ------------
-% git config --get core.filemode
+% git config get core.filemode
 ------------
 
 or
 
 ------------
-% git config core.filemode
+% git config get core.filemode
 ------------
 
 or, to query a multivar:
 
 ------------
-% git config --get core.gitproxy "for kernel.org$"
+% git config get core.gitproxy "for kernel.org$"
 ------------
 
 If you want to know all the values for a multivar, do:
 
 ------------
-% git config --get-all core.gitproxy
+% git config get-all core.gitproxy
 ------------
 
 If you like to live dangerously, you can replace *all* core.gitproxy by a
 new one with
 
 ------------
-% git config --replace-all core.gitproxy ssh
+% git config replace-all core.gitproxy ssh
 ------------
 
 However, if you really only want to replace the line for the default proxy,
 i.e. the one without a "for ..." postfix, do something like this:
 
 ------------
-% git config core.gitproxy ssh '! for '
+% git config get core.gitproxy ssh '! for '
 ------------
 
 To actually match only values with an exclamation mark, you have to
 
 ------------
-% git config section.key value '[!]'
+% git config get section.key value '[!]'
 ------------
 
 To add a new proxy, without altering any of the existing ones, use
 
 ------------
-% git config --add core.gitproxy '"proxy-command" for example.com'
+% git config add core.gitproxy '"proxy-command" for example.com'
 ------------
 
 An example to use customized color from the configuration in your
@@ -546,8 +556,8 @@ script:
 
 ------------
 #!/bin/sh
-WS=$(git config --get-color color.diff.whitespace "blue reverse")
-RESET=$(git config --get-color "" "reset")
+WS=$(git config get-color color.diff.whitespace "blue reverse")
+RESET=$(git config get-color "" "reset")
 echo "${WS}your whitespace color or blue reverse${RESET}"
 ------------
 
@@ -555,11 +565,11 @@ For URLs in `https://weak.example.com`, `http.sslVerify` is set to
 false, while it is set to `true` for all others:
 
 ------------
-% git config --type=bool --get-urlmatch http.sslverify https://good.example.com
+% git config get-urlmatch --type=bool http.sslverify https://good.example.com
 true
-% git config --type=bool --get-urlmatch http.sslverify https://weak.example.com
+% git config get-urlmatch --type=bool http.sslverify https://weak.example.com
 false
-% git config --get-urlmatch http https://weak.example.com
+% git config get-urlmatch http https://weak.example.com
 http.cookieFile /tmp/cookie.txt
 http.sslverify false
 ------------
@@ -578,7 +588,7 @@ looks like
     key = value1
 --------
 
-and running `git config section.Subsection.key value2` will result in
+and running `git config add section.Subsection.key value2` will result in
 
 --------
   [section.subsection]
diff --git a/builtin/config.c b/builtin/config.c
index 10fa933931..61a9ad3eaa 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -16,7 +16,22 @@
 #include "worktree.h"
 
 static const char *const builtin_config_usage[] = {
-	N_("git config [<options>]"),
+	N_("git config list [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only]"),
+	N_("git config get [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] <name> [<value-pattern>]"),
+	N_("git config get-all [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] <name> [<value-pattern>]"),
+	N_("git config get-color [<file-option>] <name> [<default>]"),
+	N_("git config get-colorbool [<file-option>] <name> [<stdout-is-tty>]"),
+	N_("git config get-regexp [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] <name-regex> [<value-pattern>]"),
+	N_("git config get-urlmatch [<file-option>] [--type=<type>] [-z|--null] <name> <URL>"),
+	N_("git config add [<file-option>] [--type=<type>] <name> <value>"),
+	N_("git config set [<file-option>] [--type=<type>] <name> <value>"),
+	N_("git config set-all [<file-option>] [--type=<type>] [--fixed-value] <name> <value> [<value-pattern>]"),
+	N_("git config unset [<file-option>] [--fixed-value] <name> [<value-pattern>]"),
+	N_("git config unset-all [<file-option>] [--fixed-value] <name> [<value-pattern>]"),
+	N_("git config replace-all [<file-option>] [--type=<type>] [--fixed-value] <name> <value> [<value-pattern>]"),
+	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
+	N_("git config remove-section [<file-option>] <name>"),
+	N_("git config edit [<file-option>]"),
 	NULL
 };
 
@@ -887,21 +902,20 @@ static struct option builtin_config_options[] = {
 	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
-	OPT_GROUP(N_("Action")),
-	OPT_CMDMODE(0, "get", &action, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_CMDMODE(0, "get-all", &action, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_CMDMODE(0, "get-regexp", &action, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_CMDMODE(0, "get-urlmatch", &action, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_CMDMODE(0, "replace-all", &action, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_CMDMODE(0, "add", &action, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_CMDMODE(0, "unset", &action, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_CMDMODE(0, "unset-all", &action, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_CMDMODE(0, "rename-section", &action, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_CMDMODE(0, "remove-section", &action, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_CMDMODE('l', "list", &action, N_("list all"), ACTION_LIST),
-	OPT_CMDMODE('e', "edit", &action, N_("open an editor"), ACTION_EDIT),
-	OPT_CMDMODE(0, "get-color", &action, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_CMDMODE(0, "get-colorbool", &action, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_CMDMODE_F(0, "get", &action, N_("get value: name [value-pattern]"), ACTION_GET, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "get-all", &action, N_("get all values: key [value-pattern]"), ACTION_GET_ALL, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "get-regexp", &action, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "get-urlmatch", &action, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "replace-all", &action, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "add", &action, N_("add a new variable: name value"), ACTION_ADD, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "unset", &action, N_("remove a variable: name [value-pattern]"), ACTION_UNSET, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "unset-all", &action, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "rename-section", &action, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "remove-section", &action, N_("remove a section: name"), ACTION_REMOVE_SECTION, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F('l', "list", &action, N_("list all"), ACTION_LIST, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F('e', "edit", &action, N_("open an editor"), ACTION_EDIT, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "get-color", &action, N_("find the color configured: slot [default]"), ACTION_GET_COLOR, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "get-colorbool", &action, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL, PARSE_OPT_HIDDEN),
 	OPT_GROUP(N_("Type")),
 	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
 	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
diff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches
index a0777acd66..28003f18c9 100644
--- a/t/t0450/txt-help-mismatches
+++ b/t/t0450/txt-help-mismatches
@@ -10,7 +10,6 @@ checkout
 checkout-index
 clone
 column
-config
 credential
 credential-cache
 credential-store
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH 0/8] builtin/config: introduce subcommands
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (7 preceding siblings ...)
  2024-03-06 11:32 ` [PATCH 8/8] Documentation/git-config: update to new-style syntax Patrick Steinhardt
@ 2024-03-06 17:06 ` Junio C Hamano
  2024-03-06 23:46   ` Taylor Blau
  2024-03-06 22:49 ` Kristoffer Haugsbakk
                   ` (4 subsequent siblings)
  13 siblings, 1 reply; 113+ messages in thread
From: Junio C Hamano @ 2024-03-06 17:06 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git

Patrick Steinhardt <ps@pks.im> writes:

>   - `git config foo.bar` -> `git config get foo.bar`
>
>   - `git config foo.bar value` -> `git config set foo.bar value`

I actually have been perfectly OK with the above two, but I agree
that ...

>   - `git config foo.bar value value-pattern` -> `git config set-all
>     foo.bar value value-pattern`

... this was less than discoverable, and would be a good update.
This one ...

>   - `git config --get-urlmatch` -> `git config get-urlmatch`.

... is a Meh to me, personally.  I'd not actively push it
enthusiastically, but I'd passively accept its existence.

> Most importantly, this should help discoverability quite a lot by now
> also having a proper synopsis in both the manpage, but also in `git
> config -h`.
>
> Of course, backwards compatibility is a big concern. We don't want to
> just switch over to the new syntax and break all existing scripts and
> muscle memory. This patch series thus abuses the fact that the implicit
> modes (`git config foo.bar`, `git config foo.bar value` and `git config
> foo.bar value value-pattern`) all require a key as first argument. As
> keys _must_ have a dot, this allows us to unambiguously discern those
> from actual subcommands.

Clever ;-).

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

* Re: [PATCH 6/8] builtin/config: introduce subcommands
  2024-03-06 11:31 ` [PATCH 6/8] builtin/config: introduce subcommands Patrick Steinhardt
@ 2024-03-06 21:38   ` Karthik Nayak
  2024-03-07  7:14     ` Patrick Steinhardt
  0 siblings, 1 reply; 113+ messages in thread
From: Karthik Nayak @ 2024-03-06 21:38 UTC (permalink / raw)
  To: Patrick Steinhardt, git

[-- Attachment #1: Type: text/plain, Size: 1131 bytes --]

Patrick Steinhardt <ps@pks.im> writes:

> @@ -910,6 +930,20 @@ int cmd_config(int argc, const char **argv, const char *prefix)
>  {
>  	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
>
> +	/*
> +	 * This is somewhat hacky: we first parse the command line while
> +	 * keeping all args intact in order to determine whether a subcommand
> +	 * has been specified. If so, we re-parse it a second time, but this
> +	 * time we drop KEEP_ARGV0. This is so that we don't munge the command
> +	 * line in case no subcommand was given, which would otherwise confuse
> +	 * us when parsing the implicit modes.
> +	 */
> +	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
> +			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
> +	if (subcommand)
> +		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
> +				     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);
> +

I wonder if we can drop the PARSE_OPT_SUBCOMMAND_OPTIONAL in the second
iteration to make it stricter. But this is OK.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]

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

* Re: [PATCH 5/8] builtin/config: track subcommands by action
  2024-03-06 11:31 ` [PATCH 5/8] builtin/config: track subcommands by action Patrick Steinhardt
@ 2024-03-06 21:54   ` Jean-Noël AVILA
  2024-03-07  6:37     ` Patrick Steinhardt
  2024-03-07  0:10   ` Taylor Blau
  1 sibling, 1 reply; 113+ messages in thread
From: Jean-Noël AVILA @ 2024-03-06 21:54 UTC (permalink / raw)
  To: git, Patrick Steinhardt

Le mercredi 6 mars 2024, 12:31:50 CET Patrick Steinhardt a écrit :
> Part of `cmd_config()` is a rather unwieldy switch statement that
> invokes the correct subcommand function based on which action has been
> requested by the user. Now that we have converted actions to be tracked
> via a `OPT_CMDMODE()`, we know that the `actions` variable will
> only ever have at most one bit set. This allows us to convert the
> variable to use an `enum` instead, and thus to create an array that maps
> from this newly introduced `enum` to the corresponding subcommand
> function.
> 
> Refactor the code to do so. Besides allowing us to get rid of the giant
> switch statement, this refactoring will also make it easier to introduce
> proper subcommands to git-config(1).
> 
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>  builtin/config.c | 207 +++++++++++++++++++++++------------------------
>  1 file changed, 99 insertions(+), 108 deletions(-)
> 
> diff --git a/builtin/config.c b/builtin/config.c
> index a6ab9b8204..0d58397ef5 100644
> --- a/builtin/config.c
> +++ b/builtin/config.c
> @@ -20,6 +20,26 @@ static const char *const builtin_config_usage[] = {
>  	NULL
>  };
>  
> +enum config_action {
> +	ACTION_NONE,
> +	ACTION_GET,
> +	ACTION_GET_ALL,
> +	ACTION_GET_REGEXP,
> +	ACTION_REPLACE_ALL,
> +	ACTION_ADD,
> +	ACTION_UNSET,
> +	ACTION_UNSET_ALL,
> +	ACTION_RENAME_SECTION,
> +	ACTION_REMOVE_SECTION,
> +	ACTION_LIST,
> +	ACTION_EDIT,
> +	ACTION_SET,
> +	ACTION_SET_ALL,
> +	ACTION_GET_COLOR,
> +	ACTION_GET_COLORBOOL,
> +	ACTION_GET_URLMATCH,
> +};
> +
>  static char *key;
>  static regex_t *key_regexp;
>  static const char *value_pattern;
> @@ -33,10 +53,12 @@ static char delim = '=';
>  static char key_delim = ' ';
>  static char term = '\n';
>  
> +static parse_opt_subcommand_fn *subcommand;
> +static enum config_action action = ACTION_NONE;
>  static int use_global_config, use_system_config, use_local_config;
>  static int use_worktree_config;
>  static struct git_config_source given_config_source;
> -static int actions, type;
> +static int type;
>  static char *default_value;
>  static int end_nul;
>  static int respect_includes_opt = -1;
> @@ -46,30 +68,6 @@ static int show_scope;
>  static int fixed_value;
>  static int config_flags;
>  
> -#define ACTION_GET (1<<0)
> -#define ACTION_GET_ALL (1<<1)
> -#define ACTION_GET_REGEXP (1<<2)
> -#define ACTION_REPLACE_ALL (1<<3)
> -#define ACTION_ADD (1<<4)
> -#define ACTION_UNSET (1<<5)
> -#define ACTION_UNSET_ALL (1<<6)
> -#define ACTION_RENAME_SECTION (1<<7)
> -#define ACTION_REMOVE_SECTION (1<<8)
> -#define ACTION_LIST (1<<9)
> -#define ACTION_EDIT (1<<10)
> -#define ACTION_SET (1<<11)
> -#define ACTION_SET_ALL (1<<12)
> -#define ACTION_GET_COLOR (1<<13)
> -#define ACTION_GET_COLORBOOL (1<<14)
> -#define ACTION_GET_URLMATCH (1<<15)
> -
> -/*
> - * The actions "ACTION_LIST | ACTION_GET_*" which may produce more than
> - * one line of output and which should therefore be paged.
> - */
> -#define PAGING_ACTIONS (ACTION_LIST | ACTION_GET_ALL | \
> -			ACTION_GET_REGEXP | ACTION_GET_URLMATCH)
> -
>  #define TYPE_BOOL		1
>  #define TYPE_INT		2
>  #define TYPE_BOOL_OR_INT	3
> @@ -842,6 +840,25 @@ static int cmd_config_get_colorbool(int argc, const char **argv, const char *pre
>  	return get_colorbool(argv[0], argc == 2);
>  }
>  
> +static parse_opt_subcommand_fn *subcommands_by_action[] = {
> +	[ACTION_LIST] = cmd_config_list,
> +	[ACTION_EDIT] = cmd_config_edit,
> +	[ACTION_SET] = cmd_config_set,
> +	[ACTION_SET_ALL] = cmd_config_set_all,
> +	[ACTION_ADD] = cmd_config_add,
> +	[ACTION_REPLACE_ALL] = cmd_config_replace_all,
> +	[ACTION_GET] = cmd_config_get,
> +	[ACTION_GET_ALL] = cmd_config_get_all,
> +	[ACTION_GET_REGEXP] = cmd_config_get_regexp,
> +	[ACTION_GET_URLMATCH] = cmd_config_get_urlmatch,
> +	[ACTION_UNSET] = cmd_config_unset,
> +	[ACTION_UNSET_ALL] = cmd_config_unset_all,
> +	[ACTION_RENAME_SECTION] = cmd_config_rename_section,
> +	[ACTION_REMOVE_SECTION] = cmd_config_remove_section,
> +	[ACTION_GET_COLOR] = cmd_config_get_color,
> +	[ACTION_GET_COLORBOOL] = cmd_config_get_colorbool,
> +};
> +
>  static struct option builtin_config_options[] = {
>  	OPT_GROUP(N_("Config file location")),
>  	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
> @@ -851,20 +868,20 @@ static struct option builtin_config_options[] = {
>  	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
>  	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
>  	OPT_GROUP(N_("Action")),
> -	OPT_CMDMODE(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
> -	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
> -	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
> -	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
> -	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
> -	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
> -	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
> -	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
> -	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
> -	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
> -	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
> -	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
> -	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
> -	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
> +	OPT_CMDMODE(0, "get", &action, N_("get value: name [value-pattern]"), ACTION_GET),

I guess value-pattern is a placeholder. So for uniformity with the style guidelines, it should be formatted  <value-pattern>.
This also applies to other placeholders below.

> +	OPT_CMDMODE(0, "get-all", &action, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
> +	OPT_CMDMODE(0, "get-regexp", &action, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
> +	OPT_CMDMODE(0, "get-urlmatch", &action, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
> +	OPT_CMDMODE(0, "replace-all", &action, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
> +	OPT_CMDMODE(0, "add", &action, N_("add a new variable: name value"), ACTION_ADD),
> +	OPT_CMDMODE(0, "unset", &action, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
> +	OPT_CMDMODE(0, "unset-all", &action, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
> +	OPT_CMDMODE(0, "rename-section", &action, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
> +	OPT_CMDMODE(0, "remove-section", &action, N_("remove a section: name"), ACTION_REMOVE_SECTION),
> +	OPT_CMDMODE('l', "list", &action, N_("list all"), ACTION_LIST),
> +	OPT_CMDMODE('e', "edit", &action, N_("open an editor"), ACTION_EDIT),
> +	OPT_CMDMODE(0, "get-color", &action, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
> +	OPT_CMDMODE(0, "get-colorbool", &action, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
>  	OPT_GROUP(N_("Type")),
>  	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
>  	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
> @@ -976,33 +993,43 @@ int cmd_config(int argc, const char **argv, const char *prefix)
>  		key_delim = '\n';
>  	}
>  





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

* Re: [PATCH 0/8] builtin/config: introduce subcommands
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (8 preceding siblings ...)
  2024-03-06 17:06 ` [PATCH 0/8] builtin/config: introduce subcommands Junio C Hamano
@ 2024-03-06 22:49 ` Kristoffer Haugsbakk
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                   ` (3 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Kristoffer Haugsbakk @ 2024-03-06 22:49 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git

Wow, this is great. I’ve always found git-config a bit hard to use.

Thanks for this.

-- 
Kristoffer Haugsbakk

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

* Re: [PATCH 0/8] builtin/config: introduce subcommands
  2024-03-06 17:06 ` [PATCH 0/8] builtin/config: introduce subcommands Junio C Hamano
@ 2024-03-06 23:46   ` Taylor Blau
  2024-03-06 23:52     ` Junio C Hamano
                       ` (2 more replies)
  0 siblings, 3 replies; 113+ messages in thread
From: Taylor Blau @ 2024-03-06 23:46 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Patrick Steinhardt, git

On Wed, Mar 06, 2024 at 09:06:08AM -0800, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> >   - `git config foo.bar` -> `git config get foo.bar`
> >
> >   - `git config foo.bar value` -> `git config set foo.bar value`
>
> I actually have been perfectly OK with the above two, but I agree
> that ...

Same here, though I think that we are probably both biased by many years
of familiarity with the existing syntax.

> >   - `git config foo.bar value value-pattern` -> `git config set-all
> >     foo.bar value value-pattern`
>
> ... this was less than discoverable, and would be a good update.
> This one ...

Agreed.

> >   - `git config --get-urlmatch` -> `git config get-urlmatch`.
>
> ... is a Meh to me, personally.  I'd not actively push it
> enthusiastically, but I'd passively accept its existence.

I don't have strong feelings about this, but I wonder if `--urlmatch`
(or `--url-match`) might be an argument to the "get" mode of this
sub-command instead. Something like `git config get --urlmatch` feels
much more natural to me than `git config get-urlmatch`.

Thanks,
Taylor

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

* Re: [PATCH 0/8] builtin/config: introduce subcommands
  2024-03-06 23:46   ` Taylor Blau
@ 2024-03-06 23:52     ` Junio C Hamano
  2024-03-07  0:13       ` Taylor Blau
  2024-03-07  0:31     ` Dragan Simic
  2024-03-07  6:31     ` Patrick Steinhardt
  2 siblings, 1 reply; 113+ messages in thread
From: Junio C Hamano @ 2024-03-06 23:52 UTC (permalink / raw)
  To: Taylor Blau; +Cc: Patrick Steinhardt, git

Taylor Blau <me@ttaylorr.com> writes:

> I don't have strong feelings about this, but I wonder if `--urlmatch`
> (or `--url-match`) might be an argument to the "get" mode of this
> sub-command instead. Something like `git config get --urlmatch` feels
> much more natural to me than `git config get-urlmatch`.

I like that, too.

"--get-regexp" may also be a good candidate to be folded into the
base verb "get", with "--regexp" option to tweak what kind of key is
used.

Could "--get-color" and "--get-colorbool" become verb "get" with
"--type=color"?  The other parameters they get are somewhat
different from "get", so that may not work very well, I guess.

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

* Re: [PATCH 3/8] builtin/config: use `OPT_CMDMODE()` to specify modes
  2024-03-06 11:31 ` [PATCH 3/8] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
@ 2024-03-06 23:52   ` Taylor Blau
  2024-03-07  7:02     ` Patrick Steinhardt
  0 siblings, 1 reply; 113+ messages in thread
From: Taylor Blau @ 2024-03-06 23:52 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git

On Wed, Mar 06, 2024 at 12:31:42PM +0100, Patrick Steinhardt wrote:
> The git-config(1) command has various different modes which are
> accessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are
> declared with `OPT_BIT()`, which causes two minor issues:
>
>   - The respective modes also have a negated form `--no-get-urlmatch`,
>     which is unintended.
>
>   - We have to manually handle exclusiveness of the modes.
>
> Switch these options to instead use `OPT_CMDMODE()`, which is made
> exactly for this usecase. Remove the now-unneeded check that only a
> single mode is given, which is now handled by the parse-options
> interface.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>

> +	OPT_CMDMODE(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
> +	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
> +	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
> +	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),

Expanding a little on my reply to Junio later on in the thread, I
suspect that these would translate into "get", "get --all", "get
--regexp", and "get --urlmatch", respectively.

> +	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
> +	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
> +	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
> +	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),

Same with this one turning into "unset --all".

> +	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
> +	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
> +	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
> +	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
> +	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
> +	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),

These two could probably join the other "get-xyz" modes, and similarly
become "get --color", and "get --colorbool", respectively. It looks like
that's where they lived originally... perhaps I'm missing something as
to why they were moved.

>  	OPT_GROUP(N_("Type")),
>  	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
>  	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
> @@ -767,10 +767,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)
>  		usage_builtin_config();
>  	}
>
> -	if (HAS_MULTI_BITS(actions)) {
> -		error(_("only one action at a time"));
> -		usage_builtin_config();
> -	}

If you apply just this hunk, I would have expected it to break t1300,
but it doesn't appear to. In fact, it looks like the test suite doesn't
seem to mind, either, which indicates a gap in our test coverage...

> diff --git a/t/t1300-config.sh b/t/t1300-config.sh
> index 31c3878687..2d1bc1e27e 100755
> --- a/t/t1300-config.sh
> +++ b/t/t1300-config.sh
> @@ -2612,4 +2612,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such
>  	grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
>  '
>
> +test_expect_success 'negated mode causes failure' '
> +	test_must_fail git config --no-get 2>err &&
> +	grep "unknown option \`no-get${SQ}" err
> +'
> +
> +test_expect_success 'specifying multiple modes causes failure' '
> +	cat >expect <<-EOF &&
> +	error: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together
> +	EOF
> +	test_must_fail git config --get --get-all 2>err &&
> +	test_cmp expect err
> +'

But this gap is rectified by this hunk here. Thank you!

Thanks,
Taylor

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

* Re: [PATCH 5/8] builtin/config: track subcommands by action
  2024-03-06 11:31 ` [PATCH 5/8] builtin/config: track subcommands by action Patrick Steinhardt
  2024-03-06 21:54   ` Jean-Noël AVILA
@ 2024-03-07  0:10   ` Taylor Blau
  2024-03-07  6:36     ` Patrick Steinhardt
  1 sibling, 1 reply; 113+ messages in thread
From: Taylor Blau @ 2024-03-07  0:10 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git

On Wed, Mar 06, 2024 at 12:31:50PM +0100, Patrick Steinhardt wrote:
> ---
>  builtin/config.c | 207 +++++++++++++++++++++++------------------------
>  1 file changed, 99 insertions(+), 108 deletions(-)

This is definitely easier (for me, at least) to view with:

    $ git show --color-moved --color-moved-ws=ignore-all-space

which makes clearer that this change does not introduce or change any
existing behavior.

>  static struct option builtin_config_options[] = {
>  	OPT_GROUP(N_("Config file location")),
>  	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
> @@ -851,20 +868,20 @@ static struct option builtin_config_options[] = {
>  	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
>  	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
>  	OPT_GROUP(N_("Action")),
> -	OPT_CMDMODE(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
> -	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
> -	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
> [...]

Got it, this whole hunk is changing "&actions" to "&action", and nothing
else.

> @@ -976,33 +993,43 @@ int cmd_config(int argc, const char **argv, const char *prefix)
>  		key_delim = '\n';
>  	}
>
> -	if ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {
> -		error(_("--get-color and variable type are incoherent"));
> -		usage_builtin_config();
> -	}
> -
> -	if (actions == 0)
> +	if (action == ACTION_NONE) {
>  		switch (argc) {
> -		case 1: actions = ACTION_GET; break;
> -		case 2: actions = ACTION_SET; break;
> -		case 3: actions = ACTION_SET_ALL; break;
> +		case 1: action = ACTION_GET; break;
> +		case 2: action = ACTION_SET; break;
> +		case 3: action = ACTION_SET_ALL; break;

Wondering aloud a little bit... is it safe to set us to "ACTION_SET",
for example, if we have exactly two arguments? On the one hand, it seems
like the answer is yes. But on the other hand, if we were invoked as:

    $ git config ste foo

We would get something like:

    $ git config ste foo
    error: key does not contain a section: ste

which is sensible. It would be nice to say something more along the
lines of "oops, you probably meant 'set' instead of 'ste'". I think you
could claim that the error on the user's part is one of:

  - (using the new style 'git config') misspelling "set" as "ste", and
    thus trying to invoke a non-existent sub-command

  - (using the old style) trying to set the key "ste", which does not
    contain a section name, and thus is not a valid configuration key

I have no idea which is more likely, and I think that there isn't a good
way to distinguish between the two at all. So I think the error message
is OK as-is, but let me know if you have other thoughts.

>  		default:
>  			usage_builtin_config();
>  		}
> +	}
> +	if (action <= ACTION_NONE || action >= ARRAY_SIZE(subcommands_by_action))
> +		BUG("invalid action %d", action);
> +	subcommand = subcommands_by_action[action];
> +
> +	if (type && (subcommand == cmd_config_get_color ||
> +		     subcommand == cmd_config_get_colorbool)) {
I don't think there's anything wrong with using the function-pointer
equality here to figure out which subcommand we're in, but I wonder if
it might be cleaner to write this as:

    if (type && (action == ACTION_GET_COLOR ||
                 action == ACTION_GET_COLORBOOL)) {
        ...

> -	if (actions == ACTION_LIST) {
> -		return cmd_config_list(argc, argv, prefix);
> -	} else if (actions == ACTION_EDIT) {
> -		return cmd_config_edit(argc, argv, prefix);
> -	} else if (actions == ACTION_SET) {
> -		return cmd_config_set(argc, argv, prefix);
> -	} else if (actions == ACTION_SET_ALL) {
> -		return cmd_config_set_all(argc, argv, prefix);
> -	} else if (actions == ACTION_ADD) {
> -		return cmd_config_add(argc, argv, prefix);
> -	} else if (actions == ACTION_REPLACE_ALL) {
> -		return cmd_config_replace_all(argc, argv, prefix);
> -	} else if (actions == ACTION_GET) {
> -		return cmd_config_get(argc, argv, prefix);
> -	} else if (actions == ACTION_GET_ALL) {
> -		return cmd_config_get_all(argc, argv, prefix);
> -	} else if (actions == ACTION_GET_REGEXP) {
> -		return cmd_config_get_regexp(argc, argv, prefix);
> -	} else if (actions == ACTION_GET_URLMATCH) {
> -		return cmd_config_get_urlmatch(argc, argv, prefix);
> -	} else if (actions == ACTION_UNSET) {
> -		return cmd_config_unset(argc, argv, prefix);
> -	} else if (actions == ACTION_UNSET_ALL) {
> -		return cmd_config_unset_all(argc, argv, prefix);
> -	} else if (actions == ACTION_RENAME_SECTION) {
> -		return cmd_config_rename_section(argc, argv, prefix);
> -	} else if (actions == ACTION_REMOVE_SECTION) {
> -		return cmd_config_remove_section(argc, argv, prefix);
> -	} else if (actions == ACTION_GET_COLOR) {
> -		return cmd_config_get_color(argc, argv, prefix);
> -	} else if (actions == ACTION_GET_COLORBOOL) {
> -		return cmd_config_get_colorbool(argc, argv, prefix);
> -	}
> -
> -	BUG("invalid action");
> +	return subcommand(argc, argv, prefix);

Very nice.

Thanks,
Taylor

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

* Re: [PATCH 0/8] builtin/config: introduce subcommands
  2024-03-06 23:52     ` Junio C Hamano
@ 2024-03-07  0:13       ` Taylor Blau
  0 siblings, 0 replies; 113+ messages in thread
From: Taylor Blau @ 2024-03-07  0:13 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Patrick Steinhardt, git

On Wed, Mar 06, 2024 at 03:52:23PM -0800, Junio C Hamano wrote:
> Taylor Blau <me@ttaylorr.com> writes:
>
> > I don't have strong feelings about this, but I wonder if `--urlmatch`
> > (or `--url-match`) might be an argument to the "get" mode of this
> > sub-command instead. Something like `git config get --urlmatch` feels
> > much more natural to me than `git config get-urlmatch`.
>
> I like that, too.
>
> "--get-regexp" may also be a good candidate to be folded into the
> base verb "get", with "--regexp" option to tweak what kind of key is
> used.

Yes, definitely.

> Could "--get-color" and "--get-colorbool" become verb "get" with
> "--type=color"?  The other parameters they get are somewhat
> different from "get", so that may not work very well, I guess.

I'm somewhat embarrassed to not have a better suggestion, since I
vaguely remember working on the `--type` option as one of my first
contributions to the project.

Indeed: fb0dc3bac13 (builtin/config.c: support `--type=<type>` as
preferred alias for `--<type>`, 2018-04-18). ;-).

Thanks,
Taylor

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

* Re: [PATCH 0/8] builtin/config: introduce subcommands
  2024-03-06 23:46   ` Taylor Blau
  2024-03-06 23:52     ` Junio C Hamano
@ 2024-03-07  0:31     ` Dragan Simic
  2024-03-07  6:31     ` Patrick Steinhardt
  2 siblings, 0 replies; 113+ messages in thread
From: Dragan Simic @ 2024-03-07  0:31 UTC (permalink / raw)
  To: Taylor Blau; +Cc: Junio C Hamano, Patrick Steinhardt, git

Hello all,

On 2024-03-07 00:46, Taylor Blau wrote:
> On Wed, Mar 06, 2024 at 09:06:08AM -0800, Junio C Hamano wrote:
>> Patrick Steinhardt <ps@pks.im> writes:
>> 
>> >   - `git config foo.bar` -> `git config get foo.bar`
>> >
>> >   - `git config foo.bar value` -> `git config set foo.bar value`
>> 
>> I actually have been perfectly OK with the above two, but I agree
>> that ...
> 
> Same here, though I think that we are probably both biased by many 
> years
> of familiarity with the existing syntax.

Ditto.  Though, having "get" and "set" commands will be nice, making it
all more self descriptive.

>> >   - `git config foo.bar value value-pattern` -> `git config set-all
>> >     foo.bar value value-pattern`
>> 
>> ... this was less than discoverable, and would be a good update.
>> This one ...
> 
> Agreed.

Also agreed.  Having the point below in mind, perhaps we could actually
end up with "set --all" instead of "set-all".

>> >   - `git config --get-urlmatch` -> `git config get-urlmatch`.
>> 
>> ... is a Meh to me, personally.  I'd not actively push it
>> enthusiastically, but I'd passively accept its existence.
> 
> I don't have strong feelings about this, but I wonder if `--urlmatch`
> (or `--url-match`) might be an argument to the "get" mode of this
> sub-command instead. Something like `git config get --urlmatch` feels
> much more natural to me than `git config get-urlmatch`.

Good point.  I'd vote for having "get --urlmatch" or "get --url-match",
because it feels more natural to me, it doesn't "clog up" the command
space, and such an approach, in general, allows git-config(1) to be
expanded later easier with more new arguments for the existing commands.

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

* Re: [PATCH 0/8] builtin/config: introduce subcommands
  2024-03-06 23:46   ` Taylor Blau
  2024-03-06 23:52     ` Junio C Hamano
  2024-03-07  0:31     ` Dragan Simic
@ 2024-03-07  6:31     ` Patrick Steinhardt
  2024-03-07 13:22       ` Junio C Hamano
  2 siblings, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-07  6:31 UTC (permalink / raw)
  To: Taylor Blau; +Cc: Junio C Hamano, git

[-- Attachment #1: Type: text/plain, Size: 1931 bytes --]

On Wed, Mar 06, 2024 at 06:46:33PM -0500, Taylor Blau wrote:
> On Wed, Mar 06, 2024 at 09:06:08AM -0800, Junio C Hamano wrote:
> > Patrick Steinhardt <ps@pks.im> writes:
> > >   - `git config --get-urlmatch` -> `git config get-urlmatch`.
> >
> > ... is a Meh to me, personally.  I'd not actively push it
> > enthusiastically, but I'd passively accept its existence.
> 
> I don't have strong feelings about this, but I wonder if `--urlmatch`
> (or `--url-match`) might be an argument to the "get" mode of this
> sub-command instead. Something like `git config get --urlmatch` feels
> much more natural to me than `git config get-urlmatch`.

Agreed. I allude to this somewhere in the patch series that I only see
this as a first incremental step, and that some of the subcommands
really should be converted to become options eventually. Specifically:

    - `git config get-urlmatch` -> `git config get --urlmatch`

    - `git config get-regexp` -> `git config get --regexp`

    - `git config get-all` -> `git config get --all`
 
    - `git config set-all` -> `git config set --all`

    - `git config unset-all` -> `git config unset --all`

I didn't yet do it as part of this patch series because I didn't want to
make functional changes at the same time (well, except of course for the
introduction of subcommands). But if we all agree that this patch series
is something we want _and_ that we don't want to have an intermediate
step with the above somewhat weird subcommands then I would be happy to
pile onto the series.

I wouldn't think that keeping this intermediate step would be too bad
though. While we would eventually omit the above subcommands, the infra
to keep them around needs to stay anyway to support old syntax like
`--get-all`. Thus, we can keep the subcommands themselves almost for
free even if we eventually decide to hide them and replace them with
flags.

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH 5/8] builtin/config: track subcommands by action
  2024-03-07  0:10   ` Taylor Blau
@ 2024-03-07  6:36     ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-07  6:36 UTC (permalink / raw)
  To: Taylor Blau; +Cc: git

[-- Attachment #1: Type: text/plain, Size: 3143 bytes --]

On Wed, Mar 06, 2024 at 07:10:09PM -0500, Taylor Blau wrote:
> On Wed, Mar 06, 2024 at 12:31:50PM +0100, Patrick Steinhardt wrote:
> > @@ -976,33 +993,43 @@ int cmd_config(int argc, const char **argv, const char *prefix)
> >  		key_delim = '\n';
> >  	}
> >
> > -	if ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {
> > -		error(_("--get-color and variable type are incoherent"));
> > -		usage_builtin_config();
> > -	}
> > -
> > -	if (actions == 0)
> > +	if (action == ACTION_NONE) {
> >  		switch (argc) {
> > -		case 1: actions = ACTION_GET; break;
> > -		case 2: actions = ACTION_SET; break;
> > -		case 3: actions = ACTION_SET_ALL; break;
> > +		case 1: action = ACTION_GET; break;
> > +		case 2: action = ACTION_SET; break;
> > +		case 3: action = ACTION_SET_ALL; break;
> 
> Wondering aloud a little bit... is it safe to set us to "ACTION_SET",
> for example, if we have exactly two arguments? On the one hand, it seems
> like the answer is yes. But on the other hand, if we were invoked as:
> 
>     $ git config ste foo
> 
> We would get something like:
> 
>     $ git config ste foo
>     error: key does not contain a section: ste
> 
> which is sensible. It would be nice to say something more along the
> lines of "oops, you probably meant 'set' instead of 'ste'". I think you
> could claim that the error on the user's part is one of:
> 
>   - (using the new style 'git config') misspelling "set" as "ste", and
>     thus trying to invoke a non-existent sub-command
> 
>   - (using the old style) trying to set the key "ste", which does not
>     contain a section name, and thus is not a valid configuration key
> 
> I have no idea which is more likely, and I think that there isn't a good
> way to distinguish between the two at all. So I think the error message
> is OK as-is, but let me know if you have other thoughts.

The transition period will be a tad weird, I agree. I think initially
I'd prefer to keep the old behaviour just to ensure we don't regress
anything, but after a couple of releases I think we should revisit this
and become more aggressive about using the new style.

> >  		default:
> >  			usage_builtin_config();
> >  		}
> > +	}
> > +	if (action <= ACTION_NONE || action >= ARRAY_SIZE(subcommands_by_action))
> > +		BUG("invalid action %d", action);
> > +	subcommand = subcommands_by_action[action];
> > +
> > +	if (type && (subcommand == cmd_config_get_color ||
> > +		     subcommand == cmd_config_get_colorbool)) {
> I don't think there's anything wrong with using the function-pointer
> equality here to figure out which subcommand we're in, but I wonder if
> it might be cleaner to write this as:
> 
>     if (type && (action == ACTION_GET_COLOR ||
>                  action == ACTION_GET_COLORBOOL)) {
>         ...

The reason I didn't is that we don't always have an action set once we
support subcommands. We can of course figure it out by walking the array
of functions pointers to find the one corresponding to this action. But
I figured it's easier to just use function pointers exclusively.

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH 5/8] builtin/config: track subcommands by action
  2024-03-06 21:54   ` Jean-Noël AVILA
@ 2024-03-07  6:37     ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-07  6:37 UTC (permalink / raw)
  To: Jean-Noël AVILA; +Cc: git

[-- Attachment #1: Type: text/plain, Size: 2284 bytes --]

On Wed, Mar 06, 2024 at 10:54:01PM +0100, Jean-Noël AVILA wrote:
> Le mercredi 6 mars 2024, 12:31:50 CET Patrick Steinhardt a écrit :
[snip]
> > @@ -851,20 +868,20 @@ static struct option builtin_config_options[] = {
> >  	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
> >  	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
> >  	OPT_GROUP(N_("Action")),
> > -	OPT_CMDMODE(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
> > -	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
> > -	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
> > -	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
> > -	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
> > -	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
> > -	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
> > -	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
> > -	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
> > -	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
> > -	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
> > -	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
> > -	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
> > -	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
> > +	OPT_CMDMODE(0, "get", &action, N_("get value: name [value-pattern]"), ACTION_GET),
> 
> I guess value-pattern is a placeholder. So for uniformity with the
> style guidelines, it should be formatted  <value-pattern>. This also
> applies to other placeholders below.

Ah, indeed, thanks for catching this!

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH 8/8] Documentation/git-config: update to new-style syntax
  2024-03-06 11:32 ` [PATCH 8/8] Documentation/git-config: update to new-style syntax Patrick Steinhardt
@ 2024-03-07  6:57   ` Eric Sunshine
  2024-03-07  7:33     ` Patrick Steinhardt
  0 siblings, 1 reply; 113+ messages in thread
From: Eric Sunshine @ 2024-03-07  6:57 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git

On Wed, Mar 6, 2024 at 6:32 AM Patrick Steinhardt <ps@pks.im> wrote:
> Update our documentation of git-config(1) to stop mentioning the old
> syntax while starting to mention the new syntax. Remove the help
> mismatch in t0450 so that we start to ensure that the manpage and
> builtin synopsis match.
> [...]
> Signed-off-by: Patrick Steinhardt <ps@pks.im>

I'd like to push back on the part of this patch which completely
eradicates mention of the old syntax. Doing so makes it difficult for
people to figure out the meaning of git-config commands which they run
across in blogs and existing tooling. For instance, the following
recommendation is commonly encountered in the wild:

    git config --global user.name "Your Name"
    git config --global user.email "youremail@yourdomain.com"

Typically, we instead retain the old syntax (or options or whatever)
in the documentation so that people who want to learn can learn, but
we rewrite it to make it clear that it is deprecated, and explicitly
point the reader at the modern replacement command or option (or
whatever).

So, perhaps you can have a SYNOPSIS section for the deprecated usage,
as well as a "DEPRECATED OPTIONS" section (or such), so that we don't
leave readers entirely in the dark.

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

* Re: [PATCH 3/8] builtin/config: use `OPT_CMDMODE()` to specify modes
  2024-03-06 23:52   ` Taylor Blau
@ 2024-03-07  7:02     ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-07  7:02 UTC (permalink / raw)
  To: Taylor Blau; +Cc: git

[-- Attachment #1: Type: text/plain, Size: 3134 bytes --]

On Wed, Mar 06, 2024 at 06:52:46PM -0500, Taylor Blau wrote:
> On Wed, Mar 06, 2024 at 12:31:42PM +0100, Patrick Steinhardt wrote:
> > The git-config(1) command has various different modes which are
> > accessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are
> > declared with `OPT_BIT()`, which causes two minor issues:
> >
> >   - The respective modes also have a negated form `--no-get-urlmatch`,
> >     which is unintended.
> >
> >   - We have to manually handle exclusiveness of the modes.
> >
> > Switch these options to instead use `OPT_CMDMODE()`, which is made
> > exactly for this usecase. Remove the now-unneeded check that only a
> > single mode is given, which is now handled by the parse-options
> > interface.
> >
> > Signed-off-by: Patrick Steinhardt <ps@pks.im>
> 
> > +	OPT_CMDMODE(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
> > +	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
> > +	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
> > +	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
> 
> Expanding a little on my reply to Junio later on in the thread, I
> suspect that these would translate into "get", "get --all", "get
> --regexp", and "get --urlmatch", respectively.

Yup.

> > +	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
> > +	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
> > +	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
> > +	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
> 
> Same with this one turning into "unset --all".

Yup.

> > +	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
> > +	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
> > +	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
> > +	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
> > +	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
> > +	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
> 
> These two could probably join the other "get-xyz" modes, and similarly
> become "get --color", and "get --colorbool", respectively. It looks like
> that's where they lived originally... perhaps I'm missing something as
> to why they were moved.

Yes, although I think putting it into the `--type` parameter might be
even better.

I'm still a bit torn on whether we should do this all as part of this
patch series or as part of a follow-up patch series. But if anybody
feels strongly one way or the other I'm happy to adapt accordingly.

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH 6/8] builtin/config: introduce subcommands
  2024-03-06 21:38   ` Karthik Nayak
@ 2024-03-07  7:14     ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-07  7:14 UTC (permalink / raw)
  To: Karthik Nayak; +Cc: git

[-- Attachment #1: Type: text/plain, Size: 1492 bytes --]

On Wed, Mar 06, 2024 at 01:38:25PM -0800, Karthik Nayak wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > @@ -910,6 +930,20 @@ int cmd_config(int argc, const char **argv, const char *prefix)
> >  {
> >  	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
> >
> > +	/*
> > +	 * This is somewhat hacky: we first parse the command line while
> > +	 * keeping all args intact in order to determine whether a subcommand
> > +	 * has been specified. If so, we re-parse it a second time, but this
> > +	 * time we drop KEEP_ARGV0. This is so that we don't munge the command
> > +	 * line in case no subcommand was given, which would otherwise confuse
> > +	 * us when parsing the implicit modes.
> > +	 */
> > +	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
> > +			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
> > +	if (subcommand)
> > +		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
> > +				     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);
> > +
> 
> I wonder if we can drop the PARSE_OPT_SUBCOMMAND_OPTIONAL in the second
> iteration to make it stricter. But this is OK.

We can't due to a restriction in the parse-options interface: when
subcommands are present, then PARSE_OPT_KEEP_UNKNOWN_OPT requires us to
also pass PARSE_OPT_SUBCOMMAND_OPTIONAL. Otherwise we trigger a `BUG()`.

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH 8/8] Documentation/git-config: update to new-style syntax
  2024-03-07  6:57   ` Eric Sunshine
@ 2024-03-07  7:33     ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-07  7:33 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: git

[-- Attachment #1: Type: text/plain, Size: 1475 bytes --]

On Thu, Mar 07, 2024 at 01:57:41AM -0500, Eric Sunshine wrote:
> On Wed, Mar 6, 2024 at 6:32 AM Patrick Steinhardt <ps@pks.im> wrote:
> > Update our documentation of git-config(1) to stop mentioning the old
> > syntax while starting to mention the new syntax. Remove the help
> > mismatch in t0450 so that we start to ensure that the manpage and
> > builtin synopsis match.
> > [...]
> > Signed-off-by: Patrick Steinhardt <ps@pks.im>
> 
> I'd like to push back on the part of this patch which completely
> eradicates mention of the old syntax. Doing so makes it difficult for
> people to figure out the meaning of git-config commands which they run
> across in blogs and existing tooling. For instance, the following
> recommendation is commonly encountered in the wild:
> 
>     git config --global user.name "Your Name"
>     git config --global user.email "youremail@yourdomain.com"
> 
> Typically, we instead retain the old syntax (or options or whatever)
> in the documentation so that people who want to learn can learn, but
> we rewrite it to make it clear that it is deprecated, and explicitly
> point the reader at the modern replacement command or option (or
> whatever).
> 
> So, perhaps you can have a SYNOPSIS section for the deprecated usage,
> as well as a "DEPRECATED OPTIONS" section (or such), so that we don't
> leave readers entirely in the dark.

Fair point indeed. I will update this patch accordingly, thanks!

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH 0/8] builtin/config: introduce subcommands
  2024-03-07  6:31     ` Patrick Steinhardt
@ 2024-03-07 13:22       ` Junio C Hamano
  0 siblings, 0 replies; 113+ messages in thread
From: Junio C Hamano @ 2024-03-07 13:22 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: Taylor Blau, git

Patrick Steinhardt <ps@pks.im> writes:

> On Wed, Mar 06, 2024 at 06:46:33PM -0500, Taylor Blau wrote:
>> On Wed, Mar 06, 2024 at 09:06:08AM -0800, Junio C Hamano wrote:
>> > Patrick Steinhardt <ps@pks.im> writes:
>> > >   - `git config --get-urlmatch` -> `git config get-urlmatch`.
>> >
>> > ... is a Meh to me, personally.  I'd not actively push it
>> > enthusiastically, but I'd passively accept its existence.
>> 
>> I don't have strong feelings about this, but I wonder if `--urlmatch`
>> (or `--url-match`) might be an argument to the "get" mode of this
>> sub-command instead. Something like `git config get --urlmatch` feels
>> much more natural to me than `git config get-urlmatch`.
>
> Agreed. I allude to this somewhere in the patch series that I only see
> this as a first incremental step, and that some of the subcommands
> really should be converted to become options eventually. Specifically:
>
>     - `git config get-urlmatch` -> `git config get --urlmatch`
>
>     - `git config get-regexp` -> `git config get --regexp`
>
>     - `git config get-all` -> `git config get --all`
>  
>     - `git config set-all` -> `git config set --all`
>
>     - `git config unset-all` -> `git config unset --all`
>
> I didn't yet do it as part of this patch series because I didn't want to
> make functional changes at the same time (well, except of course for the
> introduction of subcommands). But if we all agree that this patch series
> is something we want _and_ that we don't want to have an intermediate
> step with the above somewhat weird subcommands then I would be happy to
> pile onto the series.
>
> I wouldn't think that keeping this intermediate step would be too bad
> though. While we would eventually omit the above subcommands, the infra
> to keep them around needs to stay anyway to support old syntax like
> `--get-all`. Thus, we can keep the subcommands themselves almost for
> free even if we eventually decide to hide them and replace them with
> flags.

The "more involved" changes along the lines Taylor suggested take
some thought in designing the end-user experience, so even if you do
not initially plan to support them in the implementation, it would
be better to know how the final command line should look like before
starting, so that we won't surprise users, if anything.

The trickies I anticipate offhand are:

 * The urlmatch thing wants name and URL.  In the new syntax, I
   think the URL part should become an option parameter to the --url
   option.  E.g. a request with the current UI

    $ git config --get-urlmatch SECTION.VARIABLE URL

   would become

    $ git config get --url=URL SECTION.VARIABLE

 * The color thing takes the default value to be used when
   unconfigured.  That should also become an option parameter to the
   --default option.

    $ git config --get-color SECTION.VARIABLE DEFAULT

   would become

    $ git config get --type=color --default=DEFAULT SECTION.VARIABLE

   Similarly for --tty=true/false used in --get-colorbool.

 * Now, these extended ones with options introduce possibilities to
   combine them in ways that weren't possible before.  For example,
   "--get" and "--get-all" are distinct, and there is no way to tell
   "--get-urlmatch" to give all values that would match, but it is
   plausible that we may want to handle

    $ git config get --all --url=URL SECTION.VARIABLE

   to do so.  A more interesting one is the --default=DEFAULT
   option.  What we currently write

    $ git config --get no.such.variable || echo 'default value'

   can become

    $ git config get --default='default value' no.such.variable

HTH.


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

* [PATCH v2 00/13] builtin/config: introduce subcommands
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (9 preceding siblings ...)
  2024-03-06 22:49 ` Kristoffer Haugsbakk
@ 2024-03-11 23:19 ` Patrick Steinhardt
  2024-03-11 23:19   ` [PATCH v2 01/13] builtin/config: move option array around Patrick Steinhardt
                     ` (12 more replies)
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (2 subsequent siblings)
  13 siblings, 13 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:19 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 2540 bytes --]

Hi,

this is the second version of my patch series that aims to add
subcommands to git-config(1) in a backwards-compatible manner. 

This series is mostly a rewrite compared to v1, so I won't go into all
the details. Most importantly:

  - I have replaced several subcommands by option. For example, `git
    config get-all` is now `git config get --all` and `git config add`
    is `git config set --append`.

  - Subcommands now perform their own parsing. This makes the code a
    whole lot more easy to reason about and ensures that there are no
    backwards incompatible changes to the old modes. This comes at the
    cost of additional code though.

  - The documentation has been adapted to mention the old syntax in a
    "DEPRECATED SYNTAX" section. It also lists how to convert old style
    syntax to new style syntax.

As I expect there to be some more discussion and thus some more versions
of this series I stopped short of some things:

  - Tests exist for most of the parts, but some modes aren't yet
    properly tested. This is mostly due to incompatibilities between old
    and new style syntax which make it hard to just run the same tests
    twice with some prefixes.

  - The `--get-colorbool` option hasn't been converted yet. I _think_ we
    might be able to introduce a new `--type=colorbool` option, but I'm
    not sure I fully grasp the usecase of this command.

Thanks for all the feedback so far!

Patrick

Patrick Steinhardt (13):
  builtin/config: move option array around
  builtin/config: move "fixed-value" option to correct group
  builtin/config: use `OPT_CMDMODE()` to specify modes
  builtin/config: pull out function to handle config location
  builtin/config: pull out function to handle `--null`
  builtin/config: introduce "list" subcommand
  builtin/config: introduce "get" subcommand
  builtin/config: introduce "set" subcommand
  builtin/config: introduce "unset" subcommand
  builtin/config: introduce "rename-section" subcommand
  builtin/config: introduce "remove-section" subcommand
  builtin/config: introduce "edit" subcommand
  builtin/config: display subcommand help

 Documentation/git-config.txt | 213 +++++++++------
 builtin/config.c             | 496 ++++++++++++++++++++++++++++-------
 t/t0450/txt-help-mismatches  |   1 -
 t/t1300-config.sh            | 417 ++++++++++++++++++-----------
 4 files changed, 786 insertions(+), 341 deletions(-)


base-commit: e09f1254c54329773904fe25d7c545a1fb4fa920
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 01/13] builtin/config: move option array around
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
@ 2024-03-11 23:19   ` Patrick Steinhardt
  2024-03-11 23:19   ` [PATCH v2 02/13] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
                     ` (11 subsequent siblings)
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:19 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 8066 bytes --]

Move around the option array. This will help us with a follow-up commit
that introduces subcommands to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 94 ++++++++++++++++++++++++------------------------
 1 file changed, 47 insertions(+), 47 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index b55bfae7d6..6eb6aff917 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -134,53 +134,6 @@ static int option_parse_type(const struct option *opt, const char *arg,
 	return 0;
 }
 
-static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
-	OPT_GROUP(N_("Action")),
-	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
-	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
-	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
-	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_END(),
-};
-
-static NORETURN void usage_builtin_config(void)
-{
-	usage_with_options(builtin_config_usage, builtin_config_options);
-}
-
 static void check_argc(int argc, int min, int max)
 {
 	if (argc >= min && argc <= max)
@@ -669,6 +622,53 @@ static char *default_user_config(void)
 	return strbuf_detach(&buf, NULL);
 }
 
+static struct option builtin_config_options[] = {
+	OPT_GROUP(N_("Config file location")),
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	OPT_GROUP(N_("Action")),
+	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
+	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
+	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
+	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
+	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
+	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
+	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
+	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_GROUP(N_("Type")),
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	OPT_GROUP(N_("Other")),
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
+	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_END(),
+};
+
+static NORETURN void usage_builtin_config(void)
+{
+	usage_with_options(builtin_config_usage, builtin_config_options);
+}
+
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	int nongit = !startup_info->have_repository;
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 02/13] builtin/config: move "fixed-value" option to correct group
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
  2024-03-11 23:19   ` [PATCH v2 01/13] builtin/config: move option array around Patrick Steinhardt
@ 2024-03-11 23:19   ` Patrick Steinhardt
  2024-03-11 23:20   ` [PATCH v2 03/13] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
                     ` (10 subsequent siblings)
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:19 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 1922 bytes --]

The `--fixed-value` option can be used to alter how the value-pattern
parameter is interpreted for the various submodes of git-config(1). But
while it is an option, it is currently listed as part of the submodes
group the command, which is wrong.

Move the option to the "Other" group, which hosts the various options
known to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/config.c b/builtin/config.c
index 6eb6aff917..fcd6190f12 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -642,7 +642,6 @@ static struct option builtin_config_options[] = {
 	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
 	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
 	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
 	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
 	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
 	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
@@ -661,6 +660,7 @@ static struct option builtin_config_options[] = {
 	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
 	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
 	OPT_END(),
 };
 
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 03/13] builtin/config: use `OPT_CMDMODE()` to specify modes
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
  2024-03-11 23:19   ` [PATCH v2 01/13] builtin/config: move option array around Patrick Steinhardt
  2024-03-11 23:19   ` [PATCH v2 02/13] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
@ 2024-03-11 23:20   ` Patrick Steinhardt
  2024-03-11 23:20   ` [PATCH v2 04/13] builtin/config: pull out function to handle config location Patrick Steinhardt
                     ` (9 subsequent siblings)
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:20 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 5525 bytes --]

The git-config(1) command has various different modes which are
accessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are
declared with `OPT_BIT()`, which causes two minor issues:

  - The respective modes also have a negated form `--no-get-urlmatch`,
    which is unintended.

  - We have to manually handle exclusiveness of the modes.

Switch these options to instead use `OPT_CMDMODE()`, which is made
exactly for this usecase. Remove the now-unneeded check that only a
single mode is given, which is now handled by the parse-options
interface.

While at it, format optional placeholders for arguments to conform to
our style guidelines by using `[<placeholder>]`.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c  | 32 ++++++++++++++------------------
 t/t1300-config.sh | 13 +++++++++++++
 2 files changed, 27 insertions(+), 18 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index fcd6190f12..a293161be4 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -631,20 +631,20 @@ static struct option builtin_config_options[] = {
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
 	OPT_GROUP(N_("Action")),
-	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
+	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
+	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
+	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
+	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
+	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
+	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
+	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
 	OPT_GROUP(N_("Type")),
 	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
 	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
@@ -767,10 +767,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (HAS_MULTI_BITS(actions)) {
-		error(_("only one action at a time"));
-		usage_builtin_config();
-	}
 	if (actions == 0)
 		switch (argc) {
 		case 1: actions = ACTION_GET; break;
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 31c3878687..2d1bc1e27e 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -2612,4 +2612,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such
 	grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
 '
 
+test_expect_success 'negated mode causes failure' '
+	test_must_fail git config --no-get 2>err &&
+	grep "unknown option \`no-get${SQ}" err
+'
+
+test_expect_success 'specifying multiple modes causes failure' '
+	cat >expect <<-EOF &&
+	error: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together
+	EOF
+	test_must_fail git config --get --get-all 2>err &&
+	test_cmp expect err
+'
+
 test_done
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 04/13] builtin/config: pull out function to handle config location
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                     ` (2 preceding siblings ...)
  2024-03-11 23:20   ` [PATCH v2 03/13] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
@ 2024-03-11 23:20   ` Patrick Steinhardt
  2024-03-11 23:20   ` [PATCH v2 05/13] builtin/config: pull out function to handle `--null` Patrick Steinhardt
                     ` (8 subsequent siblings)
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:20 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 10533 bytes --]

There's quite a bunch of options to git-config(1) that allow the user to
specify which config location to use when reading or writing config
options. The logic to handle this is thus by necessity also quite
involved.

Pull it out into a separate function so that we can reuse it in
subsequent commits which introduce proper subcommands.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 131 ++++++++++++++++++++++++-----------------------
 1 file changed, 67 insertions(+), 64 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index a293161be4..693df32526 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -622,67 +622,8 @@ static char *default_user_config(void)
 	return strbuf_detach(&buf, NULL);
 }
 
-static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
-	OPT_GROUP(N_("Action")),
-	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
-	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
-	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
-	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
-	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
-	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
-	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
-	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
-	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
-	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
-	OPT_END(),
-};
-
-static NORETURN void usage_builtin_config(void)
-{
-	usage_with_options(builtin_config_usage, builtin_config_options);
-}
-
-int cmd_config(int argc, const char **argv, const char *prefix)
+static void handle_config_location(const char *prefix)
 {
-	int nongit = !startup_info->have_repository;
-	char *value = NULL;
-	int flags = 0;
-	int ret = 0;
-	struct key_value_info default_kvi = KVI_INIT;
-
-	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
-
-	argc = parse_options(argc, argv, prefix, builtin_config_options,
-			     builtin_config_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
-
 	if (use_global_config + use_system_config + use_local_config +
 	    use_worktree_config +
 	    !!given_config_source.file + !!given_config_source.blob > 1) {
@@ -690,14 +631,13 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (nongit) {
+	if (!startup_info->have_repository) {
 		if (use_local_config)
 			die(_("--local can only be used inside a git repository"));
 		if (given_config_source.blob)
 			die(_("--blob can only be used inside a git repository"));
 		if (use_worktree_config)
 			die(_("--worktree can only be used inside a git repository"));
-
 	}
 
 	if (given_config_source.file &&
@@ -751,10 +691,73 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		config_options.respect_includes = !given_config_source.file;
 	else
 		config_options.respect_includes = respect_includes_opt;
-	if (!nongit) {
+	if (startup_info->have_repository) {
 		config_options.commondir = get_git_common_dir();
 		config_options.git_dir = get_git_dir();
 	}
+}
+
+static struct option builtin_config_options[] = {
+	OPT_GROUP(N_("Config file location")),
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	OPT_GROUP(N_("Action")),
+	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
+	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
+	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
+	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
+	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
+	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
+	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
+	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
+	OPT_GROUP(N_("Type")),
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	OPT_GROUP(N_("Other")),
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
+	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_END(),
+};
+
+static NORETURN void usage_builtin_config(void)
+{
+	usage_with_options(builtin_config_usage, builtin_config_options);
+}
+
+int cmd_config(int argc, const char **argv, const char *prefix)
+{
+	char *value = NULL;
+	int flags = 0;
+	int ret = 0;
+	struct key_value_info default_kvi = KVI_INIT;
+
+	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
+
+	argc = parse_options(argc, argv, prefix, builtin_config_options,
+			     builtin_config_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+
+	handle_config_location(prefix);
 
 	if (end_nul) {
 		term = '\0';
@@ -848,7 +851,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		char *config_file;
 
 		check_argc(argc, 0, 0);
-		if (!given_config_source.file && nongit)
+		if (!given_config_source.file && !startup_info->have_repository)
 			die(_("not in a git directory"));
 		if (given_config_source.use_stdin)
 			die(_("editing stdin is not supported"));
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 05/13] builtin/config: pull out function to handle `--null`
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                     ` (3 preceding siblings ...)
  2024-03-11 23:20   ` [PATCH v2 04/13] builtin/config: pull out function to handle config location Patrick Steinhardt
@ 2024-03-11 23:20   ` Patrick Steinhardt
  2024-03-11 23:20   ` [PATCH v2 06/13] builtin/config: introduce "list" subcommand Patrick Steinhardt
                     ` (7 subsequent siblings)
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:20 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 1177 bytes --]

Pull out function to handle the `--null` option, which we are about to
reuse in subsequent commits.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 693df32526..ce2d3fecd4 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -697,6 +697,14 @@ static void handle_config_location(const char *prefix)
 	}
 }
 
+static void handle_nul(void) {
+	if (end_nul) {
+		term = '\0';
+		delim = '\n';
+		key_delim = '\n';
+	}
+}
+
 static struct option builtin_config_options[] = {
 	OPT_GROUP(N_("Config file location")),
 	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
@@ -758,12 +766,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 			     PARSE_OPT_STOP_AT_NON_OPTION);
 
 	handle_config_location(prefix);
-
-	if (end_nul) {
-		term = '\0';
-		delim = '\n';
-		key_delim = '\n';
-	}
+	handle_nul();
 
 	if ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {
 		error(_("--get-color and variable type are incoherent"));
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 06/13] builtin/config: introduce "list" subcommand
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                     ` (4 preceding siblings ...)
  2024-03-11 23:20   ` [PATCH v2 05/13] builtin/config: pull out function to handle `--null` Patrick Steinhardt
@ 2024-03-11 23:20   ` Patrick Steinhardt
  2024-03-13  2:45     ` Eric Sunshine
  2024-03-11 23:20   ` [PATCH v2 07/13] builtin/config: introduce "get" subcommand Patrick Steinhardt
                     ` (6 subsequent siblings)
  12 siblings, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:20 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 27558 bytes --]

While git-config(1) has several modes, those modes are not exposed with
subcommands but instead by specifying e.g. `--unset` or `--list`. This
user interface is not really in line with how our more modern commands
work, where it is a lot more customary to say e.g. `git remote list`.
Furthermore, to add to the confusion, git-config(1) also allows the user
to request modes implicitly by just specifying the correct number of
arguments. Thus, `git config foo.bar` will retrieve the value of
"foo.bar" while `git config foo.bar baz` will set it to "baz".

Overall, this makes for a confusing interface that could really use a
makeover. It hurts discoverability of what you can do with git-config(1)
and is comparatively easy to get wrong. Converting the command to have
subcommands instead would go a long way to help address these issues.

One concern in this context is backwards compatibility. Luckily, we can
introduce subcommands without breaking backwards compatibility at all.
This is because all the implicit modes of git-config(1) require that the
first argument is a properly formatted config key. And as config keys
_must_ have a dot in their name, any value without a dot would have been
discarded by git-config(1) previous to this change. Thus, given that
none of the subcommands do have a dot, they are unambiguous.

Introduce the first such new subcommand, which is "git config list". To
retain backwards compatibility we only conditionally use subcommands and
will fall back to the old syntax in case no subcommand was detected.
This should help to transition to the new-style syntax until we
eventually deprecate and remove the old-style syntax.

Note that the way we handle this we're duplicating some functionality
across old and new syntax. While this isn't pretty, it helps us to
ensure that there really is no change in behaviour for the old syntax.

Amend tests such that we run them both with old and new style syntax.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt |  26 +++++---
 builtin/config.c             |  90 ++++++++++++++++++++++++----
 t/t1300-config.sh            | 111 +++++++++++++++++++++--------------
 3 files changed, 163 insertions(+), 64 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index dff39093b5..976ba26757 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -9,6 +9,7 @@ git-config - Get and set repository or global options
 SYNOPSIS
 --------
 [verse]
+'git config list' [<file-option>] [<display-option>] [--includes]
 'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
 'git config' [<file-option>] [--type=<type>] --add <name> <value>
 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
@@ -20,7 +21,6 @@ SYNOPSIS
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
-'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list
 'git config' [<file-option>] --get-color <name> [<default>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
@@ -74,6 +74,12 @@ On success, the command returns the exit code 0.
 A list of all available configuration variables can be obtained using the
 `git help --config` command.
 
+COMMANDS
+--------
+
+list::
+	List all variables set in config file, along with their values.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -178,10 +184,6 @@ See also <<FILES>>.
 --unset-all::
 	Remove all lines matching the key from config file.
 
--l::
---list::
-	List all variables set in config file, along with their values.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -236,7 +238,7 @@ Valid `<type>`'s include:
 	contain line breaks.
 
 --name-only::
-	Output only the names of config variables for `--list` or
+	Output only the names of config variables for `list` or
 	`--get-regexp`.
 
 --show-origin::
@@ -287,10 +289,20 @@ Valid `<type>`'s include:
   When using `--get`, and the requested variable is not found, behave as if
   <value> were the value assigned to the that variable.
 
+DEPRECATED MODES
+----------------
+
+The following modes have been deprecated in favor of subcommands. It is
+recommended to migrate to the new syntax.
+
+-l::
+--list::
+	Replaced by `git config list`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
-using `--list` or any of the `--get-*` which may return multiple results.
+using `list` or any of the `--get-*` which may return multiple results.
 The default is to use a pager.
 
 [[FILES]]
diff --git a/builtin/config.c b/builtin/config.c
index ce2d3fecd4..ee7ac9381e 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -16,10 +16,16 @@
 #include "worktree.h"
 
 static const char *const builtin_config_usage[] = {
+	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config [<options>]"),
 	NULL
 };
 
+static const char *const builtin_config_list_usage[] = {
+	N_("git config list [<file-option>] [<display-option>] [--includes]"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -33,6 +39,7 @@ static char delim = '=';
 static char key_delim = ' ';
 static char term = '\n';
 
+static parse_opt_subcommand_fn *subcommand;
 static int use_global_config, use_system_config, use_local_config;
 static int use_worktree_config;
 static struct git_config_source given_config_source;
@@ -705,14 +712,24 @@ static void handle_nul(void) {
 	}
 }
 
+#define CONFIG_LOCATION_OPTIONS \
+	OPT_GROUP(N_("Config file location")), \
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")), \
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")), \
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")), \
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")), \
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")), \
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object"))
+
+#define CONFIG_DISPLAY_OPTIONS \
+	OPT_GROUP(N_("Display options")), \
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), \
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")), \
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")), \
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)"))
+
 static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	CONFIG_LOCATION_OPTIONS,
 	OPT_GROUP(N_("Action")),
 	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
 	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
@@ -736,14 +753,11 @@ static struct option builtin_config_options[] = {
 	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
 	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
 	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	CONFIG_DISPLAY_OPTIONS,
 	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
 	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
 	OPT_END(),
 };
 
@@ -752,6 +766,42 @@ static NORETURN void usage_builtin_config(void)
 	usage_with_options(builtin_config_usage, builtin_config_options);
 }
 
+static int cmd_config_list(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_DISPLAY_OPTIONS,
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);
+	check_argc(argc, 0, 0);
+
+	handle_config_location(prefix);
+	handle_nul();
+
+	setup_auto_pager("config", 1);
+
+	if (config_with_options(show_all_config, NULL,
+				&given_config_source, the_repository,
+				&config_options) < 0) {
+		if (given_config_source.file)
+			die_errno(_("unable to read config file '%s'"),
+				  given_config_source.file);
+		else
+			die(_("error processing config file(s)"));
+	}
+
+	return 0;
+}
+
+static struct option builtin_subcommand_options[] = {
+	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
+	OPT_END(),
+};
+
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	char *value = NULL;
@@ -761,6 +811,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 
 	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
 
+	/*
+	 * This is somewhat hacky: we first parse the command line while
+	 * keeping all args intact in order to determine whether a subcommand
+	 * has been specified. If so, we re-parse it a second time, but this
+	 * time we drop KEEP_ARGV0. This is so that we don't munge the command
+	 * line in case no subcommand was given, which would otherwise confuse
+	 * us when parsing the implicit modes.
+	 */
+	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
+	if (subcommand) {
+		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
+		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);
+		return subcommand(argc, argv, prefix);
+	}
+
 	argc = parse_options(argc, argv, prefix, builtin_config_options,
 			     builtin_config_usage,
 			     PARSE_OPT_STOP_AT_NON_OPTION);
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 2d1bc1e27e..720f0ee929 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -11,6 +11,21 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
+for mode in legacy subcommands
+do
+
+case "$mode" in
+legacy)
+	mode_prefix="--"
+	;;
+subcommands)
+	mode_prefix=""
+	;;
+*)
+	echo "unknown mode $mode" >&2
+	exit 1;;
+esac
+
 test_expect_success 'clear default config' '
 	rm -f .git/config
 '
@@ -350,11 +365,11 @@ version.1.2.3eX.alpha=beta
 EOF
 
 test_expect_success 'working --list' '
-	git config --list > output &&
+	git config ${mode_prefix}list > output &&
 	test_cmp expect output
 '
 test_expect_success '--list without repo produces empty output' '
-	git --git-dir=nonexistent config --list >output &&
+	git --git-dir=nonexistent config ${mode_prefix}list >output &&
 	test_must_be_empty output
 '
 
@@ -366,7 +381,7 @@ version.1.2.3eX.alpha
 EOF
 
 test_expect_success '--name-only --list' '
-	git config --name-only --list >output &&
+	git config ${mode_prefix}list --name-only >output &&
 	test_cmp expect output
 '
 
@@ -504,17 +519,17 @@ ein.bahn=strasse
 EOF
 
 test_expect_success 'alternative GIT_CONFIG' '
-	GIT_CONFIG=other-config git config --list >output &&
+	GIT_CONFIG=other-config git config ${mode_prefix}list >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'alternative GIT_CONFIG (--file)' '
-	git config --file other-config --list >output &&
+	git config ${mode_prefix}list --file other-config >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'alternative GIT_CONFIG (--file=-)' '
-	git config --file - --list <other-config >output &&
+	git config ${mode_prefix}list --file - <other-config >output &&
 	test_cmp expect output
 '
 
@@ -527,6 +542,7 @@ test_expect_success 'editing stdin is an error' '
 '
 
 test_expect_success 'refer config from subdirectory' '
+	test_when_finished "rm -r x" &&
 	mkdir x &&
 	test_cmp_config -C x strasse --file=../other-config --get ein.bahn
 '
@@ -737,7 +753,7 @@ test_expect_success 'line number is reported correctly' '
 '
 
 test_expect_success 'invalid stdin config' '
-	echo "[broken" | test_must_fail git config --list --file - >output 2>&1 &&
+	echo "[broken" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&
 	test_grep "bad config line 1 in standard input" output
 '
 
@@ -1029,7 +1045,7 @@ section.quotecont=cont;inued
 EOF
 
 test_expect_success 'value continued on next line' '
-	git config --list > result &&
+	git config ${mode_prefix}list > result &&
 	test_cmp expect result
 '
 
@@ -1053,7 +1069,7 @@ Qsection.sub=section.val4
 Qsection.sub=section.val5Q
 EOF
 test_expect_success '--null --list' '
-	git config --null --list >result.raw &&
+	git config ${mode_prefix}list --null >result.raw &&
 	nul_to_q <result.raw >result &&
 	echo >>result &&
 	test_cmp expect result
@@ -1072,6 +1088,7 @@ test_expect_success 'inner whitespace kept verbatim' '
 '
 
 test_expect_success SYMLINKS 'symlinked configuration' '
+	test_when_finished "rm myconfig" &&
 	ln -s notyet myconfig &&
 	git config --file=myconfig test.frotz nitfol &&
 	test -h myconfig &&
@@ -1092,10 +1109,11 @@ test_expect_success SYMLINKS 'symlinked configuration' '
 '
 
 test_expect_success SYMLINKS 'symlink to nonexistent configuration' '
+	test_when_finished "rm linktonada linktolinktonada" &&
 	ln -s doesnotexist linktonada &&
 	ln -s linktonada linktolinktonada &&
-	test_must_fail git config --file=linktonada --list &&
-	test_must_fail git config --file=linktolinktonada --list
+	test_must_fail git config ${mode_prefix}list --file=linktonada &&
+	test_must_fail git config ${mode_prefix}list --file=linktolinktonada
 '
 
 test_expect_success 'check split_cmdline return' '
@@ -1352,7 +1370,7 @@ do
 done
 
 test_expect_success 'git -c is not confused by empty environment' '
-	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config --list
+	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config ${mode_prefix}list
 '
 
 test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
@@ -1543,31 +1561,31 @@ test_expect_success 'git config ignores pairs with empty count' '
 '
 
 test_expect_success 'git config fails with invalid count' '
-	test_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&
+	test_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&
 	test_grep "bogus count" error &&
-	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&
+	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&
 	test_grep "too many entries" error
 '
 
 test_expect_success 'git config fails with missing config key' '
 	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0="value" \
-		git config --list 2>error &&
+		git config ${mode_prefix}list 2>error &&
 	test_grep "missing config key" error
 '
 
 test_expect_success 'git config fails with missing config value' '
 	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="pair.one" \
-		git config --list 2>error &&
+		git config ${mode_prefix}list 2>error &&
 	test_grep "missing config value" error
 '
 
 test_expect_success 'git config fails with invalid config pair key' '
 	test_must_fail env GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \
-		git config --list &&
+		git config ${mode_prefix}list &&
 	test_must_fail env GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \
-		git config --list
+		git config ${mode_prefix}list
 '
 
 test_expect_success 'environment overrides config file' '
@@ -1607,7 +1625,7 @@ test_expect_success 'git config --edit works' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
 	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
-	git config -f tmp --list >actual &&
+	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
 
@@ -1616,7 +1634,7 @@ test_expect_success 'git config --edit respects core.editor' '
 	echo test.value=yes >expect &&
 	test_config core.editor "echo [test]value=yes >" &&
 	git config -f tmp --edit &&
-	git config -f tmp --list >actual &&
+	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
 
@@ -1967,7 +1985,7 @@ test_expect_success '--show-origin with --list' '
 	command line:	user.cmdline=true
 	EOF
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\
-		git -c user.cmdline=true config --list --show-origin >output &&
+		git -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -1984,7 +2002,7 @@ test_expect_success '--show-origin with --list --null' '
 	includeQcommand line:Quser.cmdline
 	trueQ
 	EOF
-	git -c user.cmdline=true config --null --list --show-origin >output.raw &&
+	git -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&
 	nul_to_q <output.raw >output &&
 	# The here-doc above adds a newline that the --null output would not
 	# include. Add it here to make the two comparable.
@@ -1998,7 +2016,7 @@ test_expect_success '--show-origin with single file' '
 	file:.git/config	user.override=local
 	file:.git/config	include.path=../include/relative.include
 	EOF
-	git config --local --list --show-origin >output &&
+	git config ${mode_prefix}list --local --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2036,7 +2054,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '
 	cat >expect <<-\EOF &&
 	file:"file\" (dq) and spaces.conf"	user.custom=true
 	EOF
-	git config --file "$WEIRDLY_NAMED_FILE" --show-origin --list >output &&
+	git config ${mode_prefix}list --file "$WEIRDLY_NAMED_FILE" --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2044,7 +2062,7 @@ test_expect_success '--show-origin stdin' '
 	cat >expect <<-\EOF &&
 	standard input:	user.custom=true
 	EOF
-	git config --file - --show-origin --list <"$CUSTOM_CONFIG_FILE" >output &&
+	git config ${mode_prefix}list --file - --show-origin <"$CUSTOM_CONFIG_FILE" >output &&
 	test_cmp expect output
 '
 
@@ -2071,7 +2089,7 @@ test_expect_success '--show-origin blob' '
 		cat >expect <<-EOF &&
 		blob:$blob	user.custom=true
 		EOF
-		git config --blob=$blob --show-origin --list >output &&
+		git config ${mode_prefix}list --blob=$blob --show-origin >output &&
 		test_cmp expect output
 	)
 '
@@ -2087,7 +2105,7 @@ test_expect_success '--show-origin blob ref' '
 		cp "$CUSTOM_CONFIG_FILE" custom.conf &&
 		git add custom.conf &&
 		git commit -m "new config file" &&
-		git config --blob=main:custom.conf --show-origin --list >output &&
+		git config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&
 		test_cmp expect output
 	)
 '
@@ -2113,13 +2131,14 @@ test_expect_success '--show-scope with --list' '
 	worktree	user.worktree=true
 	command	user.cmdline=true
 	EOF
+	test_when_finished "git worktree remove wt1" &&
 	git worktree add wt1 &&
 	# We need these to test for worktree scope, but outside of this
 	# test, this is just noise
 	test_config core.repositoryformatversion 1 &&
 	test_config extensions.worktreeConfig true &&
 	git config --worktree user.worktree true &&
-	git -c user.cmdline=true config --list --show-scope >output &&
+	git -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2128,7 +2147,7 @@ test_expect_success !MINGW '--show-scope with --blob' '
 	cat >expect <<-EOF &&
 	command	user.custom=true
 	EOF
-	git config --blob=$blob --show-scope --list >output &&
+	git config ${mode_prefix}list --blob=$blob --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2138,7 +2157,7 @@ test_expect_success '--show-scope with --local' '
 	local	user.override=local
 	local	include.path=../include/relative.include
 	EOF
-	git config --local --list --show-scope >output &&
+	git config ${mode_prefix}list --local --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2162,7 +2181,7 @@ test_expect_success '--show-scope with --show-origin' '
 	local	file:.git/../include/relative.include	user.relative=include
 	command	command line:	user.cmdline=true
 	EOF
-	git -c user.cmdline=true config --list --show-origin --show-scope >output &&
+	git -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2203,7 +2222,7 @@ test_expect_success 'override global and system config' '
 	global	home.config=true
 	local	local.config=true
 	EOF
-	git config --show-scope --list >output &&
+	git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output &&
 
 	cat >expect <<-EOF &&
@@ -2212,20 +2231,20 @@ test_expect_success 'override global and system config' '
 	local	local.config=true
 	EOF
 	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \
-		git config --show-scope --list >output &&
+		git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output &&
 
 	cat >expect <<-EOF &&
 	local	local.config=true
 	EOF
 	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \
-		git config --show-scope --list >output &&
+		git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'override global and system config with missing file' '
-	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&
-	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&
+	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&
+	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&
 	GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version
 '
 
@@ -2352,7 +2371,7 @@ test_expect_success 'set all config with value-pattern' '
 	# no match => add new entry
 	cp initial config &&
 	git config --file=config abc.key two a+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2365,7 +2384,7 @@ test_expect_success 'set all config with value-pattern' '
 
 	# multiple values, no match => add
 	git config --file=config abc.key three a+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2375,7 +2394,7 @@ test_expect_success 'set all config with value-pattern' '
 
 	# single match => replace
 	git config --file=config abc.key four h+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2390,7 +2409,7 @@ test_expect_success '--replace-all and value-pattern' '
 	git config --file=config --add abc.key two &&
 	git config --file=config --add abc.key three &&
 	git config --file=config --replace-all abc.key four "o+" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=four
 	abc.key=three
@@ -2408,7 +2427,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --rename-section dev null &&
 	test_must_fail git config --file=config --fixed-value --remove-section dev &&
-	test_must_fail git config --file=config --fixed-value --list &&
+	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
 
@@ -2429,7 +2448,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
 	fixed.test=bogus
@@ -2438,7 +2457,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config --fixed-value fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	fixed.test=bogus
 	EOF
@@ -2456,7 +2475,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config --replace-all fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
 	fixed.test=bogus
@@ -2464,7 +2483,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 	test_cmp expect actual &&
 
 	git config --file=config --fixed-value --replace-all fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=bogus
 	fixed.test=bogus
@@ -2625,4 +2644,6 @@ test_expect_success 'specifying multiple modes causes failure' '
 	test_cmp expect err
 '
 
+done
+
 test_done
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 07/13] builtin/config: introduce "get" subcommand
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                     ` (5 preceding siblings ...)
  2024-03-11 23:20   ` [PATCH v2 06/13] builtin/config: introduce "list" subcommand Patrick Steinhardt
@ 2024-03-11 23:20   ` Patrick Steinhardt
  2024-03-13  3:11     ` Eric Sunshine
  2024-03-11 23:20   ` [PATCH v2 08/13] builtin/config: introduce "set" subcommand Patrick Steinhardt
                     ` (5 subsequent siblings)
  12 siblings, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:20 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 32322 bytes --]

Introduce a new "get" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt |  89 +++++++++++++------------
 builtin/config.c             |  69 ++++++++++++++++---
 t/t1300-config.sh            | 125 ++++++++++++++++++++++++-----------
 3 files changed, 192 insertions(+), 91 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 976ba26757..f3d5e3e613 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -10,18 +10,14 @@ SYNOPSIS
 --------
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
+'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
 'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
 'git config' [<file-option>] [--type=<type>] --add <name> <value>
 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL>
 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
-'git config' [<file-option>] --get-color <name> [<default>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
 
@@ -80,6 +76,12 @@ COMMANDS
 list::
 	List all variables set in config file, along with their values.
 
+get::
+	Get value for one or more config options. Values can be filtered by
+	regexes and URLs.Returns error code 1 if the key was not found and the
+	last value if multiple key values were found. If `--all` is set, then
+	all values will be shown.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -93,22 +95,16 @@ OPTIONS
 	values.  This is the same as providing '^$' as the `value-pattern`
 	in `--replace-all`.
 
---get::
-	Get the value for a given key (optionally filtered by a regex
-	matching the value). Returns error code 1 if the key was not
-	found and the last value if multiple key values were found.
-
---get-all::
-	Like get, but returns all values for a multi-valued key.
+--all::
+	With "get", Return all values for a multi-valued key.
 
---get-regexp::
-	Like --get-all, but interprets the name as a regular expression and
-	writes out the key names.  Regular expression matching is currently
-	case-sensitive and done against a canonicalized version of the key
-	in which section and variable names are lowercased, but subsection
-	names are not.
+---regexp::
+	With "get", interpret the name as a regular expression. Regular
+	expression matching is currently case-sensitive and done against a
+	canonicalized version of the key in which section and variable names
+	are lowercased, but subsection names are not.
 
---get-urlmatch <name> <URL>::
+--url=<URL>::
 	When given a two-part <name> as <section>.<key>, the value for
 	<section>.<URL>.<key> whose <URL> part matches the best to the
 	given URL is returned (if no such key exists, the value for
@@ -239,7 +235,7 @@ Valid `<type>`'s include:
 
 --name-only::
 	Output only the names of config variables for `list` or
-	`--get-regexp`.
+	`get`.
 
 --show-origin::
 	Augment the output of all queried config options with the
@@ -263,17 +259,6 @@ Valid `<type>`'s include:
 	When the color setting for `name` is undefined, the command uses
 	`color.ui` as fallback.
 
---get-color <name> [<default>]::
-
-	Find the color configured for `name` (e.g. `color.diff.new`) and
-	output it as the ANSI color escape sequence to the standard
-	output.  The optional `default` parameter is used instead, if
-	there is no color configured for `name`.
-+
-`--type=color [--default=<default>]` is preferred over `--get-color`
-(but note that `--get-color` will omit the trailing newline printed by
-`--type=color`).
-
 -e::
 --edit::
 	Opens an editor to modify the specified config file; either
@@ -286,7 +271,7 @@ Valid `<type>`'s include:
 	config files.
 
 --default <value>::
-  When using `--get`, and the requested variable is not found, behave as if
+  When using `get`, and the requested variable is not found, behave as if
   <value> were the value assigned to the that variable.
 
 DEPRECATED MODES
@@ -295,15 +280,33 @@ DEPRECATED MODES
 The following modes have been deprecated in favor of subcommands. It is
 recommended to migrate to the new syntax.
 
+'git config <name>'::
+	Replaced by `git config get <name>`.
+
 -l::
 --list::
 	Replaced by `git config list`.
 
+--get <name> [<value-pattern>]::
+	Replaced by `git config get [--value=<pattern>] <name>`.
+
+--get-all <name> [<value-pattern>]::
+	Replaced by `git config get [--value=<pattern>] --all --show-names <name>`.
+
+--get-regexp <name-regexp>::
+	Replaced by `git config get --all --show-names --regexp <name-regexp>`.
+
+--get-urlmatch <name> <URL>::
+	Replaced by `git config get --all --show-names --url=<URL> <name>`.
+
+--get-color <name> [<default>]::
+	Replaced by `git config get --type=color [--default=<default>] <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
-using `list` or any of the `--get-*` which may return multiple results.
-The default is to use a pager.
+using `list` or `get` which may return multiple results. The default is to use
+a pager.
 
 [[FILES]]
 FILES
@@ -506,25 +509,25 @@ you have to provide a regex matching the value of exactly one line.
 To query the value for a given key, do
 
 ------------
-% git config --get core.filemode
+% git config get core.filemode
 ------------
 
 or
 
 ------------
-% git config core.filemode
+% git config get core.filemode
 ------------
 
 or, to query a multivar:
 
 ------------
-% git config --get core.gitproxy "for kernel.org$"
+% git config get --value="for kernel.org$" core.gitproxy
 ------------
 
 If you want to know all the values for a multivar, do:
 
 ------------
-% git config --get-all core.gitproxy
+% git config get --all --show-names core.gitproxy
 ------------
 
 If you like to live dangerously, you can replace *all* core.gitproxy by a
@@ -558,8 +561,8 @@ script:
 
 ------------
 #!/bin/sh
-WS=$(git config --get-color color.diff.whitespace "blue reverse")
-RESET=$(git config --get-color "" "reset")
+WS=$(git config get --type=color --default="blue reverse" color.diff.whitespace)
+RESET=$(git config get --type=color --default="reset" "")
 echo "${WS}your whitespace color or blue reverse${RESET}"
 ------------
 
@@ -567,11 +570,11 @@ For URLs in `https://weak.example.com`, `http.sslVerify` is set to
 false, while it is set to `true` for all others:
 
 ------------
-% git config --type=bool --get-urlmatch http.sslverify https://good.example.com
+% git config get --type=bool --url=https://good.example.com http.sslverify
 true
-% git config --type=bool --get-urlmatch http.sslverify https://weak.example.com
+% git config get --type=bool --url=https://weak.example.com http.sslverify
 false
-% git config --get-urlmatch http https://weak.example.com
+% git config get --url=https://weak.example.com http
 http.cookieFile /tmp/cookie.txt
 http.sslverify false
 ------------
diff --git a/builtin/config.c b/builtin/config.c
index ee7ac9381e..aaa8b15e86 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -17,7 +17,7 @@
 
 static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
-	N_("git config [<options>]"),
+	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	NULL
 };
 
@@ -26,6 +26,11 @@ static const char *const builtin_config_list_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_get_usage[] = {
+	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -721,6 +726,16 @@ static void handle_nul(void) {
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")), \
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object"))
 
+#define CONFIG_TYPE_OPTIONS \
+	OPT_GROUP(N_("Type")), \
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type), \
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL), \
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT), \
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT), \
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR), \
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH), \
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE)
+
 #define CONFIG_DISPLAY_OPTIONS \
 	OPT_GROUP(N_("Display options")), \
 	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), \
@@ -745,14 +760,7 @@ static struct option builtin_config_options[] = {
 	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
 	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
 	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	CONFIG_TYPE_OPTIONS,
 	CONFIG_DISPLAY_OPTIONS,
 	OPT_GROUP(N_("Other")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
@@ -797,8 +805,51 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix)
 	return 0;
 }
 
+static int cmd_config_get(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL, *url = NULL;
+	int flags = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_TYPE_OPTIONS,
+		OPT_GROUP(N_("Filter options")),
+		OPT_BOOL(0, "all", &do_all, N_("return all values for multi-valued config options")),
+		OPT_BOOL(0, "regexp", &use_key_regexp, N_("interpret the name as a regular expression")),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_STRING(0, "url", &url, N_("URL"), N_("show config matching the given URL")),
+		CONFIG_DISPLAY_OPTIONS,
+		OPT_BOOL(0, "show-names", &show_keys, N_("show config keys in addition to their values")),
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+		OPT_STRING(0, "default", &default_value, N_("value"), N_("use default value when missing entry")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_get_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_argc(argc, 1, 1);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with 'value-pattern'"));
+	if (default_value && (do_all || url))
+		die(_("--default= cannot be used with --all or --url="));
+	if (url && (do_all || use_key_regexp || value_pattern))
+		die(_("--url= cannot be used with --all, --regexp or --value"));
+
+	handle_config_location(prefix);
+	handle_nul();
+
+	setup_auto_pager("config", 1);
+
+	if (url)
+		return get_urlmatch(argv[0], url);
+	return get_value(argv[0], value_pattern, flags);
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
+	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 720f0ee929..7f6746936e 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -17,9 +17,15 @@ do
 case "$mode" in
 legacy)
 	mode_prefix="--"
+	mode_get=""
+	mode_get_all="--get-all"
+	mode_get_regexp="--get-regexp"
 	;;
 subcommands)
 	mode_prefix=""
+	mode_get="get"
+	mode_get_all="get --all"
+	mode_get_regexp="get --regexp --all --show-names"
 	;;
 *)
 	echo "unknown mode $mode" >&2
@@ -289,7 +295,7 @@ test_expect_success 'multi-valued get-all returns all' '
 	wow
 	wow2 for me
 	EOF
-	git config --get-all nextsection.nonewline >actual &&
+	git config ${mode_get_all} nextsection.nonewline >actual &&
 	test_cmp expect actual
 '
 
@@ -391,7 +397,7 @@ nextsection.nonewline wow2 for me
 EOF
 
 test_expect_success '--get-regexp' '
-	git config --get-regexp in >output &&
+	git config ${mode_get_regexp} in >output &&
 	test_cmp expect output
 '
 
@@ -401,7 +407,7 @@ nextsection.nonewline
 EOF
 
 test_expect_success '--name-only --get-regexp' '
-	git config --name-only --get-regexp in >output &&
+	git config ${mode_get_regexp} --name-only in >output &&
 	test_cmp expect output
 '
 
@@ -412,7 +418,7 @@ EOF
 
 test_expect_success '--add' '
 	git config --add nextsection.nonewline "wow4 for you" &&
-	git config --get-all nextsection.nonewline > output &&
+	git config ${mode_get_all} nextsection.nonewline > output &&
 	test_cmp expect output
 '
 
@@ -434,21 +440,21 @@ test_expect_success 'get variable with empty value' '
 echo novalue.variable > expect
 
 test_expect_success 'get-regexp variable with no value' '
-	git config --get-regexp novalue > output &&
+	git config ${mode_get_regexp} novalue > output &&
 	test_cmp expect output
 '
 
 echo 'novalue.variable true' > expect
 
 test_expect_success 'get-regexp --bool variable with no value' '
-	git config --bool --get-regexp novalue > output &&
+	git config ${mode_get_regexp} --bool novalue > output &&
 	test_cmp expect output
 '
 
 echo 'emptyvalue.variable ' > expect
 
 test_expect_success 'get-regexp variable with empty value' '
-	git config --get-regexp emptyvalue > output &&
+	git config ${mode_get_regexp} emptyvalue > output &&
 	test_cmp expect output
 '
 
@@ -1022,7 +1028,7 @@ test_expect_success 'quoting' '
 '
 
 test_expect_success 'key with newline' '
-	test_must_fail git config "key.with
+	test_must_fail git config ${mode_get} "key.with
 newline" 123'
 
 test_expect_success 'value with newline' 'git config key.sub value.with\\\
@@ -1076,7 +1082,7 @@ test_expect_success '--null --list' '
 '
 
 test_expect_success '--null --get-regexp' '
-	git config --null --get-regexp "val[0-9]" >result.raw &&
+	git config ${mode_get_regexp} --null "val[0-9]" >result.raw &&
 	nul_to_q <result.raw >result &&
 	echo >>result &&
 	test_cmp expect result
@@ -1158,11 +1164,11 @@ test_expect_success 'git -c can represent empty string' '
 '
 
 test_expect_success 'key sanity-checking' '
-	test_must_fail git config foo=bar &&
-	test_must_fail git config foo=.bar &&
-	test_must_fail git config foo.ba=r &&
-	test_must_fail git config foo.1bar &&
-	test_must_fail git config foo."ba
+	test_must_fail git config ${mode_get} foo=bar &&
+	test_must_fail git config ${mode_get} foo=.bar &&
+	test_must_fail git config ${mode_get} foo.ba=r &&
+	test_must_fail git config ${mode_get} foo.1bar &&
+	test_must_fail git config ${mode_get} foo."ba
 				z".bar &&
 	test_must_fail git config . false &&
 	test_must_fail git config .foo false &&
@@ -1211,7 +1217,7 @@ test_expect_success 'git -c complains about empty key and value' '
 '
 
 test_expect_success 'multiple git -c appends config' '
-	test_config alias.x "!git -c x.two=2 config --get-regexp ^x\.*" &&
+	test_config alias.x "!git -c x.two=2 config ${mode_get_regexp} ^x\.*" &&
 	cat >expect <<-\EOF &&
 	x.one 1
 	x.two 2
@@ -1377,7 +1383,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
 	v="${SQ}key.one=foo${SQ}" &&
 	v="$v  ${SQ}key.two=bar${SQ}" &&
 	v="$v ${SQ}key.ambiguous=section.whatever=value${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.one foo
 	key.two bar
@@ -1390,7 +1396,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '
 	v="${SQ}key.one${SQ}=${SQ}foo${SQ}" &&
 	v="$v  ${SQ}key.two${SQ}=${SQ}bar${SQ}" &&
 	v="$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.one foo
 	key.two bar
@@ -1404,7 +1410,7 @@ test_expect_success 'old and new-style entries can mix' '
 	v="$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}" &&
 	v="$v ${SQ}key.oldtwo=oldbar${SQ}" &&
 	v="$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.oldone oldfoo
 	key.newone newfoo
@@ -1417,7 +1423,7 @@ test_expect_success 'old and new-style entries can mix' '
 test_expect_success 'old and new bools with ambiguous subsection' '
 	v="${SQ}key.with=equals.oldbool${SQ}" &&
 	v="$v ${SQ}key.with=equals.newbool${SQ}=" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.with equals.oldbool
 	key.with=equals.newbool
@@ -1431,7 +1437,7 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
 	env.two two
 	EOF
 	GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ} ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*" >actual &&
+		git config ${mode_get_regexp} "env.*" >actual &&
 	test_cmp expect actual &&
 
 	cat >expect <<-EOF &&
@@ -1439,12 +1445,12 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
 	env.two two
 	EOF
 	GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ}\\$SQ$SQ$SQ ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*" >actual &&
+		git config ${mode_get_regexp} "env.*" >actual &&
 	test_cmp expect actual &&
 
 	test_must_fail env \
 		GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ}\\$SQ ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*"
+		git config ${mode_get_regexp} "env.*"
 '
 
 test_expect_success 'git --config-env=key=envvar support' '
@@ -1492,7 +1498,7 @@ test_expect_success 'git -c and --config-env work together' '
 	ENVVAR=env-value git \
 		-c bar.cmd=cmd-value \
 		--config-env=bar.env=ENVVAR \
-		config --get-regexp "^bar.*" >actual &&
+		config ${mode_get_regexp} "^bar.*" >actual &&
 	test_cmp expect actual
 '
 
@@ -1520,7 +1526,7 @@ test_expect_success 'git config handles environment config pairs' '
 	GIT_CONFIG_COUNT=2 \
 		GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="foo" \
 		GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="bar" \
-		git config --get-regexp "pair.*" >actual &&
+		git config ${mode_get_regexp} "pair.*" >actual &&
 	cat >expect <<-EOF &&
 	pair.one foo
 	pair.two bar
@@ -1530,7 +1536,7 @@ test_expect_success 'git config handles environment config pairs' '
 
 test_expect_success 'git config ignores pairs without count' '
 	test_must_fail env GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
@@ -1538,7 +1544,7 @@ test_expect_success 'git config ignores pairs exceeding count' '
 	GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
 		GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="value" \
-		git config --get-regexp "pair.*" >actual 2>error &&
+		git config ${mode_get_regexp} "pair.*" >actual 2>error &&
 	cat >expect <<-EOF &&
 	pair.one value
 	EOF
@@ -1549,14 +1555,14 @@ test_expect_success 'git config ignores pairs exceeding count' '
 test_expect_success 'git config ignores pairs with zero count' '
 	test_must_fail env \
 		GIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
 test_expect_success 'git config ignores pairs with empty count' '
 	test_must_fail env \
 		GIT_CONFIG_COUNT= GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
@@ -1595,7 +1601,7 @@ test_expect_success 'environment overrides config file' '
 	one = value
 	EOF
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \
-		git config pair.one >actual &&
+		git config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1605,7 +1611,7 @@ test_expect_success 'environment overrides config file' '
 test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \
 		GIT_CONFIG_PARAMETERS="${SQ}pair.one=override${SQ}" \
-		git config pair.one >actual &&
+		git config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1680,20 +1686,28 @@ test_expect_success 'urlmatch' '
 
 	test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&
 	test_must_be_empty actual &&
+	test_expect_code 1 git config get --url=https://good.example.com --bool doesnt.exist >actual &&
+	test_must_be_empty actual &&
 
 	echo true >expect &&
 	git config --bool --get-urlmatch http.SSLverify https://good.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --bool --url=https://good.example.com http.SSLverify >actual &&
+	test_cmp expect actual &&
 
 	echo false >expect &&
 	git config --bool --get-urlmatch http.sslverify https://weak.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --bool --url=https://weak.example.com http.sslverify >actual &&
+	test_cmp expect actual &&
 
 	{
 		echo http.cookiefile /tmp/cookie.txt &&
 		echo http.sslverify false
 	} >expect &&
 	git config --get-urlmatch HTTP https://weak.example.com >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://weak.example.com HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -1709,6 +1723,8 @@ test_expect_success 'urlmatch with --show-scope' '
 	local	http.sslverify false
 	EOF
 	git config --get-urlmatch --show-scope HTTP https://weak.example.com >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://weak.example.com --show-scope HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -1741,45 +1757,67 @@ test_expect_success 'urlmatch favors more specific URLs' '
 	echo http.cookiefile /tmp/root.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com/subdirectory >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com/subdirectory HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com/subdirectory/nested HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/user.txt >expect &&
 	git config --get-urlmatch HTTP https://user@example.com/ >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://user@example.com/ HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://averylonguser@example.com/subdirectory HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/preceding.txt >expect &&
 	git config --get-urlmatch HTTP https://preceding.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://preceding.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/wildcard.txt >expect &&
 	git config --get-urlmatch HTTP https://wildcard.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://wildcard.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/sub.txt >expect &&
 	git config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://sub.example.com/wildcardwithsubdomain HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/trailing.txt >expect &&
 	git config --get-urlmatch HTTP https://trailing.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://trailing.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/sub.txt >expect &&
 	git config --get-urlmatch HTTP https://user@sub.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://user@sub.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/multiwildcard.txt >expect &&
 	git config --get-urlmatch HTTP https://wildcard.example.org >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://wildcard.example.org HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -1902,7 +1940,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[one]
 	EOF
 	git config --unset two.subsection.key &&
-	test "not [two subsection]" = "$(git config one.key)" &&
+	test "not [two subsection]" = "$(git config ${mode_get} one.key)" &&
 	test_line_count = 3 .git/config
 '
 
@@ -2025,7 +2063,7 @@ test_expect_success '--show-origin with --get-regexp' '
 	file:$HOME/.gitconfig	user.global true
 	file:.git/config	user.local true
 	EOF
-	git config --show-origin --get-regexp "user\.[g|l].*" >output &&
+	git config ${mode_get_regexp} --show-origin "user\.[g|l].*" >output &&
 	test_cmp expect output
 '
 
@@ -2033,7 +2071,7 @@ test_expect_success '--show-origin getting a single key' '
 	cat >expect <<-\EOF &&
 	file:.git/config	local
 	EOF
-	git config --show-origin user.override >output &&
+	git config ${mode_get} --show-origin user.override >output &&
 	test_cmp expect output
 '
 
@@ -2165,7 +2203,7 @@ test_expect_success '--show-scope getting a single value' '
 	cat >expect <<-\EOF &&
 	local	true
 	EOF
-	git config --show-scope --get user.local >output &&
+	git config ${mode_get} --show-scope user.local >output &&
 	test_cmp expect output
 '
 
@@ -2434,9 +2472,9 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	# These modes complain when --fixed-value has no value-pattern
 	test_must_fail git config --file=config --fixed-value dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --get dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-all dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-regexp "dev.*" &&
+	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
 	test_must_fail git config --file=config --fixed-value --unset dev.null &&
 	test_must_fail git config --file=config --fixed-value --unset-all dev.null
 '
@@ -2466,12 +2504,12 @@ test_expect_success '--fixed-value uses exact string matching' '
 	cp initial config &&
 	test_must_fail git config --file=config --unset fixed.test "$META" &&
 	git config --file=config --fixed-value --unset fixed.test "$META" &&
-	test_must_fail git config --file=config fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
 	test_must_fail git config --file=config --unset-all fixed.test "$META" &&
 	git config --file=config --fixed-value --unset-all fixed.test "$META" &&
-	test_must_fail git config --file=config fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
 	git config --file=config --replace-all fixed.test bogus "$META" &&
@@ -2498,18 +2536,27 @@ test_expect_success '--get and --get-all with --fixed-value' '
 	git config --file=config --add fixed.test "$META" &&
 
 	git config --file=config --get fixed.test bogus &&
+	git config get --file=config --value=bogus fixed.test &&
 	test_must_fail git config --file=config --get fixed.test "$META" &&
+	test_must_fail git config get --file=config --value="$META" fixed.test &&
 	git config --file=config --get --fixed-value fixed.test "$META" &&
+	git config get --file=config --fixed-value --value="$META" fixed.test &&
 	test_must_fail git config --file=config --get --fixed-value fixed.test non-existent &&
 
 	git config --file=config --get-all fixed.test bogus &&
+	git config get --all --file=config --value=bogus fixed.test &&
 	test_must_fail git config --file=config --get-all fixed.test "$META" &&
+	test_must_fail git config get --all --file=config --value="$META" fixed.test &&
 	git config --file=config --get-all --fixed-value fixed.test "$META" &&
+	git config get --all --file=config --value="$META" --fixed-value fixed.test &&
 	test_must_fail git config --file=config --get-all --fixed-value fixed.test non-existent &&
 
 	git config --file=config --get-regexp fixed+ bogus &&
+	git config get --regexp --file=config --value=bogus fixed+ &&
 	test_must_fail git config --file=config --get-regexp fixed+ "$META" &&
+	test_must_fail git config get --regexp --file=config --value="$META" fixed+ &&
 	git config --file=config --get-regexp --fixed-value fixed+ "$META" &&
+	git config get --regexp --file=config --fixed-value --value="$META" fixed+ &&
 	test_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent
 '
 
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 08/13] builtin/config: introduce "set" subcommand
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                     ` (6 preceding siblings ...)
  2024-03-11 23:20   ` [PATCH v2 07/13] builtin/config: introduce "get" subcommand Patrick Steinhardt
@ 2024-03-11 23:20   ` Patrick Steinhardt
  2024-03-11 23:21   ` [PATCH v2 09/13] builtin/config: introduce "unset" subcommand Patrick Steinhardt
                     ` (4 subsequent siblings)
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:20 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 18708 bytes --]

Introduce a new "set" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 38 +++++++++------
 builtin/config.c             | 57 ++++++++++++++++++++++
 t/t1300-config.sh            | 92 +++++++++++++++++++-----------------
 3 files changed, 129 insertions(+), 58 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index f3d5e3e613..e540f41b56 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -11,9 +11,7 @@ SYNOPSIS
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
-'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
-'git config' [<file-option>] [--type=<type>] --add <name> <value>
-'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
+'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
@@ -27,7 +25,7 @@ You can query/set/replace/unset options with this command. The name is
 actually the section and the key separated by a dot, and the value will be
 escaped.
 
-Multiple lines can be added to an option by using the `--add` option.
+Multiple lines can be added to an option by using the `--append` option.
 If you want to update or unset an option which can occur on multiple
 lines, a `value-pattern` (which is an extended regular expression,
 unless the `--fixed-value` option is given) needs to be given.  Only the
@@ -82,6 +80,13 @@ get::
 	last value if multiple key values were found. If `--all` is set, then
 	all values will be shown.
 
+set::
+	Set value for one or more config options. By default, this command
+	refuses to write multi-valued config options. Passing `--all` will
+	replace all multi-valued config options with the new value, whereas
+	`--value=` will replace all config options whose values match the given
+	pattern.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -90,10 +95,9 @@ OPTIONS
 	Default behavior is to replace at most one line. This replaces
 	all lines matching the key (and optionally the `value-pattern`).
 
---add::
+--append::
 	Adds a new line to the option without altering any existing
-	values.  This is the same as providing '^$' as the `value-pattern`
-	in `--replace-all`.
+	values. This is the same as providing '--value=^$' in `set`.
 
 --all::
 	With "get", Return all values for a multi-valued key.
@@ -283,6 +287,9 @@ recommended to migrate to the new syntax.
 'git config <name>'::
 	Replaced by `git config get <name>`.
 
+'git config <name> <value> [<value-pattern>]'::
+	Replaced by `git config set [--value=<pattern>] <name> <value>`.
+
 -l::
 --list::
 	Replaced by `git config list`.
@@ -302,6 +309,9 @@ recommended to migrate to the new syntax.
 --get-color <name> [<default>]::
 	Replaced by `git config get --type=color [--default=<default>] <name>`.
 
+--add <name> <value>::
+	Replaced by `git config set --append <name> <value>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
@@ -348,7 +358,7 @@ precedence over values read earlier.  When multiple values are taken then all
 values of a key from all files will be used.
 
 By default, options are only written to the repository specific
-configuration file. Note that this also affects options like `--replace-all`
+configuration file. Note that this also affects options like `set`
 and `--unset`. *'git config' will only ever change one file at a time*.
 
 You can limit which configuration sources are read from or written to by
@@ -484,7 +494,7 @@ Given a .git/config like this:
 you can set the filemode to true with
 
 ------------
-% git config core.filemode true
+% git config set core.filemode true
 ------------
 
 The hypothetical proxy command entries actually have a postfix to discern
@@ -492,7 +502,7 @@ what URL they apply to. Here is how to change the entry for kernel.org
 to "ssh".
 
 ------------
-% git config core.gitproxy '"ssh" for kernel.org' 'for kernel.org$'
+% git config set --value='for kernel.org$' core.gitproxy '"ssh" for kernel.org'
 ------------
 
 This makes sure that only the key/value pair for kernel.org is replaced.
@@ -534,26 +544,26 @@ If you like to live dangerously, you can replace *all* core.gitproxy by a
 new one with
 
 ------------
-% git config --replace-all core.gitproxy ssh
+% git config set --all core.gitproxy ssh
 ------------
 
 However, if you really only want to replace the line for the default proxy,
 i.e. the one without a "for ..." postfix, do something like this:
 
 ------------
-% git config core.gitproxy ssh '! for '
+% git config set --value='! for ' core.gitproxy ssh
 ------------
 
 To actually match only values with an exclamation mark, you have to
 
 ------------
-% git config section.key value '[!]'
+% git config set --value='[!]' section.key value
 ------------
 
 To add a new proxy, without altering any of the existing ones, use
 
 ------------
-% git config --add core.gitproxy '"proxy-command" for example.com'
+% git config set --append core.gitproxy '"proxy-command" for example.com'
 ------------
 
 An example to use customized color from the configuration in your
diff --git a/builtin/config.c b/builtin/config.c
index aaa8b15e86..18f6dc69d5 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -18,6 +18,7 @@
 static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
+	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	NULL
 };
 
@@ -31,6 +32,11 @@ static const char *const builtin_config_get_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_set_usage[] = {
+	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -847,9 +853,60 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix)
 	return get_value(argv[0], value_pattern, flags);
 }
 
+static int cmd_config_set(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL;
+	int flags = 0, append = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_TYPE_OPTIONS,
+		OPT_GROUP(N_("Filter")),
+		OPT_BIT(0, "all", &flags, N_("replace multi-valued config option with new value"), CONFIG_FLAGS_MULTI_REPLACE),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "append", &append, N_("add a new line without altering any existing values")),
+		OPT_END(),
+	};
+	struct key_value_info default_kvi = KVI_INIT;
+	char *value;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 2, 2);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with --value=<pattern>"));
+	if (append && value_pattern)
+		die(_("--append cannot be used with --value=<pattern>"));
+	if (append)
+		value_pattern = CONFIG_REGEX_NONE;
+
+	handle_config_location(prefix);
+
+	value = normalize_value(argv[0], argv[1], &default_kvi);
+
+	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {
+		ret = git_config_set_multivar_in_file_gently(given_config_source.file,
+							     argv[0], value, value_pattern,
+							     flags);
+	} else {
+		ret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);
+		if (ret == CONFIG_NOTHING_SET)
+			error(_("cannot overwrite multiple values with a single value\n"
+			"       Use a regexp, --add or --replace-all to change %s."), argv[0]);
+	}
+
+	free(value);
+	return ret;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
+	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 7f6746936e..2c2d97e0e9 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -20,12 +20,16 @@ legacy)
 	mode_get=""
 	mode_get_all="--get-all"
 	mode_get_regexp="--get-regexp"
+	mode_set=""
+	mode_replace_all="--replace-all"
 	;;
 subcommands)
 	mode_prefix=""
 	mode_get="get"
 	mode_get_all="get --all"
 	mode_get_regexp="get --regexp --all --show-names"
+	mode_set="set"
+	mode_replace_all="set --all"
 	;;
 *)
 	echo "unknown mode $mode" >&2
@@ -41,7 +45,7 @@ cat > expect << EOF
 	penguin = little blue
 EOF
 test_expect_success 'initial' '
-	git config section.penguin "little blue" &&
+	git config ${mode_set} section.penguin "little blue" &&
 	test_cmp expect .git/config
 '
 
@@ -51,7 +55,7 @@ cat > expect << EOF
 	Movie = BadPhysics
 EOF
 test_expect_success 'mixed case' '
-	git config Section.Movie BadPhysics &&
+	git config ${mode_set} Section.Movie BadPhysics &&
 	test_cmp expect .git/config
 '
 
@@ -63,7 +67,7 @@ cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'similar section' '
-	git config Sections.WhatEver Second &&
+	git config ${mode_set} Sections.WhatEver Second &&
 	test_cmp expect .git/config
 '
 
@@ -76,7 +80,7 @@ cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'uppercase section' '
-	git config SECTION.UPPERCASE true &&
+	git config ${mode_set} SECTION.UPPERCASE true &&
 	test_cmp expect .git/config
 '
 
@@ -192,14 +196,14 @@ test_expect_success 'multiple unset is correct' '
 cp .git/config2 .git/config
 
 test_expect_success '--replace-all missing value' '
-	test_must_fail git config --replace-all beta.haha &&
+	test_must_fail git config ${mode_replace_all} beta.haha &&
 	test_cmp .git/config2 .git/config
 '
 
 rm .git/config2
 
 test_expect_success '--replace-all' '
-	git config --replace-all beta.haha gamma
+	git config ${mode_replace_all} beta.haha gamma
 '
 
 cat > expect << EOF
@@ -226,7 +230,7 @@ noIndent= sillyValue ; 'nother silly comment
 [nextSection] noNewline = ouch
 EOF
 test_expect_success 'really mean test' '
-	git config beta.haha alpha &&
+	git config ${mode_set} beta.haha alpha &&
 	test_cmp expect .git/config
 '
 
@@ -241,7 +245,7 @@ noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'really really mean test' '
-	git config nextsection.nonewline wow &&
+	git config ${mode_set} nextsection.nonewline wow &&
 	test_cmp expect .git/config
 '
 
@@ -715,16 +719,16 @@ EOF
 
 test_expect_success 'section ending' '
 	rm -f .git/config &&
-	git config gitcvs.enabled true &&
-	git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
-	git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.enabled true &&
+	git config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
 	test_cmp expect .git/config
 
 '
 
 test_expect_success numbers '
-	git config kilo.gram 1k &&
-	git config mega.ton 1m &&
+	git config ${mode_set} kilo.gram 1k &&
+	git config ${mode_set} mega.ton 1m &&
 	echo 1024 >expect &&
 	echo 1048576 >>expect &&
 	git config --int --get kilo.gram >actual &&
@@ -733,20 +737,20 @@ test_expect_success numbers '
 '
 
 test_expect_success '--int is at least 64 bits' '
-	git config giga.watts 121g &&
+	git config ${mode_set} giga.watts 121g &&
 	echo  >expect &&
 	test_cmp_config 129922760704 --int --get giga.watts
 '
 
 test_expect_success 'invalid unit' '
-	git config aninvalid.unit "1auto" &&
+	git config ${mode_set} aninvalid.unit "1auto" &&
 	test_cmp_config 1auto aninvalid.unit &&
 	test_must_fail git config --int --get aninvalid.unit 2>actual &&
 	test_grep "bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit" actual
 '
 
 test_expect_success 'invalid unit boolean' '
-	git config commit.gpgsign "1true" &&
+	git config ${mode_set} commit.gpgsign "1true" &&
 	test_cmp_config 1true commit.gpgsign &&
 	test_must_fail git config --bool --get commit.gpgsign 2>actual &&
 	test_grep "bad boolean config value .1true. for .commit.gpgsign." actual
@@ -776,14 +780,14 @@ EOF
 
 test_expect_success bool '
 
-	git config bool.true1 01 &&
-	git config bool.true2 -1 &&
-	git config bool.true3 YeS &&
-	git config bool.true4 true &&
-	git config bool.false1 000 &&
-	git config bool.false2 "" &&
-	git config bool.false3 nO &&
-	git config bool.false4 FALSE &&
+	git config ${mode_set} bool.true1 01 &&
+	git config ${mode_set} bool.true2 -1 &&
+	git config ${mode_set} bool.true3 YeS &&
+	git config ${mode_set} bool.true4 true &&
+	git config ${mode_set} bool.false1 000 &&
+	git config ${mode_set} bool.false2 "" &&
+	git config ${mode_set} bool.false3 nO &&
+	git config ${mode_set} bool.false4 FALSE &&
 	rm -f result &&
 	for i in 1 2 3 4
 	do
@@ -794,7 +798,7 @@ test_expect_success bool '
 
 test_expect_success 'invalid bool (--get)' '
 
-	git config bool.nobool foobar &&
+	git config ${mode_set} bool.nobool foobar &&
 	test_must_fail git config --bool --get bool.nobool'
 
 test_expect_success 'invalid bool (set)' '
@@ -983,7 +987,7 @@ test_expect_success 'get --expiry-date' '
 
 test_expect_success 'get --type=color' '
 	rm .git/config &&
-	git config foo.color "red" &&
+	git config ${mode_set} foo.color "red" &&
 	git config --get --type=color foo.color >actual.raw &&
 	test_decode_color <actual.raw >actual &&
 	echo "<RED>" >expect &&
@@ -1020,10 +1024,10 @@ cat > expect << EOF
 EOF
 test_expect_success 'quoting' '
 	rm -f .git/config &&
-	git config quote.leading " test" &&
-	git config quote.ending "test " &&
-	git config quote.semicolon "test;test" &&
-	git config quote.hash "test#test" &&
+	git config ${mode_set} quote.leading " test" &&
+	git config ${mode_set} quote.ending "test " &&
+	git config ${mode_set} quote.semicolon "test;test" &&
+	git config ${mode_set} quote.hash "test#test" &&
 	test_cmp expect .git/config
 '
 
@@ -1031,7 +1035,7 @@ test_expect_success 'key with newline' '
 	test_must_fail git config ${mode_get} "key.with
 newline" 123'
 
-test_expect_success 'value with newline' 'git config key.sub value.with\\\
+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\
 newline'
 
 cat > .git/config <<\EOF
@@ -1089,7 +1093,7 @@ test_expect_success '--null --get-regexp' '
 '
 
 test_expect_success 'inner whitespace kept verbatim' '
-	git config section.val "foo 	  bar" &&
+	git config ${mode_set} section.val "foo 	  bar" &&
 	test_cmp_config "foo 	  bar" section.val
 '
 
@@ -1127,12 +1131,12 @@ test_expect_success 'check split_cmdline return' '
 	git init repo &&
 	(
 		cd repo &&
-		git config alias.split-cmdline-fix "echo \"" &&
+		git config ${mode_set} alias.split-cmdline-fix "echo \"" &&
 		test_must_fail git split-cmdline-fix &&
 		echo foo >foo &&
 		git add foo &&
 		git commit -m "initial commit" &&
-		git config branch.main.mergeoptions "echo \"" &&
+		git config ${mode_set} branch.main.mergeoptions "echo \"" &&
 		test_must_fail git merge main
 	)
 '
@@ -1170,12 +1174,12 @@ test_expect_success 'key sanity-checking' '
 	test_must_fail git config ${mode_get} foo.1bar &&
 	test_must_fail git config ${mode_get} foo."ba
 				z".bar &&
-	test_must_fail git config . false &&
-	test_must_fail git config .foo false &&
-	test_must_fail git config foo. false &&
-	test_must_fail git config .foo. false &&
-	git config foo.bar true &&
-	git config foo."ba =z".bar false
+	test_must_fail git config ${mode_set} . false &&
+	test_must_fail git config ${mode_set} .foo false &&
+	test_must_fail git config ${mode_set} foo. false &&
+	test_must_fail git config ${mode_set} .foo. false &&
+	git config ${mode_set} foo.bar true &&
+	git config ${mode_set} foo."ba =z".bar false
 '
 
 test_expect_success 'git -c works with aliases of builtins' '
@@ -2398,7 +2402,7 @@ test_expect_success '--replace-all does not invent newlines' '
 	[abc]
 	Qkey = b
 	EOF
-	git config --replace-all abc.key b &&
+	git config ${mode_replace_all} abc.key b &&
 	test_cmp expect .git/config
 '
 
@@ -2470,8 +2474,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
 
 	# These modes complain when --fixed-value has no value-pattern
-	test_must_fail git config --file=config --fixed-value dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
+	test_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&
+	test_must_fail git config ${mode_replace_all} --file=config --fixed-value dev.null bogus &&
 	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
@@ -2512,7 +2516,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
-	git config --file=config --replace-all fixed.test bogus "$META" &&
+	git config --file=config fixed.test bogus "$META" &&
 	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 09/13] builtin/config: introduce "unset" subcommand
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                     ` (7 preceding siblings ...)
  2024-03-11 23:20   ` [PATCH v2 08/13] builtin/config: introduce "set" subcommand Patrick Steinhardt
@ 2024-03-11 23:21   ` Patrick Steinhardt
  2024-03-11 23:21   ` [PATCH v2 10/13] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
                     ` (3 subsequent siblings)
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:21 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 11749 bytes --]

Introduce a new "unset" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 25 ++++++++++++--------
 builtin/config.c             | 38 +++++++++++++++++++++++++++++++
 t/t1300-config.sh            | 44 ++++++++++++++++++++++++------------
 3 files changed, 82 insertions(+), 25 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index e540f41b56..42e659ae63 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -12,8 +12,7 @@ SYNOPSIS
 'git config list' [<file-option>] [<display-option>] [--includes]
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
-'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
-'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
+'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
@@ -87,6 +86,12 @@ set::
 	`--value=` will replace all config options whose values match the given
 	pattern.
 
+unset::
+	Unset value for one or more config options. By default, this command
+	refuses to unset multi-valued keys. Passing `--all` will unset all
+	multi-valued config options, whereas `--value` will unset all config
+	options whose values match the given pattern.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -178,12 +183,6 @@ See also <<FILES>>.
 --rename-section::
 	Rename the given section to a new name.
 
---unset::
-	Remove the line matching the key from config file.
-
---unset-all::
-	Remove all lines matching the key from config file.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -312,6 +311,12 @@ recommended to migrate to the new syntax.
 --add <name> <value>::
 	Replaced by `git config set --append <name> <value>`.
 
+--unset <name> [<value-pattern>]::
+	Replaced by `git config unset [--value=<pattern>] <name>`.
+
+--unset-all <name> [<value-pattern>]::
+	Replaced by `git config unset [--value=<pattern>] --all <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
@@ -359,7 +364,7 @@ values of a key from all files will be used.
 
 By default, options are only written to the repository specific
 configuration file. Note that this also affects options like `set`
-and `--unset`. *'git config' will only ever change one file at a time*.
+and `unset`. *'git config' will only ever change one file at a time*.
 
 You can limit which configuration sources are read from or written to by
 specifying the path of a file with the `--file` option, or by specifying a
@@ -510,7 +515,7 @@ This makes sure that only the key/value pair for kernel.org is replaced.
 To delete the entry for renames, do
 
 ------------
-% git config --unset diff.renames
+% git config unset diff.renames
 ------------
 
 If you want to delete an entry for a multivar (like core.gitproxy above),
diff --git a/builtin/config.c b/builtin/config.c
index 18f6dc69d5..e62a09061f 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -19,6 +19,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	NULL
 };
 
@@ -37,6 +38,11 @@ static const char *const builtin_config_set_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_unset_usage[] = {
+	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -903,10 +909,42 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix)
 	return ret;
 }
 
+static int cmd_config_unset(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL;
+	int flags = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_GROUP(N_("Filter")),
+		OPT_BIT(0, "all", &flags, N_("replace multi-valued config option with new value"), CONFIG_FLAGS_MULTI_REPLACE),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_unset_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 1, 1);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with 'value-pattern'"));
+
+	handle_config_location(prefix);
+
+	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)
+		return git_config_set_multivar_in_file_gently(given_config_source.file,
+							      argv[0], NULL, value_pattern,
+							      flags);
+	else
+		return git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
+	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 2c2d97e0e9..06d34b1d6a 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -22,6 +22,8 @@ legacy)
 	mode_get_regexp="--get-regexp"
 	mode_set=""
 	mode_replace_all="--replace-all"
+	mode_unset="--unset"
+	mode_unset_all="--unset-all"
 	;;
 subcommands)
 	mode_prefix=""
@@ -30,6 +32,8 @@ subcommands)
 	mode_get_regexp="get --regexp --all --show-names"
 	mode_set="set"
 	mode_replace_all="set --all"
+	mode_unset="unset"
+	mode_unset_all="unset --all"
 	;;
 *)
 	echo "unknown mode $mode" >&2
@@ -150,7 +154,7 @@ foo = bar
 EOF
 
 test_expect_success 'unset with cont. lines' '
-	git config --unset beta.baz
+	git config ${mode_unset} beta.baz
 '
 
 cat > expect <<\EOF
@@ -177,7 +181,7 @@ EOF
 cp .git/config .git/config2
 
 test_expect_success 'multiple unset' '
-	git config --unset-all beta.haha
+	git config ${mode_unset_all} beta.haha
 '
 
 cat > expect << EOF
@@ -263,7 +267,7 @@ noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'unset' '
-	git config --unset beta.haha &&
+	git config ${mode_unset} beta.haha &&
 	test_cmp expect .git/config
 '
 
@@ -319,11 +323,11 @@ test_expect_success 'multivar replace' '
 '
 
 test_expect_success 'ambiguous unset' '
-	test_must_fail git config --unset nextsection.nonewline
+	test_must_fail git config ${mode_unset} nextsection.nonewline
 '
 
 test_expect_success 'invalid unset' '
-	test_must_fail git config --unset somesection.nonewline
+	test_must_fail git config ${mode_unset} somesection.nonewline
 '
 
 cat > expect << EOF
@@ -337,7 +341,12 @@ noIndent= sillyValue ; 'nother silly comment
 EOF
 
 test_expect_success 'multivar unset' '
-	git config --unset nextsection.nonewline "wow3$" &&
+	case "$mode" in
+	legacy)
+		git config --unset nextsection.nonewline "wow3$";;
+	subcommands)
+		git config unset --value="wow3$" nextsection.nonewline;;
+	esac &&
 	test_cmp expect .git/config
 '
 
@@ -1888,7 +1897,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	# please be careful when you update the above variable
 	EOF
 
-	git config --unset section.key &&
+	git config ${mode_unset} section.key &&
 	test_cmp expect .git/config &&
 
 	cat >.git/config <<-\EOF &&
@@ -1901,7 +1910,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[next-section]
 	EOF
 
-	git config --unset section.key &&
+	git config ${mode_unset} section.key &&
 	test_cmp expect .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1911,7 +1920,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[two]
 	key = true
 	EOF
-	git config --unset two.key &&
+	git config ${mode_unset} two.key &&
 	! grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1921,7 +1930,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[one]
 	key = true
 	EOF
-	git config --unset-all one.key &&
+	git config ${mode_unset_all} one.key &&
 	test_line_count = 0 .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1931,7 +1940,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[two]
 	Qkey = true
 	EOF
-	git config --unset two.key &&
+	git config ${mode_unset} two.key &&
 	grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1943,7 +1952,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[TWO "subsection"]
 	[one]
 	EOF
-	git config --unset two.subsection.key &&
+	git config ${mode_unset} two.subsection.key &&
 	test "not [two subsection]" = "$(git config ${mode_get} one.key)" &&
 	test_line_count = 3 .git/config
 '
@@ -1955,7 +1964,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' '
 	key = value2
 	EOF
 
-	git config --unset-all section.key &&
+	git config ${mode_unset_all} section.key &&
 	test_line_count = 0 .git/config
 '
 
@@ -2479,8 +2488,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
-	test_must_fail git config --file=config --fixed-value --unset dev.null &&
-	test_must_fail git config --file=config --fixed-value --unset-all dev.null
+	test_must_fail git config ${mode_unset} --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_unset_all} --file=config --fixed-value dev.null
 '
 
 test_expect_success '--fixed-value uses exact string matching' '
@@ -2510,6 +2519,11 @@ test_expect_success '--fixed-value uses exact string matching' '
 	git config --file=config --fixed-value --unset fixed.test "$META" &&
 	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
+	cp initial config &&
+	test_must_fail git config unset --file=config --value="$META" fixed.test &&
+	git config unset --file=config --fixed-value --value="$META" fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
+
 	cp initial config &&
 	test_must_fail git config --file=config --unset-all fixed.test "$META" &&
 	git config --file=config --fixed-value --unset-all fixed.test "$META" &&
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 10/13] builtin/config: introduce "rename-section" subcommand
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                     ` (8 preceding siblings ...)
  2024-03-11 23:21   ` [PATCH v2 09/13] builtin/config: introduce "unset" subcommand Patrick Steinhardt
@ 2024-03-11 23:21   ` Patrick Steinhardt
  2024-03-11 23:21   ` [PATCH v2 11/13] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
                     ` (2 subsequent siblings)
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:21 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 8500 bytes --]

Introduce a new "rename-section" subcommand to git-config(1). Please
refer to preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 11 +++++++----
 builtin/config.c             | 32 ++++++++++++++++++++++++++++++++
 t/t1300-config.sh            | 22 +++++++++++-----------
 3 files changed, 50 insertions(+), 15 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 42e659ae63..ebde360c1e 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -13,7 +13,7 @@ SYNOPSIS
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
-'git config' [<file-option>] --rename-section <old-name> <new-name>
+'git config rename-section' [<file-option>] <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
@@ -92,6 +92,9 @@ unset::
 	multi-valued config options, whereas `--value` will unset all config
 	options whose values match the given pattern.
 
+rename-section::
+	Rename the given section to a new name.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -180,9 +183,6 @@ See also <<FILES>>.
 --remove-section::
 	Remove the given section from the configuration file.
 
---rename-section::
-	Rename the given section to a new name.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -317,6 +317,9 @@ recommended to migrate to the new syntax.
 --unset-all <name> [<value-pattern>]::
 	Replaced by `git config unset [--value=<pattern>] --all <name>`.
 
+--rename-section <old-name> <new-name>::
+	Replaced by `git config rename-section <old-name> <new-name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index e62a09061f..09e8f47b8e 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -20,6 +20,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	NULL
 };
 
@@ -43,6 +44,11 @@ static const char *const builtin_config_unset_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_rename_section_usage[] = {
+	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -940,11 +946,37 @@ static int cmd_config_unset(int argc, const char **argv, const char *prefix)
 		return git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);
 }
 
+static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_rename_section_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 2, 2);
+
+	handle_config_location(prefix);
+
+	ret = git_config_rename_section_in_file(given_config_source.file,
+						argv[0], argv[1]);
+	if (ret < 0)
+		return ret;
+	else if (!ret)
+		die(_("no such section: %s"), argv[0]);
+
+	return 0;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
+	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 06d34b1d6a..0020296df6 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -590,7 +590,7 @@ weird
 EOF
 
 test_expect_success 'rename section' '
-	git config --rename-section branch.eins branch.zwei
+	git config ${mode_prefix}rename-section branch.eins branch.zwei
 '
 
 cat > expect << EOF
@@ -609,7 +609,7 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'rename non-existing section' '
-	test_must_fail git config --rename-section \
+	test_must_fail git config ${mode_prefix}rename-section \
 		branch."world domination" branch.drei
 '
 
@@ -618,7 +618,7 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'rename another section' '
-	git config --rename-section branch."1 234 blabl/a" branch.drei
+	git config ${mode_prefix}rename-section branch."1 234 blabl/a" branch.drei
 '
 
 cat > expect << EOF
@@ -641,7 +641,7 @@ cat >> .git/config << EOF
 EOF
 
 test_expect_success 'rename a section with a var on the same line' '
-	git config --rename-section branch.vier branch.zwei
+	git config ${mode_prefix}rename-section branch.vier branch.zwei
 '
 
 cat > expect << EOF
@@ -662,11 +662,11 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'renaming empty section name is rejected' '
-	test_must_fail git config --rename-section branch.zwei ""
+	test_must_fail git config ${mode_prefix}rename-section branch.zwei ""
 '
 
 test_expect_success 'renaming to bogus section is rejected' '
-	test_must_fail git config --rename-section branch.zwei "bogus name"
+	test_must_fail git config ${mode_prefix}rename-section branch.zwei "bogus name"
 '
 
 test_expect_success 'renaming a section with a long line' '
@@ -675,7 +675,7 @@ test_expect_success 'renaming a section with a long line' '
 		printf "  c = d %1024s [a] e = f\\n" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	git config -f y --rename-section a xyz &&
+	git config ${mode_prefix}rename-section -f y a xyz &&
 	test_must_fail git config -f y b.e
 '
 
@@ -685,7 +685,7 @@ test_expect_success 'renaming an embedded section with a long line' '
 		printf "  c = d %1024s [a] [foo] e = f\\n" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	git config -f y --rename-section a xyz &&
+	git config ${mode_prefix}rename-section -f y a xyz &&
 	test_must_fail git config -f y foo.e
 '
 
@@ -695,7 +695,7 @@ test_expect_success 'renaming a section with an overly-long line' '
 		printf "  c = d %525000s e" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	test_must_fail git config -f y --rename-section a xyz 2>err &&
+	test_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&
 	grep "refusing to work with overly long line in .y. on line 2" err
 '
 
@@ -1987,7 +1987,7 @@ test_expect_success POSIXPERM,PERL 'preserves existing permissions' '
 	git config imap.pass Hunter2 &&
 	perl -e \
 	  "die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600" &&
-	git config --rename-section imap pop &&
+	git config ${mode_prefix}rename-section imap pop &&
 	perl -e \
 	  "die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600"
 '
@@ -2476,7 +2476,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --add dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --rename-section dev null &&
+	test_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&
 	test_must_fail git config --file=config --fixed-value --remove-section dev &&
 	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 11/13] builtin/config: introduce "remove-section" subcommand
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                     ` (9 preceding siblings ...)
  2024-03-11 23:21   ` [PATCH v2 10/13] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
@ 2024-03-11 23:21   ` Patrick Steinhardt
  2024-03-11 23:21   ` [PATCH v2 12/13] builtin/config: introduce "edit" subcommand Patrick Steinhardt
  2024-03-11 23:21   ` [PATCH v2 13/13] builtin/config: display subcommand help Patrick Steinhardt
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:21 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 5327 bytes --]

Introduce a new "remove-section" subcommand to git-config(1). Please
refer to preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 11 +++++++----
 builtin/config.c             | 32 ++++++++++++++++++++++++++++++++
 t/t1300-config.sh            |  4 ++--
 3 files changed, 41 insertions(+), 6 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index ebde360c1e..615ed44350 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -14,7 +14,7 @@ SYNOPSIS
 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config rename-section' [<file-option>] <old-name> <new-name>
-'git config' [<file-option>] --remove-section <name>
+'git config remove-section' [<file-option>] <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
 
@@ -95,6 +95,9 @@ unset::
 rename-section::
 	Rename the given section to a new name.
 
+remove-section::
+	Remove the given section from the configuration file.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -180,9 +183,6 @@ See also <<FILES>>.
 	section in linkgit:gitrevisions[7] for a more complete list of
 	ways to spell blob names.
 
---remove-section::
-	Remove the given section from the configuration file.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -320,6 +320,9 @@ recommended to migrate to the new syntax.
 --rename-section <old-name> <new-name>::
 	Replaced by `git config rename-section <old-name> <new-name>`.
 
+--remove-section <name>::
+	Replaced by `git config remove-section <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index 09e8f47b8e..d3aa5bc298 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -21,6 +21,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
+	N_("git config remove-section [<file-option>] <name>"),
 	NULL
 };
 
@@ -49,6 +50,11 @@ static const char *const builtin_config_rename_section_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_remove_section_usage[] = {
+	N_("git config remove-section [<file-option>] <name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -971,12 +977,38 @@ static int cmd_config_rename_section(int argc, const char **argv, const char *pr
 	return 0;
 }
 
+static int cmd_config_remove_section(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_remove_section_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 1, 1);
+
+	handle_config_location(prefix);
+
+	ret = git_config_rename_section_in_file(given_config_source.file,
+						argv[0], NULL);
+	if (ret < 0)
+		return ret;
+	else if (!ret)
+		die(_("no such section: %s"), argv[0]);
+
+	return 0;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
+	OPT_SUBCOMMAND("remove-section", &subcommand, cmd_config_remove_section),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 0020296df6..eafecf84a4 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -704,7 +704,7 @@ cat >> .git/config << EOF
 EOF
 
 test_expect_success 'remove section' '
-	git config --remove-section branch.zwei
+	git config ${mode_prefix}remove-section branch.zwei
 '
 
 cat > expect << EOF
@@ -2477,7 +2477,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&
-	test_must_fail git config --file=config --fixed-value --remove-section dev &&
+	test_must_fail git config ${mode_prefix}remove-section --file=config --fixed-value dev &&
 	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 12/13] builtin/config: introduce "edit" subcommand
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                     ` (10 preceding siblings ...)
  2024-03-11 23:21   ` [PATCH v2 11/13] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
@ 2024-03-11 23:21   ` Patrick Steinhardt
  2024-03-11 23:21   ` [PATCH v2 13/13] builtin/config: display subcommand help Patrick Steinhardt
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:21 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 7164 bytes --]

Introduce a new "edit" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 15 ++++---
 builtin/config.c             | 81 ++++++++++++++++++++++++------------
 t/t1300-config.sh            |  6 +--
 3 files changed, 67 insertions(+), 35 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 615ed44350..9fb94e2fad 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -15,8 +15,8 @@ SYNOPSIS
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config rename-section' [<file-option>] <old-name> <new-name>
 'git config remove-section' [<file-option>] <name>
+'git config edit' [<file-option>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
-'git config' [<file-option>] -e | --edit
 
 DESCRIPTION
 -----------
@@ -98,6 +98,10 @@ rename-section::
 remove-section::
 	Remove the given section from the configuration file.
 
+edit::
+	Opens an editor to modify the specified config file; either
+	`--system`, `--global`, or repository (default).
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -262,11 +266,6 @@ Valid `<type>`'s include:
 	When the color setting for `name` is undefined, the command uses
 	`color.ui` as fallback.
 
--e::
---edit::
-	Opens an editor to modify the specified config file; either
-	`--system`, `--global`, or repository (default).
-
 --[no-]includes::
 	Respect `include.*` directives in config files when looking up
 	values. Defaults to `off` when a specific file is given (e.g.,
@@ -323,6 +322,10 @@ recommended to migrate to the new syntax.
 --remove-section <name>::
 	Replaced by `git config remove-section <name>`.
 
+-e::
+--edit::
+	Replaced by `git config edit`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index d3aa5bc298..b1008c487d 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -22,6 +22,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	N_("git config remove-section [<file-option>] <name>"),
+	N_("git config edit [<file-option>]"),
 	NULL
 };
 
@@ -55,6 +56,11 @@ static const char *const builtin_config_remove_section_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_edit_usage[] = {
+	N_("git config edit [<file-option>]"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -1002,6 +1008,53 @@ static int cmd_config_remove_section(int argc, const char **argv, const char *pr
 	return 0;
 }
 
+static int show_editor(void)
+{
+	char *config_file;
+
+	if (!given_config_source.file && !startup_info->have_repository)
+		die(_("not in a git directory"));
+	if (given_config_source.use_stdin)
+		die(_("editing stdin is not supported"));
+	if (given_config_source.blob)
+		die(_("editing blobs is not supported"));
+	git_config(git_default_config, NULL);
+	config_file = given_config_source.file ?
+			xstrdup(given_config_source.file) :
+			git_pathdup("config");
+	if (use_global_config) {
+		int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);
+		if (fd >= 0) {
+			char *content = default_user_config();
+			write_str_in_full(fd, content);
+			free(content);
+			close(fd);
+		}
+		else if (errno != EEXIST)
+			die_errno(_("cannot create configuration file %s"), config_file);
+	}
+	launch_editor(config_file, NULL, NULL);
+	free(config_file);
+
+	return 0;
+}
+
+static int cmd_config_edit(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_edit_usage, 0);
+	check_write();
+	check_argc(argc, 0, 0);
+
+	handle_config_location(prefix);
+
+	return show_editor();
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
@@ -1009,6 +1062,7 @@ static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
 	OPT_SUBCOMMAND("remove-section", &subcommand, cmd_config_remove_section),
+	OPT_SUBCOMMAND("edit", &subcommand, cmd_config_edit),
 	OPT_END(),
 };
 
@@ -1127,32 +1181,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		}
 	}
 	else if (actions == ACTION_EDIT) {
-		char *config_file;
-
-		check_argc(argc, 0, 0);
-		if (!given_config_source.file && !startup_info->have_repository)
-			die(_("not in a git directory"));
-		if (given_config_source.use_stdin)
-			die(_("editing stdin is not supported"));
-		if (given_config_source.blob)
-			die(_("editing blobs is not supported"));
-		git_config(git_default_config, NULL);
-		config_file = given_config_source.file ?
-				xstrdup(given_config_source.file) :
-				git_pathdup("config");
-		if (use_global_config) {
-			int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);
-			if (fd >= 0) {
-				char *content = default_user_config();
-				write_str_in_full(fd, content);
-				free(content);
-				close(fd);
-			}
-			else if (errno != EEXIST)
-				die_errno(_("cannot create configuration file %s"), config_file);
-		}
-		launch_editor(config_file, NULL, NULL);
-		free(config_file);
+		ret = show_editor();
 	}
 	else if (actions == ACTION_SET) {
 		check_write();
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index eafecf84a4..f820f87619 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -557,7 +557,7 @@ test_expect_success 'setting a value in stdin is an error' '
 '
 
 test_expect_success 'editing stdin is an error' '
-	test_must_fail git config --file - --edit
+	test_must_fail git config ${mode_prefix}edit --file -
 '
 
 test_expect_success 'refer config from subdirectory' '
@@ -1643,7 +1643,7 @@ test_expect_success 'command line overrides environment config' '
 test_expect_success 'git config --edit works' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
-	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
+	GIT_EDITOR="echo [test]value=yes >" git config ${mode_prefix}edit -f tmp &&
 	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
@@ -1652,7 +1652,7 @@ test_expect_success 'git config --edit respects core.editor' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
 	test_config core.editor "echo [test]value=yes >" &&
-	git config -f tmp --edit &&
+	git config ${mode_prefix}edit -f tmp &&
 	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v2 13/13] builtin/config: display subcommand help
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
                     ` (11 preceding siblings ...)
  2024-03-11 23:21   ` [PATCH v2 12/13] builtin/config: introduce "edit" subcommand Patrick Steinhardt
@ 2024-03-11 23:21   ` Patrick Steinhardt
  12 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-11 23:21 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 2139 bytes --]

Until now, `git config -h` would have printed help for the old-style
syntax. Now that all modes have proper subcommands though it is
preferable to instead display the subcommand help.

Drop the `NO_INTERNAL_HELP` flag to do so. While at it, drop the help
mismatch in t0450 and add the `--get-colorbool` option to the usage such
that git-config(1)'s synopsis and `git config -h` match.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c            | 5 +++--
 t/t0450/txt-help-mismatches | 1 -
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index b1008c487d..8ba8dd77bc 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -23,6 +23,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	N_("git config remove-section [<file-option>] <name>"),
 	N_("git config edit [<file-option>]"),
+	N_("git config [<file-option>] --get-colorbool <name> [<stdout-is-tty>]"),
 	NULL
 };
 
@@ -1084,10 +1085,10 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 	 * us when parsing the implicit modes.
 	 */
 	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
-			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
 	if (subcommand) {
 		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
-		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);
+		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);
 		return subcommand(argc, argv, prefix);
 	}
 
diff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches
index a0777acd66..28003f18c9 100644
--- a/t/t0450/txt-help-mismatches
+++ b/t/t0450/txt-help-mismatches
@@ -10,7 +10,6 @@ checkout
 checkout-index
 clone
 column
-config
 credential
 credential-cache
 credential-store
-- 
2.44.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v2 06/13] builtin/config: introduce "list" subcommand
  2024-03-11 23:20   ` [PATCH v2 06/13] builtin/config: introduce "list" subcommand Patrick Steinhardt
@ 2024-03-13  2:45     ` Eric Sunshine
  2024-03-27  8:42       ` Patrick Steinhardt
  0 siblings, 1 reply; 113+ messages in thread
From: Eric Sunshine @ 2024-03-13  2:45 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Junio C Hamano

On Mon, Mar 11, 2024 at 7:20 PM Patrick Steinhardt <ps@pks.im> wrote:
> [...]
> Introduce the first such new subcommand, which is "git config list". To
> retain backwards compatibility we only conditionally use subcommands and
> will fall back to the old syntax in case no subcommand was detected.
> This should help to transition to the new-style syntax until we
> eventually deprecate and remove the old-style syntax.
> [...]
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> diff --git a/builtin/config.c b/builtin/config.c
> @@ -761,6 +811,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)
> +       /*
> +        * This is somewhat hacky: we first parse the command line while
> +        * keeping all args intact in order to determine whether a subcommand
> +        * has been specified. If so, we re-parse it a second time, but this
> +        * time we drop KEEP_ARGV0. This is so that we don't munge the command
> +        * line in case no subcommand was given, which would otherwise confuse
> +        * us when parsing the implicit modes.
> +        */
> +       argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,

Upon reading this, I wasn't quite sure what "when parsing the implicit
modes" meant. I suppose it is referring to the legacy style command
invocation?

> diff --git a/t/t1300-config.sh b/t/t1300-config.sh
> @@ -11,6 +11,21 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
> +for mode in legacy subcommands
> +do
> +
> +case "$mode" in
> +legacy)
> +       mode_prefix="--"
> +       ;;
> +subcommands)
> +       mode_prefix=""
> +       ;;
> +*)
> +       echo "unknown mode $mode" >&2
> +       exit 1;;
> +esac

t/test-lib.sh defines a BUG() function for signaling the sort of
programmer error handled by the "*" arm of this `case`.

An alternative simpler implementation, which wouldn't require any sort
of programmer-error fallback, would be:

    for mode_prefix in -- "" # legacy & subcommand modes
    do
        test_expect_success '...' '
            ...
        '
        ...
    done

> @@ -527,6 +542,7 @@ test_expect_success 'editing stdin is an error' '
>  test_expect_success 'refer config from subdirectory' '
> +       test_when_finished "rm -r x" &&
>         mkdir x &&
>         test_cmp_config -C x strasse --file=../other-config --get ein.bahn
>  '

Is this an unrelated cleanup?

> @@ -1072,6 +1088,7 @@ test_expect_success 'inner whitespace kept verbatim' '
>  test_expect_success SYMLINKS 'symlinked configuration' '
> +       test_when_finished "rm myconfig" &&
>         ln -s notyet myconfig &&
>         git config --file=myconfig test.frotz nitfol &&
>         test -h myconfig &&

Ditto.

Same question regarding similar changes to several later tests.

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

* Re: [PATCH v2 07/13] builtin/config: introduce "get" subcommand
  2024-03-11 23:20   ` [PATCH v2 07/13] builtin/config: introduce "get" subcommand Patrick Steinhardt
@ 2024-03-13  3:11     ` Eric Sunshine
  2024-03-27  8:42       ` Patrick Steinhardt
  0 siblings, 1 reply; 113+ messages in thread
From: Eric Sunshine @ 2024-03-13  3:11 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Junio C Hamano

On Mon, Mar 11, 2024 at 7:20 PM Patrick Steinhardt <ps@pks.im> wrote:
> Introduce a new "get" subcommand to git-config(1). Please refer to
> preceding commits regarding the motivation behind this change.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
> diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
> @@ -80,6 +76,12 @@ COMMANDS
> +get::
> +       Get value for one or more config options. Values can be filtered by
> +       regexes and URLs.Returns error code 1 if the key was not found and the
> +       last value if multiple key values were found. If `--all` is set, then
> +       all values will be shown.

s/URLs.Returns/URLs. Returns/

It's not a new problem with this description (since you're mostly just
relocating existing text), but I find the discussion of what is
returned quite confusing and difficult to interpret. Breaking it down
into simpler sentences might help:

    Emits the value of the specified key. If key is present
    multiple times in the configuration, emits the last
    value. If `--all` is specified, emits all values
    associated with key. Returns error code 1 if key
    is not present.

But, doing so may be outside the scope of this patch series and can be
tackled at a later date (or not at all).

> @@ -93,22 +95,16 @@ OPTIONS
> +--all::
> +       With "get", Return all values for a multi-valued key.

s/Return/return/
s/"get"/`get`/

> +---regexp::
> +       With "get", interpret the name as a regular expression. Regular
> +       expression matching is currently case-sensitive and done against a
> +       canonicalized version of the key in which section and variable names
> +       are lowercased, but subsection names are not.

s/"get"/`get`/

> @@ -286,7 +271,7 @@ Valid `<type>`'s include:
>  --default <value>::
> -  When using `--get`, and the requested variable is not found, behave as if
> +  When using `get`, and the requested variable is not found, behave as if
>    <value> were the value assigned to the that variable.

Not a fault of this patch (and need not be fixed by this series): "to
the that" should be either "to the" or "to that".

> @@ -506,25 +509,25 @@ you have to provide a regex matching the value of exactly one line.
>  To query the value for a given key, do
>
>  ------------
> -% git config --get core.filemode
> +% git config get core.filemode
>  ------------
>
>  or
>
>  ------------
> -% git config core.filemode
> +% git config get core.filemode
>  ------------

Meh. We only need to retain one of these examples now, not both, right?

> diff --git a/t/t1300-config.sh b/t/t1300-config.sh
> @@ -17,9 +17,15 @@ do
>  case "$mode" in
>  legacy)
>         mode_prefix="--"
> +       mode_get=""
> +       mode_get_all="--get-all"
> +       mode_get_regexp="--get-regexp"
>         ;;
>  subcommands)
>         mode_prefix=""
> +       mode_get="get"
> +       mode_get_all="get --all"
> +       mode_get_regexp="get --regexp --all --show-names"
>         ;;
>  *)
>         echo "unknown mode $mode" >&2

The variables added by this patch to the `case` arms invalidate the
suggested simplification in my review of [6/13].

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

* Re: [PATCH v2 06/13] builtin/config: introduce "list" subcommand
  2024-03-13  2:45     ` Eric Sunshine
@ 2024-03-27  8:42       ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:42 UTC (permalink / raw)
  To: Eric Sunshine
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 3691 bytes --]

On Tue, Mar 12, 2024 at 10:45:56PM -0400, Eric Sunshine wrote:
> On Mon, Mar 11, 2024 at 7:20 PM Patrick Steinhardt <ps@pks.im> wrote:
> > [...]
> > Introduce the first such new subcommand, which is "git config list". To
> > retain backwards compatibility we only conditionally use subcommands and
> > will fall back to the old syntax in case no subcommand was detected.
> > This should help to transition to the new-style syntax until we
> > eventually deprecate and remove the old-style syntax.
> > [...]
> > Signed-off-by: Patrick Steinhardt <ps@pks.im>
> > ---
> > diff --git a/builtin/config.c b/builtin/config.c
> > @@ -761,6 +811,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)
> > +       /*
> > +        * This is somewhat hacky: we first parse the command line while
> > +        * keeping all args intact in order to determine whether a subcommand
> > +        * has been specified. If so, we re-parse it a second time, but this
> > +        * time we drop KEEP_ARGV0. This is so that we don't munge the command
> > +        * line in case no subcommand was given, which would otherwise confuse
> > +        * us when parsing the implicit modes.
> > +        */
> > +       argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
> 
> Upon reading this, I wasn't quite sure what "when parsing the implicit
> modes" meant. I suppose it is referring to the legacy style command
> invocation?

Yeah, indeed. Let me rephrase it as "when parsing the legacy-style
command modes", which is hopefully a bit clearer.

> > diff --git a/t/t1300-config.sh b/t/t1300-config.sh
> > @@ -11,6 +11,21 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
> > +for mode in legacy subcommands
> > +do
> > +
> > +case "$mode" in
> > +legacy)
> > +       mode_prefix="--"
> > +       ;;
> > +subcommands)
> > +       mode_prefix=""
> > +       ;;
> > +*)
> > +       echo "unknown mode $mode" >&2
> > +       exit 1;;
> > +esac
> 
> t/test-lib.sh defines a BUG() function for signaling the sort of
> programmer error handled by the "*" arm of this `case`.

Ah, right, I'll use that.

> An alternative simpler implementation, which wouldn't require any sort
> of programmer-error fallback, would be:
> 
>     for mode_prefix in -- "" # legacy & subcommand modes
>     do
>         test_expect_success '...' '
>             ...
>         '
>         ...
>     done

It works now, but will stop working in subsequent patches. Not all modes
can be translated this easily. E.g. "git config config.key value" needs
to be translated to "git config set config.key value".

> > @@ -527,6 +542,7 @@ test_expect_success 'editing stdin is an error' '
> >  test_expect_success 'refer config from subdirectory' '
> > +       test_when_finished "rm -r x" &&
> >         mkdir x &&
> >         test_cmp_config -C x strasse --file=../other-config --get ein.bahn
> >  '
> 
> Is this an unrelated cleanup?
> 
> > @@ -1072,6 +1088,7 @@ test_expect_success 'inner whitespace kept verbatim' '
> >  test_expect_success SYMLINKS 'symlinked configuration' '
> > +       test_when_finished "rm myconfig" &&
> >         ln -s notyet myconfig &&
> >         git config --file=myconfig test.frotz nitfol &&
> >         test -h myconfig &&
> 
> Ditto.
> 
> Same question regarding similar changes to several later tests.

These are required because we now re-run tests twice. Consequently,
state from the first run may still be around in the second run. The
tests here broke because of that, which I've fixed by cleaning up the
state. I'll clarify this in the commit messages.

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v2 07/13] builtin/config: introduce "get" subcommand
  2024-03-13  3:11     ` Eric Sunshine
@ 2024-03-27  8:42       ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:42 UTC (permalink / raw)
  To: Eric Sunshine
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 3650 bytes --]

On Tue, Mar 12, 2024 at 11:11:07PM -0400, Eric Sunshine wrote:
> On Mon, Mar 11, 2024 at 7:20 PM Patrick Steinhardt <ps@pks.im> wrote:
> > Introduce a new "get" subcommand to git-config(1). Please refer to
> > preceding commits regarding the motivation behind this change.
> >
> > Signed-off-by: Patrick Steinhardt <ps@pks.im>
> > ---
> > diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
> > @@ -80,6 +76,12 @@ COMMANDS
> > +get::
> > +       Get value for one or more config options. Values can be filtered by
> > +       regexes and URLs.Returns error code 1 if the key was not found and the
> > +       last value if multiple key values were found. If `--all` is set, then
> > +       all values will be shown.
> 
> s/URLs.Returns/URLs. Returns/
> 
> It's not a new problem with this description (since you're mostly just
> relocating existing text), but I find the discussion of what is
> returned quite confusing and difficult to interpret. Breaking it down
> into simpler sentences might help:
> 
>     Emits the value of the specified key. If key is present
>     multiple times in the configuration, emits the last
>     value. If `--all` is specified, emits all values
>     associated with key. Returns error code 1 if key
>     is not present.
> 
> But, doing so may be outside the scope of this patch series and can be
> tackled at a later date (or not at all).

I like this description, so let's just go with it.

> > @@ -93,22 +95,16 @@ OPTIONS
> > +--all::
> > +       With "get", Return all values for a multi-valued key.
> 
> s/Return/return/
> s/"get"/`get`/
> 
> > +---regexp::
> > +       With "get", interpret the name as a regular expression. Regular
> > +       expression matching is currently case-sensitive and done against a
> > +       canonicalized version of the key in which section and variable names
> > +       are lowercased, but subsection names are not.
> 
> s/"get"/`get`/
> 
> > @@ -286,7 +271,7 @@ Valid `<type>`'s include:
> >  --default <value>::
> > -  When using `--get`, and the requested variable is not found, behave as if
> > +  When using `get`, and the requested variable is not found, behave as if
> >    <value> were the value assigned to the that variable.
> 
> Not a fault of this patch (and need not be fixed by this series): "to
> the that" should be either "to the" or "to that".

This was fixed via 86f9ce7dd6 (docs: fix typo in git-config `--default`,
2024-03-16).

> > @@ -506,25 +509,25 @@ you have to provide a regex matching the value of exactly one line.
> >  To query the value for a given key, do
> >
> >  ------------
> > -% git config --get core.filemode
> > +% git config get core.filemode
> >  ------------
> >
> >  or
> >
> >  ------------
> > -% git config core.filemode
> > +% git config get core.filemode
> >  ------------
> 
> Meh. We only need to retain one of these examples now, not both, right?

Oh, of course.

Patrick

> > diff --git a/t/t1300-config.sh b/t/t1300-config.sh
> > @@ -17,9 +17,15 @@ do
> >  case "$mode" in
> >  legacy)
> >         mode_prefix="--"
> > +       mode_get=""
> > +       mode_get_all="--get-all"
> > +       mode_get_regexp="--get-regexp"
> >         ;;
> >  subcommands)
> >         mode_prefix=""
> > +       mode_get="get"
> > +       mode_get_all="get --all"
> > +       mode_get_regexp="get --regexp --all --show-names"
> >         ;;
> >  *)
> >         echo "unknown mode $mode" >&2
> 
> The variables added by this patch to the `case` arms invalidate the
> suggested simplification in my review of [6/13].

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 00/13] builtin/config: introduce subcommands
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (10 preceding siblings ...)
  2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
@ 2024-03-27  8:46 ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 01/13] builtin/config: move option array around Patrick Steinhardt
                     ` (13 more replies)
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
  13 siblings, 14 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 10430 bytes --]

Hi,

this is the third version of my patch series that aims to add
subcommands to git-config(1) in a backwards-compatible manner.

There's only been a few changes compared to v2 based on Eric's comments:

    - Touched up a commit message to explain why I had to add cleanups
      to tests.

    - Reformulate "implicit modes" in a comment to clarify that this
      refers to the legacy-style modes.

    - Reformulated documentation of the "get" subcommand.

    - Use `BUG` instead of `echo >&2 && exit 1`.

    - Removed duplicate example in the documentation.

    - Fixed conflicts with 86f9ce7dd6 (docs: fix typo in git-config
      `--default`, 2024-03-16) and 7823a51203 (docs: clarify file
      options in git-config `--edit`, 2024-03-16) by rebasing onto
      latest `master`.

Thanks!

Patrick

Patrick Steinhardt (13):
  builtin/config: move option array around
  builtin/config: move "fixed-value" option to correct group
  builtin/config: use `OPT_CMDMODE()` to specify modes
  builtin/config: pull out function to handle config location
  builtin/config: pull out function to handle `--null`
  builtin/config: introduce "list" subcommand
  builtin/config: introduce "get" subcommand
  builtin/config: introduce "set" subcommand
  builtin/config: introduce "unset" subcommand
  builtin/config: introduce "rename-section" subcommand
  builtin/config: introduce "remove-section" subcommand
  builtin/config: introduce "edit" subcommand
  builtin/config: display subcommand help

 Documentation/git-config.txt | 219 +++++++++-------
 builtin/config.c             | 496 ++++++++++++++++++++++++++++-------
 t/t0450/txt-help-mismatches  |   1 -
 t/t1300-config.sh            | 416 ++++++++++++++++++-----------
 4 files changed, 785 insertions(+), 347 deletions(-)

Range-diff against v2:
 1:  1f21e7fa0d =  1:  bfcb50e393 builtin/config: move option array around
 2:  54e536394a =  2:  ff428d8a22 builtin/config: move "fixed-value" option to correct group
 3:  49d190ac24 =  3:  e049c05713 builtin/config: use `OPT_CMDMODE()` to specify modes
 4:  80093f3908 =  4:  41585803bf builtin/config: pull out function to handle config location
 5:  283a199578 =  5:  95f661f267 builtin/config: pull out function to handle `--null`
 6:  53401299fa !  6:  b50f32d074 builtin/config: introduce "list" subcommand
    @@ Commit message
         ensure that there really is no change in behaviour for the old syntax.
     
         Amend tests such that we run them both with old and new style syntax.
    +    As tests are now run twice, state from the first run may be still be
    +    around in the second run and thus cause tests to fail. Add cleanup logic
    +    as required to fix such tests.
     
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     
    @@ Documentation/git-config.txt: Valid `<type>`'s include:
      --show-origin::
     @@ Documentation/git-config.txt: Valid `<type>`'s include:
        When using `--get`, and the requested variable is not found, behave as if
    -   <value> were the value assigned to the that variable.
    +   <value> were the value assigned to that variable.
      
     +DEPRECATED MODES
     +----------------
    @@ builtin/config.c: int cmd_config(int argc, const char **argv, const char *prefix
     +	 * has been specified. If so, we re-parse it a second time, but this
     +	 * time we drop KEEP_ARGV0. This is so that we don't munge the command
     +	 * line in case no subcommand was given, which would otherwise confuse
    -+	 * us when parsing the implicit modes.
    ++	 * us when parsing the legacy-style modes that don't use subcommands.
     +	 */
     +	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
     +			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
    @@ t/t1300-config.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
     +	mode_prefix=""
     +	;;
     +*)
    -+	echo "unknown mode $mode" >&2
    -+	exit 1;;
    ++	BUG "unknown mode $mode";;
     +esac
     +
      test_expect_success 'clear default config' '
 7:  8ceced0fc5 !  7:  eee1fae50c builtin/config: introduce "get" subcommand
    @@ Documentation/git-config.txt: COMMANDS
      	List all variables set in config file, along with their values.
      
     +get::
    -+	Get value for one or more config options. Values can be filtered by
    -+	regexes and URLs.Returns error code 1 if the key was not found and the
    -+	last value if multiple key values were found. If `--all` is set, then
    -+	all values will be shown.
    ++	Emits the value of the specified key. If key is present multiple times
    ++	in the configuration, emits the last value. If `--all` is specified,
    ++	emits all values associated with key. Returns error code 1 if key is
    ++	not present.
     +
      [[OPTIONS]]
      OPTIONS
    @@ Documentation/git-config.txt: OPTIONS
     ---get-all::
     -	Like get, but returns all values for a multi-valued key.
     +--all::
    -+	With "get", Return all values for a multi-valued key.
    ++	With `get`, Return all values for a multi-valued key.
      
     ---get-regexp::
     -	Like --get-all, but interprets the name as a regular expression and
    @@ Documentation/git-config.txt: OPTIONS
     -	in which section and variable names are lowercased, but subsection
     -	names are not.
     +---regexp::
    -+	With "get", interpret the name as a regular expression. Regular
    ++	With `get`, interpret the name as a regular expression. Regular
     +	expression matching is currently case-sensitive and done against a
     +	canonicalized version of the key in which section and variable names
     +	are lowercased, but subsection names are not.
    @@ Documentation/git-config.txt: Valid `<type>`'s include:
      --default <value>::
     -  When using `--get`, and the requested variable is not found, behave as if
     +  When using `get`, and the requested variable is not found, behave as if
    -   <value> were the value assigned to the that variable.
    +   <value> were the value assigned to that variable.
      
      DEPRECATED MODES
     @@ Documentation/git-config.txt: DEPRECATED MODES
    @@ Documentation/git-config.txt: you have to provide a regex matching the value of
      
      ------------
     -% git config --get core.filemode
    -+% git config get core.filemode
    - ------------
    - 
    - or
    - 
    - ------------
    +-------------
    +-
    +-or
    +-
    +-------------
     -% git config core.filemode
     +% git config get core.filemode
      ------------
    @@ t/t1300-config.sh: do
     +	mode_get_regexp="get --regexp --all --show-names"
      	;;
      *)
    - 	echo "unknown mode $mode" >&2
    + 	BUG "unknown mode $mode";;
     @@ t/t1300-config.sh: test_expect_success 'multi-valued get-all returns all' '
      	wow
      	wow2 for me
 8:  aa5c9743ad !  8:  e2815affab builtin/config: introduce "set" subcommand
    @@ Documentation/git-config.txt: You can query/set/replace/unset options with this
      lines, a `value-pattern` (which is an extended regular expression,
      unless the `--fixed-value` option is given) needs to be given.  Only the
     @@ Documentation/git-config.txt: get::
    - 	last value if multiple key values were found. If `--all` is set, then
    - 	all values will be shown.
    + 	emits all values associated with key. Returns error code 1 if key is
    + 	not present.
      
     +set::
     +	Set value for one or more config options. By default, this command
    @@ Documentation/git-config.txt: OPTIONS
     +	values. This is the same as providing '--value=^$' in `set`.
      
      --all::
    - 	With "get", Return all values for a multi-valued key.
    + 	With `get`, Return all values for a multi-valued key.
     @@ Documentation/git-config.txt: recommended to migrate to the new syntax.
      'git config <name>'::
      	Replaced by `git config get <name>`.
    @@ t/t1300-config.sh: legacy)
     +	mode_replace_all="set --all"
      	;;
      *)
    - 	echo "unknown mode $mode" >&2
    + 	BUG "unknown mode $mode";;
     @@ t/t1300-config.sh: cat > expect << EOF
      	penguin = little blue
      EOF
 9:  c8a44b6189 !  9:  90f055ae1d builtin/config: introduce "unset" subcommand
    @@ t/t1300-config.sh: subcommands)
     +	mode_unset_all="unset --all"
      	;;
      *)
    - 	echo "unknown mode $mode" >&2
    + 	BUG "unknown mode $mode";;
     @@ t/t1300-config.sh: foo = bar
      EOF
      
10:  a3c66431b0 = 10:  3e360b1f47 builtin/config: introduce "rename-section" subcommand
11:  6314d51cec = 11:  d610b5fda1 builtin/config: introduce "remove-section" subcommand
12:  12011714e3 ! 12:  4a6512c88a builtin/config: introduce "edit" subcommand
    @@ Documentation/git-config.txt: rename-section::
      
     +edit::
     +	Opens an editor to modify the specified config file; either
    -+	`--system`, `--global`, or repository (default).
    ++	`--system`, `--global`, `--local` (default), `--worktree`, or
    ++	`--file <config-file>`.
     +
      [[OPTIONS]]
      OPTIONS
    @@ Documentation/git-config.txt: Valid `<type>`'s include:
     --e::
     ---edit::
     -	Opens an editor to modify the specified config file; either
    --	`--system`, `--global`, or repository (default).
    +-	`--system`, `--global`, `--local` (default), `--worktree`, or
    +-	`--file <config-file>`.
     -
      --[no-]includes::
      	Respect `include.*` directives in config files when looking up
13:  2046f6652a ! 13:  657d1355b5 builtin/config: display subcommand help
    @@ builtin/config.c: static const char *const builtin_config_usage[] = {
      };
      
     @@ builtin/config.c: int cmd_config(int argc, const char **argv, const char *prefix)
    - 	 * us when parsing the implicit modes.
    + 	 * us when parsing the legacy-style modes that don't use subcommands.
      	 */
      	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
     -			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);

base-commit: c75fd8d8150afdf836b63a8e0534d9b9e3e111ba
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 01/13] builtin/config: move option array around
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 02/13] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
                     ` (12 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 8068 bytes --]

Move around the option array. This will help us with a follow-up commit
that introduces subcommands to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 94 ++++++++++++++++++++++++------------------------
 1 file changed, 47 insertions(+), 47 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index b55bfae7d6..6eb6aff917 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -134,53 +134,6 @@ static int option_parse_type(const struct option *opt, const char *arg,
 	return 0;
 }
 
-static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
-	OPT_GROUP(N_("Action")),
-	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
-	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
-	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
-	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_END(),
-};
-
-static NORETURN void usage_builtin_config(void)
-{
-	usage_with_options(builtin_config_usage, builtin_config_options);
-}
-
 static void check_argc(int argc, int min, int max)
 {
 	if (argc >= min && argc <= max)
@@ -669,6 +622,53 @@ static char *default_user_config(void)
 	return strbuf_detach(&buf, NULL);
 }
 
+static struct option builtin_config_options[] = {
+	OPT_GROUP(N_("Config file location")),
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	OPT_GROUP(N_("Action")),
+	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
+	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
+	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
+	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
+	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
+	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
+	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
+	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_GROUP(N_("Type")),
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	OPT_GROUP(N_("Other")),
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
+	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_END(),
+};
+
+static NORETURN void usage_builtin_config(void)
+{
+	usage_with_options(builtin_config_usage, builtin_config_options);
+}
+
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	int nongit = !startup_info->have_repository;
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 02/13] builtin/config: move "fixed-value" option to correct group
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 01/13] builtin/config: move option array around Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 03/13] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
                     ` (11 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 1924 bytes --]

The `--fixed-value` option can be used to alter how the value-pattern
parameter is interpreted for the various submodes of git-config(1). But
while it is an option, it is currently listed as part of the submodes
group the command, which is wrong.

Move the option to the "Other" group, which hosts the various options
known to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/config.c b/builtin/config.c
index 6eb6aff917..fcd6190f12 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -642,7 +642,6 @@ static struct option builtin_config_options[] = {
 	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
 	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
 	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
 	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
 	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
 	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
@@ -661,6 +660,7 @@ static struct option builtin_config_options[] = {
 	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
 	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
 	OPT_END(),
 };
 
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 03/13] builtin/config: use `OPT_CMDMODE()` to specify modes
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 01/13] builtin/config: move option array around Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 02/13] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 04/13] builtin/config: pull out function to handle config location Patrick Steinhardt
                     ` (10 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 5527 bytes --]

The git-config(1) command has various different modes which are
accessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are
declared with `OPT_BIT()`, which causes two minor issues:

  - The respective modes also have a negated form `--no-get-urlmatch`,
    which is unintended.

  - We have to manually handle exclusiveness of the modes.

Switch these options to instead use `OPT_CMDMODE()`, which is made
exactly for this usecase. Remove the now-unneeded check that only a
single mode is given, which is now handled by the parse-options
interface.

While at it, format optional placeholders for arguments to conform to
our style guidelines by using `[<placeholder>]`.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c  | 32 ++++++++++++++------------------
 t/t1300-config.sh | 13 +++++++++++++
 2 files changed, 27 insertions(+), 18 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index fcd6190f12..a293161be4 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -631,20 +631,20 @@ static struct option builtin_config_options[] = {
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
 	OPT_GROUP(N_("Action")),
-	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
+	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
+	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
+	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
+	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
+	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
+	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
+	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
 	OPT_GROUP(N_("Type")),
 	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
 	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
@@ -767,10 +767,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (HAS_MULTI_BITS(actions)) {
-		error(_("only one action at a time"));
-		usage_builtin_config();
-	}
 	if (actions == 0)
 		switch (argc) {
 		case 1: actions = ACTION_GET; break;
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 31c3878687..2d1bc1e27e 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -2612,4 +2612,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such
 	grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
 '
 
+test_expect_success 'negated mode causes failure' '
+	test_must_fail git config --no-get 2>err &&
+	grep "unknown option \`no-get${SQ}" err
+'
+
+test_expect_success 'specifying multiple modes causes failure' '
+	cat >expect <<-EOF &&
+	error: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together
+	EOF
+	test_must_fail git config --get --get-all 2>err &&
+	test_cmp expect err
+'
+
 test_done
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 04/13] builtin/config: pull out function to handle config location
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (2 preceding siblings ...)
  2024-03-27  8:46   ` [PATCH v3 03/13] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 05/13] builtin/config: pull out function to handle `--null` Patrick Steinhardt
                     ` (9 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 10535 bytes --]

There's quite a bunch of options to git-config(1) that allow the user to
specify which config location to use when reading or writing config
options. The logic to handle this is thus by necessity also quite
involved.

Pull it out into a separate function so that we can reuse it in
subsequent commits which introduce proper subcommands.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 131 ++++++++++++++++++++++++-----------------------
 1 file changed, 67 insertions(+), 64 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index a293161be4..693df32526 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -622,67 +622,8 @@ static char *default_user_config(void)
 	return strbuf_detach(&buf, NULL);
 }
 
-static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
-	OPT_GROUP(N_("Action")),
-	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
-	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
-	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
-	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
-	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
-	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
-	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
-	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
-	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
-	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
-	OPT_END(),
-};
-
-static NORETURN void usage_builtin_config(void)
-{
-	usage_with_options(builtin_config_usage, builtin_config_options);
-}
-
-int cmd_config(int argc, const char **argv, const char *prefix)
+static void handle_config_location(const char *prefix)
 {
-	int nongit = !startup_info->have_repository;
-	char *value = NULL;
-	int flags = 0;
-	int ret = 0;
-	struct key_value_info default_kvi = KVI_INIT;
-
-	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
-
-	argc = parse_options(argc, argv, prefix, builtin_config_options,
-			     builtin_config_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
-
 	if (use_global_config + use_system_config + use_local_config +
 	    use_worktree_config +
 	    !!given_config_source.file + !!given_config_source.blob > 1) {
@@ -690,14 +631,13 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (nongit) {
+	if (!startup_info->have_repository) {
 		if (use_local_config)
 			die(_("--local can only be used inside a git repository"));
 		if (given_config_source.blob)
 			die(_("--blob can only be used inside a git repository"));
 		if (use_worktree_config)
 			die(_("--worktree can only be used inside a git repository"));
-
 	}
 
 	if (given_config_source.file &&
@@ -751,10 +691,73 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		config_options.respect_includes = !given_config_source.file;
 	else
 		config_options.respect_includes = respect_includes_opt;
-	if (!nongit) {
+	if (startup_info->have_repository) {
 		config_options.commondir = get_git_common_dir();
 		config_options.git_dir = get_git_dir();
 	}
+}
+
+static struct option builtin_config_options[] = {
+	OPT_GROUP(N_("Config file location")),
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	OPT_GROUP(N_("Action")),
+	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
+	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
+	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
+	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
+	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
+	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
+	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
+	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
+	OPT_GROUP(N_("Type")),
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	OPT_GROUP(N_("Other")),
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
+	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_END(),
+};
+
+static NORETURN void usage_builtin_config(void)
+{
+	usage_with_options(builtin_config_usage, builtin_config_options);
+}
+
+int cmd_config(int argc, const char **argv, const char *prefix)
+{
+	char *value = NULL;
+	int flags = 0;
+	int ret = 0;
+	struct key_value_info default_kvi = KVI_INIT;
+
+	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
+
+	argc = parse_options(argc, argv, prefix, builtin_config_options,
+			     builtin_config_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+
+	handle_config_location(prefix);
 
 	if (end_nul) {
 		term = '\0';
@@ -848,7 +851,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		char *config_file;
 
 		check_argc(argc, 0, 0);
-		if (!given_config_source.file && nongit)
+		if (!given_config_source.file && !startup_info->have_repository)
 			die(_("not in a git directory"));
 		if (given_config_source.use_stdin)
 			die(_("editing stdin is not supported"));
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 05/13] builtin/config: pull out function to handle `--null`
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (3 preceding siblings ...)
  2024-03-27  8:46   ` [PATCH v3 04/13] builtin/config: pull out function to handle config location Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 06/13] builtin/config: introduce "list" subcommand Patrick Steinhardt
                     ` (8 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 1179 bytes --]

Pull out function to handle the `--null` option, which we are about to
reuse in subsequent commits.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 693df32526..ce2d3fecd4 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -697,6 +697,14 @@ static void handle_config_location(const char *prefix)
 	}
 }
 
+static void handle_nul(void) {
+	if (end_nul) {
+		term = '\0';
+		delim = '\n';
+		key_delim = '\n';
+	}
+}
+
 static struct option builtin_config_options[] = {
 	OPT_GROUP(N_("Config file location")),
 	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
@@ -758,12 +766,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 			     PARSE_OPT_STOP_AT_NON_OPTION);
 
 	handle_config_location(prefix);
-
-	if (end_nul) {
-		term = '\0';
-		delim = '\n';
-		key_delim = '\n';
-	}
+	handle_nul();
 
 	if ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {
 		error(_("--get-color and variable type are incoherent"));
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 06/13] builtin/config: introduce "list" subcommand
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (4 preceding siblings ...)
  2024-03-27  8:46   ` [PATCH v3 05/13] builtin/config: pull out function to handle `--null` Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 07/13] builtin/config: introduce "get" subcommand Patrick Steinhardt
                     ` (7 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 27749 bytes --]

While git-config(1) has several modes, those modes are not exposed with
subcommands but instead by specifying e.g. `--unset` or `--list`. This
user interface is not really in line with how our more modern commands
work, where it is a lot more customary to say e.g. `git remote list`.
Furthermore, to add to the confusion, git-config(1) also allows the user
to request modes implicitly by just specifying the correct number of
arguments. Thus, `git config foo.bar` will retrieve the value of
"foo.bar" while `git config foo.bar baz` will set it to "baz".

Overall, this makes for a confusing interface that could really use a
makeover. It hurts discoverability of what you can do with git-config(1)
and is comparatively easy to get wrong. Converting the command to have
subcommands instead would go a long way to help address these issues.

One concern in this context is backwards compatibility. Luckily, we can
introduce subcommands without breaking backwards compatibility at all.
This is because all the implicit modes of git-config(1) require that the
first argument is a properly formatted config key. And as config keys
_must_ have a dot in their name, any value without a dot would have been
discarded by git-config(1) previous to this change. Thus, given that
none of the subcommands do have a dot, they are unambiguous.

Introduce the first such new subcommand, which is "git config list". To
retain backwards compatibility we only conditionally use subcommands and
will fall back to the old syntax in case no subcommand was detected.
This should help to transition to the new-style syntax until we
eventually deprecate and remove the old-style syntax.

Note that the way we handle this we're duplicating some functionality
across old and new syntax. While this isn't pretty, it helps us to
ensure that there really is no change in behaviour for the old syntax.

Amend tests such that we run them both with old and new style syntax.
As tests are now run twice, state from the first run may be still be
around in the second run and thus cause tests to fail. Add cleanup logic
as required to fix such tests.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt |  26 ++++++---
 builtin/config.c             |  90 ++++++++++++++++++++++++----
 t/t1300-config.sh            | 110 +++++++++++++++++++++--------------
 3 files changed, 162 insertions(+), 64 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index a6e82b871b..485e84cd0f 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -9,6 +9,7 @@ git-config - Get and set repository or global options
 SYNOPSIS
 --------
 [verse]
+'git config list' [<file-option>] [<display-option>] [--includes]
 'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
 'git config' [<file-option>] [--type=<type>] --add <name> <value>
 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
@@ -20,7 +21,6 @@ SYNOPSIS
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
-'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list
 'git config' [<file-option>] --get-color <name> [<default>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
@@ -74,6 +74,12 @@ On success, the command returns the exit code 0.
 A list of all available configuration variables can be obtained using the
 `git help --config` command.
 
+COMMANDS
+--------
+
+list::
+	List all variables set in config file, along with their values.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -178,10 +184,6 @@ See also <<FILES>>.
 --unset-all::
 	Remove all lines matching the key from config file.
 
--l::
---list::
-	List all variables set in config file, along with their values.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -236,7 +238,7 @@ Valid `<type>`'s include:
 	contain line breaks.
 
 --name-only::
-	Output only the names of config variables for `--list` or
+	Output only the names of config variables for `list` or
 	`--get-regexp`.
 
 --show-origin::
@@ -288,10 +290,20 @@ Valid `<type>`'s include:
   When using `--get`, and the requested variable is not found, behave as if
   <value> were the value assigned to that variable.
 
+DEPRECATED MODES
+----------------
+
+The following modes have been deprecated in favor of subcommands. It is
+recommended to migrate to the new syntax.
+
+-l::
+--list::
+	Replaced by `git config list`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
-using `--list` or any of the `--get-*` which may return multiple results.
+using `list` or any of the `--get-*` which may return multiple results.
 The default is to use a pager.
 
 [[FILES]]
diff --git a/builtin/config.c b/builtin/config.c
index ce2d3fecd4..7dd588a622 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -16,10 +16,16 @@
 #include "worktree.h"
 
 static const char *const builtin_config_usage[] = {
+	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config [<options>]"),
 	NULL
 };
 
+static const char *const builtin_config_list_usage[] = {
+	N_("git config list [<file-option>] [<display-option>] [--includes]"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -33,6 +39,7 @@ static char delim = '=';
 static char key_delim = ' ';
 static char term = '\n';
 
+static parse_opt_subcommand_fn *subcommand;
 static int use_global_config, use_system_config, use_local_config;
 static int use_worktree_config;
 static struct git_config_source given_config_source;
@@ -705,14 +712,24 @@ static void handle_nul(void) {
 	}
 }
 
+#define CONFIG_LOCATION_OPTIONS \
+	OPT_GROUP(N_("Config file location")), \
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")), \
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")), \
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")), \
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")), \
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")), \
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object"))
+
+#define CONFIG_DISPLAY_OPTIONS \
+	OPT_GROUP(N_("Display options")), \
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), \
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")), \
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")), \
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)"))
+
 static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	CONFIG_LOCATION_OPTIONS,
 	OPT_GROUP(N_("Action")),
 	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
 	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
@@ -736,14 +753,11 @@ static struct option builtin_config_options[] = {
 	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
 	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
 	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	CONFIG_DISPLAY_OPTIONS,
 	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
 	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
 	OPT_END(),
 };
 
@@ -752,6 +766,42 @@ static NORETURN void usage_builtin_config(void)
 	usage_with_options(builtin_config_usage, builtin_config_options);
 }
 
+static int cmd_config_list(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_DISPLAY_OPTIONS,
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);
+	check_argc(argc, 0, 0);
+
+	handle_config_location(prefix);
+	handle_nul();
+
+	setup_auto_pager("config", 1);
+
+	if (config_with_options(show_all_config, NULL,
+				&given_config_source, the_repository,
+				&config_options) < 0) {
+		if (given_config_source.file)
+			die_errno(_("unable to read config file '%s'"),
+				  given_config_source.file);
+		else
+			die(_("error processing config file(s)"));
+	}
+
+	return 0;
+}
+
+static struct option builtin_subcommand_options[] = {
+	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
+	OPT_END(),
+};
+
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	char *value = NULL;
@@ -761,6 +811,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 
 	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
 
+	/*
+	 * This is somewhat hacky: we first parse the command line while
+	 * keeping all args intact in order to determine whether a subcommand
+	 * has been specified. If so, we re-parse it a second time, but this
+	 * time we drop KEEP_ARGV0. This is so that we don't munge the command
+	 * line in case no subcommand was given, which would otherwise confuse
+	 * us when parsing the legacy-style modes that don't use subcommands.
+	 */
+	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
+	if (subcommand) {
+		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
+		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);
+		return subcommand(argc, argv, prefix);
+	}
+
 	argc = parse_options(argc, argv, prefix, builtin_config_options,
 			     builtin_config_usage,
 			     PARSE_OPT_STOP_AT_NON_OPTION);
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 2d1bc1e27e..b140674293 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -11,6 +11,20 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
+for mode in legacy subcommands
+do
+
+case "$mode" in
+legacy)
+	mode_prefix="--"
+	;;
+subcommands)
+	mode_prefix=""
+	;;
+*)
+	BUG "unknown mode $mode";;
+esac
+
 test_expect_success 'clear default config' '
 	rm -f .git/config
 '
@@ -350,11 +364,11 @@ version.1.2.3eX.alpha=beta
 EOF
 
 test_expect_success 'working --list' '
-	git config --list > output &&
+	git config ${mode_prefix}list > output &&
 	test_cmp expect output
 '
 test_expect_success '--list without repo produces empty output' '
-	git --git-dir=nonexistent config --list >output &&
+	git --git-dir=nonexistent config ${mode_prefix}list >output &&
 	test_must_be_empty output
 '
 
@@ -366,7 +380,7 @@ version.1.2.3eX.alpha
 EOF
 
 test_expect_success '--name-only --list' '
-	git config --name-only --list >output &&
+	git config ${mode_prefix}list --name-only >output &&
 	test_cmp expect output
 '
 
@@ -504,17 +518,17 @@ ein.bahn=strasse
 EOF
 
 test_expect_success 'alternative GIT_CONFIG' '
-	GIT_CONFIG=other-config git config --list >output &&
+	GIT_CONFIG=other-config git config ${mode_prefix}list >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'alternative GIT_CONFIG (--file)' '
-	git config --file other-config --list >output &&
+	git config ${mode_prefix}list --file other-config >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'alternative GIT_CONFIG (--file=-)' '
-	git config --file - --list <other-config >output &&
+	git config ${mode_prefix}list --file - <other-config >output &&
 	test_cmp expect output
 '
 
@@ -527,6 +541,7 @@ test_expect_success 'editing stdin is an error' '
 '
 
 test_expect_success 'refer config from subdirectory' '
+	test_when_finished "rm -r x" &&
 	mkdir x &&
 	test_cmp_config -C x strasse --file=../other-config --get ein.bahn
 '
@@ -737,7 +752,7 @@ test_expect_success 'line number is reported correctly' '
 '
 
 test_expect_success 'invalid stdin config' '
-	echo "[broken" | test_must_fail git config --list --file - >output 2>&1 &&
+	echo "[broken" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&
 	test_grep "bad config line 1 in standard input" output
 '
 
@@ -1029,7 +1044,7 @@ section.quotecont=cont;inued
 EOF
 
 test_expect_success 'value continued on next line' '
-	git config --list > result &&
+	git config ${mode_prefix}list > result &&
 	test_cmp expect result
 '
 
@@ -1053,7 +1068,7 @@ Qsection.sub=section.val4
 Qsection.sub=section.val5Q
 EOF
 test_expect_success '--null --list' '
-	git config --null --list >result.raw &&
+	git config ${mode_prefix}list --null >result.raw &&
 	nul_to_q <result.raw >result &&
 	echo >>result &&
 	test_cmp expect result
@@ -1072,6 +1087,7 @@ test_expect_success 'inner whitespace kept verbatim' '
 '
 
 test_expect_success SYMLINKS 'symlinked configuration' '
+	test_when_finished "rm myconfig" &&
 	ln -s notyet myconfig &&
 	git config --file=myconfig test.frotz nitfol &&
 	test -h myconfig &&
@@ -1092,10 +1108,11 @@ test_expect_success SYMLINKS 'symlinked configuration' '
 '
 
 test_expect_success SYMLINKS 'symlink to nonexistent configuration' '
+	test_when_finished "rm linktonada linktolinktonada" &&
 	ln -s doesnotexist linktonada &&
 	ln -s linktonada linktolinktonada &&
-	test_must_fail git config --file=linktonada --list &&
-	test_must_fail git config --file=linktolinktonada --list
+	test_must_fail git config ${mode_prefix}list --file=linktonada &&
+	test_must_fail git config ${mode_prefix}list --file=linktolinktonada
 '
 
 test_expect_success 'check split_cmdline return' '
@@ -1352,7 +1369,7 @@ do
 done
 
 test_expect_success 'git -c is not confused by empty environment' '
-	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config --list
+	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config ${mode_prefix}list
 '
 
 test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
@@ -1543,31 +1560,31 @@ test_expect_success 'git config ignores pairs with empty count' '
 '
 
 test_expect_success 'git config fails with invalid count' '
-	test_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&
+	test_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&
 	test_grep "bogus count" error &&
-	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&
+	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&
 	test_grep "too many entries" error
 '
 
 test_expect_success 'git config fails with missing config key' '
 	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0="value" \
-		git config --list 2>error &&
+		git config ${mode_prefix}list 2>error &&
 	test_grep "missing config key" error
 '
 
 test_expect_success 'git config fails with missing config value' '
 	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="pair.one" \
-		git config --list 2>error &&
+		git config ${mode_prefix}list 2>error &&
 	test_grep "missing config value" error
 '
 
 test_expect_success 'git config fails with invalid config pair key' '
 	test_must_fail env GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \
-		git config --list &&
+		git config ${mode_prefix}list &&
 	test_must_fail env GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \
-		git config --list
+		git config ${mode_prefix}list
 '
 
 test_expect_success 'environment overrides config file' '
@@ -1607,7 +1624,7 @@ test_expect_success 'git config --edit works' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
 	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
-	git config -f tmp --list >actual &&
+	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
 
@@ -1616,7 +1633,7 @@ test_expect_success 'git config --edit respects core.editor' '
 	echo test.value=yes >expect &&
 	test_config core.editor "echo [test]value=yes >" &&
 	git config -f tmp --edit &&
-	git config -f tmp --list >actual &&
+	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
 
@@ -1967,7 +1984,7 @@ test_expect_success '--show-origin with --list' '
 	command line:	user.cmdline=true
 	EOF
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\
-		git -c user.cmdline=true config --list --show-origin >output &&
+		git -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -1984,7 +2001,7 @@ test_expect_success '--show-origin with --list --null' '
 	includeQcommand line:Quser.cmdline
 	trueQ
 	EOF
-	git -c user.cmdline=true config --null --list --show-origin >output.raw &&
+	git -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&
 	nul_to_q <output.raw >output &&
 	# The here-doc above adds a newline that the --null output would not
 	# include. Add it here to make the two comparable.
@@ -1998,7 +2015,7 @@ test_expect_success '--show-origin with single file' '
 	file:.git/config	user.override=local
 	file:.git/config	include.path=../include/relative.include
 	EOF
-	git config --local --list --show-origin >output &&
+	git config ${mode_prefix}list --local --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2036,7 +2053,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '
 	cat >expect <<-\EOF &&
 	file:"file\" (dq) and spaces.conf"	user.custom=true
 	EOF
-	git config --file "$WEIRDLY_NAMED_FILE" --show-origin --list >output &&
+	git config ${mode_prefix}list --file "$WEIRDLY_NAMED_FILE" --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2044,7 +2061,7 @@ test_expect_success '--show-origin stdin' '
 	cat >expect <<-\EOF &&
 	standard input:	user.custom=true
 	EOF
-	git config --file - --show-origin --list <"$CUSTOM_CONFIG_FILE" >output &&
+	git config ${mode_prefix}list --file - --show-origin <"$CUSTOM_CONFIG_FILE" >output &&
 	test_cmp expect output
 '
 
@@ -2071,7 +2088,7 @@ test_expect_success '--show-origin blob' '
 		cat >expect <<-EOF &&
 		blob:$blob	user.custom=true
 		EOF
-		git config --blob=$blob --show-origin --list >output &&
+		git config ${mode_prefix}list --blob=$blob --show-origin >output &&
 		test_cmp expect output
 	)
 '
@@ -2087,7 +2104,7 @@ test_expect_success '--show-origin blob ref' '
 		cp "$CUSTOM_CONFIG_FILE" custom.conf &&
 		git add custom.conf &&
 		git commit -m "new config file" &&
-		git config --blob=main:custom.conf --show-origin --list >output &&
+		git config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&
 		test_cmp expect output
 	)
 '
@@ -2113,13 +2130,14 @@ test_expect_success '--show-scope with --list' '
 	worktree	user.worktree=true
 	command	user.cmdline=true
 	EOF
+	test_when_finished "git worktree remove wt1" &&
 	git worktree add wt1 &&
 	# We need these to test for worktree scope, but outside of this
 	# test, this is just noise
 	test_config core.repositoryformatversion 1 &&
 	test_config extensions.worktreeConfig true &&
 	git config --worktree user.worktree true &&
-	git -c user.cmdline=true config --list --show-scope >output &&
+	git -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2128,7 +2146,7 @@ test_expect_success !MINGW '--show-scope with --blob' '
 	cat >expect <<-EOF &&
 	command	user.custom=true
 	EOF
-	git config --blob=$blob --show-scope --list >output &&
+	git config ${mode_prefix}list --blob=$blob --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2138,7 +2156,7 @@ test_expect_success '--show-scope with --local' '
 	local	user.override=local
 	local	include.path=../include/relative.include
 	EOF
-	git config --local --list --show-scope >output &&
+	git config ${mode_prefix}list --local --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2162,7 +2180,7 @@ test_expect_success '--show-scope with --show-origin' '
 	local	file:.git/../include/relative.include	user.relative=include
 	command	command line:	user.cmdline=true
 	EOF
-	git -c user.cmdline=true config --list --show-origin --show-scope >output &&
+	git -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2203,7 +2221,7 @@ test_expect_success 'override global and system config' '
 	global	home.config=true
 	local	local.config=true
 	EOF
-	git config --show-scope --list >output &&
+	git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output &&
 
 	cat >expect <<-EOF &&
@@ -2212,20 +2230,20 @@ test_expect_success 'override global and system config' '
 	local	local.config=true
 	EOF
 	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \
-		git config --show-scope --list >output &&
+		git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output &&
 
 	cat >expect <<-EOF &&
 	local	local.config=true
 	EOF
 	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \
-		git config --show-scope --list >output &&
+		git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'override global and system config with missing file' '
-	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&
-	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&
+	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&
+	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&
 	GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version
 '
 
@@ -2352,7 +2370,7 @@ test_expect_success 'set all config with value-pattern' '
 	# no match => add new entry
 	cp initial config &&
 	git config --file=config abc.key two a+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2365,7 +2383,7 @@ test_expect_success 'set all config with value-pattern' '
 
 	# multiple values, no match => add
 	git config --file=config abc.key three a+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2375,7 +2393,7 @@ test_expect_success 'set all config with value-pattern' '
 
 	# single match => replace
 	git config --file=config abc.key four h+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2390,7 +2408,7 @@ test_expect_success '--replace-all and value-pattern' '
 	git config --file=config --add abc.key two &&
 	git config --file=config --add abc.key three &&
 	git config --file=config --replace-all abc.key four "o+" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=four
 	abc.key=three
@@ -2408,7 +2426,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --rename-section dev null &&
 	test_must_fail git config --file=config --fixed-value --remove-section dev &&
-	test_must_fail git config --file=config --fixed-value --list &&
+	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
 
@@ -2429,7 +2447,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
 	fixed.test=bogus
@@ -2438,7 +2456,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config --fixed-value fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	fixed.test=bogus
 	EOF
@@ -2456,7 +2474,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config --replace-all fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
 	fixed.test=bogus
@@ -2464,7 +2482,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 	test_cmp expect actual &&
 
 	git config --file=config --fixed-value --replace-all fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=bogus
 	fixed.test=bogus
@@ -2625,4 +2643,6 @@ test_expect_success 'specifying multiple modes causes failure' '
 	test_cmp expect err
 '
 
+done
+
 test_done
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 07/13] builtin/config: introduce "get" subcommand
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (5 preceding siblings ...)
  2024-03-27  8:46   ` [PATCH v3 06/13] builtin/config: introduce "list" subcommand Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 08/13] builtin/config: introduce "set" subcommand Patrick Steinhardt
                     ` (6 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 32272 bytes --]

Introduce a new "get" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt |  93 +++++++++++++-------------
 builtin/config.c             |  69 ++++++++++++++++---
 t/t1300-config.sh            | 125 ++++++++++++++++++++++++-----------
 3 files changed, 191 insertions(+), 96 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 485e84cd0f..4a7d6b7543 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -10,18 +10,14 @@ SYNOPSIS
 --------
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
+'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
 'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
 'git config' [<file-option>] [--type=<type>] --add <name> <value>
 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL>
 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
-'git config' [<file-option>] --get-color <name> [<default>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
 
@@ -80,6 +76,12 @@ COMMANDS
 list::
 	List all variables set in config file, along with their values.
 
+get::
+	Emits the value of the specified key. If key is present multiple times
+	in the configuration, emits the last value. If `--all` is specified,
+	emits all values associated with key. Returns error code 1 if key is
+	not present.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -93,22 +95,16 @@ OPTIONS
 	values.  This is the same as providing '^$' as the `value-pattern`
 	in `--replace-all`.
 
---get::
-	Get the value for a given key (optionally filtered by a regex
-	matching the value). Returns error code 1 if the key was not
-	found and the last value if multiple key values were found.
-
---get-all::
-	Like get, but returns all values for a multi-valued key.
+--all::
+	With `get`, Return all values for a multi-valued key.
 
---get-regexp::
-	Like --get-all, but interprets the name as a regular expression and
-	writes out the key names.  Regular expression matching is currently
-	case-sensitive and done against a canonicalized version of the key
-	in which section and variable names are lowercased, but subsection
-	names are not.
+---regexp::
+	With `get`, interpret the name as a regular expression. Regular
+	expression matching is currently case-sensitive and done against a
+	canonicalized version of the key in which section and variable names
+	are lowercased, but subsection names are not.
 
---get-urlmatch <name> <URL>::
+--url=<URL>::
 	When given a two-part <name> as <section>.<key>, the value for
 	<section>.<URL>.<key> whose <URL> part matches the best to the
 	given URL is returned (if no such key exists, the value for
@@ -239,7 +235,7 @@ Valid `<type>`'s include:
 
 --name-only::
 	Output only the names of config variables for `list` or
-	`--get-regexp`.
+	`get`.
 
 --show-origin::
 	Augment the output of all queried config options with the
@@ -263,17 +259,6 @@ Valid `<type>`'s include:
 	When the color setting for `name` is undefined, the command uses
 	`color.ui` as fallback.
 
---get-color <name> [<default>]::
-
-	Find the color configured for `name` (e.g. `color.diff.new`) and
-	output it as the ANSI color escape sequence to the standard
-	output.  The optional `default` parameter is used instead, if
-	there is no color configured for `name`.
-+
-`--type=color [--default=<default>]` is preferred over `--get-color`
-(but note that `--get-color` will omit the trailing newline printed by
-`--type=color`).
-
 -e::
 --edit::
 	Opens an editor to modify the specified config file; either
@@ -287,7 +272,7 @@ Valid `<type>`'s include:
 	config files.
 
 --default <value>::
-  When using `--get`, and the requested variable is not found, behave as if
+  When using `get`, and the requested variable is not found, behave as if
   <value> were the value assigned to that variable.
 
 DEPRECATED MODES
@@ -296,15 +281,33 @@ DEPRECATED MODES
 The following modes have been deprecated in favor of subcommands. It is
 recommended to migrate to the new syntax.
 
+'git config <name>'::
+	Replaced by `git config get <name>`.
+
 -l::
 --list::
 	Replaced by `git config list`.
 
+--get <name> [<value-pattern>]::
+	Replaced by `git config get [--value=<pattern>] <name>`.
+
+--get-all <name> [<value-pattern>]::
+	Replaced by `git config get [--value=<pattern>] --all --show-names <name>`.
+
+--get-regexp <name-regexp>::
+	Replaced by `git config get --all --show-names --regexp <name-regexp>`.
+
+--get-urlmatch <name> <URL>::
+	Replaced by `git config get --all --show-names --url=<URL> <name>`.
+
+--get-color <name> [<default>]::
+	Replaced by `git config get --type=color [--default=<default>] <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
-using `list` or any of the `--get-*` which may return multiple results.
-The default is to use a pager.
+using `list` or `get` which may return multiple results. The default is to use
+a pager.
 
 [[FILES]]
 FILES
@@ -507,25 +510,19 @@ you have to provide a regex matching the value of exactly one line.
 To query the value for a given key, do
 
 ------------
-% git config --get core.filemode
-------------
-
-or
-
-------------
-% git config core.filemode
+% git config get core.filemode
 ------------
 
 or, to query a multivar:
 
 ------------
-% git config --get core.gitproxy "for kernel.org$"
+% git config get --value="for kernel.org$" core.gitproxy
 ------------
 
 If you want to know all the values for a multivar, do:
 
 ------------
-% git config --get-all core.gitproxy
+% git config get --all --show-names core.gitproxy
 ------------
 
 If you like to live dangerously, you can replace *all* core.gitproxy by a
@@ -559,8 +556,8 @@ script:
 
 ------------
 #!/bin/sh
-WS=$(git config --get-color color.diff.whitespace "blue reverse")
-RESET=$(git config --get-color "" "reset")
+WS=$(git config get --type=color --default="blue reverse" color.diff.whitespace)
+RESET=$(git config get --type=color --default="reset" "")
 echo "${WS}your whitespace color or blue reverse${RESET}"
 ------------
 
@@ -568,11 +565,11 @@ For URLs in `https://weak.example.com`, `http.sslVerify` is set to
 false, while it is set to `true` for all others:
 
 ------------
-% git config --type=bool --get-urlmatch http.sslverify https://good.example.com
+% git config get --type=bool --url=https://good.example.com http.sslverify
 true
-% git config --type=bool --get-urlmatch http.sslverify https://weak.example.com
+% git config get --type=bool --url=https://weak.example.com http.sslverify
 false
-% git config --get-urlmatch http https://weak.example.com
+% git config get --url=https://weak.example.com http
 http.cookieFile /tmp/cookie.txt
 http.sslverify false
 ------------
diff --git a/builtin/config.c b/builtin/config.c
index 7dd588a622..447500dbd2 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -17,7 +17,7 @@
 
 static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
-	N_("git config [<options>]"),
+	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	NULL
 };
 
@@ -26,6 +26,11 @@ static const char *const builtin_config_list_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_get_usage[] = {
+	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -721,6 +726,16 @@ static void handle_nul(void) {
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")), \
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object"))
 
+#define CONFIG_TYPE_OPTIONS \
+	OPT_GROUP(N_("Type")), \
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type), \
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL), \
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT), \
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT), \
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR), \
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH), \
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE)
+
 #define CONFIG_DISPLAY_OPTIONS \
 	OPT_GROUP(N_("Display options")), \
 	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), \
@@ -745,14 +760,7 @@ static struct option builtin_config_options[] = {
 	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
 	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
 	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	CONFIG_TYPE_OPTIONS,
 	CONFIG_DISPLAY_OPTIONS,
 	OPT_GROUP(N_("Other")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
@@ -797,8 +805,51 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix)
 	return 0;
 }
 
+static int cmd_config_get(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL, *url = NULL;
+	int flags = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_TYPE_OPTIONS,
+		OPT_GROUP(N_("Filter options")),
+		OPT_BOOL(0, "all", &do_all, N_("return all values for multi-valued config options")),
+		OPT_BOOL(0, "regexp", &use_key_regexp, N_("interpret the name as a regular expression")),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_STRING(0, "url", &url, N_("URL"), N_("show config matching the given URL")),
+		CONFIG_DISPLAY_OPTIONS,
+		OPT_BOOL(0, "show-names", &show_keys, N_("show config keys in addition to their values")),
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+		OPT_STRING(0, "default", &default_value, N_("value"), N_("use default value when missing entry")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_get_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_argc(argc, 1, 1);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with 'value-pattern'"));
+	if (default_value && (do_all || url))
+		die(_("--default= cannot be used with --all or --url="));
+	if (url && (do_all || use_key_regexp || value_pattern))
+		die(_("--url= cannot be used with --all, --regexp or --value"));
+
+	handle_config_location(prefix);
+	handle_nul();
+
+	setup_auto_pager("config", 1);
+
+	if (url)
+		return get_urlmatch(argv[0], url);
+	return get_value(argv[0], value_pattern, flags);
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
+	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index b140674293..f1547921c6 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -17,9 +17,15 @@ do
 case "$mode" in
 legacy)
 	mode_prefix="--"
+	mode_get=""
+	mode_get_all="--get-all"
+	mode_get_regexp="--get-regexp"
 	;;
 subcommands)
 	mode_prefix=""
+	mode_get="get"
+	mode_get_all="get --all"
+	mode_get_regexp="get --regexp --all --show-names"
 	;;
 *)
 	BUG "unknown mode $mode";;
@@ -288,7 +294,7 @@ test_expect_success 'multi-valued get-all returns all' '
 	wow
 	wow2 for me
 	EOF
-	git config --get-all nextsection.nonewline >actual &&
+	git config ${mode_get_all} nextsection.nonewline >actual &&
 	test_cmp expect actual
 '
 
@@ -390,7 +396,7 @@ nextsection.nonewline wow2 for me
 EOF
 
 test_expect_success '--get-regexp' '
-	git config --get-regexp in >output &&
+	git config ${mode_get_regexp} in >output &&
 	test_cmp expect output
 '
 
@@ -400,7 +406,7 @@ nextsection.nonewline
 EOF
 
 test_expect_success '--name-only --get-regexp' '
-	git config --name-only --get-regexp in >output &&
+	git config ${mode_get_regexp} --name-only in >output &&
 	test_cmp expect output
 '
 
@@ -411,7 +417,7 @@ EOF
 
 test_expect_success '--add' '
 	git config --add nextsection.nonewline "wow4 for you" &&
-	git config --get-all nextsection.nonewline > output &&
+	git config ${mode_get_all} nextsection.nonewline > output &&
 	test_cmp expect output
 '
 
@@ -433,21 +439,21 @@ test_expect_success 'get variable with empty value' '
 echo novalue.variable > expect
 
 test_expect_success 'get-regexp variable with no value' '
-	git config --get-regexp novalue > output &&
+	git config ${mode_get_regexp} novalue > output &&
 	test_cmp expect output
 '
 
 echo 'novalue.variable true' > expect
 
 test_expect_success 'get-regexp --bool variable with no value' '
-	git config --bool --get-regexp novalue > output &&
+	git config ${mode_get_regexp} --bool novalue > output &&
 	test_cmp expect output
 '
 
 echo 'emptyvalue.variable ' > expect
 
 test_expect_success 'get-regexp variable with empty value' '
-	git config --get-regexp emptyvalue > output &&
+	git config ${mode_get_regexp} emptyvalue > output &&
 	test_cmp expect output
 '
 
@@ -1021,7 +1027,7 @@ test_expect_success 'quoting' '
 '
 
 test_expect_success 'key with newline' '
-	test_must_fail git config "key.with
+	test_must_fail git config ${mode_get} "key.with
 newline" 123'
 
 test_expect_success 'value with newline' 'git config key.sub value.with\\\
@@ -1075,7 +1081,7 @@ test_expect_success '--null --list' '
 '
 
 test_expect_success '--null --get-regexp' '
-	git config --null --get-regexp "val[0-9]" >result.raw &&
+	git config ${mode_get_regexp} --null "val[0-9]" >result.raw &&
 	nul_to_q <result.raw >result &&
 	echo >>result &&
 	test_cmp expect result
@@ -1157,11 +1163,11 @@ test_expect_success 'git -c can represent empty string' '
 '
 
 test_expect_success 'key sanity-checking' '
-	test_must_fail git config foo=bar &&
-	test_must_fail git config foo=.bar &&
-	test_must_fail git config foo.ba=r &&
-	test_must_fail git config foo.1bar &&
-	test_must_fail git config foo."ba
+	test_must_fail git config ${mode_get} foo=bar &&
+	test_must_fail git config ${mode_get} foo=.bar &&
+	test_must_fail git config ${mode_get} foo.ba=r &&
+	test_must_fail git config ${mode_get} foo.1bar &&
+	test_must_fail git config ${mode_get} foo."ba
 				z".bar &&
 	test_must_fail git config . false &&
 	test_must_fail git config .foo false &&
@@ -1210,7 +1216,7 @@ test_expect_success 'git -c complains about empty key and value' '
 '
 
 test_expect_success 'multiple git -c appends config' '
-	test_config alias.x "!git -c x.two=2 config --get-regexp ^x\.*" &&
+	test_config alias.x "!git -c x.two=2 config ${mode_get_regexp} ^x\.*" &&
 	cat >expect <<-\EOF &&
 	x.one 1
 	x.two 2
@@ -1376,7 +1382,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
 	v="${SQ}key.one=foo${SQ}" &&
 	v="$v  ${SQ}key.two=bar${SQ}" &&
 	v="$v ${SQ}key.ambiguous=section.whatever=value${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.one foo
 	key.two bar
@@ -1389,7 +1395,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '
 	v="${SQ}key.one${SQ}=${SQ}foo${SQ}" &&
 	v="$v  ${SQ}key.two${SQ}=${SQ}bar${SQ}" &&
 	v="$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.one foo
 	key.two bar
@@ -1403,7 +1409,7 @@ test_expect_success 'old and new-style entries can mix' '
 	v="$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}" &&
 	v="$v ${SQ}key.oldtwo=oldbar${SQ}" &&
 	v="$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.oldone oldfoo
 	key.newone newfoo
@@ -1416,7 +1422,7 @@ test_expect_success 'old and new-style entries can mix' '
 test_expect_success 'old and new bools with ambiguous subsection' '
 	v="${SQ}key.with=equals.oldbool${SQ}" &&
 	v="$v ${SQ}key.with=equals.newbool${SQ}=" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.with equals.oldbool
 	key.with=equals.newbool
@@ -1430,7 +1436,7 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
 	env.two two
 	EOF
 	GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ} ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*" >actual &&
+		git config ${mode_get_regexp} "env.*" >actual &&
 	test_cmp expect actual &&
 
 	cat >expect <<-EOF &&
@@ -1438,12 +1444,12 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
 	env.two two
 	EOF
 	GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ}\\$SQ$SQ$SQ ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*" >actual &&
+		git config ${mode_get_regexp} "env.*" >actual &&
 	test_cmp expect actual &&
 
 	test_must_fail env \
 		GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ}\\$SQ ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*"
+		git config ${mode_get_regexp} "env.*"
 '
 
 test_expect_success 'git --config-env=key=envvar support' '
@@ -1491,7 +1497,7 @@ test_expect_success 'git -c and --config-env work together' '
 	ENVVAR=env-value git \
 		-c bar.cmd=cmd-value \
 		--config-env=bar.env=ENVVAR \
-		config --get-regexp "^bar.*" >actual &&
+		config ${mode_get_regexp} "^bar.*" >actual &&
 	test_cmp expect actual
 '
 
@@ -1519,7 +1525,7 @@ test_expect_success 'git config handles environment config pairs' '
 	GIT_CONFIG_COUNT=2 \
 		GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="foo" \
 		GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="bar" \
-		git config --get-regexp "pair.*" >actual &&
+		git config ${mode_get_regexp} "pair.*" >actual &&
 	cat >expect <<-EOF &&
 	pair.one foo
 	pair.two bar
@@ -1529,7 +1535,7 @@ test_expect_success 'git config handles environment config pairs' '
 
 test_expect_success 'git config ignores pairs without count' '
 	test_must_fail env GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
@@ -1537,7 +1543,7 @@ test_expect_success 'git config ignores pairs exceeding count' '
 	GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
 		GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="value" \
-		git config --get-regexp "pair.*" >actual 2>error &&
+		git config ${mode_get_regexp} "pair.*" >actual 2>error &&
 	cat >expect <<-EOF &&
 	pair.one value
 	EOF
@@ -1548,14 +1554,14 @@ test_expect_success 'git config ignores pairs exceeding count' '
 test_expect_success 'git config ignores pairs with zero count' '
 	test_must_fail env \
 		GIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
 test_expect_success 'git config ignores pairs with empty count' '
 	test_must_fail env \
 		GIT_CONFIG_COUNT= GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
@@ -1594,7 +1600,7 @@ test_expect_success 'environment overrides config file' '
 	one = value
 	EOF
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \
-		git config pair.one >actual &&
+		git config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1604,7 +1610,7 @@ test_expect_success 'environment overrides config file' '
 test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \
 		GIT_CONFIG_PARAMETERS="${SQ}pair.one=override${SQ}" \
-		git config pair.one >actual &&
+		git config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1679,20 +1685,28 @@ test_expect_success 'urlmatch' '
 
 	test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&
 	test_must_be_empty actual &&
+	test_expect_code 1 git config get --url=https://good.example.com --bool doesnt.exist >actual &&
+	test_must_be_empty actual &&
 
 	echo true >expect &&
 	git config --bool --get-urlmatch http.SSLverify https://good.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --bool --url=https://good.example.com http.SSLverify >actual &&
+	test_cmp expect actual &&
 
 	echo false >expect &&
 	git config --bool --get-urlmatch http.sslverify https://weak.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --bool --url=https://weak.example.com http.sslverify >actual &&
+	test_cmp expect actual &&
 
 	{
 		echo http.cookiefile /tmp/cookie.txt &&
 		echo http.sslverify false
 	} >expect &&
 	git config --get-urlmatch HTTP https://weak.example.com >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://weak.example.com HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -1708,6 +1722,8 @@ test_expect_success 'urlmatch with --show-scope' '
 	local	http.sslverify false
 	EOF
 	git config --get-urlmatch --show-scope HTTP https://weak.example.com >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://weak.example.com --show-scope HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -1740,45 +1756,67 @@ test_expect_success 'urlmatch favors more specific URLs' '
 	echo http.cookiefile /tmp/root.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com/subdirectory >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com/subdirectory HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com/subdirectory/nested HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/user.txt >expect &&
 	git config --get-urlmatch HTTP https://user@example.com/ >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://user@example.com/ HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://averylonguser@example.com/subdirectory HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/preceding.txt >expect &&
 	git config --get-urlmatch HTTP https://preceding.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://preceding.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/wildcard.txt >expect &&
 	git config --get-urlmatch HTTP https://wildcard.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://wildcard.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/sub.txt >expect &&
 	git config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://sub.example.com/wildcardwithsubdomain HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/trailing.txt >expect &&
 	git config --get-urlmatch HTTP https://trailing.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://trailing.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/sub.txt >expect &&
 	git config --get-urlmatch HTTP https://user@sub.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://user@sub.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/multiwildcard.txt >expect &&
 	git config --get-urlmatch HTTP https://wildcard.example.org >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://wildcard.example.org HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -1901,7 +1939,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[one]
 	EOF
 	git config --unset two.subsection.key &&
-	test "not [two subsection]" = "$(git config one.key)" &&
+	test "not [two subsection]" = "$(git config ${mode_get} one.key)" &&
 	test_line_count = 3 .git/config
 '
 
@@ -2024,7 +2062,7 @@ test_expect_success '--show-origin with --get-regexp' '
 	file:$HOME/.gitconfig	user.global true
 	file:.git/config	user.local true
 	EOF
-	git config --show-origin --get-regexp "user\.[g|l].*" >output &&
+	git config ${mode_get_regexp} --show-origin "user\.[g|l].*" >output &&
 	test_cmp expect output
 '
 
@@ -2032,7 +2070,7 @@ test_expect_success '--show-origin getting a single key' '
 	cat >expect <<-\EOF &&
 	file:.git/config	local
 	EOF
-	git config --show-origin user.override >output &&
+	git config ${mode_get} --show-origin user.override >output &&
 	test_cmp expect output
 '
 
@@ -2164,7 +2202,7 @@ test_expect_success '--show-scope getting a single value' '
 	cat >expect <<-\EOF &&
 	local	true
 	EOF
-	git config --show-scope --get user.local >output &&
+	git config ${mode_get} --show-scope user.local >output &&
 	test_cmp expect output
 '
 
@@ -2433,9 +2471,9 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	# These modes complain when --fixed-value has no value-pattern
 	test_must_fail git config --file=config --fixed-value dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --get dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-all dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-regexp "dev.*" &&
+	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
 	test_must_fail git config --file=config --fixed-value --unset dev.null &&
 	test_must_fail git config --file=config --fixed-value --unset-all dev.null
 '
@@ -2465,12 +2503,12 @@ test_expect_success '--fixed-value uses exact string matching' '
 	cp initial config &&
 	test_must_fail git config --file=config --unset fixed.test "$META" &&
 	git config --file=config --fixed-value --unset fixed.test "$META" &&
-	test_must_fail git config --file=config fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
 	test_must_fail git config --file=config --unset-all fixed.test "$META" &&
 	git config --file=config --fixed-value --unset-all fixed.test "$META" &&
-	test_must_fail git config --file=config fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
 	git config --file=config --replace-all fixed.test bogus "$META" &&
@@ -2497,18 +2535,27 @@ test_expect_success '--get and --get-all with --fixed-value' '
 	git config --file=config --add fixed.test "$META" &&
 
 	git config --file=config --get fixed.test bogus &&
+	git config get --file=config --value=bogus fixed.test &&
 	test_must_fail git config --file=config --get fixed.test "$META" &&
+	test_must_fail git config get --file=config --value="$META" fixed.test &&
 	git config --file=config --get --fixed-value fixed.test "$META" &&
+	git config get --file=config --fixed-value --value="$META" fixed.test &&
 	test_must_fail git config --file=config --get --fixed-value fixed.test non-existent &&
 
 	git config --file=config --get-all fixed.test bogus &&
+	git config get --all --file=config --value=bogus fixed.test &&
 	test_must_fail git config --file=config --get-all fixed.test "$META" &&
+	test_must_fail git config get --all --file=config --value="$META" fixed.test &&
 	git config --file=config --get-all --fixed-value fixed.test "$META" &&
+	git config get --all --file=config --value="$META" --fixed-value fixed.test &&
 	test_must_fail git config --file=config --get-all --fixed-value fixed.test non-existent &&
 
 	git config --file=config --get-regexp fixed+ bogus &&
+	git config get --regexp --file=config --value=bogus fixed+ &&
 	test_must_fail git config --file=config --get-regexp fixed+ "$META" &&
+	test_must_fail git config get --regexp --file=config --value="$META" fixed+ &&
 	git config --file=config --get-regexp --fixed-value fixed+ "$META" &&
+	git config get --regexp --file=config --fixed-value --value="$META" fixed+ &&
 	test_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent
 '
 
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 08/13] builtin/config: introduce "set" subcommand
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (6 preceding siblings ...)
  2024-03-27  8:46   ` [PATCH v3 07/13] builtin/config: introduce "get" subcommand Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 09/13] builtin/config: introduce "unset" subcommand Patrick Steinhardt
                     ` (5 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 18693 bytes --]

Introduce a new "set" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 38 +++++++++------
 builtin/config.c             | 57 ++++++++++++++++++++++
 t/t1300-config.sh            | 92 +++++++++++++++++++-----------------
 3 files changed, 129 insertions(+), 58 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 4a7d6b7543..7fdf9b434e 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -11,9 +11,7 @@ SYNOPSIS
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
-'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
-'git config' [<file-option>] [--type=<type>] --add <name> <value>
-'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
+'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
@@ -27,7 +25,7 @@ You can query/set/replace/unset options with this command. The name is
 actually the section and the key separated by a dot, and the value will be
 escaped.
 
-Multiple lines can be added to an option by using the `--add` option.
+Multiple lines can be added to an option by using the `--append` option.
 If you want to update or unset an option which can occur on multiple
 lines, a `value-pattern` (which is an extended regular expression,
 unless the `--fixed-value` option is given) needs to be given.  Only the
@@ -82,6 +80,13 @@ get::
 	emits all values associated with key. Returns error code 1 if key is
 	not present.
 
+set::
+	Set value for one or more config options. By default, this command
+	refuses to write multi-valued config options. Passing `--all` will
+	replace all multi-valued config options with the new value, whereas
+	`--value=` will replace all config options whose values match the given
+	pattern.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -90,10 +95,9 @@ OPTIONS
 	Default behavior is to replace at most one line. This replaces
 	all lines matching the key (and optionally the `value-pattern`).
 
---add::
+--append::
 	Adds a new line to the option without altering any existing
-	values.  This is the same as providing '^$' as the `value-pattern`
-	in `--replace-all`.
+	values. This is the same as providing '--value=^$' in `set`.
 
 --all::
 	With `get`, Return all values for a multi-valued key.
@@ -284,6 +288,9 @@ recommended to migrate to the new syntax.
 'git config <name>'::
 	Replaced by `git config get <name>`.
 
+'git config <name> <value> [<value-pattern>]'::
+	Replaced by `git config set [--value=<pattern>] <name> <value>`.
+
 -l::
 --list::
 	Replaced by `git config list`.
@@ -303,6 +310,9 @@ recommended to migrate to the new syntax.
 --get-color <name> [<default>]::
 	Replaced by `git config get --type=color [--default=<default>] <name>`.
 
+--add <name> <value>::
+	Replaced by `git config set --append <name> <value>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
@@ -349,7 +359,7 @@ precedence over values read earlier.  When multiple values are taken then all
 values of a key from all files will be used.
 
 By default, options are only written to the repository specific
-configuration file. Note that this also affects options like `--replace-all`
+configuration file. Note that this also affects options like `set`
 and `--unset`. *'git config' will only ever change one file at a time*.
 
 You can limit which configuration sources are read from or written to by
@@ -485,7 +495,7 @@ Given a .git/config like this:
 you can set the filemode to true with
 
 ------------
-% git config core.filemode true
+% git config set core.filemode true
 ------------
 
 The hypothetical proxy command entries actually have a postfix to discern
@@ -493,7 +503,7 @@ what URL they apply to. Here is how to change the entry for kernel.org
 to "ssh".
 
 ------------
-% git config core.gitproxy '"ssh" for kernel.org' 'for kernel.org$'
+% git config set --value='for kernel.org$' core.gitproxy '"ssh" for kernel.org'
 ------------
 
 This makes sure that only the key/value pair for kernel.org is replaced.
@@ -529,26 +539,26 @@ If you like to live dangerously, you can replace *all* core.gitproxy by a
 new one with
 
 ------------
-% git config --replace-all core.gitproxy ssh
+% git config set --all core.gitproxy ssh
 ------------
 
 However, if you really only want to replace the line for the default proxy,
 i.e. the one without a "for ..." postfix, do something like this:
 
 ------------
-% git config core.gitproxy ssh '! for '
+% git config set --value='! for ' core.gitproxy ssh
 ------------
 
 To actually match only values with an exclamation mark, you have to
 
 ------------
-% git config section.key value '[!]'
+% git config set --value='[!]' section.key value
 ------------
 
 To add a new proxy, without altering any of the existing ones, use
 
 ------------
-% git config --add core.gitproxy '"proxy-command" for example.com'
+% git config set --append core.gitproxy '"proxy-command" for example.com'
 ------------
 
 An example to use customized color from the configuration in your
diff --git a/builtin/config.c b/builtin/config.c
index 447500dbd2..eec57fbd17 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -18,6 +18,7 @@
 static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
+	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	NULL
 };
 
@@ -31,6 +32,11 @@ static const char *const builtin_config_get_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_set_usage[] = {
+	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -847,9 +853,60 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix)
 	return get_value(argv[0], value_pattern, flags);
 }
 
+static int cmd_config_set(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL;
+	int flags = 0, append = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_TYPE_OPTIONS,
+		OPT_GROUP(N_("Filter")),
+		OPT_BIT(0, "all", &flags, N_("replace multi-valued config option with new value"), CONFIG_FLAGS_MULTI_REPLACE),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "append", &append, N_("add a new line without altering any existing values")),
+		OPT_END(),
+	};
+	struct key_value_info default_kvi = KVI_INIT;
+	char *value;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 2, 2);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with --value=<pattern>"));
+	if (append && value_pattern)
+		die(_("--append cannot be used with --value=<pattern>"));
+	if (append)
+		value_pattern = CONFIG_REGEX_NONE;
+
+	handle_config_location(prefix);
+
+	value = normalize_value(argv[0], argv[1], &default_kvi);
+
+	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {
+		ret = git_config_set_multivar_in_file_gently(given_config_source.file,
+							     argv[0], value, value_pattern,
+							     flags);
+	} else {
+		ret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);
+		if (ret == CONFIG_NOTHING_SET)
+			error(_("cannot overwrite multiple values with a single value\n"
+			"       Use a regexp, --add or --replace-all to change %s."), argv[0]);
+	}
+
+	free(value);
+	return ret;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
+	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index f1547921c6..f435b8f194 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -20,12 +20,16 @@ legacy)
 	mode_get=""
 	mode_get_all="--get-all"
 	mode_get_regexp="--get-regexp"
+	mode_set=""
+	mode_replace_all="--replace-all"
 	;;
 subcommands)
 	mode_prefix=""
 	mode_get="get"
 	mode_get_all="get --all"
 	mode_get_regexp="get --regexp --all --show-names"
+	mode_set="set"
+	mode_replace_all="set --all"
 	;;
 *)
 	BUG "unknown mode $mode";;
@@ -40,7 +44,7 @@ cat > expect << EOF
 	penguin = little blue
 EOF
 test_expect_success 'initial' '
-	git config section.penguin "little blue" &&
+	git config ${mode_set} section.penguin "little blue" &&
 	test_cmp expect .git/config
 '
 
@@ -50,7 +54,7 @@ cat > expect << EOF
 	Movie = BadPhysics
 EOF
 test_expect_success 'mixed case' '
-	git config Section.Movie BadPhysics &&
+	git config ${mode_set} Section.Movie BadPhysics &&
 	test_cmp expect .git/config
 '
 
@@ -62,7 +66,7 @@ cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'similar section' '
-	git config Sections.WhatEver Second &&
+	git config ${mode_set} Sections.WhatEver Second &&
 	test_cmp expect .git/config
 '
 
@@ -75,7 +79,7 @@ cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'uppercase section' '
-	git config SECTION.UPPERCASE true &&
+	git config ${mode_set} SECTION.UPPERCASE true &&
 	test_cmp expect .git/config
 '
 
@@ -191,14 +195,14 @@ test_expect_success 'multiple unset is correct' '
 cp .git/config2 .git/config
 
 test_expect_success '--replace-all missing value' '
-	test_must_fail git config --replace-all beta.haha &&
+	test_must_fail git config ${mode_replace_all} beta.haha &&
 	test_cmp .git/config2 .git/config
 '
 
 rm .git/config2
 
 test_expect_success '--replace-all' '
-	git config --replace-all beta.haha gamma
+	git config ${mode_replace_all} beta.haha gamma
 '
 
 cat > expect << EOF
@@ -225,7 +229,7 @@ noIndent= sillyValue ; 'nother silly comment
 [nextSection] noNewline = ouch
 EOF
 test_expect_success 'really mean test' '
-	git config beta.haha alpha &&
+	git config ${mode_set} beta.haha alpha &&
 	test_cmp expect .git/config
 '
 
@@ -240,7 +244,7 @@ noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'really really mean test' '
-	git config nextsection.nonewline wow &&
+	git config ${mode_set} nextsection.nonewline wow &&
 	test_cmp expect .git/config
 '
 
@@ -714,16 +718,16 @@ EOF
 
 test_expect_success 'section ending' '
 	rm -f .git/config &&
-	git config gitcvs.enabled true &&
-	git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
-	git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.enabled true &&
+	git config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
 	test_cmp expect .git/config
 
 '
 
 test_expect_success numbers '
-	git config kilo.gram 1k &&
-	git config mega.ton 1m &&
+	git config ${mode_set} kilo.gram 1k &&
+	git config ${mode_set} mega.ton 1m &&
 	echo 1024 >expect &&
 	echo 1048576 >>expect &&
 	git config --int --get kilo.gram >actual &&
@@ -732,20 +736,20 @@ test_expect_success numbers '
 '
 
 test_expect_success '--int is at least 64 bits' '
-	git config giga.watts 121g &&
+	git config ${mode_set} giga.watts 121g &&
 	echo  >expect &&
 	test_cmp_config 129922760704 --int --get giga.watts
 '
 
 test_expect_success 'invalid unit' '
-	git config aninvalid.unit "1auto" &&
+	git config ${mode_set} aninvalid.unit "1auto" &&
 	test_cmp_config 1auto aninvalid.unit &&
 	test_must_fail git config --int --get aninvalid.unit 2>actual &&
 	test_grep "bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit" actual
 '
 
 test_expect_success 'invalid unit boolean' '
-	git config commit.gpgsign "1true" &&
+	git config ${mode_set} commit.gpgsign "1true" &&
 	test_cmp_config 1true commit.gpgsign &&
 	test_must_fail git config --bool --get commit.gpgsign 2>actual &&
 	test_grep "bad boolean config value .1true. for .commit.gpgsign." actual
@@ -775,14 +779,14 @@ EOF
 
 test_expect_success bool '
 
-	git config bool.true1 01 &&
-	git config bool.true2 -1 &&
-	git config bool.true3 YeS &&
-	git config bool.true4 true &&
-	git config bool.false1 000 &&
-	git config bool.false2 "" &&
-	git config bool.false3 nO &&
-	git config bool.false4 FALSE &&
+	git config ${mode_set} bool.true1 01 &&
+	git config ${mode_set} bool.true2 -1 &&
+	git config ${mode_set} bool.true3 YeS &&
+	git config ${mode_set} bool.true4 true &&
+	git config ${mode_set} bool.false1 000 &&
+	git config ${mode_set} bool.false2 "" &&
+	git config ${mode_set} bool.false3 nO &&
+	git config ${mode_set} bool.false4 FALSE &&
 	rm -f result &&
 	for i in 1 2 3 4
 	do
@@ -793,7 +797,7 @@ test_expect_success bool '
 
 test_expect_success 'invalid bool (--get)' '
 
-	git config bool.nobool foobar &&
+	git config ${mode_set} bool.nobool foobar &&
 	test_must_fail git config --bool --get bool.nobool'
 
 test_expect_success 'invalid bool (set)' '
@@ -982,7 +986,7 @@ test_expect_success 'get --expiry-date' '
 
 test_expect_success 'get --type=color' '
 	rm .git/config &&
-	git config foo.color "red" &&
+	git config ${mode_set} foo.color "red" &&
 	git config --get --type=color foo.color >actual.raw &&
 	test_decode_color <actual.raw >actual &&
 	echo "<RED>" >expect &&
@@ -1019,10 +1023,10 @@ cat > expect << EOF
 EOF
 test_expect_success 'quoting' '
 	rm -f .git/config &&
-	git config quote.leading " test" &&
-	git config quote.ending "test " &&
-	git config quote.semicolon "test;test" &&
-	git config quote.hash "test#test" &&
+	git config ${mode_set} quote.leading " test" &&
+	git config ${mode_set} quote.ending "test " &&
+	git config ${mode_set} quote.semicolon "test;test" &&
+	git config ${mode_set} quote.hash "test#test" &&
 	test_cmp expect .git/config
 '
 
@@ -1030,7 +1034,7 @@ test_expect_success 'key with newline' '
 	test_must_fail git config ${mode_get} "key.with
 newline" 123'
 
-test_expect_success 'value with newline' 'git config key.sub value.with\\\
+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\
 newline'
 
 cat > .git/config <<\EOF
@@ -1088,7 +1092,7 @@ test_expect_success '--null --get-regexp' '
 '
 
 test_expect_success 'inner whitespace kept verbatim' '
-	git config section.val "foo 	  bar" &&
+	git config ${mode_set} section.val "foo 	  bar" &&
 	test_cmp_config "foo 	  bar" section.val
 '
 
@@ -1126,12 +1130,12 @@ test_expect_success 'check split_cmdline return' '
 	git init repo &&
 	(
 		cd repo &&
-		git config alias.split-cmdline-fix "echo \"" &&
+		git config ${mode_set} alias.split-cmdline-fix "echo \"" &&
 		test_must_fail git split-cmdline-fix &&
 		echo foo >foo &&
 		git add foo &&
 		git commit -m "initial commit" &&
-		git config branch.main.mergeoptions "echo \"" &&
+		git config ${mode_set} branch.main.mergeoptions "echo \"" &&
 		test_must_fail git merge main
 	)
 '
@@ -1169,12 +1173,12 @@ test_expect_success 'key sanity-checking' '
 	test_must_fail git config ${mode_get} foo.1bar &&
 	test_must_fail git config ${mode_get} foo."ba
 				z".bar &&
-	test_must_fail git config . false &&
-	test_must_fail git config .foo false &&
-	test_must_fail git config foo. false &&
-	test_must_fail git config .foo. false &&
-	git config foo.bar true &&
-	git config foo."ba =z".bar false
+	test_must_fail git config ${mode_set} . false &&
+	test_must_fail git config ${mode_set} .foo false &&
+	test_must_fail git config ${mode_set} foo. false &&
+	test_must_fail git config ${mode_set} .foo. false &&
+	git config ${mode_set} foo.bar true &&
+	git config ${mode_set} foo."ba =z".bar false
 '
 
 test_expect_success 'git -c works with aliases of builtins' '
@@ -2397,7 +2401,7 @@ test_expect_success '--replace-all does not invent newlines' '
 	[abc]
 	Qkey = b
 	EOF
-	git config --replace-all abc.key b &&
+	git config ${mode_replace_all} abc.key b &&
 	test_cmp expect .git/config
 '
 
@@ -2469,8 +2473,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
 
 	# These modes complain when --fixed-value has no value-pattern
-	test_must_fail git config --file=config --fixed-value dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
+	test_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&
+	test_must_fail git config ${mode_replace_all} --file=config --fixed-value dev.null bogus &&
 	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
@@ -2511,7 +2515,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
-	git config --file=config --replace-all fixed.test bogus "$META" &&
+	git config --file=config fixed.test bogus "$META" &&
 	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 09/13] builtin/config: introduce "unset" subcommand
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (7 preceding siblings ...)
  2024-03-27  8:46   ` [PATCH v3 08/13] builtin/config: introduce "set" subcommand Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 10/13] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
                     ` (4 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 11748 bytes --]

Introduce a new "unset" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 25 ++++++++++++--------
 builtin/config.c             | 38 +++++++++++++++++++++++++++++++
 t/t1300-config.sh            | 44 ++++++++++++++++++++++++------------
 3 files changed, 82 insertions(+), 25 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 7fdf9b434e..4fbc73a19e 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -12,8 +12,7 @@ SYNOPSIS
 'git config list' [<file-option>] [<display-option>] [--includes]
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
-'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
-'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
+'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
@@ -87,6 +86,12 @@ set::
 	`--value=` will replace all config options whose values match the given
 	pattern.
 
+unset::
+	Unset value for one or more config options. By default, this command
+	refuses to unset multi-valued keys. Passing `--all` will unset all
+	multi-valued config options, whereas `--value` will unset all config
+	options whose values match the given pattern.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -178,12 +183,6 @@ See also <<FILES>>.
 --rename-section::
 	Rename the given section to a new name.
 
---unset::
-	Remove the line matching the key from config file.
-
---unset-all::
-	Remove all lines matching the key from config file.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -313,6 +312,12 @@ recommended to migrate to the new syntax.
 --add <name> <value>::
 	Replaced by `git config set --append <name> <value>`.
 
+--unset <name> [<value-pattern>]::
+	Replaced by `git config unset [--value=<pattern>] <name>`.
+
+--unset-all <name> [<value-pattern>]::
+	Replaced by `git config unset [--value=<pattern>] --all <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
@@ -360,7 +365,7 @@ values of a key from all files will be used.
 
 By default, options are only written to the repository specific
 configuration file. Note that this also affects options like `set`
-and `--unset`. *'git config' will only ever change one file at a time*.
+and `unset`. *'git config' will only ever change one file at a time*.
 
 You can limit which configuration sources are read from or written to by
 specifying the path of a file with the `--file` option, or by specifying a
@@ -511,7 +516,7 @@ This makes sure that only the key/value pair for kernel.org is replaced.
 To delete the entry for renames, do
 
 ------------
-% git config --unset diff.renames
+% git config unset diff.renames
 ------------
 
 If you want to delete an entry for a multivar (like core.gitproxy above),
diff --git a/builtin/config.c b/builtin/config.c
index eec57fbd17..c53b68887e 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -19,6 +19,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	NULL
 };
 
@@ -37,6 +38,11 @@ static const char *const builtin_config_set_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_unset_usage[] = {
+	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -903,10 +909,42 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix)
 	return ret;
 }
 
+static int cmd_config_unset(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL;
+	int flags = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_GROUP(N_("Filter")),
+		OPT_BIT(0, "all", &flags, N_("replace multi-valued config option with new value"), CONFIG_FLAGS_MULTI_REPLACE),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_unset_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 1, 1);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with 'value-pattern'"));
+
+	handle_config_location(prefix);
+
+	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)
+		return git_config_set_multivar_in_file_gently(given_config_source.file,
+							      argv[0], NULL, value_pattern,
+							      flags);
+	else
+		return git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
+	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index f435b8f194..c9610a1f5b 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -22,6 +22,8 @@ legacy)
 	mode_get_regexp="--get-regexp"
 	mode_set=""
 	mode_replace_all="--replace-all"
+	mode_unset="--unset"
+	mode_unset_all="--unset-all"
 	;;
 subcommands)
 	mode_prefix=""
@@ -30,6 +32,8 @@ subcommands)
 	mode_get_regexp="get --regexp --all --show-names"
 	mode_set="set"
 	mode_replace_all="set --all"
+	mode_unset="unset"
+	mode_unset_all="unset --all"
 	;;
 *)
 	BUG "unknown mode $mode";;
@@ -149,7 +153,7 @@ foo = bar
 EOF
 
 test_expect_success 'unset with cont. lines' '
-	git config --unset beta.baz
+	git config ${mode_unset} beta.baz
 '
 
 cat > expect <<\EOF
@@ -176,7 +180,7 @@ EOF
 cp .git/config .git/config2
 
 test_expect_success 'multiple unset' '
-	git config --unset-all beta.haha
+	git config ${mode_unset_all} beta.haha
 '
 
 cat > expect << EOF
@@ -262,7 +266,7 @@ noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'unset' '
-	git config --unset beta.haha &&
+	git config ${mode_unset} beta.haha &&
 	test_cmp expect .git/config
 '
 
@@ -318,11 +322,11 @@ test_expect_success 'multivar replace' '
 '
 
 test_expect_success 'ambiguous unset' '
-	test_must_fail git config --unset nextsection.nonewline
+	test_must_fail git config ${mode_unset} nextsection.nonewline
 '
 
 test_expect_success 'invalid unset' '
-	test_must_fail git config --unset somesection.nonewline
+	test_must_fail git config ${mode_unset} somesection.nonewline
 '
 
 cat > expect << EOF
@@ -336,7 +340,12 @@ noIndent= sillyValue ; 'nother silly comment
 EOF
 
 test_expect_success 'multivar unset' '
-	git config --unset nextsection.nonewline "wow3$" &&
+	case "$mode" in
+	legacy)
+		git config --unset nextsection.nonewline "wow3$";;
+	subcommands)
+		git config unset --value="wow3$" nextsection.nonewline;;
+	esac &&
 	test_cmp expect .git/config
 '
 
@@ -1887,7 +1896,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	# please be careful when you update the above variable
 	EOF
 
-	git config --unset section.key &&
+	git config ${mode_unset} section.key &&
 	test_cmp expect .git/config &&
 
 	cat >.git/config <<-\EOF &&
@@ -1900,7 +1909,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[next-section]
 	EOF
 
-	git config --unset section.key &&
+	git config ${mode_unset} section.key &&
 	test_cmp expect .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1910,7 +1919,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[two]
 	key = true
 	EOF
-	git config --unset two.key &&
+	git config ${mode_unset} two.key &&
 	! grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1920,7 +1929,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[one]
 	key = true
 	EOF
-	git config --unset-all one.key &&
+	git config ${mode_unset_all} one.key &&
 	test_line_count = 0 .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1930,7 +1939,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[two]
 	Qkey = true
 	EOF
-	git config --unset two.key &&
+	git config ${mode_unset} two.key &&
 	grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -1942,7 +1951,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[TWO "subsection"]
 	[one]
 	EOF
-	git config --unset two.subsection.key &&
+	git config ${mode_unset} two.subsection.key &&
 	test "not [two subsection]" = "$(git config ${mode_get} one.key)" &&
 	test_line_count = 3 .git/config
 '
@@ -1954,7 +1963,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' '
 	key = value2
 	EOF
 
-	git config --unset-all section.key &&
+	git config ${mode_unset_all} section.key &&
 	test_line_count = 0 .git/config
 '
 
@@ -2478,8 +2487,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
-	test_must_fail git config --file=config --fixed-value --unset dev.null &&
-	test_must_fail git config --file=config --fixed-value --unset-all dev.null
+	test_must_fail git config ${mode_unset} --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_unset_all} --file=config --fixed-value dev.null
 '
 
 test_expect_success '--fixed-value uses exact string matching' '
@@ -2509,6 +2518,11 @@ test_expect_success '--fixed-value uses exact string matching' '
 	git config --file=config --fixed-value --unset fixed.test "$META" &&
 	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
+	cp initial config &&
+	test_must_fail git config unset --file=config --value="$META" fixed.test &&
+	git config unset --file=config --fixed-value --value="$META" fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
+
 	cp initial config &&
 	test_must_fail git config --file=config --unset-all fixed.test "$META" &&
 	git config --file=config --fixed-value --unset-all fixed.test "$META" &&
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 10/13] builtin/config: introduce "rename-section" subcommand
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (8 preceding siblings ...)
  2024-03-27  8:46   ` [PATCH v3 09/13] builtin/config: introduce "unset" subcommand Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 11/13] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
                     ` (3 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 8502 bytes --]

Introduce a new "rename-section" subcommand to git-config(1). Please
refer to preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 11 +++++++----
 builtin/config.c             | 32 ++++++++++++++++++++++++++++++++
 t/t1300-config.sh            | 22 +++++++++++-----------
 3 files changed, 50 insertions(+), 15 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 4fbc73a19e..0695eeae22 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -13,7 +13,7 @@ SYNOPSIS
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
-'git config' [<file-option>] --rename-section <old-name> <new-name>
+'git config rename-section' [<file-option>] <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
@@ -92,6 +92,9 @@ unset::
 	multi-valued config options, whereas `--value` will unset all config
 	options whose values match the given pattern.
 
+rename-section::
+	Rename the given section to a new name.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -180,9 +183,6 @@ See also <<FILES>>.
 --remove-section::
 	Remove the given section from the configuration file.
 
---rename-section::
-	Rename the given section to a new name.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -318,6 +318,9 @@ recommended to migrate to the new syntax.
 --unset-all <name> [<value-pattern>]::
 	Replaced by `git config unset [--value=<pattern>] --all <name>`.
 
+--rename-section <old-name> <new-name>::
+	Replaced by `git config rename-section <old-name> <new-name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index c53b68887e..99d9ac6341 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -20,6 +20,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	NULL
 };
 
@@ -43,6 +44,11 @@ static const char *const builtin_config_unset_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_rename_section_usage[] = {
+	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -940,11 +946,37 @@ static int cmd_config_unset(int argc, const char **argv, const char *prefix)
 		return git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);
 }
 
+static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_rename_section_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 2, 2);
+
+	handle_config_location(prefix);
+
+	ret = git_config_rename_section_in_file(given_config_source.file,
+						argv[0], argv[1]);
+	if (ret < 0)
+		return ret;
+	else if (!ret)
+		die(_("no such section: %s"), argv[0]);
+
+	return 0;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
+	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index c9610a1f5b..4818c495bd 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -589,7 +589,7 @@ weird
 EOF
 
 test_expect_success 'rename section' '
-	git config --rename-section branch.eins branch.zwei
+	git config ${mode_prefix}rename-section branch.eins branch.zwei
 '
 
 cat > expect << EOF
@@ -608,7 +608,7 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'rename non-existing section' '
-	test_must_fail git config --rename-section \
+	test_must_fail git config ${mode_prefix}rename-section \
 		branch."world domination" branch.drei
 '
 
@@ -617,7 +617,7 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'rename another section' '
-	git config --rename-section branch."1 234 blabl/a" branch.drei
+	git config ${mode_prefix}rename-section branch."1 234 blabl/a" branch.drei
 '
 
 cat > expect << EOF
@@ -640,7 +640,7 @@ cat >> .git/config << EOF
 EOF
 
 test_expect_success 'rename a section with a var on the same line' '
-	git config --rename-section branch.vier branch.zwei
+	git config ${mode_prefix}rename-section branch.vier branch.zwei
 '
 
 cat > expect << EOF
@@ -661,11 +661,11 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'renaming empty section name is rejected' '
-	test_must_fail git config --rename-section branch.zwei ""
+	test_must_fail git config ${mode_prefix}rename-section branch.zwei ""
 '
 
 test_expect_success 'renaming to bogus section is rejected' '
-	test_must_fail git config --rename-section branch.zwei "bogus name"
+	test_must_fail git config ${mode_prefix}rename-section branch.zwei "bogus name"
 '
 
 test_expect_success 'renaming a section with a long line' '
@@ -674,7 +674,7 @@ test_expect_success 'renaming a section with a long line' '
 		printf "  c = d %1024s [a] e = f\\n" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	git config -f y --rename-section a xyz &&
+	git config ${mode_prefix}rename-section -f y a xyz &&
 	test_must_fail git config -f y b.e
 '
 
@@ -684,7 +684,7 @@ test_expect_success 'renaming an embedded section with a long line' '
 		printf "  c = d %1024s [a] [foo] e = f\\n" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	git config -f y --rename-section a xyz &&
+	git config ${mode_prefix}rename-section -f y a xyz &&
 	test_must_fail git config -f y foo.e
 '
 
@@ -694,7 +694,7 @@ test_expect_success 'renaming a section with an overly-long line' '
 		printf "  c = d %525000s e" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	test_must_fail git config -f y --rename-section a xyz 2>err &&
+	test_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&
 	grep "refusing to work with overly long line in .y. on line 2" err
 '
 
@@ -1986,7 +1986,7 @@ test_expect_success POSIXPERM,PERL 'preserves existing permissions' '
 	git config imap.pass Hunter2 &&
 	perl -e \
 	  "die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600" &&
-	git config --rename-section imap pop &&
+	git config ${mode_prefix}rename-section imap pop &&
 	perl -e \
 	  "die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600"
 '
@@ -2475,7 +2475,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --add dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --rename-section dev null &&
+	test_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&
 	test_must_fail git config --file=config --fixed-value --remove-section dev &&
 	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 11/13] builtin/config: introduce "remove-section" subcommand
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (9 preceding siblings ...)
  2024-03-27  8:46   ` [PATCH v3 10/13] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:46   ` [PATCH v3 12/13] builtin/config: introduce "edit" subcommand Patrick Steinhardt
                     ` (2 subsequent siblings)
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 5329 bytes --]

Introduce a new "remove-section" subcommand to git-config(1). Please
refer to preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 11 +++++++----
 builtin/config.c             | 32 ++++++++++++++++++++++++++++++++
 t/t1300-config.sh            |  4 ++--
 3 files changed, 41 insertions(+), 6 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 0695eeae22..b9ca5e83a2 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -14,7 +14,7 @@ SYNOPSIS
 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config rename-section' [<file-option>] <old-name> <new-name>
-'git config' [<file-option>] --remove-section <name>
+'git config remove-section' [<file-option>] <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
 
@@ -95,6 +95,9 @@ unset::
 rename-section::
 	Rename the given section to a new name.
 
+remove-section::
+	Remove the given section from the configuration file.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -180,9 +183,6 @@ See also <<FILES>>.
 	section in linkgit:gitrevisions[7] for a more complete list of
 	ways to spell blob names.
 
---remove-section::
-	Remove the given section from the configuration file.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -321,6 +321,9 @@ recommended to migrate to the new syntax.
 --rename-section <old-name> <new-name>::
 	Replaced by `git config rename-section <old-name> <new-name>`.
 
+--remove-section <name>::
+	Replaced by `git config remove-section <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index 99d9ac6341..6be6717f26 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -21,6 +21,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
+	N_("git config remove-section [<file-option>] <name>"),
 	NULL
 };
 
@@ -49,6 +50,11 @@ static const char *const builtin_config_rename_section_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_remove_section_usage[] = {
+	N_("git config remove-section [<file-option>] <name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -971,12 +977,38 @@ static int cmd_config_rename_section(int argc, const char **argv, const char *pr
 	return 0;
 }
 
+static int cmd_config_remove_section(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_remove_section_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 1, 1);
+
+	handle_config_location(prefix);
+
+	ret = git_config_rename_section_in_file(given_config_source.file,
+						argv[0], NULL);
+	if (ret < 0)
+		return ret;
+	else if (!ret)
+		die(_("no such section: %s"), argv[0]);
+
+	return 0;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
+	OPT_SUBCOMMAND("remove-section", &subcommand, cmd_config_remove_section),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 4818c495bd..2f8d0310b1 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -703,7 +703,7 @@ cat >> .git/config << EOF
 EOF
 
 test_expect_success 'remove section' '
-	git config --remove-section branch.zwei
+	git config ${mode_prefix}remove-section branch.zwei
 '
 
 cat > expect << EOF
@@ -2476,7 +2476,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&
-	test_must_fail git config --file=config --fixed-value --remove-section dev &&
+	test_must_fail git config ${mode_prefix}remove-section --file=config --fixed-value dev &&
 	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 12/13] builtin/config: introduce "edit" subcommand
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (10 preceding siblings ...)
  2024-03-27  8:46   ` [PATCH v3 11/13] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
@ 2024-03-27  8:46   ` Patrick Steinhardt
  2024-03-27  8:47   ` [PATCH v3 13/13] builtin/config: display subcommand help Patrick Steinhardt
  2024-03-27  8:53   ` [PATCH v3 00/13] builtin/config: introduce subcommands Eric Sunshine
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:46 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 7247 bytes --]

Introduce a new "edit" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 17 ++++----
 builtin/config.c             | 81 ++++++++++++++++++++++++------------
 t/t1300-config.sh            |  6 +--
 3 files changed, 68 insertions(+), 36 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index b9ca5e83a2..448bf43bb3 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -15,8 +15,8 @@ SYNOPSIS
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config rename-section' [<file-option>] <old-name> <new-name>
 'git config remove-section' [<file-option>] <name>
+'git config edit' [<file-option>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
-'git config' [<file-option>] -e | --edit
 
 DESCRIPTION
 -----------
@@ -98,6 +98,11 @@ rename-section::
 remove-section::
 	Remove the given section from the configuration file.
 
+edit::
+	Opens an editor to modify the specified config file; either
+	`--system`, `--global`, `--local` (default), `--worktree`, or
+	`--file <config-file>`.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -262,12 +267,6 @@ Valid `<type>`'s include:
 	When the color setting for `name` is undefined, the command uses
 	`color.ui` as fallback.
 
--e::
---edit::
-	Opens an editor to modify the specified config file; either
-	`--system`, `--global`, `--local` (default), `--worktree`, or
-	`--file <config-file>`.
-
 --[no-]includes::
 	Respect `include.*` directives in config files when looking up
 	values. Defaults to `off` when a specific file is given (e.g.,
@@ -324,6 +323,10 @@ recommended to migrate to the new syntax.
 --remove-section <name>::
 	Replaced by `git config remove-section <name>`.
 
+-e::
+--edit::
+	Replaced by `git config edit`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index 6be6717f26..b8cb940b1e 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -22,6 +22,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	N_("git config remove-section [<file-option>] <name>"),
+	N_("git config edit [<file-option>]"),
 	NULL
 };
 
@@ -55,6 +56,11 @@ static const char *const builtin_config_remove_section_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_edit_usage[] = {
+	N_("git config edit [<file-option>]"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -1002,6 +1008,53 @@ static int cmd_config_remove_section(int argc, const char **argv, const char *pr
 	return 0;
 }
 
+static int show_editor(void)
+{
+	char *config_file;
+
+	if (!given_config_source.file && !startup_info->have_repository)
+		die(_("not in a git directory"));
+	if (given_config_source.use_stdin)
+		die(_("editing stdin is not supported"));
+	if (given_config_source.blob)
+		die(_("editing blobs is not supported"));
+	git_config(git_default_config, NULL);
+	config_file = given_config_source.file ?
+			xstrdup(given_config_source.file) :
+			git_pathdup("config");
+	if (use_global_config) {
+		int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);
+		if (fd >= 0) {
+			char *content = default_user_config();
+			write_str_in_full(fd, content);
+			free(content);
+			close(fd);
+		}
+		else if (errno != EEXIST)
+			die_errno(_("cannot create configuration file %s"), config_file);
+	}
+	launch_editor(config_file, NULL, NULL);
+	free(config_file);
+
+	return 0;
+}
+
+static int cmd_config_edit(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_edit_usage, 0);
+	check_write();
+	check_argc(argc, 0, 0);
+
+	handle_config_location(prefix);
+
+	return show_editor();
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
@@ -1009,6 +1062,7 @@ static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
 	OPT_SUBCOMMAND("remove-section", &subcommand, cmd_config_remove_section),
+	OPT_SUBCOMMAND("edit", &subcommand, cmd_config_edit),
 	OPT_END(),
 };
 
@@ -1127,32 +1181,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		}
 	}
 	else if (actions == ACTION_EDIT) {
-		char *config_file;
-
-		check_argc(argc, 0, 0);
-		if (!given_config_source.file && !startup_info->have_repository)
-			die(_("not in a git directory"));
-		if (given_config_source.use_stdin)
-			die(_("editing stdin is not supported"));
-		if (given_config_source.blob)
-			die(_("editing blobs is not supported"));
-		git_config(git_default_config, NULL);
-		config_file = given_config_source.file ?
-				xstrdup(given_config_source.file) :
-				git_pathdup("config");
-		if (use_global_config) {
-			int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);
-			if (fd >= 0) {
-				char *content = default_user_config();
-				write_str_in_full(fd, content);
-				free(content);
-				close(fd);
-			}
-			else if (errno != EEXIST)
-				die_errno(_("cannot create configuration file %s"), config_file);
-		}
-		launch_editor(config_file, NULL, NULL);
-		free(config_file);
+		ret = show_editor();
 	}
 	else if (actions == ACTION_SET) {
 		check_write();
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 2f8d0310b1..3049b61533 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -556,7 +556,7 @@ test_expect_success 'setting a value in stdin is an error' '
 '
 
 test_expect_success 'editing stdin is an error' '
-	test_must_fail git config --file - --edit
+	test_must_fail git config ${mode_prefix}edit --file -
 '
 
 test_expect_success 'refer config from subdirectory' '
@@ -1642,7 +1642,7 @@ test_expect_success 'command line overrides environment config' '
 test_expect_success 'git config --edit works' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
-	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
+	GIT_EDITOR="echo [test]value=yes >" git config ${mode_prefix}edit -f tmp &&
 	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
@@ -1651,7 +1651,7 @@ test_expect_success 'git config --edit respects core.editor' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
 	test_config core.editor "echo [test]value=yes >" &&
-	git config -f tmp --edit &&
+	git config ${mode_prefix}edit -f tmp &&
 	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v3 13/13] builtin/config: display subcommand help
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (11 preceding siblings ...)
  2024-03-27  8:46   ` [PATCH v3 12/13] builtin/config: introduce "edit" subcommand Patrick Steinhardt
@ 2024-03-27  8:47   ` Patrick Steinhardt
  2024-03-27  8:53   ` [PATCH v3 00/13] builtin/config: introduce subcommands Eric Sunshine
  13 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  8:47 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 2172 bytes --]

Until now, `git config -h` would have printed help for the old-style
syntax. Now that all modes have proper subcommands though it is
preferable to instead display the subcommand help.

Drop the `NO_INTERNAL_HELP` flag to do so. While at it, drop the help
mismatch in t0450 and add the `--get-colorbool` option to the usage such
that git-config(1)'s synopsis and `git config -h` match.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c            | 5 +++--
 t/t0450/txt-help-mismatches | 1 -
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index b8cb940b1e..18b1f7014a 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -23,6 +23,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	N_("git config remove-section [<file-option>] <name>"),
 	N_("git config edit [<file-option>]"),
+	N_("git config [<file-option>] --get-colorbool <name> [<stdout-is-tty>]"),
 	NULL
 };
 
@@ -1084,10 +1085,10 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 	 * us when parsing the legacy-style modes that don't use subcommands.
 	 */
 	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
-			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
 	if (subcommand) {
 		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
-		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);
+		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);
 		return subcommand(argc, argv, prefix);
 	}
 
diff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches
index a0777acd66..28003f18c9 100644
--- a/t/t0450/txt-help-mismatches
+++ b/t/t0450/txt-help-mismatches
@@ -10,7 +10,6 @@ checkout
 checkout-index
 clone
 column
-config
 credential
 credential-cache
 credential-store
-- 
2.44.GIT


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v3 00/13] builtin/config: introduce subcommands
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
                     ` (12 preceding siblings ...)
  2024-03-27  8:47   ` [PATCH v3 13/13] builtin/config: display subcommand help Patrick Steinhardt
@ 2024-03-27  8:53   ` Eric Sunshine
  2024-03-27  9:16     ` Patrick Steinhardt
  13 siblings, 1 reply; 113+ messages in thread
From: Eric Sunshine @ 2024-03-27  8:53 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Junio C Hamano

On Wed, Mar 27, 2024 at 4:46 AM Patrick Steinhardt <ps@pks.im> wrote:
> this is the third version of my patch series that aims to add
> subcommands to git-config(1) in a backwards-compatible manner.
>
> There's only been a few changes compared to v2 based on Eric's comments:
>
>  7:  8ceced0fc5 !  7:  eee1fae50c builtin/config: introduce "get" subcommand
>     -+  With "get", Return all values for a multi-valued key.
>     ++  With `get`, Return all values for a multi-valued key.

Better. This fixes the typesetting problem but misses the
s/Return/return/ mentioned earlier[1].

[1]: https://lore.kernel.org/git/CAPig+cRwh4HCi=Q01tGJ0WOb59iE18HWSCNxGogcreOz+2w1WA@mail.gmail.com/

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

* Re: [PATCH v3 00/13] builtin/config: introduce subcommands
  2024-03-27  8:53   ` [PATCH v3 00/13] builtin/config: introduce subcommands Eric Sunshine
@ 2024-03-27  9:16     ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-03-27  9:16 UTC (permalink / raw)
  To: Eric Sunshine
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 920 bytes --]

On Wed, Mar 27, 2024 at 04:53:35AM -0400, Eric Sunshine wrote:
> On Wed, Mar 27, 2024 at 4:46 AM Patrick Steinhardt <ps@pks.im> wrote:
> > this is the third version of my patch series that aims to add
> > subcommands to git-config(1) in a backwards-compatible manner.
> >
> > There's only been a few changes compared to v2 based on Eric's comments:
> >
> >  7:  8ceced0fc5 !  7:  eee1fae50c builtin/config: introduce "get" subcommand
> >     -+  With "get", Return all values for a multi-valued key.
> >     ++  With `get`, Return all values for a multi-valued key.
> 
> Better. This fixes the typesetting problem but misses the
> s/Return/return/ mentioned earlier[1].
> 
> [1]: https://lore.kernel.org/git/CAPig+cRwh4HCi=Q01tGJ0WOb59iE18HWSCNxGogcreOz+2w1WA@mail.gmail.com/

Ah, sorry, I missed that one. Fixed locally, will send out with the
inevitable v4 of this patch series. Thanks!

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 00/14] builtin/config: introduce subcommands
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (11 preceding siblings ...)
  2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
@ 2024-05-03  9:56 ` Patrick Steinhardt
  2024-05-03  9:56   ` [PATCH v4 01/14] config: clarify memory ownership when preparing comment strings Patrick Steinhardt
                     ` (15 more replies)
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
  13 siblings, 16 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 23916 bytes --]

Hi,

this is the fourth version of my patch series that introduces
subcommands for git-config(1).

Changes compared to v3:

    - Rebased on top of d4cc1ec35f (Start the 2.46 cycle, 2024-04-30).

    - Implemented support for `git config set --comment`. This switch
      has been added since the last version of this patch series.

Here's hoping that there's a bit more interest in this patch series at
the beginning of the release cycle :)

Patrick

Patrick Steinhardt (14):
  config: clarify memory ownership when preparing comment strings
  builtin/config: move option array around
  builtin/config: move "fixed-value" option to correct group
  builtin/config: use `OPT_CMDMODE()` to specify modes
  builtin/config: pull out function to handle config location
  builtin/config: pull out function to handle `--null`
  builtin/config: introduce "list" subcommand
  builtin/config: introduce "get" subcommand
  builtin/config: introduce "set" subcommand
  builtin/config: introduce "unset" subcommand
  builtin/config: introduce "rename-section" subcommand
  builtin/config: introduce "remove-section" subcommand
  builtin/config: introduce "edit" subcommand
  builtin/config: display subcommand help

 Documentation/git-config.txt | 219 ++++++++-------
 builtin/config.c             | 512 ++++++++++++++++++++++++++++-------
 config.c                     |  16 +-
 config.h                     |   2 +-
 t/t0450/txt-help-mismatches  |   1 -
 t/t1300-config.sh            | 432 +++++++++++++++++------------
 6 files changed, 812 insertions(+), 370 deletions(-)

Range-diff against v3:
 -:  ---------- >  1:  3aa26d5bff config: clarify memory ownership when preparing comment strings
 1:  bfcb50e393 !  2:  8f0804ab48 builtin/config: move option array around
    @@ builtin/config.c: static int option_parse_type(const struct option *opt, const c
     -	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
     -	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
     -	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
    +-	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
     -	OPT_END(),
     -};
     -
    @@ builtin/config.c: static char *default_user_config(void)
     +	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
     +	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
     +	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
    ++	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
     +	OPT_END(),
     +};
     +
 2:  ff428d8a22 !  3:  ddcd8031d7 builtin/config: move "fixed-value" option to correct group
    @@ builtin/config.c: static struct option builtin_config_options[] = {
      	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
      	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
     @@ builtin/config.c: static struct option builtin_config_options[] = {
    - 	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
      	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
      	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
    + 	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
     +	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
      	OPT_END(),
      };
 3:  e049c05713 =  4:  1bc3918840 builtin/config: use `OPT_CMDMODE()` to specify modes
 4:  41585803bf !  5:  3754812309 builtin/config: pull out function to handle config location
    @@ builtin/config.c: static char *default_user_config(void)
     -	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
     -	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
     -	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
    +-	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
     -	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
     -	OPT_END(),
     -};
     -
     -static NORETURN void usage_builtin_config(void)
    --{
    ++static void handle_config_location(const char *prefix)
    + {
     -	usage_with_options(builtin_config_usage, builtin_config_options);
     -}
     -
     -int cmd_config(int argc, const char **argv, const char *prefix)
    -+static void handle_config_location(const char *prefix)
    - {
    +-{
     -	int nongit = !startup_info->have_repository;
    --	char *value = NULL;
    +-	char *value = NULL, *comment = NULL;
     -	int flags = 0;
     -	int ret = 0;
     -	struct key_value_info default_kvi = KVI_INIT;
    @@ builtin/config.c: int cmd_config(int argc, const char **argv, const char *prefix
     +	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
     +	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
     +	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
    ++	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
     +	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
     +	OPT_END(),
     +};
    @@ builtin/config.c: int cmd_config(int argc, const char **argv, const char *prefix
     +
     +int cmd_config(int argc, const char **argv, const char *prefix)
     +{
    -+	char *value = NULL;
    ++	char *value = NULL, *comment = NULL;
     +	int flags = 0;
     +	int ret = 0;
     +	struct key_value_info default_kvi = KVI_INIT;
 5:  95f661f267 =  6:  cb1714c493 builtin/config: pull out function to handle `--null`
 6:  b50f32d074 !  7:  b3f3c3ba6a builtin/config: introduce "list" subcommand
    @@ Documentation/git-config.txt: git-config - Get and set repository or global opti
      --------
      [verse]
     +'git config list' [<file-option>] [<display-option>] [--includes]
    - 'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
    - 'git config' [<file-option>] [--type=<type>] --add <name> <value>
    - 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
     @@ Documentation/git-config.txt: SYNOPSIS
      'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
      'git config' [<file-option>] --rename-section <old-name> <new-name>
    @@ builtin/config.c: static struct option builtin_config_options[] = {
     -	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
     -	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
      	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
    + 	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
      	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
     +	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
      	OPT_END(),
    @@ builtin/config.c: static NORETURN void usage_builtin_config(void)
     +
      int cmd_config(int argc, const char **argv, const char *prefix)
      {
    - 	char *value = NULL;
    + 	char *value = NULL, *comment = NULL;
     @@ builtin/config.c: int cmd_config(int argc, const char **argv, const char *prefix)
      
      	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
    @@ t/t1300-config.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
     +	BUG "unknown mode $mode";;
     +esac
     +
    - test_expect_success 'clear default config' '
    - 	rm -f .git/config
    - '
    + test_expect_success 'setup whitespace config' '
    + 	sed -e "s/^|//" \
    + 	    -e "s/[$]$//" \
     @@ t/t1300-config.sh: version.1.2.3eX.alpha=beta
      EOF
      
    @@ t/t1300-config.sh: Qsection.sub=section.val4
      	nul_to_q <result.raw >result &&
      	echo >>result &&
      	test_cmp expect result
    -@@ t/t1300-config.sh: test_expect_success 'inner whitespace kept verbatim' '
    +@@ t/t1300-config.sh: test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces'
      '
      
      test_expect_success SYMLINKS 'symlinked configuration' '
 7:  eee1fae50c !  8:  0e6da909ac builtin/config: introduce "get" subcommand
    @@ Documentation/git-config.txt: SYNOPSIS
      [verse]
      'git config list' [<file-option>] [<display-option>] [--includes]
     +'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
    - 'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
    - 'git config' [<file-option>] [--type=<type>] --add <name> <value>
    - 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
     -'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]
     -'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]
     -'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]
    @@ Documentation/git-config.txt: COMMANDS
      OPTIONS
      -------
     @@ Documentation/git-config.txt: OPTIONS
    - 	values.  This is the same as providing '^$' as the `value-pattern`
    - 	in `--replace-all`.
    + 	not contain linefeed characters (no multi-line comments are
    + 	permitted).
      
     ---get::
     -	Get the value for a given key (optionally filtered by a regex
    @@ Documentation/git-config.txt: OPTIONS
     ---get-all::
     -	Like get, but returns all values for a multi-valued key.
     +--all::
    -+	With `get`, Return all values for a multi-valued key.
    ++	With `get`, return all values for a multi-valued key.
      
     ---get-regexp::
     -	Like --get-all, but interprets the name as a regular expression and
    @@ t/t1300-config.sh: test_expect_success '--null --list' '
      	nul_to_q <result.raw >result &&
      	echo >>result &&
      	test_cmp expect result
    +@@ t/t1300-config.sh: test_expect_success '--null --get-regexp' '
    + test_expect_success 'inner whitespace kept verbatim, spaces only' '
    + 	echo "foo   bar" >expect &&
    + 	git config section.val "foo   bar" &&
    +-	git config --get section.val >actual &&
    ++	git config ${mode_get} section.val >actual &&
    + 	test_cmp expect actual
    + '
    + 
    + test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '
    + 	echo "fooQQbar" | q_to_tab >expect &&
    + 	git config section.val "$(cat expect)" &&
    +-	git config --get section.val >actual &&
    ++	git config ${mode_get} section.val >actual &&
    + 	test_cmp expect actual
    + '
    + 
    + test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '
    + 	echo "foo Q  bar" | q_to_tab >expect &&
    + 	git config section.val "$(cat expect)" &&
    +-	git config --get section.val >actual &&
    ++	git config ${mode_get} section.val >actual &&
    + 	test_cmp expect actual
    + '
    + 
     @@ t/t1300-config.sh: test_expect_success 'git -c can represent empty string' '
      '
      
 8:  e2815affab !  9:  8a623a31b9 builtin/config: introduce "set" subcommand
    @@ Documentation/git-config.txt: SYNOPSIS
      [verse]
      'git config list' [<file-option>] [<display-option>] [--includes]
      'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
    --'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
    --'git config' [<file-option>] [--type=<type>] --add <name> <value>
    --'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
    -+'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
    +-'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
    +-'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
    +-'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
    ++'git config set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>
      'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
      'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
      'git config' [<file-option>] --rename-section <old-name> <new-name>
    @@ Documentation/git-config.txt: OPTIONS
     -	in `--replace-all`.
     +	values. This is the same as providing '--value=^$' in `set`.
      
    - --all::
    - 	With `get`, Return all values for a multi-valued key.
    + --comment <message>::
    + 	Append a comment at the end of new or modified lines.
     @@ Documentation/git-config.txt: recommended to migrate to the new syntax.
      'git config <name>'::
      	Replaced by `git config get <name>`.
    @@ builtin/config.c: static const char *const builtin_config_get_usage[] = {
      };
      
     +static const char *const builtin_config_set_usage[] = {
    -+	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
    ++	N_("git config set [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
     +	NULL
     +};
     +
    @@ builtin/config.c: static int cmd_config_get(int argc, const char **argv, const c
      
     +static int cmd_config_set(int argc, const char **argv, const char *prefix)
     +{
    -+	const char *value_pattern = NULL;
    ++	const char *value_pattern = NULL, *comment_arg = NULL;
    ++	char *comment = NULL;
     +	int flags = 0, append = 0;
     +	struct option opts[] = {
     +		CONFIG_LOCATION_OPTIONS,
    @@ builtin/config.c: static int cmd_config_get(int argc, const char **argv, const c
     +		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
     +		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
     +		OPT_GROUP(N_("Other")),
    ++		OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
     +		OPT_BOOL(0, "append", &append, N_("add a new line without altering any existing values")),
     +		OPT_END(),
     +	};
    @@ builtin/config.c: static int cmd_config_get(int argc, const char **argv, const c
     +	if (append)
     +		value_pattern = CONFIG_REGEX_NONE;
     +
    ++	comment = git_config_prepare_comment_string(comment_arg);
    ++
     +	handle_config_location(prefix);
     +
     +	value = normalize_value(argv[0], argv[1], &default_kvi);
    @@ builtin/config.c: static int cmd_config_get(int argc, const char **argv, const c
     +	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {
     +		ret = git_config_set_multivar_in_file_gently(given_config_source.file,
     +							     argv[0], value, value_pattern,
    -+							     flags);
    ++							     comment, flags);
     +	} else {
    -+		ret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);
    ++		ret = git_config_set_in_file_gently(given_config_source.file,
    ++						    argv[0], comment, value);
     +		if (ret == CONFIG_NOTHING_SET)
     +			error(_("cannot overwrite multiple values with a single value\n"
     +			"       Use a regexp, --add or --replace-all to change %s."), argv[0]);
     +	}
     +
    ++	free(comment);
     +	free(value);
     +	return ret;
     +}
    @@ t/t1300-config.sh: cat > expect << EOF
      	test_cmp expect .git/config
      '
      
    +@@ t/t1300-config.sh: EOF
    + 
    + test_expect_success 'append comments' '
    + 	git config --replace-all --comment="Pygoscelis papua" section.penguin gentoo &&
    +-	git config --comment="find fish" section.disposition peckish &&
    +-	git config --comment="#abc" section.foo bar &&
    ++	git config ${mode_set} --comment="find fish" section.disposition peckish &&
    ++	git config ${mode_set} --comment="#abc" section.foo bar &&
    + 
    + 	git config --comment="and comment" section.spsp value &&
    + 	git config --comment="	# and comment" section.htsp value &&
    +@@ t/t1300-config.sh: test_expect_success 'append comments' '
    + '
    + 
    + test_expect_success 'Prohibited LF in comment' '
    +-	test_must_fail git config --comment="a${LF}b" section.k v
    ++	test_must_fail git config ${mode_set} --comment="a${LF}b" section.k v
    + '
    + 
    + test_expect_success 'non-match result' 'test_cmp expect .git/config'
     @@ t/t1300-config.sh: test_expect_success 'multiple unset is correct' '
      cp .git/config2 .git/config
      
    @@ t/t1300-config.sh: test_expect_success 'key with newline' '
      
      cat > .git/config <<\EOF
     @@ t/t1300-config.sh: test_expect_success '--null --get-regexp' '
    + 
    + test_expect_success 'inner whitespace kept verbatim, spaces only' '
    + 	echo "foo   bar" >expect &&
    +-	git config section.val "foo   bar" &&
    ++	git config ${mode_set} section.val "foo   bar" &&
    + 	git config ${mode_get} section.val >actual &&
    + 	test_cmp expect actual
      '
      
    - test_expect_success 'inner whitespace kept verbatim' '
    --	git config section.val "foo 	  bar" &&
    -+	git config ${mode_set} section.val "foo 	  bar" &&
    - 	test_cmp_config "foo 	  bar" section.val
    + test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '
    + 	echo "fooQQbar" | q_to_tab >expect &&
    +-	git config section.val "$(cat expect)" &&
    ++	git config ${mode_set} section.val "$(cat expect)" &&
    + 	git config ${mode_get} section.val >actual &&
    + 	test_cmp expect actual
      '
      
    + test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '
    + 	echo "foo Q  bar" | q_to_tab >expect &&
    +-	git config section.val "$(cat expect)" &&
    ++	git config ${mode_set} section.val "$(cat expect)" &&
    + 	git config ${mode_get} section.val >actual &&
    + 	test_cmp expect actual
    + '
     @@ t/t1300-config.sh: test_expect_success 'check split_cmdline return' '
      	git init repo &&
      	(
 9:  90f055ae1d ! 10:  e25e5b69cd builtin/config: introduce "unset" subcommand
    @@ Commit message
     
      ## Documentation/git-config.txt ##
     @@ Documentation/git-config.txt: SYNOPSIS
    + [verse]
      'git config list' [<file-option>] [<display-option>] [--includes]
      'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
    - 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
    +-'git config set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>
     -'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
     -'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
    ++'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
     +'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
      'git config' [<file-option>] --rename-section <old-name> <new-name>
      'git config' [<file-option>] --remove-section <name>
    @@ builtin/config.c: static int cmd_config_set(int argc, const char **argv, const c
     +	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)
     +		return git_config_set_multivar_in_file_gently(given_config_source.file,
     +							      argv[0], NULL, value_pattern,
    -+							      flags);
    ++							      NULL, flags);
     +	else
    -+		return git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);
    ++		return git_config_set_in_file_gently(given_config_source.file, argv[0],
    ++						     NULL, NULL);
     +}
     +
      static struct option builtin_subcommand_options[] = {
10:  3e360b1f47 ! 11:  f24008d356 builtin/config: introduce "rename-section" subcommand
    @@ builtin/config.c: static const char *const builtin_config_unset_usage[] = {
      static regex_t *key_regexp;
      static const char *value_pattern;
     @@ builtin/config.c: static int cmd_config_unset(int argc, const char **argv, const char *prefix)
    - 		return git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);
    + 						     NULL, NULL);
      }
      
     +static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)
11:  d610b5fda1 = 12:  fc2ddd3201 builtin/config: introduce "remove-section" subcommand
12:  4a6512c88a = 13:  4c2d817eff builtin/config: introduce "edit" subcommand
13:  657d1355b5 = 14:  4c351b12b8 builtin/config: display subcommand help
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 01/14] config: clarify memory ownership when preparing comment strings
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
@ 2024-05-03  9:56   ` Patrick Steinhardt
  2024-05-03 10:13     ` Kristoffer Haugsbakk
  2024-05-03  9:56   ` [PATCH v4 02/14] builtin/config: move option array around Patrick Steinhardt
                     ` (14 subsequent siblings)
  15 siblings, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 4815 bytes --]

The ownership of memory returned when preparing a comment string is
quite intricate: when the returned value is different than the passed
value, then the caller is responsible to free the memory. This is quite
subtle, and it's even easier to miss because the returned value is in
fact a `const char *`.

Adapt the function to always return either `NULL` or a newly allocated
string. The function is called at most once per git-config(1), so it's
not like this micro-optimization really matters. Thus, callers are now
always responsible for freeing the value.
---
 builtin/config.c | 11 ++++++-----
 config.c         | 16 ++++++----------
 config.h         |  2 +-
 3 files changed, 13 insertions(+), 16 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 0015620dde..40456c0770 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -44,7 +44,7 @@ static struct config_options config_options;
 static int show_origin;
 static int show_scope;
 static int fixed_value;
-static const char *comment;
+static const char *comment_arg;
 
 #define ACTION_GET (1<<0)
 #define ACTION_GET_ALL (1<<1)
@@ -174,7 +174,7 @@ static struct option builtin_config_options[] = {
 	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
 	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_STRING(0, "comment", &comment, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
+	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
 	OPT_END(),
 };
 
@@ -674,7 +674,7 @@ static char *default_user_config(void)
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	int nongit = !startup_info->have_repository;
-	char *value = NULL;
+	char *value = NULL, *comment = NULL;
 	int flags = 0;
 	int ret = 0;
 	struct key_value_info default_kvi = KVI_INIT;
@@ -799,7 +799,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (comment &&
+	if (comment_arg &&
 	    !(actions & (ACTION_ADD|ACTION_SET|ACTION_SET_ALL|ACTION_REPLACE_ALL))) {
 		error(_("--comment is only applicable to add/set/replace operations"));
 		usage_builtin_config();
@@ -841,7 +841,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		flags |= CONFIG_FLAGS_FIXED_VALUE;
 	}
 
-	comment = git_config_prepare_comment_string(comment);
+	comment = git_config_prepare_comment_string(comment_arg);
 
 	if (actions & PAGING_ACTIONS)
 		setup_auto_pager("config", 1);
@@ -993,6 +993,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		return get_colorbool(argv[0], argc == 2);
 	}
 
+	free(comment);
 	free(value);
 	return ret;
 }
diff --git a/config.c b/config.c
index ae3652b08f..13cf9eeb16 100644
--- a/config.c
+++ b/config.c
@@ -3182,14 +3182,10 @@ void git_config_set(const char *key, const char *value)
 	trace2_cmd_set_config(key, value);
 }
 
-/*
- * The ownership rule is that the caller will own the string
- * if it receives a piece of memory different from what it passed
- * as the parameter.
- */
-const char *git_config_prepare_comment_string(const char *comment)
+char *git_config_prepare_comment_string(const char *comment)
 {
 	size_t leading_blanks;
+	char *prepared;
 
 	if (!comment)
 		return NULL;
@@ -3210,13 +3206,13 @@ const char *git_config_prepare_comment_string(const char *comment)
 
 	leading_blanks = strspn(comment, " \t");
 	if (leading_blanks && comment[leading_blanks] == '#')
-		; /* use it as-is */
+		prepared = xstrdup(comment); /* use it as-is */
 	else if (comment[0] == '#')
-		comment = xstrfmt(" %s", comment);
+		prepared = xstrfmt(" %s", comment);
 	else
-		comment = xstrfmt(" # %s", comment);
+		prepared = xstrfmt(" # %s", comment);
 
-	return comment;
+	return prepared;
 }
 
 static void validate_comment_string(const char *comment)
diff --git a/config.h b/config.h
index f4966e3749..db8b608064 100644
--- a/config.h
+++ b/config.h
@@ -338,7 +338,7 @@ void git_config_set_multivar(const char *, const char *, const char *, unsigned)
 int repo_config_set_multivar_gently(struct repository *, const char *, const char *, const char *, unsigned);
 int git_config_set_multivar_in_file_gently(const char *, const char *, const char *, const char *, const char *, unsigned);
 
-const char *git_config_prepare_comment_string(const char *);
+char *git_config_prepare_comment_string(const char *);
 
 /**
  * takes four parameters:
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 02/14] builtin/config: move option array around
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
  2024-05-03  9:56   ` [PATCH v4 01/14] config: clarify memory ownership when preparing comment strings Patrick Steinhardt
@ 2024-05-03  9:56   ` Patrick Steinhardt
  2024-05-03  9:56   ` [PATCH v4 03/14] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
                     ` (13 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 8316 bytes --]

Move around the option array. This will help us with a follow-up commit
that introduces subcommands to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 96 ++++++++++++++++++++++++------------------------
 1 file changed, 48 insertions(+), 48 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 40456c0770..59ae5996eb 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -135,54 +135,6 @@ static int option_parse_type(const struct option *opt, const char *arg,
 	return 0;
 }
 
-static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
-	OPT_GROUP(N_("Action")),
-	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
-	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
-	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
-	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
-	OPT_END(),
-};
-
-static NORETURN void usage_builtin_config(void)
-{
-	usage_with_options(builtin_config_usage, builtin_config_options);
-}
-
 static void check_argc(int argc, int min, int max)
 {
 	if (argc >= min && argc <= max)
@@ -671,6 +623,54 @@ static char *default_user_config(void)
 	return strbuf_detach(&buf, NULL);
 }
 
+static struct option builtin_config_options[] = {
+	OPT_GROUP(N_("Config file location")),
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	OPT_GROUP(N_("Action")),
+	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
+	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
+	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
+	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
+	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
+	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
+	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
+	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_GROUP(N_("Type")),
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	OPT_GROUP(N_("Other")),
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
+	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
+	OPT_END(),
+};
+
+static NORETURN void usage_builtin_config(void)
+{
+	usage_with_options(builtin_config_usage, builtin_config_options);
+}
+
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	int nongit = !startup_info->have_repository;
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 03/14] builtin/config: move "fixed-value" option to correct group
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
  2024-05-03  9:56   ` [PATCH v4 01/14] config: clarify memory ownership when preparing comment strings Patrick Steinhardt
  2024-05-03  9:56   ` [PATCH v4 02/14] builtin/config: move option array around Patrick Steinhardt
@ 2024-05-03  9:56   ` Patrick Steinhardt
  2024-05-03 12:28     ` Karthik Nayak
  2024-05-03  9:57   ` [PATCH v4 04/14] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
                     ` (12 subsequent siblings)
  15 siblings, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 1930 bytes --]

The `--fixed-value` option can be used to alter how the value-pattern
parameter is interpreted for the various submodes of git-config(1). But
while it is an option, it is currently listed as part of the submodes
group the command, which is wrong.

Move the option to the "Other" group, which hosts the various options
known to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/config.c b/builtin/config.c
index 59ae5996eb..054019b70c 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -643,7 +643,6 @@ static struct option builtin_config_options[] = {
 	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
 	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
 	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
 	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
 	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
 	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
@@ -663,6 +662,7 @@ static struct option builtin_config_options[] = {
 	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
 	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
 	OPT_END(),
 };
 
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 04/14] builtin/config: use `OPT_CMDMODE()` to specify modes
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (2 preceding siblings ...)
  2024-05-03  9:56   ` [PATCH v4 03/14] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03  9:57   ` [PATCH v4 05/14] builtin/config: pull out function to handle config location Patrick Steinhardt
                     ` (11 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 5525 bytes --]

The git-config(1) command has various different modes which are
accessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are
declared with `OPT_BIT()`, which causes two minor issues:

  - The respective modes also have a negated form `--no-get-urlmatch`,
    which is unintended.

  - We have to manually handle exclusiveness of the modes.

Switch these options to instead use `OPT_CMDMODE()`, which is made
exactly for this usecase. Remove the now-unneeded check that only a
single mode is given, which is now handled by the parse-options
interface.

While at it, format optional placeholders for arguments to conform to
our style guidelines by using `[<placeholder>]`.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c  | 32 ++++++++++++++------------------
 t/t1300-config.sh | 13 +++++++++++++
 2 files changed, 27 insertions(+), 18 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 054019b70c..8991533e08 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -632,20 +632,20 @@ static struct option builtin_config_options[] = {
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
 	OPT_GROUP(N_("Action")),
-	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
+	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
+	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
+	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
+	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
+	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
+	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
+	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
 	OPT_GROUP(N_("Type")),
 	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
 	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
@@ -769,10 +769,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (HAS_MULTI_BITS(actions)) {
-		error(_("only one action at a time"));
-		usage_builtin_config();
-	}
 	if (actions == 0)
 		switch (argc) {
 		case 1: actions = ACTION_GET; break;
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 9b65d9eaf5..86dc70769a 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -2738,4 +2738,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such
 	grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
 '
 
+test_expect_success 'negated mode causes failure' '
+	test_must_fail git config --no-get 2>err &&
+	grep "unknown option \`no-get${SQ}" err
+'
+
+test_expect_success 'specifying multiple modes causes failure' '
+	cat >expect <<-EOF &&
+	error: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together
+	EOF
+	test_must_fail git config --get --get-all 2>err &&
+	test_cmp expect err
+'
+
 test_done
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 05/14] builtin/config: pull out function to handle config location
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (3 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 04/14] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03  9:57   ` [PATCH v4 06/14] builtin/config: pull out function to handle `--null` Patrick Steinhardt
                     ` (10 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 10817 bytes --]

There's quite a bunch of options to git-config(1) that allow the user to
specify which config location to use when reading or writing config
options. The logic to handle this is thus by necessity also quite
involved.

Pull it out into a separate function so that we can reuse it in
subsequent commits which introduce proper subcommands.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 133 ++++++++++++++++++++++++-----------------------
 1 file changed, 68 insertions(+), 65 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 8991533e08..8c7cd30cb4 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -623,68 +623,8 @@ static char *default_user_config(void)
 	return strbuf_detach(&buf, NULL);
 }
 
-static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
-	OPT_GROUP(N_("Action")),
-	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
-	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
-	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
-	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
-	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
-	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
-	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
-	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
-	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
-	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
-	OPT_END(),
-};
-
-static NORETURN void usage_builtin_config(void)
+static void handle_config_location(const char *prefix)
 {
-	usage_with_options(builtin_config_usage, builtin_config_options);
-}
-
-int cmd_config(int argc, const char **argv, const char *prefix)
-{
-	int nongit = !startup_info->have_repository;
-	char *value = NULL, *comment = NULL;
-	int flags = 0;
-	int ret = 0;
-	struct key_value_info default_kvi = KVI_INIT;
-
-	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
-
-	argc = parse_options(argc, argv, prefix, builtin_config_options,
-			     builtin_config_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
-
 	if (use_global_config + use_system_config + use_local_config +
 	    use_worktree_config +
 	    !!given_config_source.file + !!given_config_source.blob > 1) {
@@ -692,14 +632,13 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (nongit) {
+	if (!startup_info->have_repository) {
 		if (use_local_config)
 			die(_("--local can only be used inside a git repository"));
 		if (given_config_source.blob)
 			die(_("--blob can only be used inside a git repository"));
 		if (use_worktree_config)
 			die(_("--worktree can only be used inside a git repository"));
-
 	}
 
 	if (given_config_source.file &&
@@ -753,10 +692,74 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		config_options.respect_includes = !given_config_source.file;
 	else
 		config_options.respect_includes = respect_includes_opt;
-	if (!nongit) {
+	if (startup_info->have_repository) {
 		config_options.commondir = get_git_common_dir();
 		config_options.git_dir = get_git_dir();
 	}
+}
+
+static struct option builtin_config_options[] = {
+	OPT_GROUP(N_("Config file location")),
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	OPT_GROUP(N_("Action")),
+	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
+	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
+	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
+	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
+	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
+	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
+	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
+	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
+	OPT_GROUP(N_("Type")),
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	OPT_GROUP(N_("Other")),
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
+	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_END(),
+};
+
+static NORETURN void usage_builtin_config(void)
+{
+	usage_with_options(builtin_config_usage, builtin_config_options);
+}
+
+int cmd_config(int argc, const char **argv, const char *prefix)
+{
+	char *value = NULL, *comment = NULL;
+	int flags = 0;
+	int ret = 0;
+	struct key_value_info default_kvi = KVI_INIT;
+
+	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
+
+	argc = parse_options(argc, argv, prefix, builtin_config_options,
+			     builtin_config_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+
+	handle_config_location(prefix);
 
 	if (end_nul) {
 		term = '\0';
@@ -858,7 +861,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		char *config_file;
 
 		check_argc(argc, 0, 0);
-		if (!given_config_source.file && nongit)
+		if (!given_config_source.file && !startup_info->have_repository)
 			die(_("not in a git directory"));
 		if (given_config_source.use_stdin)
 			die(_("editing stdin is not supported"));
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 06/14] builtin/config: pull out function to handle `--null`
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (4 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 05/14] builtin/config: pull out function to handle config location Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03  9:57   ` [PATCH v4 07/14] builtin/config: introduce "list" subcommand Patrick Steinhardt
                     ` (9 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 1177 bytes --]

Pull out function to handle the `--null` option, which we are about to
reuse in subsequent commits.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 8c7cd30cb4..59877065f8 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -698,6 +698,14 @@ static void handle_config_location(const char *prefix)
 	}
 }
 
+static void handle_nul(void) {
+	if (end_nul) {
+		term = '\0';
+		delim = '\n';
+		key_delim = '\n';
+	}
+}
+
 static struct option builtin_config_options[] = {
 	OPT_GROUP(N_("Config file location")),
 	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
@@ -760,12 +768,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 			     PARSE_OPT_STOP_AT_NON_OPTION);
 
 	handle_config_location(prefix);
-
-	if (end_nul) {
-		term = '\0';
-		delim = '\n';
-		key_delim = '\n';
-	}
+	handle_nul();
 
 	if ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {
 		error(_("--get-color and variable type are incoherent"));
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (5 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 06/14] builtin/config: pull out function to handle `--null` Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03 13:08     ` Karthik Nayak
  2024-05-03  9:57   ` [PATCH v4 08/14] builtin/config: introduce "get" subcommand Patrick Steinhardt
                     ` (8 subsequent siblings)
  15 siblings, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 28003 bytes --]

While git-config(1) has several modes, those modes are not exposed with
subcommands but instead by specifying e.g. `--unset` or `--list`. This
user interface is not really in line with how our more modern commands
work, where it is a lot more customary to say e.g. `git remote list`.
Furthermore, to add to the confusion, git-config(1) also allows the user
to request modes implicitly by just specifying the correct number of
arguments. Thus, `git config foo.bar` will retrieve the value of
"foo.bar" while `git config foo.bar baz` will set it to "baz".

Overall, this makes for a confusing interface that could really use a
makeover. It hurts discoverability of what you can do with git-config(1)
and is comparatively easy to get wrong. Converting the command to have
subcommands instead would go a long way to help address these issues.

One concern in this context is backwards compatibility. Luckily, we can
introduce subcommands without breaking backwards compatibility at all.
This is because all the implicit modes of git-config(1) require that the
first argument is a properly formatted config key. And as config keys
_must_ have a dot in their name, any value without a dot would have been
discarded by git-config(1) previous to this change. Thus, given that
none of the subcommands do have a dot, they are unambiguous.

Introduce the first such new subcommand, which is "git config list". To
retain backwards compatibility we only conditionally use subcommands and
will fall back to the old syntax in case no subcommand was detected.
This should help to transition to the new-style syntax until we
eventually deprecate and remove the old-style syntax.

Note that the way we handle this we're duplicating some functionality
across old and new syntax. While this isn't pretty, it helps us to
ensure that there really is no change in behaviour for the old syntax.

Amend tests such that we run them both with old and new style syntax.
As tests are now run twice, state from the first run may be still be
around in the second run and thus cause tests to fail. Add cleanup logic
as required to fix such tests.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt |  26 ++++++---
 builtin/config.c             |  90 ++++++++++++++++++++++++----
 t/t1300-config.sh            | 110 +++++++++++++++++++++--------------
 3 files changed, 162 insertions(+), 64 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index ac61113fcc..c83c97cb7e 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -9,6 +9,7 @@ git-config - Get and set repository or global options
 SYNOPSIS
 --------
 [verse]
+'git config list' [<file-option>] [<display-option>] [--includes]
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
@@ -20,7 +21,6 @@ SYNOPSIS
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
-'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list
 'git config' [<file-option>] --get-color <name> [<default>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
@@ -74,6 +74,12 @@ On success, the command returns the exit code 0.
 A list of all available configuration variables can be obtained using the
 `git help --config` command.
 
+COMMANDS
+--------
+
+list::
+	List all variables set in config file, along with their values.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -190,10 +196,6 @@ See also <<FILES>>.
 --unset-all::
 	Remove all lines matching the key from config file.
 
--l::
---list::
-	List all variables set in config file, along with their values.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -248,7 +250,7 @@ Valid `<type>`'s include:
 	contain line breaks.
 
 --name-only::
-	Output only the names of config variables for `--list` or
+	Output only the names of config variables for `list` or
 	`--get-regexp`.
 
 --show-origin::
@@ -300,10 +302,20 @@ Valid `<type>`'s include:
   When using `--get`, and the requested variable is not found, behave as if
   <value> were the value assigned to that variable.
 
+DEPRECATED MODES
+----------------
+
+The following modes have been deprecated in favor of subcommands. It is
+recommended to migrate to the new syntax.
+
+-l::
+--list::
+	Replaced by `git config list`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
-using `--list` or any of the `--get-*` which may return multiple results.
+using `list` or any of the `--get-*` which may return multiple results.
 The default is to use a pager.
 
 [[FILES]]
diff --git a/builtin/config.c b/builtin/config.c
index 59877065f8..f89ddce8da 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -16,10 +16,16 @@
 #include "worktree.h"
 
 static const char *const builtin_config_usage[] = {
+	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config [<options>]"),
 	NULL
 };
 
+static const char *const builtin_config_list_usage[] = {
+	N_("git config list [<file-option>] [<display-option>] [--includes]"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -33,6 +39,7 @@ static char delim = '=';
 static char key_delim = ' ';
 static char term = '\n';
 
+static parse_opt_subcommand_fn *subcommand;
 static int use_global_config, use_system_config, use_local_config;
 static int use_worktree_config;
 static struct git_config_source given_config_source;
@@ -706,14 +713,24 @@ static void handle_nul(void) {
 	}
 }
 
+#define CONFIG_LOCATION_OPTIONS \
+	OPT_GROUP(N_("Config file location")), \
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")), \
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")), \
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")), \
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")), \
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")), \
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object"))
+
+#define CONFIG_DISPLAY_OPTIONS \
+	OPT_GROUP(N_("Display options")), \
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), \
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")), \
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")), \
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)"))
+
 static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	CONFIG_LOCATION_OPTIONS,
 	OPT_GROUP(N_("Action")),
 	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
 	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
@@ -737,15 +754,12 @@ static struct option builtin_config_options[] = {
 	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
 	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
 	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	CONFIG_DISPLAY_OPTIONS,
 	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
 	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
 	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
 	OPT_END(),
 };
 
@@ -754,6 +768,42 @@ static NORETURN void usage_builtin_config(void)
 	usage_with_options(builtin_config_usage, builtin_config_options);
 }
 
+static int cmd_config_list(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_DISPLAY_OPTIONS,
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);
+	check_argc(argc, 0, 0);
+
+	handle_config_location(prefix);
+	handle_nul();
+
+	setup_auto_pager("config", 1);
+
+	if (config_with_options(show_all_config, NULL,
+				&given_config_source, the_repository,
+				&config_options) < 0) {
+		if (given_config_source.file)
+			die_errno(_("unable to read config file '%s'"),
+				  given_config_source.file);
+		else
+			die(_("error processing config file(s)"));
+	}
+
+	return 0;
+}
+
+static struct option builtin_subcommand_options[] = {
+	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
+	OPT_END(),
+};
+
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	char *value = NULL, *comment = NULL;
@@ -763,6 +813,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 
 	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
 
+	/*
+	 * This is somewhat hacky: we first parse the command line while
+	 * keeping all args intact in order to determine whether a subcommand
+	 * has been specified. If so, we re-parse it a second time, but this
+	 * time we drop KEEP_ARGV0. This is so that we don't munge the command
+	 * line in case no subcommand was given, which would otherwise confuse
+	 * us when parsing the legacy-style modes that don't use subcommands.
+	 */
+	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
+	if (subcommand) {
+		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
+		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);
+		return subcommand(argc, argv, prefix);
+	}
+
 	argc = parse_options(argc, argv, prefix, builtin_config_options,
 			     builtin_config_usage,
 			     PARSE_OPT_STOP_AT_NON_OPTION);
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 86dc70769a..f77d2f7847 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -11,6 +11,20 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
+for mode in legacy subcommands
+do
+
+case "$mode" in
+legacy)
+	mode_prefix="--"
+	;;
+subcommands)
+	mode_prefix=""
+	;;
+*)
+	BUG "unknown mode $mode";;
+esac
+
 test_expect_success 'setup whitespace config' '
 	sed -e "s/^|//" \
 	    -e "s/[$]$//" \
@@ -460,11 +474,11 @@ version.1.2.3eX.alpha=beta
 EOF
 
 test_expect_success 'working --list' '
-	git config --list > output &&
+	git config ${mode_prefix}list > output &&
 	test_cmp expect output
 '
 test_expect_success '--list without repo produces empty output' '
-	git --git-dir=nonexistent config --list >output &&
+	git --git-dir=nonexistent config ${mode_prefix}list >output &&
 	test_must_be_empty output
 '
 
@@ -476,7 +490,7 @@ version.1.2.3eX.alpha
 EOF
 
 test_expect_success '--name-only --list' '
-	git config --name-only --list >output &&
+	git config ${mode_prefix}list --name-only >output &&
 	test_cmp expect output
 '
 
@@ -614,17 +628,17 @@ ein.bahn=strasse
 EOF
 
 test_expect_success 'alternative GIT_CONFIG' '
-	GIT_CONFIG=other-config git config --list >output &&
+	GIT_CONFIG=other-config git config ${mode_prefix}list >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'alternative GIT_CONFIG (--file)' '
-	git config --file other-config --list >output &&
+	git config ${mode_prefix}list --file other-config >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'alternative GIT_CONFIG (--file=-)' '
-	git config --file - --list <other-config >output &&
+	git config ${mode_prefix}list --file - <other-config >output &&
 	test_cmp expect output
 '
 
@@ -637,6 +651,7 @@ test_expect_success 'editing stdin is an error' '
 '
 
 test_expect_success 'refer config from subdirectory' '
+	test_when_finished "rm -r x" &&
 	mkdir x &&
 	test_cmp_config -C x strasse --file=../other-config --get ein.bahn
 '
@@ -847,7 +862,7 @@ test_expect_success 'line number is reported correctly' '
 '
 
 test_expect_success 'invalid stdin config' '
-	echo "[broken" | test_must_fail git config --list --file - >output 2>&1 &&
+	echo "[broken" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&
 	test_grep "bad config line 1 in standard input" output
 '
 
@@ -1139,7 +1154,7 @@ section.quotecont=cont;inued
 EOF
 
 test_expect_success 'value continued on next line' '
-	git config --list > result &&
+	git config ${mode_prefix}list > result &&
 	test_cmp expect result
 '
 
@@ -1163,7 +1178,7 @@ Qsection.sub=section.val4
 Qsection.sub=section.val5Q
 EOF
 test_expect_success '--null --list' '
-	git config --null --list >result.raw &&
+	git config ${mode_prefix}list --null >result.raw &&
 	nul_to_q <result.raw >result &&
 	echo >>result &&
 	test_cmp expect result
@@ -1198,6 +1213,7 @@ test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces'
 '
 
 test_expect_success SYMLINKS 'symlinked configuration' '
+	test_when_finished "rm myconfig" &&
 	ln -s notyet myconfig &&
 	git config --file=myconfig test.frotz nitfol &&
 	test -h myconfig &&
@@ -1218,10 +1234,11 @@ test_expect_success SYMLINKS 'symlinked configuration' '
 '
 
 test_expect_success SYMLINKS 'symlink to nonexistent configuration' '
+	test_when_finished "rm linktonada linktolinktonada" &&
 	ln -s doesnotexist linktonada &&
 	ln -s linktonada linktolinktonada &&
-	test_must_fail git config --file=linktonada --list &&
-	test_must_fail git config --file=linktolinktonada --list
+	test_must_fail git config ${mode_prefix}list --file=linktonada &&
+	test_must_fail git config ${mode_prefix}list --file=linktolinktonada
 '
 
 test_expect_success 'check split_cmdline return' '
@@ -1478,7 +1495,7 @@ do
 done
 
 test_expect_success 'git -c is not confused by empty environment' '
-	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config --list
+	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config ${mode_prefix}list
 '
 
 test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
@@ -1669,31 +1686,31 @@ test_expect_success 'git config ignores pairs with empty count' '
 '
 
 test_expect_success 'git config fails with invalid count' '
-	test_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&
+	test_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&
 	test_grep "bogus count" error &&
-	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&
+	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&
 	test_grep "too many entries" error
 '
 
 test_expect_success 'git config fails with missing config key' '
 	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0="value" \
-		git config --list 2>error &&
+		git config ${mode_prefix}list 2>error &&
 	test_grep "missing config key" error
 '
 
 test_expect_success 'git config fails with missing config value' '
 	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="pair.one" \
-		git config --list 2>error &&
+		git config ${mode_prefix}list 2>error &&
 	test_grep "missing config value" error
 '
 
 test_expect_success 'git config fails with invalid config pair key' '
 	test_must_fail env GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \
-		git config --list &&
+		git config ${mode_prefix}list &&
 	test_must_fail env GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \
-		git config --list
+		git config ${mode_prefix}list
 '
 
 test_expect_success 'environment overrides config file' '
@@ -1733,7 +1750,7 @@ test_expect_success 'git config --edit works' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
 	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
-	git config -f tmp --list >actual &&
+	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
 
@@ -1742,7 +1759,7 @@ test_expect_success 'git config --edit respects core.editor' '
 	echo test.value=yes >expect &&
 	test_config core.editor "echo [test]value=yes >" &&
 	git config -f tmp --edit &&
-	git config -f tmp --list >actual &&
+	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
 
@@ -2093,7 +2110,7 @@ test_expect_success '--show-origin with --list' '
 	command line:	user.cmdline=true
 	EOF
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\
-		git -c user.cmdline=true config --list --show-origin >output &&
+		git -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2110,7 +2127,7 @@ test_expect_success '--show-origin with --list --null' '
 	includeQcommand line:Quser.cmdline
 	trueQ
 	EOF
-	git -c user.cmdline=true config --null --list --show-origin >output.raw &&
+	git -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&
 	nul_to_q <output.raw >output &&
 	# The here-doc above adds a newline that the --null output would not
 	# include. Add it here to make the two comparable.
@@ -2124,7 +2141,7 @@ test_expect_success '--show-origin with single file' '
 	file:.git/config	user.override=local
 	file:.git/config	include.path=../include/relative.include
 	EOF
-	git config --local --list --show-origin >output &&
+	git config ${mode_prefix}list --local --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2162,7 +2179,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '
 	cat >expect <<-\EOF &&
 	file:"file\" (dq) and spaces.conf"	user.custom=true
 	EOF
-	git config --file "$WEIRDLY_NAMED_FILE" --show-origin --list >output &&
+	git config ${mode_prefix}list --file "$WEIRDLY_NAMED_FILE" --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2170,7 +2187,7 @@ test_expect_success '--show-origin stdin' '
 	cat >expect <<-\EOF &&
 	standard input:	user.custom=true
 	EOF
-	git config --file - --show-origin --list <"$CUSTOM_CONFIG_FILE" >output &&
+	git config ${mode_prefix}list --file - --show-origin <"$CUSTOM_CONFIG_FILE" >output &&
 	test_cmp expect output
 '
 
@@ -2197,7 +2214,7 @@ test_expect_success '--show-origin blob' '
 		cat >expect <<-EOF &&
 		blob:$blob	user.custom=true
 		EOF
-		git config --blob=$blob --show-origin --list >output &&
+		git config ${mode_prefix}list --blob=$blob --show-origin >output &&
 		test_cmp expect output
 	)
 '
@@ -2213,7 +2230,7 @@ test_expect_success '--show-origin blob ref' '
 		cp "$CUSTOM_CONFIG_FILE" custom.conf &&
 		git add custom.conf &&
 		git commit -m "new config file" &&
-		git config --blob=main:custom.conf --show-origin --list >output &&
+		git config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&
 		test_cmp expect output
 	)
 '
@@ -2239,13 +2256,14 @@ test_expect_success '--show-scope with --list' '
 	worktree	user.worktree=true
 	command	user.cmdline=true
 	EOF
+	test_when_finished "git worktree remove wt1" &&
 	git worktree add wt1 &&
 	# We need these to test for worktree scope, but outside of this
 	# test, this is just noise
 	test_config core.repositoryformatversion 1 &&
 	test_config extensions.worktreeConfig true &&
 	git config --worktree user.worktree true &&
-	git -c user.cmdline=true config --list --show-scope >output &&
+	git -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2254,7 +2272,7 @@ test_expect_success !MINGW '--show-scope with --blob' '
 	cat >expect <<-EOF &&
 	command	user.custom=true
 	EOF
-	git config --blob=$blob --show-scope --list >output &&
+	git config ${mode_prefix}list --blob=$blob --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2264,7 +2282,7 @@ test_expect_success '--show-scope with --local' '
 	local	user.override=local
 	local	include.path=../include/relative.include
 	EOF
-	git config --local --list --show-scope >output &&
+	git config ${mode_prefix}list --local --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2288,7 +2306,7 @@ test_expect_success '--show-scope with --show-origin' '
 	local	file:.git/../include/relative.include	user.relative=include
 	command	command line:	user.cmdline=true
 	EOF
-	git -c user.cmdline=true config --list --show-origin --show-scope >output &&
+	git -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2329,7 +2347,7 @@ test_expect_success 'override global and system config' '
 	global	home.config=true
 	local	local.config=true
 	EOF
-	git config --show-scope --list >output &&
+	git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output &&
 
 	cat >expect <<-EOF &&
@@ -2338,20 +2356,20 @@ test_expect_success 'override global and system config' '
 	local	local.config=true
 	EOF
 	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \
-		git config --show-scope --list >output &&
+		git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output &&
 
 	cat >expect <<-EOF &&
 	local	local.config=true
 	EOF
 	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \
-		git config --show-scope --list >output &&
+		git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'override global and system config with missing file' '
-	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&
-	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&
+	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&
+	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&
 	GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version
 '
 
@@ -2478,7 +2496,7 @@ test_expect_success 'set all config with value-pattern' '
 	# no match => add new entry
 	cp initial config &&
 	git config --file=config abc.key two a+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2491,7 +2509,7 @@ test_expect_success 'set all config with value-pattern' '
 
 	# multiple values, no match => add
 	git config --file=config abc.key three a+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2501,7 +2519,7 @@ test_expect_success 'set all config with value-pattern' '
 
 	# single match => replace
 	git config --file=config abc.key four h+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2516,7 +2534,7 @@ test_expect_success '--replace-all and value-pattern' '
 	git config --file=config --add abc.key two &&
 	git config --file=config --add abc.key three &&
 	git config --file=config --replace-all abc.key four "o+" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=four
 	abc.key=three
@@ -2534,7 +2552,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --rename-section dev null &&
 	test_must_fail git config --file=config --fixed-value --remove-section dev &&
-	test_must_fail git config --file=config --fixed-value --list &&
+	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
 
@@ -2555,7 +2573,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
 	fixed.test=bogus
@@ -2564,7 +2582,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config --fixed-value fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	fixed.test=bogus
 	EOF
@@ -2582,7 +2600,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config --replace-all fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
 	fixed.test=bogus
@@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 	test_cmp expect actual &&
 
 	git config --file=config --fixed-value --replace-all fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=bogus
 	fixed.test=bogus
@@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes failure' '
 	test_cmp expect err
 '
 
+done
+
 test_done
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 08/14] builtin/config: introduce "get" subcommand
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (6 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 07/14] builtin/config: introduce "list" subcommand Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03  9:57   ` [PATCH v4 09/14] builtin/config: introduce "set" subcommand Patrick Steinhardt
                     ` (7 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 33245 bytes --]

Introduce a new "get" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt |  93 ++++++++++++-------------
 builtin/config.c             |  69 +++++++++++++++---
 t/t1300-config.sh            | 131 ++++++++++++++++++++++++-----------
 3 files changed, 194 insertions(+), 99 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index c83c97cb7e..d0878663db 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -10,18 +10,14 @@ SYNOPSIS
 --------
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
+'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL>
 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
-'git config' [<file-option>] --get-color <name> [<default>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
 
@@ -80,6 +76,12 @@ COMMANDS
 list::
 	List all variables set in config file, along with their values.
 
+get::
+	Emits the value of the specified key. If key is present multiple times
+	in the configuration, emits the last value. If `--all` is specified,
+	emits all values associated with key. Returns error code 1 if key is
+	not present.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -105,22 +107,16 @@ OPTIONS
 	not contain linefeed characters (no multi-line comments are
 	permitted).
 
---get::
-	Get the value for a given key (optionally filtered by a regex
-	matching the value). Returns error code 1 if the key was not
-	found and the last value if multiple key values were found.
-
---get-all::
-	Like get, but returns all values for a multi-valued key.
+--all::
+	With `get`, return all values for a multi-valued key.
 
---get-regexp::
-	Like --get-all, but interprets the name as a regular expression and
-	writes out the key names.  Regular expression matching is currently
-	case-sensitive and done against a canonicalized version of the key
-	in which section and variable names are lowercased, but subsection
-	names are not.
+---regexp::
+	With `get`, interpret the name as a regular expression. Regular
+	expression matching is currently case-sensitive and done against a
+	canonicalized version of the key in which section and variable names
+	are lowercased, but subsection names are not.
 
---get-urlmatch <name> <URL>::
+--url=<URL>::
 	When given a two-part <name> as <section>.<key>, the value for
 	<section>.<URL>.<key> whose <URL> part matches the best to the
 	given URL is returned (if no such key exists, the value for
@@ -251,7 +247,7 @@ Valid `<type>`'s include:
 
 --name-only::
 	Output only the names of config variables for `list` or
-	`--get-regexp`.
+	`get`.
 
 --show-origin::
 	Augment the output of all queried config options with the
@@ -275,17 +271,6 @@ Valid `<type>`'s include:
 	When the color setting for `name` is undefined, the command uses
 	`color.ui` as fallback.
 
---get-color <name> [<default>]::
-
-	Find the color configured for `name` (e.g. `color.diff.new`) and
-	output it as the ANSI color escape sequence to the standard
-	output.  The optional `default` parameter is used instead, if
-	there is no color configured for `name`.
-+
-`--type=color [--default=<default>]` is preferred over `--get-color`
-(but note that `--get-color` will omit the trailing newline printed by
-`--type=color`).
-
 -e::
 --edit::
 	Opens an editor to modify the specified config file; either
@@ -299,7 +284,7 @@ Valid `<type>`'s include:
 	config files.
 
 --default <value>::
-  When using `--get`, and the requested variable is not found, behave as if
+  When using `get`, and the requested variable is not found, behave as if
   <value> were the value assigned to that variable.
 
 DEPRECATED MODES
@@ -308,15 +293,33 @@ DEPRECATED MODES
 The following modes have been deprecated in favor of subcommands. It is
 recommended to migrate to the new syntax.
 
+'git config <name>'::
+	Replaced by `git config get <name>`.
+
 -l::
 --list::
 	Replaced by `git config list`.
 
+--get <name> [<value-pattern>]::
+	Replaced by `git config get [--value=<pattern>] <name>`.
+
+--get-all <name> [<value-pattern>]::
+	Replaced by `git config get [--value=<pattern>] --all --show-names <name>`.
+
+--get-regexp <name-regexp>::
+	Replaced by `git config get --all --show-names --regexp <name-regexp>`.
+
+--get-urlmatch <name> <URL>::
+	Replaced by `git config get --all --show-names --url=<URL> <name>`.
+
+--get-color <name> [<default>]::
+	Replaced by `git config get --type=color [--default=<default>] <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
-using `list` or any of the `--get-*` which may return multiple results.
-The default is to use a pager.
+using `list` or `get` which may return multiple results. The default is to use
+a pager.
 
 [[FILES]]
 FILES
@@ -519,25 +522,19 @@ you have to provide a regex matching the value of exactly one line.
 To query the value for a given key, do
 
 ------------
-% git config --get core.filemode
-------------
-
-or
-
-------------
-% git config core.filemode
+% git config get core.filemode
 ------------
 
 or, to query a multivar:
 
 ------------
-% git config --get core.gitproxy "for kernel.org$"
+% git config get --value="for kernel.org$" core.gitproxy
 ------------
 
 If you want to know all the values for a multivar, do:
 
 ------------
-% git config --get-all core.gitproxy
+% git config get --all --show-names core.gitproxy
 ------------
 
 If you like to live dangerously, you can replace *all* core.gitproxy by a
@@ -571,8 +568,8 @@ script:
 
 ------------
 #!/bin/sh
-WS=$(git config --get-color color.diff.whitespace "blue reverse")
-RESET=$(git config --get-color "" "reset")
+WS=$(git config get --type=color --default="blue reverse" color.diff.whitespace)
+RESET=$(git config get --type=color --default="reset" "")
 echo "${WS}your whitespace color or blue reverse${RESET}"
 ------------
 
@@ -580,11 +577,11 @@ For URLs in `https://weak.example.com`, `http.sslVerify` is set to
 false, while it is set to `true` for all others:
 
 ------------
-% git config --type=bool --get-urlmatch http.sslverify https://good.example.com
+% git config get --type=bool --url=https://good.example.com http.sslverify
 true
-% git config --type=bool --get-urlmatch http.sslverify https://weak.example.com
+% git config get --type=bool --url=https://weak.example.com http.sslverify
 false
-% git config --get-urlmatch http https://weak.example.com
+% git config get --url=https://weak.example.com http
 http.cookieFile /tmp/cookie.txt
 http.sslverify false
 ------------
diff --git a/builtin/config.c b/builtin/config.c
index f89ddce8da..9e7ae49c02 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -17,7 +17,7 @@
 
 static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
-	N_("git config [<options>]"),
+	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	NULL
 };
 
@@ -26,6 +26,11 @@ static const char *const builtin_config_list_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_get_usage[] = {
+	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -722,6 +727,16 @@ static void handle_nul(void) {
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")), \
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object"))
 
+#define CONFIG_TYPE_OPTIONS \
+	OPT_GROUP(N_("Type")), \
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type), \
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL), \
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT), \
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT), \
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR), \
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH), \
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE)
+
 #define CONFIG_DISPLAY_OPTIONS \
 	OPT_GROUP(N_("Display options")), \
 	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), \
@@ -746,14 +761,7 @@ static struct option builtin_config_options[] = {
 	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
 	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
 	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	CONFIG_TYPE_OPTIONS,
 	CONFIG_DISPLAY_OPTIONS,
 	OPT_GROUP(N_("Other")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
@@ -799,8 +807,51 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix)
 	return 0;
 }
 
+static int cmd_config_get(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL, *url = NULL;
+	int flags = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_TYPE_OPTIONS,
+		OPT_GROUP(N_("Filter options")),
+		OPT_BOOL(0, "all", &do_all, N_("return all values for multi-valued config options")),
+		OPT_BOOL(0, "regexp", &use_key_regexp, N_("interpret the name as a regular expression")),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_STRING(0, "url", &url, N_("URL"), N_("show config matching the given URL")),
+		CONFIG_DISPLAY_OPTIONS,
+		OPT_BOOL(0, "show-names", &show_keys, N_("show config keys in addition to their values")),
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+		OPT_STRING(0, "default", &default_value, N_("value"), N_("use default value when missing entry")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_get_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_argc(argc, 1, 1);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with 'value-pattern'"));
+	if (default_value && (do_all || url))
+		die(_("--default= cannot be used with --all or --url="));
+	if (url && (do_all || use_key_regexp || value_pattern))
+		die(_("--url= cannot be used with --all, --regexp or --value"));
+
+	handle_config_location(prefix);
+	handle_nul();
+
+	setup_auto_pager("config", 1);
+
+	if (url)
+		return get_urlmatch(argv[0], url);
+	return get_value(argv[0], value_pattern, flags);
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
+	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index f77d2f7847..7ce13ed6cd 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -17,9 +17,15 @@ do
 case "$mode" in
 legacy)
 	mode_prefix="--"
+	mode_get=""
+	mode_get_all="--get-all"
+	mode_get_regexp="--get-regexp"
 	;;
 subcommands)
 	mode_prefix=""
+	mode_get="get"
+	mode_get_all="get --all"
+	mode_get_regexp="get --regexp --all --show-names"
 	;;
 *)
 	BUG "unknown mode $mode";;
@@ -398,7 +404,7 @@ test_expect_success 'multi-valued get-all returns all' '
 	wow
 	wow2 for me
 	EOF
-	git config --get-all nextsection.nonewline >actual &&
+	git config ${mode_get_all} nextsection.nonewline >actual &&
 	test_cmp expect actual
 '
 
@@ -500,7 +506,7 @@ nextsection.nonewline wow2 for me
 EOF
 
 test_expect_success '--get-regexp' '
-	git config --get-regexp in >output &&
+	git config ${mode_get_regexp} in >output &&
 	test_cmp expect output
 '
 
@@ -510,7 +516,7 @@ nextsection.nonewline
 EOF
 
 test_expect_success '--name-only --get-regexp' '
-	git config --name-only --get-regexp in >output &&
+	git config ${mode_get_regexp} --name-only in >output &&
 	test_cmp expect output
 '
 
@@ -521,7 +527,7 @@ EOF
 
 test_expect_success '--add' '
 	git config --add nextsection.nonewline "wow4 for you" &&
-	git config --get-all nextsection.nonewline > output &&
+	git config ${mode_get_all} nextsection.nonewline > output &&
 	test_cmp expect output
 '
 
@@ -543,21 +549,21 @@ test_expect_success 'get variable with empty value' '
 echo novalue.variable > expect
 
 test_expect_success 'get-regexp variable with no value' '
-	git config --get-regexp novalue > output &&
+	git config ${mode_get_regexp} novalue > output &&
 	test_cmp expect output
 '
 
 echo 'novalue.variable true' > expect
 
 test_expect_success 'get-regexp --bool variable with no value' '
-	git config --bool --get-regexp novalue > output &&
+	git config ${mode_get_regexp} --bool novalue > output &&
 	test_cmp expect output
 '
 
 echo 'emptyvalue.variable ' > expect
 
 test_expect_success 'get-regexp variable with empty value' '
-	git config --get-regexp emptyvalue > output &&
+	git config ${mode_get_regexp} emptyvalue > output &&
 	test_cmp expect output
 '
 
@@ -1131,7 +1137,7 @@ test_expect_success 'quoting' '
 '
 
 test_expect_success 'key with newline' '
-	test_must_fail git config "key.with
+	test_must_fail git config ${mode_get} "key.with
 newline" 123'
 
 test_expect_success 'value with newline' 'git config key.sub value.with\\\
@@ -1185,7 +1191,7 @@ test_expect_success '--null --list' '
 '
 
 test_expect_success '--null --get-regexp' '
-	git config --null --get-regexp "val[0-9]" >result.raw &&
+	git config ${mode_get_regexp} --null "val[0-9]" >result.raw &&
 	nul_to_q <result.raw >result &&
 	echo >>result &&
 	test_cmp expect result
@@ -1194,21 +1200,21 @@ test_expect_success '--null --get-regexp' '
 test_expect_success 'inner whitespace kept verbatim, spaces only' '
 	echo "foo   bar" >expect &&
 	git config section.val "foo   bar" &&
-	git config --get section.val >actual &&
+	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '
 	echo "fooQQbar" | q_to_tab >expect &&
 	git config section.val "$(cat expect)" &&
-	git config --get section.val >actual &&
+	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '
 	echo "foo Q  bar" | q_to_tab >expect &&
 	git config section.val "$(cat expect)" &&
-	git config --get section.val >actual &&
+	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
 
@@ -1283,11 +1289,11 @@ test_expect_success 'git -c can represent empty string' '
 '
 
 test_expect_success 'key sanity-checking' '
-	test_must_fail git config foo=bar &&
-	test_must_fail git config foo=.bar &&
-	test_must_fail git config foo.ba=r &&
-	test_must_fail git config foo.1bar &&
-	test_must_fail git config foo."ba
+	test_must_fail git config ${mode_get} foo=bar &&
+	test_must_fail git config ${mode_get} foo=.bar &&
+	test_must_fail git config ${mode_get} foo.ba=r &&
+	test_must_fail git config ${mode_get} foo.1bar &&
+	test_must_fail git config ${mode_get} foo."ba
 				z".bar &&
 	test_must_fail git config . false &&
 	test_must_fail git config .foo false &&
@@ -1336,7 +1342,7 @@ test_expect_success 'git -c complains about empty key and value' '
 '
 
 test_expect_success 'multiple git -c appends config' '
-	test_config alias.x "!git -c x.two=2 config --get-regexp ^x\.*" &&
+	test_config alias.x "!git -c x.two=2 config ${mode_get_regexp} ^x\.*" &&
 	cat >expect <<-\EOF &&
 	x.one 1
 	x.two 2
@@ -1502,7 +1508,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
 	v="${SQ}key.one=foo${SQ}" &&
 	v="$v  ${SQ}key.two=bar${SQ}" &&
 	v="$v ${SQ}key.ambiguous=section.whatever=value${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.one foo
 	key.two bar
@@ -1515,7 +1521,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '
 	v="${SQ}key.one${SQ}=${SQ}foo${SQ}" &&
 	v="$v  ${SQ}key.two${SQ}=${SQ}bar${SQ}" &&
 	v="$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.one foo
 	key.two bar
@@ -1529,7 +1535,7 @@ test_expect_success 'old and new-style entries can mix' '
 	v="$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}" &&
 	v="$v ${SQ}key.oldtwo=oldbar${SQ}" &&
 	v="$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.oldone oldfoo
 	key.newone newfoo
@@ -1542,7 +1548,7 @@ test_expect_success 'old and new-style entries can mix' '
 test_expect_success 'old and new bools with ambiguous subsection' '
 	v="${SQ}key.with=equals.oldbool${SQ}" &&
 	v="$v ${SQ}key.with=equals.newbool${SQ}=" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.with equals.oldbool
 	key.with=equals.newbool
@@ -1556,7 +1562,7 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
 	env.two two
 	EOF
 	GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ} ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*" >actual &&
+		git config ${mode_get_regexp} "env.*" >actual &&
 	test_cmp expect actual &&
 
 	cat >expect <<-EOF &&
@@ -1564,12 +1570,12 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
 	env.two two
 	EOF
 	GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ}\\$SQ$SQ$SQ ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*" >actual &&
+		git config ${mode_get_regexp} "env.*" >actual &&
 	test_cmp expect actual &&
 
 	test_must_fail env \
 		GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ}\\$SQ ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*"
+		git config ${mode_get_regexp} "env.*"
 '
 
 test_expect_success 'git --config-env=key=envvar support' '
@@ -1617,7 +1623,7 @@ test_expect_success 'git -c and --config-env work together' '
 	ENVVAR=env-value git \
 		-c bar.cmd=cmd-value \
 		--config-env=bar.env=ENVVAR \
-		config --get-regexp "^bar.*" >actual &&
+		config ${mode_get_regexp} "^bar.*" >actual &&
 	test_cmp expect actual
 '
 
@@ -1645,7 +1651,7 @@ test_expect_success 'git config handles environment config pairs' '
 	GIT_CONFIG_COUNT=2 \
 		GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="foo" \
 		GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="bar" \
-		git config --get-regexp "pair.*" >actual &&
+		git config ${mode_get_regexp} "pair.*" >actual &&
 	cat >expect <<-EOF &&
 	pair.one foo
 	pair.two bar
@@ -1655,7 +1661,7 @@ test_expect_success 'git config handles environment config pairs' '
 
 test_expect_success 'git config ignores pairs without count' '
 	test_must_fail env GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
@@ -1663,7 +1669,7 @@ test_expect_success 'git config ignores pairs exceeding count' '
 	GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
 		GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="value" \
-		git config --get-regexp "pair.*" >actual 2>error &&
+		git config ${mode_get_regexp} "pair.*" >actual 2>error &&
 	cat >expect <<-EOF &&
 	pair.one value
 	EOF
@@ -1674,14 +1680,14 @@ test_expect_success 'git config ignores pairs exceeding count' '
 test_expect_success 'git config ignores pairs with zero count' '
 	test_must_fail env \
 		GIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
 test_expect_success 'git config ignores pairs with empty count' '
 	test_must_fail env \
 		GIT_CONFIG_COUNT= GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
@@ -1720,7 +1726,7 @@ test_expect_success 'environment overrides config file' '
 	one = value
 	EOF
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \
-		git config pair.one >actual &&
+		git config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1730,7 +1736,7 @@ test_expect_success 'environment overrides config file' '
 test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \
 		GIT_CONFIG_PARAMETERS="${SQ}pair.one=override${SQ}" \
-		git config pair.one >actual &&
+		git config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1805,20 +1811,28 @@ test_expect_success 'urlmatch' '
 
 	test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&
 	test_must_be_empty actual &&
+	test_expect_code 1 git config get --url=https://good.example.com --bool doesnt.exist >actual &&
+	test_must_be_empty actual &&
 
 	echo true >expect &&
 	git config --bool --get-urlmatch http.SSLverify https://good.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --bool --url=https://good.example.com http.SSLverify >actual &&
+	test_cmp expect actual &&
 
 	echo false >expect &&
 	git config --bool --get-urlmatch http.sslverify https://weak.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --bool --url=https://weak.example.com http.sslverify >actual &&
+	test_cmp expect actual &&
 
 	{
 		echo http.cookiefile /tmp/cookie.txt &&
 		echo http.sslverify false
 	} >expect &&
 	git config --get-urlmatch HTTP https://weak.example.com >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://weak.example.com HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -1834,6 +1848,8 @@ test_expect_success 'urlmatch with --show-scope' '
 	local	http.sslverify false
 	EOF
 	git config --get-urlmatch --show-scope HTTP https://weak.example.com >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://weak.example.com --show-scope HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -1866,45 +1882,67 @@ test_expect_success 'urlmatch favors more specific URLs' '
 	echo http.cookiefile /tmp/root.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com/subdirectory >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com/subdirectory HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com/subdirectory/nested HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/user.txt >expect &&
 	git config --get-urlmatch HTTP https://user@example.com/ >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://user@example.com/ HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://averylonguser@example.com/subdirectory HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/preceding.txt >expect &&
 	git config --get-urlmatch HTTP https://preceding.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://preceding.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/wildcard.txt >expect &&
 	git config --get-urlmatch HTTP https://wildcard.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://wildcard.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/sub.txt >expect &&
 	git config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://sub.example.com/wildcardwithsubdomain HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/trailing.txt >expect &&
 	git config --get-urlmatch HTTP https://trailing.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://trailing.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/sub.txt >expect &&
 	git config --get-urlmatch HTTP https://user@sub.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://user@sub.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/multiwildcard.txt >expect &&
 	git config --get-urlmatch HTTP https://wildcard.example.org >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://wildcard.example.org HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -2027,7 +2065,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[one]
 	EOF
 	git config --unset two.subsection.key &&
-	test "not [two subsection]" = "$(git config one.key)" &&
+	test "not [two subsection]" = "$(git config ${mode_get} one.key)" &&
 	test_line_count = 3 .git/config
 '
 
@@ -2150,7 +2188,7 @@ test_expect_success '--show-origin with --get-regexp' '
 	file:$HOME/.gitconfig	user.global true
 	file:.git/config	user.local true
 	EOF
-	git config --show-origin --get-regexp "user\.[g|l].*" >output &&
+	git config ${mode_get_regexp} --show-origin "user\.[g|l].*" >output &&
 	test_cmp expect output
 '
 
@@ -2158,7 +2196,7 @@ test_expect_success '--show-origin getting a single key' '
 	cat >expect <<-\EOF &&
 	file:.git/config	local
 	EOF
-	git config --show-origin user.override >output &&
+	git config ${mode_get} --show-origin user.override >output &&
 	test_cmp expect output
 '
 
@@ -2290,7 +2328,7 @@ test_expect_success '--show-scope getting a single value' '
 	cat >expect <<-\EOF &&
 	local	true
 	EOF
-	git config --show-scope --get user.local >output &&
+	git config ${mode_get} --show-scope user.local >output &&
 	test_cmp expect output
 '
 
@@ -2559,9 +2597,9 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	# These modes complain when --fixed-value has no value-pattern
 	test_must_fail git config --file=config --fixed-value dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --get dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-all dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-regexp "dev.*" &&
+	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
 	test_must_fail git config --file=config --fixed-value --unset dev.null &&
 	test_must_fail git config --file=config --fixed-value --unset-all dev.null
 '
@@ -2591,12 +2629,12 @@ test_expect_success '--fixed-value uses exact string matching' '
 	cp initial config &&
 	test_must_fail git config --file=config --unset fixed.test "$META" &&
 	git config --file=config --fixed-value --unset fixed.test "$META" &&
-	test_must_fail git config --file=config fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
 	test_must_fail git config --file=config --unset-all fixed.test "$META" &&
 	git config --file=config --fixed-value --unset-all fixed.test "$META" &&
-	test_must_fail git config --file=config fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
 	git config --file=config --replace-all fixed.test bogus "$META" &&
@@ -2623,18 +2661,27 @@ test_expect_success '--get and --get-all with --fixed-value' '
 	git config --file=config --add fixed.test "$META" &&
 
 	git config --file=config --get fixed.test bogus &&
+	git config get --file=config --value=bogus fixed.test &&
 	test_must_fail git config --file=config --get fixed.test "$META" &&
+	test_must_fail git config get --file=config --value="$META" fixed.test &&
 	git config --file=config --get --fixed-value fixed.test "$META" &&
+	git config get --file=config --fixed-value --value="$META" fixed.test &&
 	test_must_fail git config --file=config --get --fixed-value fixed.test non-existent &&
 
 	git config --file=config --get-all fixed.test bogus &&
+	git config get --all --file=config --value=bogus fixed.test &&
 	test_must_fail git config --file=config --get-all fixed.test "$META" &&
+	test_must_fail git config get --all --file=config --value="$META" fixed.test &&
 	git config --file=config --get-all --fixed-value fixed.test "$META" &&
+	git config get --all --file=config --value="$META" --fixed-value fixed.test &&
 	test_must_fail git config --file=config --get-all --fixed-value fixed.test non-existent &&
 
 	git config --file=config --get-regexp fixed+ bogus &&
+	git config get --regexp --file=config --value=bogus fixed+ &&
 	test_must_fail git config --file=config --get-regexp fixed+ "$META" &&
+	test_must_fail git config get --regexp --file=config --value="$META" fixed+ &&
 	git config --file=config --get-regexp --fixed-value fixed+ "$META" &&
+	git config get --regexp --file=config --fixed-value --value="$META" fixed+ &&
 	test_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent
 '
 
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 09/14] builtin/config: introduce "set" subcommand
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (7 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 08/14] builtin/config: introduce "get" subcommand Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03  9:57   ` [PATCH v4 10/14] builtin/config: introduce "unset" subcommand Patrick Steinhardt
                     ` (6 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 20661 bytes --]

Introduce a new "set" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt |  38 ++++++++-----
 builtin/config.c             |  63 ++++++++++++++++++++++
 t/t1300-config.sh            | 102 ++++++++++++++++++-----------------
 3 files changed, 140 insertions(+), 63 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index d0878663db..f57fa01085 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -11,9 +11,7 @@ SYNOPSIS
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
-'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
-'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
-'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
+'git config set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
@@ -27,7 +25,7 @@ You can query/set/replace/unset options with this command. The name is
 actually the section and the key separated by a dot, and the value will be
 escaped.
 
-Multiple lines can be added to an option by using the `--add` option.
+Multiple lines can be added to an option by using the `--append` option.
 If you want to update or unset an option which can occur on multiple
 lines, a `value-pattern` (which is an extended regular expression,
 unless the `--fixed-value` option is given) needs to be given.  Only the
@@ -82,6 +80,13 @@ get::
 	emits all values associated with key. Returns error code 1 if key is
 	not present.
 
+set::
+	Set value for one or more config options. By default, this command
+	refuses to write multi-valued config options. Passing `--all` will
+	replace all multi-valued config options with the new value, whereas
+	`--value=` will replace all config options whose values match the given
+	pattern.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -90,10 +95,9 @@ OPTIONS
 	Default behavior is to replace at most one line. This replaces
 	all lines matching the key (and optionally the `value-pattern`).
 
---add::
+--append::
 	Adds a new line to the option without altering any existing
-	values.  This is the same as providing '^$' as the `value-pattern`
-	in `--replace-all`.
+	values. This is the same as providing '--value=^$' in `set`.
 
 --comment <message>::
 	Append a comment at the end of new or modified lines.
@@ -296,6 +300,9 @@ recommended to migrate to the new syntax.
 'git config <name>'::
 	Replaced by `git config get <name>`.
 
+'git config <name> <value> [<value-pattern>]'::
+	Replaced by `git config set [--value=<pattern>] <name> <value>`.
+
 -l::
 --list::
 	Replaced by `git config list`.
@@ -315,6 +322,9 @@ recommended to migrate to the new syntax.
 --get-color <name> [<default>]::
 	Replaced by `git config get --type=color [--default=<default>] <name>`.
 
+--add <name> <value>::
+	Replaced by `git config set --append <name> <value>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
@@ -361,7 +371,7 @@ precedence over values read earlier.  When multiple values are taken then all
 values of a key from all files will be used.
 
 By default, options are only written to the repository specific
-configuration file. Note that this also affects options like `--replace-all`
+configuration file. Note that this also affects options like `set`
 and `--unset`. *'git config' will only ever change one file at a time*.
 
 You can limit which configuration sources are read from or written to by
@@ -497,7 +507,7 @@ Given a .git/config like this:
 you can set the filemode to true with
 
 ------------
-% git config core.filemode true
+% git config set core.filemode true
 ------------
 
 The hypothetical proxy command entries actually have a postfix to discern
@@ -505,7 +515,7 @@ what URL they apply to. Here is how to change the entry for kernel.org
 to "ssh".
 
 ------------
-% git config core.gitproxy '"ssh" for kernel.org' 'for kernel.org$'
+% git config set --value='for kernel.org$' core.gitproxy '"ssh" for kernel.org'
 ------------
 
 This makes sure that only the key/value pair for kernel.org is replaced.
@@ -541,26 +551,26 @@ If you like to live dangerously, you can replace *all* core.gitproxy by a
 new one with
 
 ------------
-% git config --replace-all core.gitproxy ssh
+% git config set --all core.gitproxy ssh
 ------------
 
 However, if you really only want to replace the line for the default proxy,
 i.e. the one without a "for ..." postfix, do something like this:
 
 ------------
-% git config core.gitproxy ssh '! for '
+% git config set --value='! for ' core.gitproxy ssh
 ------------
 
 To actually match only values with an exclamation mark, you have to
 
 ------------
-% git config section.key value '[!]'
+% git config set --value='[!]' section.key value
 ------------
 
 To add a new proxy, without altering any of the existing ones, use
 
 ------------
-% git config --add core.gitproxy '"proxy-command" for example.com'
+% git config set --append core.gitproxy '"proxy-command" for example.com'
 ------------
 
 An example to use customized color from the configuration in your
diff --git a/builtin/config.c b/builtin/config.c
index 9e7ae49c02..87fd12fda2 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -18,6 +18,7 @@
 static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
+	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	NULL
 };
 
@@ -31,6 +32,11 @@ static const char *const builtin_config_get_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_set_usage[] = {
+	N_("git config set [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -849,9 +855,66 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix)
 	return get_value(argv[0], value_pattern, flags);
 }
 
+static int cmd_config_set(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL, *comment_arg = NULL;
+	char *comment = NULL;
+	int flags = 0, append = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_TYPE_OPTIONS,
+		OPT_GROUP(N_("Filter")),
+		OPT_BIT(0, "all", &flags, N_("replace multi-valued config option with new value"), CONFIG_FLAGS_MULTI_REPLACE),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_GROUP(N_("Other")),
+		OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
+		OPT_BOOL(0, "append", &append, N_("add a new line without altering any existing values")),
+		OPT_END(),
+	};
+	struct key_value_info default_kvi = KVI_INIT;
+	char *value;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 2, 2);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with --value=<pattern>"));
+	if (append && value_pattern)
+		die(_("--append cannot be used with --value=<pattern>"));
+	if (append)
+		value_pattern = CONFIG_REGEX_NONE;
+
+	comment = git_config_prepare_comment_string(comment_arg);
+
+	handle_config_location(prefix);
+
+	value = normalize_value(argv[0], argv[1], &default_kvi);
+
+	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {
+		ret = git_config_set_multivar_in_file_gently(given_config_source.file,
+							     argv[0], value, value_pattern,
+							     comment, flags);
+	} else {
+		ret = git_config_set_in_file_gently(given_config_source.file,
+						    argv[0], comment, value);
+		if (ret == CONFIG_NOTHING_SET)
+			error(_("cannot overwrite multiple values with a single value\n"
+			"       Use a regexp, --add or --replace-all to change %s."), argv[0]);
+	}
+
+	free(comment);
+	free(value);
+	return ret;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
+	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 7ce13ed6cd..aa3f179be5 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -20,12 +20,16 @@ legacy)
 	mode_get=""
 	mode_get_all="--get-all"
 	mode_get_regexp="--get-regexp"
+	mode_set=""
+	mode_replace_all="--replace-all"
 	;;
 subcommands)
 	mode_prefix=""
 	mode_get="get"
 	mode_get_all="get --all"
 	mode_get_regexp="get --regexp --all --show-names"
+	mode_set="set"
+	mode_replace_all="set --all"
 	;;
 *)
 	BUG "unknown mode $mode";;
@@ -132,7 +136,7 @@ cat > expect << EOF
 	penguin = little blue
 EOF
 test_expect_success 'initial' '
-	git config section.penguin "little blue" &&
+	git config ${mode_set} section.penguin "little blue" &&
 	test_cmp expect .git/config
 '
 
@@ -142,7 +146,7 @@ cat > expect << EOF
 	Movie = BadPhysics
 EOF
 test_expect_success 'mixed case' '
-	git config Section.Movie BadPhysics &&
+	git config ${mode_set} Section.Movie BadPhysics &&
 	test_cmp expect .git/config
 '
 
@@ -154,7 +158,7 @@ cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'similar section' '
-	git config Sections.WhatEver Second &&
+	git config ${mode_set} Sections.WhatEver Second &&
 	test_cmp expect .git/config
 '
 
@@ -167,7 +171,7 @@ cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'uppercase section' '
-	git config SECTION.UPPERCASE true &&
+	git config ${mode_set} SECTION.UPPERCASE true &&
 	test_cmp expect .git/config
 '
 
@@ -194,8 +198,8 @@ EOF
 
 test_expect_success 'append comments' '
 	git config --replace-all --comment="Pygoscelis papua" section.penguin gentoo &&
-	git config --comment="find fish" section.disposition peckish &&
-	git config --comment="#abc" section.foo bar &&
+	git config ${mode_set} --comment="find fish" section.disposition peckish &&
+	git config ${mode_set} --comment="#abc" section.foo bar &&
 
 	git config --comment="and comment" section.spsp value &&
 	git config --comment="	# and comment" section.htsp value &&
@@ -204,7 +208,7 @@ test_expect_success 'append comments' '
 '
 
 test_expect_success 'Prohibited LF in comment' '
-	test_must_fail git config --comment="a${LF}b" section.k v
+	test_must_fail git config ${mode_set} --comment="a${LF}b" section.k v
 '
 
 test_expect_success 'non-match result' 'test_cmp expect .git/config'
@@ -301,14 +305,14 @@ test_expect_success 'multiple unset is correct' '
 cp .git/config2 .git/config
 
 test_expect_success '--replace-all missing value' '
-	test_must_fail git config --replace-all beta.haha &&
+	test_must_fail git config ${mode_replace_all} beta.haha &&
 	test_cmp .git/config2 .git/config
 '
 
 rm .git/config2
 
 test_expect_success '--replace-all' '
-	git config --replace-all beta.haha gamma
+	git config ${mode_replace_all} beta.haha gamma
 '
 
 cat > expect << EOF
@@ -335,7 +339,7 @@ noIndent= sillyValue ; 'nother silly comment
 [nextSection] noNewline = ouch
 EOF
 test_expect_success 'really mean test' '
-	git config beta.haha alpha &&
+	git config ${mode_set} beta.haha alpha &&
 	test_cmp expect .git/config
 '
 
@@ -350,7 +354,7 @@ noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'really really mean test' '
-	git config nextsection.nonewline wow &&
+	git config ${mode_set} nextsection.nonewline wow &&
 	test_cmp expect .git/config
 '
 
@@ -824,16 +828,16 @@ EOF
 
 test_expect_success 'section ending' '
 	rm -f .git/config &&
-	git config gitcvs.enabled true &&
-	git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
-	git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.enabled true &&
+	git config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
 	test_cmp expect .git/config
 
 '
 
 test_expect_success numbers '
-	git config kilo.gram 1k &&
-	git config mega.ton 1m &&
+	git config ${mode_set} kilo.gram 1k &&
+	git config ${mode_set} mega.ton 1m &&
 	echo 1024 >expect &&
 	echo 1048576 >>expect &&
 	git config --int --get kilo.gram >actual &&
@@ -842,20 +846,20 @@ test_expect_success numbers '
 '
 
 test_expect_success '--int is at least 64 bits' '
-	git config giga.watts 121g &&
+	git config ${mode_set} giga.watts 121g &&
 	echo  >expect &&
 	test_cmp_config 129922760704 --int --get giga.watts
 '
 
 test_expect_success 'invalid unit' '
-	git config aninvalid.unit "1auto" &&
+	git config ${mode_set} aninvalid.unit "1auto" &&
 	test_cmp_config 1auto aninvalid.unit &&
 	test_must_fail git config --int --get aninvalid.unit 2>actual &&
 	test_grep "bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit" actual
 '
 
 test_expect_success 'invalid unit boolean' '
-	git config commit.gpgsign "1true" &&
+	git config ${mode_set} commit.gpgsign "1true" &&
 	test_cmp_config 1true commit.gpgsign &&
 	test_must_fail git config --bool --get commit.gpgsign 2>actual &&
 	test_grep "bad boolean config value .1true. for .commit.gpgsign." actual
@@ -885,14 +889,14 @@ EOF
 
 test_expect_success bool '
 
-	git config bool.true1 01 &&
-	git config bool.true2 -1 &&
-	git config bool.true3 YeS &&
-	git config bool.true4 true &&
-	git config bool.false1 000 &&
-	git config bool.false2 "" &&
-	git config bool.false3 nO &&
-	git config bool.false4 FALSE &&
+	git config ${mode_set} bool.true1 01 &&
+	git config ${mode_set} bool.true2 -1 &&
+	git config ${mode_set} bool.true3 YeS &&
+	git config ${mode_set} bool.true4 true &&
+	git config ${mode_set} bool.false1 000 &&
+	git config ${mode_set} bool.false2 "" &&
+	git config ${mode_set} bool.false3 nO &&
+	git config ${mode_set} bool.false4 FALSE &&
 	rm -f result &&
 	for i in 1 2 3 4
 	do
@@ -903,7 +907,7 @@ test_expect_success bool '
 
 test_expect_success 'invalid bool (--get)' '
 
-	git config bool.nobool foobar &&
+	git config ${mode_set} bool.nobool foobar &&
 	test_must_fail git config --bool --get bool.nobool'
 
 test_expect_success 'invalid bool (set)' '
@@ -1092,7 +1096,7 @@ test_expect_success 'get --expiry-date' '
 
 test_expect_success 'get --type=color' '
 	rm .git/config &&
-	git config foo.color "red" &&
+	git config ${mode_set} foo.color "red" &&
 	git config --get --type=color foo.color >actual.raw &&
 	test_decode_color <actual.raw >actual &&
 	echo "<RED>" >expect &&
@@ -1129,10 +1133,10 @@ cat > expect << EOF
 EOF
 test_expect_success 'quoting' '
 	rm -f .git/config &&
-	git config quote.leading " test" &&
-	git config quote.ending "test " &&
-	git config quote.semicolon "test;test" &&
-	git config quote.hash "test#test" &&
+	git config ${mode_set} quote.leading " test" &&
+	git config ${mode_set} quote.ending "test " &&
+	git config ${mode_set} quote.semicolon "test;test" &&
+	git config ${mode_set} quote.hash "test#test" &&
 	test_cmp expect .git/config
 '
 
@@ -1140,7 +1144,7 @@ test_expect_success 'key with newline' '
 	test_must_fail git config ${mode_get} "key.with
 newline" 123'
 
-test_expect_success 'value with newline' 'git config key.sub value.with\\\
+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\
 newline'
 
 cat > .git/config <<\EOF
@@ -1199,21 +1203,21 @@ test_expect_success '--null --get-regexp' '
 
 test_expect_success 'inner whitespace kept verbatim, spaces only' '
 	echo "foo   bar" >expect &&
-	git config section.val "foo   bar" &&
+	git config ${mode_set} section.val "foo   bar" &&
 	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '
 	echo "fooQQbar" | q_to_tab >expect &&
-	git config section.val "$(cat expect)" &&
+	git config ${mode_set} section.val "$(cat expect)" &&
 	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '
 	echo "foo Q  bar" | q_to_tab >expect &&
-	git config section.val "$(cat expect)" &&
+	git config ${mode_set} section.val "$(cat expect)" &&
 	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
@@ -1252,12 +1256,12 @@ test_expect_success 'check split_cmdline return' '
 	git init repo &&
 	(
 		cd repo &&
-		git config alias.split-cmdline-fix "echo \"" &&
+		git config ${mode_set} alias.split-cmdline-fix "echo \"" &&
 		test_must_fail git split-cmdline-fix &&
 		echo foo >foo &&
 		git add foo &&
 		git commit -m "initial commit" &&
-		git config branch.main.mergeoptions "echo \"" &&
+		git config ${mode_set} branch.main.mergeoptions "echo \"" &&
 		test_must_fail git merge main
 	)
 '
@@ -1295,12 +1299,12 @@ test_expect_success 'key sanity-checking' '
 	test_must_fail git config ${mode_get} foo.1bar &&
 	test_must_fail git config ${mode_get} foo."ba
 				z".bar &&
-	test_must_fail git config . false &&
-	test_must_fail git config .foo false &&
-	test_must_fail git config foo. false &&
-	test_must_fail git config .foo. false &&
-	git config foo.bar true &&
-	git config foo."ba =z".bar false
+	test_must_fail git config ${mode_set} . false &&
+	test_must_fail git config ${mode_set} .foo false &&
+	test_must_fail git config ${mode_set} foo. false &&
+	test_must_fail git config ${mode_set} .foo. false &&
+	git config ${mode_set} foo.bar true &&
+	git config ${mode_set} foo."ba =z".bar false
 '
 
 test_expect_success 'git -c works with aliases of builtins' '
@@ -2523,7 +2527,7 @@ test_expect_success '--replace-all does not invent newlines' '
 	[abc]
 	Qkey = b
 	EOF
-	git config --replace-all abc.key b &&
+	git config ${mode_replace_all} abc.key b &&
 	test_cmp expect .git/config
 '
 
@@ -2595,8 +2599,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
 
 	# These modes complain when --fixed-value has no value-pattern
-	test_must_fail git config --file=config --fixed-value dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
+	test_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&
+	test_must_fail git config ${mode_replace_all} --file=config --fixed-value dev.null bogus &&
 	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
@@ -2637,7 +2641,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
-	git config --file=config --replace-all fixed.test bogus "$META" &&
+	git config --file=config fixed.test bogus "$META" &&
 	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 10/14] builtin/config: introduce "unset" subcommand
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (8 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 09/14] builtin/config: introduce "set" subcommand Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03  9:57   ` [PATCH v4 11/14] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
                     ` (5 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 11914 bytes --]

Introduce a new "unset" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 27 +++++++++++++---------
 builtin/config.c             | 39 ++++++++++++++++++++++++++++++++
 t/t1300-config.sh            | 44 ++++++++++++++++++++++++------------
 3 files changed, 84 insertions(+), 26 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index f57fa01085..5575fe55ea 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -11,9 +11,8 @@ SYNOPSIS
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
-'git config set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>
-'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
-'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
+'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
+'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
@@ -87,6 +86,12 @@ set::
 	`--value=` will replace all config options whose values match the given
 	pattern.
 
+unset::
+	Unset value for one or more config options. By default, this command
+	refuses to unset multi-valued keys. Passing `--all` will unset all
+	multi-valued config options, whereas `--value` will unset all config
+	options whose values match the given pattern.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -190,12 +195,6 @@ See also <<FILES>>.
 --rename-section::
 	Rename the given section to a new name.
 
---unset::
-	Remove the line matching the key from config file.
-
---unset-all::
-	Remove all lines matching the key from config file.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -325,6 +324,12 @@ recommended to migrate to the new syntax.
 --add <name> <value>::
 	Replaced by `git config set --append <name> <value>`.
 
+--unset <name> [<value-pattern>]::
+	Replaced by `git config unset [--value=<pattern>] <name>`.
+
+--unset-all <name> [<value-pattern>]::
+	Replaced by `git config unset [--value=<pattern>] --all <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
@@ -372,7 +377,7 @@ values of a key from all files will be used.
 
 By default, options are only written to the repository specific
 configuration file. Note that this also affects options like `set`
-and `--unset`. *'git config' will only ever change one file at a time*.
+and `unset`. *'git config' will only ever change one file at a time*.
 
 You can limit which configuration sources are read from or written to by
 specifying the path of a file with the `--file` option, or by specifying a
@@ -523,7 +528,7 @@ This makes sure that only the key/value pair for kernel.org is replaced.
 To delete the entry for renames, do
 
 ------------
-% git config --unset diff.renames
+% git config unset diff.renames
 ------------
 
 If you want to delete an entry for a multivar (like core.gitproxy above),
diff --git a/builtin/config.c b/builtin/config.c
index 87fd12fda2..2578d4c930 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -19,6 +19,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	NULL
 };
 
@@ -37,6 +38,11 @@ static const char *const builtin_config_set_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_unset_usage[] = {
+	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -911,10 +917,43 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix)
 	return ret;
 }
 
+static int cmd_config_unset(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL;
+	int flags = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_GROUP(N_("Filter")),
+		OPT_BIT(0, "all", &flags, N_("replace multi-valued config option with new value"), CONFIG_FLAGS_MULTI_REPLACE),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_unset_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 1, 1);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with 'value-pattern'"));
+
+	handle_config_location(prefix);
+
+	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)
+		return git_config_set_multivar_in_file_gently(given_config_source.file,
+							      argv[0], NULL, value_pattern,
+							      NULL, flags);
+	else
+		return git_config_set_in_file_gently(given_config_source.file, argv[0],
+						     NULL, NULL);
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
+	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index aa3f179be5..2ab58bbd95 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -22,6 +22,8 @@ legacy)
 	mode_get_regexp="--get-regexp"
 	mode_set=""
 	mode_replace_all="--replace-all"
+	mode_unset="--unset"
+	mode_unset_all="--unset-all"
 	;;
 subcommands)
 	mode_prefix=""
@@ -30,6 +32,8 @@ subcommands)
 	mode_get_regexp="get --regexp --all --show-names"
 	mode_set="set"
 	mode_replace_all="set --all"
+	mode_unset="unset"
+	mode_unset_all="unset --all"
 	;;
 *)
 	BUG "unknown mode $mode";;
@@ -259,7 +263,7 @@ foo = bar
 EOF
 
 test_expect_success 'unset with cont. lines' '
-	git config --unset beta.baz
+	git config ${mode_unset} beta.baz
 '
 
 cat > expect <<\EOF
@@ -286,7 +290,7 @@ EOF
 cp .git/config .git/config2
 
 test_expect_success 'multiple unset' '
-	git config --unset-all beta.haha
+	git config ${mode_unset_all} beta.haha
 '
 
 cat > expect << EOF
@@ -372,7 +376,7 @@ noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'unset' '
-	git config --unset beta.haha &&
+	git config ${mode_unset} beta.haha &&
 	test_cmp expect .git/config
 '
 
@@ -428,11 +432,11 @@ test_expect_success 'multivar replace' '
 '
 
 test_expect_success 'ambiguous unset' '
-	test_must_fail git config --unset nextsection.nonewline
+	test_must_fail git config ${mode_unset} nextsection.nonewline
 '
 
 test_expect_success 'invalid unset' '
-	test_must_fail git config --unset somesection.nonewline
+	test_must_fail git config ${mode_unset} somesection.nonewline
 '
 
 cat > expect << EOF
@@ -446,7 +450,12 @@ noIndent= sillyValue ; 'nother silly comment
 EOF
 
 test_expect_success 'multivar unset' '
-	git config --unset nextsection.nonewline "wow3$" &&
+	case "$mode" in
+	legacy)
+		git config --unset nextsection.nonewline "wow3$";;
+	subcommands)
+		git config unset --value="wow3$" nextsection.nonewline;;
+	esac &&
 	test_cmp expect .git/config
 '
 
@@ -2013,7 +2022,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	# please be careful when you update the above variable
 	EOF
 
-	git config --unset section.key &&
+	git config ${mode_unset} section.key &&
 	test_cmp expect .git/config &&
 
 	cat >.git/config <<-\EOF &&
@@ -2026,7 +2035,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[next-section]
 	EOF
 
-	git config --unset section.key &&
+	git config ${mode_unset} section.key &&
 	test_cmp expect .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -2036,7 +2045,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[two]
 	key = true
 	EOF
-	git config --unset two.key &&
+	git config ${mode_unset} two.key &&
 	! grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -2046,7 +2055,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[one]
 	key = true
 	EOF
-	git config --unset-all one.key &&
+	git config ${mode_unset_all} one.key &&
 	test_line_count = 0 .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -2056,7 +2065,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[two]
 	Qkey = true
 	EOF
-	git config --unset two.key &&
+	git config ${mode_unset} two.key &&
 	grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -2068,7 +2077,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[TWO "subsection"]
 	[one]
 	EOF
-	git config --unset two.subsection.key &&
+	git config ${mode_unset} two.subsection.key &&
 	test "not [two subsection]" = "$(git config ${mode_get} one.key)" &&
 	test_line_count = 3 .git/config
 '
@@ -2080,7 +2089,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' '
 	key = value2
 	EOF
 
-	git config --unset-all section.key &&
+	git config ${mode_unset_all} section.key &&
 	test_line_count = 0 .git/config
 '
 
@@ -2604,8 +2613,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
-	test_must_fail git config --file=config --fixed-value --unset dev.null &&
-	test_must_fail git config --file=config --fixed-value --unset-all dev.null
+	test_must_fail git config ${mode_unset} --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_unset_all} --file=config --fixed-value dev.null
 '
 
 test_expect_success '--fixed-value uses exact string matching' '
@@ -2635,6 +2644,11 @@ test_expect_success '--fixed-value uses exact string matching' '
 	git config --file=config --fixed-value --unset fixed.test "$META" &&
 	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
+	cp initial config &&
+	test_must_fail git config unset --file=config --value="$META" fixed.test &&
+	git config unset --file=config --fixed-value --value="$META" fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
+
 	cp initial config &&
 	test_must_fail git config --file=config --unset-all fixed.test "$META" &&
 	git config --file=config --fixed-value --unset-all fixed.test "$META" &&
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 11/14] builtin/config: introduce "rename-section" subcommand
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (9 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 10/14] builtin/config: introduce "unset" subcommand Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03  9:57   ` [PATCH v4 12/14] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
                     ` (4 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 8443 bytes --]

Introduce a new "rename-section" subcommand to git-config(1). Please
refer to preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 11 +++++++----
 builtin/config.c             | 32 ++++++++++++++++++++++++++++++++
 t/t1300-config.sh            | 22 +++++++++++-----------
 3 files changed, 50 insertions(+), 15 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 5575fe55ea..ede86ad085 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -13,7 +13,7 @@ SYNOPSIS
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
-'git config' [<file-option>] --rename-section <old-name> <new-name>
+'git config rename-section' [<file-option>] <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
@@ -92,6 +92,9 @@ unset::
 	multi-valued config options, whereas `--value` will unset all config
 	options whose values match the given pattern.
 
+rename-section::
+	Rename the given section to a new name.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -192,9 +195,6 @@ See also <<FILES>>.
 --remove-section::
 	Remove the given section from the configuration file.
 
---rename-section::
-	Rename the given section to a new name.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -330,6 +330,9 @@ recommended to migrate to the new syntax.
 --unset-all <name> [<value-pattern>]::
 	Replaced by `git config unset [--value=<pattern>] --all <name>`.
 
+--rename-section <old-name> <new-name>::
+	Replaced by `git config rename-section <old-name> <new-name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index 2578d4c930..a60dca9b2b 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -20,6 +20,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	NULL
 };
 
@@ -43,6 +44,11 @@ static const char *const builtin_config_unset_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_rename_section_usage[] = {
+	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -949,11 +955,37 @@ static int cmd_config_unset(int argc, const char **argv, const char *prefix)
 						     NULL, NULL);
 }
 
+static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_rename_section_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 2, 2);
+
+	handle_config_location(prefix);
+
+	ret = git_config_rename_section_in_file(given_config_source.file,
+						argv[0], argv[1]);
+	if (ret < 0)
+		return ret;
+	else if (!ret)
+		die(_("no such section: %s"), argv[0]);
+
+	return 0;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
+	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 2ab58bbd95..5d7b08fa4c 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -699,7 +699,7 @@ weird
 EOF
 
 test_expect_success 'rename section' '
-	git config --rename-section branch.eins branch.zwei
+	git config ${mode_prefix}rename-section branch.eins branch.zwei
 '
 
 cat > expect << EOF
@@ -718,7 +718,7 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'rename non-existing section' '
-	test_must_fail git config --rename-section \
+	test_must_fail git config ${mode_prefix}rename-section \
 		branch."world domination" branch.drei
 '
 
@@ -727,7 +727,7 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'rename another section' '
-	git config --rename-section branch."1 234 blabl/a" branch.drei
+	git config ${mode_prefix}rename-section branch."1 234 blabl/a" branch.drei
 '
 
 cat > expect << EOF
@@ -750,7 +750,7 @@ cat >> .git/config << EOF
 EOF
 
 test_expect_success 'rename a section with a var on the same line' '
-	git config --rename-section branch.vier branch.zwei
+	git config ${mode_prefix}rename-section branch.vier branch.zwei
 '
 
 cat > expect << EOF
@@ -771,11 +771,11 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'renaming empty section name is rejected' '
-	test_must_fail git config --rename-section branch.zwei ""
+	test_must_fail git config ${mode_prefix}rename-section branch.zwei ""
 '
 
 test_expect_success 'renaming to bogus section is rejected' '
-	test_must_fail git config --rename-section branch.zwei "bogus name"
+	test_must_fail git config ${mode_prefix}rename-section branch.zwei "bogus name"
 '
 
 test_expect_success 'renaming a section with a long line' '
@@ -784,7 +784,7 @@ test_expect_success 'renaming a section with a long line' '
 		printf "  c = d %1024s [a] e = f\\n" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	git config -f y --rename-section a xyz &&
+	git config ${mode_prefix}rename-section -f y a xyz &&
 	test_must_fail git config -f y b.e
 '
 
@@ -794,7 +794,7 @@ test_expect_success 'renaming an embedded section with a long line' '
 		printf "  c = d %1024s [a] [foo] e = f\\n" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	git config -f y --rename-section a xyz &&
+	git config ${mode_prefix}rename-section -f y a xyz &&
 	test_must_fail git config -f y foo.e
 '
 
@@ -804,7 +804,7 @@ test_expect_success 'renaming a section with an overly-long line' '
 		printf "  c = d %525000s e" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	test_must_fail git config -f y --rename-section a xyz 2>err &&
+	test_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&
 	grep "refusing to work with overly long line in .y. on line 2" err
 '
 
@@ -2112,7 +2112,7 @@ test_expect_success POSIXPERM,PERL 'preserves existing permissions' '
 	git config imap.pass Hunter2 &&
 	perl -e \
 	  "die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600" &&
-	git config --rename-section imap pop &&
+	git config ${mode_prefix}rename-section imap pop &&
 	perl -e \
 	  "die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600"
 '
@@ -2601,7 +2601,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --add dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --rename-section dev null &&
+	test_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&
 	test_must_fail git config --file=config --fixed-value --remove-section dev &&
 	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 12/14] builtin/config: introduce "remove-section" subcommand
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (10 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 11/14] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03  9:57   ` [PATCH v4 13/14] builtin/config: introduce "edit" subcommand Patrick Steinhardt
                     ` (3 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 5327 bytes --]

Introduce a new "remove-section" subcommand to git-config(1). Please
refer to preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 11 +++++++----
 builtin/config.c             | 32 ++++++++++++++++++++++++++++++++
 t/t1300-config.sh            |  4 ++--
 3 files changed, 41 insertions(+), 6 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index ede86ad085..ef46520c15 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -14,7 +14,7 @@ SYNOPSIS
 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config rename-section' [<file-option>] <old-name> <new-name>
-'git config' [<file-option>] --remove-section <name>
+'git config remove-section' [<file-option>] <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
 
@@ -95,6 +95,9 @@ unset::
 rename-section::
 	Rename the given section to a new name.
 
+remove-section::
+	Remove the given section from the configuration file.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -192,9 +195,6 @@ See also <<FILES>>.
 	section in linkgit:gitrevisions[7] for a more complete list of
 	ways to spell blob names.
 
---remove-section::
-	Remove the given section from the configuration file.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -333,6 +333,9 @@ recommended to migrate to the new syntax.
 --rename-section <old-name> <new-name>::
 	Replaced by `git config rename-section <old-name> <new-name>`.
 
+--remove-section <name>::
+	Replaced by `git config remove-section <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index a60dca9b2b..cea03fb517 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -21,6 +21,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
+	N_("git config remove-section [<file-option>] <name>"),
 	NULL
 };
 
@@ -49,6 +50,11 @@ static const char *const builtin_config_rename_section_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_remove_section_usage[] = {
+	N_("git config remove-section [<file-option>] <name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -980,12 +986,38 @@ static int cmd_config_rename_section(int argc, const char **argv, const char *pr
 	return 0;
 }
 
+static int cmd_config_remove_section(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_remove_section_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 1, 1);
+
+	handle_config_location(prefix);
+
+	ret = git_config_rename_section_in_file(given_config_source.file,
+						argv[0], NULL);
+	if (ret < 0)
+		return ret;
+	else if (!ret)
+		die(_("no such section: %s"), argv[0]);
+
+	return 0;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
+	OPT_SUBCOMMAND("remove-section", &subcommand, cmd_config_remove_section),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 5d7b08fa4c..9d71ec1260 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -813,7 +813,7 @@ cat >> .git/config << EOF
 EOF
 
 test_expect_success 'remove section' '
-	git config --remove-section branch.zwei
+	git config ${mode_prefix}remove-section branch.zwei
 '
 
 cat > expect << EOF
@@ -2602,7 +2602,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&
-	test_must_fail git config --file=config --fixed-value --remove-section dev &&
+	test_must_fail git config ${mode_prefix}remove-section --file=config --fixed-value dev &&
 	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 13/14] builtin/config: introduce "edit" subcommand
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (11 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 12/14] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03  9:57   ` [PATCH v4 14/14] builtin/config: display subcommand help Patrick Steinhardt
                     ` (2 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 7245 bytes --]

Introduce a new "edit" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 17 ++++----
 builtin/config.c             | 81 ++++++++++++++++++++++++------------
 t/t1300-config.sh            |  6 +--
 3 files changed, 68 insertions(+), 36 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index ef46520c15..65c645d461 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -15,8 +15,8 @@ SYNOPSIS
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config rename-section' [<file-option>] <old-name> <new-name>
 'git config remove-section' [<file-option>] <name>
+'git config edit' [<file-option>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
-'git config' [<file-option>] -e | --edit
 
 DESCRIPTION
 -----------
@@ -98,6 +98,11 @@ rename-section::
 remove-section::
 	Remove the given section from the configuration file.
 
+edit::
+	Opens an editor to modify the specified config file; either
+	`--system`, `--global`, `--local` (default), `--worktree`, or
+	`--file <config-file>`.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -274,12 +279,6 @@ Valid `<type>`'s include:
 	When the color setting for `name` is undefined, the command uses
 	`color.ui` as fallback.
 
--e::
---edit::
-	Opens an editor to modify the specified config file; either
-	`--system`, `--global`, `--local` (default), `--worktree`, or
-	`--file <config-file>`.
-
 --[no-]includes::
 	Respect `include.*` directives in config files when looking up
 	values. Defaults to `off` when a specific file is given (e.g.,
@@ -336,6 +335,10 @@ recommended to migrate to the new syntax.
 --remove-section <name>::
 	Replaced by `git config remove-section <name>`.
 
+-e::
+--edit::
+	Replaced by `git config edit`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index cea03fb517..8f7fa8f31a 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -22,6 +22,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	N_("git config remove-section [<file-option>] <name>"),
+	N_("git config edit [<file-option>]"),
 	NULL
 };
 
@@ -55,6 +56,11 @@ static const char *const builtin_config_remove_section_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_edit_usage[] = {
+	N_("git config edit [<file-option>]"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -1011,6 +1017,53 @@ static int cmd_config_remove_section(int argc, const char **argv, const char *pr
 	return 0;
 }
 
+static int show_editor(void)
+{
+	char *config_file;
+
+	if (!given_config_source.file && !startup_info->have_repository)
+		die(_("not in a git directory"));
+	if (given_config_source.use_stdin)
+		die(_("editing stdin is not supported"));
+	if (given_config_source.blob)
+		die(_("editing blobs is not supported"));
+	git_config(git_default_config, NULL);
+	config_file = given_config_source.file ?
+			xstrdup(given_config_source.file) :
+			git_pathdup("config");
+	if (use_global_config) {
+		int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);
+		if (fd >= 0) {
+			char *content = default_user_config();
+			write_str_in_full(fd, content);
+			free(content);
+			close(fd);
+		}
+		else if (errno != EEXIST)
+			die_errno(_("cannot create configuration file %s"), config_file);
+	}
+	launch_editor(config_file, NULL, NULL);
+	free(config_file);
+
+	return 0;
+}
+
+static int cmd_config_edit(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_edit_usage, 0);
+	check_write();
+	check_argc(argc, 0, 0);
+
+	handle_config_location(prefix);
+
+	return show_editor();
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
@@ -1018,6 +1071,7 @@ static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
 	OPT_SUBCOMMAND("remove-section", &subcommand, cmd_config_remove_section),
+	OPT_SUBCOMMAND("edit", &subcommand, cmd_config_edit),
 	OPT_END(),
 };
 
@@ -1144,32 +1198,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		}
 	}
 	else if (actions == ACTION_EDIT) {
-		char *config_file;
-
-		check_argc(argc, 0, 0);
-		if (!given_config_source.file && !startup_info->have_repository)
-			die(_("not in a git directory"));
-		if (given_config_source.use_stdin)
-			die(_("editing stdin is not supported"));
-		if (given_config_source.blob)
-			die(_("editing blobs is not supported"));
-		git_config(git_default_config, NULL);
-		config_file = given_config_source.file ?
-				xstrdup(given_config_source.file) :
-				git_pathdup("config");
-		if (use_global_config) {
-			int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);
-			if (fd >= 0) {
-				char *content = default_user_config();
-				write_str_in_full(fd, content);
-				free(content);
-				close(fd);
-			}
-			else if (errno != EEXIST)
-				die_errno(_("cannot create configuration file %s"), config_file);
-		}
-		launch_editor(config_file, NULL, NULL);
-		free(config_file);
+		ret = show_editor();
 	}
 	else if (actions == ACTION_SET) {
 		check_write();
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 9d71ec1260..f3c4d28e06 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -666,7 +666,7 @@ test_expect_success 'setting a value in stdin is an error' '
 '
 
 test_expect_success 'editing stdin is an error' '
-	test_must_fail git config --file - --edit
+	test_must_fail git config ${mode_prefix}edit --file -
 '
 
 test_expect_success 'refer config from subdirectory' '
@@ -1768,7 +1768,7 @@ test_expect_success 'command line overrides environment config' '
 test_expect_success 'git config --edit works' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
-	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
+	GIT_EDITOR="echo [test]value=yes >" git config ${mode_prefix}edit -f tmp &&
 	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
@@ -1777,7 +1777,7 @@ test_expect_success 'git config --edit respects core.editor' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
 	test_config core.editor "echo [test]value=yes >" &&
-	git config -f tmp --edit &&
+	git config ${mode_prefix}edit -f tmp &&
 	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v4 14/14] builtin/config: display subcommand help
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (12 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 13/14] builtin/config: introduce "edit" subcommand Patrick Steinhardt
@ 2024-05-03  9:57   ` Patrick Steinhardt
  2024-05-03 13:36   ` [PATCH v4 00/14] builtin/config: introduce subcommands Dragan Simic
  2024-05-03 16:09   ` Junio C Hamano
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-03  9:57 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 2170 bytes --]

Until now, `git config -h` would have printed help for the old-style
syntax. Now that all modes have proper subcommands though it is
preferable to instead display the subcommand help.

Drop the `NO_INTERNAL_HELP` flag to do so. While at it, drop the help
mismatch in t0450 and add the `--get-colorbool` option to the usage such
that git-config(1)'s synopsis and `git config -h` match.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c            | 5 +++--
 t/t0450/txt-help-mismatches | 1 -
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 8f7fa8f31a..80aa9d8a66 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -23,6 +23,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	N_("git config remove-section [<file-option>] <name>"),
 	N_("git config edit [<file-option>]"),
+	N_("git config [<file-option>] --get-colorbool <name> [<stdout-is-tty>]"),
 	NULL
 };
 
@@ -1093,10 +1094,10 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 	 * us when parsing the legacy-style modes that don't use subcommands.
 	 */
 	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
-			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
 	if (subcommand) {
 		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
-		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);
+		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);
 		return subcommand(argc, argv, prefix);
 	}
 
diff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches
index a0777acd66..28003f18c9 100644
--- a/t/t0450/txt-help-mismatches
+++ b/t/t0450/txt-help-mismatches
@@ -10,7 +10,6 @@ checkout
 checkout-index
 clone
 column
-config
 credential
 credential-cache
 credential-store
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v4 01/14] config: clarify memory ownership when preparing comment strings
  2024-05-03  9:56   ` [PATCH v4 01/14] config: clarify memory ownership when preparing comment strings Patrick Steinhardt
@ 2024-05-03 10:13     ` Kristoffer Haugsbakk
  0 siblings, 0 replies; 113+ messages in thread
From: Kristoffer Haugsbakk @ 2024-05-03 10:13 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: Taylor Blau, Jean-Noël AVILA, Eric Sunshine, Junio C Hamano, git

On Fri, May 3, 2024, at 11:56, Patrick Steinhardt wrote:
> The ownership of memory returned when preparing a comment string is
> quite intricate: when the returned value is different than the passed
> value, then the caller is responsible to free the memory. This is quite
> subtle, and it's even easier to miss because the returned value is in
> fact a `const char *`.
>
> Adapt the function to always return either `NULL` or a newly allocated
> string. The function is called at most once per git-config(1), so it's
> not like this micro-optimization really matters. Thus, callers are now
> always responsible for freeing the value.
> ---

Missing a signoff on this one. The only one in the series.

-- 
Kristoffer Haugsbakk

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

* Re: [PATCH v4 03/14] builtin/config: move "fixed-value" option to correct group
  2024-05-03  9:56   ` [PATCH v4 03/14] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
@ 2024-05-03 12:28     ` Karthik Nayak
  2024-05-06  9:34       ` Patrick Steinhardt
  0 siblings, 1 reply; 113+ messages in thread
From: Karthik Nayak @ 2024-05-03 12:28 UTC (permalink / raw)
  To: Patrick Steinhardt, git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 2125 bytes --]

Patrick Steinhardt <ps@pks.im> writes:

> The `--fixed-value` option can be used to alter how the value-pattern
> parameter is interpreted for the various submodes of git-config(1). But
> while it is an option, it is currently listed as part of the submodes
> group the command, which is wrong.
>

Isn't it currently listed as part of the 'Action' group? Also I'm not
sure if the last sentence needs to be corrected here.

>
> Move the option to the "Other" group, which hosts the various options
> known to git-config(1).
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>  builtin/config.c | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/builtin/config.c b/builtin/config.c
> index 59ae5996eb..054019b70c 100644
> --- a/builtin/config.c
> +++ b/builtin/config.c
> @@ -643,7 +643,6 @@ static struct option builtin_config_options[] = {
>  	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
>  	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
>  	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
> -	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
>  	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
>  	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
>  	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
> @@ -663,6 +662,7 @@ static struct option builtin_config_options[] = {
>  	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
>  	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
>  	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
> +	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
>  	OPT_END(),
>  };
>
> --
> 2.45.0

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]

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

* Re: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-03  9:57   ` [PATCH v4 07/14] builtin/config: introduce "list" subcommand Patrick Steinhardt
@ 2024-05-03 13:08     ` Karthik Nayak
  2024-05-03 13:13       ` rsbecker
  2024-05-06  7:58       ` Patrick Steinhardt
  0 siblings, 2 replies; 113+ messages in thread
From: Karthik Nayak @ 2024-05-03 13:08 UTC (permalink / raw)
  To: Patrick Steinhardt, git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 28916 bytes --]

Patrick Steinhardt <ps@pks.im> writes:

> While git-config(1) has several modes, those modes are not exposed with
> subcommands but instead by specifying e.g. `--unset` or `--list`. This

s/specifying/specifying flags/ perhaps?

> user interface is not really in line with how our more modern commands
> work, where it is a lot more customary to say e.g. `git remote list`.

Tangent: I totally agree with the patch, but it would be nice to have a
'DesigningCommands' document which would highlight UX do's and don'ts.
It would be nice to add that as reference in discussions.

> Furthermore, to add to the confusion, git-config(1) also allows the user
> to request modes implicitly by just specifying the correct number of
> arguments. Thus, `git config foo.bar` will retrieve the value of
> "foo.bar" while `git config foo.bar baz` will set it to "baz".
>
> Overall, this makes for a confusing interface that could really use a
> makeover. It hurts discoverability of what you can do with git-config(1)
> and is comparatively easy to get wrong. Converting the command to have
> subcommands instead would go a long way to help address these issues.
>
> One concern in this context is backwards compatibility. Luckily, we can
> introduce subcommands without breaking backwards compatibility at all.
> This is because all the implicit modes of git-config(1) require that the
> first argument is a properly formatted config key. And as config keys
> _must_ have a dot in their name, any value without a dot would have been
> discarded by git-config(1) previous to this change. Thus, given that
> none of the subcommands do have a dot, they are unambiguous.
>
> Introduce the first such new subcommand, which is "git config list". To
> retain backwards compatibility we only conditionally use subcommands and
> will fall back to the old syntax in case no subcommand was detected.
> This should help to transition to the new-style syntax until we
> eventually deprecate and remove the old-style syntax.
>
> Note that the way we handle this we're duplicating some functionality
> across old and new syntax. While this isn't pretty, it helps us to
> ensure that there really is no change in behaviour for the old syntax.
>
> Amend tests such that we run them both with old and new style syntax.
> As tests are now run twice, state from the first run may be still be
> around in the second run and thus cause tests to fail. Add cleanup logic
> as required to fix such tests.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>  Documentation/git-config.txt |  26 ++++++---
>  builtin/config.c             |  90 ++++++++++++++++++++++++----
>  t/t1300-config.sh            | 110 +++++++++++++++++++++--------------
>  3 files changed, 162 insertions(+), 64 deletions(-)
>
> diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
> index ac61113fcc..c83c97cb7e 100644
> --- a/Documentation/git-config.txt
> +++ b/Documentation/git-config.txt
> @@ -9,6 +9,7 @@ git-config - Get and set repository or global options
>  SYNOPSIS
>  --------
>  [verse]
> +'git config list' [<file-option>] [<display-option>] [--includes]
>  'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
>  'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
>  'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
> @@ -20,7 +21,6 @@ SYNOPSIS
>  'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
>  'git config' [<file-option>] --rename-section <old-name> <new-name>
>  'git config' [<file-option>] --remove-section <name>
> -'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list
>  'git config' [<file-option>] --get-color <name> [<default>]
>  'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
>  'git config' [<file-option>] -e | --edit
> @@ -74,6 +74,12 @@ On success, the command returns the exit code 0.
>  A list of all available configuration variables can be obtained using the
>  `git help --config` command.
>
> +COMMANDS
> +--------
> +
> +list::
> +	List all variables set in config file, along with their values.
> +
>  [[OPTIONS]]
>  OPTIONS
>  -------
> @@ -190,10 +196,6 @@ See also <<FILES>>.
>  --unset-all::
>  	Remove all lines matching the key from config file.
>
> --l::
> ---list::
> -	List all variables set in config file, along with their values.
> -
>  --fixed-value::
>  	When used with the `value-pattern` argument, treat `value-pattern` as
>  	an exact string instead of a regular expression. This will restrict
> @@ -248,7 +250,7 @@ Valid `<type>`'s include:
>  	contain line breaks.
>
>  --name-only::
> -	Output only the names of config variables for `--list` or
> +	Output only the names of config variables for `list` or
>  	`--get-regexp`.
>
>  --show-origin::
> @@ -300,10 +302,20 @@ Valid `<type>`'s include:
>    When using `--get`, and the requested variable is not found, behave as if
>    <value> were the value assigned to that variable.
>
> +DEPRECATED MODES
> +----------------
> +
> +The following modes have been deprecated in favor of subcommands. It is
> +recommended to migrate to the new syntax.
> +
> +-l::
> +--list::
> +	Replaced by `git config list`.
> +
>  CONFIGURATION
>  -------------
>  `pager.config` is only respected when listing configuration, i.e., when
> -using `--list` or any of the `--get-*` which may return multiple results.
> +using `list` or any of the `--get-*` which may return multiple results.
>  The default is to use a pager.
>
>  [[FILES]]
> diff --git a/builtin/config.c b/builtin/config.c
> index 59877065f8..f89ddce8da 100644
> --- a/builtin/config.c
> +++ b/builtin/config.c
> @@ -16,10 +16,16 @@
>  #include "worktree.h"
>
>  static const char *const builtin_config_usage[] = {
> +	N_("git config list [<file-option>] [<display-option>] [--includes]"),
>  	N_("git config [<options>]"),
>  	NULL
>  };
>
> +static const char *const builtin_config_list_usage[] = {
> +	N_("git config list [<file-option>] [<display-option>] [--includes]"),
> +	NULL
> +};
> +
>  static char *key;
>  static regex_t *key_regexp;
>  static const char *value_pattern;
> @@ -33,6 +39,7 @@ static char delim = '=';
>  static char key_delim = ' ';
>  static char term = '\n';
>
> +static parse_opt_subcommand_fn *subcommand;
>  static int use_global_config, use_system_config, use_local_config;
>  static int use_worktree_config;
>  static struct git_config_source given_config_source;
> @@ -706,14 +713,24 @@ static void handle_nul(void) {
>  	}
>  }
>
> +#define CONFIG_LOCATION_OPTIONS \
> +	OPT_GROUP(N_("Config file location")), \
> +	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")), \
> +	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")), \
> +	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")), \
> +	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")), \
> +	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")), \
> +	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object"))
> +
> +#define CONFIG_DISPLAY_OPTIONS \
> +	OPT_GROUP(N_("Display options")), \
> +	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), \
> +	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")), \
> +	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")), \
> +	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)"))
> +
>  static struct option builtin_config_options[] = {
> -	OPT_GROUP(N_("Config file location")),
> -	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
> -	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
> -	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
> -	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
> -	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
> -	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
> +	CONFIG_LOCATION_OPTIONS,
>  	OPT_GROUP(N_("Action")),
>  	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
>  	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
> @@ -737,15 +754,12 @@ static struct option builtin_config_options[] = {
>  	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
>  	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
>  	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
> +	CONFIG_DISPLAY_OPTIONS,
>  	OPT_GROUP(N_("Other")),
> -	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
> -	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
> -	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
> -	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
> -	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
>  	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
>  	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
>  	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
> +	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
>  	OPT_END(),
>  };
>
> @@ -754,6 +768,42 @@ static NORETURN void usage_builtin_config(void)
>  	usage_with_options(builtin_config_usage, builtin_config_options);
>  }
>
> +static int cmd_config_list(int argc, const char **argv, const char *prefix)
> +{
> +	struct option opts[] = {
> +		CONFIG_LOCATION_OPTIONS,
> +		CONFIG_DISPLAY_OPTIONS,
> +		OPT_GROUP(N_("Other")),
> +		OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
> +		OPT_END(),
> +	};
> +
> +	argc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);
> +	check_argc(argc, 0, 0);
> +
> +	handle_config_location(prefix);
> +	handle_nul();
> +
> +	setup_auto_pager("config", 1);
> +
> +	if (config_with_options(show_all_config, NULL,
> +				&given_config_source, the_repository,
> +				&config_options) < 0) {
> +		if (given_config_source.file)
> +			die_errno(_("unable to read config file '%s'"),
> +				  given_config_source.file);
> +		else
> +			die(_("error processing config file(s)"));
> +	}
> +
> +	return 0;
> +}
> +
> +static struct option builtin_subcommand_options[] = {
> +	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
> +	OPT_END(),
> +};
> +
>  int cmd_config(int argc, const char **argv, const char *prefix)
>  {
>  	char *value = NULL, *comment = NULL;
> @@ -763,6 +813,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)
>
>  	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
>
> +	/*
> +	 * This is somewhat hacky: we first parse the command line while
> +	 * keeping all args intact in order to determine whether a subcommand
> +	 * has been specified. If so, we re-parse it a second time, but this
> +	 * time we drop KEEP_ARGV0. This is so that we don't munge the command
> +	 * line in case no subcommand was given, which would otherwise confuse
> +	 * us when parsing the legacy-style modes that don't use subcommands.
> +	 */
> +	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
> +			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
> +	if (subcommand) {
> +		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
> +		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);
> +		return subcommand(argc, argv, prefix);
> +	}
> +
>  	argc = parse_options(argc, argv, prefix, builtin_config_options,
>  			     builtin_config_usage,
>  			     PARSE_OPT_STOP_AT_NON_OPTION);
> diff --git a/t/t1300-config.sh b/t/t1300-config.sh
> index 86dc70769a..f77d2f7847 100755
> --- a/t/t1300-config.sh
> +++ b/t/t1300-config.sh
> @@ -11,6 +11,20 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
>  TEST_PASSES_SANITIZE_LEAK=true
>  . ./test-lib.sh
>
> +for mode in legacy subcommands
> +do
> +
> +case "$mode" in
> +legacy)
> +	mode_prefix="--"
> +	;;
> +subcommands)
> +	mode_prefix=""
> +	;;
> +*)
> +	BUG "unknown mode $mode";;
> +esac
> +
>  test_expect_success 'setup whitespace config' '
>  	sed -e "s/^|//" \
>  	    -e "s/[$]$//" \
> @@ -460,11 +474,11 @@ version.1.2.3eX.alpha=beta
>  EOF
>
>  test_expect_success 'working --list' '
> -	git config --list > output &&
> +	git config ${mode_prefix}list > output &&
>  	test_cmp expect output
>  '
>  test_expect_success '--list without repo produces empty output' '
> -	git --git-dir=nonexistent config --list >output &&
> +	git --git-dir=nonexistent config ${mode_prefix}list >output &&
>  	test_must_be_empty output
>  '
>
> @@ -476,7 +490,7 @@ version.1.2.3eX.alpha
>  EOF
>
>  test_expect_success '--name-only --list' '
> -	git config --name-only --list >output &&
> +	git config ${mode_prefix}list --name-only >output &&
>  	test_cmp expect output
>  '
>
> @@ -614,17 +628,17 @@ ein.bahn=strasse
>  EOF
>
>  test_expect_success 'alternative GIT_CONFIG' '
> -	GIT_CONFIG=other-config git config --list >output &&
> +	GIT_CONFIG=other-config git config ${mode_prefix}list >output &&
>  	test_cmp expect output
>  '
>
>  test_expect_success 'alternative GIT_CONFIG (--file)' '
> -	git config --file other-config --list >output &&
> +	git config ${mode_prefix}list --file other-config >output &&
>  	test_cmp expect output
>  '
>
>  test_expect_success 'alternative GIT_CONFIG (--file=-)' '
> -	git config --file - --list <other-config >output &&
> +	git config ${mode_prefix}list --file - <other-config >output &&
>  	test_cmp expect output
>  '
>
> @@ -637,6 +651,7 @@ test_expect_success 'editing stdin is an error' '
>  '
>
>  test_expect_success 'refer config from subdirectory' '
> +	test_when_finished "rm -r x" &&
>  	mkdir x &&
>  	test_cmp_config -C x strasse --file=../other-config --get ein.bahn
>  '
> @@ -847,7 +862,7 @@ test_expect_success 'line number is reported correctly' '
>  '
>
>  test_expect_success 'invalid stdin config' '
> -	echo "[broken" | test_must_fail git config --list --file - >output 2>&1 &&
> +	echo "[broken" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&
>  	test_grep "bad config line 1 in standard input" output
>  '
>
> @@ -1139,7 +1154,7 @@ section.quotecont=cont;inued
>  EOF
>
>  test_expect_success 'value continued on next line' '
> -	git config --list > result &&
> +	git config ${mode_prefix}list > result &&
>  	test_cmp expect result
>  '
>
> @@ -1163,7 +1178,7 @@ Qsection.sub=section.val4
>  Qsection.sub=section.val5Q
>  EOF
>  test_expect_success '--null --list' '
> -	git config --null --list >result.raw &&
> +	git config ${mode_prefix}list --null >result.raw &&
>  	nul_to_q <result.raw >result &&
>  	echo >>result &&
>  	test_cmp expect result
> @@ -1198,6 +1213,7 @@ test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces'
>  '
>
>  test_expect_success SYMLINKS 'symlinked configuration' '
> +	test_when_finished "rm myconfig" &&
>  	ln -s notyet myconfig &&
>  	git config --file=myconfig test.frotz nitfol &&
>  	test -h myconfig &&
> @@ -1218,10 +1234,11 @@ test_expect_success SYMLINKS 'symlinked configuration' '
>  '
>
>  test_expect_success SYMLINKS 'symlink to nonexistent configuration' '
> +	test_when_finished "rm linktonada linktolinktonada" &&
>  	ln -s doesnotexist linktonada &&
>  	ln -s linktonada linktolinktonada &&
> -	test_must_fail git config --file=linktonada --list &&
> -	test_must_fail git config --file=linktolinktonada --list
> +	test_must_fail git config ${mode_prefix}list --file=linktonada &&
> +	test_must_fail git config ${mode_prefix}list --file=linktolinktonada
>  '
>
>  test_expect_success 'check split_cmdline return' '
> @@ -1478,7 +1495,7 @@ do
>  done
>
>  test_expect_success 'git -c is not confused by empty environment' '
> -	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config --list
> +	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config ${mode_prefix}list
>  '
>
>  test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
> @@ -1669,31 +1686,31 @@ test_expect_success 'git config ignores pairs with empty count' '
>  '
>
>  test_expect_success 'git config fails with invalid count' '
> -	test_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&
> +	test_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&
>  	test_grep "bogus count" error &&
> -	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&
> +	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&
>  	test_grep "too many entries" error
>  '
>
>  test_expect_success 'git config fails with missing config key' '
>  	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0="value" \
> -		git config --list 2>error &&
> +		git config ${mode_prefix}list 2>error &&
>  	test_grep "missing config key" error
>  '
>
>  test_expect_success 'git config fails with missing config value' '
>  	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="pair.one" \
> -		git config --list 2>error &&
> +		git config ${mode_prefix}list 2>error &&
>  	test_grep "missing config value" error
>  '
>
>  test_expect_success 'git config fails with invalid config pair key' '
>  	test_must_fail env GIT_CONFIG_COUNT=1 \
>  		GIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \
> -		git config --list &&
> +		git config ${mode_prefix}list &&
>  	test_must_fail env GIT_CONFIG_COUNT=1 \
>  		GIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \
> -		git config --list
> +		git config ${mode_prefix}list
>  '
>
>  test_expect_success 'environment overrides config file' '
> @@ -1733,7 +1750,7 @@ test_expect_success 'git config --edit works' '
>  	git config -f tmp test.value no &&
>  	echo test.value=yes >expect &&
>  	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
> -	git config -f tmp --list >actual &&
> +	git config ${mode_prefix}list -f tmp >actual &&
>  	test_cmp expect actual
>  '
>
> @@ -1742,7 +1759,7 @@ test_expect_success 'git config --edit respects core.editor' '
>  	echo test.value=yes >expect &&
>  	test_config core.editor "echo [test]value=yes >" &&
>  	git config -f tmp --edit &&
> -	git config -f tmp --list >actual &&
> +	git config ${mode_prefix}list -f tmp >actual &&
>  	test_cmp expect actual
>  '
>
> @@ -2093,7 +2110,7 @@ test_expect_success '--show-origin with --list' '
>  	command line:	user.cmdline=true
>  	EOF
>  	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\
> -		git -c user.cmdline=true config --list --show-origin >output &&
> +		git -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&
>  	test_cmp expect output
>  '
>
> @@ -2110,7 +2127,7 @@ test_expect_success '--show-origin with --list --null' '
>  	includeQcommand line:Quser.cmdline
>  	trueQ
>  	EOF
> -	git -c user.cmdline=true config --null --list --show-origin >output.raw &&
> +	git -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&
>  	nul_to_q <output.raw >output &&
>  	# The here-doc above adds a newline that the --null output would not
>  	# include. Add it here to make the two comparable.
> @@ -2124,7 +2141,7 @@ test_expect_success '--show-origin with single file' '
>  	file:.git/config	user.override=local
>  	file:.git/config	include.path=../include/relative.include
>  	EOF
> -	git config --local --list --show-origin >output &&
> +	git config ${mode_prefix}list --local --show-origin >output &&
>  	test_cmp expect output
>  '
>
> @@ -2162,7 +2179,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '
>  	cat >expect <<-\EOF &&
>  	file:"file\" (dq) and spaces.conf"	user.custom=true
>  	EOF
> -	git config --file "$WEIRDLY_NAMED_FILE" --show-origin --list >output &&
> +	git config ${mode_prefix}list --file "$WEIRDLY_NAMED_FILE" --show-origin >output &&
>  	test_cmp expect output
>  '
>
> @@ -2170,7 +2187,7 @@ test_expect_success '--show-origin stdin' '
>  	cat >expect <<-\EOF &&
>  	standard input:	user.custom=true
>  	EOF
> -	git config --file - --show-origin --list <"$CUSTOM_CONFIG_FILE" >output &&
> +	git config ${mode_prefix}list --file - --show-origin <"$CUSTOM_CONFIG_FILE" >output &&
>  	test_cmp expect output
>  '
>
> @@ -2197,7 +2214,7 @@ test_expect_success '--show-origin blob' '
>  		cat >expect <<-EOF &&
>  		blob:$blob	user.custom=true
>  		EOF
> -		git config --blob=$blob --show-origin --list >output &&
> +		git config ${mode_prefix}list --blob=$blob --show-origin >output &&
>  		test_cmp expect output
>  	)
>  '
> @@ -2213,7 +2230,7 @@ test_expect_success '--show-origin blob ref' '
>  		cp "$CUSTOM_CONFIG_FILE" custom.conf &&
>  		git add custom.conf &&
>  		git commit -m "new config file" &&
> -		git config --blob=main:custom.conf --show-origin --list >output &&
> +		git config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&
>  		test_cmp expect output
>  	)
>  '
> @@ -2239,13 +2256,14 @@ test_expect_success '--show-scope with --list' '
>  	worktree	user.worktree=true
>  	command	user.cmdline=true
>  	EOF
> +	test_when_finished "git worktree remove wt1" &&
>  	git worktree add wt1 &&
>  	# We need these to test for worktree scope, but outside of this
>  	# test, this is just noise
>  	test_config core.repositoryformatversion 1 &&
>  	test_config extensions.worktreeConfig true &&
>  	git config --worktree user.worktree true &&
> -	git -c user.cmdline=true config --list --show-scope >output &&
> +	git -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&
>  	test_cmp expect output
>  '
>
> @@ -2254,7 +2272,7 @@ test_expect_success !MINGW '--show-scope with --blob' '
>  	cat >expect <<-EOF &&
>  	command	user.custom=true
>  	EOF
> -	git config --blob=$blob --show-scope --list >output &&
> +	git config ${mode_prefix}list --blob=$blob --show-scope >output &&
>  	test_cmp expect output
>  '
>
> @@ -2264,7 +2282,7 @@ test_expect_success '--show-scope with --local' '
>  	local	user.override=local
>  	local	include.path=../include/relative.include
>  	EOF
> -	git config --local --list --show-scope >output &&
> +	git config ${mode_prefix}list --local --show-scope >output &&
>  	test_cmp expect output
>  '
>
> @@ -2288,7 +2306,7 @@ test_expect_success '--show-scope with --show-origin' '
>  	local	file:.git/../include/relative.include	user.relative=include
>  	command	command line:	user.cmdline=true
>  	EOF
> -	git -c user.cmdline=true config --list --show-origin --show-scope >output &&
> +	git -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&
>  	test_cmp expect output
>  '
>
> @@ -2329,7 +2347,7 @@ test_expect_success 'override global and system config' '
>  	global	home.config=true
>  	local	local.config=true
>  	EOF
> -	git config --show-scope --list >output &&
> +	git config ${mode_prefix}list --show-scope >output &&
>  	test_cmp expect output &&
>
>  	cat >expect <<-EOF &&
> @@ -2338,20 +2356,20 @@ test_expect_success 'override global and system config' '
>  	local	local.config=true
>  	EOF
>  	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \
> -		git config --show-scope --list >output &&
> +		git config ${mode_prefix}list --show-scope >output &&
>  	test_cmp expect output &&
>
>  	cat >expect <<-EOF &&
>  	local	local.config=true
>  	EOF
>  	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \
> -		git config --show-scope --list >output &&
> +		git config ${mode_prefix}list --show-scope >output &&
>  	test_cmp expect output
>  '
>
>  test_expect_success 'override global and system config with missing file' '
> -	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&
> -	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&
> +	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&
> +	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&
>  	GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version
>  '
>
> @@ -2478,7 +2496,7 @@ test_expect_success 'set all config with value-pattern' '
>  	# no match => add new entry
>  	cp initial config &&
>  	git config --file=config abc.key two a+ &&
> -	git config --file=config --list >actual &&
> +	git config ${mode_prefix}list --file=config >actual &&
>  	cat >expect <<-\EOF &&
>  	abc.key=one
>  	abc.key=two
> @@ -2491,7 +2509,7 @@ test_expect_success 'set all config with value-pattern' '
>
>  	# multiple values, no match => add
>  	git config --file=config abc.key three a+ &&
> -	git config --file=config --list >actual &&
> +	git config ${mode_prefix}list --file=config >actual &&
>  	cat >expect <<-\EOF &&
>  	abc.key=one
>  	abc.key=two
> @@ -2501,7 +2519,7 @@ test_expect_success 'set all config with value-pattern' '
>
>  	# single match => replace
>  	git config --file=config abc.key four h+ &&
> -	git config --file=config --list >actual &&
> +	git config ${mode_prefix}list --file=config >actual &&
>  	cat >expect <<-\EOF &&
>  	abc.key=one
>  	abc.key=two
> @@ -2516,7 +2534,7 @@ test_expect_success '--replace-all and value-pattern' '
>  	git config --file=config --add abc.key two &&
>  	git config --file=config --add abc.key three &&
>  	git config --file=config --replace-all abc.key four "o+" &&
> -	git config --file=config --list >actual &&
> +	git config ${mode_prefix}list --file=config >actual &&
>  	cat >expect <<-\EOF &&
>  	abc.key=four
>  	abc.key=three
> @@ -2534,7 +2552,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
>  	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
>  	test_must_fail git config --file=config --fixed-value --rename-section dev null &&
>  	test_must_fail git config --file=config --fixed-value --remove-section dev &&
> -	test_must_fail git config --file=config --fixed-value --list &&
> +	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
>  	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
>  	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
>
> @@ -2555,7 +2573,7 @@ test_expect_success '--fixed-value uses exact string matching' '
>
>  	cp initial config &&
>  	git config --file=config fixed.test bogus "$META" &&
> -	git config --file=config --list >actual &&
> +	git config ${mode_prefix}list --file=config >actual &&
>  	cat >expect <<-EOF &&
>  	fixed.test=$META
>  	fixed.test=bogus
> @@ -2564,7 +2582,7 @@ test_expect_success '--fixed-value uses exact string matching' '
>
>  	cp initial config &&
>  	git config --file=config --fixed-value fixed.test bogus "$META" &&
> -	git config --file=config --list >actual &&
> +	git config ${mode_prefix}list --file=config >actual &&
>  	cat >expect <<-\EOF &&
>  	fixed.test=bogus
>  	EOF
> @@ -2582,7 +2600,7 @@ test_expect_success '--fixed-value uses exact string matching' '
>
>  	cp initial config &&
>  	git config --file=config --replace-all fixed.test bogus "$META" &&
> -	git config --file=config --list >actual &&
> +	git config ${mode_prefix}list --file=config >actual &&
>  	cat >expect <<-EOF &&
>  	fixed.test=$META
>  	fixed.test=bogus
> @@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string matching' '
>  	test_cmp expect actual &&
>
>  	git config --file=config --fixed-value --replace-all fixed.test bogus "$META" &&
> -	git config --file=config --list >actual &&
> +	git config ${mode_prefix}list --file=config >actual &&
>  	cat >expect <<-EOF &&
>  	fixed.test=bogus
>  	fixed.test=bogus
> @@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes failure' '
>  	test_cmp expect err
>  '
>
> +done
> +
>
Nit: Wouldn't it be better if the tests are indented here? That way you
know it's part of a loop.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]

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

* RE: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-03 13:08     ` Karthik Nayak
@ 2024-05-03 13:13       ` rsbecker
  2024-05-03 16:01         ` Junio C Hamano
  2024-05-06  7:58       ` Patrick Steinhardt
  1 sibling, 1 reply; 113+ messages in thread
From: rsbecker @ 2024-05-03 13:13 UTC (permalink / raw)
  To: 'Karthik Nayak', 'Patrick Steinhardt', git
  Cc: 'Kristoffer Haugsbakk', 'Taylor Blau',
	'Jean-Noël AVILA', 'Eric Sunshine',
	'Junio C Hamano'

On Friday, May 3, 2024 9:09 AM. Karthik Nayak wrote:
>Patrick Steinhardt <ps@pks.im> writes:
>
>> While git-config(1) has several modes, those modes are not exposed
>> with subcommands but instead by specifying e.g. `--unset` or `--list`.
>> This
>
>s/specifying/specifying flags/ perhaps?
>
>> user interface is not really in line with how our more modern commands
>> work, where it is a lot more customary to say e.g. `git remote list`.
>
>Tangent: I totally agree with the patch, but it would be nice to have a
>'DesigningCommands' document which would highlight UX do's and don'ts.
>It would be nice to add that as reference in discussions.
>
>> Furthermore, to add to the confusion, git-config(1) also allows the
>> user to request modes implicitly by just specifying the correct number
>> of arguments. Thus, `git config foo.bar` will retrieve the value of
>> "foo.bar" while `git config foo.bar baz` will set it to "baz".
>>
>> Overall, this makes for a confusing interface that could really use a
>> makeover. It hurts discoverability of what you can do with
>> git-config(1) and is comparatively easy to get wrong. Converting the
>> command to have subcommands instead would go a long way to help address
>these issues.
>>
>> One concern in this context is backwards compatibility. Luckily, we
>> can introduce subcommands without breaking backwards compatibility at all.
>> This is because all the implicit modes of git-config(1) require that
>> the first argument is a properly formatted config key. And as config
>> keys _must_ have a dot in their name, any value without a dot would
>> have been discarded by git-config(1) previous to this change. Thus,
>> given that none of the subcommands do have a dot, they are unambiguous.
>>
>> Introduce the first such new subcommand, which is "git config list".
>> To retain backwards compatibility we only conditionally use
>> subcommands and will fall back to the old syntax in case no subcommand was
>detected.
>> This should help to transition to the new-style syntax until we
>> eventually deprecate and remove the old-style syntax.
>>
>> Note that the way we handle this we're duplicating some functionality
>> across old and new syntax. While this isn't pretty, it helps us to
>> ensure that there really is no change in behaviour for the old syntax.
>>
>> Amend tests such that we run them both with old and new style syntax.
>> As tests are now run twice, state from the first run may be still be
>> around in the second run and thus cause tests to fail. Add cleanup
>> logic as required to fix such tests.
>>
>> Signed-off-by: Patrick Steinhardt <ps@pks.im>
>> ---
>>  Documentation/git-config.txt |  26 ++++++---
>>  builtin/config.c             |  90 ++++++++++++++++++++++++----
>>  t/t1300-config.sh            | 110 +++++++++++++++++++++--------------
>>  3 files changed, 162 insertions(+), 64 deletions(-)
>>
>> diff --git a/Documentation/git-config.txt
>> b/Documentation/git-config.txt index ac61113fcc..c83c97cb7e 100644
>> --- a/Documentation/git-config.txt
>> +++ b/Documentation/git-config.txt
>> @@ -9,6 +9,7 @@ git-config - Get and set repository or global options
>> SYNOPSIS
>>  --------
>>  [verse]
>> +'git config list' [<file-option>] [<display-option>] [--includes]
>>  'git config' [<file-option>] [--type=<type>] [--comment=<message>]
>> [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name>
>> [<value> [<value-pattern>]]  'git config' [<file-option>]
>> [--type=<type>] [--comment=<message>] --add <name> <value>  'git
>> config' [<file-option>] [--type=<type>] [--comment=<message>]
>> [--fixed-value] --replace-all <name> <value> [<value-pattern>] @@
>> -20,7 +21,6 @@ SYNOPSIS  'git config' [<file-option>] [--fixed-value]
>> --unset-all <name> [<value-pattern>]  'git config' [<file-option>]
>> --rename-section <old-name> <new-name>  'git config' [<file-option>]
>> --remove-section <name> -'git config' [<file-option>] [--show-origin]
>> [--show-scope] [-z|--null] [--name-only] -l | --list  'git config'
>> [<file-option>] --get-color <name> [<default>]  'git config' [<file-option>] --get-
>colorbool <name> [<stdout-is-tty>]  'git config' [<file-option>] -e | --edit @@ -74,6
>+74,12 @@ On success, the command returns the exit code 0.
>>  A list of all available configuration variables can be obtained using
>> the  `git help --config` command.
>>
>> +COMMANDS
>> +--------
>> +
>> +list::
>> +	List all variables set in config file, along with their values.
>> +
>>  [[OPTIONS]]
>>  OPTIONS
>>  -------
>> @@ -190,10 +196,6 @@ See also <<FILES>>.
>>  --unset-all::
>>  	Remove all lines matching the key from config file.
>>
>> --l::
>> ---list::
>> -	List all variables set in config file, along with their values.
>> -
>>  --fixed-value::
>>  	When used with the `value-pattern` argument, treat `value-pattern` as
>>  	an exact string instead of a regular expression. This will restrict
>> @@ -248,7 +250,7 @@ Valid `<type>`'s include:
>>  	contain line breaks.
>>
>>  --name-only::
>> -	Output only the names of config variables for `--list` or
>> +	Output only the names of config variables for `list` or
>>  	`--get-regexp`.
>>
>>  --show-origin::
>> @@ -300,10 +302,20 @@ Valid `<type>`'s include:
>>    When using `--get`, and the requested variable is not found, behave as if
>>    <value> were the value assigned to that variable.
>>
>> +DEPRECATED MODES
>> +----------------
>> +
>> +The following modes have been deprecated in favor of subcommands. It
>> +is recommended to migrate to the new syntax.
>> +
>> +-l::
>> +--list::
>> +	Replaced by `git config list`.
>> +
>>  CONFIGURATION
>>  -------------
>>  `pager.config` is only respected when listing configuration, i.e.,
>> when -using `--list` or any of the `--get-*` which may return multiple results.
>> +using `list` or any of the `--get-*` which may return multiple results.
>>  The default is to use a pager.
>>
>>  [[FILES]]
>> diff --git a/builtin/config.c b/builtin/config.c index
>> 59877065f8..f89ddce8da 100644
>> --- a/builtin/config.c
>> +++ b/builtin/config.c
>> @@ -16,10 +16,16 @@
>>  #include "worktree.h"
>>
>>  static const char *const builtin_config_usage[] = {
>> +	N_("git config list [<file-option>] [<display-option>]
>> +[--includes]"),
>>  	N_("git config [<options>]"),
>>  	NULL
>>  };
>>
>> +static const char *const builtin_config_list_usage[] = {
>> +	N_("git config list [<file-option>] [<display-option>] [--includes]"),
>> +	NULL
>> +};
>> +
>>  static char *key;
>>  static regex_t *key_regexp;
>>  static const char *value_pattern;
>> @@ -33,6 +39,7 @@ static char delim = '=';  static char key_delim = '
>> ';  static char term = '\n';
>>
>> +static parse_opt_subcommand_fn *subcommand;
>>  static int use_global_config, use_system_config, use_local_config;
>> static int use_worktree_config;  static struct git_config_source
>> given_config_source; @@ -706,14 +713,24 @@ static void
>> handle_nul(void) {
>>  	}
>>  }
>>
>> +#define CONFIG_LOCATION_OPTIONS \
>> +	OPT_GROUP(N_("Config file location")), \
>> +	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")), \
>> +	OPT_BOOL(0, "system", &use_system_config, N_("use system config
>file")), \
>> +	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")), \
>> +	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree
>config file")), \
>> +	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given
>config file")), \
>> +	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"),
>> +N_("read config from given blob object"))
>> +
>> +#define CONFIG_DISPLAY_OPTIONS \
>> +	OPT_GROUP(N_("Display options")), \
>> +	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), \
>> +	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names
>only")), \
>> +	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file,
>standard input, blob, command line)")), \
>> +	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config
>> +(worktree, local, global, system, command)"))
>> +
>>  static struct option builtin_config_options[] = {
>> -	OPT_GROUP(N_("Config file location")),
>> -	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
>> -	OPT_BOOL(0, "system", &use_system_config, N_("use system config
>file")),
>> -	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
>> -	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree
>config file")),
>> -	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given
>config file")),
>> -	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"),
>N_("read config from given blob object")),
>> +	CONFIG_LOCATION_OPTIONS,
>>  	OPT_GROUP(N_("Action")),
>>  	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-
>pattern>]"), ACTION_GET),
>>  	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key
>> [<value-pattern>]"), ACTION_GET_ALL), @@ -737,15 +754,12 @@ static struct
>option builtin_config_options[] = {
>>  	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or
>string"), TYPE_BOOL_OR_STR),
>>  	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or
>directory name)"), TYPE_PATH),
>>  	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry
>> date"), TYPE_EXPIRY_DATE),
>> +	CONFIG_DISPLAY_OPTIONS,
>>  	OPT_GROUP(N_("Other")),
>> -	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
>> -	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names
>only")),
>> -	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include
>directives on lookup")),
>> -	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file,
>standard input, blob, command line)")),
>> -	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config
>(worktree, local, global, system, command)")),
>>  	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use
>default value when missing entry")),
>>  	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-
>readable comment string (# will be prepended as needed)")),
>>  	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality
>> when comparing values to 'value-pattern'")),
>> +	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include
>> +directives on lookup")),
>>  	OPT_END(),
>>  };
>>
>> @@ -754,6 +768,42 @@ static NORETURN void usage_builtin_config(void)
>>  	usage_with_options(builtin_config_usage, builtin_config_options);  }
>>
>> +static int cmd_config_list(int argc, const char **argv, const char
>> +*prefix) {
>> +	struct option opts[] = {
>> +		CONFIG_LOCATION_OPTIONS,
>> +		CONFIG_DISPLAY_OPTIONS,
>> +		OPT_GROUP(N_("Other")),
>> +		OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect
>include directives on lookup")),
>> +		OPT_END(),
>> +	};
>> +
>> +	argc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);
>> +	check_argc(argc, 0, 0);
>> +
>> +	handle_config_location(prefix);
>> +	handle_nul();
>> +
>> +	setup_auto_pager("config", 1);
>> +
>> +	if (config_with_options(show_all_config, NULL,
>> +				&given_config_source, the_repository,
>> +				&config_options) < 0) {
>> +		if (given_config_source.file)
>> +			die_errno(_("unable to read config file '%s'"),
>> +				  given_config_source.file);
>> +		else
>> +			die(_("error processing config file(s)"));
>> +	}
>> +
>> +	return 0;
>> +}
>> +
>> +static struct option builtin_subcommand_options[] = {
>> +	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
>> +	OPT_END(),
>> +};
>> +
>>  int cmd_config(int argc, const char **argv, const char *prefix)  {
>>  	char *value = NULL, *comment = NULL; @@ -763,6 +813,22 @@ int
>> cmd_config(int argc, const char **argv, const char *prefix)
>>
>>  	given_config_source.file =
>> xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
>>
>> +	/*
>> +	 * This is somewhat hacky: we first parse the command line while
>> +	 * keeping all args intact in order to determine whether a subcommand
>> +	 * has been specified. If so, we re-parse it a second time, but this
>> +	 * time we drop KEEP_ARGV0. This is so that we don't munge the command
>> +	 * line in case no subcommand was given, which would otherwise confuse
>> +	 * us when parsing the legacy-style modes that don't use subcommands.
>> +	 */
>> +	argc = parse_options(argc, argv, prefix, builtin_subcommand_options,
>builtin_config_usage,
>> +
>PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE
>_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
>> +	if (subcommand) {
>> +		argc = parse_options(argc, argv, prefix,
>builtin_subcommand_options, builtin_config_usage,
>> +
>PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE
>_OPT_KEEP_UNKNOWN_OPT);
>> +		return subcommand(argc, argv, prefix);
>> +	}
>> +
>>  	argc = parse_options(argc, argv, prefix, builtin_config_options,
>>  			     builtin_config_usage,
>>  			     PARSE_OPT_STOP_AT_NON_OPTION); diff --git
>a/t/t1300-config.sh
>> b/t/t1300-config.sh index 86dc70769a..f77d2f7847 100755
>> --- a/t/t1300-config.sh
>> +++ b/t/t1300-config.sh
>> @@ -11,6 +11,20 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
>>  TEST_PASSES_SANITIZE_LEAK=true
>>  . ./test-lib.sh
>>
>> +for mode in legacy subcommands
>> +do
>> +
>> +case "$mode" in
>> +legacy)
>> +	mode_prefix="--"
>> +	;;
>> +subcommands)
>> +	mode_prefix=""
>> +	;;
>> +*)
>> +	BUG "unknown mode $mode";;
>> +esac
>> +
>>  test_expect_success 'setup whitespace config' '
>>  	sed -e "s/^|//" \
>>  	    -e "s/[$]$//" \
>> @@ -460,11 +474,11 @@ version.1.2.3eX.alpha=beta  EOF
>>
>>  test_expect_success 'working --list' '
>> -	git config --list > output &&
>> +	git config ${mode_prefix}list > output &&
>>  	test_cmp expect output
>>  '
>>  test_expect_success '--list without repo produces empty output' '
>> -	git --git-dir=nonexistent config --list >output &&
>> +	git --git-dir=nonexistent config ${mode_prefix}list >output &&
>>  	test_must_be_empty output
>>  '
>>
>> @@ -476,7 +490,7 @@ version.1.2.3eX.alpha  EOF
>>
>>  test_expect_success '--name-only --list' '
>> -	git config --name-only --list >output &&
>> +	git config ${mode_prefix}list --name-only >output &&
>>  	test_cmp expect output
>>  '
>>
>> @@ -614,17 +628,17 @@ ein.bahn=strasse  EOF
>>
>>  test_expect_success 'alternative GIT_CONFIG' '
>> -	GIT_CONFIG=other-config git config --list >output &&
>> +	GIT_CONFIG=other-config git config ${mode_prefix}list >output &&
>>  	test_cmp expect output
>>  '
>>
>>  test_expect_success 'alternative GIT_CONFIG (--file)' '
>> -	git config --file other-config --list >output &&
>> +	git config ${mode_prefix}list --file other-config >output &&
>>  	test_cmp expect output
>>  '
>>
>>  test_expect_success 'alternative GIT_CONFIG (--file=-)' '
>> -	git config --file - --list <other-config >output &&
>> +	git config ${mode_prefix}list --file - <other-config >output &&
>>  	test_cmp expect output
>>  '
>>
>> @@ -637,6 +651,7 @@ test_expect_success 'editing stdin is an error' '
>>  '
>>
>>  test_expect_success 'refer config from subdirectory' '
>> +	test_when_finished "rm -r x" &&
>>  	mkdir x &&
>>  	test_cmp_config -C x strasse --file=../other-config --get ein.bahn
>> '
>> @@ -847,7 +862,7 @@ test_expect_success 'line number is reported correctly' '
>>  '
>>
>>  test_expect_success 'invalid stdin config' '
>> -	echo "[broken" | test_must_fail git config --list --file - >output 2>&1 &&
>> +	echo "[broken" | test_must_fail git config ${mode_prefix}list --file
>> +- >output 2>&1 &&
>>  	test_grep "bad config line 1 in standard input" output  '
>>
>> @@ -1139,7 +1154,7 @@ section.quotecont=cont;inued  EOF
>>
>>  test_expect_success 'value continued on next line' '
>> -	git config --list > result &&
>> +	git config ${mode_prefix}list > result &&
>>  	test_cmp expect result
>>  '
>>
>> @@ -1163,7 +1178,7 @@ Qsection.sub=section.val4
>> Qsection.sub=section.val5Q  EOF  test_expect_success '--null --list' '
>> -	git config --null --list >result.raw &&
>> +	git config ${mode_prefix}list --null >result.raw &&
>>  	nul_to_q <result.raw >result &&
>>  	echo >>result &&
>>  	test_cmp expect result
>> @@ -1198,6 +1213,7 @@ test_expect_success 'inner whitespace kept verbatim,
>horizontal tabs and spaces'
>>  '
>>
>>  test_expect_success SYMLINKS 'symlinked configuration' '
>> +	test_when_finished "rm myconfig" &&
>>  	ln -s notyet myconfig &&
>>  	git config --file=myconfig test.frotz nitfol &&
>>  	test -h myconfig &&
>> @@ -1218,10 +1234,11 @@ test_expect_success SYMLINKS 'symlinked
>configuration' '
>>  '
>>
>>  test_expect_success SYMLINKS 'symlink to nonexistent configuration' '
>> +	test_when_finished "rm linktonada linktolinktonada" &&
>>  	ln -s doesnotexist linktonada &&
>>  	ln -s linktonada linktolinktonada &&
>> -	test_must_fail git config --file=linktonada --list &&
>> -	test_must_fail git config --file=linktolinktonada --list
>> +	test_must_fail git config ${mode_prefix}list --file=linktonada &&
>> +	test_must_fail git config ${mode_prefix}list --file=linktolinktonada
>>  '
>>
>>  test_expect_success 'check split_cmdline return' '
>> @@ -1478,7 +1495,7 @@ do
>>  done
>>
>>  test_expect_success 'git -c is not confused by empty environment' '
>> -	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config --list
>> +	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config ${mode_prefix}list
>>  '
>>
>>  test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
>> @@ -1669,31 +1686,31 @@ test_expect_success 'git config ignores pairs with
>empty count' '
>>  '
>>
>>  test_expect_success 'git config fails with invalid count' '
>> -	test_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&
>> +	test_must_fail env GIT_CONFIG_COUNT=10a git config
>> +${mode_prefix}list 2>error &&
>>  	test_grep "bogus count" error &&
>> -	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --
>list 2>error &&
>> +	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config
>> +${mode_prefix}list 2>error &&
>>  	test_grep "too many entries" error
>>  '
>>
>>  test_expect_success 'git config fails with missing config key' '
>>  	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0="value"
>\
>> -		git config --list 2>error &&
>> +		git config ${mode_prefix}list 2>error &&
>>  	test_grep "missing config key" error  '
>>
>>  test_expect_success 'git config fails with missing config value' '
>>  	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="pair.one" \
>> -		git config --list 2>error &&
>> +		git config ${mode_prefix}list 2>error &&
>>  	test_grep "missing config value" error  '
>>
>>  test_expect_success 'git config fails with invalid config pair key' '
>>  	test_must_fail env GIT_CONFIG_COUNT=1 \
>>  		GIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \
>> -		git config --list &&
>> +		git config ${mode_prefix}list &&
>>  	test_must_fail env GIT_CONFIG_COUNT=1 \
>>  		GIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value
>\
>> -		git config --list
>> +		git config ${mode_prefix}list
>>  '
>>
>>  test_expect_success 'environment overrides config file' '
>> @@ -1733,7 +1750,7 @@ test_expect_success 'git config --edit works' '
>>  	git config -f tmp test.value no &&
>>  	echo test.value=yes >expect &&
>>  	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
>> -	git config -f tmp --list >actual &&
>> +	git config ${mode_prefix}list -f tmp >actual &&
>>  	test_cmp expect actual
>>  '
>>
>> @@ -1742,7 +1759,7 @@ test_expect_success 'git config --edit respects
>core.editor' '
>>  	echo test.value=yes >expect &&
>>  	test_config core.editor "echo [test]value=yes >" &&
>>  	git config -f tmp --edit &&
>> -	git config -f tmp --list >actual &&
>> +	git config ${mode_prefix}list -f tmp >actual &&
>>  	test_cmp expect actual
>>  '
>>
>> @@ -2093,7 +2110,7 @@ test_expect_success '--show-origin with --list' '
>>  	command line:	user.cmdline=true
>>  	EOF
>>  	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ
>GIT_CONFIG_VALUE_0=true\
>> -		git -c user.cmdline=true config --list --show-origin >output &&
>> +		git -c user.cmdline=true config ${mode_prefix}list --show-origin
>> +>output &&
>>  	test_cmp expect output
>>  '
>>
>> @@ -2110,7 +2127,7 @@ test_expect_success '--show-origin with --list --null' '
>>  	includeQcommand line:Quser.cmdline
>>  	trueQ
>>  	EOF
>> -	git -c user.cmdline=true config --null --list --show-origin >output.raw &&
>> +	git -c user.cmdline=true config ${mode_prefix}list --null
>> +--show-origin >output.raw &&
>>  	nul_to_q <output.raw >output &&
>>  	# The here-doc above adds a newline that the --null output would not
>>  	# include. Add it here to make the two comparable.
>> @@ -2124,7 +2141,7 @@ test_expect_success '--show-origin with single file' '
>>  	file:.git/config	user.override=local
>>  	file:.git/config	include.path=../include/relative.include
>>  	EOF
>> -	git config --local --list --show-origin >output &&
>> +	git config ${mode_prefix}list --local --show-origin >output &&
>>  	test_cmp expect output
>>  '
>>
>> @@ -2162,7 +2179,7 @@ test_expect_success !MINGW '--show-origin escape
>special file name characters' '
>>  	cat >expect <<-\EOF &&
>>  	file:"file\" (dq) and spaces.conf"	user.custom=true
>>  	EOF
>> -	git config --file "$WEIRDLY_NAMED_FILE" --show-origin --list >output &&
>> +	git config ${mode_prefix}list --file "$WEIRDLY_NAMED_FILE"
>> +--show-origin >output &&
>>  	test_cmp expect output
>>  '
>>
>> @@ -2170,7 +2187,7 @@ test_expect_success '--show-origin stdin' '
>>  	cat >expect <<-\EOF &&
>>  	standard input:	user.custom=true
>>  	EOF
>> -	git config --file - --show-origin --list <"$CUSTOM_CONFIG_FILE" >output
>&&
>> +	git config ${mode_prefix}list --file - --show-origin
>> +<"$CUSTOM_CONFIG_FILE" >output &&
>>  	test_cmp expect output
>>  '
>>
>> @@ -2197,7 +2214,7 @@ test_expect_success '--show-origin blob' '
>>  		cat >expect <<-EOF &&
>>  		blob:$blob	user.custom=true
>>  		EOF
>> -		git config --blob=$blob --show-origin --list >output &&
>> +		git config ${mode_prefix}list --blob=$blob --show-origin >output
>&&
>>  		test_cmp expect output
>>  	)
>>  '
>> @@ -2213,7 +2230,7 @@ test_expect_success '--show-origin blob ref' '
>>  		cp "$CUSTOM_CONFIG_FILE" custom.conf &&
>>  		git add custom.conf &&
>>  		git commit -m "new config file" &&
>> -		git config --blob=main:custom.conf --show-origin --list >output &&
>> +		git config ${mode_prefix}list --blob=main:custom.conf --show-
>origin
>> +>output &&
>>  		test_cmp expect output
>>  	)
>>  '
>> @@ -2239,13 +2256,14 @@ test_expect_success '--show-scope with --list' '
>>  	worktree	user.worktree=true
>>  	command	user.cmdline=true
>>  	EOF
>> +	test_when_finished "git worktree remove wt1" &&
>>  	git worktree add wt1 &&
>>  	# We need these to test for worktree scope, but outside of this
>>  	# test, this is just noise
>>  	test_config core.repositoryformatversion 1 &&
>>  	test_config extensions.worktreeConfig true &&
>>  	git config --worktree user.worktree true &&
>> -	git -c user.cmdline=true config --list --show-scope >output &&
>> +	git -c user.cmdline=true config ${mode_prefix}list --show-scope
>> +>output &&
>>  	test_cmp expect output
>>  '
>>
>> @@ -2254,7 +2272,7 @@ test_expect_success !MINGW '--show-scope with --
>blob' '
>>  	cat >expect <<-EOF &&
>>  	command	user.custom=true
>>  	EOF
>> -	git config --blob=$blob --show-scope --list >output &&
>> +	git config ${mode_prefix}list --blob=$blob --show-scope >output &&
>>  	test_cmp expect output
>>  '
>>
>> @@ -2264,7 +2282,7 @@ test_expect_success '--show-scope with --local' '
>>  	local	user.override=local
>>  	local	include.path=../include/relative.include
>>  	EOF
>> -	git config --local --list --show-scope >output &&
>> +	git config ${mode_prefix}list --local --show-scope >output &&
>>  	test_cmp expect output
>>  '
>>
>> @@ -2288,7 +2306,7 @@ test_expect_success '--show-scope with --show-origin'
>'
>>  	local	file:.git/../include/relative.include	user.relative=include
>>  	command	command line:	user.cmdline=true
>>  	EOF
>> -	git -c user.cmdline=true config --list --show-origin --show-scope >output
>&&
>> +	git -c user.cmdline=true config ${mode_prefix}list --show-origin
>> +--show-scope >output &&
>>  	test_cmp expect output
>>  '
>>
>> @@ -2329,7 +2347,7 @@ test_expect_success 'override global and system
>config' '
>>  	global	home.config=true
>>  	local	local.config=true
>>  	EOF
>> -	git config --show-scope --list >output &&
>> +	git config ${mode_prefix}list --show-scope >output &&
>>  	test_cmp expect output &&
>>
>>  	cat >expect <<-EOF &&
>> @@ -2338,20 +2356,20 @@ test_expect_success 'override global and system
>config' '
>>  	local	local.config=true
>>  	EOF
>>  	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-
>config GIT_CONFIG_GLOBAL=custom-global-config \
>> -		git config --show-scope --list >output &&
>> +		git config ${mode_prefix}list --show-scope >output &&
>>  	test_cmp expect output &&
>>
>>  	cat >expect <<-EOF &&
>>  	local	local.config=true
>>  	EOF
>>  	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null
>GIT_CONFIG_GLOBAL=/dev/null \
>> -		git config --show-scope --list >output &&
>> +		git config ${mode_prefix}list --show-scope >output &&
>>  	test_cmp expect output
>>  '
>>
>>  test_expect_success 'override global and system config with missing file' '
>> -	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist
>GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&
>> -	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null
>GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&
>> +	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist
>GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&
>> +	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null
>> +GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list
>> +--system &&
>>  	GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist
>> git version  '
>>
>> @@ -2478,7 +2496,7 @@ test_expect_success 'set all config with value-pattern' '
>>  	# no match => add new entry
>>  	cp initial config &&
>>  	git config --file=config abc.key two a+ &&
>> -	git config --file=config --list >actual &&
>> +	git config ${mode_prefix}list --file=config >actual &&
>>  	cat >expect <<-\EOF &&
>>  	abc.key=one
>>  	abc.key=two
>> @@ -2491,7 +2509,7 @@ test_expect_success 'set all config with value-pattern' '
>>
>>  	# multiple values, no match => add
>>  	git config --file=config abc.key three a+ &&
>> -	git config --file=config --list >actual &&
>> +	git config ${mode_prefix}list --file=config >actual &&
>>  	cat >expect <<-\EOF &&
>>  	abc.key=one
>>  	abc.key=two
>> @@ -2501,7 +2519,7 @@ test_expect_success 'set all config with value-pattern' '
>>
>>  	# single match => replace
>>  	git config --file=config abc.key four h+ &&
>> -	git config --file=config --list >actual &&
>> +	git config ${mode_prefix}list --file=config >actual &&
>>  	cat >expect <<-\EOF &&
>>  	abc.key=one
>>  	abc.key=two
>> @@ -2516,7 +2534,7 @@ test_expect_success '--replace-all and value-pattern' '
>>  	git config --file=config --add abc.key two &&
>>  	git config --file=config --add abc.key three &&
>>  	git config --file=config --replace-all abc.key four "o+" &&
>> -	git config --file=config --list >actual &&
>> +	git config ${mode_prefix}list --file=config >actual &&
>>  	cat >expect <<-\EOF &&
>>  	abc.key=four
>>  	abc.key=three
>> @@ -2534,7 +2552,7 @@ test_expect_success 'refuse --fixed-value for
>incompatible actions' '
>>  	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null
>bogus &&
>>  	test_must_fail git config --file=config --fixed-value --rename-section dev null
>&&
>>  	test_must_fail git config --file=config --fixed-value --remove-section dev &&
>> -	test_must_fail git config --file=config --fixed-value --list &&
>> +	test_must_fail git config ${mode_prefix}list --file=config
>> +--fixed-value &&
>>  	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
>>  	test_must_fail git config --file=config --fixed-value
>> --get-colorbool dev.null &&
>>
>> @@ -2555,7 +2573,7 @@ test_expect_success '--fixed-value uses exact string
>matching' '
>>
>>  	cp initial config &&
>>  	git config --file=config fixed.test bogus "$META" &&
>> -	git config --file=config --list >actual &&
>> +	git config ${mode_prefix}list --file=config >actual &&
>>  	cat >expect <<-EOF &&
>>  	fixed.test=$META
>>  	fixed.test=bogus
>> @@ -2564,7 +2582,7 @@ test_expect_success '--fixed-value uses exact string
>matching' '
>>
>>  	cp initial config &&
>>  	git config --file=config --fixed-value fixed.test bogus "$META" &&
>> -	git config --file=config --list >actual &&
>> +	git config ${mode_prefix}list --file=config >actual &&
>>  	cat >expect <<-\EOF &&
>>  	fixed.test=bogus
>>  	EOF
>> @@ -2582,7 +2600,7 @@ test_expect_success '--fixed-value uses exact string
>matching' '
>>
>>  	cp initial config &&
>>  	git config --file=config --replace-all fixed.test bogus "$META" &&
>> -	git config --file=config --list >actual &&
>> +	git config ${mode_prefix}list --file=config >actual &&
>>  	cat >expect <<-EOF &&
>>  	fixed.test=$META
>>  	fixed.test=bogus
>> @@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string
>matching' '
>>  	test_cmp expect actual &&
>>
>>  	git config --file=config --fixed-value --replace-all fixed.test bogus "$META"
>&&
>> -	git config --file=config --list >actual &&
>> +	git config ${mode_prefix}list --file=config >actual &&
>>  	cat >expect <<-EOF &&
>>  	fixed.test=bogus
>>  	fixed.test=bogus
>> @@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes
>failure' '
>>  	test_cmp expect err
>>  '
>>
>> +done
>> +
>>
>Nit: Wouldn't it be better if the tests are indented here? That way you know it's part
>of a loop.

Removing the --list option is going to break backward compatibility for users who script the use of config for things like setup, clone automation, etc. Adding list as a sub-command could (but should not) cause ambiguities between a list and configuration value. If you are going to add 'list', please do not remove '--list'. That is a breaking change.

Thanks,
Randall


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

* Re: [PATCH v4 00/14] builtin/config: introduce subcommands
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (13 preceding siblings ...)
  2024-05-03  9:57   ` [PATCH v4 14/14] builtin/config: display subcommand help Patrick Steinhardt
@ 2024-05-03 13:36   ` Dragan Simic
  2024-05-03 16:09   ` Junio C Hamano
  15 siblings, 0 replies; 113+ messages in thread
From: Dragan Simic @ 2024-05-03 13:36 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

Hello Patrick,

On 2024-05-03 11:56, Patrick Steinhardt wrote:
> this is the fourth version of my patch series that introduces
> subcommands for git-config(1).
> 
> Changes compared to v3:
> 
>     - Rebased on top of d4cc1ec35f (Start the 2.46 cycle, 2024-04-30).
> 
>     - Implemented support for `git config set --comment`. This switch
>       has been added since the last version of this patch series.
> 
> Here's hoping that there's a bit more interest in this patch series at
> the beginning of the release cycle :)

FWIW, I find the introduction of the subcommands really great.
It makes the way git-config(1) works much more intuitive.

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

* Re: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-03 13:13       ` rsbecker
@ 2024-05-03 16:01         ` Junio C Hamano
  2024-05-06  7:51           ` Patrick Steinhardt
  0 siblings, 1 reply; 113+ messages in thread
From: Junio C Hamano @ 2024-05-03 16:01 UTC (permalink / raw)
  To: rsbecker
  Cc: 'Karthik Nayak', 'Patrick Steinhardt',
	git, 'Kristoffer Haugsbakk', 'Taylor Blau',
	'Jean-Noël AVILA', 'Eric Sunshine'

<rsbecker@nexbridge.com> writes:

>>> +DEPRECATED MODES
>>> +----------------
>>> +
>>> +The following modes have been deprecated in favor of subcommands. It
>>> +is recommended to migrate to the new syntax.
>>> +
>>> +-l::
>>> +--list::
>>> +	Replaced by `git config list`.

Can you trim quotes in your response that has no relevance to what
you are responding to?

> Removing the --list option is going to break backward
> compatibility for users who script the use of config for things
> like setup, clone automation, etc.

Yes, that is why this is merely a deprecation.

> Adding list as a sub-command could (but should not) cause
> ambiguities between a list and configuration value.

Because there is no sectionless variable, there exists no ambiguity.

In other words,

    $ git config list

cannot be asking for the value of the variable "list", because such
a variable cannot exist.  When you start your command with

    $ git config list ...

whatever other tokens follow that token "list" on the command line,
it is a "--list" subcommand.  A traditional and lazy "get a single
variable" without an explicit subcommand would look like

    $ git config section.variable

that is a short-hand to

    $ git config --get section.variable

and would become

    $ git config get section.variable

in the new world order.

Thanks.

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

* Re: [PATCH v4 00/14] builtin/config: introduce subcommands
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
                     ` (14 preceding siblings ...)
  2024-05-03 13:36   ` [PATCH v4 00/14] builtin/config: introduce subcommands Dragan Simic
@ 2024-05-03 16:09   ` Junio C Hamano
  15 siblings, 0 replies; 113+ messages in thread
From: Junio C Hamano @ 2024-05-03 16:09 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine

Patrick Steinhardt <ps@pks.im> writes:

> this is the fourth version of my patch series that introduces
> subcommands for git-config(1).
>
> Changes compared to v3:
>
>     - Rebased on top of d4cc1ec35f (Start the 2.46 cycle, 2024-04-30).
>
>     - Implemented support for `git config set --comment`. This switch
>       has been added since the last version of this patch series.
>
> Here's hoping that there's a bit more interest in this patch series at
> the beginning of the release cycle :)

As you made the cover letter of v4 a response to the cover letter of
v1, and because the primary motivation of the series has not changed
since the very beginning, it was very easy to find the cover letter
of the original series that outlined the motivation (including why
it does not risk ambiguity) quite well.  People cannot go directly
back to v2 and v3 with the information alone, but at that point, we
can visit https://lore.kernel.org/git/cover.1709724089.git.ps@pks.im/
and everything is there on one page.

Thanks for not forgetting the "comment" thing that was added since
the last round.

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

* Re: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-03 16:01         ` Junio C Hamano
@ 2024-05-06  7:51           ` Patrick Steinhardt
  2024-05-06 17:13             ` Junio C Hamano
  0 siblings, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  7:51 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: rsbecker, 'Karthik Nayak',
	git, 'Kristoffer Haugsbakk', 'Taylor Blau',
	'Jean-Noël AVILA', 'Eric Sunshine'

[-- Attachment #1: Type: text/plain, Size: 2250 bytes --]

On Fri, May 03, 2024 at 09:01:06AM -0700, Junio C Hamano wrote:
> <rsbecker@nexbridge.com> writes:
> 
> >>> +DEPRECATED MODES
> >>> +----------------
> >>> +
> >>> +The following modes have been deprecated in favor of subcommands. It
> >>> +is recommended to migrate to the new syntax.
> >>> +
> >>> +-l::
> >>> +--list::
> >>> +	Replaced by `git config list`.
> 
> Can you trim quotes in your response that has no relevance to what
> you are responding to?
> 
> > Removing the --list option is going to break backward
> > compatibility for users who script the use of config for things
> > like setup, clone automation, etc.
> 
> Yes, that is why this is merely a deprecation.

I will probably send a follow-up patch series that converts other tests
to use the new syntax and that does some more cleanups. I was pondering
whether we want to introduce a document as part of that patch series
that starts to keep track of upcoming removals for a potential Git 3.0
release.

There are multiple items that could be added:

  - Removal of the old syntax of git-config(1).

  - Removal of the dumb HTTP transport.

  - Removal of `info/grafts`.

There are probably other items.

In any case, the old actions are here to stay for the foreseeable future
until we commit to a breaking major release.

> > Adding list as a sub-command could (but should not) cause
> > ambiguities between a list and configuration value.
> 
> Because there is no sectionless variable, there exists no ambiguity.
> 
> In other words,
> 
>     $ git config list
> 
> cannot be asking for the value of the variable "list", because such
> a variable cannot exist.  When you start your command with
> 
>     $ git config list ...
> 
> whatever other tokens follow that token "list" on the command line,
> it is a "--list" subcommand.  A traditional and lazy "get a single
> variable" without an explicit subcommand would look like
> 
>     $ git config section.variable
> 
> that is a short-hand to
> 
>     $ git config --get section.variable
> 
> and would become
> 
>     $ git config get section.variable
> 
> in the new world order.

Thanks for the thorough explanation, I have nothing to add!

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-03 13:08     ` Karthik Nayak
  2024-05-03 13:13       ` rsbecker
@ 2024-05-06  7:58       ` Patrick Steinhardt
  2024-05-06 11:26         ` Karthik Nayak
  1 sibling, 1 reply; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  7:58 UTC (permalink / raw)
  To: Karthik Nayak
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 1772 bytes --]

On Fri, May 03, 2024 at 06:08:57AM -0700, Karthik Nayak wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > While git-config(1) has several modes, those modes are not exposed with
> > subcommands but instead by specifying e.g. `--unset` or `--list`. This
> 
> s/specifying/specifying flags/ perhaps?
> 
> > user interface is not really in line with how our more modern commands
> > work, where it is a lot more customary to say e.g. `git remote list`.
> 
> Tangent: I totally agree with the patch, but it would be nice to have a
> 'DesigningCommands' document which would highlight UX do's and don'ts.
> It would be nice to add that as reference in discussions.

I agree that we should have that discussion, but feel like it should be
part of a separate patch series. If I were to adopt that into this
series I very much think that the resulting discussions would take quite
a while to settle.

I'll start the discussion soonish in a separate thread.

[snip]
> > @@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string matching' '
> >  	test_cmp expect actual &&
> >
> >  	git config --file=config --fixed-value --replace-all fixed.test bogus "$META" &&
> > -	git config --file=config --list >actual &&
> > +	git config ${mode_prefix}list --file=config >actual &&
> >  	cat >expect <<-EOF &&
> >  	fixed.test=bogus
> >  	fixed.test=bogus
> > @@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes failure' '
> >  	test_cmp expect err
> >  '
> >
> > +done
> > +
> >
> Nit: Wouldn't it be better if the tests are indented here? That way you
> know it's part of a loop.

We would basically have to reindent 2500 lines of code. I don't think
that'd be a helpful to reviewers :)

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 00/14] builtin/config: introduce subcommands
  2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
                   ` (12 preceding siblings ...)
  2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
@ 2024-05-06  8:55 ` Patrick Steinhardt
  2024-05-06  8:55   ` [PATCH v5 01/14] config: clarify memory ownership when preparing comment strings Patrick Steinhardt
                     ` (15 more replies)
  13 siblings, 16 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:55 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 5220 bytes --]

Hi,

this is the fifth and hopefully last version of my patch sthat
introduces subcommands into git-config(1).

The only changes compared to v4 are some fixes to commit messages.
Otherwise I'm not aware of any other feedback that would need to be
addressed.

Patrick

Patrick Steinhardt (14):
  config: clarify memory ownership when preparing comment strings
  builtin/config: move option array around
  builtin/config: move "fixed-value" option to correct group
  builtin/config: use `OPT_CMDMODE()` to specify modes
  builtin/config: pull out function to handle config location
  builtin/config: pull out function to handle `--null`
  builtin/config: introduce "list" subcommand
  builtin/config: introduce "get" subcommand
  builtin/config: introduce "set" subcommand
  builtin/config: introduce "unset" subcommand
  builtin/config: introduce "rename-section" subcommand
  builtin/config: introduce "remove-section" subcommand
  builtin/config: introduce "edit" subcommand
  builtin/config: display subcommand help

 Documentation/git-config.txt | 219 ++++++++-------
 builtin/config.c             | 512 ++++++++++++++++++++++++++++-------
 config.c                     |  16 +-
 config.h                     |   2 +-
 t/t0450/txt-help-mismatches  |   1 -
 t/t1300-config.sh            | 432 +++++++++++++++++------------
 6 files changed, 812 insertions(+), 370 deletions(-)

Range-diff against v4:
 1:  3aa26d5bff !  1:  881d2b5426 config: clarify memory ownership when preparing comment strings
    @@ Commit message
         not like this micro-optimization really matters. Thus, callers are now
         always responsible for freeing the value.
     
    +    Signed-off-by: Patrick Steinhardt <ps@pks.im>
    +
      ## builtin/config.c ##
     @@ builtin/config.c: static struct config_options config_options;
      static int show_origin;
 2:  8f0804ab48 =  2:  66dffaa8f2 builtin/config: move option array around
 3:  ddcd8031d7 !  3:  36abda0e02 builtin/config: move "fixed-value" option to correct group
    @@ Commit message
         builtin/config: move "fixed-value" option to correct group
     
         The `--fixed-value` option can be used to alter how the value-pattern
    -    parameter is interpreted for the various submodes of git-config(1). But
    -    while it is an option, it is currently listed as part of the submodes
    -    group the command, which is wrong.
    +    parameter is interpreted for the various actions of git-config(1). But
    +    while it is an option, it is currently listed as part of the actions
    +    group, which is wrong.
     
         Move the option to the "Other" group, which hosts the various options
         known to git-config(1).
 4:  1bc3918840 =  4:  34b66f9c87 builtin/config: use `OPT_CMDMODE()` to specify modes
 5:  3754812309 =  5:  4f90f206e7 builtin/config: pull out function to handle config location
 6:  cb1714c493 =  6:  df1a6f14e6 builtin/config: pull out function to handle `--null`
 7:  b3f3c3ba6a !  7:  1df76a9970 builtin/config: introduce "list" subcommand
    @@ Commit message
         builtin/config: introduce "list" subcommand
     
         While git-config(1) has several modes, those modes are not exposed with
    -    subcommands but instead by specifying e.g. `--unset` or `--list`. This
    -    user interface is not really in line with how our more modern commands
    -    work, where it is a lot more customary to say e.g. `git remote list`.
    -    Furthermore, to add to the confusion, git-config(1) also allows the user
    -    to request modes implicitly by just specifying the correct number of
    -    arguments. Thus, `git config foo.bar` will retrieve the value of
    -    "foo.bar" while `git config foo.bar baz` will set it to "baz".
    +    subcommands but instead by specifying action flags like `--unset` or
    +    `--list`. This user interface is not really in line with how our more
    +    modern commands work, where it is a lot more customary to say e.g. `git
    +    remote list`. Furthermore, to add to the confusion, git-config(1) also
    +    allows the user to request modes implicitly by just specifying the
    +    correct number of arguments. Thus, `git config foo.bar` will retrieve
    +    the value of "foo.bar" while `git config foo.bar baz` will set it to
    +    "baz".
     
         Overall, this makes for a confusing interface that could really use a
         makeover. It hurts discoverability of what you can do with git-config(1)
 8:  0e6da909ac =  8:  29676b81e0 builtin/config: introduce "get" subcommand
 9:  8a623a31b9 =  9:  94afb5a5b7 builtin/config: introduce "set" subcommand
10:  e25e5b69cd = 10:  e525c2326a builtin/config: introduce "unset" subcommand
11:  f24008d356 = 11:  a797889890 builtin/config: introduce "rename-section" subcommand
12:  fc2ddd3201 = 12:  8ec214755e builtin/config: introduce "remove-section" subcommand
13:  4c2d817eff = 13:  1893c23afc builtin/config: introduce "edit" subcommand
14:  4c351b12b8 = 14:  97a48ab81d builtin/config: display subcommand help

base-commit: d4cc1ec35f3bcce816b69986ca41943f6ce21377
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 01/14] config: clarify memory ownership when preparing comment strings
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
@ 2024-05-06  8:55   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 02/14] builtin/config: move option array around Patrick Steinhardt
                     ` (14 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:55 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 4864 bytes --]

The ownership of memory returned when preparing a comment string is
quite intricate: when the returned value is different than the passed
value, then the caller is responsible to free the memory. This is quite
subtle, and it's even easier to miss because the returned value is in
fact a `const char *`.

Adapt the function to always return either `NULL` or a newly allocated
string. The function is called at most once per git-config(1), so it's
not like this micro-optimization really matters. Thus, callers are now
always responsible for freeing the value.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 11 ++++++-----
 config.c         | 16 ++++++----------
 config.h         |  2 +-
 3 files changed, 13 insertions(+), 16 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 0015620dde..40456c0770 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -44,7 +44,7 @@ static struct config_options config_options;
 static int show_origin;
 static int show_scope;
 static int fixed_value;
-static const char *comment;
+static const char *comment_arg;
 
 #define ACTION_GET (1<<0)
 #define ACTION_GET_ALL (1<<1)
@@ -174,7 +174,7 @@ static struct option builtin_config_options[] = {
 	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
 	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_STRING(0, "comment", &comment, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
+	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
 	OPT_END(),
 };
 
@@ -674,7 +674,7 @@ static char *default_user_config(void)
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	int nongit = !startup_info->have_repository;
-	char *value = NULL;
+	char *value = NULL, *comment = NULL;
 	int flags = 0;
 	int ret = 0;
 	struct key_value_info default_kvi = KVI_INIT;
@@ -799,7 +799,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (comment &&
+	if (comment_arg &&
 	    !(actions & (ACTION_ADD|ACTION_SET|ACTION_SET_ALL|ACTION_REPLACE_ALL))) {
 		error(_("--comment is only applicable to add/set/replace operations"));
 		usage_builtin_config();
@@ -841,7 +841,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		flags |= CONFIG_FLAGS_FIXED_VALUE;
 	}
 
-	comment = git_config_prepare_comment_string(comment);
+	comment = git_config_prepare_comment_string(comment_arg);
 
 	if (actions & PAGING_ACTIONS)
 		setup_auto_pager("config", 1);
@@ -993,6 +993,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		return get_colorbool(argv[0], argc == 2);
 	}
 
+	free(comment);
 	free(value);
 	return ret;
 }
diff --git a/config.c b/config.c
index ae3652b08f..13cf9eeb16 100644
--- a/config.c
+++ b/config.c
@@ -3182,14 +3182,10 @@ void git_config_set(const char *key, const char *value)
 	trace2_cmd_set_config(key, value);
 }
 
-/*
- * The ownership rule is that the caller will own the string
- * if it receives a piece of memory different from what it passed
- * as the parameter.
- */
-const char *git_config_prepare_comment_string(const char *comment)
+char *git_config_prepare_comment_string(const char *comment)
 {
 	size_t leading_blanks;
+	char *prepared;
 
 	if (!comment)
 		return NULL;
@@ -3210,13 +3206,13 @@ const char *git_config_prepare_comment_string(const char *comment)
 
 	leading_blanks = strspn(comment, " \t");
 	if (leading_blanks && comment[leading_blanks] == '#')
-		; /* use it as-is */
+		prepared = xstrdup(comment); /* use it as-is */
 	else if (comment[0] == '#')
-		comment = xstrfmt(" %s", comment);
+		prepared = xstrfmt(" %s", comment);
 	else
-		comment = xstrfmt(" # %s", comment);
+		prepared = xstrfmt(" # %s", comment);
 
-	return comment;
+	return prepared;
 }
 
 static void validate_comment_string(const char *comment)
diff --git a/config.h b/config.h
index f4966e3749..db8b608064 100644
--- a/config.h
+++ b/config.h
@@ -338,7 +338,7 @@ void git_config_set_multivar(const char *, const char *, const char *, unsigned)
 int repo_config_set_multivar_gently(struct repository *, const char *, const char *, const char *, unsigned);
 int git_config_set_multivar_in_file_gently(const char *, const char *, const char *, const char *, const char *, unsigned);
 
-const char *git_config_prepare_comment_string(const char *);
+char *git_config_prepare_comment_string(const char *);
 
 /**
  * takes four parameters:
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 02/14] builtin/config: move option array around
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
  2024-05-06  8:55   ` [PATCH v5 01/14] config: clarify memory ownership when preparing comment strings Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 03/14] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
                     ` (13 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 8316 bytes --]

Move around the option array. This will help us with a follow-up commit
that introduces subcommands to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 96 ++++++++++++++++++++++++------------------------
 1 file changed, 48 insertions(+), 48 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 40456c0770..59ae5996eb 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -135,54 +135,6 @@ static int option_parse_type(const struct option *opt, const char *arg,
 	return 0;
 }
 
-static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
-	OPT_GROUP(N_("Action")),
-	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
-	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
-	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
-	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
-	OPT_END(),
-};
-
-static NORETURN void usage_builtin_config(void)
-{
-	usage_with_options(builtin_config_usage, builtin_config_options);
-}
-
 static void check_argc(int argc, int min, int max)
 {
 	if (argc >= min && argc <= max)
@@ -671,6 +623,54 @@ static char *default_user_config(void)
 	return strbuf_detach(&buf, NULL);
 }
 
+static struct option builtin_config_options[] = {
+	OPT_GROUP(N_("Config file location")),
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	OPT_GROUP(N_("Action")),
+	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
+	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
+	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
+	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
+	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
+	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
+	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
+	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_GROUP(N_("Type")),
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	OPT_GROUP(N_("Other")),
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
+	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
+	OPT_END(),
+};
+
+static NORETURN void usage_builtin_config(void)
+{
+	usage_with_options(builtin_config_usage, builtin_config_options);
+}
+
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	int nongit = !startup_info->have_repository;
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 03/14] builtin/config: move "fixed-value" option to correct group
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
  2024-05-06  8:55   ` [PATCH v5 01/14] config: clarify memory ownership when preparing comment strings Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 02/14] builtin/config: move option array around Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 04/14] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
                     ` (12 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 1916 bytes --]

The `--fixed-value` option can be used to alter how the value-pattern
parameter is interpreted for the various actions of git-config(1). But
while it is an option, it is currently listed as part of the actions
group, which is wrong.

Move the option to the "Other" group, which hosts the various options
known to git-config(1).

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/config.c b/builtin/config.c
index 59ae5996eb..054019b70c 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -643,7 +643,6 @@ static struct option builtin_config_options[] = {
 	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
 	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
 	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
 	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
 	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
 	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
@@ -663,6 +662,7 @@ static struct option builtin_config_options[] = {
 	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
 	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
 	OPT_END(),
 };
 
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 04/14] builtin/config: use `OPT_CMDMODE()` to specify modes
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (2 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 03/14] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 05/14] builtin/config: pull out function to handle config location Patrick Steinhardt
                     ` (11 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 5525 bytes --]

The git-config(1) command has various different modes which are
accessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are
declared with `OPT_BIT()`, which causes two minor issues:

  - The respective modes also have a negated form `--no-get-urlmatch`,
    which is unintended.

  - We have to manually handle exclusiveness of the modes.

Switch these options to instead use `OPT_CMDMODE()`, which is made
exactly for this usecase. Remove the now-unneeded check that only a
single mode is given, which is now handled by the parse-options
interface.

While at it, format optional placeholders for arguments to conform to
our style guidelines by using `[<placeholder>]`.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c  | 32 ++++++++++++++------------------
 t/t1300-config.sh | 13 +++++++++++++
 2 files changed, 27 insertions(+), 18 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 054019b70c..8991533e08 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -632,20 +632,20 @@ static struct option builtin_config_options[] = {
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
 	OPT_GROUP(N_("Action")),
-	OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET),
-	OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL),
-	OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP),
-	OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL),
-	OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET),
-	OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL),
-	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
-	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
+	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
+	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
+	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
+	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
+	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
+	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
+	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
+	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
 	OPT_GROUP(N_("Type")),
 	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
 	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
@@ -769,10 +769,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (HAS_MULTI_BITS(actions)) {
-		error(_("only one action at a time"));
-		usage_builtin_config();
-	}
 	if (actions == 0)
 		switch (argc) {
 		case 1: actions = ACTION_GET; break;
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 9b65d9eaf5..86dc70769a 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -2738,4 +2738,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such
 	grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
 '
 
+test_expect_success 'negated mode causes failure' '
+	test_must_fail git config --no-get 2>err &&
+	grep "unknown option \`no-get${SQ}" err
+'
+
+test_expect_success 'specifying multiple modes causes failure' '
+	cat >expect <<-EOF &&
+	error: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together
+	EOF
+	test_must_fail git config --get --get-all 2>err &&
+	test_cmp expect err
+'
+
 test_done
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 05/14] builtin/config: pull out function to handle config location
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (3 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 04/14] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 06/14] builtin/config: pull out function to handle `--null` Patrick Steinhardt
                     ` (10 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 10817 bytes --]

There's quite a bunch of options to git-config(1) that allow the user to
specify which config location to use when reading or writing config
options. The logic to handle this is thus by necessity also quite
involved.

Pull it out into a separate function so that we can reuse it in
subsequent commits which introduce proper subcommands.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 133 ++++++++++++++++++++++++-----------------------
 1 file changed, 68 insertions(+), 65 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 8991533e08..8c7cd30cb4 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -623,68 +623,8 @@ static char *default_user_config(void)
 	return strbuf_detach(&buf, NULL);
 }
 
-static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
-	OPT_GROUP(N_("Action")),
-	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
-	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
-	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
-	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
-	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
-	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
-	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
-	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
-	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
-	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
-	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
-	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
-	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
-	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
-	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
-	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
-	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
-	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
-	OPT_END(),
-};
-
-static NORETURN void usage_builtin_config(void)
+static void handle_config_location(const char *prefix)
 {
-	usage_with_options(builtin_config_usage, builtin_config_options);
-}
-
-int cmd_config(int argc, const char **argv, const char *prefix)
-{
-	int nongit = !startup_info->have_repository;
-	char *value = NULL, *comment = NULL;
-	int flags = 0;
-	int ret = 0;
-	struct key_value_info default_kvi = KVI_INIT;
-
-	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
-
-	argc = parse_options(argc, argv, prefix, builtin_config_options,
-			     builtin_config_usage,
-			     PARSE_OPT_STOP_AT_NON_OPTION);
-
 	if (use_global_config + use_system_config + use_local_config +
 	    use_worktree_config +
 	    !!given_config_source.file + !!given_config_source.blob > 1) {
@@ -692,14 +632,13 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		usage_builtin_config();
 	}
 
-	if (nongit) {
+	if (!startup_info->have_repository) {
 		if (use_local_config)
 			die(_("--local can only be used inside a git repository"));
 		if (given_config_source.blob)
 			die(_("--blob can only be used inside a git repository"));
 		if (use_worktree_config)
 			die(_("--worktree can only be used inside a git repository"));
-
 	}
 
 	if (given_config_source.file &&
@@ -753,10 +692,74 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		config_options.respect_includes = !given_config_source.file;
 	else
 		config_options.respect_includes = respect_includes_opt;
-	if (!nongit) {
+	if (startup_info->have_repository) {
 		config_options.commondir = get_git_common_dir();
 		config_options.git_dir = get_git_dir();
 	}
+}
+
+static struct option builtin_config_options[] = {
+	OPT_GROUP(N_("Config file location")),
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	OPT_GROUP(N_("Action")),
+	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
+	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
+	OPT_CMDMODE(0, "get-regexp", &actions, N_("get values for regexp: name-regex [<value-pattern>]"), ACTION_GET_REGEXP),
+	OPT_CMDMODE(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH),
+	OPT_CMDMODE(0, "replace-all", &actions, N_("replace all matching variables: name value [<value-pattern>]"), ACTION_REPLACE_ALL),
+	OPT_CMDMODE(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD),
+	OPT_CMDMODE(0, "unset", &actions, N_("remove a variable: name [<value-pattern>]"), ACTION_UNSET),
+	OPT_CMDMODE(0, "unset-all", &actions, N_("remove all matches: name [<value-pattern>]"), ACTION_UNSET_ALL),
+	OPT_CMDMODE(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
+	OPT_CMDMODE(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
+	OPT_CMDMODE('l', "list", &actions, N_("list all"), ACTION_LIST),
+	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
+	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
+	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
+	OPT_GROUP(N_("Type")),
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	OPT_GROUP(N_("Other")),
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
+	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
+	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
+	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_END(),
+};
+
+static NORETURN void usage_builtin_config(void)
+{
+	usage_with_options(builtin_config_usage, builtin_config_options);
+}
+
+int cmd_config(int argc, const char **argv, const char *prefix)
+{
+	char *value = NULL, *comment = NULL;
+	int flags = 0;
+	int ret = 0;
+	struct key_value_info default_kvi = KVI_INIT;
+
+	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
+
+	argc = parse_options(argc, argv, prefix, builtin_config_options,
+			     builtin_config_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+
+	handle_config_location(prefix);
 
 	if (end_nul) {
 		term = '\0';
@@ -858,7 +861,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		char *config_file;
 
 		check_argc(argc, 0, 0);
-		if (!given_config_source.file && nongit)
+		if (!given_config_source.file && !startup_info->have_repository)
 			die(_("not in a git directory"));
 		if (given_config_source.use_stdin)
 			die(_("editing stdin is not supported"));
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 06/14] builtin/config: pull out function to handle `--null`
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (4 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 05/14] builtin/config: pull out function to handle config location Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 07/14] builtin/config: introduce "list" subcommand Patrick Steinhardt
                     ` (9 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 1177 bytes --]

Pull out function to handle the `--null` option, which we are about to
reuse in subsequent commits.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 8c7cd30cb4..59877065f8 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -698,6 +698,14 @@ static void handle_config_location(const char *prefix)
 	}
 }
 
+static void handle_nul(void) {
+	if (end_nul) {
+		term = '\0';
+		delim = '\n';
+		key_delim = '\n';
+	}
+}
+
 static struct option builtin_config_options[] = {
 	OPT_GROUP(N_("Config file location")),
 	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
@@ -760,12 +768,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 			     PARSE_OPT_STOP_AT_NON_OPTION);
 
 	handle_config_location(prefix);
-
-	if (end_nul) {
-		term = '\0';
-		delim = '\n';
-		key_delim = '\n';
-	}
+	handle_nul();
 
 	if ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {
 		error(_("--get-color and variable type are incoherent"));
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 07/14] builtin/config: introduce "list" subcommand
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (5 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 06/14] builtin/config: pull out function to handle `--null` Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 08/14] builtin/config: introduce "get" subcommand Patrick Steinhardt
                     ` (8 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 28017 bytes --]

While git-config(1) has several modes, those modes are not exposed with
subcommands but instead by specifying action flags like `--unset` or
`--list`. This user interface is not really in line with how our more
modern commands work, where it is a lot more customary to say e.g. `git
remote list`. Furthermore, to add to the confusion, git-config(1) also
allows the user to request modes implicitly by just specifying the
correct number of arguments. Thus, `git config foo.bar` will retrieve
the value of "foo.bar" while `git config foo.bar baz` will set it to
"baz".

Overall, this makes for a confusing interface that could really use a
makeover. It hurts discoverability of what you can do with git-config(1)
and is comparatively easy to get wrong. Converting the command to have
subcommands instead would go a long way to help address these issues.

One concern in this context is backwards compatibility. Luckily, we can
introduce subcommands without breaking backwards compatibility at all.
This is because all the implicit modes of git-config(1) require that the
first argument is a properly formatted config key. And as config keys
_must_ have a dot in their name, any value without a dot would have been
discarded by git-config(1) previous to this change. Thus, given that
none of the subcommands do have a dot, they are unambiguous.

Introduce the first such new subcommand, which is "git config list". To
retain backwards compatibility we only conditionally use subcommands and
will fall back to the old syntax in case no subcommand was detected.
This should help to transition to the new-style syntax until we
eventually deprecate and remove the old-style syntax.

Note that the way we handle this we're duplicating some functionality
across old and new syntax. While this isn't pretty, it helps us to
ensure that there really is no change in behaviour for the old syntax.

Amend tests such that we run them both with old and new style syntax.
As tests are now run twice, state from the first run may be still be
around in the second run and thus cause tests to fail. Add cleanup logic
as required to fix such tests.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt |  26 ++++++---
 builtin/config.c             |  90 ++++++++++++++++++++++++----
 t/t1300-config.sh            | 110 +++++++++++++++++++++--------------
 3 files changed, 162 insertions(+), 64 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index ac61113fcc..c83c97cb7e 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -9,6 +9,7 @@ git-config - Get and set repository or global options
 SYNOPSIS
 --------
 [verse]
+'git config list' [<file-option>] [<display-option>] [--includes]
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
@@ -20,7 +21,6 @@ SYNOPSIS
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
-'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list
 'git config' [<file-option>] --get-color <name> [<default>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
@@ -74,6 +74,12 @@ On success, the command returns the exit code 0.
 A list of all available configuration variables can be obtained using the
 `git help --config` command.
 
+COMMANDS
+--------
+
+list::
+	List all variables set in config file, along with their values.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -190,10 +196,6 @@ See also <<FILES>>.
 --unset-all::
 	Remove all lines matching the key from config file.
 
--l::
---list::
-	List all variables set in config file, along with their values.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -248,7 +250,7 @@ Valid `<type>`'s include:
 	contain line breaks.
 
 --name-only::
-	Output only the names of config variables for `--list` or
+	Output only the names of config variables for `list` or
 	`--get-regexp`.
 
 --show-origin::
@@ -300,10 +302,20 @@ Valid `<type>`'s include:
   When using `--get`, and the requested variable is not found, behave as if
   <value> were the value assigned to that variable.
 
+DEPRECATED MODES
+----------------
+
+The following modes have been deprecated in favor of subcommands. It is
+recommended to migrate to the new syntax.
+
+-l::
+--list::
+	Replaced by `git config list`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
-using `--list` or any of the `--get-*` which may return multiple results.
+using `list` or any of the `--get-*` which may return multiple results.
 The default is to use a pager.
 
 [[FILES]]
diff --git a/builtin/config.c b/builtin/config.c
index 59877065f8..f89ddce8da 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -16,10 +16,16 @@
 #include "worktree.h"
 
 static const char *const builtin_config_usage[] = {
+	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config [<options>]"),
 	NULL
 };
 
+static const char *const builtin_config_list_usage[] = {
+	N_("git config list [<file-option>] [<display-option>] [--includes]"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -33,6 +39,7 @@ static char delim = '=';
 static char key_delim = ' ';
 static char term = '\n';
 
+static parse_opt_subcommand_fn *subcommand;
 static int use_global_config, use_system_config, use_local_config;
 static int use_worktree_config;
 static struct git_config_source given_config_source;
@@ -706,14 +713,24 @@ static void handle_nul(void) {
 	}
 }
 
+#define CONFIG_LOCATION_OPTIONS \
+	OPT_GROUP(N_("Config file location")), \
+	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")), \
+	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")), \
+	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")), \
+	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")), \
+	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")), \
+	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object"))
+
+#define CONFIG_DISPLAY_OPTIONS \
+	OPT_GROUP(N_("Display options")), \
+	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), \
+	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")), \
+	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")), \
+	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)"))
+
 static struct option builtin_config_options[] = {
-	OPT_GROUP(N_("Config file location")),
-	OPT_BOOL(0, "global", &use_global_config, N_("use global config file")),
-	OPT_BOOL(0, "system", &use_system_config, N_("use system config file")),
-	OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")),
-	OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")),
-	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")),
-	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")),
+	CONFIG_LOCATION_OPTIONS,
 	OPT_GROUP(N_("Action")),
 	OPT_CMDMODE(0, "get", &actions, N_("get value: name [<value-pattern>]"), ACTION_GET),
 	OPT_CMDMODE(0, "get-all", &actions, N_("get all values: key [<value-pattern>]"), ACTION_GET_ALL),
@@ -737,15 +754,12 @@ static struct option builtin_config_options[] = {
 	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
 	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
 	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	CONFIG_DISPLAY_OPTIONS,
 	OPT_GROUP(N_("Other")),
-	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
-	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
-	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
-	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
-	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
 	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
 	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
+	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
 	OPT_END(),
 };
 
@@ -754,6 +768,42 @@ static NORETURN void usage_builtin_config(void)
 	usage_with_options(builtin_config_usage, builtin_config_options);
 }
 
+static int cmd_config_list(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_DISPLAY_OPTIONS,
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);
+	check_argc(argc, 0, 0);
+
+	handle_config_location(prefix);
+	handle_nul();
+
+	setup_auto_pager("config", 1);
+
+	if (config_with_options(show_all_config, NULL,
+				&given_config_source, the_repository,
+				&config_options) < 0) {
+		if (given_config_source.file)
+			die_errno(_("unable to read config file '%s'"),
+				  given_config_source.file);
+		else
+			die(_("error processing config file(s)"));
+	}
+
+	return 0;
+}
+
+static struct option builtin_subcommand_options[] = {
+	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
+	OPT_END(),
+};
+
 int cmd_config(int argc, const char **argv, const char *prefix)
 {
 	char *value = NULL, *comment = NULL;
@@ -763,6 +813,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 
 	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
 
+	/*
+	 * This is somewhat hacky: we first parse the command line while
+	 * keeping all args intact in order to determine whether a subcommand
+	 * has been specified. If so, we re-parse it a second time, but this
+	 * time we drop KEEP_ARGV0. This is so that we don't munge the command
+	 * line in case no subcommand was given, which would otherwise confuse
+	 * us when parsing the legacy-style modes that don't use subcommands.
+	 */
+	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
+	if (subcommand) {
+		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
+		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);
+		return subcommand(argc, argv, prefix);
+	}
+
 	argc = parse_options(argc, argv, prefix, builtin_config_options,
 			     builtin_config_usage,
 			     PARSE_OPT_STOP_AT_NON_OPTION);
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 86dc70769a..f77d2f7847 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -11,6 +11,20 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
+for mode in legacy subcommands
+do
+
+case "$mode" in
+legacy)
+	mode_prefix="--"
+	;;
+subcommands)
+	mode_prefix=""
+	;;
+*)
+	BUG "unknown mode $mode";;
+esac
+
 test_expect_success 'setup whitespace config' '
 	sed -e "s/^|//" \
 	    -e "s/[$]$//" \
@@ -460,11 +474,11 @@ version.1.2.3eX.alpha=beta
 EOF
 
 test_expect_success 'working --list' '
-	git config --list > output &&
+	git config ${mode_prefix}list > output &&
 	test_cmp expect output
 '
 test_expect_success '--list without repo produces empty output' '
-	git --git-dir=nonexistent config --list >output &&
+	git --git-dir=nonexistent config ${mode_prefix}list >output &&
 	test_must_be_empty output
 '
 
@@ -476,7 +490,7 @@ version.1.2.3eX.alpha
 EOF
 
 test_expect_success '--name-only --list' '
-	git config --name-only --list >output &&
+	git config ${mode_prefix}list --name-only >output &&
 	test_cmp expect output
 '
 
@@ -614,17 +628,17 @@ ein.bahn=strasse
 EOF
 
 test_expect_success 'alternative GIT_CONFIG' '
-	GIT_CONFIG=other-config git config --list >output &&
+	GIT_CONFIG=other-config git config ${mode_prefix}list >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'alternative GIT_CONFIG (--file)' '
-	git config --file other-config --list >output &&
+	git config ${mode_prefix}list --file other-config >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'alternative GIT_CONFIG (--file=-)' '
-	git config --file - --list <other-config >output &&
+	git config ${mode_prefix}list --file - <other-config >output &&
 	test_cmp expect output
 '
 
@@ -637,6 +651,7 @@ test_expect_success 'editing stdin is an error' '
 '
 
 test_expect_success 'refer config from subdirectory' '
+	test_when_finished "rm -r x" &&
 	mkdir x &&
 	test_cmp_config -C x strasse --file=../other-config --get ein.bahn
 '
@@ -847,7 +862,7 @@ test_expect_success 'line number is reported correctly' '
 '
 
 test_expect_success 'invalid stdin config' '
-	echo "[broken" | test_must_fail git config --list --file - >output 2>&1 &&
+	echo "[broken" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&
 	test_grep "bad config line 1 in standard input" output
 '
 
@@ -1139,7 +1154,7 @@ section.quotecont=cont;inued
 EOF
 
 test_expect_success 'value continued on next line' '
-	git config --list > result &&
+	git config ${mode_prefix}list > result &&
 	test_cmp expect result
 '
 
@@ -1163,7 +1178,7 @@ Qsection.sub=section.val4
 Qsection.sub=section.val5Q
 EOF
 test_expect_success '--null --list' '
-	git config --null --list >result.raw &&
+	git config ${mode_prefix}list --null >result.raw &&
 	nul_to_q <result.raw >result &&
 	echo >>result &&
 	test_cmp expect result
@@ -1198,6 +1213,7 @@ test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces'
 '
 
 test_expect_success SYMLINKS 'symlinked configuration' '
+	test_when_finished "rm myconfig" &&
 	ln -s notyet myconfig &&
 	git config --file=myconfig test.frotz nitfol &&
 	test -h myconfig &&
@@ -1218,10 +1234,11 @@ test_expect_success SYMLINKS 'symlinked configuration' '
 '
 
 test_expect_success SYMLINKS 'symlink to nonexistent configuration' '
+	test_when_finished "rm linktonada linktolinktonada" &&
 	ln -s doesnotexist linktonada &&
 	ln -s linktonada linktolinktonada &&
-	test_must_fail git config --file=linktonada --list &&
-	test_must_fail git config --file=linktolinktonada --list
+	test_must_fail git config ${mode_prefix}list --file=linktonada &&
+	test_must_fail git config ${mode_prefix}list --file=linktolinktonada
 '
 
 test_expect_success 'check split_cmdline return' '
@@ -1478,7 +1495,7 @@ do
 done
 
 test_expect_success 'git -c is not confused by empty environment' '
-	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config --list
+	GIT_CONFIG_PARAMETERS="" git -c x.one=1 config ${mode_prefix}list
 '
 
 test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
@@ -1669,31 +1686,31 @@ test_expect_success 'git config ignores pairs with empty count' '
 '
 
 test_expect_success 'git config fails with invalid count' '
-	test_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&
+	test_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&
 	test_grep "bogus count" error &&
-	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&
+	test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&
 	test_grep "too many entries" error
 '
 
 test_expect_success 'git config fails with missing config key' '
 	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0="value" \
-		git config --list 2>error &&
+		git config ${mode_prefix}list 2>error &&
 	test_grep "missing config key" error
 '
 
 test_expect_success 'git config fails with missing config value' '
 	test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="pair.one" \
-		git config --list 2>error &&
+		git config ${mode_prefix}list 2>error &&
 	test_grep "missing config value" error
 '
 
 test_expect_success 'git config fails with invalid config pair key' '
 	test_must_fail env GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \
-		git config --list &&
+		git config ${mode_prefix}list &&
 	test_must_fail env GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \
-		git config --list
+		git config ${mode_prefix}list
 '
 
 test_expect_success 'environment overrides config file' '
@@ -1733,7 +1750,7 @@ test_expect_success 'git config --edit works' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
 	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
-	git config -f tmp --list >actual &&
+	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
 
@@ -1742,7 +1759,7 @@ test_expect_success 'git config --edit respects core.editor' '
 	echo test.value=yes >expect &&
 	test_config core.editor "echo [test]value=yes >" &&
 	git config -f tmp --edit &&
-	git config -f tmp --list >actual &&
+	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
 
@@ -2093,7 +2110,7 @@ test_expect_success '--show-origin with --list' '
 	command line:	user.cmdline=true
 	EOF
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\
-		git -c user.cmdline=true config --list --show-origin >output &&
+		git -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2110,7 +2127,7 @@ test_expect_success '--show-origin with --list --null' '
 	includeQcommand line:Quser.cmdline
 	trueQ
 	EOF
-	git -c user.cmdline=true config --null --list --show-origin >output.raw &&
+	git -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&
 	nul_to_q <output.raw >output &&
 	# The here-doc above adds a newline that the --null output would not
 	# include. Add it here to make the two comparable.
@@ -2124,7 +2141,7 @@ test_expect_success '--show-origin with single file' '
 	file:.git/config	user.override=local
 	file:.git/config	include.path=../include/relative.include
 	EOF
-	git config --local --list --show-origin >output &&
+	git config ${mode_prefix}list --local --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2162,7 +2179,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '
 	cat >expect <<-\EOF &&
 	file:"file\" (dq) and spaces.conf"	user.custom=true
 	EOF
-	git config --file "$WEIRDLY_NAMED_FILE" --show-origin --list >output &&
+	git config ${mode_prefix}list --file "$WEIRDLY_NAMED_FILE" --show-origin >output &&
 	test_cmp expect output
 '
 
@@ -2170,7 +2187,7 @@ test_expect_success '--show-origin stdin' '
 	cat >expect <<-\EOF &&
 	standard input:	user.custom=true
 	EOF
-	git config --file - --show-origin --list <"$CUSTOM_CONFIG_FILE" >output &&
+	git config ${mode_prefix}list --file - --show-origin <"$CUSTOM_CONFIG_FILE" >output &&
 	test_cmp expect output
 '
 
@@ -2197,7 +2214,7 @@ test_expect_success '--show-origin blob' '
 		cat >expect <<-EOF &&
 		blob:$blob	user.custom=true
 		EOF
-		git config --blob=$blob --show-origin --list >output &&
+		git config ${mode_prefix}list --blob=$blob --show-origin >output &&
 		test_cmp expect output
 	)
 '
@@ -2213,7 +2230,7 @@ test_expect_success '--show-origin blob ref' '
 		cp "$CUSTOM_CONFIG_FILE" custom.conf &&
 		git add custom.conf &&
 		git commit -m "new config file" &&
-		git config --blob=main:custom.conf --show-origin --list >output &&
+		git config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&
 		test_cmp expect output
 	)
 '
@@ -2239,13 +2256,14 @@ test_expect_success '--show-scope with --list' '
 	worktree	user.worktree=true
 	command	user.cmdline=true
 	EOF
+	test_when_finished "git worktree remove wt1" &&
 	git worktree add wt1 &&
 	# We need these to test for worktree scope, but outside of this
 	# test, this is just noise
 	test_config core.repositoryformatversion 1 &&
 	test_config extensions.worktreeConfig true &&
 	git config --worktree user.worktree true &&
-	git -c user.cmdline=true config --list --show-scope >output &&
+	git -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2254,7 +2272,7 @@ test_expect_success !MINGW '--show-scope with --blob' '
 	cat >expect <<-EOF &&
 	command	user.custom=true
 	EOF
-	git config --blob=$blob --show-scope --list >output &&
+	git config ${mode_prefix}list --blob=$blob --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2264,7 +2282,7 @@ test_expect_success '--show-scope with --local' '
 	local	user.override=local
 	local	include.path=../include/relative.include
 	EOF
-	git config --local --list --show-scope >output &&
+	git config ${mode_prefix}list --local --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2288,7 +2306,7 @@ test_expect_success '--show-scope with --show-origin' '
 	local	file:.git/../include/relative.include	user.relative=include
 	command	command line:	user.cmdline=true
 	EOF
-	git -c user.cmdline=true config --list --show-origin --show-scope >output &&
+	git -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&
 	test_cmp expect output
 '
 
@@ -2329,7 +2347,7 @@ test_expect_success 'override global and system config' '
 	global	home.config=true
 	local	local.config=true
 	EOF
-	git config --show-scope --list >output &&
+	git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output &&
 
 	cat >expect <<-EOF &&
@@ -2338,20 +2356,20 @@ test_expect_success 'override global and system config' '
 	local	local.config=true
 	EOF
 	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \
-		git config --show-scope --list >output &&
+		git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output &&
 
 	cat >expect <<-EOF &&
 	local	local.config=true
 	EOF
 	GIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \
-		git config --show-scope --list >output &&
+		git config ${mode_prefix}list --show-scope >output &&
 	test_cmp expect output
 '
 
 test_expect_success 'override global and system config with missing file' '
-	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&
-	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&
+	test_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&
+	test_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&
 	GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version
 '
 
@@ -2478,7 +2496,7 @@ test_expect_success 'set all config with value-pattern' '
 	# no match => add new entry
 	cp initial config &&
 	git config --file=config abc.key two a+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2491,7 +2509,7 @@ test_expect_success 'set all config with value-pattern' '
 
 	# multiple values, no match => add
 	git config --file=config abc.key three a+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2501,7 +2519,7 @@ test_expect_success 'set all config with value-pattern' '
 
 	# single match => replace
 	git config --file=config abc.key four h+ &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=one
 	abc.key=two
@@ -2516,7 +2534,7 @@ test_expect_success '--replace-all and value-pattern' '
 	git config --file=config --add abc.key two &&
 	git config --file=config --add abc.key three &&
 	git config --file=config --replace-all abc.key four "o+" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	abc.key=four
 	abc.key=three
@@ -2534,7 +2552,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --rename-section dev null &&
 	test_must_fail git config --file=config --fixed-value --remove-section dev &&
-	test_must_fail git config --file=config --fixed-value --list &&
+	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
 
@@ -2555,7 +2573,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
 	fixed.test=bogus
@@ -2564,7 +2582,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config --fixed-value fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-\EOF &&
 	fixed.test=bogus
 	EOF
@@ -2582,7 +2600,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 
 	cp initial config &&
 	git config --file=config --replace-all fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
 	fixed.test=bogus
@@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 	test_cmp expect actual &&
 
 	git config --file=config --fixed-value --replace-all fixed.test bogus "$META" &&
-	git config --file=config --list >actual &&
+	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=bogus
 	fixed.test=bogus
@@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes failure' '
 	test_cmp expect err
 '
 
+done
+
 test_done
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 08/14] builtin/config: introduce "get" subcommand
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (6 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 07/14] builtin/config: introduce "list" subcommand Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 09/14] builtin/config: introduce "set" subcommand Patrick Steinhardt
                     ` (7 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 33245 bytes --]

Introduce a new "get" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt |  93 ++++++++++++-------------
 builtin/config.c             |  69 +++++++++++++++---
 t/t1300-config.sh            | 131 ++++++++++++++++++++++++-----------
 3 files changed, 194 insertions(+), 99 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index c83c97cb7e..d0878663db 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -10,18 +10,14 @@ SYNOPSIS
 --------
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
+'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]
-'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL>
 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
-'git config' [<file-option>] --get-color <name> [<default>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
 
@@ -80,6 +76,12 @@ COMMANDS
 list::
 	List all variables set in config file, along with their values.
 
+get::
+	Emits the value of the specified key. If key is present multiple times
+	in the configuration, emits the last value. If `--all` is specified,
+	emits all values associated with key. Returns error code 1 if key is
+	not present.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -105,22 +107,16 @@ OPTIONS
 	not contain linefeed characters (no multi-line comments are
 	permitted).
 
---get::
-	Get the value for a given key (optionally filtered by a regex
-	matching the value). Returns error code 1 if the key was not
-	found and the last value if multiple key values were found.
-
---get-all::
-	Like get, but returns all values for a multi-valued key.
+--all::
+	With `get`, return all values for a multi-valued key.
 
---get-regexp::
-	Like --get-all, but interprets the name as a regular expression and
-	writes out the key names.  Regular expression matching is currently
-	case-sensitive and done against a canonicalized version of the key
-	in which section and variable names are lowercased, but subsection
-	names are not.
+---regexp::
+	With `get`, interpret the name as a regular expression. Regular
+	expression matching is currently case-sensitive and done against a
+	canonicalized version of the key in which section and variable names
+	are lowercased, but subsection names are not.
 
---get-urlmatch <name> <URL>::
+--url=<URL>::
 	When given a two-part <name> as <section>.<key>, the value for
 	<section>.<URL>.<key> whose <URL> part matches the best to the
 	given URL is returned (if no such key exists, the value for
@@ -251,7 +247,7 @@ Valid `<type>`'s include:
 
 --name-only::
 	Output only the names of config variables for `list` or
-	`--get-regexp`.
+	`get`.
 
 --show-origin::
 	Augment the output of all queried config options with the
@@ -275,17 +271,6 @@ Valid `<type>`'s include:
 	When the color setting for `name` is undefined, the command uses
 	`color.ui` as fallback.
 
---get-color <name> [<default>]::
-
-	Find the color configured for `name` (e.g. `color.diff.new`) and
-	output it as the ANSI color escape sequence to the standard
-	output.  The optional `default` parameter is used instead, if
-	there is no color configured for `name`.
-+
-`--type=color [--default=<default>]` is preferred over `--get-color`
-(but note that `--get-color` will omit the trailing newline printed by
-`--type=color`).
-
 -e::
 --edit::
 	Opens an editor to modify the specified config file; either
@@ -299,7 +284,7 @@ Valid `<type>`'s include:
 	config files.
 
 --default <value>::
-  When using `--get`, and the requested variable is not found, behave as if
+  When using `get`, and the requested variable is not found, behave as if
   <value> were the value assigned to that variable.
 
 DEPRECATED MODES
@@ -308,15 +293,33 @@ DEPRECATED MODES
 The following modes have been deprecated in favor of subcommands. It is
 recommended to migrate to the new syntax.
 
+'git config <name>'::
+	Replaced by `git config get <name>`.
+
 -l::
 --list::
 	Replaced by `git config list`.
 
+--get <name> [<value-pattern>]::
+	Replaced by `git config get [--value=<pattern>] <name>`.
+
+--get-all <name> [<value-pattern>]::
+	Replaced by `git config get [--value=<pattern>] --all --show-names <name>`.
+
+--get-regexp <name-regexp>::
+	Replaced by `git config get --all --show-names --regexp <name-regexp>`.
+
+--get-urlmatch <name> <URL>::
+	Replaced by `git config get --all --show-names --url=<URL> <name>`.
+
+--get-color <name> [<default>]::
+	Replaced by `git config get --type=color [--default=<default>] <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
-using `list` or any of the `--get-*` which may return multiple results.
-The default is to use a pager.
+using `list` or `get` which may return multiple results. The default is to use
+a pager.
 
 [[FILES]]
 FILES
@@ -519,25 +522,19 @@ you have to provide a regex matching the value of exactly one line.
 To query the value for a given key, do
 
 ------------
-% git config --get core.filemode
-------------
-
-or
-
-------------
-% git config core.filemode
+% git config get core.filemode
 ------------
 
 or, to query a multivar:
 
 ------------
-% git config --get core.gitproxy "for kernel.org$"
+% git config get --value="for kernel.org$" core.gitproxy
 ------------
 
 If you want to know all the values for a multivar, do:
 
 ------------
-% git config --get-all core.gitproxy
+% git config get --all --show-names core.gitproxy
 ------------
 
 If you like to live dangerously, you can replace *all* core.gitproxy by a
@@ -571,8 +568,8 @@ script:
 
 ------------
 #!/bin/sh
-WS=$(git config --get-color color.diff.whitespace "blue reverse")
-RESET=$(git config --get-color "" "reset")
+WS=$(git config get --type=color --default="blue reverse" color.diff.whitespace)
+RESET=$(git config get --type=color --default="reset" "")
 echo "${WS}your whitespace color or blue reverse${RESET}"
 ------------
 
@@ -580,11 +577,11 @@ For URLs in `https://weak.example.com`, `http.sslVerify` is set to
 false, while it is set to `true` for all others:
 
 ------------
-% git config --type=bool --get-urlmatch http.sslverify https://good.example.com
+% git config get --type=bool --url=https://good.example.com http.sslverify
 true
-% git config --type=bool --get-urlmatch http.sslverify https://weak.example.com
+% git config get --type=bool --url=https://weak.example.com http.sslverify
 false
-% git config --get-urlmatch http https://weak.example.com
+% git config get --url=https://weak.example.com http
 http.cookieFile /tmp/cookie.txt
 http.sslverify false
 ------------
diff --git a/builtin/config.c b/builtin/config.c
index f89ddce8da..9e7ae49c02 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -17,7 +17,7 @@
 
 static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
-	N_("git config [<options>]"),
+	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	NULL
 };
 
@@ -26,6 +26,11 @@ static const char *const builtin_config_list_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_get_usage[] = {
+	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -722,6 +727,16 @@ static void handle_nul(void) {
 	OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")), \
 	OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object"))
 
+#define CONFIG_TYPE_OPTIONS \
+	OPT_GROUP(N_("Type")), \
+	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type), \
+	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL), \
+	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT), \
+	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT), \
+	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR), \
+	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH), \
+	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE)
+
 #define CONFIG_DISPLAY_OPTIONS \
 	OPT_GROUP(N_("Display options")), \
 	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), \
@@ -746,14 +761,7 @@ static struct option builtin_config_options[] = {
 	OPT_CMDMODE('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
 	OPT_CMDMODE(0, "get-color", &actions, N_("find the color configured: slot [<default>]"), ACTION_GET_COLOR),
 	OPT_CMDMODE(0, "get-colorbool", &actions, N_("find the color setting: slot [<stdout-is-tty>]"), ACTION_GET_COLORBOOL),
-	OPT_GROUP(N_("Type")),
-	OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type),
-	OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL),
-	OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT),
-	OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR),
-	OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH),
-	OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE),
+	CONFIG_TYPE_OPTIONS,
 	CONFIG_DISPLAY_OPTIONS,
 	OPT_GROUP(N_("Other")),
 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
@@ -799,8 +807,51 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix)
 	return 0;
 }
 
+static int cmd_config_get(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL, *url = NULL;
+	int flags = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_TYPE_OPTIONS,
+		OPT_GROUP(N_("Filter options")),
+		OPT_BOOL(0, "all", &do_all, N_("return all values for multi-valued config options")),
+		OPT_BOOL(0, "regexp", &use_key_regexp, N_("interpret the name as a regular expression")),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_STRING(0, "url", &url, N_("URL"), N_("show config matching the given URL")),
+		CONFIG_DISPLAY_OPTIONS,
+		OPT_BOOL(0, "show-names", &show_keys, N_("show config keys in addition to their values")),
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
+		OPT_STRING(0, "default", &default_value, N_("value"), N_("use default value when missing entry")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_get_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_argc(argc, 1, 1);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with 'value-pattern'"));
+	if (default_value && (do_all || url))
+		die(_("--default= cannot be used with --all or --url="));
+	if (url && (do_all || use_key_regexp || value_pattern))
+		die(_("--url= cannot be used with --all, --regexp or --value"));
+
+	handle_config_location(prefix);
+	handle_nul();
+
+	setup_auto_pager("config", 1);
+
+	if (url)
+		return get_urlmatch(argv[0], url);
+	return get_value(argv[0], value_pattern, flags);
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
+	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index f77d2f7847..7ce13ed6cd 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -17,9 +17,15 @@ do
 case "$mode" in
 legacy)
 	mode_prefix="--"
+	mode_get=""
+	mode_get_all="--get-all"
+	mode_get_regexp="--get-regexp"
 	;;
 subcommands)
 	mode_prefix=""
+	mode_get="get"
+	mode_get_all="get --all"
+	mode_get_regexp="get --regexp --all --show-names"
 	;;
 *)
 	BUG "unknown mode $mode";;
@@ -398,7 +404,7 @@ test_expect_success 'multi-valued get-all returns all' '
 	wow
 	wow2 for me
 	EOF
-	git config --get-all nextsection.nonewline >actual &&
+	git config ${mode_get_all} nextsection.nonewline >actual &&
 	test_cmp expect actual
 '
 
@@ -500,7 +506,7 @@ nextsection.nonewline wow2 for me
 EOF
 
 test_expect_success '--get-regexp' '
-	git config --get-regexp in >output &&
+	git config ${mode_get_regexp} in >output &&
 	test_cmp expect output
 '
 
@@ -510,7 +516,7 @@ nextsection.nonewline
 EOF
 
 test_expect_success '--name-only --get-regexp' '
-	git config --name-only --get-regexp in >output &&
+	git config ${mode_get_regexp} --name-only in >output &&
 	test_cmp expect output
 '
 
@@ -521,7 +527,7 @@ EOF
 
 test_expect_success '--add' '
 	git config --add nextsection.nonewline "wow4 for you" &&
-	git config --get-all nextsection.nonewline > output &&
+	git config ${mode_get_all} nextsection.nonewline > output &&
 	test_cmp expect output
 '
 
@@ -543,21 +549,21 @@ test_expect_success 'get variable with empty value' '
 echo novalue.variable > expect
 
 test_expect_success 'get-regexp variable with no value' '
-	git config --get-regexp novalue > output &&
+	git config ${mode_get_regexp} novalue > output &&
 	test_cmp expect output
 '
 
 echo 'novalue.variable true' > expect
 
 test_expect_success 'get-regexp --bool variable with no value' '
-	git config --bool --get-regexp novalue > output &&
+	git config ${mode_get_regexp} --bool novalue > output &&
 	test_cmp expect output
 '
 
 echo 'emptyvalue.variable ' > expect
 
 test_expect_success 'get-regexp variable with empty value' '
-	git config --get-regexp emptyvalue > output &&
+	git config ${mode_get_regexp} emptyvalue > output &&
 	test_cmp expect output
 '
 
@@ -1131,7 +1137,7 @@ test_expect_success 'quoting' '
 '
 
 test_expect_success 'key with newline' '
-	test_must_fail git config "key.with
+	test_must_fail git config ${mode_get} "key.with
 newline" 123'
 
 test_expect_success 'value with newline' 'git config key.sub value.with\\\
@@ -1185,7 +1191,7 @@ test_expect_success '--null --list' '
 '
 
 test_expect_success '--null --get-regexp' '
-	git config --null --get-regexp "val[0-9]" >result.raw &&
+	git config ${mode_get_regexp} --null "val[0-9]" >result.raw &&
 	nul_to_q <result.raw >result &&
 	echo >>result &&
 	test_cmp expect result
@@ -1194,21 +1200,21 @@ test_expect_success '--null --get-regexp' '
 test_expect_success 'inner whitespace kept verbatim, spaces only' '
 	echo "foo   bar" >expect &&
 	git config section.val "foo   bar" &&
-	git config --get section.val >actual &&
+	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '
 	echo "fooQQbar" | q_to_tab >expect &&
 	git config section.val "$(cat expect)" &&
-	git config --get section.val >actual &&
+	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '
 	echo "foo Q  bar" | q_to_tab >expect &&
 	git config section.val "$(cat expect)" &&
-	git config --get section.val >actual &&
+	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
 
@@ -1283,11 +1289,11 @@ test_expect_success 'git -c can represent empty string' '
 '
 
 test_expect_success 'key sanity-checking' '
-	test_must_fail git config foo=bar &&
-	test_must_fail git config foo=.bar &&
-	test_must_fail git config foo.ba=r &&
-	test_must_fail git config foo.1bar &&
-	test_must_fail git config foo."ba
+	test_must_fail git config ${mode_get} foo=bar &&
+	test_must_fail git config ${mode_get} foo=.bar &&
+	test_must_fail git config ${mode_get} foo.ba=r &&
+	test_must_fail git config ${mode_get} foo.1bar &&
+	test_must_fail git config ${mode_get} foo."ba
 				z".bar &&
 	test_must_fail git config . false &&
 	test_must_fail git config .foo false &&
@@ -1336,7 +1342,7 @@ test_expect_success 'git -c complains about empty key and value' '
 '
 
 test_expect_success 'multiple git -c appends config' '
-	test_config alias.x "!git -c x.two=2 config --get-regexp ^x\.*" &&
+	test_config alias.x "!git -c x.two=2 config ${mode_get_regexp} ^x\.*" &&
 	cat >expect <<-\EOF &&
 	x.one 1
 	x.two 2
@@ -1502,7 +1508,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
 	v="${SQ}key.one=foo${SQ}" &&
 	v="$v  ${SQ}key.two=bar${SQ}" &&
 	v="$v ${SQ}key.ambiguous=section.whatever=value${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.one foo
 	key.two bar
@@ -1515,7 +1521,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '
 	v="${SQ}key.one${SQ}=${SQ}foo${SQ}" &&
 	v="$v  ${SQ}key.two${SQ}=${SQ}bar${SQ}" &&
 	v="$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.one foo
 	key.two bar
@@ -1529,7 +1535,7 @@ test_expect_success 'old and new-style entries can mix' '
 	v="$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}" &&
 	v="$v ${SQ}key.oldtwo=oldbar${SQ}" &&
 	v="$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.oldone oldfoo
 	key.newone newfoo
@@ -1542,7 +1548,7 @@ test_expect_success 'old and new-style entries can mix' '
 test_expect_success 'old and new bools with ambiguous subsection' '
 	v="${SQ}key.with=equals.oldbool${SQ}" &&
 	v="$v ${SQ}key.with=equals.newbool${SQ}=" &&
-	GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
+	GIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} "key.*" >actual &&
 	cat >expect <<-EOF &&
 	key.with equals.oldbool
 	key.with=equals.newbool
@@ -1556,7 +1562,7 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
 	env.two two
 	EOF
 	GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ} ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*" >actual &&
+		git config ${mode_get_regexp} "env.*" >actual &&
 	test_cmp expect actual &&
 
 	cat >expect <<-EOF &&
@@ -1564,12 +1570,12 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
 	env.two two
 	EOF
 	GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ}\\$SQ$SQ$SQ ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*" >actual &&
+		git config ${mode_get_regexp} "env.*" >actual &&
 	test_cmp expect actual &&
 
 	test_must_fail env \
 		GIT_CONFIG_PARAMETERS="${SQ}env.one=one${SQ}\\$SQ ${SQ}env.two=two${SQ}" \
-		git config --get-regexp "env.*"
+		git config ${mode_get_regexp} "env.*"
 '
 
 test_expect_success 'git --config-env=key=envvar support' '
@@ -1617,7 +1623,7 @@ test_expect_success 'git -c and --config-env work together' '
 	ENVVAR=env-value git \
 		-c bar.cmd=cmd-value \
 		--config-env=bar.env=ENVVAR \
-		config --get-regexp "^bar.*" >actual &&
+		config ${mode_get_regexp} "^bar.*" >actual &&
 	test_cmp expect actual
 '
 
@@ -1645,7 +1651,7 @@ test_expect_success 'git config handles environment config pairs' '
 	GIT_CONFIG_COUNT=2 \
 		GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="foo" \
 		GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="bar" \
-		git config --get-regexp "pair.*" >actual &&
+		git config ${mode_get_regexp} "pair.*" >actual &&
 	cat >expect <<-EOF &&
 	pair.one foo
 	pair.two bar
@@ -1655,7 +1661,7 @@ test_expect_success 'git config handles environment config pairs' '
 
 test_expect_success 'git config ignores pairs without count' '
 	test_must_fail env GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
@@ -1663,7 +1669,7 @@ test_expect_success 'git config ignores pairs exceeding count' '
 	GIT_CONFIG_COUNT=1 \
 		GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
 		GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="value" \
-		git config --get-regexp "pair.*" >actual 2>error &&
+		git config ${mode_get_regexp} "pair.*" >actual 2>error &&
 	cat >expect <<-EOF &&
 	pair.one value
 	EOF
@@ -1674,14 +1680,14 @@ test_expect_success 'git config ignores pairs exceeding count' '
 test_expect_success 'git config ignores pairs with zero count' '
 	test_must_fail env \
 		GIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
 test_expect_success 'git config ignores pairs with empty count' '
 	test_must_fail env \
 		GIT_CONFIG_COUNT= GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
-		git config pair.one 2>error &&
+		git config ${mode_get} pair.one 2>error &&
 	test_must_be_empty error
 '
 
@@ -1720,7 +1726,7 @@ test_expect_success 'environment overrides config file' '
 	one = value
 	EOF
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \
-		git config pair.one >actual &&
+		git config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1730,7 +1736,7 @@ test_expect_success 'environment overrides config file' '
 test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '
 	GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \
 		GIT_CONFIG_PARAMETERS="${SQ}pair.one=override${SQ}" \
-		git config pair.one >actual &&
+		git config ${mode_get} pair.one >actual &&
 	cat >expect <<-EOF &&
 	override
 	EOF
@@ -1805,20 +1811,28 @@ test_expect_success 'urlmatch' '
 
 	test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&
 	test_must_be_empty actual &&
+	test_expect_code 1 git config get --url=https://good.example.com --bool doesnt.exist >actual &&
+	test_must_be_empty actual &&
 
 	echo true >expect &&
 	git config --bool --get-urlmatch http.SSLverify https://good.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --bool --url=https://good.example.com http.SSLverify >actual &&
+	test_cmp expect actual &&
 
 	echo false >expect &&
 	git config --bool --get-urlmatch http.sslverify https://weak.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --bool --url=https://weak.example.com http.sslverify >actual &&
+	test_cmp expect actual &&
 
 	{
 		echo http.cookiefile /tmp/cookie.txt &&
 		echo http.sslverify false
 	} >expect &&
 	git config --get-urlmatch HTTP https://weak.example.com >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://weak.example.com HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -1834,6 +1848,8 @@ test_expect_success 'urlmatch with --show-scope' '
 	local	http.sslverify false
 	EOF
 	git config --get-urlmatch --show-scope HTTP https://weak.example.com >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://weak.example.com --show-scope HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -1866,45 +1882,67 @@ test_expect_success 'urlmatch favors more specific URLs' '
 	echo http.cookiefile /tmp/root.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com/subdirectory >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com/subdirectory HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://example.com/subdirectory/nested HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/user.txt >expect &&
 	git config --get-urlmatch HTTP https://user@example.com/ >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://user@example.com/ HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/subdirectory.txt >expect &&
 	git config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://averylonguser@example.com/subdirectory HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/preceding.txt >expect &&
 	git config --get-urlmatch HTTP https://preceding.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://preceding.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/wildcard.txt >expect &&
 	git config --get-urlmatch HTTP https://wildcard.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://wildcard.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/sub.txt >expect &&
 	git config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://sub.example.com/wildcardwithsubdomain HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/trailing.txt >expect &&
 	git config --get-urlmatch HTTP https://trailing.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://trailing.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/sub.txt >expect &&
 	git config --get-urlmatch HTTP https://user@sub.example.com >actual &&
 	test_cmp expect actual &&
+	git config get --url=https://user@sub.example.com HTTP >actual &&
+	test_cmp expect actual &&
 
 	echo http.cookiefile /tmp/multiwildcard.txt >expect &&
 	git config --get-urlmatch HTTP https://wildcard.example.org >actual &&
+	test_cmp expect actual &&
+	git config get --url=https://wildcard.example.org HTTP >actual &&
 	test_cmp expect actual
 '
 
@@ -2027,7 +2065,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[one]
 	EOF
 	git config --unset two.subsection.key &&
-	test "not [two subsection]" = "$(git config one.key)" &&
+	test "not [two subsection]" = "$(git config ${mode_get} one.key)" &&
 	test_line_count = 3 .git/config
 '
 
@@ -2150,7 +2188,7 @@ test_expect_success '--show-origin with --get-regexp' '
 	file:$HOME/.gitconfig	user.global true
 	file:.git/config	user.local true
 	EOF
-	git config --show-origin --get-regexp "user\.[g|l].*" >output &&
+	git config ${mode_get_regexp} --show-origin "user\.[g|l].*" >output &&
 	test_cmp expect output
 '
 
@@ -2158,7 +2196,7 @@ test_expect_success '--show-origin getting a single key' '
 	cat >expect <<-\EOF &&
 	file:.git/config	local
 	EOF
-	git config --show-origin user.override >output &&
+	git config ${mode_get} --show-origin user.override >output &&
 	test_cmp expect output
 '
 
@@ -2290,7 +2328,7 @@ test_expect_success '--show-scope getting a single value' '
 	cat >expect <<-\EOF &&
 	local	true
 	EOF
-	git config --show-scope --get user.local >output &&
+	git config ${mode_get} --show-scope user.local >output &&
 	test_cmp expect output
 '
 
@@ -2559,9 +2597,9 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	# These modes complain when --fixed-value has no value-pattern
 	test_must_fail git config --file=config --fixed-value dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --get dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-all dev.null &&
-	test_must_fail git config --file=config --fixed-value --get-regexp "dev.*" &&
+	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
 	test_must_fail git config --file=config --fixed-value --unset dev.null &&
 	test_must_fail git config --file=config --fixed-value --unset-all dev.null
 '
@@ -2591,12 +2629,12 @@ test_expect_success '--fixed-value uses exact string matching' '
 	cp initial config &&
 	test_must_fail git config --file=config --unset fixed.test "$META" &&
 	git config --file=config --fixed-value --unset fixed.test "$META" &&
-	test_must_fail git config --file=config fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
 	test_must_fail git config --file=config --unset-all fixed.test "$META" &&
 	git config --file=config --fixed-value --unset-all fixed.test "$META" &&
-	test_must_fail git config --file=config fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
 	git config --file=config --replace-all fixed.test bogus "$META" &&
@@ -2623,18 +2661,27 @@ test_expect_success '--get and --get-all with --fixed-value' '
 	git config --file=config --add fixed.test "$META" &&
 
 	git config --file=config --get fixed.test bogus &&
+	git config get --file=config --value=bogus fixed.test &&
 	test_must_fail git config --file=config --get fixed.test "$META" &&
+	test_must_fail git config get --file=config --value="$META" fixed.test &&
 	git config --file=config --get --fixed-value fixed.test "$META" &&
+	git config get --file=config --fixed-value --value="$META" fixed.test &&
 	test_must_fail git config --file=config --get --fixed-value fixed.test non-existent &&
 
 	git config --file=config --get-all fixed.test bogus &&
+	git config get --all --file=config --value=bogus fixed.test &&
 	test_must_fail git config --file=config --get-all fixed.test "$META" &&
+	test_must_fail git config get --all --file=config --value="$META" fixed.test &&
 	git config --file=config --get-all --fixed-value fixed.test "$META" &&
+	git config get --all --file=config --value="$META" --fixed-value fixed.test &&
 	test_must_fail git config --file=config --get-all --fixed-value fixed.test non-existent &&
 
 	git config --file=config --get-regexp fixed+ bogus &&
+	git config get --regexp --file=config --value=bogus fixed+ &&
 	test_must_fail git config --file=config --get-regexp fixed+ "$META" &&
+	test_must_fail git config get --regexp --file=config --value="$META" fixed+ &&
 	git config --file=config --get-regexp --fixed-value fixed+ "$META" &&
+	git config get --regexp --file=config --fixed-value --value="$META" fixed+ &&
 	test_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent
 '
 
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 09/14] builtin/config: introduce "set" subcommand
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (7 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 08/14] builtin/config: introduce "get" subcommand Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 10/14] builtin/config: introduce "unset" subcommand Patrick Steinhardt
                     ` (6 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 20661 bytes --]

Introduce a new "set" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt |  38 ++++++++-----
 builtin/config.c             |  63 ++++++++++++++++++++++
 t/t1300-config.sh            | 102 ++++++++++++++++++-----------------
 3 files changed, 140 insertions(+), 63 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index d0878663db..f57fa01085 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -11,9 +11,7 @@ SYNOPSIS
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
-'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
-'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
-'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
+'git config set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 'git config' [<file-option>] --rename-section <old-name> <new-name>
@@ -27,7 +25,7 @@ You can query/set/replace/unset options with this command. The name is
 actually the section and the key separated by a dot, and the value will be
 escaped.
 
-Multiple lines can be added to an option by using the `--add` option.
+Multiple lines can be added to an option by using the `--append` option.
 If you want to update or unset an option which can occur on multiple
 lines, a `value-pattern` (which is an extended regular expression,
 unless the `--fixed-value` option is given) needs to be given.  Only the
@@ -82,6 +80,13 @@ get::
 	emits all values associated with key. Returns error code 1 if key is
 	not present.
 
+set::
+	Set value for one or more config options. By default, this command
+	refuses to write multi-valued config options. Passing `--all` will
+	replace all multi-valued config options with the new value, whereas
+	`--value=` will replace all config options whose values match the given
+	pattern.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -90,10 +95,9 @@ OPTIONS
 	Default behavior is to replace at most one line. This replaces
 	all lines matching the key (and optionally the `value-pattern`).
 
---add::
+--append::
 	Adds a new line to the option without altering any existing
-	values.  This is the same as providing '^$' as the `value-pattern`
-	in `--replace-all`.
+	values. This is the same as providing '--value=^$' in `set`.
 
 --comment <message>::
 	Append a comment at the end of new or modified lines.
@@ -296,6 +300,9 @@ recommended to migrate to the new syntax.
 'git config <name>'::
 	Replaced by `git config get <name>`.
 
+'git config <name> <value> [<value-pattern>]'::
+	Replaced by `git config set [--value=<pattern>] <name> <value>`.
+
 -l::
 --list::
 	Replaced by `git config list`.
@@ -315,6 +322,9 @@ recommended to migrate to the new syntax.
 --get-color <name> [<default>]::
 	Replaced by `git config get --type=color [--default=<default>] <name>`.
 
+--add <name> <value>::
+	Replaced by `git config set --append <name> <value>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
@@ -361,7 +371,7 @@ precedence over values read earlier.  When multiple values are taken then all
 values of a key from all files will be used.
 
 By default, options are only written to the repository specific
-configuration file. Note that this also affects options like `--replace-all`
+configuration file. Note that this also affects options like `set`
 and `--unset`. *'git config' will only ever change one file at a time*.
 
 You can limit which configuration sources are read from or written to by
@@ -497,7 +507,7 @@ Given a .git/config like this:
 you can set the filemode to true with
 
 ------------
-% git config core.filemode true
+% git config set core.filemode true
 ------------
 
 The hypothetical proxy command entries actually have a postfix to discern
@@ -505,7 +515,7 @@ what URL they apply to. Here is how to change the entry for kernel.org
 to "ssh".
 
 ------------
-% git config core.gitproxy '"ssh" for kernel.org' 'for kernel.org$'
+% git config set --value='for kernel.org$' core.gitproxy '"ssh" for kernel.org'
 ------------
 
 This makes sure that only the key/value pair for kernel.org is replaced.
@@ -541,26 +551,26 @@ If you like to live dangerously, you can replace *all* core.gitproxy by a
 new one with
 
 ------------
-% git config --replace-all core.gitproxy ssh
+% git config set --all core.gitproxy ssh
 ------------
 
 However, if you really only want to replace the line for the default proxy,
 i.e. the one without a "for ..." postfix, do something like this:
 
 ------------
-% git config core.gitproxy ssh '! for '
+% git config set --value='! for ' core.gitproxy ssh
 ------------
 
 To actually match only values with an exclamation mark, you have to
 
 ------------
-% git config section.key value '[!]'
+% git config set --value='[!]' section.key value
 ------------
 
 To add a new proxy, without altering any of the existing ones, use
 
 ------------
-% git config --add core.gitproxy '"proxy-command" for example.com'
+% git config set --append core.gitproxy '"proxy-command" for example.com'
 ------------
 
 An example to use customized color from the configuration in your
diff --git a/builtin/config.c b/builtin/config.c
index 9e7ae49c02..87fd12fda2 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -18,6 +18,7 @@
 static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
+	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	NULL
 };
 
@@ -31,6 +32,11 @@ static const char *const builtin_config_get_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_set_usage[] = {
+	N_("git config set [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -849,9 +855,66 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix)
 	return get_value(argv[0], value_pattern, flags);
 }
 
+static int cmd_config_set(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL, *comment_arg = NULL;
+	char *comment = NULL;
+	int flags = 0, append = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_TYPE_OPTIONS,
+		OPT_GROUP(N_("Filter")),
+		OPT_BIT(0, "all", &flags, N_("replace multi-valued config option with new value"), CONFIG_FLAGS_MULTI_REPLACE),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_GROUP(N_("Other")),
+		OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
+		OPT_BOOL(0, "append", &append, N_("add a new line without altering any existing values")),
+		OPT_END(),
+	};
+	struct key_value_info default_kvi = KVI_INIT;
+	char *value;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 2, 2);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with --value=<pattern>"));
+	if (append && value_pattern)
+		die(_("--append cannot be used with --value=<pattern>"));
+	if (append)
+		value_pattern = CONFIG_REGEX_NONE;
+
+	comment = git_config_prepare_comment_string(comment_arg);
+
+	handle_config_location(prefix);
+
+	value = normalize_value(argv[0], argv[1], &default_kvi);
+
+	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {
+		ret = git_config_set_multivar_in_file_gently(given_config_source.file,
+							     argv[0], value, value_pattern,
+							     comment, flags);
+	} else {
+		ret = git_config_set_in_file_gently(given_config_source.file,
+						    argv[0], comment, value);
+		if (ret == CONFIG_NOTHING_SET)
+			error(_("cannot overwrite multiple values with a single value\n"
+			"       Use a regexp, --add or --replace-all to change %s."), argv[0]);
+	}
+
+	free(comment);
+	free(value);
+	return ret;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
+	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 7ce13ed6cd..aa3f179be5 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -20,12 +20,16 @@ legacy)
 	mode_get=""
 	mode_get_all="--get-all"
 	mode_get_regexp="--get-regexp"
+	mode_set=""
+	mode_replace_all="--replace-all"
 	;;
 subcommands)
 	mode_prefix=""
 	mode_get="get"
 	mode_get_all="get --all"
 	mode_get_regexp="get --regexp --all --show-names"
+	mode_set="set"
+	mode_replace_all="set --all"
 	;;
 *)
 	BUG "unknown mode $mode";;
@@ -132,7 +136,7 @@ cat > expect << EOF
 	penguin = little blue
 EOF
 test_expect_success 'initial' '
-	git config section.penguin "little blue" &&
+	git config ${mode_set} section.penguin "little blue" &&
 	test_cmp expect .git/config
 '
 
@@ -142,7 +146,7 @@ cat > expect << EOF
 	Movie = BadPhysics
 EOF
 test_expect_success 'mixed case' '
-	git config Section.Movie BadPhysics &&
+	git config ${mode_set} Section.Movie BadPhysics &&
 	test_cmp expect .git/config
 '
 
@@ -154,7 +158,7 @@ cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'similar section' '
-	git config Sections.WhatEver Second &&
+	git config ${mode_set} Sections.WhatEver Second &&
 	test_cmp expect .git/config
 '
 
@@ -167,7 +171,7 @@ cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'uppercase section' '
-	git config SECTION.UPPERCASE true &&
+	git config ${mode_set} SECTION.UPPERCASE true &&
 	test_cmp expect .git/config
 '
 
@@ -194,8 +198,8 @@ EOF
 
 test_expect_success 'append comments' '
 	git config --replace-all --comment="Pygoscelis papua" section.penguin gentoo &&
-	git config --comment="find fish" section.disposition peckish &&
-	git config --comment="#abc" section.foo bar &&
+	git config ${mode_set} --comment="find fish" section.disposition peckish &&
+	git config ${mode_set} --comment="#abc" section.foo bar &&
 
 	git config --comment="and comment" section.spsp value &&
 	git config --comment="	# and comment" section.htsp value &&
@@ -204,7 +208,7 @@ test_expect_success 'append comments' '
 '
 
 test_expect_success 'Prohibited LF in comment' '
-	test_must_fail git config --comment="a${LF}b" section.k v
+	test_must_fail git config ${mode_set} --comment="a${LF}b" section.k v
 '
 
 test_expect_success 'non-match result' 'test_cmp expect .git/config'
@@ -301,14 +305,14 @@ test_expect_success 'multiple unset is correct' '
 cp .git/config2 .git/config
 
 test_expect_success '--replace-all missing value' '
-	test_must_fail git config --replace-all beta.haha &&
+	test_must_fail git config ${mode_replace_all} beta.haha &&
 	test_cmp .git/config2 .git/config
 '
 
 rm .git/config2
 
 test_expect_success '--replace-all' '
-	git config --replace-all beta.haha gamma
+	git config ${mode_replace_all} beta.haha gamma
 '
 
 cat > expect << EOF
@@ -335,7 +339,7 @@ noIndent= sillyValue ; 'nother silly comment
 [nextSection] noNewline = ouch
 EOF
 test_expect_success 'really mean test' '
-	git config beta.haha alpha &&
+	git config ${mode_set} beta.haha alpha &&
 	test_cmp expect .git/config
 '
 
@@ -350,7 +354,7 @@ noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'really really mean test' '
-	git config nextsection.nonewline wow &&
+	git config ${mode_set} nextsection.nonewline wow &&
 	test_cmp expect .git/config
 '
 
@@ -824,16 +828,16 @@ EOF
 
 test_expect_success 'section ending' '
 	rm -f .git/config &&
-	git config gitcvs.enabled true &&
-	git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
-	git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.enabled true &&
+	git config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
 	test_cmp expect .git/config
 
 '
 
 test_expect_success numbers '
-	git config kilo.gram 1k &&
-	git config mega.ton 1m &&
+	git config ${mode_set} kilo.gram 1k &&
+	git config ${mode_set} mega.ton 1m &&
 	echo 1024 >expect &&
 	echo 1048576 >>expect &&
 	git config --int --get kilo.gram >actual &&
@@ -842,20 +846,20 @@ test_expect_success numbers '
 '
 
 test_expect_success '--int is at least 64 bits' '
-	git config giga.watts 121g &&
+	git config ${mode_set} giga.watts 121g &&
 	echo  >expect &&
 	test_cmp_config 129922760704 --int --get giga.watts
 '
 
 test_expect_success 'invalid unit' '
-	git config aninvalid.unit "1auto" &&
+	git config ${mode_set} aninvalid.unit "1auto" &&
 	test_cmp_config 1auto aninvalid.unit &&
 	test_must_fail git config --int --get aninvalid.unit 2>actual &&
 	test_grep "bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit" actual
 '
 
 test_expect_success 'invalid unit boolean' '
-	git config commit.gpgsign "1true" &&
+	git config ${mode_set} commit.gpgsign "1true" &&
 	test_cmp_config 1true commit.gpgsign &&
 	test_must_fail git config --bool --get commit.gpgsign 2>actual &&
 	test_grep "bad boolean config value .1true. for .commit.gpgsign." actual
@@ -885,14 +889,14 @@ EOF
 
 test_expect_success bool '
 
-	git config bool.true1 01 &&
-	git config bool.true2 -1 &&
-	git config bool.true3 YeS &&
-	git config bool.true4 true &&
-	git config bool.false1 000 &&
-	git config bool.false2 "" &&
-	git config bool.false3 nO &&
-	git config bool.false4 FALSE &&
+	git config ${mode_set} bool.true1 01 &&
+	git config ${mode_set} bool.true2 -1 &&
+	git config ${mode_set} bool.true3 YeS &&
+	git config ${mode_set} bool.true4 true &&
+	git config ${mode_set} bool.false1 000 &&
+	git config ${mode_set} bool.false2 "" &&
+	git config ${mode_set} bool.false3 nO &&
+	git config ${mode_set} bool.false4 FALSE &&
 	rm -f result &&
 	for i in 1 2 3 4
 	do
@@ -903,7 +907,7 @@ test_expect_success bool '
 
 test_expect_success 'invalid bool (--get)' '
 
-	git config bool.nobool foobar &&
+	git config ${mode_set} bool.nobool foobar &&
 	test_must_fail git config --bool --get bool.nobool'
 
 test_expect_success 'invalid bool (set)' '
@@ -1092,7 +1096,7 @@ test_expect_success 'get --expiry-date' '
 
 test_expect_success 'get --type=color' '
 	rm .git/config &&
-	git config foo.color "red" &&
+	git config ${mode_set} foo.color "red" &&
 	git config --get --type=color foo.color >actual.raw &&
 	test_decode_color <actual.raw >actual &&
 	echo "<RED>" >expect &&
@@ -1129,10 +1133,10 @@ cat > expect << EOF
 EOF
 test_expect_success 'quoting' '
 	rm -f .git/config &&
-	git config quote.leading " test" &&
-	git config quote.ending "test " &&
-	git config quote.semicolon "test;test" &&
-	git config quote.hash "test#test" &&
+	git config ${mode_set} quote.leading " test" &&
+	git config ${mode_set} quote.ending "test " &&
+	git config ${mode_set} quote.semicolon "test;test" &&
+	git config ${mode_set} quote.hash "test#test" &&
 	test_cmp expect .git/config
 '
 
@@ -1140,7 +1144,7 @@ test_expect_success 'key with newline' '
 	test_must_fail git config ${mode_get} "key.with
 newline" 123'
 
-test_expect_success 'value with newline' 'git config key.sub value.with\\\
+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\
 newline'
 
 cat > .git/config <<\EOF
@@ -1199,21 +1203,21 @@ test_expect_success '--null --get-regexp' '
 
 test_expect_success 'inner whitespace kept verbatim, spaces only' '
 	echo "foo   bar" >expect &&
-	git config section.val "foo   bar" &&
+	git config ${mode_set} section.val "foo   bar" &&
 	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '
 	echo "fooQQbar" | q_to_tab >expect &&
-	git config section.val "$(cat expect)" &&
+	git config ${mode_set} section.val "$(cat expect)" &&
 	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
 
 test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '
 	echo "foo Q  bar" | q_to_tab >expect &&
-	git config section.val "$(cat expect)" &&
+	git config ${mode_set} section.val "$(cat expect)" &&
 	git config ${mode_get} section.val >actual &&
 	test_cmp expect actual
 '
@@ -1252,12 +1256,12 @@ test_expect_success 'check split_cmdline return' '
 	git init repo &&
 	(
 		cd repo &&
-		git config alias.split-cmdline-fix "echo \"" &&
+		git config ${mode_set} alias.split-cmdline-fix "echo \"" &&
 		test_must_fail git split-cmdline-fix &&
 		echo foo >foo &&
 		git add foo &&
 		git commit -m "initial commit" &&
-		git config branch.main.mergeoptions "echo \"" &&
+		git config ${mode_set} branch.main.mergeoptions "echo \"" &&
 		test_must_fail git merge main
 	)
 '
@@ -1295,12 +1299,12 @@ test_expect_success 'key sanity-checking' '
 	test_must_fail git config ${mode_get} foo.1bar &&
 	test_must_fail git config ${mode_get} foo."ba
 				z".bar &&
-	test_must_fail git config . false &&
-	test_must_fail git config .foo false &&
-	test_must_fail git config foo. false &&
-	test_must_fail git config .foo. false &&
-	git config foo.bar true &&
-	git config foo."ba =z".bar false
+	test_must_fail git config ${mode_set} . false &&
+	test_must_fail git config ${mode_set} .foo false &&
+	test_must_fail git config ${mode_set} foo. false &&
+	test_must_fail git config ${mode_set} .foo. false &&
+	git config ${mode_set} foo.bar true &&
+	git config ${mode_set} foo."ba =z".bar false
 '
 
 test_expect_success 'git -c works with aliases of builtins' '
@@ -2523,7 +2527,7 @@ test_expect_success '--replace-all does not invent newlines' '
 	[abc]
 	Qkey = b
 	EOF
-	git config --replace-all abc.key b &&
+	git config ${mode_replace_all} abc.key b &&
 	test_cmp expect .git/config
 '
 
@@ -2595,8 +2599,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
 
 	# These modes complain when --fixed-value has no value-pattern
-	test_must_fail git config --file=config --fixed-value dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
+	test_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&
+	test_must_fail git config ${mode_replace_all} --file=config --fixed-value dev.null bogus &&
 	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
@@ -2637,7 +2641,7 @@ test_expect_success '--fixed-value uses exact string matching' '
 	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
-	git config --file=config --replace-all fixed.test bogus "$META" &&
+	git config --file=config fixed.test bogus "$META" &&
 	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 10/14] builtin/config: introduce "unset" subcommand
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (8 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 09/14] builtin/config: introduce "set" subcommand Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 11/14] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
                     ` (5 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 11914 bytes --]

Introduce a new "unset" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 27 +++++++++++++---------
 builtin/config.c             | 39 ++++++++++++++++++++++++++++++++
 t/t1300-config.sh            | 44 ++++++++++++++++++++++++------------
 3 files changed, 84 insertions(+), 26 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index f57fa01085..5575fe55ea 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -11,9 +11,8 @@ SYNOPSIS
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
-'git config set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>
-'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
-'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
+'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
+'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config' [<file-option>] --rename-section <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
@@ -87,6 +86,12 @@ set::
 	`--value=` will replace all config options whose values match the given
 	pattern.
 
+unset::
+	Unset value for one or more config options. By default, this command
+	refuses to unset multi-valued keys. Passing `--all` will unset all
+	multi-valued config options, whereas `--value` will unset all config
+	options whose values match the given pattern.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -190,12 +195,6 @@ See also <<FILES>>.
 --rename-section::
 	Rename the given section to a new name.
 
---unset::
-	Remove the line matching the key from config file.
-
---unset-all::
-	Remove all lines matching the key from config file.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -325,6 +324,12 @@ recommended to migrate to the new syntax.
 --add <name> <value>::
 	Replaced by `git config set --append <name> <value>`.
 
+--unset <name> [<value-pattern>]::
+	Replaced by `git config unset [--value=<pattern>] <name>`.
+
+--unset-all <name> [<value-pattern>]::
+	Replaced by `git config unset [--value=<pattern>] --all <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
@@ -372,7 +377,7 @@ values of a key from all files will be used.
 
 By default, options are only written to the repository specific
 configuration file. Note that this also affects options like `set`
-and `--unset`. *'git config' will only ever change one file at a time*.
+and `unset`. *'git config' will only ever change one file at a time*.
 
 You can limit which configuration sources are read from or written to by
 specifying the path of a file with the `--file` option, or by specifying a
@@ -523,7 +528,7 @@ This makes sure that only the key/value pair for kernel.org is replaced.
 To delete the entry for renames, do
 
 ------------
-% git config --unset diff.renames
+% git config unset diff.renames
 ------------
 
 If you want to delete an entry for a multivar (like core.gitproxy above),
diff --git a/builtin/config.c b/builtin/config.c
index 87fd12fda2..2578d4c930 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -19,6 +19,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	NULL
 };
 
@@ -37,6 +38,11 @@ static const char *const builtin_config_set_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_unset_usage[] = {
+	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -911,10 +917,43 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix)
 	return ret;
 }
 
+static int cmd_config_unset(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL;
+	int flags = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_GROUP(N_("Filter")),
+		OPT_BIT(0, "all", &flags, N_("replace multi-valued config option with new value"), CONFIG_FLAGS_MULTI_REPLACE),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_unset_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 1, 1);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with 'value-pattern'"));
+
+	handle_config_location(prefix);
+
+	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)
+		return git_config_set_multivar_in_file_gently(given_config_source.file,
+							      argv[0], NULL, value_pattern,
+							      NULL, flags);
+	else
+		return git_config_set_in_file_gently(given_config_source.file, argv[0],
+						     NULL, NULL);
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
+	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index aa3f179be5..2ab58bbd95 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -22,6 +22,8 @@ legacy)
 	mode_get_regexp="--get-regexp"
 	mode_set=""
 	mode_replace_all="--replace-all"
+	mode_unset="--unset"
+	mode_unset_all="--unset-all"
 	;;
 subcommands)
 	mode_prefix=""
@@ -30,6 +32,8 @@ subcommands)
 	mode_get_regexp="get --regexp --all --show-names"
 	mode_set="set"
 	mode_replace_all="set --all"
+	mode_unset="unset"
+	mode_unset_all="unset --all"
 	;;
 *)
 	BUG "unknown mode $mode";;
@@ -259,7 +263,7 @@ foo = bar
 EOF
 
 test_expect_success 'unset with cont. lines' '
-	git config --unset beta.baz
+	git config ${mode_unset} beta.baz
 '
 
 cat > expect <<\EOF
@@ -286,7 +290,7 @@ EOF
 cp .git/config .git/config2
 
 test_expect_success 'multiple unset' '
-	git config --unset-all beta.haha
+	git config ${mode_unset_all} beta.haha
 '
 
 cat > expect << EOF
@@ -372,7 +376,7 @@ noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'unset' '
-	git config --unset beta.haha &&
+	git config ${mode_unset} beta.haha &&
 	test_cmp expect .git/config
 '
 
@@ -428,11 +432,11 @@ test_expect_success 'multivar replace' '
 '
 
 test_expect_success 'ambiguous unset' '
-	test_must_fail git config --unset nextsection.nonewline
+	test_must_fail git config ${mode_unset} nextsection.nonewline
 '
 
 test_expect_success 'invalid unset' '
-	test_must_fail git config --unset somesection.nonewline
+	test_must_fail git config ${mode_unset} somesection.nonewline
 '
 
 cat > expect << EOF
@@ -446,7 +450,12 @@ noIndent= sillyValue ; 'nother silly comment
 EOF
 
 test_expect_success 'multivar unset' '
-	git config --unset nextsection.nonewline "wow3$" &&
+	case "$mode" in
+	legacy)
+		git config --unset nextsection.nonewline "wow3$";;
+	subcommands)
+		git config unset --value="wow3$" nextsection.nonewline;;
+	esac &&
 	test_cmp expect .git/config
 '
 
@@ -2013,7 +2022,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	# please be careful when you update the above variable
 	EOF
 
-	git config --unset section.key &&
+	git config ${mode_unset} section.key &&
 	test_cmp expect .git/config &&
 
 	cat >.git/config <<-\EOF &&
@@ -2026,7 +2035,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[next-section]
 	EOF
 
-	git config --unset section.key &&
+	git config ${mode_unset} section.key &&
 	test_cmp expect .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -2036,7 +2045,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[two]
 	key = true
 	EOF
-	git config --unset two.key &&
+	git config ${mode_unset} two.key &&
 	! grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -2046,7 +2055,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[one]
 	key = true
 	EOF
-	git config --unset-all one.key &&
+	git config ${mode_unset_all} one.key &&
 	test_line_count = 0 .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -2056,7 +2065,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[two]
 	Qkey = true
 	EOF
-	git config --unset two.key &&
+	git config ${mode_unset} two.key &&
 	grep two .git/config &&
 
 	q_to_tab >.git/config <<-\EOF &&
@@ -2068,7 +2077,7 @@ test_expect_success '--unset last key removes section (except if commented)' '
 	[TWO "subsection"]
 	[one]
 	EOF
-	git config --unset two.subsection.key &&
+	git config ${mode_unset} two.subsection.key &&
 	test "not [two subsection]" = "$(git config ${mode_get} one.key)" &&
 	test_line_count = 3 .git/config
 '
@@ -2080,7 +2089,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' '
 	key = value2
 	EOF
 
-	git config --unset-all section.key &&
+	git config ${mode_unset_all} section.key &&
 	test_line_count = 0 .git/config
 '
 
@@ -2604,8 +2613,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
-	test_must_fail git config --file=config --fixed-value --unset dev.null &&
-	test_must_fail git config --file=config --fixed-value --unset-all dev.null
+	test_must_fail git config ${mode_unset} --file=config --fixed-value dev.null &&
+	test_must_fail git config ${mode_unset_all} --file=config --fixed-value dev.null
 '
 
 test_expect_success '--fixed-value uses exact string matching' '
@@ -2635,6 +2644,11 @@ test_expect_success '--fixed-value uses exact string matching' '
 	git config --file=config --fixed-value --unset fixed.test "$META" &&
 	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
+	cp initial config &&
+	test_must_fail git config unset --file=config --value="$META" fixed.test &&
+	git config unset --file=config --fixed-value --value="$META" fixed.test &&
+	test_must_fail git config ${mode_get} --file=config fixed.test &&
+
 	cp initial config &&
 	test_must_fail git config --file=config --unset-all fixed.test "$META" &&
 	git config --file=config --fixed-value --unset-all fixed.test "$META" &&
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 11/14] builtin/config: introduce "rename-section" subcommand
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (9 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 10/14] builtin/config: introduce "unset" subcommand Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 12/14] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
                     ` (4 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 8443 bytes --]

Introduce a new "rename-section" subcommand to git-config(1). Please
refer to preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 11 +++++++----
 builtin/config.c             | 32 ++++++++++++++++++++++++++++++++
 t/t1300-config.sh            | 22 +++++++++++-----------
 3 files changed, 50 insertions(+), 15 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 5575fe55ea..ede86ad085 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -13,7 +13,7 @@ SYNOPSIS
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
-'git config' [<file-option>] --rename-section <old-name> <new-name>
+'git config rename-section' [<file-option>] <old-name> <new-name>
 'git config' [<file-option>] --remove-section <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
@@ -92,6 +92,9 @@ unset::
 	multi-valued config options, whereas `--value` will unset all config
 	options whose values match the given pattern.
 
+rename-section::
+	Rename the given section to a new name.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -192,9 +195,6 @@ See also <<FILES>>.
 --remove-section::
 	Remove the given section from the configuration file.
 
---rename-section::
-	Rename the given section to a new name.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -330,6 +330,9 @@ recommended to migrate to the new syntax.
 --unset-all <name> [<value-pattern>]::
 	Replaced by `git config unset [--value=<pattern>] --all <name>`.
 
+--rename-section <old-name> <new-name>::
+	Replaced by `git config rename-section <old-name> <new-name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index 2578d4c930..a60dca9b2b 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -20,6 +20,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	NULL
 };
 
@@ -43,6 +44,11 @@ static const char *const builtin_config_unset_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_rename_section_usage[] = {
+	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -949,11 +955,37 @@ static int cmd_config_unset(int argc, const char **argv, const char *prefix)
 						     NULL, NULL);
 }
 
+static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_rename_section_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 2, 2);
+
+	handle_config_location(prefix);
+
+	ret = git_config_rename_section_in_file(given_config_source.file,
+						argv[0], argv[1]);
+	if (ret < 0)
+		return ret;
+	else if (!ret)
+		die(_("no such section: %s"), argv[0]);
+
+	return 0;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
+	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 2ab58bbd95..5d7b08fa4c 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -699,7 +699,7 @@ weird
 EOF
 
 test_expect_success 'rename section' '
-	git config --rename-section branch.eins branch.zwei
+	git config ${mode_prefix}rename-section branch.eins branch.zwei
 '
 
 cat > expect << EOF
@@ -718,7 +718,7 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'rename non-existing section' '
-	test_must_fail git config --rename-section \
+	test_must_fail git config ${mode_prefix}rename-section \
 		branch."world domination" branch.drei
 '
 
@@ -727,7 +727,7 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'rename another section' '
-	git config --rename-section branch."1 234 blabl/a" branch.drei
+	git config ${mode_prefix}rename-section branch."1 234 blabl/a" branch.drei
 '
 
 cat > expect << EOF
@@ -750,7 +750,7 @@ cat >> .git/config << EOF
 EOF
 
 test_expect_success 'rename a section with a var on the same line' '
-	git config --rename-section branch.vier branch.zwei
+	git config ${mode_prefix}rename-section branch.vier branch.zwei
 '
 
 cat > expect << EOF
@@ -771,11 +771,11 @@ test_expect_success 'rename succeeded' '
 '
 
 test_expect_success 'renaming empty section name is rejected' '
-	test_must_fail git config --rename-section branch.zwei ""
+	test_must_fail git config ${mode_prefix}rename-section branch.zwei ""
 '
 
 test_expect_success 'renaming to bogus section is rejected' '
-	test_must_fail git config --rename-section branch.zwei "bogus name"
+	test_must_fail git config ${mode_prefix}rename-section branch.zwei "bogus name"
 '
 
 test_expect_success 'renaming a section with a long line' '
@@ -784,7 +784,7 @@ test_expect_success 'renaming a section with a long line' '
 		printf "  c = d %1024s [a] e = f\\n" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	git config -f y --rename-section a xyz &&
+	git config ${mode_prefix}rename-section -f y a xyz &&
 	test_must_fail git config -f y b.e
 '
 
@@ -794,7 +794,7 @@ test_expect_success 'renaming an embedded section with a long line' '
 		printf "  c = d %1024s [a] [foo] e = f\\n" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	git config -f y --rename-section a xyz &&
+	git config ${mode_prefix}rename-section -f y a xyz &&
 	test_must_fail git config -f y foo.e
 '
 
@@ -804,7 +804,7 @@ test_expect_success 'renaming a section with an overly-long line' '
 		printf "  c = d %525000s e" " " &&
 		printf "[a] g = h\\n"
 	} >y &&
-	test_must_fail git config -f y --rename-section a xyz 2>err &&
+	test_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&
 	grep "refusing to work with overly long line in .y. on line 2" err
 '
 
@@ -2112,7 +2112,7 @@ test_expect_success POSIXPERM,PERL 'preserves existing permissions' '
 	git config imap.pass Hunter2 &&
 	perl -e \
 	  "die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600" &&
-	git config --rename-section imap pop &&
+	git config ${mode_prefix}rename-section imap pop &&
 	perl -e \
 	  "die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600"
 '
@@ -2601,7 +2601,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --add dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --rename-section dev null &&
+	test_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&
 	test_must_fail git config --file=config --fixed-value --remove-section dev &&
 	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 12/14] builtin/config: introduce "remove-section" subcommand
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (10 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 11/14] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 13/14] builtin/config: introduce "edit" subcommand Patrick Steinhardt
                     ` (3 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 5327 bytes --]

Introduce a new "remove-section" subcommand to git-config(1). Please
refer to preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 11 +++++++----
 builtin/config.c             | 32 ++++++++++++++++++++++++++++++++
 t/t1300-config.sh            |  4 ++--
 3 files changed, 41 insertions(+), 6 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index ede86ad085..ef46520c15 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -14,7 +14,7 @@ SYNOPSIS
 'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config rename-section' [<file-option>] <old-name> <new-name>
-'git config' [<file-option>] --remove-section <name>
+'git config remove-section' [<file-option>] <name>
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
 'git config' [<file-option>] -e | --edit
 
@@ -95,6 +95,9 @@ unset::
 rename-section::
 	Rename the given section to a new name.
 
+remove-section::
+	Remove the given section from the configuration file.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -192,9 +195,6 @@ See also <<FILES>>.
 	section in linkgit:gitrevisions[7] for a more complete list of
 	ways to spell blob names.
 
---remove-section::
-	Remove the given section from the configuration file.
-
 --fixed-value::
 	When used with the `value-pattern` argument, treat `value-pattern` as
 	an exact string instead of a regular expression. This will restrict
@@ -333,6 +333,9 @@ recommended to migrate to the new syntax.
 --rename-section <old-name> <new-name>::
 	Replaced by `git config rename-section <old-name> <new-name>`.
 
+--remove-section <name>::
+	Replaced by `git config remove-section <name>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index a60dca9b2b..cea03fb517 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -21,6 +21,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
+	N_("git config remove-section [<file-option>] <name>"),
 	NULL
 };
 
@@ -49,6 +50,11 @@ static const char *const builtin_config_rename_section_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_remove_section_usage[] = {
+	N_("git config remove-section [<file-option>] <name>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -980,12 +986,38 @@ static int cmd_config_rename_section(int argc, const char **argv, const char *pr
 	return 0;
 }
 
+static int cmd_config_remove_section(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_remove_section_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 1, 1);
+
+	handle_config_location(prefix);
+
+	ret = git_config_rename_section_in_file(given_config_source.file,
+						argv[0], NULL);
+	if (ret < 0)
+		return ret;
+	else if (!ret)
+		die(_("no such section: %s"), argv[0]);
+
+	return 0;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
 	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
+	OPT_SUBCOMMAND("remove-section", &subcommand, cmd_config_remove_section),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 5d7b08fa4c..9d71ec1260 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -813,7 +813,7 @@ cat >> .git/config << EOF
 EOF
 
 test_expect_success 'remove section' '
-	git config --remove-section branch.zwei
+	git config ${mode_prefix}remove-section branch.zwei
 '
 
 cat > expect << EOF
@@ -2602,7 +2602,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&
 	test_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&
-	test_must_fail git config --file=config --fixed-value --remove-section dev &&
+	test_must_fail git config ${mode_prefix}remove-section --file=config --fixed-value dev &&
 	test_must_fail git config ${mode_prefix}list --file=config --fixed-value &&
 	test_must_fail git config --file=config --fixed-value --get-color dev.null &&
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 13/14] builtin/config: introduce "edit" subcommand
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (11 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 12/14] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06  8:56   ` [PATCH v5 14/14] builtin/config: display subcommand help Patrick Steinhardt
                     ` (2 subsequent siblings)
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 7245 bytes --]

Introduce a new "edit" subcommand to git-config(1). Please refer to
preceding commits regarding the motivation behind this change.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-config.txt | 17 ++++----
 builtin/config.c             | 81 ++++++++++++++++++++++++------------
 t/t1300-config.sh            |  6 +--
 3 files changed, 68 insertions(+), 36 deletions(-)

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index ef46520c15..65c645d461 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -15,8 +15,8 @@ SYNOPSIS
 'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config rename-section' [<file-option>] <old-name> <new-name>
 'git config remove-section' [<file-option>] <name>
+'git config edit' [<file-option>]
 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]
-'git config' [<file-option>] -e | --edit
 
 DESCRIPTION
 -----------
@@ -98,6 +98,11 @@ rename-section::
 remove-section::
 	Remove the given section from the configuration file.
 
+edit::
+	Opens an editor to modify the specified config file; either
+	`--system`, `--global`, `--local` (default), `--worktree`, or
+	`--file <config-file>`.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -274,12 +279,6 @@ Valid `<type>`'s include:
 	When the color setting for `name` is undefined, the command uses
 	`color.ui` as fallback.
 
--e::
---edit::
-	Opens an editor to modify the specified config file; either
-	`--system`, `--global`, `--local` (default), `--worktree`, or
-	`--file <config-file>`.
-
 --[no-]includes::
 	Respect `include.*` directives in config files when looking up
 	values. Defaults to `off` when a specific file is given (e.g.,
@@ -336,6 +335,10 @@ recommended to migrate to the new syntax.
 --remove-section <name>::
 	Replaced by `git config remove-section <name>`.
 
+-e::
+--edit::
+	Replaced by `git config edit`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
diff --git a/builtin/config.c b/builtin/config.c
index cea03fb517..8f7fa8f31a 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -22,6 +22,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	N_("git config remove-section [<file-option>] <name>"),
+	N_("git config edit [<file-option>]"),
 	NULL
 };
 
@@ -55,6 +56,11 @@ static const char *const builtin_config_remove_section_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_edit_usage[] = {
+	N_("git config edit [<file-option>]"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -1011,6 +1017,53 @@ static int cmd_config_remove_section(int argc, const char **argv, const char *pr
 	return 0;
 }
 
+static int show_editor(void)
+{
+	char *config_file;
+
+	if (!given_config_source.file && !startup_info->have_repository)
+		die(_("not in a git directory"));
+	if (given_config_source.use_stdin)
+		die(_("editing stdin is not supported"));
+	if (given_config_source.blob)
+		die(_("editing blobs is not supported"));
+	git_config(git_default_config, NULL);
+	config_file = given_config_source.file ?
+			xstrdup(given_config_source.file) :
+			git_pathdup("config");
+	if (use_global_config) {
+		int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);
+		if (fd >= 0) {
+			char *content = default_user_config();
+			write_str_in_full(fd, content);
+			free(content);
+			close(fd);
+		}
+		else if (errno != EEXIST)
+			die_errno(_("cannot create configuration file %s"), config_file);
+	}
+	launch_editor(config_file, NULL, NULL);
+	free(config_file);
+
+	return 0;
+}
+
+static int cmd_config_edit(int argc, const char **argv, const char *prefix)
+{
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_edit_usage, 0);
+	check_write();
+	check_argc(argc, 0, 0);
+
+	handle_config_location(prefix);
+
+	return show_editor();
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
@@ -1018,6 +1071,7 @@ static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("unset", &subcommand, cmd_config_unset),
 	OPT_SUBCOMMAND("rename-section", &subcommand, cmd_config_rename_section),
 	OPT_SUBCOMMAND("remove-section", &subcommand, cmd_config_remove_section),
+	OPT_SUBCOMMAND("edit", &subcommand, cmd_config_edit),
 	OPT_END(),
 };
 
@@ -1144,32 +1198,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 		}
 	}
 	else if (actions == ACTION_EDIT) {
-		char *config_file;
-
-		check_argc(argc, 0, 0);
-		if (!given_config_source.file && !startup_info->have_repository)
-			die(_("not in a git directory"));
-		if (given_config_source.use_stdin)
-			die(_("editing stdin is not supported"));
-		if (given_config_source.blob)
-			die(_("editing blobs is not supported"));
-		git_config(git_default_config, NULL);
-		config_file = given_config_source.file ?
-				xstrdup(given_config_source.file) :
-				git_pathdup("config");
-		if (use_global_config) {
-			int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);
-			if (fd >= 0) {
-				char *content = default_user_config();
-				write_str_in_full(fd, content);
-				free(content);
-				close(fd);
-			}
-			else if (errno != EEXIST)
-				die_errno(_("cannot create configuration file %s"), config_file);
-		}
-		launch_editor(config_file, NULL, NULL);
-		free(config_file);
+		ret = show_editor();
 	}
 	else if (actions == ACTION_SET) {
 		check_write();
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 9d71ec1260..f3c4d28e06 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -666,7 +666,7 @@ test_expect_success 'setting a value in stdin is an error' '
 '
 
 test_expect_success 'editing stdin is an error' '
-	test_must_fail git config --file - --edit
+	test_must_fail git config ${mode_prefix}edit --file -
 '
 
 test_expect_success 'refer config from subdirectory' '
@@ -1768,7 +1768,7 @@ test_expect_success 'command line overrides environment config' '
 test_expect_success 'git config --edit works' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
-	GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit &&
+	GIT_EDITOR="echo [test]value=yes >" git config ${mode_prefix}edit -f tmp &&
 	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
@@ -1777,7 +1777,7 @@ test_expect_success 'git config --edit respects core.editor' '
 	git config -f tmp test.value no &&
 	echo test.value=yes >expect &&
 	test_config core.editor "echo [test]value=yes >" &&
-	git config -f tmp --edit &&
+	git config ${mode_prefix}edit -f tmp &&
 	git config ${mode_prefix}list -f tmp >actual &&
 	test_cmp expect actual
 '
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* [PATCH v5 14/14] builtin/config: display subcommand help
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (12 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 13/14] builtin/config: introduce "edit" subcommand Patrick Steinhardt
@ 2024-05-06  8:56   ` Patrick Steinhardt
  2024-05-06 11:30   ` [PATCH v5 00/14] builtin/config: introduce subcommands Karthik Nayak
  2024-05-06 20:21   ` Taylor Blau
  15 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  8:56 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 2170 bytes --]

Until now, `git config -h` would have printed help for the old-style
syntax. Now that all modes have proper subcommands though it is
preferable to instead display the subcommand help.

Drop the `NO_INTERNAL_HELP` flag to do so. While at it, drop the help
mismatch in t0450 and add the `--get-colorbool` option to the usage such
that git-config(1)'s synopsis and `git config -h` match.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/config.c            | 5 +++--
 t/t0450/txt-help-mismatches | 1 -
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/builtin/config.c b/builtin/config.c
index 8f7fa8f31a..80aa9d8a66 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -23,6 +23,7 @@ static const char *const builtin_config_usage[] = {
 	N_("git config rename-section [<file-option>] <old-name> <new-name>"),
 	N_("git config remove-section [<file-option>] <name>"),
 	N_("git config edit [<file-option>]"),
+	N_("git config [<file-option>] --get-colorbool <name> [<stdout-is-tty>]"),
 	NULL
 };
 
@@ -1093,10 +1094,10 @@ int cmd_config(int argc, const char **argv, const char *prefix)
 	 * us when parsing the legacy-style modes that don't use subcommands.
 	 */
 	argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
-			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
+			     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);
 	if (subcommand) {
 		argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,
-		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);
+		       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);
 		return subcommand(argc, argv, prefix);
 	}
 
diff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches
index a0777acd66..28003f18c9 100644
--- a/t/t0450/txt-help-mismatches
+++ b/t/t0450/txt-help-mismatches
@@ -10,7 +10,6 @@ checkout
 checkout-index
 clone
 column
-config
 credential
 credential-cache
 credential-store
-- 
2.45.0


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v4 03/14] builtin/config: move "fixed-value" option to correct group
  2024-05-03 12:28     ` Karthik Nayak
@ 2024-05-06  9:34       ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-06  9:34 UTC (permalink / raw)
  To: Karthik Nayak
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 2559 bytes --]

On Fri, May 03, 2024 at 05:28:38AM -0700, Karthik Nayak wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > The `--fixed-value` option can be used to alter how the value-pattern
> > parameter is interpreted for the various submodes of git-config(1). But
> > while it is an option, it is currently listed as part of the submodes
> > group the command, which is wrong.
> >
> 
> Isn't it currently listed as part of the 'Action' group? Also I'm not
> sure if the last sentence needs to be corrected here.

It's what I actually intended to say, yes. But I agree with you that
"action" feels like the more reasonable way to put it given that we also
call them "actions" in code.

And yes, the last sentence needs to be corrected :)

Patrick

> >
> > Move the option to the "Other" group, which hosts the various options
> > known to git-config(1).
> >
> > Signed-off-by: Patrick Steinhardt <ps@pks.im>
> > ---
> >  builtin/config.c | 2 +-
> >  1 file changed, 1 insertion(+), 1 deletion(-)
> >
> > diff --git a/builtin/config.c b/builtin/config.c
> > index 59ae5996eb..054019b70c 100644
> > --- a/builtin/config.c
> > +++ b/builtin/config.c
> > @@ -643,7 +643,6 @@ static struct option builtin_config_options[] = {
> >  	OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION),
> >  	OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION),
> >  	OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST),
> > -	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
> >  	OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT),
> >  	OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR),
> >  	OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL),
> > @@ -663,6 +662,7 @@ static struct option builtin_config_options[] = {
> >  	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
> >  	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
> >  	OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
> > +	OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")),
> >  	OPT_END(),
> >  };
> >
> > --
> > 2.45.0



[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-06  7:58       ` Patrick Steinhardt
@ 2024-05-06 11:26         ` Karthik Nayak
  0 siblings, 0 replies; 113+ messages in thread
From: Karthik Nayak @ 2024-05-06 11:26 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano

[-- Attachment #1: Type: text/plain, Size: 1957 bytes --]

Patrick Steinhardt <ps@pks.im> writes:

> On Fri, May 03, 2024 at 06:08:57AM -0700, Karthik Nayak wrote:
>> Patrick Steinhardt <ps@pks.im> writes:
>>
>> > While git-config(1) has several modes, those modes are not exposed with
>> > subcommands but instead by specifying e.g. `--unset` or `--list`. This
>>
>> s/specifying/specifying flags/ perhaps?
>>
>> > user interface is not really in line with how our more modern commands
>> > work, where it is a lot more customary to say e.g. `git remote list`.
>>
>> Tangent: I totally agree with the patch, but it would be nice to have a
>> 'DesigningCommands' document which would highlight UX do's and don'ts.
>> It would be nice to add that as reference in discussions.
>
> I agree that we should have that discussion, but feel like it should be
> part of a separate patch series. If I were to adopt that into this
> series I very much think that the resulting discussions would take quite
> a while to settle.
>
> I'll start the discussion soonish in a separate thread.
>

Ofcourse, not part of this series. I was merely using this patch to
bring up that topic. Thanks, will stay tuned.

> [snip]
>> > @@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string matching' '
>> >  	test_cmp expect actual &&
>> >
>> >  	git config --file=config --fixed-value --replace-all fixed.test bogus "$META" &&
>> > -	git config --file=config --list >actual &&
>> > +	git config ${mode_prefix}list --file=config >actual &&
>> >  	cat >expect <<-EOF &&
>> >  	fixed.test=bogus
>> >  	fixed.test=bogus
>> > @@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes failure' '
>> >  	test_cmp expect err
>> >  '
>> >
>> > +done
>> > +
>> >
>> Nit: Wouldn't it be better if the tests are indented here? That way you
>> know it's part of a loop.
>
> We would basically have to reindent 2500 lines of code. I don't think
> that'd be a helpful to reviewers :)
>
> Patrick

Fair enough.

Karthik

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]

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

* Re: [PATCH v5 00/14] builtin/config: introduce subcommands
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (13 preceding siblings ...)
  2024-05-06  8:56   ` [PATCH v5 14/14] builtin/config: display subcommand help Patrick Steinhardt
@ 2024-05-06 11:30   ` Karthik Nayak
  2024-05-06 20:21   ` Taylor Blau
  15 siblings, 0 replies; 113+ messages in thread
From: Karthik Nayak @ 2024-05-06 11:30 UTC (permalink / raw)
  To: Patrick Steinhardt, git
  Cc: Kristoffer Haugsbakk, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Dragan Simic, rsbecker

[-- Attachment #1: Type: text/plain, Size: 5413 bytes --]

Patrick Steinhardt <ps@pks.im> writes:

> Hi,
>
> this is the fifth and hopefully last version of my patch sthat
> introduces subcommands into git-config(1).
>
> The only changes compared to v4 are some fixes to commit messages.
> Otherwise I'm not aware of any other feedback that would need to be
> addressed.
>
> Patrick
>
> Patrick Steinhardt (14):
>   config: clarify memory ownership when preparing comment strings
>   builtin/config: move option array around
>   builtin/config: move "fixed-value" option to correct group
>   builtin/config: use `OPT_CMDMODE()` to specify modes
>   builtin/config: pull out function to handle config location
>   builtin/config: pull out function to handle `--null`
>   builtin/config: introduce "list" subcommand
>   builtin/config: introduce "get" subcommand
>   builtin/config: introduce "set" subcommand
>   builtin/config: introduce "unset" subcommand
>   builtin/config: introduce "rename-section" subcommand
>   builtin/config: introduce "remove-section" subcommand
>   builtin/config: introduce "edit" subcommand
>   builtin/config: display subcommand help
>
>  Documentation/git-config.txt | 219 ++++++++-------
>  builtin/config.c             | 512 ++++++++++++++++++++++++++++-------
>  config.c                     |  16 +-
>  config.h                     |   2 +-
>  t/t0450/txt-help-mismatches  |   1 -
>  t/t1300-config.sh            | 432 +++++++++++++++++------------
>  6 files changed, 812 insertions(+), 370 deletions(-)
>
> Range-diff against v4:
>  1:  3aa26d5bff !  1:  881d2b5426 config: clarify memory ownership when preparing comment strings
>     @@ Commit message
>          not like this micro-optimization really matters. Thus, callers are now
>          always responsible for freeing the value.
>
>     +    Signed-off-by: Patrick Steinhardt <ps@pks.im>
>     +
>       ## builtin/config.c ##
>      @@ builtin/config.c: static struct config_options config_options;
>       static int show_origin;
>  2:  8f0804ab48 =  2:  66dffaa8f2 builtin/config: move option array around
>  3:  ddcd8031d7 !  3:  36abda0e02 builtin/config: move "fixed-value" option to correct group
>     @@ Commit message
>          builtin/config: move "fixed-value" option to correct group
>
>          The `--fixed-value` option can be used to alter how the value-pattern
>     -    parameter is interpreted for the various submodes of git-config(1). But
>     -    while it is an option, it is currently listed as part of the submodes
>     -    group the command, which is wrong.
>     +    parameter is interpreted for the various actions of git-config(1). But
>     +    while it is an option, it is currently listed as part of the actions
>     +    group, which is wrong.
>
>          Move the option to the "Other" group, which hosts the various options
>          known to git-config(1).
>  4:  1bc3918840 =  4:  34b66f9c87 builtin/config: use `OPT_CMDMODE()` to specify modes
>  5:  3754812309 =  5:  4f90f206e7 builtin/config: pull out function to handle config location
>  6:  cb1714c493 =  6:  df1a6f14e6 builtin/config: pull out function to handle `--null`
>  7:  b3f3c3ba6a !  7:  1df76a9970 builtin/config: introduce "list" subcommand
>     @@ Commit message
>          builtin/config: introduce "list" subcommand
>
>          While git-config(1) has several modes, those modes are not exposed with
>     -    subcommands but instead by specifying e.g. `--unset` or `--list`. This
>     -    user interface is not really in line with how our more modern commands
>     -    work, where it is a lot more customary to say e.g. `git remote list`.
>     -    Furthermore, to add to the confusion, git-config(1) also allows the user
>     -    to request modes implicitly by just specifying the correct number of
>     -    arguments. Thus, `git config foo.bar` will retrieve the value of
>     -    "foo.bar" while `git config foo.bar baz` will set it to "baz".
>     +    subcommands but instead by specifying action flags like `--unset` or
>     +    `--list`. This user interface is not really in line with how our more
>     +    modern commands work, where it is a lot more customary to say e.g. `git
>     +    remote list`. Furthermore, to add to the confusion, git-config(1) also
>     +    allows the user to request modes implicitly by just specifying the
>     +    correct number of arguments. Thus, `git config foo.bar` will retrieve
>     +    the value of "foo.bar" while `git config foo.bar baz` will set it to
>     +    "baz".
>
>          Overall, this makes for a confusing interface that could really use a
>          makeover. It hurts discoverability of what you can do with git-config(1)
>  8:  0e6da909ac =  8:  29676b81e0 builtin/config: introduce "get" subcommand
>  9:  8a623a31b9 =  9:  94afb5a5b7 builtin/config: introduce "set" subcommand
> 10:  e25e5b69cd = 10:  e525c2326a builtin/config: introduce "unset" subcommand
> 11:  f24008d356 = 11:  a797889890 builtin/config: introduce "rename-section" subcommand
> 12:  fc2ddd3201 = 12:  8ec214755e builtin/config: introduce "remove-section" subcommand
> 13:  4c2d817eff = 13:  1893c23afc builtin/config: introduce "edit" subcommand
> 14:  4c351b12b8 = 14:  97a48ab81d builtin/config: display subcommand help
>
> base-commit: d4cc1ec35f3bcce816b69986ca41943f6ce21377
> --
> 2.45.0

The range diff looks good. Overall I have nothing more to add to this
series.

Thanks
Karthik

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 690 bytes --]

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

* Re: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-06  7:51           ` Patrick Steinhardt
@ 2024-05-06 17:13             ` Junio C Hamano
  2024-05-06 18:33               ` rsbecker
                                 ` (2 more replies)
  0 siblings, 3 replies; 113+ messages in thread
From: Junio C Hamano @ 2024-05-06 17:13 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: rsbecker, 'Karthik Nayak',
	git, 'Kristoffer Haugsbakk', 'Taylor Blau',
	'Jean-Noël AVILA', 'Eric Sunshine'

Patrick Steinhardt <ps@pks.im> writes:

> ... I was pondering
> whether we want to introduce a document as part of that patch series
> that starts to keep track of upcoming removals for a potential Git 3.0
> release.

Finally somebody has bit it ;-)  In the 2.44 cycle, I wrote

    The RelNotes symbolic link says we are now working towards Git 2.44.
    It may not be a bad idea to reflect on what technical debt and UI
    warts we have accumulated so far to see if we have enough of them to
    start planning for a breaking Git 3.0 release (or, of course, keep
    incrementally improve the system, which is much more preferrable---
    continuity and stability is good).  End of year being a relatively
    quiet period, it may be a good time to think about your favorite pet
    peeve, to be discussed early next year.

in a few of the "What's cooking" reports.

> There are multiple items that could be added:
>
>   - Removal of the old syntax of git-config(1).
>
>   - Removal of the dumb HTTP transport.
>
>   - Removal of `info/grafts`.
>
> There are probably other items.

A list of things I can think of that I won't be the primary advocate
for but I do not mind too terribly if we had champions for the
topics are attached at the end.

> In any case, the old actions are here to stay for the foreseeable future
> until we commit to a breaking major release.

True.

> Thanks for the thorough explanation, I have nothing to add!

You could have avoided it if you copied some from the initial cover
letter to each round (i.e. preparing the series to be read by some
folks who did not read an earlier round).


Possible additional Git 3.0 items:

 - Removing "git http-push" to push over HTTP/DAV.

 - Removing support of `$GIT_DIR/branches/` from remote.c API.

 - Removing "git update-server-info".

 - Removing "git annotate".

 - Removing "gitweb" and "git instaweb".

 - Removing "git filter-branch", now we have a better alternative
   "git filter-repo".

 - Removing discovery of hook script in "$GIT_DIR/hooks/", in favor
   of the configuration variables that point at them.

 - Switching to SHA-256 as the default hash algorithm.

 - Switching to reftable as the default ref backend.

 - Switching the hardcoded default branch name away from "master" to
   "main".

 - Declaring that "git restore" and "git switch" were failed
   experiments and deprecating them.

 - Declaring that "git submodule" was a failed experiment and
   deprecating it.

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

* RE: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-06 17:13             ` Junio C Hamano
@ 2024-05-06 18:33               ` rsbecker
  2024-05-06 18:45                 ` Dragan Simic
  2024-05-07  6:20                 ` Kristoffer Haugsbakk
  2024-05-06 21:33               ` Git 3.0? Junio C Hamano
  2024-05-07  4:02               ` [PATCH v4 07/14] builtin/config: introduce "list" subcommand Patrick Steinhardt
  2 siblings, 2 replies; 113+ messages in thread
From: rsbecker @ 2024-05-06 18:33 UTC (permalink / raw)
  To: 'Junio C Hamano', 'Patrick Steinhardt'
  Cc: 'Karthik Nayak', git, 'Kristoffer Haugsbakk',
	'Taylor Blau', 'Jean-Noël AVILA',
	'Eric Sunshine'

On Monday, May 6, 2024 1:13 PM, Junio C Hamano wrote:
>Patrick Steinhardt <ps@pks.im> writes:
>
>> ... I was pondering
>> whether we want to introduce a document as part of that patch series
>> that starts to keep track of upcoming removals for a potential Git 3.0
>> release.
>
>Finally somebody has bit it ;-)  In the 2.44 cycle, I wrote
>
>    The RelNotes symbolic link says we are now working towards Git 2.44.
>    It may not be a bad idea to reflect on what technical debt and UI
>    warts we have accumulated so far to see if we have enough of them to
>    start planning for a breaking Git 3.0 release (or, of course, keep
>    incrementally improve the system, which is much more preferrable---
>    continuity and stability is good).  End of year being a relatively
>    quiet period, it may be a good time to think about your favorite pet
>    peeve, to be discussed early next year.
>
>in a few of the "What's cooking" reports.
>
>> There are multiple items that could be added:
>>
>>   - Removal of the old syntax of git-config(1).
>>
>>   - Removal of the dumb HTTP transport.
>>
>>   - Removal of `info/grafts`.
>>
>> There are probably other items.
>
>A list of things I can think of that I won't be the primary advocate for but I do not
>mind too terribly if we had champions for the topics are attached at the end.
>
>> In any case, the old actions are here to stay for the foreseeable
>> future until we commit to a breaking major release.
>
>True.
>
>> Thanks for the thorough explanation, I have nothing to add!
>
>You could have avoided it if you copied some from the initial cover letter to each
>round (i.e. preparing the series to be read by some folks who did not read an earlier
>round).
>
>
>Possible additional Git 3.0 items:
>
> - Removing "git http-push" to push over HTTP/DAV.
>
> - Removing support of `$GIT_DIR/branches/` from remote.c API.
>
> - Removing "git update-server-info".
>
> - Removing "git annotate".

I must have missed this... I thought annotate was replacing blame.

> - Removing "gitweb" and "git instaweb".
>
> - Removing "git filter-branch", now we have a better alternative
>   "git filter-repo".
>
> - Removing discovery of hook script in "$GIT_DIR/hooks/", in favor
>   of the configuration variables that point at them.
>
> - Switching to SHA-256 as the default hash algorithm.
>
> - Switching to reftable as the default ref backend.
>
> - Switching the hardcoded default branch name away from "master" to
>   "main".
>
> - Declaring that "git restore" and "git switch" were failed
>   experiments and deprecating them.

This restore and switch have broad use in my community. I do not consider them failed experiments at all, but essential in scripts and usage. Removing these would block migration to git 3, in my view. I have actually been thinking about doing a YouTube video on these commands.

> - Declaring that "git submodule" was a failed experiment and
>   deprecating it.

This would be a very high-impact decision. Aside from my community's use (and my own company's dependence on submodules), this will break large numbers of GNU-based projects that use submodules for distribution, including build-aux. I would suggest staying away from this decision. Submodules have definite value. Please keep them.

If you really want to get rid of stuff that has limited use instead of submodules, it is worktrees, the benefit of which is reduced given sparce checkouts and fetch depth.

--Randall


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

* Re: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-06 18:33               ` rsbecker
@ 2024-05-06 18:45                 ` Dragan Simic
  2024-05-07  6:20                 ` Kristoffer Haugsbakk
  1 sibling, 0 replies; 113+ messages in thread
From: Dragan Simic @ 2024-05-06 18:45 UTC (permalink / raw)
  To: rsbecker
  Cc: 'Junio C Hamano', 'Patrick Steinhardt',
	'Karthik Nayak', git, 'Kristoffer Haugsbakk',
	'Taylor Blau', 'Jean-Noël AVILA',
	'Eric Sunshine'

On 2024-05-06 20:33, rsbecker@nexbridge.com wrote:
> On Monday, May 6, 2024 1:13 PM, Junio C Hamano wrote:
>> - Declaring that "git restore" and "git switch" were failed
>>   experiments and deprecating them.
> 
> This restore and switch have broad use in my community. I do not
> consider them failed experiments at all, but essential in scripts and
> usage. Removing these would block migration to git 3, in my view. I
> have actually been thinking about doing a YouTube video on these
> commands.

Huh, I was also really surprised to see "git switch" and "git restore"
on the planned deprecated feature list.  For example, I use "git switch"
literally all the time, and "git restore" also sees a fair amount of 
use.
Thus, I think that deprecating these two features is a really bad idea.

>> - Declaring that "git submodule" was a failed experiment and
>>   deprecating it.
> 
> This would be a very high-impact decision. Aside from my community's
> use (and my own company's dependence on submodules), this will break
> large numbers of GNU-based projects that use submodules for
> distribution, including build-aux. I would suggest staying away from
> this decision. Submodules have definite value. Please keep them.

I agree that deprecating "git submodule" isn't a good idea.

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

* Re: [PATCH v5 00/14] builtin/config: introduce subcommands
  2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
                     ` (14 preceding siblings ...)
  2024-05-06 11:30   ` [PATCH v5 00/14] builtin/config: introduce subcommands Karthik Nayak
@ 2024-05-06 20:21   ` Taylor Blau
  2024-05-06 20:38     ` rsbecker
  15 siblings, 1 reply; 113+ messages in thread
From: Taylor Blau @ 2024-05-06 20:21 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, Kristoffer Haugsbakk, Jean-Noël AVILA, Eric Sunshine,
	Junio C Hamano, Dragan Simic, rsbecker

On Mon, May 06, 2024 at 10:55:51AM +0200, Patrick Steinhardt wrote:
> Hi,
>
> this is the fifth and hopefully last version of my patch sthat
> introduces subcommands into git-config(1).
>
> The only changes compared to v4 are some fixes to commit messages.
> Otherwise I'm not aware of any other feedback that would need to be
> addressed.

Thanks for the new round. I took a close look through the series, and
couldn't see any remaining issues. I appreciate the translation guide
you included in the documentation to indicate "git config --add" is
replaced by things like "git config set --append".

Thanks,
Taylor

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

* RE: [PATCH v5 00/14] builtin/config: introduce subcommands
  2024-05-06 20:21   ` Taylor Blau
@ 2024-05-06 20:38     ` rsbecker
  2024-05-07  4:07       ` Patrick Steinhardt
  0 siblings, 1 reply; 113+ messages in thread
From: rsbecker @ 2024-05-06 20:38 UTC (permalink / raw)
  To: 'Taylor Blau', 'Patrick Steinhardt'
  Cc: git, 'Kristoffer Haugsbakk',
	'Jean-Noël AVILA', 'Eric Sunshine',
	'Junio C Hamano', 'Dragan Simic'

On Monday, May 6, 2024 4:22 PM, Taylor Blau wrote:
>On Mon, May 06, 2024 at 10:55:51AM +0200, Patrick Steinhardt wrote:
>> Hi,
>>
>> this is the fifth and hopefully last version of my patch sthat
>> introduces subcommands into git-config(1).
>>
>> The only changes compared to v4 are some fixes to commit messages.
>> Otherwise I'm not aware of any other feedback that would need to be
>> addressed.
>
>Thanks for the new round. I took a close look through the series, and couldn't see
>any remaining issues. I appreciate the translation guide you included in the
>documentation to indicate "git config --add" is replaced by things like "git config set
>--append".

Please make sure that there is a compatibility mode available for some large period. There are huge numbers of scripts in the customer base I deal with that use config extensively. Changing the CLI for this will be dire consequences.


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

* Git 3.0?
  2024-05-06 17:13             ` Junio C Hamano
  2024-05-06 18:33               ` rsbecker
@ 2024-05-06 21:33               ` Junio C Hamano
  2024-05-07  4:18                 ` Patrick Steinhardt
  2024-05-07  4:02               ` [PATCH v4 07/14] builtin/config: introduce "list" subcommand Patrick Steinhardt
  2 siblings, 1 reply; 113+ messages in thread
From: Junio C Hamano @ 2024-05-06 21:33 UTC (permalink / raw)
  To: git

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

> Patrick Steinhardt <ps@pks.im> writes:
>
>> ... I was pondering
>> whether we want to introduce a document as part of that patch series
>> that starts to keep track of upcoming removals for a potential Git 3.0
>> release.
>
> Finally somebody has bit it ;-)  In the 2.44 cycle, I wrote
>
>     The RelNotes symbolic link says we are now working towards Git 2.44.
>     It may not be a bad idea to reflect on what technical debt and UI
>     warts we have accumulated so far to see if we have enough of them to
>     start planning for a breaking Git 3.0 release (or, of course, keep
>     incrementally improve the system, which is much more preferrable---
>     continuity and stability is good).  End of year being a relatively
>     quiet period, it may be a good time to think about your favorite pet
>     peeve, to be discussed early next year.
>
> in a few of the "What's cooking" reports.
>
>> There are multiple items that could be added:
>>
>>   - Removal of the old syntax of git-config(1).
>>
>>   - Removal of the dumb HTTP transport.
>>
>>   - Removal of `info/grafts`.
>>
>> There are probably other items.
> 
> A list of things I can think of that I won't be the primary advocate
> for but I do not mind too terribly if we had champions for the
> topics are attached at the end.

Just to avoid unnecessary misunderstanding, as I said, what I listed
are not my proposals.  Rather, take them together with Patrick's list
that they are invitations for others to let their imagination go wild,
trying to come up as many ideas regardless of how good they are, sort
of the initial "brain-storming" phase of a discussion.  We will need
to cull bad ideas, pick good ones, and refine what we are going to
implement in later stages, but this is not a time to shoot down what
you do not like, yet.  It is instead for you to add your pet peeve.

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

* Re: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-06 17:13             ` Junio C Hamano
  2024-05-06 18:33               ` rsbecker
  2024-05-06 21:33               ` Git 3.0? Junio C Hamano
@ 2024-05-07  4:02               ` Patrick Steinhardt
  2 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-07  4:02 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: rsbecker, 'Karthik Nayak',
	git, 'Kristoffer Haugsbakk', 'Taylor Blau',
	'Jean-Noël AVILA', 'Eric Sunshine'

[-- Attachment #1: Type: text/plain, Size: 4772 bytes --]

On Mon, May 06, 2024 at 10:13:25AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > ... I was pondering
> > whether we want to introduce a document as part of that patch series
> > that starts to keep track of upcoming removals for a potential Git 3.0
> > release.
> 
> Finally somebody has bit it ;-)  In the 2.44 cycle, I wrote
> 
>     The RelNotes symbolic link says we are now working towards Git 2.44.
>     It may not be a bad idea to reflect on what technical debt and UI
>     warts we have accumulated so far to see if we have enough of them to
>     start planning for a breaking Git 3.0 release (or, of course, keep
>     incrementally improve the system, which is much more preferrable---
>     continuity and stability is good).  End of year being a relatively
>     quiet period, it may be a good time to think about your favorite pet
>     peeve, to be discussed early next year.
> 
> in a few of the "What's cooking" reports.

I know :) I have been thinking about this on and off, but never felt
like pushing for it yet.

> > There are multiple items that could be added:
> >
> >   - Removal of the old syntax of git-config(1).
> >
> >   - Removal of the dumb HTTP transport.
> >
> >   - Removal of `info/grafts`.
> >
> > There are probably other items.
> 
> A list of things I can think of that I won't be the primary advocate
> for but I do not mind too terribly if we had champions for the
> topics are attached at the end.

I'd be happy to champion for each of those.

> > In any case, the old actions are here to stay for the foreseeable future
> > until we commit to a breaking major release.
> 
> True.
> 
> > Thanks for the thorough explanation, I have nothing to add!
> 
> You could have avoided it if you copied some from the initial cover
> letter to each round (i.e. preparing the series to be read by some
> folks who did not read an earlier round).

Fair enough. I should have known that this part is indeed quite
important to the whole series and included it in the newer cover
letters.

> 
> Possible additional Git 3.0 items:

Some pretty controversial takes in here ;) I guess that's by design.

>  - Removing "git http-push" to push over HTTP/DAV.
> 
>  - Removing support of `$GIT_DIR/branches/` from remote.c API.
> 
>  - Removing "git update-server-info".

Yes, all of these are quite sensible.

>  - Removing "git annotate".

I don't care much about this one, but it's nice indeed to get rid of
duplicate functionality.

>  - Removing "gitweb" and "git instaweb".

I don't care about this one, to be honest. It's basically unmaintained
though as far as I know, so we might just be accepting the status quo.

>  - Removing "git filter-branch", now we have a better alternative
>    "git filter-repo".

This one I'm sceptical about. The one upside of git-filter-branch(1) is
that it's part of Git itself, whereas git-filter-repo(1) is not. I thus
think that a prerequisite here should be that we first land the new
script in Git before actually deprecating the old tool such that things
remain discoverable. And upstreaming git-filter-repo(1) would be a
worthy goal by itself already.

>  - Removing discovery of hook script in "$GIT_DIR/hooks/", in favor
>    of the configuration variables that point at them.

Those have been an attack vector in the past, and I think that using
config to set up hooks is quite a bit saner. But of course, we first
need to land this topic :)

>  - Switching to SHA-256 as the default hash algorithm.
> 
>  - Switching to reftable as the default ref backend.
> 
>  - Switching the hardcoded default branch name away from "master" to
>    "main".

All three of those may be nice. The first one is going to be hardest as
it requires support in forges. GitLab started to support SHA256 a month
ago, but it's still experimental. But to the best of my knowledge GitHub
does not yet support SHA256 at all.

>  - Declaring that "git restore" and "git switch" were failed
>    experiments and deprecating them.

I use those quite a lot, so it'd be a shame if those went away.

>  - Declaring that "git submodule" was a failed experiment and
>    deprecating it.

Well. I know that most people think that submodules don't work and
should just go away, and I count myself as part of that group. But
realistically I don't see that as a feasible goal, even more so because
we don't actually have a proper replacement. They may be somewhat
broken, but there is no better substitute.

You know, let me take this list and propose it as something like
"Documents/UpcomingDeprecations". This will make the whole discussion
here a lot more discoverable.

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v5 00/14] builtin/config: introduce subcommands
  2024-05-06 20:38     ` rsbecker
@ 2024-05-07  4:07       ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-07  4:07 UTC (permalink / raw)
  To: rsbecker
  Cc: 'Taylor Blau', git, 'Kristoffer Haugsbakk',
	'Jean-Noël AVILA', 'Eric Sunshine',
	'Junio C Hamano', 'Dragan Simic'

[-- Attachment #1: Type: text/plain, Size: 1295 bytes --]

On Mon, May 06, 2024 at 04:38:59PM -0400, rsbecker@nexbridge.com wrote:
> On Monday, May 6, 2024 4:22 PM, Taylor Blau wrote:
> >On Mon, May 06, 2024 at 10:55:51AM +0200, Patrick Steinhardt wrote:
> >> Hi,
> >>
> >> this is the fifth and hopefully last version of my patch sthat
> >> introduces subcommands into git-config(1).
> >>
> >> The only changes compared to v4 are some fixes to commit messages.
> >> Otherwise I'm not aware of any other feedback that would need to be
> >> addressed.
> >
> >Thanks for the new round. I took a close look through the series, and couldn't see
> >any remaining issues. I appreciate the translation guide you included in the
> >documentation to indicate "git config --add" is replaced by things like "git config set
> >--append".
> 
> Please make sure that there is a compatibility mode available for some
> large period. There are huge numbers of scripts in the customer base I
> deal with that use config extensively. Changing the CLI for this will
> be dire consequences.

Yes, that's a very sensible thing to ask for. I think the bare minimum
we should aim for is a year (4 releases), where two years (8 releases)
feels a bit more sensible. Do you feel like that is reasonable, or do
you think it would be too short?

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: Git 3.0?
  2024-05-06 21:33               ` Git 3.0? Junio C Hamano
@ 2024-05-07  4:18                 ` Patrick Steinhardt
  0 siblings, 0 replies; 113+ messages in thread
From: Patrick Steinhardt @ 2024-05-07  4:18 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git

[-- Attachment #1: Type: text/plain, Size: 2387 bytes --]

On Mon, May 06, 2024 at 02:33:33PM -0700, Junio C Hamano wrote:
> Junio C Hamano <gitster@pobox.com> writes:
> 
> > Patrick Steinhardt <ps@pks.im> writes:
> >
> >> ... I was pondering
> >> whether we want to introduce a document as part of that patch series
> >> that starts to keep track of upcoming removals for a potential Git 3.0
> >> release.
> >
> > Finally somebody has bit it ;-)  In the 2.44 cycle, I wrote
> >
> >     The RelNotes symbolic link says we are now working towards Git 2.44.
> >     It may not be a bad idea to reflect on what technical debt and UI
> >     warts we have accumulated so far to see if we have enough of them to
> >     start planning for a breaking Git 3.0 release (or, of course, keep
> >     incrementally improve the system, which is much more preferrable---
> >     continuity and stability is good).  End of year being a relatively
> >     quiet period, it may be a good time to think about your favorite pet
> >     peeve, to be discussed early next year.
> >
> > in a few of the "What's cooking" reports.
> >
> >> There are multiple items that could be added:
> >>
> >>   - Removal of the old syntax of git-config(1).
> >>
> >>   - Removal of the dumb HTTP transport.
> >>
> >>   - Removal of `info/grafts`.
> >>
> >> There are probably other items.
> > 
> > A list of things I can think of that I won't be the primary advocate
> > for but I do not mind too terribly if we had champions for the
> > topics are attached at the end.
> 
> Just to avoid unnecessary misunderstanding, as I said, what I listed
> are not my proposals.  Rather, take them together with Patrick's list
> that they are invitations for others to let their imagination go wild,
> trying to come up as many ideas regardless of how good they are, sort
> of the initial "brain-storming" phase of a discussion.  We will need
> to cull bad ideas, pick good ones, and refine what we are going to
> implement in later stages, but this is not a time to shoot down what
> you do not like, yet.  It is instead for you to add your pet peeve.

Just to avoid starting this discussion deep down in this thread, I plan
to send an RFC-style patch later today that proposes several potential
deprecations for Git 3.0. The intent of that patch will be to trigger
discussion and it is thus by design going to be controversial :)

Patrick

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v4 07/14] builtin/config: introduce "list" subcommand
  2024-05-06 18:33               ` rsbecker
  2024-05-06 18:45                 ` Dragan Simic
@ 2024-05-07  6:20                 ` Kristoffer Haugsbakk
  1 sibling, 0 replies; 113+ messages in thread
From: Kristoffer Haugsbakk @ 2024-05-07  6:20 UTC (permalink / raw)
  To: rsbecker
  Cc: Karthik Nayak, git, Taylor Blau, Jean-Noël AVILA,
	Eric Sunshine, Junio C Hamano, Patrick Steinhardt

On Mon, May 6, 2024, at 20:33, rsbecker@nexbridge.com wrote:
> If you really want to get rid of stuff that has limited use instead of
> submodules, it is worktrees, the benefit of which is reduced given
> sparce checkouts and fetch depth.

Git-worktrees is very useful. One use-case is having a dedicated
worktree for an old version of the program/app that you need to work on
for whatever reason (using the same worktree for very different versions
can confuse build systems and IDEs). These things aren’t covered by
those other supposed alternatives (which I never use because I don’t
work on big repositories).

Maybe the idea is to use a fetch depth of 1 on N clones instead of N
worktrees. But then you still have clones laying around in an ad hoc
manner compared to having on overview of all the worktrees that you are
using from one repository.

-- 
Kristoffer Haugsbakk


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

end of thread, other threads:[~2024-05-07  6:21 UTC | newest]

Thread overview: 113+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-03-06 11:31 [PATCH 0/8] builtin/config: introduce subcommands Patrick Steinhardt
2024-03-06 11:31 ` [PATCH 1/8] builtin/config: move option array around Patrick Steinhardt
2024-03-06 11:31 ` [PATCH 2/8] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
2024-03-06 11:31 ` [PATCH 3/8] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
2024-03-06 23:52   ` Taylor Blau
2024-03-07  7:02     ` Patrick Steinhardt
2024-03-06 11:31 ` [PATCH 4/8] builtin/config: move modes into separate functions Patrick Steinhardt
2024-03-06 11:31 ` [PATCH 5/8] builtin/config: track subcommands by action Patrick Steinhardt
2024-03-06 21:54   ` Jean-Noël AVILA
2024-03-07  6:37     ` Patrick Steinhardt
2024-03-07  0:10   ` Taylor Blau
2024-03-07  6:36     ` Patrick Steinhardt
2024-03-06 11:31 ` [PATCH 6/8] builtin/config: introduce subcommands Patrick Steinhardt
2024-03-06 21:38   ` Karthik Nayak
2024-03-07  7:14     ` Patrick Steinhardt
2024-03-06 11:31 ` [PATCH 7/8] t1300: exercise both old- and new-style modes Patrick Steinhardt
2024-03-06 11:32 ` [PATCH 8/8] Documentation/git-config: update to new-style syntax Patrick Steinhardt
2024-03-07  6:57   ` Eric Sunshine
2024-03-07  7:33     ` Patrick Steinhardt
2024-03-06 17:06 ` [PATCH 0/8] builtin/config: introduce subcommands Junio C Hamano
2024-03-06 23:46   ` Taylor Blau
2024-03-06 23:52     ` Junio C Hamano
2024-03-07  0:13       ` Taylor Blau
2024-03-07  0:31     ` Dragan Simic
2024-03-07  6:31     ` Patrick Steinhardt
2024-03-07 13:22       ` Junio C Hamano
2024-03-06 22:49 ` Kristoffer Haugsbakk
2024-03-11 23:19 ` [PATCH v2 00/13] " Patrick Steinhardt
2024-03-11 23:19   ` [PATCH v2 01/13] builtin/config: move option array around Patrick Steinhardt
2024-03-11 23:19   ` [PATCH v2 02/13] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
2024-03-11 23:20   ` [PATCH v2 03/13] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
2024-03-11 23:20   ` [PATCH v2 04/13] builtin/config: pull out function to handle config location Patrick Steinhardt
2024-03-11 23:20   ` [PATCH v2 05/13] builtin/config: pull out function to handle `--null` Patrick Steinhardt
2024-03-11 23:20   ` [PATCH v2 06/13] builtin/config: introduce "list" subcommand Patrick Steinhardt
2024-03-13  2:45     ` Eric Sunshine
2024-03-27  8:42       ` Patrick Steinhardt
2024-03-11 23:20   ` [PATCH v2 07/13] builtin/config: introduce "get" subcommand Patrick Steinhardt
2024-03-13  3:11     ` Eric Sunshine
2024-03-27  8:42       ` Patrick Steinhardt
2024-03-11 23:20   ` [PATCH v2 08/13] builtin/config: introduce "set" subcommand Patrick Steinhardt
2024-03-11 23:21   ` [PATCH v2 09/13] builtin/config: introduce "unset" subcommand Patrick Steinhardt
2024-03-11 23:21   ` [PATCH v2 10/13] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
2024-03-11 23:21   ` [PATCH v2 11/13] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
2024-03-11 23:21   ` [PATCH v2 12/13] builtin/config: introduce "edit" subcommand Patrick Steinhardt
2024-03-11 23:21   ` [PATCH v2 13/13] builtin/config: display subcommand help Patrick Steinhardt
2024-03-27  8:46 ` [PATCH v3 00/13] builtin/config: introduce subcommands Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 01/13] builtin/config: move option array around Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 02/13] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 03/13] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 04/13] builtin/config: pull out function to handle config location Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 05/13] builtin/config: pull out function to handle `--null` Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 06/13] builtin/config: introduce "list" subcommand Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 07/13] builtin/config: introduce "get" subcommand Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 08/13] builtin/config: introduce "set" subcommand Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 09/13] builtin/config: introduce "unset" subcommand Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 10/13] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 11/13] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
2024-03-27  8:46   ` [PATCH v3 12/13] builtin/config: introduce "edit" subcommand Patrick Steinhardt
2024-03-27  8:47   ` [PATCH v3 13/13] builtin/config: display subcommand help Patrick Steinhardt
2024-03-27  8:53   ` [PATCH v3 00/13] builtin/config: introduce subcommands Eric Sunshine
2024-03-27  9:16     ` Patrick Steinhardt
2024-05-03  9:56 ` [PATCH v4 00/14] " Patrick Steinhardt
2024-05-03  9:56   ` [PATCH v4 01/14] config: clarify memory ownership when preparing comment strings Patrick Steinhardt
2024-05-03 10:13     ` Kristoffer Haugsbakk
2024-05-03  9:56   ` [PATCH v4 02/14] builtin/config: move option array around Patrick Steinhardt
2024-05-03  9:56   ` [PATCH v4 03/14] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
2024-05-03 12:28     ` Karthik Nayak
2024-05-06  9:34       ` Patrick Steinhardt
2024-05-03  9:57   ` [PATCH v4 04/14] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
2024-05-03  9:57   ` [PATCH v4 05/14] builtin/config: pull out function to handle config location Patrick Steinhardt
2024-05-03  9:57   ` [PATCH v4 06/14] builtin/config: pull out function to handle `--null` Patrick Steinhardt
2024-05-03  9:57   ` [PATCH v4 07/14] builtin/config: introduce "list" subcommand Patrick Steinhardt
2024-05-03 13:08     ` Karthik Nayak
2024-05-03 13:13       ` rsbecker
2024-05-03 16:01         ` Junio C Hamano
2024-05-06  7:51           ` Patrick Steinhardt
2024-05-06 17:13             ` Junio C Hamano
2024-05-06 18:33               ` rsbecker
2024-05-06 18:45                 ` Dragan Simic
2024-05-07  6:20                 ` Kristoffer Haugsbakk
2024-05-06 21:33               ` Git 3.0? Junio C Hamano
2024-05-07  4:18                 ` Patrick Steinhardt
2024-05-07  4:02               ` [PATCH v4 07/14] builtin/config: introduce "list" subcommand Patrick Steinhardt
2024-05-06  7:58       ` Patrick Steinhardt
2024-05-06 11:26         ` Karthik Nayak
2024-05-03  9:57   ` [PATCH v4 08/14] builtin/config: introduce "get" subcommand Patrick Steinhardt
2024-05-03  9:57   ` [PATCH v4 09/14] builtin/config: introduce "set" subcommand Patrick Steinhardt
2024-05-03  9:57   ` [PATCH v4 10/14] builtin/config: introduce "unset" subcommand Patrick Steinhardt
2024-05-03  9:57   ` [PATCH v4 11/14] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
2024-05-03  9:57   ` [PATCH v4 12/14] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
2024-05-03  9:57   ` [PATCH v4 13/14] builtin/config: introduce "edit" subcommand Patrick Steinhardt
2024-05-03  9:57   ` [PATCH v4 14/14] builtin/config: display subcommand help Patrick Steinhardt
2024-05-03 13:36   ` [PATCH v4 00/14] builtin/config: introduce subcommands Dragan Simic
2024-05-03 16:09   ` Junio C Hamano
2024-05-06  8:55 ` [PATCH v5 " Patrick Steinhardt
2024-05-06  8:55   ` [PATCH v5 01/14] config: clarify memory ownership when preparing comment strings Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 02/14] builtin/config: move option array around Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 03/14] builtin/config: move "fixed-value" option to correct group Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 04/14] builtin/config: use `OPT_CMDMODE()` to specify modes Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 05/14] builtin/config: pull out function to handle config location Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 06/14] builtin/config: pull out function to handle `--null` Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 07/14] builtin/config: introduce "list" subcommand Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 08/14] builtin/config: introduce "get" subcommand Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 09/14] builtin/config: introduce "set" subcommand Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 10/14] builtin/config: introduce "unset" subcommand Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 11/14] builtin/config: introduce "rename-section" subcommand Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 12/14] builtin/config: introduce "remove-section" subcommand Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 13/14] builtin/config: introduce "edit" subcommand Patrick Steinhardt
2024-05-06  8:56   ` [PATCH v5 14/14] builtin/config: display subcommand help Patrick Steinhardt
2024-05-06 11:30   ` [PATCH v5 00/14] builtin/config: introduce subcommands Karthik Nayak
2024-05-06 20:21   ` Taylor Blau
2024-05-06 20:38     ` rsbecker
2024-05-07  4:07       ` Patrick Steinhardt

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.