{"thread":{"id":"61061","subject":"[PATCH 0/8] builtin/config: introduce subcommands","startedAt":"2024-03-06T11:31:36Z","lastAt":"2025-09-11T11:05:19Z","messageCount":116,"participants":["Patrick Steinhardt","Junio C Hamano","Karthik Nayak","Jean-Noël AVILA","Kristoffer Haugsbakk","Taylor Blau","Dragan Simic","Eric Sunshine","rsbecker@nexbridge.com","SZEDER Gábor"],"isPatch":true,"patchVersion":1,"patchTotal":8},"messages":[{"id":"490068","messageId":"cover.1709724089.git.ps@pks.im","threadId":"61061","inReplyTo":null,"subject":"[PATCH 0/8] builtin/config: introduce subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-06T11:31:30Z","receivedAt":"2024-03-06T11:31:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthe UI of git-config(1) is quite arcane and does not really conform to\nthe more modern UIs that we have nowadays in Git:\n\n  - While it does have modes, those modes come in the form of switches.\n    E.g. you have to say git config --get-all to execute the \"get-all\"\n    mode.\n\n  - Its interface depends on the number of args. Given one arg it will\n    print the value of the corresponding config key, given two args it\n    will set that key. Did you know you can even give it three args? I\n    didn't. Now guess what this mode does.\n\nThis patch series overhauls git-config(1) by introducing subcommands.\nThis results in the following UI that matches more closely what we have\nin other Git commands which are more modern:\n\n  - `git config foo.bar` -> `git config get foo.bar`\n\n  - `git config foo.bar value` -> `git config set foo.bar value`\n\n  - `git config foo.bar value value-pattern` -> `git config set-all\n    foo.bar value value-pattern`\n\n  - `git config --get-urlmatch` -> `git config get-urlmatch`.\n\nMost importantly, this should help discoverability quite a lot by now\nalso having a proper synopsis in both the manpage, but also in `git\nconfig -h`.\n\nOf course, backwards compatibility is a big concern. We don't want to\njust switch over to the new syntax and break all existing scripts and\nmuscle memory. This patch series thus abuses the fact that the implicit\nmodes (`git config foo.bar`, `git config foo.bar value` and `git config\nfoo.bar value value-pattern`) all require a key as first argument. As\nkeys _must_ have a dot, this allows us to unambiguously discern those\nfrom actual subcommands.\n\nThus, git-config(1) now supports both old and new style arguments in a\ncompletely backwards compatible way, which is also demonstrated by the\nadapted tests. Eventually, I think we should consider dropping the old\nstyle syntax with e.g. Git v3.0.\n\nWe could of course iterate further from here and keep on improving the\nUI of the new subcommands, e.g. by merging closely related subcommands.\nBut for the time being I think it's easier to stop at this point and\nrevisit the result at a later point in time.\n\nAlso, note that I see this as kind of an experiment for how to modernize\nour UI slowly over time so that things become more consistent and thus\nhopefully easier to use in the long term.\n\nPatrick\n\nPatrick Steinhardt (8):\n  builtin/config: move option array around\n  builtin/config: move \"fixed-value\" option to correct group\n  builtin/config: use `OPT_CMDMODE()` to specify modes\n  builtin/config: move modes into separate functions\n  builtin/config: track subcommands by action\n  builtin/config: introduce subcommands\n  t1300: exercise both old- and new-style modes\n  Documentation/git-config: update to new-style syntax\n\n Documentation/git-config.txt | 204 +++++-----\n builtin/config.c             | 671 +++++++++++++++++++-------------\n t/t0450/txt-help-mismatches  |   1 -\n t/t1300-config.sh            | 734 ++++++++++++++++++-----------------\n 4 files changed, 900 insertions(+), 710 deletions(-)\n\n-- \n2.44.0\n\n"},{"id":"490069","messageId":"04210c1932c7df1b2e336089e49d50cf422e9da0.1709724089.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH 1/8] builtin/config: move option array around","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-06T11:31:34Z","receivedAt":"2024-03-06T11:31:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move around the option array. This will help us with a follow-up commit\nthat introduces subcommands to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 94 ++++++++++++++++++++++++------------------------\n 1 file changed, 47 insertions(+), 47 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex b55bfae7d6..6eb6aff917 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -134,53 +134,6 @@ static int option_parse_type(const struct option *opt, const char *arg,\n \treturn 0;\n }\n \n-static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n-\tOPT_GROUP(N_(\"Action\")),\n-\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n-\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n-\tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n-\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_END(),\n-};\n-\n-static NORETURN void usage_builtin_config(void)\n-{\n-\tusage_with_options(builtin_config_usage, builtin_config_options);\n-}\n-\n static void check_argc(int argc, int min, int max)\n {\n \tif (argc >= min && argc <= max)\n@@ -669,6 +622,53 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n+static struct option builtin_config_options[] = {\n+\tOPT_GROUP(N_(\"Config file location\")),\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tOPT_GROUP(N_(\"Action\")),\n+\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n+\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n+\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n+\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n+\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n+\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n+\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n+\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_GROUP(N_(\"Type\")),\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tOPT_GROUP(N_(\"Other\")),\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n+\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_END(),\n+};\n+\n+static NORETURN void usage_builtin_config(void)\n+{\n+\tusage_with_options(builtin_config_usage, builtin_config_options);\n+}\n+\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tint nongit = !startup_info->have_repository;\n-- \n2.44.0\n\n"},{"id":"490070","messageId":"4c3655574ab7ef1eadf75aed883536204e542979.1709724089.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH 2/8] builtin/config: move \"fixed-value\" option to correct group","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-06T11:31:38Z","receivedAt":"2024-03-06T11:31:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `--fixed-value` option can be used to alter how the value-pattern\nparameter is interpreted for the various submodes of git-config(1). But\nwhile it is an option, it is currently listed as part of the submodes\ngroup the command, which is wrong.\n\nMove the option to the \"Other\" group, which hosts the various options\nknown to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 6eb6aff917..fcd6190f12 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -642,7 +642,6 @@ static struct option builtin_config_options[] = {\n \tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n \tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n \tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n \tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n \tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n \tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n@@ -661,6 +660,7 @@ static struct option builtin_config_options[] = {\n \tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n \tOPT_END(),\n };\n \n-- \n2.44.0\n\n"},{"id":"490071","messageId":"41e5bf1d6aa35a32f961b7f9d82a70781674eed0.1709724089.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH 3/8] builtin/config: use `OPT_CMDMODE()` to specify modes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-06T11:31:42Z","receivedAt":"2024-03-06T11:31:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The git-config(1) command has various different modes which are\naccessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are\ndeclared with `OPT_BIT()`, which causes two minor issues:\n\n  - The respective modes also have a negated form `--no-get-urlmatch`,\n    which is unintended.\n\n  - We have to manually handle exclusiveness of the modes.\n\nSwitch these options to instead use `OPT_CMDMODE()`, which is made\nexactly for this usecase. Remove the now-unneeded check that only a\nsingle mode is given, which is now handled by the parse-options\ninterface.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c  | 32 ++++++++++++++------------------\n t/t1300-config.sh | 13 +++++++++++++\n 2 files changed, 27 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex fcd6190f12..8a2d1a5de7 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -631,20 +631,20 @@ static struct option builtin_config_options[] = {\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n \tOPT_GROUP(N_(\"Action\")),\n-\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n+\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n+\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n+\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n+\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n+\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n+\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n+\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n \tOPT_GROUP(N_(\"Type\")),\n \tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n \tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n@@ -767,10 +767,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (HAS_MULTI_BITS(actions)) {\n-\t\terror(_(\"only one action at a time\"));\n-\t\tusage_builtin_config();\n-\t}\n \tif (actions == 0)\n \t\tswitch (argc) {\n \t\tcase 1: actions = ACTION_GET; break;\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 31c3878687..2d1bc1e27e 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -2612,4 +2612,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such\n \tgrep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n '\n \n+test_expect_success 'negated mode causes failure' '\n+\ttest_must_fail git config --no-get 2>err &&\n+\tgrep \"unknown option \\`no-get${SQ}\" err\n+'\n+\n+test_expect_success 'specifying multiple modes causes failure' '\n+\tcat >expect <<-EOF &&\n+\terror: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together\n+\tEOF\n+\ttest_must_fail git config --get --get-all 2>err &&\n+\ttest_cmp expect err\n+'\n+\n test_done\n-- \n2.44.0\n\n"},{"id":"490072","messageId":"b258f31b7d9d7bb104175b7dedc3e13506cea9e9.1709724089.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH 4/8] builtin/config: move modes into separate functions","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-06T11:31:46Z","receivedAt":"2024-03-06T11:31:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The git-config(1) command has several different modes which cause it to\ndo different things. The logic for each of these modes is hosted in a\ngiant switch in `cmd_config()` itself. For one, this is hard to read.\nBut second, we're about to introduce proper subcommands to git-config(1)\nthat will require separate functions for each of the modes.\n\nRefactor the code and move each mode into its own function to prepare\nfor this.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 410 +++++++++++++++++++++++++++++------------------\n 1 file changed, 255 insertions(+), 155 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 8a2d1a5de7..a6ab9b8204 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -44,6 +44,7 @@ static struct config_options config_options;\n static int show_origin;\n static int show_scope;\n static int fixed_value;\n+static int config_flags;\n \n #define ACTION_GET (1<<0)\n #define ACTION_GET_ALL (1<<1)\n@@ -622,6 +623,225 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n+static int cmd_config_list(int argc, const char **argv, const char *prefix)\n+{\n+\tcheck_argc(argc, 0, 0);\n+\tif (config_with_options(show_all_config, NULL,\n+\t\t\t\t&given_config_source, the_repository,\n+\t\t\t\t&config_options) < 0) {\n+\t\tif (given_config_source.file)\n+\t\t\tdie_errno(_(\"unable to read config file '%s'\"),\n+\t\t\t\t  given_config_source.file);\n+\t\telse\n+\t\t\tdie(_(\"error processing config file(s)\"));\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int cmd_config_edit(int argc, const char **argv, const char *prefix)\n+{\n+\tchar *config_file;\n+\n+\tcheck_argc(argc, 0, 0);\n+\tif (!given_config_source.file && !startup_info->have_repository)\n+\t\tdie(_(\"not in a git directory\"));\n+\tif (given_config_source.use_stdin)\n+\t\tdie(_(\"editing stdin is not supported\"));\n+\tif (given_config_source.blob)\n+\t\tdie(_(\"editing blobs is not supported\"));\n+\tgit_config(git_default_config, NULL);\n+\tconfig_file = given_config_source.file ?\n+\t\t\txstrdup(given_config_source.file) :\n+\t\t\tgit_pathdup(\"config\");\n+\tif (use_global_config) {\n+\t\tint fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);\n+\t\tif (fd >= 0) {\n+\t\t\tchar *content = default_user_config();\n+\t\t\twrite_str_in_full(fd, content);\n+\t\t\tfree(content);\n+\t\t\tclose(fd);\n+\t\t}\n+\t\telse if (errno != EEXIST)\n+\t\t\tdie_errno(_(\"cannot create configuration file %s\"), config_file);\n+\t}\n+\tlaunch_editor(config_file, NULL, NULL);\n+\tfree(config_file);\n+\n+\treturn 0;\n+}\n+\n+static int cmd_config_set(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\tchar *value = NULL;\n+\tint ret;\n+\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n+\tret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);\n+\tif (ret == CONFIG_NOTHING_SET)\n+\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n+\t\t\"       Use a regexp, --add or --replace-all to change %s.\"), argv[0]);\n+\n+\tfree(value);\n+\treturn ret;\n+}\n+\n+static int cmd_config_set_all(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\tchar *value = NULL;\n+\tint ret;\n+\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 3);\n+\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n+\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t     argv[0], value, argv[2],\n+\t\t\t\t\t\t     config_flags);\n+\n+\tfree(value);\n+\treturn ret;\n+}\n+\n+static int cmd_config_add(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\tchar *value = NULL;\n+\tint ret;\n+\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n+\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t     argv[0], value,\n+\t\t\t\t\t\t     CONFIG_REGEX_NONE,\n+\t\t\t\t\t\t     config_flags);\n+\n+\tfree(value);\n+\treturn ret;\n+}\n+\n+static int cmd_config_replace_all(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\tchar *value = NULL;\n+\tint ret;\n+\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 3);\n+\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n+\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t     argv[0], value, argv[2],\n+\t\t\t\t\t\t     config_flags | CONFIG_FLAGS_MULTI_REPLACE);\n+\n+\tfree(value);\n+\treturn ret;\n+}\n+\n+static int cmd_config_get(int argc, const char **argv, const char *prefix)\n+{\n+\tcheck_argc(argc, 1, 2);\n+\treturn get_value(argv[0], argv[1], config_flags);\n+}\n+\n+static int cmd_config_get_all(int argc, const char **argv, const char *prefix)\n+{\n+\tdo_all = 1;\n+\tcheck_argc(argc, 1, 2);\n+\treturn get_value(argv[0], argv[1], config_flags);\n+}\n+\n+static int cmd_config_get_regexp(int argc, const char **argv, const char *prefix)\n+{\n+\tshow_keys = 1;\n+\tuse_key_regexp = 1;\n+\tdo_all = 1;\n+\tcheck_argc(argc, 1, 2);\n+\treturn get_value(argv[0], argv[1], config_flags);\n+}\n+\n+static int cmd_config_get_urlmatch(int argc, const char **argv, const char *prefix)\n+{\n+\tcheck_argc(argc, 2, 2);\n+\treturn get_urlmatch(argv[0], argv[1]);\n+}\n+\n+static int cmd_config_unset(int argc, const char **argv, const char *prefix)\n+{\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 2);\n+\tif (argc == 2)\n+\t\treturn git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t\t      argv[0], NULL, argv[1],\n+\t\t\t\t\t\t\t      config_flags);\n+\telse\n+\t\treturn git_config_set_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t     argv[0], NULL);\n+}\n+\n+static int cmd_config_unset_all(int argc, const char **argv, const char *prefix)\n+{\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 2);\n+\treturn git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t      argv[0], NULL, argv[1],\n+\t\t\t\t\t\t      config_flags | CONFIG_FLAGS_MULTI_REPLACE);\n+}\n+\n+static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)\n+{\n+\tint ret;\n+\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\tret = git_config_rename_section_in_file(given_config_source.file,\n+\t\t\t\t\t\targv[0], argv[1]);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!ret)\n+\t\tdie(_(\"no such section: %s\"), argv[0]);\n+\telse\n+\t\tret = 0;\n+\n+\treturn ret;\n+}\n+\n+static int cmd_config_remove_section(int argc, const char **argv, const char *prefix)\n+{\n+\tint ret;\n+\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 1);\n+\tret = git_config_rename_section_in_file(given_config_source.file,\n+\t\t\t\t\t\targv[0], NULL);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!ret)\n+\t\tdie(_(\"no such section: %s\"), argv[0]);\n+\telse\n+\t\tret = 0;\n+\n+\treturn ret;\n+}\n+\n+static int cmd_config_get_color(int argc, const char **argv, const char *prefix)\n+{\n+\tcheck_argc(argc, 1, 2);\n+\tget_color(argv[0], argv[1]);\n+\treturn 0;\n+}\n+\n+static int cmd_config_get_colorbool(int argc, const char **argv, const char *prefix)\n+{\n+\tcheck_argc(argc, 1, 2);\n+\tif (argc == 2)\n+\t\tcolor_stdout_is_tty = git_config_bool(\"command line\", argv[1]);\n+\treturn get_colorbool(argv[0], argc == 2);\n+}\n+\n static struct option builtin_config_options[] = {\n \tOPT_GROUP(N_(\"Config file location\")),\n \tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n@@ -671,12 +891,6 @@ static NORETURN void usage_builtin_config(void)\n \n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n-\tint nongit = !startup_info->have_repository;\n-\tchar *value = NULL;\n-\tint flags = 0;\n-\tint ret = 0;\n-\tstruct key_value_info default_kvi = KVI_INIT;\n-\n \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n \n \targc = parse_options(argc, argv, prefix, builtin_config_options,\n@@ -690,7 +904,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (nongit) {\n+\tif (!startup_info->have_repository) {\n \t\tif (use_local_config)\n \t\t\tdie(_(\"--local can only be used inside a git repository\"));\n \t\tif (given_config_source.blob)\n@@ -751,7 +965,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tconfig_options.respect_includes = !given_config_source.file;\n \telse\n \t\tconfig_options.respect_includes = respect_includes_opt;\n-\tif (!nongit) {\n+\tif (startup_info->have_repository) {\n \t\tconfig_options.commondir = get_git_common_dir();\n \t\tconfig_options.git_dir = get_git_dir();\n \t}\n@@ -826,159 +1040,45 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\t\tusage_builtin_config();\n \t\t}\n \n-\t\tflags |= CONFIG_FLAGS_FIXED_VALUE;\n+\t\tconfig_flags |= CONFIG_FLAGS_FIXED_VALUE;\n \t}\n \n \tif (actions & PAGING_ACTIONS)\n \t\tsetup_auto_pager(\"config\", 1);\n \n \tif (actions == ACTION_LIST) {\n-\t\tcheck_argc(argc, 0, 0);\n-\t\tif (config_with_options(show_all_config, NULL,\n-\t\t\t\t\t&given_config_source, the_repository,\n-\t\t\t\t\t&config_options) < 0) {\n-\t\t\tif (given_config_source.file)\n-\t\t\t\tdie_errno(_(\"unable to read config file '%s'\"),\n-\t\t\t\t\t  given_config_source.file);\n-\t\t\telse\n-\t\t\t\tdie(_(\"error processing config file(s)\"));\n-\t\t}\n+\t\treturn cmd_config_list(argc, argv, prefix);\n+\t} else if (actions == ACTION_EDIT) {\n+\t\treturn cmd_config_edit(argc, argv, prefix);\n+\t} else if (actions == ACTION_SET) {\n+\t\treturn cmd_config_set(argc, argv, prefix);\n+\t} else if (actions == ACTION_SET_ALL) {\n+\t\treturn cmd_config_set_all(argc, argv, prefix);\n+\t} else if (actions == ACTION_ADD) {\n+\t\treturn cmd_config_add(argc, argv, prefix);\n+\t} else if (actions == ACTION_REPLACE_ALL) {\n+\t\treturn cmd_config_replace_all(argc, argv, prefix);\n+\t} else if (actions == ACTION_GET) {\n+\t\treturn cmd_config_get(argc, argv, prefix);\n+\t} else if (actions == ACTION_GET_ALL) {\n+\t\treturn cmd_config_get_all(argc, argv, prefix);\n+\t} else if (actions == ACTION_GET_REGEXP) {\n+\t\treturn cmd_config_get_regexp(argc, argv, prefix);\n+\t} else if (actions == ACTION_GET_URLMATCH) {\n+\t\treturn cmd_config_get_urlmatch(argc, argv, prefix);\n+\t} else if (actions == ACTION_UNSET) {\n+\t\treturn cmd_config_unset(argc, argv, prefix);\n+\t} else if (actions == ACTION_UNSET_ALL) {\n+\t\treturn cmd_config_unset_all(argc, argv, prefix);\n+\t} else if (actions == ACTION_RENAME_SECTION) {\n+\t\treturn cmd_config_rename_section(argc, argv, prefix);\n+\t} else if (actions == ACTION_REMOVE_SECTION) {\n+\t\treturn cmd_config_remove_section(argc, argv, prefix);\n+\t} else if (actions == ACTION_GET_COLOR) {\n+\t\treturn cmd_config_get_color(argc, argv, prefix);\n+\t} else if (actions == ACTION_GET_COLORBOOL) {\n+\t\treturn cmd_config_get_colorbool(argc, argv, prefix);\n \t}\n-\telse if (actions == ACTION_EDIT) {\n-\t\tchar *config_file;\n \n-\t\tcheck_argc(argc, 0, 0);\n-\t\tif (!given_config_source.file && nongit)\n-\t\t\tdie(_(\"not in a git directory\"));\n-\t\tif (given_config_source.use_stdin)\n-\t\t\tdie(_(\"editing stdin is not supported\"));\n-\t\tif (given_config_source.blob)\n-\t\t\tdie(_(\"editing blobs is not supported\"));\n-\t\tgit_config(git_default_config, NULL);\n-\t\tconfig_file = given_config_source.file ?\n-\t\t\t\txstrdup(given_config_source.file) :\n-\t\t\t\tgit_pathdup(\"config\");\n-\t\tif (use_global_config) {\n-\t\t\tint fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);\n-\t\t\tif (fd >= 0) {\n-\t\t\t\tchar *content = default_user_config();\n-\t\t\t\twrite_str_in_full(fd, content);\n-\t\t\t\tfree(content);\n-\t\t\t\tclose(fd);\n-\t\t\t}\n-\t\t\telse if (errno != EEXIST)\n-\t\t\t\tdie_errno(_(\"cannot create configuration file %s\"), config_file);\n-\t\t}\n-\t\tlaunch_editor(config_file, NULL, NULL);\n-\t\tfree(config_file);\n-\t}\n-\telse if (actions == ACTION_SET) {\n-\t\tcheck_write();\n-\t\tcheck_argc(argc, 2, 2);\n-\t\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n-\t\tret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);\n-\t\tif (ret == CONFIG_NOTHING_SET)\n-\t\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n-\t\t\t\"       Use a regexp, --add or --replace-all to change %s.\"), argv[0]);\n-\t}\n-\telse if (actions == ACTION_SET_ALL) {\n-\t\tcheck_write();\n-\t\tcheck_argc(argc, 2, 3);\n-\t\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n-\t\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n-\t\t\t\t\t\t\t     argv[0], value, argv[2],\n-\t\t\t\t\t\t\t     flags);\n-\t}\n-\telse if (actions == ACTION_ADD) {\n-\t\tcheck_write();\n-\t\tcheck_argc(argc, 2, 2);\n-\t\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n-\t\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n-\t\t\t\t\t\t\t     argv[0], value,\n-\t\t\t\t\t\t\t     CONFIG_REGEX_NONE,\n-\t\t\t\t\t\t\t     flags);\n-\t}\n-\telse if (actions == ACTION_REPLACE_ALL) {\n-\t\tcheck_write();\n-\t\tcheck_argc(argc, 2, 3);\n-\t\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n-\t\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n-\t\t\t\t\t\t\t     argv[0], value, argv[2],\n-\t\t\t\t\t\t\t     flags | CONFIG_FLAGS_MULTI_REPLACE);\n-\t}\n-\telse if (actions == ACTION_GET) {\n-\t\tcheck_argc(argc, 1, 2);\n-\t\treturn get_value(argv[0], argv[1], flags);\n-\t}\n-\telse if (actions == ACTION_GET_ALL) {\n-\t\tdo_all = 1;\n-\t\tcheck_argc(argc, 1, 2);\n-\t\treturn get_value(argv[0], argv[1], flags);\n-\t}\n-\telse if (actions == ACTION_GET_REGEXP) {\n-\t\tshow_keys = 1;\n-\t\tuse_key_regexp = 1;\n-\t\tdo_all = 1;\n-\t\tcheck_argc(argc, 1, 2);\n-\t\treturn get_value(argv[0], argv[1], flags);\n-\t}\n-\telse if (actions == ACTION_GET_URLMATCH) {\n-\t\tcheck_argc(argc, 2, 2);\n-\t\treturn get_urlmatch(argv[0], argv[1]);\n-\t}\n-\telse if (actions == ACTION_UNSET) {\n-\t\tcheck_write();\n-\t\tcheck_argc(argc, 1, 2);\n-\t\tif (argc == 2)\n-\t\t\treturn git_config_set_multivar_in_file_gently(given_config_source.file,\n-\t\t\t\t\t\t\t\t      argv[0], NULL, argv[1],\n-\t\t\t\t\t\t\t\t      flags);\n-\t\telse\n-\t\t\treturn git_config_set_in_file_gently(given_config_source.file,\n-\t\t\t\t\t\t\t     argv[0], NULL);\n-\t}\n-\telse if (actions == ACTION_UNSET_ALL) {\n-\t\tcheck_write();\n-\t\tcheck_argc(argc, 1, 2);\n-\t\treturn git_config_set_multivar_in_file_gently(given_config_source.file,\n-\t\t\t\t\t\t\t      argv[0], NULL, argv[1],\n-\t\t\t\t\t\t\t      flags | CONFIG_FLAGS_MULTI_REPLACE);\n-\t}\n-\telse if (actions == ACTION_RENAME_SECTION) {\n-\t\tcheck_write();\n-\t\tcheck_argc(argc, 2, 2);\n-\t\tret = git_config_rename_section_in_file(given_config_source.file,\n-\t\t\t\t\t\t\targv[0], argv[1]);\n-\t\tif (ret < 0)\n-\t\t\treturn ret;\n-\t\telse if (!ret)\n-\t\t\tdie(_(\"no such section: %s\"), argv[0]);\n-\t\telse\n-\t\t\tret = 0;\n-\t}\n-\telse if (actions == ACTION_REMOVE_SECTION) {\n-\t\tcheck_write();\n-\t\tcheck_argc(argc, 1, 1);\n-\t\tret = git_config_rename_section_in_file(given_config_source.file,\n-\t\t\t\t\t\t\targv[0], NULL);\n-\t\tif (ret < 0)\n-\t\t\treturn ret;\n-\t\telse if (!ret)\n-\t\t\tdie(_(\"no such section: %s\"), argv[0]);\n-\t\telse\n-\t\t\tret = 0;\n-\t}\n-\telse if (actions == ACTION_GET_COLOR) {\n-\t\tcheck_argc(argc, 1, 2);\n-\t\tget_color(argv[0], argv[1]);\n-\t}\n-\telse if (actions == ACTION_GET_COLORBOOL) {\n-\t\tcheck_argc(argc, 1, 2);\n-\t\tif (argc == 2)\n-\t\t\tcolor_stdout_is_tty = git_config_bool(\"command line\", argv[1]);\n-\t\treturn get_colorbool(argv[0], argc == 2);\n-\t}\n-\n-\tfree(value);\n-\treturn ret;\n+\tBUG(\"invalid action\");\n }\n-- \n2.44.0\n\n"},{"id":"490073","messageId":"e39774d6495767c6505999bdc33110678139e347.1709724089.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH 5/8] builtin/config: track subcommands by action","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-06T11:31:50Z","receivedAt":"2024-03-06T11:31:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Part of `cmd_config()` is a rather unwieldy switch statement that\ninvokes the correct subcommand function based on which action has been\nrequested by the user. Now that we have converted actions to be tracked\nvia a `OPT_CMDMODE()`, we know that the `actions` variable will\nonly ever have at most one bit set. This allows us to convert the\nvariable to use an `enum` instead, and thus to create an array that maps\nfrom this newly introduced `enum` to the corresponding subcommand\nfunction.\n\nRefactor the code to do so. Besides allowing us to get rid of the giant\nswitch statement, this refactoring will also make it easier to introduce\nproper subcommands to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 207 +++++++++++++++++++++++------------------------\n 1 file changed, 99 insertions(+), 108 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex a6ab9b8204..0d58397ef5 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -20,6 +20,26 @@ static const char *const builtin_config_usage[] = {\n \tNULL\n };\n \n+enum config_action {\n+\tACTION_NONE,\n+\tACTION_GET,\n+\tACTION_GET_ALL,\n+\tACTION_GET_REGEXP,\n+\tACTION_REPLACE_ALL,\n+\tACTION_ADD,\n+\tACTION_UNSET,\n+\tACTION_UNSET_ALL,\n+\tACTION_RENAME_SECTION,\n+\tACTION_REMOVE_SECTION,\n+\tACTION_LIST,\n+\tACTION_EDIT,\n+\tACTION_SET,\n+\tACTION_SET_ALL,\n+\tACTION_GET_COLOR,\n+\tACTION_GET_COLORBOOL,\n+\tACTION_GET_URLMATCH,\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -33,10 +53,12 @@ static char delim = '=';\n static char key_delim = ' ';\n static char term = '\\n';\n \n+static parse_opt_subcommand_fn *subcommand;\n+static enum config_action action = ACTION_NONE;\n static int use_global_config, use_system_config, use_local_config;\n static int use_worktree_config;\n static struct git_config_source given_config_source;\n-static int actions, type;\n+static int type;\n static char *default_value;\n static int end_nul;\n static int respect_includes_opt = -1;\n@@ -46,30 +68,6 @@ static int show_scope;\n static int fixed_value;\n static int config_flags;\n \n-#define ACTION_GET (1<<0)\n-#define ACTION_GET_ALL (1<<1)\n-#define ACTION_GET_REGEXP (1<<2)\n-#define ACTION_REPLACE_ALL (1<<3)\n-#define ACTION_ADD (1<<4)\n-#define ACTION_UNSET (1<<5)\n-#define ACTION_UNSET_ALL (1<<6)\n-#define ACTION_RENAME_SECTION (1<<7)\n-#define ACTION_REMOVE_SECTION (1<<8)\n-#define ACTION_LIST (1<<9)\n-#define ACTION_EDIT (1<<10)\n-#define ACTION_SET (1<<11)\n-#define ACTION_SET_ALL (1<<12)\n-#define ACTION_GET_COLOR (1<<13)\n-#define ACTION_GET_COLORBOOL (1<<14)\n-#define ACTION_GET_URLMATCH (1<<15)\n-\n-/*\n- * The actions \"ACTION_LIST | ACTION_GET_*\" which may produce more than\n- * one line of output and which should therefore be paged.\n- */\n-#define PAGING_ACTIONS (ACTION_LIST | ACTION_GET_ALL | \\\n-\t\t\tACTION_GET_REGEXP | ACTION_GET_URLMATCH)\n-\n #define TYPE_BOOL\t\t1\n #define TYPE_INT\t\t2\n #define TYPE_BOOL_OR_INT\t3\n@@ -842,6 +840,25 @@ static int cmd_config_get_colorbool(int argc, const char **argv, const char *pre\n \treturn get_colorbool(argv[0], argc == 2);\n }\n \n+static parse_opt_subcommand_fn *subcommands_by_action[] = {\n+\t[ACTION_LIST] = cmd_config_list,\n+\t[ACTION_EDIT] = cmd_config_edit,\n+\t[ACTION_SET] = cmd_config_set,\n+\t[ACTION_SET_ALL] = cmd_config_set_all,\n+\t[ACTION_ADD] = cmd_config_add,\n+\t[ACTION_REPLACE_ALL] = cmd_config_replace_all,\n+\t[ACTION_GET] = cmd_config_get,\n+\t[ACTION_GET_ALL] = cmd_config_get_all,\n+\t[ACTION_GET_REGEXP] = cmd_config_get_regexp,\n+\t[ACTION_GET_URLMATCH] = cmd_config_get_urlmatch,\n+\t[ACTION_UNSET] = cmd_config_unset,\n+\t[ACTION_UNSET_ALL] = cmd_config_unset_all,\n+\t[ACTION_RENAME_SECTION] = cmd_config_rename_section,\n+\t[ACTION_REMOVE_SECTION] = cmd_config_remove_section,\n+\t[ACTION_GET_COLOR] = cmd_config_get_color,\n+\t[ACTION_GET_COLORBOOL] = cmd_config_get_colorbool,\n+};\n+\n static struct option builtin_config_options[] = {\n \tOPT_GROUP(N_(\"Config file location\")),\n \tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n@@ -851,20 +868,20 @@ static struct option builtin_config_options[] = {\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n \tOPT_GROUP(N_(\"Action\")),\n-\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_CMDMODE(0, \"get\", &action, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n+\tOPT_CMDMODE(0, \"get-all\", &action, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n+\tOPT_CMDMODE(0, \"get-regexp\", &action, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n+\tOPT_CMDMODE(0, \"get-urlmatch\", &action, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_CMDMODE(0, \"replace-all\", &action, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n+\tOPT_CMDMODE(0, \"add\", &action, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_CMDMODE(0, \"unset\", &action, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n+\tOPT_CMDMODE(0, \"unset-all\", &action, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n+\tOPT_CMDMODE(0, \"rename-section\", &action, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_CMDMODE(0, \"remove-section\", &action, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_CMDMODE('l', \"list\", &action, N_(\"list all\"), ACTION_LIST),\n+\tOPT_CMDMODE('e', \"edit\", &action, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_CMDMODE(0, \"get-color\", &action, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n+\tOPT_CMDMODE(0, \"get-colorbool\", &action, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n \tOPT_GROUP(N_(\"Type\")),\n \tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n \tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n@@ -976,33 +993,43 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tkey_delim = '\\n';\n \t}\n \n-\tif ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {\n-\t\terror(_(\"--get-color and variable type are incoherent\"));\n-\t\tusage_builtin_config();\n-\t}\n-\n-\tif (actions == 0)\n+\tif (action == ACTION_NONE) {\n \t\tswitch (argc) {\n-\t\tcase 1: actions = ACTION_GET; break;\n-\t\tcase 2: actions = ACTION_SET; break;\n-\t\tcase 3: actions = ACTION_SET_ALL; break;\n+\t\tcase 1: action = ACTION_GET; break;\n+\t\tcase 2: action = ACTION_SET; break;\n+\t\tcase 3: action = ACTION_SET_ALL; break;\n \t\tdefault:\n \t\t\tusage_builtin_config();\n \t\t}\n+\t}\n+\tif (action <= ACTION_NONE || action >= ARRAY_SIZE(subcommands_by_action))\n+\t\tBUG(\"invalid action %d\", action);\n+\tsubcommand = subcommands_by_action[action];\n+\n+\tif (type && (subcommand == cmd_config_get_color ||\n+\t\t     subcommand == cmd_config_get_colorbool)) {\n+\t\terror(_(\"--get-color and variable type are incoherent\"));\n+\t\tusage_builtin_config();\n+\t}\n+\n \tif (omit_values &&\n-\t    !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {\n+\t    subcommand != cmd_config_list &&\n+\t    subcommand != cmd_config_get_regexp) {\n \t\terror(_(\"--name-only is only applicable to --list or --get-regexp\"));\n \t\tusage_builtin_config();\n \t}\n \n-\tif (show_origin && !(actions &\n-\t\t(ACTION_GET|ACTION_GET_ALL|ACTION_GET_REGEXP|ACTION_LIST))) {\n+\tif (show_origin &&\n+\t    subcommand != cmd_config_get &&\n+\t    subcommand != cmd_config_get_all &&\n+\t    subcommand != cmd_config_get_regexp &&\n+\t    subcommand != cmd_config_list) {\n \t\terror(_(\"--show-origin is only applicable to --get, --get-all, \"\n \t\t\t\"--get-regexp, and --list\"));\n \t\tusage_builtin_config();\n \t}\n \n-\tif (default_value && !(actions & ACTION_GET)) {\n+\tif (default_value && subcommand != cmd_config_get) {\n \t\terror(_(\"--default is only applicable to --get\"));\n \t\tusage_builtin_config();\n \t}\n@@ -1011,28 +1038,19 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \tif (fixed_value) {\n \t\tint allowed_usage = 0;\n \n-\t\tswitch (actions) {\n-\t\t/* git config --get <name> <value-pattern> */\n-\t\tcase ACTION_GET:\n-\t\t/* git config --get-all <name> <value-pattern> */\n-\t\tcase ACTION_GET_ALL:\n-\t\t/* git config --get-regexp <name-pattern> <value-pattern> */\n-\t\tcase ACTION_GET_REGEXP:\n-\t\t/* git config --unset <name> <value-pattern> */\n-\t\tcase ACTION_UNSET:\n-\t\t/* git config --unset-all <name> <value-pattern> */\n-\t\tcase ACTION_UNSET_ALL:\n+\t\tif (subcommand == cmd_config_get ||\n+\t\t    subcommand == cmd_config_get_all ||\n+\t\t    subcommand == cmd_config_get_regexp ||\n+\t\t    subcommand == cmd_config_unset ||\n+\t\t    subcommand == cmd_config_unset_all) {\n+\t\t\t/* git config --<action> <name> <value-pattern> */\n \t\t\tallowed_usage = argc > 1 && !!argv[1];\n-\t\t\tbreak;\n-\n-\t\t/* git config <name> <value> <value-pattern> */\n-\t\tcase ACTION_SET_ALL:\n-\t\t/* git config --replace-all <name> <value> <value-pattern> */\n-\t\tcase ACTION_REPLACE_ALL:\n+\t\t} else if (subcommand == cmd_config_set_all ||\n+\t\t\t   subcommand == cmd_config_replace_all) {\n+\t\t\t/* git config --<action> <name> <value> <value-pattern> */\n \t\t\tallowed_usage = argc > 2 && !!argv[2];\n-\t\t\tbreak;\n-\n-\t\t/* other options don't allow --fixed-value */\n+\t\t} else {\n+\t\t\t/* other options don't allow --fixed-value */\n \t\t}\n \n \t\tif (!allowed_usage) {\n@@ -1043,42 +1061,15 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tconfig_flags |= CONFIG_FLAGS_FIXED_VALUE;\n \t}\n \n-\tif (actions & PAGING_ACTIONS)\n+\t/*\n+\t * The actions \"ACTION_LIST | ACTION_GET_*\" which may produce more than\n+\t * one line of output and which should therefore be paged.\n+\t */\n+\tif (subcommand == cmd_config_list ||\n+\t    subcommand == cmd_config_get_all ||\n+\t    subcommand == cmd_config_get_regexp ||\n+\t    subcommand == cmd_config_get_urlmatch)\n \t\tsetup_auto_pager(\"config\", 1);\n \n-\tif (actions == ACTION_LIST) {\n-\t\treturn cmd_config_list(argc, argv, prefix);\n-\t} else if (actions == ACTION_EDIT) {\n-\t\treturn cmd_config_edit(argc, argv, prefix);\n-\t} else if (actions == ACTION_SET) {\n-\t\treturn cmd_config_set(argc, argv, prefix);\n-\t} else if (actions == ACTION_SET_ALL) {\n-\t\treturn cmd_config_set_all(argc, argv, prefix);\n-\t} else if (actions == ACTION_ADD) {\n-\t\treturn cmd_config_add(argc, argv, prefix);\n-\t} else if (actions == ACTION_REPLACE_ALL) {\n-\t\treturn cmd_config_replace_all(argc, argv, prefix);\n-\t} else if (actions == ACTION_GET) {\n-\t\treturn cmd_config_get(argc, argv, prefix);\n-\t} else if (actions == ACTION_GET_ALL) {\n-\t\treturn cmd_config_get_all(argc, argv, prefix);\n-\t} else if (actions == ACTION_GET_REGEXP) {\n-\t\treturn cmd_config_get_regexp(argc, argv, prefix);\n-\t} else if (actions == ACTION_GET_URLMATCH) {\n-\t\treturn cmd_config_get_urlmatch(argc, argv, prefix);\n-\t} else if (actions == ACTION_UNSET) {\n-\t\treturn cmd_config_unset(argc, argv, prefix);\n-\t} else if (actions == ACTION_UNSET_ALL) {\n-\t\treturn cmd_config_unset_all(argc, argv, prefix);\n-\t} else if (actions == ACTION_RENAME_SECTION) {\n-\t\treturn cmd_config_rename_section(argc, argv, prefix);\n-\t} else if (actions == ACTION_REMOVE_SECTION) {\n-\t\treturn cmd_config_remove_section(argc, argv, prefix);\n-\t} else if (actions == ACTION_GET_COLOR) {\n-\t\treturn cmd_config_get_color(argc, argv, prefix);\n-\t} else if (actions == ACTION_GET_COLORBOOL) {\n-\t\treturn cmd_config_get_colorbool(argc, argv, prefix);\n-\t}\n-\n-\tBUG(\"invalid action\");\n+\treturn subcommand(argc, argv, prefix);\n }\n-- \n2.44.0\n\n"},{"id":"490074","messageId":"f79c0f94e415de5c1c7b4120af5270fe4900d825.1709724089.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH 6/8] builtin/config: introduce subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-06T11:31:54Z","receivedAt":"2024-03-06T11:31:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-config(1) has several modes, those modes are not exposed with\nsubcommands but instead by specifying e.g. `--unset` or `--list`. This\nuser interface is not really in line with how our more modern commands\nwork, where it is a lot more customary to say e.g. `git remote list`.\nFurthermore, to add to the confusion, git-config(1) also allows the user\nto request modes implicitly by just specifying the correct number of\narguments. Thus, `git config foo.bar` will retrieve the value of\n\"foo.bar\" while `git config foo.bar baz` will set it to \"baz\".\n\nOverall, this makes for a confusing interface that could really use a\nmakeover. It hurts discoverability of what you can do with git-config(1)\nand is comparatively easy to get wrong.\n\nModernize git-config(1) so that it understands proper subcommands like\n\"list\" or \"unset\". Like this, a user can say \"git config get foo.bar\" to\nretrieve the config key's value and \"git config set foo.bar baz\" to set\nit.\n\nOne concern in this context is backwards compatibility. Luckily, we can\nintroduce subcommands without breaking backwards compatibility at all.\nThis is because all the implicit modes of git-config(1) require that the\nfirst argument is a properly formatted config key. And as config keys\n_must_ have a dot in their name, any value without a dot would have been\ndiscarded by git-config(1) previous to this change. Thus, given that\nnone of the subcommands do have a dot, they are unambiguous.\n\nConsequently, we introduce subcommands in such a way that git-config(1)\nunderstands both the old and the new syntax at the same time. This\nshould help to transition to the new-style syntax until we eventually\ndeprecate and remove the old-style syntax.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 52 +++++++++++++++++++++++++++++++++++++++++-------\n 1 file changed, 45 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 0d58397ef5..10fa933931 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -859,6 +859,26 @@ static parse_opt_subcommand_fn *subcommands_by_action[] = {\n \t[ACTION_GET_COLORBOOL] = cmd_config_get_colorbool,\n };\n \n+static struct option builtin_subcommand_options[] = {\n+\tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n+\tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n+\tOPT_SUBCOMMAND(\"get-all\", &subcommand, cmd_config_get_all),\n+\tOPT_SUBCOMMAND(\"get-color\", &subcommand, cmd_config_get_color),\n+\tOPT_SUBCOMMAND(\"get-colorbool\", &subcommand, cmd_config_get_colorbool),\n+\tOPT_SUBCOMMAND(\"get-regexp\", &subcommand, cmd_config_get_regexp),\n+\tOPT_SUBCOMMAND(\"get-urlmatch\", &subcommand, cmd_config_get_urlmatch),\n+\tOPT_SUBCOMMAND(\"add\", &subcommand, cmd_config_add),\n+\tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n+\tOPT_SUBCOMMAND(\"set-all\", &subcommand, cmd_config_set_all),\n+\tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n+\tOPT_SUBCOMMAND(\"unset-all\", &subcommand, cmd_config_unset_all),\n+\tOPT_SUBCOMMAND(\"replace-all\", &subcommand, cmd_config_replace_all),\n+\tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n+\tOPT_SUBCOMMAND(\"remove-section\", &subcommand, cmd_config_remove_section),\n+\tOPT_SUBCOMMAND(\"edit\", &subcommand, cmd_config_edit),\n+\tOPT_END(),\n+};\n+\n static struct option builtin_config_options[] = {\n \tOPT_GROUP(N_(\"Config file location\")),\n \tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n@@ -910,6 +930,20 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n \n+\t/*\n+\t * This is somewhat hacky: we first parse the command line while\n+\t * keeping all args intact in order to determine whether a subcommand\n+\t * has been specified. If so, we re-parse it a second time, but this\n+\t * time we drop KEEP_ARGV0. This is so that we don't munge the command\n+\t * line in case no subcommand was given, which would otherwise confuse\n+\t * us when parsing the implicit modes.\n+\t */\n+\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\tif (subcommand)\n+\t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n+\t\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\n \targc = parse_options(argc, argv, prefix, builtin_config_options,\n \t\t\t     builtin_config_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n@@ -993,18 +1027,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tkey_delim = '\\n';\n \t}\n \n-\tif (action == ACTION_NONE) {\n+\tif (action != ACTION_NONE && subcommand) {\n+\t\terror(_(\"subcommand and action modes are incompatible\"));\n+\t\tusage_builtin_config();\n+\t} else if (action == ACTION_NONE && !subcommand) {\n \t\tswitch (argc) {\n-\t\tcase 1: action = ACTION_GET; break;\n-\t\tcase 2: action = ACTION_SET; break;\n-\t\tcase 3: action = ACTION_SET_ALL; break;\n+\t\tcase 1: subcommand = cmd_config_get; break;\n+\t\tcase 2: subcommand = cmd_config_set; break;\n+\t\tcase 3: subcommand = cmd_config_set_all; break;\n \t\tdefault:\n \t\t\tusage_builtin_config();\n \t\t}\n+\t} else if (action != ACTION_NONE) {\n+\t\tif (action < ACTION_NONE || action >= ARRAY_SIZE(subcommands_by_action))\n+\t\t\tBUG(\"invalid action %d\", action);\n+\t\tsubcommand = subcommands_by_action[action];\n \t}\n-\tif (action <= ACTION_NONE || action >= ARRAY_SIZE(subcommands_by_action))\n-\t\tBUG(\"invalid action %d\", action);\n-\tsubcommand = subcommands_by_action[action];\n \n \tif (type && (subcommand == cmd_config_get_color ||\n \t\t     subcommand == cmd_config_get_colorbool)) {\n-- \n2.44.0\n\n"},{"id":"490075","messageId":"4ad65cb8d6e325d8a14f8d81d0904f7819c76155.1709724089.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH 7/8] t1300: exercise both old- and new-style modes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-06T11:31:59Z","receivedAt":"2024-03-06T11:32:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert t1300 so that tests exercise both the old and new-style way to\nspecify the mode.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t1300-config.sh | 721 ++++++++++++++++++++++++----------------------\n 1 file changed, 375 insertions(+), 346 deletions(-)\n\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 2d1bc1e27e..705c17a1fb 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -11,6 +11,24 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n+for style in old new\n+do\n+\n+case \"$style\" in\n+old)\n+\tmode_prefix=\"--\"\n+\tmode_get=\"\"\n+\tmode_set=\"\"\n+\tmode_set_all=\"\"\n+\t;;\n+new)\n+\tmode_prefix=\"\"\n+\tmode_get=\"get\"\n+\tmode_set=\"set\"\n+\tmode_set_all=\"set-all\"\n+\t;;\n+esac\n+\n test_expect_success 'clear default config' '\n \trm -f .git/config\n '\n@@ -20,7 +38,7 @@ cat > expect << EOF\n \tpenguin = little blue\n EOF\n test_expect_success 'initial' '\n-\tgit config section.penguin \"little blue\" &&\n+\tgit config ${mode_set} section.penguin \"little blue\" &&\n \ttest_cmp expect .git/config\n '\n \n@@ -30,7 +48,7 @@ cat > expect << EOF\n \tMovie = BadPhysics\n EOF\n test_expect_success 'mixed case' '\n-\tgit config Section.Movie BadPhysics &&\n+\tgit config ${mode_set} Section.Movie BadPhysics &&\n \ttest_cmp expect .git/config\n '\n \n@@ -42,7 +60,7 @@ cat > expect << EOF\n \tWhatEver = Second\n EOF\n test_expect_success 'similar section' '\n-\tgit config Sections.WhatEver Second &&\n+\tgit config ${mode_set} Sections.WhatEver Second &&\n \ttest_cmp expect .git/config\n '\n \n@@ -55,16 +73,16 @@ cat > expect << EOF\n \tWhatEver = Second\n EOF\n test_expect_success 'uppercase section' '\n-\tgit config SECTION.UPPERCASE true &&\n+\tgit config ${mode_set} SECTION.UPPERCASE true &&\n \ttest_cmp expect .git/config\n '\n \n test_expect_success 'replace with non-match' '\n-\tgit config section.penguin kingpin !blue\n+\tgit config ${mode_set_all} section.penguin kingpin !blue\n '\n \n test_expect_success 'replace with non-match (actually matching)' '\n-\tgit config section.penguin \"very blue\" !kingpin\n+\tgit config ${mode_set_all} section.penguin \"very blue\" !kingpin\n '\n \n cat > expect << EOF\n@@ -102,7 +120,7 @@ test_missing_key () {\n \tlocal key=\"$1\" &&\n \tlocal title=\"$2\" &&\n \ttest_expect_success \"value for $title is not printed\" '\n-\t\ttest_must_fail git config \"$key\" >out 2>err &&\n+\t\ttest_must_fail git config ${mode_get} \"$key\" >out 2>err &&\n \t\ttest_must_be_empty out &&\n \t\ttest_must_be_empty err\n \t'\n@@ -125,7 +143,7 @@ foo = bar\n EOF\n \n test_expect_success 'unset with cont. lines' '\n-\tgit config --unset beta.baz\n+\tgit config ${mode_prefix}unset beta.baz\n '\n \n cat > expect <<\\EOF\n@@ -152,7 +170,7 @@ EOF\n cp .git/config .git/config2\n \n test_expect_success 'multiple unset' '\n-\tgit config --unset-all beta.haha\n+\tgit config ${mode_prefix}unset-all beta.haha\n '\n \n cat > expect << EOF\n@@ -178,7 +196,7 @@ test_expect_success '--replace-all missing value' '\n rm .git/config2\n \n test_expect_success '--replace-all' '\n-\tgit config --replace-all beta.haha gamma\n+\tgit config ${mode_prefix}replace-all beta.haha gamma\n '\n \n cat > expect << EOF\n@@ -220,7 +238,7 @@ noIndent= sillyValue ; 'nother silly comment\n \tnonewline = wow\n EOF\n test_expect_success 'really really mean test' '\n-\tgit config nextsection.nonewline wow &&\n+\tgit config ${mode_set} nextsection.nonewline wow &&\n \ttest_cmp expect .git/config\n '\n \n@@ -238,7 +256,7 @@ noIndent= sillyValue ; 'nother silly comment\n \tnonewline = wow\n EOF\n test_expect_success 'unset' '\n-\tgit config --unset beta.haha &&\n+\tgit config ${mode_prefix}unset beta.haha &&\n \ttest_cmp expect .git/config\n '\n \n@@ -258,15 +276,15 @@ test_expect_success 'multivar' '\n '\n \n test_expect_success 'non-match' '\n-\tgit config --get nextsection.nonewline !for\n+\tgit config ${mode_prefix}get nextsection.nonewline !for\n '\n \n test_expect_success 'non-match value' '\n-\ttest_cmp_config wow --get nextsection.nonewline !for\n+\ttest_cmp_config wow ${mode_prefix}get nextsection.nonewline !for\n '\n \n test_expect_success 'multi-valued get returns final one' '\n-\ttest_cmp_config \"wow2 for me\" --get nextsection.nonewline\n+\ttest_cmp_config \"wow2 for me\" ${mode_prefix}get nextsection.nonewline\n '\n \n test_expect_success 'multi-valued get-all returns all' '\n@@ -274,7 +292,7 @@ test_expect_success 'multi-valued get-all returns all' '\n \twow\n \twow2 for me\n \tEOF\n-\tgit config --get-all nextsection.nonewline >actual &&\n+\tgit config ${mode_prefix}get-all nextsection.nonewline >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -289,16 +307,16 @@ noIndent= sillyValue ; 'nother silly comment\n \tNoNewLine = wow2 for me\n EOF\n test_expect_success 'multivar replace' '\n-\tgit config nextsection.nonewline \"wow3\" \"wow$\" &&\n+\tgit config ${mode_set_all} nextsection.nonewline \"wow3\" \"wow$\" &&\n \ttest_cmp expect .git/config\n '\n \n test_expect_success 'ambiguous unset' '\n-\ttest_must_fail git config --unset nextsection.nonewline\n+\ttest_must_fail git config ${mode_prefix}unset nextsection.nonewline\n '\n \n test_expect_success 'invalid unset' '\n-\ttest_must_fail git config --unset somesection.nonewline\n+\ttest_must_fail git config ${mode_prefix}unset somesection.nonewline\n '\n \n cat > expect << EOF\n@@ -312,16 +330,16 @@ noIndent= sillyValue ; 'nother silly comment\n EOF\n \n test_expect_success 'multivar unset' '\n-\tgit config --unset nextsection.nonewline \"wow3$\" &&\n+\tgit config ${mode_prefix}unset nextsection.nonewline \"wow3$\" &&\n \ttest_cmp expect .git/config\n '\n \n-test_expect_success 'invalid key' 'test_must_fail git config inval.2key blabla'\n+test_expect_success 'invalid key' 'test_must_fail git config ${mode_set} inval.2key blabla'\n \n-test_expect_success 'correct key' 'git config 123456.a123 987'\n+test_expect_success 'correct key' 'git config ${mode_set} 123456.a123 987'\n \n test_expect_success 'hierarchical section' '\n-\tgit config Version.1.2.3eX.Alpha beta\n+\tgit config ${mode_set} Version.1.2.3eX.Alpha beta\n '\n \n cat > expect << EOF\n@@ -350,11 +368,11 @@ version.1.2.3eX.alpha=beta\n EOF\n \n test_expect_success 'working --list' '\n-\tgit config --list > output &&\n+\tgit config ${mode_prefix}list > output &&\n \ttest_cmp expect output\n '\n test_expect_success '--list without repo produces empty output' '\n-\tgit --git-dir=nonexistent config --list >output &&\n+\tgit --git-dir=nonexistent config ${mode_prefix}list >output &&\n \ttest_must_be_empty output\n '\n \n@@ -366,7 +384,7 @@ version.1.2.3eX.alpha\n EOF\n \n test_expect_success '--name-only --list' '\n-\tgit config --name-only --list >output &&\n+\tgit config ${mode_prefix}list --name-only >output &&\n \ttest_cmp expect output\n '\n \n@@ -376,7 +394,7 @@ nextsection.nonewline wow2 for me\n EOF\n \n test_expect_success '--get-regexp' '\n-\tgit config --get-regexp in >output &&\n+\tgit config ${mode_prefix}get-regexp in >output &&\n \ttest_cmp expect output\n '\n \n@@ -386,7 +404,7 @@ nextsection.nonewline\n EOF\n \n test_expect_success '--name-only --get-regexp' '\n-\tgit config --name-only --get-regexp in >output &&\n+\tgit config ${mode_prefix}get-regexp --name-only in >output &&\n \ttest_cmp expect output\n '\n \n@@ -396,8 +414,8 @@ wow4 for you\n EOF\n \n test_expect_success '--add' '\n-\tgit config --add nextsection.nonewline \"wow4 for you\" &&\n-\tgit config --get-all nextsection.nonewline > output &&\n+\tgit config ${mode_prefix}add nextsection.nonewline \"wow4 for you\" &&\n+\tgit config ${mode_prefix}get-all nextsection.nonewline > output &&\n \ttest_cmp expect output\n '\n \n@@ -409,45 +427,45 @@ cat > .git/config << EOF\n EOF\n \n test_expect_success 'get variable with no value' '\n-\tgit config --get novalue.variable ^$\n+\tgit config ${mode_prefix}get novalue.variable ^$\n '\n \n test_expect_success 'get variable with empty value' '\n-\tgit config --get emptyvalue.variable ^$\n+\tgit config ${mode_prefix}get emptyvalue.variable ^$\n '\n \n echo novalue.variable > expect\n \n test_expect_success 'get-regexp variable with no value' '\n-\tgit config --get-regexp novalue > output &&\n+\tgit config ${mode_prefix}get-regexp novalue > output &&\n \ttest_cmp expect output\n '\n \n echo 'novalue.variable true' > expect\n \n test_expect_success 'get-regexp --bool variable with no value' '\n-\tgit config --bool --get-regexp novalue > output &&\n+\tgit config ${mode_prefix}get-regexp --bool novalue > output &&\n \ttest_cmp expect output\n '\n \n echo 'emptyvalue.variable ' > expect\n \n test_expect_success 'get-regexp variable with empty value' '\n-\tgit config --get-regexp emptyvalue > output &&\n+\tgit config ${mode_prefix}get-regexp emptyvalue > output &&\n \ttest_cmp expect output\n '\n \n echo true > expect\n \n test_expect_success 'get bool variable with no value' '\n-\tgit config --bool novalue.variable > output &&\n+\tgit config ${mode_get} --bool novalue.variable > output &&\n \ttest_cmp expect output\n '\n \n echo false > expect\n \n test_expect_success 'get bool variable with empty value' '\n-\tgit config --bool emptyvalue.variable > output &&\n+\tgit config ${mode_get} --bool emptyvalue.variable > output &&\n \ttest_cmp expect output\n '\n \n@@ -469,7 +487,7 @@ cat > expect << EOF\n EOF\n \n test_expect_success 'new section is partial match of another' '\n-\tgit config a.x y &&\n+\tgit config ${mode_set} a.x y &&\n \ttest_cmp expect .git/config\n '\n \n@@ -484,14 +502,14 @@ cat > expect << EOF\n EOF\n \n test_expect_success 'new variable inserts into proper section' '\n-\tgit config b.x y &&\n-\tgit config a.b c &&\n+\tgit config ${mode_set} b.x y &&\n+\tgit config ${mode_set} a.b c &&\n \ttest_cmp expect .git/config\n '\n \n test_expect_success 'alternative --file (non-existing file should fail)' '\n-\ttest_must_fail git config --file non-existing-config -l &&\n-\ttest_must_fail git config --file non-existing-config test.xyzzy\n+\ttest_must_fail git config ${mode_prefix}list --file non-existing-config &&\n+\ttest_must_fail git config ${mode_get} --file non-existing-config test.xyzzy\n '\n \n cat > other-config << EOF\n@@ -504,29 +522,30 @@ ein.bahn=strasse\n EOF\n \n test_expect_success 'alternative GIT_CONFIG' '\n-\tGIT_CONFIG=other-config git config --list >output &&\n+\tGIT_CONFIG=other-config git config ${mode_prefix}list >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'alternative GIT_CONFIG (--file)' '\n-\tgit config --file other-config --list >output &&\n+\tgit config ${mode_prefix}list --file other-config >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'alternative GIT_CONFIG (--file=-)' '\n-\tgit config --file - --list <other-config >output &&\n+\tgit config ${mode_prefix}list --file - <other-config >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'setting a value in stdin is an error' '\n-\ttest_must_fail git config --file - some.value foo\n+\ttest_must_fail git config ${mode_set} --file - some.value foo\n '\n \n test_expect_success 'editing stdin is an error' '\n-\ttest_must_fail git config --file - --edit\n+\ttest_must_fail git config ${mode_prefix}edit --file -\n '\n \n test_expect_success 'refer config from subdirectory' '\n+\ttest_when_finished \"rm -r x\" &&\n \tmkdir x &&\n \ttest_cmp_config -C x strasse --file=../other-config --get ein.bahn\n '\n@@ -539,7 +558,7 @@ cat > expect << EOF\n EOF\n \n test_expect_success '--set in alternative file' '\n-\tgit config --file=other-config anwohner.park ausweis &&\n+\tgit config ${mode_set} --file=other-config anwohner.park ausweis &&\n \ttest_cmp expect other-config\n '\n \n@@ -555,7 +574,7 @@ weird\n EOF\n \n test_expect_success 'rename section' '\n-\tgit config --rename-section branch.eins branch.zwei\n+\tgit config ${mode_prefix}rename-section branch.eins branch.zwei\n '\n \n cat > expect << EOF\n@@ -574,7 +593,7 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'rename non-existing section' '\n-\ttest_must_fail git config --rename-section \\\n+\ttest_must_fail git config ${mode_prefix}rename-section \\\n \t\tbranch.\"world domination\" branch.drei\n '\n \n@@ -583,7 +602,7 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'rename another section' '\n-\tgit config --rename-section branch.\"1 234 blabl/a\" branch.drei\n+\tgit config ${mode_prefix}rename-section branch.\"1 234 blabl/a\" branch.drei\n '\n \n cat > expect << EOF\n@@ -606,7 +625,7 @@ cat >> .git/config << EOF\n EOF\n \n test_expect_success 'rename a section with a var on the same line' '\n-\tgit config --rename-section branch.vier branch.zwei\n+\tgit config ${mode_prefix}rename-section branch.vier branch.zwei\n '\n \n cat > expect << EOF\n@@ -627,11 +646,11 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'renaming empty section name is rejected' '\n-\ttest_must_fail git config --rename-section branch.zwei \"\"\n+\ttest_must_fail git config ${mode_prefix}rename-section branch.zwei \"\"\n '\n \n test_expect_success 'renaming to bogus section is rejected' '\n-\ttest_must_fail git config --rename-section branch.zwei \"bogus name\"\n+\ttest_must_fail git config ${mode_prefix}rename-section branch.zwei \"bogus name\"\n '\n \n test_expect_success 'renaming a section with a long line' '\n@@ -640,7 +659,7 @@ test_expect_success 'renaming a section with a long line' '\n \t\tprintf \"  c = d %1024s [a] e = f\\\\n\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\tgit config -f y --rename-section a xyz &&\n+\tgit config ${mode_prefix}rename-section -f y a xyz &&\n \ttest_must_fail git config -f y b.e\n '\n \n@@ -650,7 +669,7 @@ test_expect_success 'renaming an embedded section with a long line' '\n \t\tprintf \"  c = d %1024s [a] [foo] e = f\\\\n\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\tgit config -f y --rename-section a xyz &&\n+\tgit config ${mode_prefix}rename-section -f y a xyz &&\n \ttest_must_fail git config -f y foo.e\n '\n \n@@ -660,7 +679,7 @@ test_expect_success 'renaming a section with an overly-long line' '\n \t\tprintf \"  c = d %525000s e\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\ttest_must_fail git config -f y --rename-section a xyz 2>err &&\n+\ttest_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&\n \tgrep \"refusing to work with overly long line in .y. on line 2\" err\n '\n \n@@ -669,7 +688,7 @@ cat >> .git/config << EOF\n EOF\n \n test_expect_success 'remove section' '\n-\tgit config --remove-section branch.zwei\n+\tgit config ${mode_prefix}remove-section branch.zwei\n '\n \n cat > expect << EOF\n@@ -693,20 +712,20 @@ EOF\n \n test_expect_success 'section ending' '\n \trm -f .git/config &&\n-\tgit config gitcvs.enabled true &&\n-\tgit config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&\n-\tgit config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&\n+\tgit config ${mode_set} gitcvs.enabled true &&\n+\tgit config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&\n+\tgit config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&\n \ttest_cmp expect .git/config\n \n '\n \n test_expect_success numbers '\n-\tgit config kilo.gram 1k &&\n-\tgit config mega.ton 1m &&\n+\tgit config ${mode_set} kilo.gram 1k &&\n+\tgit config ${mode_set} mega.ton 1m &&\n \techo 1024 >expect &&\n \techo 1048576 >>expect &&\n-\tgit config --int --get kilo.gram >actual &&\n-\tgit config --int --get mega.ton >>actual &&\n+\tgit config ${mode_prefix}get --int kilo.gram >actual &&\n+\tgit config ${mode_prefix}get --int mega.ton >>actual &&\n \ttest_cmp expect actual\n '\n \n@@ -719,25 +738,25 @@ test_expect_success '--int is at least 64 bits' '\n test_expect_success 'invalid unit' '\n \tgit config aninvalid.unit \"1auto\" &&\n \ttest_cmp_config 1auto aninvalid.unit &&\n-\ttest_must_fail git config --int --get aninvalid.unit 2>actual &&\n+\ttest_must_fail git config ${mode_prefix}get --int aninvalid.unit 2>actual &&\n \ttest_grep \"bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit\" actual\n '\n \n test_expect_success 'invalid unit boolean' '\n \tgit config commit.gpgsign \"1true\" &&\n \ttest_cmp_config 1true commit.gpgsign &&\n-\ttest_must_fail git config --bool --get commit.gpgsign 2>actual &&\n+\ttest_must_fail git config ${mode_prefix}get --bool commit.gpgsign 2>actual &&\n \ttest_grep \"bad boolean config value .1true. for .commit.gpgsign.\" actual\n '\n \n test_expect_success 'line number is reported correctly' '\n \tprintf \"[bool]\\n\\tvar\\n\" >invalid &&\n-\ttest_must_fail git config -f invalid --path bool.var 2>actual &&\n+\ttest_must_fail git config ${mode_get} -f invalid --path bool.var 2>actual &&\n \ttest_grep \"line 2\" actual\n '\n \n test_expect_success 'invalid stdin config' '\n-\techo \"[broken\" | test_must_fail git config --list --file - >output 2>&1 &&\n+\techo \"[broken\" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&\n \ttest_grep \"bad config line 1 in standard input\" output\n '\n \n@@ -765,19 +784,19 @@ test_expect_success bool '\n \trm -f result &&\n \tfor i in 1 2 3 4\n \tdo\n-\t    git config --bool --get bool.true$i >>result &&\n-\t    git config --bool --get bool.false$i >>result || return 1\n+\t    git config ${mode_prefix}get --bool bool.true$i >>result &&\n+\t    git config ${mode_prefix}get --bool bool.false$i >>result || return 1\n \tdone &&\n \ttest_cmp expect result'\n \n test_expect_success 'invalid bool (--get)' '\n \n \tgit config bool.nobool foobar &&\n-\ttest_must_fail git config --bool --get bool.nobool'\n+\ttest_must_fail git config ${mode_prefix}get --bool bool.nobool'\n \n test_expect_success 'invalid bool (set)' '\n \n-\ttest_must_fail git config --bool bool.nobool foobar'\n+\ttest_must_fail git config ${mode_set} --bool bool.nobool foobar'\n \n cat > expect <<\\EOF\n [bool]\n@@ -794,14 +813,14 @@ EOF\n test_expect_success 'set --bool' '\n \n \trm -f .git/config &&\n-\tgit config --bool bool.true1 01 &&\n-\tgit config --bool bool.true2 -1 &&\n-\tgit config --bool bool.true3 YeS &&\n-\tgit config --bool bool.true4 true &&\n-\tgit config --bool bool.false1 000 &&\n-\tgit config --bool bool.false2 \"\" &&\n-\tgit config --bool bool.false3 nO &&\n-\tgit config --bool bool.false4 FALSE &&\n+\tgit config ${mode_set} --bool bool.true1 01 &&\n+\tgit config ${mode_set} --bool bool.true2 -1 &&\n+\tgit config ${mode_set} --bool bool.true3 YeS &&\n+\tgit config ${mode_set} --bool bool.true4 true &&\n+\tgit config ${mode_set} --bool bool.false1 000 &&\n+\tgit config ${mode_set} --bool bool.false2 \"\" &&\n+\tgit config ${mode_set} --bool bool.false3 nO &&\n+\tgit config ${mode_set} --bool bool.false4 FALSE &&\n \ttest_cmp expect .git/config'\n \n cat > expect <<\\EOF\n@@ -814,9 +833,9 @@ EOF\n test_expect_success 'set --int' '\n \n \trm -f .git/config &&\n-\tgit config --int int.val1 01 &&\n-\tgit config --int int.val2 -1 &&\n-\tgit config --int int.val3 5m &&\n+\tgit config ${mode_set} --int int.val1 01 &&\n+\tgit config ${mode_set} --int int.val2 -1 &&\n+\tgit config ${mode_set} --int int.val3 5m &&\n \ttest_cmp expect .git/config\n '\n \n@@ -840,12 +859,12 @@ test_expect_success 'get --bool-or-int' '\n \t-1\n \tEOF\n \t{\n-\t\tgit config --bool-or-int bool.true1 &&\n-\t\tgit config --bool-or-int bool.true2 &&\n-\t\tgit config --bool-or-int bool.false &&\n-\t\tgit config --bool-or-int int.int1 &&\n-\t\tgit config --bool-or-int int.int2 &&\n-\t\tgit config --bool-or-int int.int3\n+\t\tgit config ${mode_get} --bool-or-int bool.true1 &&\n+\t\tgit config ${mode_get} --bool-or-int bool.true2 &&\n+\t\tgit config ${mode_get} --bool-or-int bool.false &&\n+\t\tgit config ${mode_get} --bool-or-int int.int1 &&\n+\t\tgit config ${mode_get} --bool-or-int int.int2 &&\n+\t\tgit config ${mode_get} --bool-or-int int.int3\n \t} >actual &&\n \ttest_cmp expect actual\n '\n@@ -864,13 +883,13 @@ EOF\n \n test_expect_success 'set --bool-or-int' '\n \trm -f .git/config &&\n-\tgit config --bool-or-int bool.true1 true &&\n-\tgit config --bool-or-int bool.false1 false &&\n-\tgit config --bool-or-int bool.true2 yes &&\n-\tgit config --bool-or-int bool.false2 no &&\n-\tgit config --bool-or-int int.int1 0 &&\n-\tgit config --bool-or-int int.int2 1 &&\n-\tgit config --bool-or-int int.int3 -1 &&\n+\tgit config ${mode_set} --bool-or-int bool.true1 true &&\n+\tgit config ${mode_set} --bool-or-int bool.false1 false &&\n+\tgit config ${mode_set} --bool-or-int bool.true2 yes &&\n+\tgit config ${mode_set} --bool-or-int bool.false2 no &&\n+\tgit config ${mode_set} --bool-or-int int.int1 0 &&\n+\tgit config ${mode_set} --bool-or-int int.int2 1 &&\n+\tgit config ${mode_set} --bool-or-int int.int3 -1 &&\n \ttest_cmp expect .git/config\n '\n \n@@ -883,9 +902,9 @@ EOF\n \n test_expect_success !MINGW 'set --path' '\n \trm -f .git/config &&\n-\tgit config --path path.home \"~/\" &&\n-\tgit config --path path.normal \"/dev/null\" &&\n-\tgit config --path path.trailingtilde \"foo~\" &&\n+\tgit config ${mode_set} --path path.home \"~/\" &&\n+\tgit config ${mode_set} --path path.normal \"/dev/null\" &&\n+\tgit config ${mode_set} --path path.trailingtilde \"foo~\" &&\n \ttest_cmp expect .git/config'\n \n if test_have_prereq !MINGW && test \"${HOME+set}\"\n@@ -900,9 +919,9 @@ foo~\n EOF\n \n test_expect_success HOMEVAR 'get --path' '\n-\tgit config --get --path path.home > result &&\n-\tgit config --get --path path.normal >> result &&\n-\tgit config --get --path path.trailingtilde >> result &&\n+\tgit config ${mode_prefix}get --path path.home > result &&\n+\tgit config ${mode_prefix}get --path path.normal >> result &&\n+\tgit config ${mode_prefix}get --path path.trailingtilde >> result &&\n \ttest_cmp expect result\n '\n \n@@ -914,10 +933,10 @@ EOF\n test_expect_success !MINGW 'get --path copes with unset $HOME' '\n \t(\n \t\tsane_unset HOME &&\n-\t\ttest_must_fail git config --get --path path.home \\\n+\t\ttest_must_fail git config ${mode_prefix}get --path path.home \\\n \t\t\t>result 2>msg &&\n-\t\tgit config --get --path path.normal >>result &&\n-\t\tgit config --get --path path.trailingtilde >>result\n+\t\tgit config ${mode_prefix}get --path path.normal >>result &&\n+\t\tgit config ${mode_prefix}get --path path.trailingtilde >>result\n \t) &&\n \ttest_grep \"[Ff]ailed to expand.*~/\" msg &&\n \ttest_cmp expect result\n@@ -925,7 +944,7 @@ test_expect_success !MINGW 'get --path copes with unset $HOME' '\n \n test_expect_success 'get --path barfs on boolean variable' '\n \techo \"[path]bool\" >.git/config &&\n-\ttest_must_fail git config --get --path path.bool\n+\ttest_must_fail git config ${mode_prefix}get --path path.bool\n '\n \n test_expect_success 'get --expiry-date' '\n@@ -949,20 +968,20 @@ test_expect_success 'get --expiry-date' '\n \tEOF\n \t: \"work around heredoc parsing bug fixed in dash 0.5.7 (in ec2c84d)\" &&\n \t{\n-\t\techo \"$rel_out $(git config --expiry-date date.valid1)\" &&\n-\t\tgit config --expiry-date date.valid2 &&\n-\t\tgit config --expiry-date date.valid3 &&\n-\t\tgit config --expiry-date date.valid4 &&\n-\t\tgit config --expiry-date date.valid5\n+\t\techo \"$rel_out $(git config ${mode_get} --expiry-date date.valid1)\" &&\n+\t\tgit config ${mode_get} --expiry-date date.valid2 &&\n+\t\tgit config ${mode_get} --expiry-date date.valid3 &&\n+\t\tgit config ${mode_get} --expiry-date date.valid4 &&\n+\t\tgit config ${mode_get} --expiry-date date.valid5\n \t} >actual &&\n \ttest_cmp expect actual &&\n-\ttest_must_fail git config --expiry-date date.invalid1\n+\ttest_must_fail git config ${mode_get} --expiry-date date.invalid1\n '\n \n test_expect_success 'get --type=color' '\n \trm .git/config &&\n \tgit config foo.color \"red\" &&\n-\tgit config --get --type=color foo.color >actual.raw &&\n+\tgit config ${mode_prefix}get --type=color foo.color >actual.raw &&\n \ttest_decode_color <actual.raw >actual &&\n \techo \"<RED>\" >expect &&\n \ttest_cmp expect actual\n@@ -975,17 +994,17 @@ EOF\n \n test_expect_success 'set --type=color' '\n \trm .git/config &&\n-\tgit config --type=color foo.color \"red\" &&\n+\tgit config ${mode_set} --type=color foo.color \"red\" &&\n \ttest_cmp expect .git/config\n '\n \n test_expect_success 'get --type=color barfs on non-color' '\n \techo \"[foo]bar=not-a-color\" >.git/config &&\n-\ttest_must_fail git config --get --type=color foo.bar\n+\ttest_must_fail git config ${mode_prefix}get --type=color foo.bar\n '\n \n test_expect_success 'set --type=color barfs on non-color' '\n-\ttest_must_fail git config --type=color foo.color \"not-a-color\" 2>error &&\n+\ttest_must_fail git config ${mode_set} --type=color foo.color \"not-a-color\" 2>error &&\n \ttest_grep \"cannot parse color\" error\n '\n \n@@ -998,18 +1017,18 @@ cat > expect << EOF\n EOF\n test_expect_success 'quoting' '\n \trm -f .git/config &&\n-\tgit config quote.leading \" test\" &&\n-\tgit config quote.ending \"test \" &&\n-\tgit config quote.semicolon \"test;test\" &&\n-\tgit config quote.hash \"test#test\" &&\n+\tgit config ${mode_set} quote.leading \" test\" &&\n+\tgit config ${mode_set} quote.ending \"test \" &&\n+\tgit config ${mode_set} quote.semicolon \"test;test\" &&\n+\tgit config ${mode_set} quote.hash \"test#test\" &&\n \ttest_cmp expect .git/config\n '\n \n test_expect_success 'key with newline' '\n-\ttest_must_fail git config \"key.with\n+\ttest_must_fail git config ${mode_set} \"key.with\n newline\" 123'\n \n-test_expect_success 'value with newline' 'git config key.sub value.with\\\\\\\n+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\\\\\n newline'\n \n cat > .git/config <<\\EOF\n@@ -1029,7 +1048,7 @@ section.quotecont=cont;inued\n EOF\n \n test_expect_success 'value continued on next line' '\n-\tgit config --list > result &&\n+\tgit config ${mode_prefix}list > result &&\n \ttest_cmp expect result\n '\n \n@@ -1053,14 +1072,14 @@ Qsection.sub=section.val4\n Qsection.sub=section.val5Q\n EOF\n test_expect_success '--null --list' '\n-\tgit config --null --list >result.raw &&\n+\tgit config ${mode_prefix}list --null >result.raw &&\n \tnul_to_q <result.raw >result &&\n \techo >>result &&\n \ttest_cmp expect result\n '\n \n test_expect_success '--null --get-regexp' '\n-\tgit config --null --get-regexp \"val[0-9]\" >result.raw &&\n+\tgit config ${mode_prefix}get-regexp --null \"val[0-9]\" >result.raw &&\n \tnul_to_q <result.raw >result &&\n \techo >>result &&\n \ttest_cmp expect result\n@@ -1072,12 +1091,13 @@ test_expect_success 'inner whitespace kept verbatim' '\n '\n \n test_expect_success SYMLINKS 'symlinked configuration' '\n+\ttest_when_finished \"rm myconfig\" &&\n \tln -s notyet myconfig &&\n-\tgit config --file=myconfig test.frotz nitfol &&\n+\tgit config ${mode_set} --file=myconfig test.frotz nitfol &&\n \ttest -h myconfig &&\n \ttest -f notyet &&\n-\ttest \"z$(git config --file=notyet test.frotz)\" = znitfol &&\n-\tgit config --file=myconfig test.xyzzy rezrov &&\n+\ttest \"z$(git config ${mode_get} --file=notyet test.frotz)\" = znitfol &&\n+\tgit config ${mode_set} --file=myconfig test.xyzzy rezrov &&\n \ttest -h myconfig &&\n \ttest -f notyet &&\n \tcat >expect <<-\\EOF &&\n@@ -1085,17 +1105,18 @@ test_expect_success SYMLINKS 'symlinked configuration' '\n \trezrov\n \tEOF\n \t{\n-\t\tgit config --file=notyet test.frotz &&\n-\t\tgit config --file=notyet test.xyzzy\n+\t\tgit config ${mode_get} --file=notyet test.frotz &&\n+\t\tgit config ${mode_get} --file=notyet test.xyzzy\n \t} >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success SYMLINKS 'symlink to nonexistent configuration' '\n+\ttest_when_finished \"rm linktonada linktolinktonada\" &&\n \tln -s doesnotexist linktonada &&\n \tln -s linktonada linktolinktonada &&\n-\ttest_must_fail git config --file=linktonada --list &&\n-\ttest_must_fail git config --file=linktolinktonada --list\n+\ttest_must_fail git config ${mode_prefix}list --file=linktonada &&\n+\ttest_must_fail git config ${mode_prefix}list --file=linktolinktonada\n '\n \n test_expect_success 'check split_cmdline return' '\n@@ -1103,12 +1124,12 @@ test_expect_success 'check split_cmdline return' '\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tgit config alias.split-cmdline-fix \"echo \\\"\" &&\n+\t\tgit config ${mode_set} alias.split-cmdline-fix \"echo \\\"\" &&\n \t\ttest_must_fail git split-cmdline-fix &&\n \t\techo foo >foo &&\n \t\tgit add foo &&\n \t\tgit commit -m \"initial commit\" &&\n-\t\tgit config branch.main.mergeoptions \"echo \\\"\" &&\n+\t\tgit config ${mode_set} branch.main.mergeoptions \"echo \\\"\" &&\n \t\ttest_must_fail git merge main\n \t)\n '\n@@ -1122,10 +1143,10 @@ test_expect_success 'git -c \"key=value\" support' '\n \t{\n \t\tgit -c section.name=value config section.name &&\n \t\tgit -c foo.CamelCase=value config foo.camelcase &&\n-\t\tgit -c foo.flag config --bool foo.flag\n+\t\tgit -c foo.flag config ${mode_get} --bool foo.flag\n \t} >actual &&\n \ttest_cmp expect actual &&\n-\ttest_must_fail git -c name=value config section.name\n+\ttest_must_fail git -c name=value config ${mode_get} section.name\n '\n \n # We just need a type-specifier here that cares about the\n@@ -1135,27 +1156,27 @@ test_expect_success 'git -c \"key=value\" support' '\n # its semantics.\n test_expect_success 'git -c can represent empty string' '\n \techo >expect &&\n-\tgit -c foo.empty= config --path foo.empty >actual &&\n+\tgit -c foo.empty= config ${mode_get} --path foo.empty >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'key sanity-checking' '\n-\ttest_must_fail git config foo=bar &&\n-\ttest_must_fail git config foo=.bar &&\n-\ttest_must_fail git config foo.ba=r &&\n-\ttest_must_fail git config foo.1bar &&\n-\ttest_must_fail git config foo.\"ba\n+\ttest_must_fail git config ${mode_get} foo=bar &&\n+\ttest_must_fail git config ${mode_get} foo=.bar &&\n+\ttest_must_fail git config ${mode_get} foo.ba=r &&\n+\ttest_must_fail git config ${mode_get} foo.1bar &&\n+\ttest_must_fail git config ${mode_get} foo.\"ba\n \t\t\t\tz\".bar &&\n-\ttest_must_fail git config . false &&\n-\ttest_must_fail git config .foo false &&\n-\ttest_must_fail git config foo. false &&\n-\ttest_must_fail git config .foo. false &&\n-\tgit config foo.bar true &&\n-\tgit config foo.\"ba =z\".bar false\n+\ttest_must_fail git config ${mode_set} . false &&\n+\ttest_must_fail git config ${mode_set} .foo false &&\n+\ttest_must_fail git config ${mode_set} foo. false &&\n+\ttest_must_fail git config ${mode_set} .foo. false &&\n+\tgit config ${mode_set} foo.bar true &&\n+\tgit config ${mode_set} foo.\"ba =z\".bar false\n '\n \n test_expect_success 'git -c works with aliases of builtins' '\n-\tgit config alias.checkconfig \"-c foo.check=bar config foo.check\" &&\n+\tgit config ${mode_set} alias.checkconfig \"-c foo.check=bar config foo.check\" &&\n \techo bar >expect &&\n \tgit checkconfig >actual &&\n \ttest_cmp expect actual\n@@ -1167,7 +1188,7 @@ test_expect_success 'aliases can be CamelCased' '\n \t(\n \t\tcd repo &&\n \t\ttest_commit A &&\n-\t\tgit config alias.CamelCased \"rev-parse HEAD\" &&\n+\t\tgit config ${mode_set} alias.CamelCased \"rev-parse HEAD\" &&\n \t\tgit CamelCased >out &&\n \t\tgit rev-parse HEAD >expect &&\n \t\ttest_cmp expect out\n@@ -1176,7 +1197,7 @@ test_expect_success 'aliases can be CamelCased' '\n \n test_expect_success 'git -c does not split values on equals' '\n \techo \"value with = in it\" >expect &&\n-\tgit -c section.foo=\"value with = in it\" config section.foo >actual &&\n+\tgit -c section.foo=\"value with = in it\" config ${mode_get} section.foo >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1193,7 +1214,7 @@ test_expect_success 'git -c complains about empty key and value' '\n '\n \n test_expect_success 'multiple git -c appends config' '\n-\ttest_config alias.x \"!git -c x.two=2 config --get-regexp ^x\\.*\" &&\n+\ttest_config alias.x \"!git -c x.two=2 config ${mode_prefix}get-regexp ^x\\.*\" &&\n \tcat >expect <<-\\EOF &&\n \tx.one 1\n \tx.two 2\n@@ -1210,14 +1231,14 @@ test_expect_success 'last one wins: two level vars' '\n \n \techo VAL >expect &&\n \n-\tgit -c sec.var=val -c sec.VAR=VAL config --get sec.var >actual &&\n+\tgit -c sec.var=val -c sec.VAR=VAL config ${mode_prefix}get sec.var >actual &&\n \ttest_cmp expect actual &&\n-\tgit -c SEC.var=val -c sec.var=VAL config --get sec.var >actual &&\n+\tgit -c SEC.var=val -c sec.var=VAL config ${mode_prefix}get sec.var >actual &&\n \ttest_cmp expect actual &&\n \n-\tgit -c sec.var=val -c sec.VAR=VAL config --get SEC.var >actual &&\n+\tgit -c sec.var=val -c sec.VAR=VAL config ${mode_prefix}get SEC.var >actual &&\n \ttest_cmp expect actual &&\n-\tgit -c SEC.var=val -c sec.var=VAL config --get sec.VAR >actual &&\n+\tgit -c SEC.var=val -c sec.var=VAL config ${mode_prefix}get sec.VAR >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1228,9 +1249,9 @@ test_expect_success 'last one wins: three level vars' '\n \t# case sensitive.\n \n \techo val >expect &&\n-\tgit -c v.a.r=val -c v.A.r=VAL config --get v.a.r >actual &&\n+\tgit -c v.a.r=val -c v.A.r=VAL config ${mode_prefix}get v.a.r >actual &&\n \ttest_cmp expect actual &&\n-\tgit -c v.a.r=val -c v.A.r=VAL config --get V.a.R >actual &&\n+\tgit -c v.a.r=val -c v.A.r=VAL config ${mode_prefix}get V.a.R >actual &&\n \ttest_cmp expect actual &&\n \n \t# v.a.r and V.a.R are the same variable, as the first\n@@ -1238,13 +1259,13 @@ test_expect_success 'last one wins: three level vars' '\n \t# case insensitive.\n \n \techo VAL >expect &&\n-\tgit -c v.a.r=val -c v.a.R=VAL config --get v.a.r >actual &&\n+\tgit -c v.a.r=val -c v.a.R=VAL config ${mode_prefix}get v.a.r >actual &&\n \ttest_cmp expect actual &&\n-\tgit -c v.a.r=val -c V.a.r=VAL config --get v.a.r >actual &&\n+\tgit -c v.a.r=val -c V.a.r=VAL config ${mode_prefix}get v.a.r >actual &&\n \ttest_cmp expect actual &&\n-\tgit -c v.a.r=val -c v.a.R=VAL config --get V.a.R >actual &&\n+\tgit -c v.a.r=val -c v.a.R=VAL config ${mode_prefix}get V.a.R >actual &&\n \ttest_cmp expect actual &&\n-\tgit -c v.a.r=val -c V.a.r=VAL config --get V.a.R >actual &&\n+\tgit -c v.a.r=val -c V.a.r=VAL config ${mode_prefix}get V.a.R >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1259,7 +1280,7 @@ test_expect_success 'old-fashioned settings are case insensitive' '\n \t[V.A]\n \tQr = value2\n \tEOF\n-\tgit config -f testConfig_actual \"v.a.r\" value2 &&\n+\tgit config ${mode_set} -f testConfig_actual \"v.a.r\" value2 &&\n \ttest_cmp testConfig_expect testConfig_actual &&\n \n \tcat >testConfig_actual <<-EOF &&\n@@ -1270,7 +1291,7 @@ test_expect_success 'old-fashioned settings are case insensitive' '\n \t[V.A]\n \tQR = value2\n \tEOF\n-\tgit config -f testConfig_actual \"V.a.R\" value2 &&\n+\tgit config ${mode_set} -f testConfig_actual \"V.a.R\" value2 &&\n \ttest_cmp testConfig_expect testConfig_actual &&\n \n \tcat >testConfig_actual <<-EOF &&\n@@ -1282,7 +1303,7 @@ test_expect_success 'old-fashioned settings are case insensitive' '\n \tr = value1\n \tQr = value2\n \tEOF\n-\tgit config -f testConfig_actual \"V.A.r\" value2 &&\n+\tgit config ${mode_set} -f testConfig_actual \"V.A.r\" value2 &&\n \ttest_cmp testConfig_expect testConfig_actual &&\n \n \tcat >testConfig_actual <<-EOF &&\n@@ -1294,7 +1315,7 @@ test_expect_success 'old-fashioned settings are case insensitive' '\n \tr = value1\n \tQr = value2\n \tEOF\n-\tgit config -f testConfig_actual \"v.A.r\" value2 &&\n+\tgit config ${mode_set} -f testConfig_actual \"v.A.r\" value2 &&\n \ttest_cmp testConfig_expect testConfig_actual\n '\n \n@@ -1324,21 +1345,21 @@ test_expect_success 'setting different case sensitive subsections ' '\n \tQf = v2\n \tEOF\n \t# exact match\n-\tgit config -f testConfig_actual a.b.c v2 &&\n+\tgit config ${mode_set} -f testConfig_actual a.b.c v2 &&\n \t# match section and subsection, key is cased differently.\n-\tgit config -f testConfig_actual K.E.y v2 &&\n+\tgit config ${mode_set} -f testConfig_actual K.E.y v2 &&\n \t# section and key are matched case insensitive, but subsection needs\n \t# to match; When writing out new values only the key is adjusted\n-\tgit config -f testConfig_actual v.A.r v2 &&\n+\tgit config ${mode_set} -f testConfig_actual v.A.r v2 &&\n \t# subsection is not matched:\n-\tgit config -f testConfig_actual d.E.f v2 &&\n+\tgit config ${mode_set} -f testConfig_actual d.E.f v2 &&\n \ttest_cmp testConfig_expect testConfig_actual\n '\n \n for VAR in a .a a. a.0b a.\"b c\". a.\"b c\".0d\n do\n \ttest_expect_success \"git -c $VAR=VAL rejects invalid '$VAR'\" '\n-\t\ttest_must_fail git -c \"$VAR=VAL\" config -l\n+\t\ttest_must_fail git -c \"$VAR=VAL\" config ${mode_prefix}list\n \t'\n done\n \n@@ -1346,20 +1367,20 @@ for VAR in a.b a.\"b c\".d\n do\n \ttest_expect_success \"git -c $VAR=VAL works with valid '$VAR'\" '\n \t\techo VAL >expect &&\n-\t\tgit -c \"$VAR=VAL\" config --get \"$VAR\" >actual &&\n+\t\tgit -c \"$VAR=VAL\" config ${mode_prefix}get \"$VAR\" >actual &&\n \t\ttest_cmp expect actual\n \t'\n done\n \n test_expect_success 'git -c is not confused by empty environment' '\n-\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config --list\n+\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config ${mode_prefix}list\n '\n \n test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n \tv=\"${SQ}key.one=foo${SQ}\" &&\n \tv=\"$v  ${SQ}key.two=bar${SQ}\" &&\n \tv=\"$v ${SQ}key.ambiguous=section.whatever=value${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_prefix}get-regexp \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.one foo\n \tkey.two bar\n@@ -1372,7 +1393,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '\n \tv=\"${SQ}key.one${SQ}=${SQ}foo${SQ}\" &&\n \tv=\"$v  ${SQ}key.two${SQ}=${SQ}bar${SQ}\" &&\n \tv=\"$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_prefix}get-regexp \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.one foo\n \tkey.two bar\n@@ -1386,7 +1407,7 @@ test_expect_success 'old and new-style entries can mix' '\n \tv=\"$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}\" &&\n \tv=\"$v ${SQ}key.oldtwo=oldbar${SQ}\" &&\n \tv=\"$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_prefix}get-regexp \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.oldone oldfoo\n \tkey.newone newfoo\n@@ -1399,7 +1420,7 @@ test_expect_success 'old and new-style entries can mix' '\n test_expect_success 'old and new bools with ambiguous subsection' '\n \tv=\"${SQ}key.with=equals.oldbool${SQ}\" &&\n \tv=\"$v ${SQ}key.with=equals.newbool${SQ}=\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_prefix}get-regexp \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.with equals.oldbool\n \tkey.with=equals.newbool\n@@ -1413,7 +1434,7 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '\n \tenv.two two\n \tEOF\n \tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ} ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\" >actual &&\n+\t\tgit config ${mode_prefix}get-regexp \"env.*\" >actual &&\n \ttest_cmp expect actual &&\n \n \tcat >expect <<-EOF &&\n@@ -1421,12 +1442,12 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '\n \tenv.two two\n \tEOF\n \tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ}\\\\$SQ$SQ$SQ ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\" >actual &&\n+\t\tgit config ${mode_prefix}get-regexp \"env.*\" >actual &&\n \ttest_cmp expect actual &&\n \n \ttest_must_fail env \\\n \t\tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ}\\\\$SQ ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\"\n+\t\tgit config ${mode_prefix}get-regexp \"env.*\"\n '\n \n test_expect_success 'git --config-env=key=envvar support' '\n@@ -1439,12 +1460,12 @@ test_expect_success 'git --config-env=key=envvar support' '\n \tfalse\n \tEOF\n \t{\n-\t\tENVVAR=value git --config-env=core.name=ENVVAR config core.name &&\n-\t\tENVVAR=value git --config-env core.name=ENVVAR config core.name &&\n-\t\tENVVAR=value git --config-env=foo.CamelCase=ENVVAR config foo.camelcase &&\n-\t\tENVVAR=value git --config-env foo.CamelCase=ENVVAR config foo.camelcase &&\n-\t\tENVVAR= git --config-env=foo.flag=ENVVAR config --bool foo.flag &&\n-\t\tENVVAR= git --config-env foo.flag=ENVVAR config --bool foo.flag\n+\t\tENVVAR=value git --config-env=core.name=ENVVAR config ${mode_get} core.name &&\n+\t\tENVVAR=value git --config-env core.name=ENVVAR config ${mode_get} core.name &&\n+\t\tENVVAR=value git --config-env=foo.CamelCase=ENVVAR config ${mode_get} foo.camelcase &&\n+\t\tENVVAR=value git --config-env foo.CamelCase=ENVVAR config ${mode_get} foo.camelcase &&\n+\t\tENVVAR= git --config-env=foo.flag=ENVVAR config ${mode_get} --bool foo.flag &&\n+\t\tENVVAR= git --config-env foo.flag=ENVVAR config ${mode_get} --bool foo.flag\n \t} >actual &&\n \ttest_cmp expect actual\n '\n@@ -1452,17 +1473,17 @@ test_expect_success 'git --config-env=key=envvar support' '\n test_expect_success 'git --config-env with missing value' '\n \ttest_must_fail env ENVVAR=value git --config-env 2>error &&\n \tgrep \"no config key given for --config-env\" error &&\n-\ttest_must_fail env ENVVAR=value git --config-env config core.name 2>error &&\n+\ttest_must_fail env ENVVAR=value git --config-env config ${mode_get} core.name 2>error &&\n \tgrep \"invalid config format: config\" error\n '\n \n test_expect_success 'git --config-env fails with invalid parameters' '\n-\ttest_must_fail git --config-env=foo.flag config --bool foo.flag 2>error &&\n+\ttest_must_fail git --config-env=foo.flag config ${mode_get} --bool foo.flag 2>error &&\n \ttest_grep \"invalid config format: foo.flag\" error &&\n-\ttest_must_fail git --config-env=foo.flag= config --bool foo.flag 2>error &&\n+\ttest_must_fail git --config-env=foo.flag= config ${mode_get} --bool foo.flag 2>error &&\n \ttest_grep \"missing environment variable name for configuration ${SQ}foo.flag${SQ}\" error &&\n \tsane_unset NONEXISTENT &&\n-\ttest_must_fail git --config-env=foo.flag=NONEXISTENT config --bool foo.flag 2>error &&\n+\ttest_must_fail git --config-env=foo.flag=NONEXISTENT config ${mode_get} --bool foo.flag 2>error &&\n \ttest_grep \"missing environment variable ${SQ}NONEXISTENT${SQ} for configuration ${SQ}foo.flag${SQ}\" error\n '\n \n@@ -1474,7 +1495,7 @@ test_expect_success 'git -c and --config-env work together' '\n \tENVVAR=env-value git \\\n \t\t-c bar.cmd=cmd-value \\\n \t\t--config-env=bar.env=ENVVAR \\\n-\t\tconfig --get-regexp \"^bar.*\" >actual &&\n+\t\tconfig ${mode_prefix}get-regexp \"^bar.*\" >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1484,8 +1505,8 @@ test_expect_success 'git -c and --config-env override each other' '\n \tcmd\n \tEOF\n \t{\n-\t\tENVVAR=env git -c bar.bar=cmd --config-env=bar.bar=ENVVAR config bar.bar &&\n-\t\tENVVAR=env git --config-env=bar.bar=ENVVAR -c bar.bar=cmd config bar.bar\n+\t\tENVVAR=env git -c bar.bar=cmd --config-env=bar.bar=ENVVAR config ${config_get} bar.bar &&\n+\t\tENVVAR=env git --config-env=bar.bar=ENVVAR -c bar.bar=cmd config ${config_get} bar.bar\n \t} >actual &&\n \ttest_cmp expect actual\n '\n@@ -1494,7 +1515,7 @@ test_expect_success '--config-env handles keys with equals' '\n \techo value=with=equals >expect &&\n \tENVVAR=value=with=equals git \\\n \t\t--config-env=section.subsection=with=equals.key=ENVVAR \\\n-\t\tconfig section.subsection=with=equals.key >actual &&\n+\t\tconfig ${config_get} section.subsection=with=equals.key >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1502,7 +1523,7 @@ test_expect_success 'git config handles environment config pairs' '\n \tGIT_CONFIG_COUNT=2 \\\n \t\tGIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"foo\" \\\n \t\tGIT_CONFIG_KEY_1=\"pair.two\" GIT_CONFIG_VALUE_1=\"bar\" \\\n-\t\tgit config --get-regexp \"pair.*\" >actual &&\n+\t\tgit config ${mode_prefix}get-regexp \"pair.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tpair.one foo\n \tpair.two bar\n@@ -1520,7 +1541,7 @@ test_expect_success 'git config ignores pairs exceeding count' '\n \tGIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n \t\tGIT_CONFIG_KEY_1=\"pair.two\" GIT_CONFIG_VALUE_1=\"value\" \\\n-\t\tgit config --get-regexp \"pair.*\" >actual 2>error &&\n+\t\tgit config ${mode_prefix}get-regexp \"pair.*\" >actual 2>error &&\n \tcat >expect <<-EOF &&\n \tpair.one value\n \tEOF\n@@ -1531,43 +1552,43 @@ test_expect_success 'git config ignores pairs exceeding count' '\n test_expect_success 'git config ignores pairs with zero count' '\n \ttest_must_fail env \\\n \t\tGIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${config_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n test_expect_success 'git config ignores pairs with empty count' '\n \ttest_must_fail env \\\n \t\tGIT_CONFIG_COUNT= GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${config_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n test_expect_success 'git config fails with invalid count' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&\n+\ttest_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&\n \ttest_grep \"bogus count\" error &&\n-\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&\n+\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&\n \ttest_grep \"too many entries\" error\n '\n \n test_expect_success 'git config fails with missing config key' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config --list 2>error &&\n+\t\tgit config ${mode_prefix}list 2>error &&\n \ttest_grep \"missing config key\" error\n '\n \n test_expect_success 'git config fails with missing config value' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=\"pair.one\" \\\n-\t\tgit config --list 2>error &&\n+\t\tgit config ${mode_prefix}list 2>error &&\n \ttest_grep \"missing config value\" error\n '\n \n test_expect_success 'git config fails with invalid config pair key' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \\\n-\t\tgit config --list &&\n+\t\tgit config ${mode_prefix}list &&\n \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \\\n-\t\tgit config --list\n+\t\tgit config ${mode_prefix}list\n '\n \n test_expect_success 'environment overrides config file' '\n@@ -1577,7 +1598,7 @@ test_expect_success 'environment overrides config file' '\n \tone = value\n \tEOF\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \\\n-\t\tgit config pair.one >actual &&\n+\t\tgit config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1587,7 +1608,7 @@ test_expect_success 'environment overrides config file' '\n test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \\\n \t\tGIT_CONFIG_PARAMETERS=\"${SQ}pair.one=override${SQ}\" \\\n-\t\tgit config pair.one >actual &&\n+\t\tgit config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1596,7 +1617,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '\n \n test_expect_success 'command line overrides environment config' '\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \\\n-\t\tgit -c pair.one=override config pair.one >actual &&\n+\t\tgit -c pair.one=override config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1606,8 +1627,8 @@ test_expect_success 'command line overrides environment config' '\n test_expect_success 'git config --edit works' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n-\tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n-\tgit config -f tmp --list >actual &&\n+\tGIT_EDITOR=\"echo [test]value=yes >\" git config ${mode_prefix}edit -f tmp &&\n+\tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1615,8 +1636,8 @@ test_expect_success 'git config --edit respects core.editor' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n \ttest_config core.editor \"echo [test]value=yes >\" &&\n-\tgit config -f tmp --edit &&\n-\tgit config -f tmp --list >actual &&\n+\tgit config ${mode_prefix}edit -f tmp &&\n+\tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1627,7 +1648,7 @@ test_expect_success 'barf on syntax error' '\n \t[section]\n \tkey garbage\n \tEOF\n-\ttest_must_fail git config --get section.key 2>error &&\n+\ttest_must_fail git config ${mode_prefix}get section.key 2>error &&\n \ttest_grep \" line 3 \" error\n '\n \n@@ -1637,7 +1658,7 @@ test_expect_success 'barf on incomplete section header' '\n \t[section\n \tkey = value\n \tEOF\n-\ttest_must_fail git config --get section.key 2>error &&\n+\ttest_must_fail git config ${mode_prefix}get section.key 2>error &&\n \ttest_grep \" line 2 \" error\n '\n \n@@ -1647,7 +1668,7 @@ test_expect_success 'barf on incomplete string' '\n \t[section]\n \tkey = \"value string\n \tEOF\n-\ttest_must_fail git config --get section.key 2>error &&\n+\ttest_must_fail git config ${mode_prefix}get section.key 2>error &&\n \ttest_grep \" line 3 \" error\n '\n \n@@ -1660,22 +1681,22 @@ test_expect_success 'urlmatch' '\n \t\tcookieFile = /tmp/cookie.txt\n \tEOF\n \n-\ttest_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&\n+\ttest_expect_code 1 git config ${mode_prefix}get-urlmatch --bool doesnt.exist https://good.example.com >actual &&\n \ttest_must_be_empty actual &&\n \n \techo true >expect &&\n-\tgit config --bool --get-urlmatch http.SSLverify https://good.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch --bool http.SSLverify https://good.example.com >actual &&\n \ttest_cmp expect actual &&\n \n \techo false >expect &&\n-\tgit config --bool --get-urlmatch http.sslverify https://weak.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch --bool http.sslverify https://weak.example.com >actual &&\n \ttest_cmp expect actual &&\n \n \t{\n \t\techo http.cookiefile /tmp/cookie.txt &&\n \t\techo http.sslverify false\n \t} >expect &&\n-\tgit config --get-urlmatch HTTP https://weak.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://weak.example.com >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1690,7 +1711,7 @@ test_expect_success 'urlmatch with --show-scope' '\n \tlocal\thttp.cookiefile /tmp/cookie.txt\n \tlocal\thttp.sslverify false\n \tEOF\n-\tgit config --get-urlmatch --show-scope HTTP https://weak.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch --show-scope HTTP https://weak.example.com >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1721,47 +1742,47 @@ test_expect_success 'urlmatch favors more specific URLs' '\n \tEOF\n \n \techo http.cookiefile /tmp/root.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://example.com >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://example.com/subdirectory >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://example.com/subdirectory >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/user.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://user@example.com/ >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://user@example.com/ >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/preceding.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://preceding.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://preceding.example.com >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/wildcard.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://wildcard.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://wildcard.example.com >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/trailing.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://trailing.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://trailing.example.com >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://user@sub.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://user@sub.example.com >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/multiwildcard.txt >expect &&\n-\tgit config --get-urlmatch HTTP https://wildcard.example.org >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://wildcard.example.org >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1774,34 +1795,34 @@ test_expect_success 'urlmatch with wildcard' '\n \t\tcookieFile = /tmp/cookie.txt\n \tEOF\n \n-\ttest_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&\n+\ttest_expect_code 1 git config ${mode_prefix}get-urlmatch --bool doesnt.exist https://good.example.com >actual &&\n \ttest_must_be_empty actual &&\n \n \techo true >expect &&\n-\tgit config --bool --get-urlmatch http.SSLverify https://example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch --bool http.SSLverify https://example.com >actual &&\n \ttest_cmp expect actual &&\n \n \techo true >expect &&\n-\tgit config --bool --get-urlmatch http.SSLverify https://good-example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch --bool http.SSLverify https://good-example.com >actual &&\n \ttest_cmp expect actual &&\n \n \techo true >expect &&\n-\tgit config --bool --get-urlmatch http.sslverify https://deep.nested.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch --bool http.sslverify https://deep.nested.example.com >actual &&\n \ttest_cmp expect actual &&\n \n \techo false >expect &&\n-\tgit config --bool --get-urlmatch http.sslverify https://good.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch --bool http.sslverify https://good.example.com >actual &&\n \ttest_cmp expect actual &&\n \n \t{\n \t\techo http.cookiefile /tmp/cookie.txt &&\n \t\techo http.sslverify false\n \t} >expect &&\n-\tgit config --get-urlmatch HTTP https://good.example.com >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://good.example.com >actual &&\n \ttest_cmp expect actual &&\n \n \techo http.sslverify >expect &&\n-\tgit config --get-urlmatch HTTP https://more.example.com.au >actual &&\n+\tgit config ${mode_prefix}get-urlmatch HTTP https://more.example.com.au >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1828,7 +1849,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t# please be careful when you update the above variable\n \tEOF\n \n-\tgit config --unset section.key &&\n+\tgit config ${mode_prefix}unset section.key &&\n \ttest_cmp expect .git/config &&\n \n \tcat >.git/config <<-\\EOF &&\n@@ -1841,7 +1862,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[next-section]\n \tEOF\n \n-\tgit config --unset section.key &&\n+\tgit config ${mode_prefix}unset section.key &&\n \ttest_cmp expect .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1851,7 +1872,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[two]\n \tkey = true\n \tEOF\n-\tgit config --unset two.key &&\n+\tgit config ${mode_prefix}unset two.key &&\n \t! grep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1861,7 +1882,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[one]\n \tkey = true\n \tEOF\n-\tgit config --unset-all one.key &&\n+\tgit config ${mode_prefix}unset-all one.key &&\n \ttest_line_count = 0 .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1871,7 +1892,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[two]\n \tQkey = true\n \tEOF\n-\tgit config --unset two.key &&\n+\tgit config ${mode_prefix}unset two.key &&\n \tgrep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1883,7 +1904,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[TWO \"subsection\"]\n \t[one]\n \tEOF\n-\tgit config --unset two.subsection.key &&\n+\tgit config ${mode_prefix}unset two.subsection.key &&\n \ttest \"not [two subsection]\" = \"$(git config one.key)\" &&\n \ttest_line_count = 3 .git/config\n '\n@@ -1895,7 +1916,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' '\n \tkey = value2\n \tEOF\n \n-\tgit config --unset-all section.key &&\n+\tgit config ${mode_prefix}unset-all section.key &&\n \ttest_line_count = 0 .git/config\n '\n \n@@ -1909,7 +1930,7 @@ test_expect_success 'adding a key into an empty section reuses header' '\n \tQkey = value\n \tEOF\n \n-\tgit config section.key value &&\n+\tgit config ${mode_set} section.key value &&\n \ttest_cmp expect .git/config\n '\n \n@@ -1918,7 +1939,7 @@ test_expect_success POSIXPERM,PERL 'preserves existing permissions' '\n \tgit config imap.pass Hunter2 &&\n \tperl -e \\\n \t  \"die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600\" &&\n-\tgit config --rename-section imap pop &&\n+\tgit config ${mode_prefix}rename-section imap pop &&\n \tperl -e \\\n \t  \"die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600\"\n '\n@@ -1967,7 +1988,7 @@ test_expect_success '--show-origin with --list' '\n \tcommand line:\tuser.cmdline=true\n \tEOF\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\\\n-\t\tgit -c user.cmdline=true config --list --show-origin >output &&\n+\t\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -1984,7 +2005,7 @@ test_expect_success '--show-origin with --list --null' '\n \tincludeQcommand line:Quser.cmdline\n \ttrueQ\n \tEOF\n-\tgit -c user.cmdline=true config --null --list --show-origin >output.raw &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&\n \tnul_to_q <output.raw >output &&\n \t# The here-doc above adds a newline that the --null output would not\n \t# include. Add it here to make the two comparable.\n@@ -1998,7 +2019,7 @@ test_expect_success '--show-origin with single file' '\n \tfile:.git/config\tuser.override=local\n \tfile:.git/config\tinclude.path=../include/relative.include\n \tEOF\n-\tgit config --local --list --show-origin >output &&\n+\tgit config ${mode_prefix}list --local --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2007,7 +2028,7 @@ test_expect_success '--show-origin with --get-regexp' '\n \tfile:$HOME/.gitconfig\tuser.global true\n \tfile:.git/config\tuser.local true\n \tEOF\n-\tgit config --show-origin --get-regexp \"user\\.[g|l].*\" >output &&\n+\tgit config ${mode_prefix}get-regexp --show-origin \"user\\.[g|l].*\" >output &&\n \ttest_cmp expect output\n '\n \n@@ -2015,7 +2036,7 @@ test_expect_success '--show-origin getting a single key' '\n \tcat >expect <<-\\EOF &&\n \tfile:.git/config\tlocal\n \tEOF\n-\tgit config --show-origin user.override >output &&\n+\tgit config ${mode_get} --show-origin user.override >output &&\n \ttest_cmp expect output\n '\n \n@@ -2036,7 +2057,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '\n \tcat >expect <<-\\EOF &&\n \tfile:\"file\\\" (dq) and spaces.conf\"\tuser.custom=true\n \tEOF\n-\tgit config --file \"$WEIRDLY_NAMED_FILE\" --show-origin --list >output &&\n+\tgit config ${mode_prefix}list --file \"$WEIRDLY_NAMED_FILE\" --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2044,7 +2065,7 @@ test_expect_success '--show-origin stdin' '\n \tcat >expect <<-\\EOF &&\n \tstandard input:\tuser.custom=true\n \tEOF\n-\tgit config --file - --show-origin --list <\"$CUSTOM_CONFIG_FILE\" >output &&\n+\tgit config ${mode_prefix}list --file - --show-origin <\"$CUSTOM_CONFIG_FILE\" >output &&\n \ttest_cmp expect output\n '\n \n@@ -2057,7 +2078,7 @@ test_expect_success '--show-origin stdin with file include' '\n \tfile:$INCLUDE_DIR/stdin.include\tinclude\n \tEOF\n \techo \"[include]path=\\\"$INCLUDE_DIR\\\"/stdin.include\" |\n-\tgit config --show-origin --includes --file - user.stdin >output &&\n+\tgit config ${mode_get} --show-origin --includes --file - user.stdin >output &&\n \n \ttest_cmp expect output\n '\n@@ -2071,7 +2092,7 @@ test_expect_success '--show-origin blob' '\n \t\tcat >expect <<-EOF &&\n \t\tblob:$blob\tuser.custom=true\n \t\tEOF\n-\t\tgit config --blob=$blob --show-origin --list >output &&\n+\t\tgit config ${mode_prefix}list --blob=$blob --show-origin >output &&\n \t\ttest_cmp expect output\n \t)\n '\n@@ -2087,13 +2108,13 @@ test_expect_success '--show-origin blob ref' '\n \t\tcp \"$CUSTOM_CONFIG_FILE\" custom.conf &&\n \t\tgit add custom.conf &&\n \t\tgit commit -m \"new config file\" &&\n-\t\tgit config --blob=main:custom.conf --show-origin --list >output &&\n+\t\tgit config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&\n \t\ttest_cmp expect output\n \t)\n '\n \n test_expect_success '--show-origin with --default' '\n-\tgit config --show-origin --default foo some.key >actual &&\n+\tgit config ${mode_get} --show-origin --default foo some.key >actual &&\n \techo \"command line:\tfoo\" >expect &&\n \ttest_cmp expect actual\n '\n@@ -2113,13 +2134,14 @@ test_expect_success '--show-scope with --list' '\n \tworktree\tuser.worktree=true\n \tcommand\tuser.cmdline=true\n \tEOF\n+\ttest_when_finished \"git worktree remove wt1\" &&\n \tgit worktree add wt1 &&\n \t# We need these to test for worktree scope, but outside of this\n \t# test, this is just noise\n \ttest_config core.repositoryformatversion 1 &&\n \ttest_config extensions.worktreeConfig true &&\n-\tgit config --worktree user.worktree true &&\n-\tgit -c user.cmdline=true config --list --show-scope >output &&\n+\tgit config ${mode_get} --worktree user.worktree true &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2128,7 +2150,7 @@ test_expect_success !MINGW '--show-scope with --blob' '\n \tcat >expect <<-EOF &&\n \tcommand\tuser.custom=true\n \tEOF\n-\tgit config --blob=$blob --show-scope --list >output &&\n+\tgit config ${mode_prefix}list --blob=$blob --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2138,7 +2160,7 @@ test_expect_success '--show-scope with --local' '\n \tlocal\tuser.override=local\n \tlocal\tinclude.path=../include/relative.include\n \tEOF\n-\tgit config --local --list --show-scope >output &&\n+\tgit config ${mode_prefix}list --local --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2146,7 +2168,7 @@ test_expect_success '--show-scope getting a single value' '\n \tcat >expect <<-\\EOF &&\n \tlocal\ttrue\n \tEOF\n-\tgit config --show-scope --get user.local >output &&\n+\tgit config ${mode_prefix}get --show-scope user.local >output &&\n \ttest_cmp expect output\n '\n \n@@ -2162,12 +2184,12 @@ test_expect_success '--show-scope with --show-origin' '\n \tlocal\tfile:.git/../include/relative.include\tuser.relative=include\n \tcommand\tcommand line:\tuser.cmdline=true\n \tEOF\n-\tgit -c user.cmdline=true config --list --show-origin --show-scope >output &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success '--show-scope with --default' '\n-\tgit config --show-scope --default foo some.key >actual &&\n+\tgit config ${mode_get} --show-scope --default foo some.key >actual &&\n \techo \"command\tfoo\" >expect &&\n \ttest_cmp expect actual\n '\n@@ -2203,7 +2225,7 @@ test_expect_success 'override global and system config' '\n \tglobal\thome.config=true\n \tlocal\tlocal.config=true\n \tEOF\n-\tgit config --show-scope --list >output &&\n+\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output &&\n \n \tcat >expect <<-EOF &&\n@@ -2212,20 +2234,20 @@ test_expect_success 'override global and system config' '\n \tlocal\tlocal.config=true\n \tEOF\n \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \\\n-\t\tgit config --show-scope --list >output &&\n+\t\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output &&\n \n \tcat >expect <<-EOF &&\n \tlocal\tlocal.config=true\n \tEOF\n \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \\\n-\t\tgit config --show-scope --list >output &&\n+\t\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'override global and system config with missing file' '\n-\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&\n-\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&\n+\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&\n+\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&\n \tGIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version\n '\n \n@@ -2251,10 +2273,10 @@ test_expect_success 'write to overridden global and system config' '\n \tkey = value\n EOF\n \n-\tGIT_CONFIG_GLOBAL=write-to-global git config --global config.key value &&\n+\tGIT_CONFIG_GLOBAL=write-to-global git config ${mode_set} --global config.key value &&\n \ttest_cmp expect write-to-global &&\n \n-\tGIT_CONFIG_SYSTEM=write-to-system git config --system config.key value &&\n+\tGIT_CONFIG_SYSTEM=write-to-system git config ${mode_set} --system config.key value &&\n \ttest_cmp expect write-to-system\n '\n \n@@ -2263,7 +2285,7 @@ do\n \ttest_expect_success \"$opt requires a repo\" '\n \t\t# we expect 128 to ensure that we do not simply\n \t\t# fail to find anything and return code \"1\"\n-\t\ttest_expect_code 128 nongit git config $opt foo.bar\n+\t\ttest_expect_code 128 nongit git config ${mode_get} $opt foo.bar\n \t'\n done\n \n@@ -2287,17 +2309,17 @@ test_expect_success 'identical mixed --type specifiers are allowed' '\n '\n \n test_expect_success 'non-identical modern --type specifiers are not allowed' '\n-\ttest_must_fail git config --type=int --type=bool section.big 2>error &&\n+\ttest_must_fail git config ${mode_get} --type=int --type=bool section.big 2>error &&\n \ttest_grep \"only one type at a time\" error\n '\n \n test_expect_success 'non-identical legacy --type specifiers are not allowed' '\n-\ttest_must_fail git config --int --bool section.big 2>error &&\n+\ttest_must_fail git config ${mode_get} --int --bool section.big 2>error &&\n \ttest_grep \"only one type at a time\" error\n '\n \n test_expect_success 'non-identical mixed --type specifiers are not allowed' '\n-\ttest_must_fail git config --type=int --bool section.big 2>error &&\n+\ttest_must_fail git config ${mode_get} --type=int --bool section.big 2>error &&\n \ttest_grep \"only one type at a time\" error\n '\n \n@@ -2314,12 +2336,12 @@ test_expect_success 'unset type specifiers may be reset to conflicting ones' '\n '\n \n test_expect_success '--type rejects unknown specifiers' '\n-\ttest_must_fail git config --type=nonsense section.foo 2>error &&\n+\ttest_must_fail git config ${mode_get} --type=nonsense section.foo 2>error &&\n \ttest_grep \"unrecognized --type argument\" error\n '\n \n test_expect_success '--type=int requires at least one digit' '\n-\ttest_must_fail git config --type int --default m some.key >out 2>error &&\n+\ttest_must_fail git config ${mode_get} --type int --default m some.key >out 2>error &&\n \tgrep \"bad numeric config value\" error &&\n \ttest_must_be_empty out\n '\n@@ -2341,18 +2363,18 @@ test_expect_success '--replace-all does not invent newlines' '\n \t[abc]\n \tQkey = b\n \tEOF\n-\tgit config --replace-all abc.key b &&\n+\tgit config ${mode_prefix}replace-all abc.key b &&\n \ttest_cmp expect .git/config\n '\n \n test_expect_success 'set all config with value-pattern' '\n \ttest_when_finished rm -f config initial &&\n-\tgit config --file=initial abc.key one &&\n+\tgit config ${mode_set} --file=initial abc.key one &&\n \n \t# no match => add new entry\n \tcp initial config &&\n-\tgit config --file=config abc.key two a+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_set_all} --file=config abc.key two a+ &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2360,12 +2382,12 @@ test_expect_success 'set all config with value-pattern' '\n \ttest_cmp expect actual &&\n \n \t# multiple matches => failure\n-\ttest_must_fail git config --file=config abc.key three o+ 2>err &&\n+\ttest_must_fail git config ${mode_set_all} --file=config abc.key three o+ 2>err &&\n \ttest_grep \"has multiple values\" err &&\n \n \t# multiple values, no match => add\n-\tgit config --file=config abc.key three a+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_set_all} --file=config abc.key three a+ &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2374,8 +2396,8 @@ test_expect_success 'set all config with value-pattern' '\n \ttest_cmp expect actual &&\n \n \t# single match => replace\n-\tgit config --file=config abc.key four h+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_set_all} --file=config abc.key four h+ &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2386,11 +2408,11 @@ test_expect_success 'set all config with value-pattern' '\n \n test_expect_success '--replace-all and value-pattern' '\n \ttest_when_finished rm -f config &&\n-\tgit config --file=config --add abc.key one &&\n-\tgit config --file=config --add abc.key two &&\n-\tgit config --file=config --add abc.key three &&\n-\tgit config --file=config --replace-all abc.key four \"o+\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}add --file=config abc.key one &&\n+\tgit config ${mode_prefix}add --file=config abc.key two &&\n+\tgit config ${mode_prefix}add --file=config abc.key three &&\n+\tgit config ${mode_prefix}replace-all --file=config abc.key four \"o+\" &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=four\n \tabc.key=three\n@@ -2403,33 +2425,33 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \tgit config --file=config dev.null bogus &&\n \n \t# These modes do not allow --fixed-value at all\n-\ttest_must_fail git config --file=config --fixed-value --add dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --rename-section dev null &&\n-\ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n-\ttest_must_fail git config --file=config --fixed-value --list &&\n-\ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n+\ttest_must_fail git config ${mode_prefix}add --file=config --fixed-value dev.null bogus &&\n+\ttest_must_fail git config ${mode_prefix}get-urlmatch --file=config --fixed-value dev.null bogus &&\n+\ttest_must_fail git config ${mode_prefix}get-urlmatch --file=config --fixed-value dev.null bogus &&\n+\ttest_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&\n+\ttest_must_fail git config ${mode_prefix}remove-section --file=config --fixed-value dev &&\n+\ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n+\ttest_must_fail git config ${mode_prefix}get-color --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_prefix}get-colorbool --file=config --fixed-value dev.null &&\n \n \t# These modes complain when --fixed-value has no value-pattern\n-\ttest_must_fail git config --file=config --fixed-value dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --get dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-all dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-regexp \"dev.*\" &&\n-\ttest_must_fail git config --file=config --fixed-value --unset dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --unset-all dev.null\n+\ttest_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&\n+\ttest_must_fail git config ${mode_prefix}replace-all --file=config --fixed-value dev.null bogus &&\n+\ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_prefix}get-all --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_prefix}get-regexp --file=config --fixed-value \"dev.*\" &&\n+\ttest_must_fail git config ${mode_prefix}unset --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_prefix}unset-all --file=config --fixed-value dev.null\n '\n \n test_expect_success '--fixed-value uses exact string matching' '\n \ttest_when_finished rm -f config initial &&\n \tMETA=\"a+b*c?d[e]f.g\" &&\n-\tgit config --file=initial fixed.test \"$META\" &&\n+\tgit config ${mode_set} --file=initial fixed.test \"$META\" &&\n \n \tcp initial config &&\n-\tgit config --file=config fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_set_all} --file=config fixed.test bogus \"$META\" &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n \tfixed.test=bogus\n@@ -2437,34 +2459,34 @@ test_expect_success '--fixed-value uses exact string matching' '\n \ttest_cmp expect actual &&\n \n \tcp initial config &&\n-\tgit config --file=config --fixed-value fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_set_all} --file=config --fixed-value fixed.test bogus \"$META\" &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tfixed.test=bogus\n \tEOF\n \ttest_cmp expect actual &&\n \n \tcp initial config &&\n-\ttest_must_fail git config --file=config --unset fixed.test \"$META\" &&\n-\tgit config --file=config --fixed-value --unset fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config fixed.test &&\n+\ttest_must_fail git config ${mode_prefix}unset --file=config fixed.test \"$META\" &&\n+\tgit config ${mode_prefix}unset --file=config --fixed-value fixed.test \"$META\" &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n-\ttest_must_fail git config --file=config --unset-all fixed.test \"$META\" &&\n-\tgit config --file=config --fixed-value --unset-all fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config fixed.test &&\n+\ttest_must_fail git config ${mode_prefix}unset-all --file=config fixed.test \"$META\" &&\n+\tgit config ${mode_prefix}unset-all --file=config --fixed-value fixed.test \"$META\" &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n-\tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}replace-all --file=config fixed.test bogus \"$META\" &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n \tfixed.test=bogus\n \tEOF\n \ttest_cmp expect actual &&\n \n-\tgit config --file=config --fixed-value --replace-all fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}replace-all --file=config --fixed-value fixed.test bogus \"$META\" &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=bogus\n \tfixed.test=bogus\n@@ -2475,23 +2497,23 @@ test_expect_success '--fixed-value uses exact string matching' '\n test_expect_success '--get and --get-all with --fixed-value' '\n \ttest_when_finished rm -f config &&\n \tMETA=\"a+b*c?d[e]f.g\" &&\n-\tgit config --file=config fixed.test bogus &&\n-\tgit config --file=config --add fixed.test \"$META\" &&\n+\tgit config ${mode_set} --file=config fixed.test bogus &&\n+\tgit config ${mode_prefix}add --file=config fixed.test \"$META\" &&\n \n-\tgit config --file=config --get fixed.test bogus &&\n-\ttest_must_fail git config --file=config --get fixed.test \"$META\" &&\n-\tgit config --file=config --get --fixed-value fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config --get --fixed-value fixed.test non-existent &&\n+\tgit config ${mode_prefix}get --file=config fixed.test bogus &&\n+\ttest_must_fail git config ${mode_prefix}get --file=config fixed.test \"$META\" &&\n+\tgit config ${mode_prefix}get --file=config --fixed-value fixed.test \"$META\" &&\n+\ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value fixed.test non-existent &&\n \n-\tgit config --file=config --get-all fixed.test bogus &&\n-\ttest_must_fail git config --file=config --get-all fixed.test \"$META\" &&\n-\tgit config --file=config --get-all --fixed-value fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config --get-all --fixed-value fixed.test non-existent &&\n+\tgit config ${mode_prefix}get-all --file=config fixed.test bogus &&\n+\ttest_must_fail git config ${mode_prefix}get-all --file=config fixed.test \"$META\" &&\n+\tgit config ${mode_prefix}get-all --file=config --fixed-value fixed.test \"$META\" &&\n+\ttest_must_fail git config ${mode_prefix}get-all --file=config --fixed-value fixed.test non-existent &&\n \n-\tgit config --file=config --get-regexp fixed+ bogus &&\n-\ttest_must_fail git config --file=config --get-regexp fixed+ \"$META\" &&\n-\tgit config --file=config --get-regexp --fixed-value fixed+ \"$META\" &&\n-\ttest_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent\n+\tgit config ${mode_prefix}get-regexp --file=config fixed+ bogus &&\n+\ttest_must_fail git config ${mode_prefix}get-regexp --file=config fixed+ \"$META\" &&\n+\tgit config ${mode_prefix}get-regexp --file=config --fixed-value fixed+ \"$META\" &&\n+\ttest_must_fail git config ${mode_prefix}get-regexp --file=config --fixed-value fixed+ non-existent\n '\n \n test_expect_success 'includeIf.hasconfig:remote.*.url' '\n@@ -2516,10 +2538,10 @@ test_expect_success 'includeIf.hasconfig:remote.*.url' '\n \tEOF\n \n \techo this-is-included >expect-this &&\n-\tgit -C hasremoteurlTest config --get user.this >actual-this &&\n+\tgit -C hasremoteurlTest config ${mode_prefix}get user.this >actual-this &&\n \ttest_cmp expect-this actual-this &&\n \n-\ttest_must_fail git -C hasremoteurlTest config --get user.that\n+\ttest_must_fail git -C hasremoteurlTest config ${mode_prefix}get user.that\n '\n \n test_expect_success 'includeIf.hasconfig:remote.*.url respects last-config-wins' '\n@@ -2546,13 +2568,13 @@ test_expect_success 'includeIf.hasconfig:remote.*.url respects last-config-wins'\n \techo main-config >expect-main-config &&\n \techo included-config >expect-included-config &&\n \n-\tgit -C hasremoteurlTest config --get user.one >actual &&\n+\tgit -C hasremoteurlTest config ${mode_prefix}get user.one >actual &&\n \ttest_cmp expect-main-config actual &&\n \n-\tgit -C hasremoteurlTest config --get user.two >actual &&\n+\tgit -C hasremoteurlTest config ${mode_prefix}get user.two >actual &&\n \ttest_cmp expect-included-config actual &&\n \n-\tgit -C hasremoteurlTest config --get user.three >actual &&\n+\tgit -C hasremoteurlTest config ${mode_prefix}get user.three >actual &&\n \ttest_cmp expect-main-config actual\n '\n \n@@ -2587,11 +2609,11 @@ test_expect_success 'includeIf.hasconfig:remote.*.url globs' '\n \t\tpath = \"$(pwd)/no\"\n \tEOF\n \n-\tgit -C hasremoteurlTest config --get user.dss &&\n-\tgit -C hasremoteurlTest config --get user.dse &&\n-\tgit -C hasremoteurlTest config --get user.dsm &&\n-\tgit -C hasremoteurlTest config --get user.ssm &&\n-\ttest_must_fail git -C hasremoteurlTest config --get user.no\n+\tgit -C hasremoteurlTest config ${mode_prefix}get user.dss &&\n+\tgit -C hasremoteurlTest config ${mode_prefix}get user.dse &&\n+\tgit -C hasremoteurlTest config ${mode_prefix}get user.dsm &&\n+\tgit -C hasremoteurlTest config ${mode_prefix}get user.ssm &&\n+\ttest_must_fail git -C hasremoteurlTest config ${mode_prefix}get user.no\n '\n \n test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such included files' '\n@@ -2625,4 +2647,11 @@ test_expect_success 'specifying multiple modes causes failure' '\n \ttest_cmp expect err\n '\n \n+test_expect_success 'specifying mode and subcommand causes failure' '\n+\ttest_must_fail git config get --get 2>err &&\n+\tgrep \"subcommand and action modes are incompatible\" err\n+'\n+\n+done\n+\n test_done\n-- \n2.44.0\n\n"},{"id":"490076","messageId":"5ff2bf4a2286431a3b3048e0ea04d6551006d0d7.1709724089.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH 8/8] Documentation/git-config: update to new-style syntax","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-06T11:32:03Z","receivedAt":"2024-03-06T11:32:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Update our documentation of git-config(1) to stop mentioning the old\nsyntax while starting to mention the new syntax. Remove the help\nmismatch in t0450 so that we start to ensure that the manpage and\nbuiltin synopsis match.\n\nNote that there will still be lots of mentions of the old style syntax\nof git-config(1) throughout our documentation. This documentation will\nbe updated over time.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 204 ++++++++++++++++++-----------------\n builtin/config.c             |  46 +++++---\n t/t0450/txt-help-mismatches  |   1 -\n 3 files changed, 137 insertions(+), 114 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex dff39093b5..edf67c85f7 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -9,21 +9,22 @@ git-config - Get and set repository or global options\n SYNOPSIS\n --------\n [verse]\n-'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n-'git config' [<file-option>] [--type=<type>] --add <name> <value>\n-'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL>\n-'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n-'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n-'git config' [<file-option>] --rename-section <old-name> <new-name>\n-'git config' [<file-option>] --remove-section <name>\n-'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list\n-'git config' [<file-option>] --get-color <name> [<default>]\n-'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n-'git config' [<file-option>] -e | --edit\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>]\n \n DESCRIPTION\n -----------\n@@ -74,35 +75,50 @@ On success, the command returns the exit code 0.\n A list of all available configuration variables can be obtained using the\n `git help --config` command.\n \n-[[OPTIONS]]\n-OPTIONS\n--------\n-\n---replace-all::\n-\tDefault behavior is to replace at most one line. This replaces\n-\tall lines matching the key (and optionally the `value-pattern`).\n+COMMANDS\n+--------\n \n---add::\n-\tAdds a new line to the option without altering any existing\n-\tvalues.  This is the same as providing '^$' as the `value-pattern`\n-\tin `--replace-all`.\n+list::\n+\tList all variables set in config file, along with their values.\n \n---get::\n+get <name> [<value-pattern>]::\n \tGet the value for a given key (optionally filtered by a regex\n \tmatching the value). Returns error code 1 if the key was not\n \tfound and the last value if multiple key values were found.\n \n---get-all::\n+get-all <name> [<value-pattern>]::\n \tLike get, but returns all values for a multi-valued key.\n \n---get-regexp::\n-\tLike --get-all, but interprets the name as a regular expression and\n+get-color <name> [<default>]::\n+\n+\tFind the color configured for `name` (e.g. `color.diff.new`) and\n+\toutput it as the ANSI color escape sequence to the standard\n+\toutput.  The optional `default` parameter is used instead, if\n+\tthere is no color configured for `name`.\n++\n+`--type=color [--default=<default>]` is preferred over `get-color`\n+(but note that `get-color` will omit the trailing newline printed by\n+`--type=color`).\n+\n+get-colorbool <name> [<stdout-is-tty>]::\n+\n+\tFind the color setting for `<name>` (e.g. `color.diff`) and output\n+\t\"true\" or \"false\".  `<stdout-is-tty>` should be either \"true\" or\n+\t\"false\", and is taken into account when configuration says\n+\t\"auto\".  If `<stdout-is-tty>` is missing, then checks the standard\n+\toutput of the command itself, and exits with status 0 if color\n+\tis to be used, or exits with status 1 otherwise.\n+\tWhen the color setting for `name` is undefined, the command uses\n+\t`color.ui` as fallback.\n+\n+get-regex <name-regex> [<value-pattern>]::\n+\tLike `get-all`, but interprets the name as a regular expression and\n \twrites out the key names.  Regular expression matching is currently\n \tcase-sensitive and done against a canonicalized version of the key\n \tin which section and variable names are lowercased, but subsection\n \tnames are not.\n \n---get-urlmatch <name> <URL>::\n+get-urlmatch <name> <URL>::\n \tWhen given a two-part <name> as <section>.<key>, the value for\n \t<section>.<URL>.<key> whose <URL> part matches the best to the\n \tgiven URL is returned (if no such key exists, the value for\n@@ -110,6 +126,43 @@ OPTIONS\n \t<section> as name, do so for all the keys in the section and\n \tlist them.  Returns error code 1 if no value is found.\n \n+add <name> <value>::\n+\tAdds a new line to the option without altering any existing\n+\tvalues.  This is the same as providing '^$' as the `value-pattern`\n+\tin `--replace-all`.\n+\n+set <name> <value>::\n+\tSet the config key to the given value. Adds a new key to the\n+\tconfiguration in case it does not yet exist. This subcommand fails when\n+\tthe key has multiple values.\n+\n+set-all <name> <value> [<value-pattern>]::\n+\tSet all config keys to the given value.\n+\n+unset <name> [<value-pattern>]::\n+\tRemove the line matching the key from config file.\n+\n+unset-all <name> [<value-pattern>]::\n+\tRemove all lines matching the key from config file.\n+\n+replace-all <name> <value> [<value-pattern>]::\n+\tDefault behavior is to replace at most one line. This replaces\n+\tall lines matching the key (and optionally the `value-pattern`).\n+\n+remove-section <name>::\n+\tRemove the given section from the configuration file.\n+\n+rename-section <old-name> <new-name>::\n+\tRename the given section to a new name.\n+\n+edit::\n+\tOpens an editor to modify the specified config file; either\n+\t`--system`, `--global`, or repository (default).\n+\n+[[OPTIONS]]\n+OPTIONS\n+-------\n+\n --global::\n \tFor writing options: write to global `~/.gitconfig` file\n \trather than the repository `.git/config`, write to\n@@ -166,22 +219,6 @@ See also <<FILES>>.\n \tsection in linkgit:gitrevisions[7] for a more complete list of\n \tways to spell blob names.\n \n---remove-section::\n-\tRemove the given section from the configuration file.\n-\n---rename-section::\n-\tRename the given section to a new name.\n-\n---unset::\n-\tRemove the line matching the key from config file.\n-\n---unset-all::\n-\tRemove all lines matching the key from config file.\n-\n--l::\n---list::\n-\tList all variables set in config file, along with their values.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -236,8 +273,8 @@ Valid `<type>`'s include:\n \tcontain line breaks.\n \n --name-only::\n-\tOutput only the names of config variables for `--list` or\n-\t`--get-regexp`.\n+\tOutput only the names of config variables for `list` or\n+\t`get-regexp`.\n \n --show-origin::\n \tAugment the output of all queried config options with the\n@@ -250,33 +287,6 @@ Valid `<type>`'s include:\n \tall queried config options with the scope of that value\n \t(worktree, local, global, system, command).\n \n---get-colorbool <name> [<stdout-is-tty>]::\n-\n-\tFind the color setting for `<name>` (e.g. `color.diff`) and output\n-\t\"true\" or \"false\".  `<stdout-is-tty>` should be either \"true\" or\n-\t\"false\", and is taken into account when configuration says\n-\t\"auto\".  If `<stdout-is-tty>` is missing, then checks the standard\n-\toutput of the command itself, and exits with status 0 if color\n-\tis to be used, or exits with status 1 otherwise.\n-\tWhen the color setting for `name` is undefined, the command uses\n-\t`color.ui` as fallback.\n-\n---get-color <name> [<default>]::\n-\n-\tFind the color configured for `name` (e.g. `color.diff.new`) and\n-\toutput it as the ANSI color escape sequence to the standard\n-\toutput.  The optional `default` parameter is used instead, if\n-\tthere is no color configured for `name`.\n-+\n-`--type=color [--default=<default>]` is preferred over `--get-color`\n-(but note that `--get-color` will omit the trailing newline printed by\n-`--type=color`).\n-\n--e::\n---edit::\n-\tOpens an editor to modify the specified config file; either\n-\t`--system`, `--global`, or repository (default).\n-\n --[no-]includes::\n \tRespect `include.*` directives in config files when looking up\n \tvalues. Defaults to `off` when a specific file is given (e.g.,\n@@ -284,13 +294,13 @@ Valid `<type>`'s include:\n \tconfig files.\n \n --default <value>::\n-  When using `--get`, and the requested variable is not found, behave as if\n+  When using `get`, and the requested variable is not found, behave as if\n   <value> were the value assigned to the that variable.\n \n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n-using `--list` or any of the `--get-*` which may return multiple results.\n+using `list` or any of the `get-*` subcommands which may return multiple results.\n The default is to use a pager.\n \n [[FILES]]\n@@ -333,8 +343,8 @@ precedence over values read earlier.  When multiple values are taken then all\n values of a key from all files will be used.\n \n By default, options are only written to the repository specific\n-configuration file. Note that this also affects options like `--replace-all`\n-and `--unset`. *'git config' will only ever change one file at a time*.\n+configuration file. Note that this also affects subcommands like `replace-all`\n+and `unset`. *'git config' will only ever change one file at a time*.\n \n You can limit which configuration sources are read from or written to by\n specifying the path of a file with the `--file` option, or by specifying a\n@@ -469,7 +479,7 @@ Given a .git/config like this:\n you can set the filemode to true with\n \n ------------\n-% git config core.filemode true\n+% git config set core.filemode true\n ------------\n \n The hypothetical proxy command entries actually have a postfix to discern\n@@ -477,7 +487,7 @@ what URL they apply to. Here is how to change the entry for kernel.org\n to \"ssh\".\n \n ------------\n-% git config core.gitproxy '\"ssh\" for kernel.org' 'for kernel.org$'\n+% git config set core.gitproxy '\"ssh\" for kernel.org' 'for kernel.org$'\n ------------\n \n This makes sure that only the key/value pair for kernel.org is replaced.\n@@ -485,7 +495,7 @@ This makes sure that only the key/value pair for kernel.org is replaced.\n To delete the entry for renames, do\n \n ------------\n-% git config --unset diff.renames\n+% git config unset diff.renames\n ------------\n \n If you want to delete an entry for a multivar (like core.gitproxy above),\n@@ -494,51 +504,51 @@ you have to provide a regex matching the value of exactly one line.\n To query the value for a given key, do\n \n ------------\n-% git config --get core.filemode\n+% git config get core.filemode\n ------------\n \n or\n \n ------------\n-% git config core.filemode\n+% git config get core.filemode\n ------------\n \n or, to query a multivar:\n \n ------------\n-% git config --get core.gitproxy \"for kernel.org$\"\n+% git config get core.gitproxy \"for kernel.org$\"\n ------------\n \n If you want to know all the values for a multivar, do:\n \n ------------\n-% git config --get-all core.gitproxy\n+% git config get-all core.gitproxy\n ------------\n \n If you like to live dangerously, you can replace *all* core.gitproxy by a\n new one with\n \n ------------\n-% git config --replace-all core.gitproxy ssh\n+% git config replace-all core.gitproxy ssh\n ------------\n \n However, if you really only want to replace the line for the default proxy,\n i.e. the one without a \"for ...\" postfix, do something like this:\n \n ------------\n-% git config core.gitproxy ssh '! for '\n+% git config get core.gitproxy ssh '! for '\n ------------\n \n To actually match only values with an exclamation mark, you have to\n \n ------------\n-% git config section.key value '[!]'\n+% git config get section.key value '[!]'\n ------------\n \n To add a new proxy, without altering any of the existing ones, use\n \n ------------\n-% git config --add core.gitproxy '\"proxy-command\" for example.com'\n+% git config add core.gitproxy '\"proxy-command\" for example.com'\n ------------\n \n An example to use customized color from the configuration in your\n@@ -546,8 +556,8 @@ script:\n \n ------------\n #!/bin/sh\n-WS=$(git config --get-color color.diff.whitespace \"blue reverse\")\n-RESET=$(git config --get-color \"\" \"reset\")\n+WS=$(git config get-color color.diff.whitespace \"blue reverse\")\n+RESET=$(git config get-color \"\" \"reset\")\n echo \"${WS}your whitespace color or blue reverse${RESET}\"\n ------------\n \n@@ -555,11 +565,11 @@ For URLs in `https://weak.example.com`, `http.sslVerify` is set to\n false, while it is set to `true` for all others:\n \n ------------\n-% git config --type=bool --get-urlmatch http.sslverify https://good.example.com\n+% git config get-urlmatch --type=bool http.sslverify https://good.example.com\n true\n-% git config --type=bool --get-urlmatch http.sslverify https://weak.example.com\n+% git config get-urlmatch --type=bool http.sslverify https://weak.example.com\n false\n-% git config --get-urlmatch http https://weak.example.com\n+% git config get-urlmatch http https://weak.example.com\n http.cookieFile /tmp/cookie.txt\n http.sslverify false\n ------------\n@@ -578,7 +588,7 @@ looks like\n     key = value1\n --------\n \n-and running `git config section.Subsection.key value2` will result in\n+and running `git config add section.Subsection.key value2` will result in\n \n --------\n   [section.subsection]\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 10fa933931..61a9ad3eaa 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -16,7 +16,22 @@\n #include \"worktree.h\"\n \n static const char *const builtin_config_usage[] = {\n-\tN_(\"git config [<options>]\"),\n+\tN_(\"git config list [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only]\"),\n+\tN_(\"git config get [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] <name> [<value-pattern>]\"),\n+\tN_(\"git config get-all [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] <name> [<value-pattern>]\"),\n+\tN_(\"git config get-color [<file-option>] <name> [<default>]\"),\n+\tN_(\"git config get-colorbool [<file-option>] <name> [<stdout-is-tty>]\"),\n+\tN_(\"git config get-regexp [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] <name-regex> [<value-pattern>]\"),\n+\tN_(\"git config get-urlmatch [<file-option>] [--type=<type>] [-z|--null] <name> <URL>\"),\n+\tN_(\"git config add [<file-option>] [--type=<type>] <name> <value>\"),\n+\tN_(\"git config set [<file-option>] [--type=<type>] <name> <value>\"),\n+\tN_(\"git config set-all [<file-option>] [--type=<type>] [--fixed-value] <name> <value> [<value-pattern>]\"),\n+\tN_(\"git config unset [<file-option>] [--fixed-value] <name> [<value-pattern>]\"),\n+\tN_(\"git config unset-all [<file-option>] [--fixed-value] <name> [<value-pattern>]\"),\n+\tN_(\"git config replace-all [<file-option>] [--type=<type>] [--fixed-value] <name> <value> [<value-pattern>]\"),\n+\tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n+\tN_(\"git config remove-section [<file-option>] <name>\"),\n+\tN_(\"git config edit [<file-option>]\"),\n \tNULL\n };\n \n@@ -887,21 +902,20 @@ static struct option builtin_config_options[] = {\n \tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n-\tOPT_GROUP(N_(\"Action\")),\n-\tOPT_CMDMODE(0, \"get\", &action, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_CMDMODE(0, \"get-all\", &action, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_CMDMODE(0, \"get-regexp\", &action, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_CMDMODE(0, \"get-urlmatch\", &action, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_CMDMODE(0, \"replace-all\", &action, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_CMDMODE(0, \"add\", &action, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_CMDMODE(0, \"unset\", &action, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_CMDMODE(0, \"unset-all\", &action, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_CMDMODE(0, \"rename-section\", &action, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_CMDMODE(0, \"remove-section\", &action, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_CMDMODE('l', \"list\", &action, N_(\"list all\"), ACTION_LIST),\n-\tOPT_CMDMODE('e', \"edit\", &action, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_CMDMODE(0, \"get-color\", &action, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_CMDMODE(0, \"get-colorbool\", &action, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_CMDMODE_F(0, \"get\", &action, N_(\"get value: name [value-pattern]\"), ACTION_GET, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"get-all\", &action, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"get-regexp\", &action, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"get-urlmatch\", &action, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"replace-all\", &action, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"add\", &action, N_(\"add a new variable: name value\"), ACTION_ADD, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"unset\", &action, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"unset-all\", &action, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"rename-section\", &action, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"remove-section\", &action, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F('l', \"list\", &action, N_(\"list all\"), ACTION_LIST, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F('e', \"edit\", &action, N_(\"open an editor\"), ACTION_EDIT, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"get-color\", &action, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"get-colorbool\", &action, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL, PARSE_OPT_HIDDEN),\n \tOPT_GROUP(N_(\"Type\")),\n \tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n \tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\ndiff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches\nindex a0777acd66..28003f18c9 100644\n--- a/t/t0450/txt-help-mismatches\n+++ b/t/t0450/txt-help-mismatches\n@@ -10,7 +10,6 @@ checkout\n checkout-index\n clone\n column\n-config\n credential\n credential-cache\n credential-store\n-- \n2.44.0\n\n"},{"id":"490101","messageId":"xmqqil1zs327.fsf@gitster.g","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"Re: [PATCH 0/8] builtin/config: introduce subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-06T17:06:08Z","receivedAt":"2024-03-06T17:06:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>   - `git config foo.bar` -> `git config get foo.bar`\n>\n>   - `git config foo.bar value` -> `git config set foo.bar value`\n\nI actually have been perfectly OK with the above two, but I agree\nthat ...\n\n>   - `git config foo.bar value value-pattern` -> `git config set-all\n>     foo.bar value value-pattern`\n\n... this was less than discoverable, and would be a good update.\nThis one ...\n\n>   - `git config --get-urlmatch` -> `git config get-urlmatch`.\n\n... is a Meh to me, personally.  I'd not actively push it\nenthusiastically, but I'd passively accept its existence.\n\n> Most importantly, this should help discoverability quite a lot by now\n> also having a proper synopsis in both the manpage, but also in `git\n> config -h`.\n>\n> Of course, backwards compatibility is a big concern. We don't want to\n> just switch over to the new syntax and break all existing scripts and\n> muscle memory. This patch series thus abuses the fact that the implicit\n> modes (`git config foo.bar`, `git config foo.bar value` and `git config\n> foo.bar value value-pattern`) all require a key as first argument. As\n> keys _must_ have a dot, this allows us to unambiguously discern those\n> from actual subcommands.\n\nClever ;-).\n"},{"id":"490115","messageId":"CAOLa=ZSg35atoTsBVtWj_j94n2Mt8otzfw3DYLYS9FCbNgp_Xg@mail.gmail.com","threadId":"61061","inReplyTo":"f79c0f94e415de5c1c7b4120af5270fe4900d825.1709724089.git.ps@pks.im","subject":"Re: [PATCH 6/8] builtin/config: introduce subcommands","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-03-06T21:38:25Z","receivedAt":"2024-03-06T21:38:27Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> @@ -910,6 +930,20 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n>  {\n>  \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n>\n> +\t/*\n> +\t * This is somewhat hacky: we first parse the command line while\n> +\t * keeping all args intact in order to determine whether a subcommand\n> +\t * has been specified. If so, we re-parse it a second time, but this\n> +\t * time we drop KEEP_ARGV0. This is so that we don't munge the command\n> +\t * line in case no subcommand was given, which would otherwise confuse\n> +\t * us when parsing the implicit modes.\n> +\t */\n> +\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n> +\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n> +\tif (subcommand)\n> +\t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n> +\t\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);\n> +\n\nI wonder if we can drop the PARSE_OPT_SUBCOMMAND_OPTIONAL in the second\niteration to make it stricter. But this is OK.\n"},{"id":"490118","messageId":"6044285.lOV4Wx5bFT@cayenne","threadId":"61061","inReplyTo":"e39774d6495767c6505999bdc33110678139e347.1709724089.git.ps@pks.im","subject":"Re: [PATCH 5/8] builtin/config: track subcommands by action","fromName":"Jean-Noël AVILA","fromEmail":"jn.avila@free.fr","sentAt":"2024-03-06T21:54:01Z","receivedAt":"2024-03-06T21:54:17Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"Le mercredi 6 mars 2024, 12:31:50 CET Patrick Steinhardt a écrit :\n> Part of `cmd_config()` is a rather unwieldy switch statement that\n> invokes the correct subcommand function based on which action has been\n> requested by the user. Now that we have converted actions to be tracked\n> via a `OPT_CMDMODE()`, we know that the `actions` variable will\n> only ever have at most one bit set. This allows us to convert the\n> variable to use an `enum` instead, and thus to create an array that maps\n> from this newly introduced `enum` to the corresponding subcommand\n> function.\n> \n> Refactor the code to do so. Besides allowing us to get rid of the giant\n> switch statement, this refactoring will also make it easier to introduce\n> proper subcommands to git-config(1).\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/config.c | 207 +++++++++++++++++++++++------------------------\n>  1 file changed, 99 insertions(+), 108 deletions(-)\n> \n> diff --git a/builtin/config.c b/builtin/config.c\n> index a6ab9b8204..0d58397ef5 100644\n> --- a/builtin/config.c\n> +++ b/builtin/config.c\n> @@ -20,6 +20,26 @@ static const char *const builtin_config_usage[] = {\n>  \tNULL\n>  };\n>  \n> +enum config_action {\n> +\tACTION_NONE,\n> +\tACTION_GET,\n> +\tACTION_GET_ALL,\n> +\tACTION_GET_REGEXP,\n> +\tACTION_REPLACE_ALL,\n> +\tACTION_ADD,\n> +\tACTION_UNSET,\n> +\tACTION_UNSET_ALL,\n> +\tACTION_RENAME_SECTION,\n> +\tACTION_REMOVE_SECTION,\n> +\tACTION_LIST,\n> +\tACTION_EDIT,\n> +\tACTION_SET,\n> +\tACTION_SET_ALL,\n> +\tACTION_GET_COLOR,\n> +\tACTION_GET_COLORBOOL,\n> +\tACTION_GET_URLMATCH,\n> +};\n> +\n>  static char *key;\n>  static regex_t *key_regexp;\n>  static const char *value_pattern;\n> @@ -33,10 +53,12 @@ static char delim = '=';\n>  static char key_delim = ' ';\n>  static char term = '\\n';\n>  \n> +static parse_opt_subcommand_fn *subcommand;\n> +static enum config_action action = ACTION_NONE;\n>  static int use_global_config, use_system_config, use_local_config;\n>  static int use_worktree_config;\n>  static struct git_config_source given_config_source;\n> -static int actions, type;\n> +static int type;\n>  static char *default_value;\n>  static int end_nul;\n>  static int respect_includes_opt = -1;\n> @@ -46,30 +68,6 @@ static int show_scope;\n>  static int fixed_value;\n>  static int config_flags;\n>  \n> -#define ACTION_GET (1<<0)\n> -#define ACTION_GET_ALL (1<<1)\n> -#define ACTION_GET_REGEXP (1<<2)\n> -#define ACTION_REPLACE_ALL (1<<3)\n> -#define ACTION_ADD (1<<4)\n> -#define ACTION_UNSET (1<<5)\n> -#define ACTION_UNSET_ALL (1<<6)\n> -#define ACTION_RENAME_SECTION (1<<7)\n> -#define ACTION_REMOVE_SECTION (1<<8)\n> -#define ACTION_LIST (1<<9)\n> -#define ACTION_EDIT (1<<10)\n> -#define ACTION_SET (1<<11)\n> -#define ACTION_SET_ALL (1<<12)\n> -#define ACTION_GET_COLOR (1<<13)\n> -#define ACTION_GET_COLORBOOL (1<<14)\n> -#define ACTION_GET_URLMATCH (1<<15)\n> -\n> -/*\n> - * The actions \"ACTION_LIST | ACTION_GET_*\" which may produce more than\n> - * one line of output and which should therefore be paged.\n> - */\n> -#define PAGING_ACTIONS (ACTION_LIST | ACTION_GET_ALL | \\\n> -\t\t\tACTION_GET_REGEXP | ACTION_GET_URLMATCH)\n> -\n>  #define TYPE_BOOL\t\t1\n>  #define TYPE_INT\t\t2\n>  #define TYPE_BOOL_OR_INT\t3\n> @@ -842,6 +840,25 @@ static int cmd_config_get_colorbool(int argc, const char **argv, const char *pre\n>  \treturn get_colorbool(argv[0], argc == 2);\n>  }\n>  \n> +static parse_opt_subcommand_fn *subcommands_by_action[] = {\n> +\t[ACTION_LIST] = cmd_config_list,\n> +\t[ACTION_EDIT] = cmd_config_edit,\n> +\t[ACTION_SET] = cmd_config_set,\n> +\t[ACTION_SET_ALL] = cmd_config_set_all,\n> +\t[ACTION_ADD] = cmd_config_add,\n> +\t[ACTION_REPLACE_ALL] = cmd_config_replace_all,\n> +\t[ACTION_GET] = cmd_config_get,\n> +\t[ACTION_GET_ALL] = cmd_config_get_all,\n> +\t[ACTION_GET_REGEXP] = cmd_config_get_regexp,\n> +\t[ACTION_GET_URLMATCH] = cmd_config_get_urlmatch,\n> +\t[ACTION_UNSET] = cmd_config_unset,\n> +\t[ACTION_UNSET_ALL] = cmd_config_unset_all,\n> +\t[ACTION_RENAME_SECTION] = cmd_config_rename_section,\n> +\t[ACTION_REMOVE_SECTION] = cmd_config_remove_section,\n> +\t[ACTION_GET_COLOR] = cmd_config_get_color,\n> +\t[ACTION_GET_COLORBOOL] = cmd_config_get_colorbool,\n> +};\n> +\n>  static struct option builtin_config_options[] = {\n>  \tOPT_GROUP(N_(\"Config file location\")),\n>  \tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n> @@ -851,20 +868,20 @@ static struct option builtin_config_options[] = {\n>  \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n>  \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n>  \tOPT_GROUP(N_(\"Action\")),\n> -\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n> -\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n> -\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n> -\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n> -\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n> -\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n> -\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n> -\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n> -\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n> -\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n> -\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n> -\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n> -\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n> -\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n> +\tOPT_CMDMODE(0, \"get\", &action, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n\nI guess value-pattern is a placeholder. So for uniformity with the style guidelines, it should be formatted  <value-pattern>.\nThis also applies to other placeholders below.\n\n> +\tOPT_CMDMODE(0, \"get-all\", &action, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n> +\tOPT_CMDMODE(0, \"get-regexp\", &action, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n> +\tOPT_CMDMODE(0, \"get-urlmatch\", &action, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n> +\tOPT_CMDMODE(0, \"replace-all\", &action, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n> +\tOPT_CMDMODE(0, \"add\", &action, N_(\"add a new variable: name value\"), ACTION_ADD),\n> +\tOPT_CMDMODE(0, \"unset\", &action, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n> +\tOPT_CMDMODE(0, \"unset-all\", &action, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n> +\tOPT_CMDMODE(0, \"rename-section\", &action, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n> +\tOPT_CMDMODE(0, \"remove-section\", &action, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n> +\tOPT_CMDMODE('l', \"list\", &action, N_(\"list all\"), ACTION_LIST),\n> +\tOPT_CMDMODE('e', \"edit\", &action, N_(\"open an editor\"), ACTION_EDIT),\n> +\tOPT_CMDMODE(0, \"get-color\", &action, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n> +\tOPT_CMDMODE(0, \"get-colorbool\", &action, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n>  \tOPT_GROUP(N_(\"Type\")),\n>  \tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n>  \tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n> @@ -976,33 +993,43 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n>  \t\tkey_delim = '\\n';\n>  \t}\n>  \n\n\n\n\n"},{"id":"490120","messageId":"6d01b506-f3ce-4485-b9b1-c5d3ac976c00@app.fastmail.com","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"Re: [PATCH 0/8] builtin/config: introduce subcommands","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-03-06T22:49:38Z","receivedAt":"2024-03-06T22:50:00Z","isPatch":true,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"Wow, this is great. I’ve always found git-config a bit hard to use.\n\nThanks for this.\n\n-- \nKristoffer Haugsbakk\n"},{"id":"490123","messageId":"ZekAWSqr9qb8FIAD@nand.local","threadId":"61061","inReplyTo":"xmqqil1zs327.fsf@gitster.g","subject":"Re: [PATCH 0/8] builtin/config: introduce subcommands","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-03-06T23:46:33Z","receivedAt":"2024-03-06T23:46:35Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Wed, Mar 06, 2024 at 09:06:08AM -0800, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n> >   - `git config foo.bar` -> `git config get foo.bar`\n> >\n> >   - `git config foo.bar value` -> `git config set foo.bar value`\n>\n> I actually have been perfectly OK with the above two, but I agree\n> that ...\n\nSame here, though I think that we are probably both biased by many years\nof familiarity with the existing syntax.\n\n> >   - `git config foo.bar value value-pattern` -> `git config set-all\n> >     foo.bar value value-pattern`\n>\n> ... this was less than discoverable, and would be a good update.\n> This one ...\n\nAgreed.\n\n> >   - `git config --get-urlmatch` -> `git config get-urlmatch`.\n>\n> ... is a Meh to me, personally.  I'd not actively push it\n> enthusiastically, but I'd passively accept its existence.\n\nI don't have strong feelings about this, but I wonder if `--urlmatch`\n(or `--url-match`) might be an argument to the \"get\" mode of this\nsub-command instead. Something like `git config get --urlmatch` feels\nmuch more natural to me than `git config get-urlmatch`.\n\nThanks,\nTaylor\n"},{"id":"490125","messageId":"xmqq1q8mq5oo.fsf@gitster.g","threadId":"61061","inReplyTo":"ZekAWSqr9qb8FIAD@nand.local","subject":"Re: [PATCH 0/8] builtin/config: introduce subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-06T23:52:23Z","receivedAt":"2024-03-06T23:52:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> I don't have strong feelings about this, but I wonder if `--urlmatch`\n> (or `--url-match`) might be an argument to the \"get\" mode of this\n> sub-command instead. Something like `git config get --urlmatch` feels\n> much more natural to me than `git config get-urlmatch`.\n\nI like that, too.\n\n\"--get-regexp\" may also be a good candidate to be folded into the\nbase verb \"get\", with \"--regexp\" option to tweak what kind of key is\nused.\n\nCould \"--get-color\" and \"--get-colorbool\" become verb \"get\" with\n\"--type=color\"?  The other parameters they get are somewhat\ndifferent from \"get\", so that may not work very well, I guess.\n"},{"id":"490126","messageId":"ZekBznq74P3QVsSC@nand.local","threadId":"61061","inReplyTo":"41e5bf1d6aa35a32f961b7f9d82a70781674eed0.1709724089.git.ps@pks.im","subject":"Re: [PATCH 3/8] builtin/config: use `OPT_CMDMODE()` to specify modes","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-03-06T23:52:46Z","receivedAt":"2024-03-06T23:52:48Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Wed, Mar 06, 2024 at 12:31:42PM +0100, Patrick Steinhardt wrote:\n> The git-config(1) command has various different modes which are\n> accessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are\n> declared with `OPT_BIT()`, which causes two minor issues:\n>\n>   - The respective modes also have a negated form `--no-get-urlmatch`,\n>     which is unintended.\n>\n>   - We have to manually handle exclusiveness of the modes.\n>\n> Switch these options to instead use `OPT_CMDMODE()`, which is made\n> exactly for this usecase. Remove the now-unneeded check that only a\n> single mode is given, which is now handled by the parse-options\n> interface.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n\n> +\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n> +\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n> +\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n> +\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n\nExpanding a little on my reply to Junio later on in the thread, I\nsuspect that these would translate into \"get\", \"get --all\", \"get\n--regexp\", and \"get --urlmatch\", respectively.\n\n> +\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n> +\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n> +\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n> +\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n\nSame with this one turning into \"unset --all\".\n\n> +\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n> +\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n> +\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n> +\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n> +\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n> +\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n\nThese two could probably join the other \"get-xyz\" modes, and similarly\nbecome \"get --color\", and \"get --colorbool\", respectively. It looks like\nthat's where they lived originally... perhaps I'm missing something as\nto why they were moved.\n\n>  \tOPT_GROUP(N_(\"Type\")),\n>  \tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n>  \tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n> @@ -767,10 +767,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n>  \t\tusage_builtin_config();\n>  \t}\n>\n> -\tif (HAS_MULTI_BITS(actions)) {\n> -\t\terror(_(\"only one action at a time\"));\n> -\t\tusage_builtin_config();\n> -\t}\n\nIf you apply just this hunk, I would have expected it to break t1300,\nbut it doesn't appear to. In fact, it looks like the test suite doesn't\nseem to mind, either, which indicates a gap in our test coverage...\n\n> diff --git a/t/t1300-config.sh b/t/t1300-config.sh\n> index 31c3878687..2d1bc1e27e 100755\n> --- a/t/t1300-config.sh\n> +++ b/t/t1300-config.sh\n> @@ -2612,4 +2612,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such\n>  \tgrep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n>  '\n>\n> +test_expect_success 'negated mode causes failure' '\n> +\ttest_must_fail git config --no-get 2>err &&\n> +\tgrep \"unknown option \\`no-get${SQ}\" err\n> +'\n> +\n> +test_expect_success 'specifying multiple modes causes failure' '\n> +\tcat >expect <<-EOF &&\n> +\terror: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together\n> +\tEOF\n> +\ttest_must_fail git config --get --get-all 2>err &&\n> +\ttest_cmp expect err\n> +'\n\nBut this gap is rectified by this hunk here. Thank you!\n\nThanks,\nTaylor\n"},{"id":"490127","messageId":"ZekF4aqq+NUf52go@nand.local","threadId":"61061","inReplyTo":"e39774d6495767c6505999bdc33110678139e347.1709724089.git.ps@pks.im","subject":"Re: [PATCH 5/8] builtin/config: track subcommands by action","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-03-07T00:10:09Z","receivedAt":"2024-03-07T00:10:11Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Wed, Mar 06, 2024 at 12:31:50PM +0100, Patrick Steinhardt wrote:\n> ---\n>  builtin/config.c | 207 +++++++++++++++++++++++------------------------\n>  1 file changed, 99 insertions(+), 108 deletions(-)\n\nThis is definitely easier (for me, at least) to view with:\n\n    $ git show --color-moved --color-moved-ws=ignore-all-space\n\nwhich makes clearer that this change does not introduce or change any\nexisting behavior.\n\n>  static struct option builtin_config_options[] = {\n>  \tOPT_GROUP(N_(\"Config file location\")),\n>  \tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n> @@ -851,20 +868,20 @@ static struct option builtin_config_options[] = {\n>  \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n>  \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n>  \tOPT_GROUP(N_(\"Action\")),\n> -\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n> -\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n> -\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n> [...]\n\nGot it, this whole hunk is changing \"&actions\" to \"&action\", and nothing\nelse.\n\n> @@ -976,33 +993,43 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n>  \t\tkey_delim = '\\n';\n>  \t}\n>\n> -\tif ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {\n> -\t\terror(_(\"--get-color and variable type are incoherent\"));\n> -\t\tusage_builtin_config();\n> -\t}\n> -\n> -\tif (actions == 0)\n> +\tif (action == ACTION_NONE) {\n>  \t\tswitch (argc) {\n> -\t\tcase 1: actions = ACTION_GET; break;\n> -\t\tcase 2: actions = ACTION_SET; break;\n> -\t\tcase 3: actions = ACTION_SET_ALL; break;\n> +\t\tcase 1: action = ACTION_GET; break;\n> +\t\tcase 2: action = ACTION_SET; break;\n> +\t\tcase 3: action = ACTION_SET_ALL; break;\n\nWondering aloud a little bit... is it safe to set us to \"ACTION_SET\",\nfor example, if we have exactly two arguments? On the one hand, it seems\nlike the answer is yes. But on the other hand, if we were invoked as:\n\n    $ git config ste foo\n\nWe would get something like:\n\n    $ git config ste foo\n    error: key does not contain a section: ste\n\nwhich is sensible. It would be nice to say something more along the\nlines of \"oops, you probably meant 'set' instead of 'ste'\". I think you\ncould claim that the error on the user's part is one of:\n\n  - (using the new style 'git config') misspelling \"set\" as \"ste\", and\n    thus trying to invoke a non-existent sub-command\n\n  - (using the old style) trying to set the key \"ste\", which does not\n    contain a section name, and thus is not a valid configuration key\n\nI have no idea which is more likely, and I think that there isn't a good\nway to distinguish between the two at all. So I think the error message\nis OK as-is, but let me know if you have other thoughts.\n\n>  \t\tdefault:\n>  \t\t\tusage_builtin_config();\n>  \t\t}\n> +\t}\n> +\tif (action <= ACTION_NONE || action >= ARRAY_SIZE(subcommands_by_action))\n> +\t\tBUG(\"invalid action %d\", action);\n> +\tsubcommand = subcommands_by_action[action];\n> +\n> +\tif (type && (subcommand == cmd_config_get_color ||\n> +\t\t     subcommand == cmd_config_get_colorbool)) {\nI don't think there's anything wrong with using the function-pointer\nequality here to figure out which subcommand we're in, but I wonder if\nit might be cleaner to write this as:\n\n    if (type && (action == ACTION_GET_COLOR ||\n                 action == ACTION_GET_COLORBOOL)) {\n        ...\n\n> -\tif (actions == ACTION_LIST) {\n> -\t\treturn cmd_config_list(argc, argv, prefix);\n> -\t} else if (actions == ACTION_EDIT) {\n> -\t\treturn cmd_config_edit(argc, argv, prefix);\n> -\t} else if (actions == ACTION_SET) {\n> -\t\treturn cmd_config_set(argc, argv, prefix);\n> -\t} else if (actions == ACTION_SET_ALL) {\n> -\t\treturn cmd_config_set_all(argc, argv, prefix);\n> -\t} else if (actions == ACTION_ADD) {\n> -\t\treturn cmd_config_add(argc, argv, prefix);\n> -\t} else if (actions == ACTION_REPLACE_ALL) {\n> -\t\treturn cmd_config_replace_all(argc, argv, prefix);\n> -\t} else if (actions == ACTION_GET) {\n> -\t\treturn cmd_config_get(argc, argv, prefix);\n> -\t} else if (actions == ACTION_GET_ALL) {\n> -\t\treturn cmd_config_get_all(argc, argv, prefix);\n> -\t} else if (actions == ACTION_GET_REGEXP) {\n> -\t\treturn cmd_config_get_regexp(argc, argv, prefix);\n> -\t} else if (actions == ACTION_GET_URLMATCH) {\n> -\t\treturn cmd_config_get_urlmatch(argc, argv, prefix);\n> -\t} else if (actions == ACTION_UNSET) {\n> -\t\treturn cmd_config_unset(argc, argv, prefix);\n> -\t} else if (actions == ACTION_UNSET_ALL) {\n> -\t\treturn cmd_config_unset_all(argc, argv, prefix);\n> -\t} else if (actions == ACTION_RENAME_SECTION) {\n> -\t\treturn cmd_config_rename_section(argc, argv, prefix);\n> -\t} else if (actions == ACTION_REMOVE_SECTION) {\n> -\t\treturn cmd_config_remove_section(argc, argv, prefix);\n> -\t} else if (actions == ACTION_GET_COLOR) {\n> -\t\treturn cmd_config_get_color(argc, argv, prefix);\n> -\t} else if (actions == ACTION_GET_COLORBOOL) {\n> -\t\treturn cmd_config_get_colorbool(argc, argv, prefix);\n> -\t}\n> -\n> -\tBUG(\"invalid action\");\n> +\treturn subcommand(argc, argv, prefix);\n\nVery nice.\n\nThanks,\nTaylor\n"},{"id":"490128","messageId":"ZekGjDD6TjF0FYXy@nand.local","threadId":"61061","inReplyTo":"xmqq1q8mq5oo.fsf@gitster.g","subject":"Re: [PATCH 0/8] builtin/config: introduce subcommands","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-03-07T00:13:00Z","receivedAt":"2024-03-07T00:13:03Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Wed, Mar 06, 2024 at 03:52:23PM -0800, Junio C Hamano wrote:\n> Taylor Blau <me@ttaylorr.com> writes:\n>\n> > I don't have strong feelings about this, but I wonder if `--urlmatch`\n> > (or `--url-match`) might be an argument to the \"get\" mode of this\n> > sub-command instead. Something like `git config get --urlmatch` feels\n> > much more natural to me than `git config get-urlmatch`.\n>\n> I like that, too.\n>\n> \"--get-regexp\" may also be a good candidate to be folded into the\n> base verb \"get\", with \"--regexp\" option to tweak what kind of key is\n> used.\n\nYes, definitely.\n\n> Could \"--get-color\" and \"--get-colorbool\" become verb \"get\" with\n> \"--type=color\"?  The other parameters they get are somewhat\n> different from \"get\", so that may not work very well, I guess.\n\nI'm somewhat embarrassed to not have a better suggestion, since I\nvaguely remember working on the `--type` option as one of my first\ncontributions to the project.\n\nIndeed: fb0dc3bac13 (builtin/config.c: support `--type=<type>` as\npreferred alias for `--<type>`, 2018-04-18). ;-).\n\nThanks,\nTaylor\n"},{"id":"490131","messageId":"d3ccbb84bf3b6074d25668b2d0cd1b94@manjaro.org","threadId":"61061","inReplyTo":"ZekAWSqr9qb8FIAD@nand.local","subject":"Re: [PATCH 0/8] builtin/config: introduce subcommands","fromName":"Dragan Simic","fromEmail":"dsimic@manjaro.org","sentAt":"2024-03-07T00:31:09Z","receivedAt":"2024-03-07T00:31:18Z","isPatch":true,"sender":{"key":"dsimic@manjaro.org","avatar":null},"body":"Hello all,\n\nOn 2024-03-07 00:46, Taylor Blau wrote:\n> On Wed, Mar 06, 2024 at 09:06:08AM -0800, Junio C Hamano wrote:\n>> Patrick Steinhardt <ps@pks.im> writes:\n>> \n>> >   - `git config foo.bar` -> `git config get foo.bar`\n>> >\n>> >   - `git config foo.bar value` -> `git config set foo.bar value`\n>> \n>> I actually have been perfectly OK with the above two, but I agree\n>> that ...\n> \n> Same here, though I think that we are probably both biased by many \n> years\n> of familiarity with the existing syntax.\n\nDitto.  Though, having \"get\" and \"set\" commands will be nice, making it\nall more self descriptive.\n\n>> >   - `git config foo.bar value value-pattern` -> `git config set-all\n>> >     foo.bar value value-pattern`\n>> \n>> ... this was less than discoverable, and would be a good update.\n>> This one ...\n> \n> Agreed.\n\nAlso agreed.  Having the point below in mind, perhaps we could actually\nend up with \"set --all\" instead of \"set-all\".\n\n>> >   - `git config --get-urlmatch` -> `git config get-urlmatch`.\n>> \n>> ... is a Meh to me, personally.  I'd not actively push it\n>> enthusiastically, but I'd passively accept its existence.\n> \n> I don't have strong feelings about this, but I wonder if `--urlmatch`\n> (or `--url-match`) might be an argument to the \"get\" mode of this\n> sub-command instead. Something like `git config get --urlmatch` feels\n> much more natural to me than `git config get-urlmatch`.\n\nGood point.  I'd vote for having \"get --urlmatch\" or \"get --url-match\",\nbecause it feels more natural to me, it doesn't \"clog up\" the command\nspace, and such an approach, in general, allows git-config(1) to be\nexpanded later easier with more new arguments for the existing commands.\n"},{"id":"490136","messageId":"ZelfOQ-XvvOpaOzi@tanuki","threadId":"61061","inReplyTo":"ZekAWSqr9qb8FIAD@nand.local","subject":"Re: [PATCH 0/8] builtin/config: introduce subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-07T06:31:21Z","receivedAt":"2024-03-07T06:31:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Mar 06, 2024 at 06:46:33PM -0500, Taylor Blau wrote:\n> On Wed, Mar 06, 2024 at 09:06:08AM -0800, Junio C Hamano wrote:\n> > Patrick Steinhardt <ps@pks.im> writes:\n> > >   - `git config --get-urlmatch` -> `git config get-urlmatch`.\n> >\n> > ... is a Meh to me, personally.  I'd not actively push it\n> > enthusiastically, but I'd passively accept its existence.\n> \n> I don't have strong feelings about this, but I wonder if `--urlmatch`\n> (or `--url-match`) might be an argument to the \"get\" mode of this\n> sub-command instead. Something like `git config get --urlmatch` feels\n> much more natural to me than `git config get-urlmatch`.\n\nAgreed. I allude to this somewhere in the patch series that I only see\nthis as a first incremental step, and that some of the subcommands\nreally should be converted to become options eventually. Specifically:\n\n    - `git config get-urlmatch` -> `git config get --urlmatch`\n\n    - `git config get-regexp` -> `git config get --regexp`\n\n    - `git config get-all` -> `git config get --all`\n \n    - `git config set-all` -> `git config set --all`\n\n    - `git config unset-all` -> `git config unset --all`\n\nI didn't yet do it as part of this patch series because I didn't want to\nmake functional changes at the same time (well, except of course for the\nintroduction of subcommands). But if we all agree that this patch series\nis something we want _and_ that we don't want to have an intermediate\nstep with the above somewhat weird subcommands then I would be happy to\npile onto the series.\n\nI wouldn't think that keeping this intermediate step would be too bad\nthough. While we would eventually omit the above subcommands, the infra\nto keep them around needs to stay anyway to support old syntax like\n`--get-all`. Thus, we can keep the subcommands themselves almost for\nfree even if we eventually decide to hide them and replace them with\nflags.\n\nPatrick\n"},{"id":"490137","messageId":"Zelgbmol7AL71Jgt@tanuki","threadId":"61061","inReplyTo":"ZekF4aqq+NUf52go@nand.local","subject":"Re: [PATCH 5/8] builtin/config: track subcommands by action","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-07T06:36:30Z","receivedAt":"2024-03-07T06:36:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Mar 06, 2024 at 07:10:09PM -0500, Taylor Blau wrote:\n> On Wed, Mar 06, 2024 at 12:31:50PM +0100, Patrick Steinhardt wrote:\n> > @@ -976,33 +993,43 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n> >  \t\tkey_delim = '\\n';\n> >  \t}\n> >\n> > -\tif ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {\n> > -\t\terror(_(\"--get-color and variable type are incoherent\"));\n> > -\t\tusage_builtin_config();\n> > -\t}\n> > -\n> > -\tif (actions == 0)\n> > +\tif (action == ACTION_NONE) {\n> >  \t\tswitch (argc) {\n> > -\t\tcase 1: actions = ACTION_GET; break;\n> > -\t\tcase 2: actions = ACTION_SET; break;\n> > -\t\tcase 3: actions = ACTION_SET_ALL; break;\n> > +\t\tcase 1: action = ACTION_GET; break;\n> > +\t\tcase 2: action = ACTION_SET; break;\n> > +\t\tcase 3: action = ACTION_SET_ALL; break;\n> \n> Wondering aloud a little bit... is it safe to set us to \"ACTION_SET\",\n> for example, if we have exactly two arguments? On the one hand, it seems\n> like the answer is yes. But on the other hand, if we were invoked as:\n> \n>     $ git config ste foo\n> \n> We would get something like:\n> \n>     $ git config ste foo\n>     error: key does not contain a section: ste\n> \n> which is sensible. It would be nice to say something more along the\n> lines of \"oops, you probably meant 'set' instead of 'ste'\". I think you\n> could claim that the error on the user's part is one of:\n> \n>   - (using the new style 'git config') misspelling \"set\" as \"ste\", and\n>     thus trying to invoke a non-existent sub-command\n> \n>   - (using the old style) trying to set the key \"ste\", which does not\n>     contain a section name, and thus is not a valid configuration key\n> \n> I have no idea which is more likely, and I think that there isn't a good\n> way to distinguish between the two at all. So I think the error message\n> is OK as-is, but let me know if you have other thoughts.\n\nThe transition period will be a tad weird, I agree. I think initially\nI'd prefer to keep the old behaviour just to ensure we don't regress\nanything, but after a couple of releases I think we should revisit this\nand become more aggressive about using the new style.\n\n> >  \t\tdefault:\n> >  \t\t\tusage_builtin_config();\n> >  \t\t}\n> > +\t}\n> > +\tif (action <= ACTION_NONE || action >= ARRAY_SIZE(subcommands_by_action))\n> > +\t\tBUG(\"invalid action %d\", action);\n> > +\tsubcommand = subcommands_by_action[action];\n> > +\n> > +\tif (type && (subcommand == cmd_config_get_color ||\n> > +\t\t     subcommand == cmd_config_get_colorbool)) {\n> I don't think there's anything wrong with using the function-pointer\n> equality here to figure out which subcommand we're in, but I wonder if\n> it might be cleaner to write this as:\n> \n>     if (type && (action == ACTION_GET_COLOR ||\n>                  action == ACTION_GET_COLORBOOL)) {\n>         ...\n\nThe reason I didn't is that we don't always have an action set once we\nsupport subcommands. We can of course figure it out by walking the array\nof functions pointers to find the one corresponding to this action. But\nI figured it's easier to just use function pointers exclusively.\n\nPatrick\n"},{"id":"490138","messageId":"ZelgmZEQDDgeHEaa@tanuki","threadId":"61061","inReplyTo":"6044285.lOV4Wx5bFT@cayenne","subject":"Re: [PATCH 5/8] builtin/config: track subcommands by action","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-07T06:37:13Z","receivedAt":"2024-03-07T06:37:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Mar 06, 2024 at 10:54:01PM +0100, Jean-Noël AVILA wrote:\n> Le mercredi 6 mars 2024, 12:31:50 CET Patrick Steinhardt a écrit :\n[snip]\n> > @@ -851,20 +868,20 @@ static struct option builtin_config_options[] = {\n> >  \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n> >  \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n> >  \tOPT_GROUP(N_(\"Action\")),\n> > -\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n> > -\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n> > -\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n> > -\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n> > -\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n> > -\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n> > -\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n> > -\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n> > -\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n> > -\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n> > -\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n> > -\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n> > -\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n> > -\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n> > +\tOPT_CMDMODE(0, \"get\", &action, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n> \n> I guess value-pattern is a placeholder. So for uniformity with the\n> style guidelines, it should be formatted  <value-pattern>. This also\n> applies to other placeholders below.\n\nAh, indeed, thanks for catching this!\n\nPatrick\n"},{"id":"490139","messageId":"CAPig+cQevQKb8YA1Yf-t=AHg11P7i848gELx2kPc39g5cOWbqw@mail.gmail.com","threadId":"61061","inReplyTo":"5ff2bf4a2286431a3b3048e0ea04d6551006d0d7.1709724089.git.ps@pks.im","subject":"Re: [PATCH 8/8] Documentation/git-config: update to new-style syntax","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2024-03-07T06:57:41Z","receivedAt":"2024-03-07T06:57:53Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Wed, Mar 6, 2024 at 6:32 AM Patrick Steinhardt <ps@pks.im> wrote:\n> Update our documentation of git-config(1) to stop mentioning the old\n> syntax while starting to mention the new syntax. Remove the help\n> mismatch in t0450 so that we start to ensure that the manpage and\n> builtin synopsis match.\n> [...]\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n\nI'd like to push back on the part of this patch which completely\neradicates mention of the old syntax. Doing so makes it difficult for\npeople to figure out the meaning of git-config commands which they run\nacross in blogs and existing tooling. For instance, the following\nrecommendation is commonly encountered in the wild:\n\n    git config --global user.name \"Your Name\"\n    git config --global user.email \"youremail@yourdomain.com\"\n\nTypically, we instead retain the old syntax (or options or whatever)\nin the documentation so that people who want to learn can learn, but\nwe rewrite it to make it clear that it is deprecated, and explicitly\npoint the reader at the modern replacement command or option (or\nwhatever).\n\nSo, perhaps you can have a SYNOPSIS section for the deprecated usage,\nas well as a \"DEPRECATED OPTIONS\" section (or such), so that we don't\nleave readers entirely in the dark.\n"},{"id":"490140","messageId":"ZelmnLhGl4Vpb1Qf@tanuki","threadId":"61061","inReplyTo":"ZekBznq74P3QVsSC@nand.local","subject":"Re: [PATCH 3/8] builtin/config: use `OPT_CMDMODE()` to specify modes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-07T07:02:52Z","receivedAt":"2024-03-07T07:02:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Mar 06, 2024 at 06:52:46PM -0500, Taylor Blau wrote:\n> On Wed, Mar 06, 2024 at 12:31:42PM +0100, Patrick Steinhardt wrote:\n> > The git-config(1) command has various different modes which are\n> > accessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are\n> > declared with `OPT_BIT()`, which causes two minor issues:\n> >\n> >   - The respective modes also have a negated form `--no-get-urlmatch`,\n> >     which is unintended.\n> >\n> >   - We have to manually handle exclusiveness of the modes.\n> >\n> > Switch these options to instead use `OPT_CMDMODE()`, which is made\n> > exactly for this usecase. Remove the now-unneeded check that only a\n> > single mode is given, which is now handled by the parse-options\n> > interface.\n> >\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> \n> > +\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n> > +\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n> > +\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n> > +\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n> \n> Expanding a little on my reply to Junio later on in the thread, I\n> suspect that these would translate into \"get\", \"get --all\", \"get\n> --regexp\", and \"get --urlmatch\", respectively.\n\nYup.\n\n> > +\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n> > +\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n> > +\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n> > +\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n> \n> Same with this one turning into \"unset --all\".\n\nYup.\n\n> > +\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n> > +\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n> > +\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n> > +\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n> > +\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n> > +\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n> \n> These two could probably join the other \"get-xyz\" modes, and similarly\n> become \"get --color\", and \"get --colorbool\", respectively. It looks like\n> that's where they lived originally... perhaps I'm missing something as\n> to why they were moved.\n\nYes, although I think putting it into the `--type` parameter might be\neven better.\n\nI'm still a bit torn on whether we should do this all as part of this\npatch series or as part of a follow-up patch series. But if anybody\nfeels strongly one way or the other I'm happy to adapt accordingly.\n\nPatrick\n"},{"id":"490141","messageId":"ZelpcoIh5dalqrRF@tanuki","threadId":"61061","inReplyTo":"CAOLa=ZSg35atoTsBVtWj_j94n2Mt8otzfw3DYLYS9FCbNgp_Xg@mail.gmail.com","subject":"Re: [PATCH 6/8] builtin/config: introduce subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-07T07:14:58Z","receivedAt":"2024-03-07T07:15:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Mar 06, 2024 at 01:38:25PM -0800, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > @@ -910,6 +930,20 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n> >  {\n> >  \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n> >\n> > +\t/*\n> > +\t * This is somewhat hacky: we first parse the command line while\n> > +\t * keeping all args intact in order to determine whether a subcommand\n> > +\t * has been specified. If so, we re-parse it a second time, but this\n> > +\t * time we drop KEEP_ARGV0. This is so that we don't munge the command\n> > +\t * line in case no subcommand was given, which would otherwise confuse\n> > +\t * us when parsing the implicit modes.\n> > +\t */\n> > +\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n> > +\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n> > +\tif (subcommand)\n> > +\t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n> > +\t\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);\n> > +\n> \n> I wonder if we can drop the PARSE_OPT_SUBCOMMAND_OPTIONAL in the second\n> iteration to make it stricter. But this is OK.\n\nWe can't due to a restriction in the parse-options interface: when\nsubcommands are present, then PARSE_OPT_KEEP_UNKNOWN_OPT requires us to\nalso pass PARSE_OPT_SUBCOMMAND_OPTIONAL. Otherwise we trigger a `BUG()`.\n\nPatrick\n"},{"id":"490142","messageId":"Zelt0F-2ALVoBOfG@tanuki","threadId":"61061","inReplyTo":"CAPig+cQevQKb8YA1Yf-t=AHg11P7i848gELx2kPc39g5cOWbqw@mail.gmail.com","subject":"Re: [PATCH 8/8] Documentation/git-config: update to new-style syntax","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-07T07:33:36Z","receivedAt":"2024-03-07T07:33:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Mar 07, 2024 at 01:57:41AM -0500, Eric Sunshine wrote:\n> On Wed, Mar 6, 2024 at 6:32 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > Update our documentation of git-config(1) to stop mentioning the old\n> > syntax while starting to mention the new syntax. Remove the help\n> > mismatch in t0450 so that we start to ensure that the manpage and\n> > builtin synopsis match.\n> > [...]\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> \n> I'd like to push back on the part of this patch which completely\n> eradicates mention of the old syntax. Doing so makes it difficult for\n> people to figure out the meaning of git-config commands which they run\n> across in blogs and existing tooling. For instance, the following\n> recommendation is commonly encountered in the wild:\n> \n>     git config --global user.name \"Your Name\"\n>     git config --global user.email \"youremail@yourdomain.com\"\n> \n> Typically, we instead retain the old syntax (or options or whatever)\n> in the documentation so that people who want to learn can learn, but\n> we rewrite it to make it clear that it is deprecated, and explicitly\n> point the reader at the modern replacement command or option (or\n> whatever).\n> \n> So, perhaps you can have a SYNOPSIS section for the deprecated usage,\n> as well as a \"DEPRECATED OPTIONS\" section (or such), so that we don't\n> leave readers entirely in the dark.\n\nFair point indeed. I will update this patch accordingly, thanks!\n\nPatrick\n"},{"id":"490180","messageId":"xmqqle6unpmt.fsf@gitster.g","threadId":"61061","inReplyTo":"ZelfOQ-XvvOpaOzi@tanuki","subject":"Re: [PATCH 0/8] builtin/config: introduce subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-03-07T13:22:02Z","receivedAt":"2024-03-07T13:22:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Wed, Mar 06, 2024 at 06:46:33PM -0500, Taylor Blau wrote:\n>> On Wed, Mar 06, 2024 at 09:06:08AM -0800, Junio C Hamano wrote:\n>> > Patrick Steinhardt <ps@pks.im> writes:\n>> > >   - `git config --get-urlmatch` -> `git config get-urlmatch`.\n>> >\n>> > ... is a Meh to me, personally.  I'd not actively push it\n>> > enthusiastically, but I'd passively accept its existence.\n>> \n>> I don't have strong feelings about this, but I wonder if `--urlmatch`\n>> (or `--url-match`) might be an argument to the \"get\" mode of this\n>> sub-command instead. Something like `git config get --urlmatch` feels\n>> much more natural to me than `git config get-urlmatch`.\n>\n> Agreed. I allude to this somewhere in the patch series that I only see\n> this as a first incremental step, and that some of the subcommands\n> really should be converted to become options eventually. Specifically:\n>\n>     - `git config get-urlmatch` -> `git config get --urlmatch`\n>\n>     - `git config get-regexp` -> `git config get --regexp`\n>\n>     - `git config get-all` -> `git config get --all`\n>  \n>     - `git config set-all` -> `git config set --all`\n>\n>     - `git config unset-all` -> `git config unset --all`\n>\n> I didn't yet do it as part of this patch series because I didn't want to\n> make functional changes at the same time (well, except of course for the\n> introduction of subcommands). But if we all agree that this patch series\n> is something we want _and_ that we don't want to have an intermediate\n> step with the above somewhat weird subcommands then I would be happy to\n> pile onto the series.\n>\n> I wouldn't think that keeping this intermediate step would be too bad\n> though. While we would eventually omit the above subcommands, the infra\n> to keep them around needs to stay anyway to support old syntax like\n> `--get-all`. Thus, we can keep the subcommands themselves almost for\n> free even if we eventually decide to hide them and replace them with\n> flags.\n\nThe \"more involved\" changes along the lines Taylor suggested take\nsome thought in designing the end-user experience, so even if you do\nnot initially plan to support them in the implementation, it would\nbe better to know how the final command line should look like before\nstarting, so that we won't surprise users, if anything.\n\nThe trickies I anticipate offhand are:\n\n * The urlmatch thing wants name and URL.  In the new syntax, I\n   think the URL part should become an option parameter to the --url\n   option.  E.g. a request with the current UI\n\n    $ git config --get-urlmatch SECTION.VARIABLE URL\n\n   would become\n\n    $ git config get --url=URL SECTION.VARIABLE\n\n * The color thing takes the default value to be used when\n   unconfigured.  That should also become an option parameter to the\n   --default option.\n\n    $ git config --get-color SECTION.VARIABLE DEFAULT\n\n   would become\n\n    $ git config get --type=color --default=DEFAULT SECTION.VARIABLE\n\n   Similarly for --tty=true/false used in --get-colorbool.\n\n * Now, these extended ones with options introduce possibilities to\n   combine them in ways that weren't possible before.  For example,\n   \"--get\" and \"--get-all\" are distinct, and there is no way to tell\n   \"--get-urlmatch\" to give all values that would match, but it is\n   plausible that we may want to handle\n\n    $ git config get --all --url=URL SECTION.VARIABLE\n\n   to do so.  A more interesting one is the --default=DEFAULT\n   option.  What we currently write\n\n    $ git config --get no.such.variable || echo 'default value'\n\n   can become\n\n    $ git config get --default='default value' no.such.variable\n\nHTH.\n\n"},{"id":"490416","messageId":"cover.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH v2 00/13] builtin/config: introduce subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:19:27Z","receivedAt":"2024-03-11T23:19:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the second version of my patch series that aims to add\nsubcommands to git-config(1) in a backwards-compatible manner. \n\nThis series is mostly a rewrite compared to v1, so I won't go into all\nthe details. Most importantly:\n\n  - I have replaced several subcommands by option. For example, `git\n    config get-all` is now `git config get --all` and `git config add`\n    is `git config set --append`.\n\n  - Subcommands now perform their own parsing. This makes the code a\n    whole lot more easy to reason about and ensures that there are no\n    backwards incompatible changes to the old modes. This comes at the\n    cost of additional code though.\n\n  - The documentation has been adapted to mention the old syntax in a\n    \"DEPRECATED SYNTAX\" section. It also lists how to convert old style\n    syntax to new style syntax.\n\nAs I expect there to be some more discussion and thus some more versions\nof this series I stopped short of some things:\n\n  - Tests exist for most of the parts, but some modes aren't yet\n    properly tested. This is mostly due to incompatibilities between old\n    and new style syntax which make it hard to just run the same tests\n    twice with some prefixes.\n\n  - The `--get-colorbool` option hasn't been converted yet. I _think_ we\n    might be able to introduce a new `--type=colorbool` option, but I'm\n    not sure I fully grasp the usecase of this command.\n\nThanks for all the feedback so far!\n\nPatrick\n\nPatrick Steinhardt (13):\n  builtin/config: move option array around\n  builtin/config: move \"fixed-value\" option to correct group\n  builtin/config: use `OPT_CMDMODE()` to specify modes\n  builtin/config: pull out function to handle config location\n  builtin/config: pull out function to handle `--null`\n  builtin/config: introduce \"list\" subcommand\n  builtin/config: introduce \"get\" subcommand\n  builtin/config: introduce \"set\" subcommand\n  builtin/config: introduce \"unset\" subcommand\n  builtin/config: introduce \"rename-section\" subcommand\n  builtin/config: introduce \"remove-section\" subcommand\n  builtin/config: introduce \"edit\" subcommand\n  builtin/config: display subcommand help\n\n Documentation/git-config.txt | 213 +++++++++------\n builtin/config.c             | 496 ++++++++++++++++++++++++++++-------\n t/t0450/txt-help-mismatches  |   1 -\n t/t1300-config.sh            | 417 ++++++++++++++++++-----------\n 4 files changed, 786 insertions(+), 341 deletions(-)\n\n\nbase-commit: e09f1254c54329773904fe25d7c545a1fb4fa920\n-- \n2.44.0\n\n"},{"id":"490417","messageId":"1f21e7fa0d27f013fcffd5285ff36bf8a281ea2d.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 01/13] builtin/config: move option array around","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:19:44Z","receivedAt":"2024-03-11T23:19:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move around the option array. This will help us with a follow-up commit\nthat introduces subcommands to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 94 ++++++++++++++++++++++++------------------------\n 1 file changed, 47 insertions(+), 47 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex b55bfae7d6..6eb6aff917 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -134,53 +134,6 @@ static int option_parse_type(const struct option *opt, const char *arg,\n \treturn 0;\n }\n \n-static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n-\tOPT_GROUP(N_(\"Action\")),\n-\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n-\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n-\tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n-\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_END(),\n-};\n-\n-static NORETURN void usage_builtin_config(void)\n-{\n-\tusage_with_options(builtin_config_usage, builtin_config_options);\n-}\n-\n static void check_argc(int argc, int min, int max)\n {\n \tif (argc >= min && argc <= max)\n@@ -669,6 +622,53 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n+static struct option builtin_config_options[] = {\n+\tOPT_GROUP(N_(\"Config file location\")),\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tOPT_GROUP(N_(\"Action\")),\n+\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n+\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n+\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n+\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n+\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n+\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n+\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n+\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_GROUP(N_(\"Type\")),\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tOPT_GROUP(N_(\"Other\")),\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n+\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_END(),\n+};\n+\n+static NORETURN void usage_builtin_config(void)\n+{\n+\tusage_with_options(builtin_config_usage, builtin_config_options);\n+}\n+\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tint nongit = !startup_info->have_repository;\n-- \n2.44.0\n\n"},{"id":"490418","messageId":"54e536394a0d88449c327b47ebbc6943a34001b8.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 02/13] builtin/config: move \"fixed-value\" option to correct group","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:19:54Z","receivedAt":"2024-03-11T23:20:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `--fixed-value` option can be used to alter how the value-pattern\nparameter is interpreted for the various submodes of git-config(1). But\nwhile it is an option, it is currently listed as part of the submodes\ngroup the command, which is wrong.\n\nMove the option to the \"Other\" group, which hosts the various options\nknown to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 6eb6aff917..fcd6190f12 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -642,7 +642,6 @@ static struct option builtin_config_options[] = {\n \tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n \tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n \tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n \tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n \tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n \tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n@@ -661,6 +660,7 @@ static struct option builtin_config_options[] = {\n \tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n \tOPT_END(),\n };\n \n-- \n2.44.0\n\n"},{"id":"490419","messageId":"49d190ac2456bf3823885f9e8877c307dc4a12bd.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 03/13] builtin/config: use `OPT_CMDMODE()` to specify modes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:20:04Z","receivedAt":"2024-03-11T23:20:12Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The git-config(1) command has various different modes which are\naccessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are\ndeclared with `OPT_BIT()`, which causes two minor issues:\n\n  - The respective modes also have a negated form `--no-get-urlmatch`,\n    which is unintended.\n\n  - We have to manually handle exclusiveness of the modes.\n\nSwitch these options to instead use `OPT_CMDMODE()`, which is made\nexactly for this usecase. Remove the now-unneeded check that only a\nsingle mode is given, which is now handled by the parse-options\ninterface.\n\nWhile at it, format optional placeholders for arguments to conform to\nour style guidelines by using `[<placeholder>]`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c  | 32 ++++++++++++++------------------\n t/t1300-config.sh | 13 +++++++++++++\n 2 files changed, 27 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex fcd6190f12..a293161be4 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -631,20 +631,20 @@ static struct option builtin_config_options[] = {\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n \tOPT_GROUP(N_(\"Action\")),\n-\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n+\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n+\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n+\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n+\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n+\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n+\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n+\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n \tOPT_GROUP(N_(\"Type\")),\n \tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n \tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n@@ -767,10 +767,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (HAS_MULTI_BITS(actions)) {\n-\t\terror(_(\"only one action at a time\"));\n-\t\tusage_builtin_config();\n-\t}\n \tif (actions == 0)\n \t\tswitch (argc) {\n \t\tcase 1: actions = ACTION_GET; break;\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 31c3878687..2d1bc1e27e 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -2612,4 +2612,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such\n \tgrep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n '\n \n+test_expect_success 'negated mode causes failure' '\n+\ttest_must_fail git config --no-get 2>err &&\n+\tgrep \"unknown option \\`no-get${SQ}\" err\n+'\n+\n+test_expect_success 'specifying multiple modes causes failure' '\n+\tcat >expect <<-EOF &&\n+\terror: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together\n+\tEOF\n+\ttest_must_fail git config --get --get-all 2>err &&\n+\ttest_cmp expect err\n+'\n+\n test_done\n-- \n2.44.0\n\n"},{"id":"490420","messageId":"80093f39082e8429381ccf30967738b892a8ed28.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 04/13] builtin/config: pull out function to handle config location","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:20:14Z","receivedAt":"2024-03-11T23:20:22Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There's quite a bunch of options to git-config(1) that allow the user to\nspecify which config location to use when reading or writing config\noptions. The logic to handle this is thus by necessity also quite\ninvolved.\n\nPull it out into a separate function so that we can reuse it in\nsubsequent commits which introduce proper subcommands.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 131 ++++++++++++++++++++++++-----------------------\n 1 file changed, 67 insertions(+), 64 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex a293161be4..693df32526 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -622,67 +622,8 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n-\tOPT_GROUP(N_(\"Action\")),\n-\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n-\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n-\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n-\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n-\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n-\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n-\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n-\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n-\tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n-\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n-\tOPT_END(),\n-};\n-\n-static NORETURN void usage_builtin_config(void)\n-{\n-\tusage_with_options(builtin_config_usage, builtin_config_options);\n-}\n-\n-int cmd_config(int argc, const char **argv, const char *prefix)\n+static void handle_config_location(const char *prefix)\n {\n-\tint nongit = !startup_info->have_repository;\n-\tchar *value = NULL;\n-\tint flags = 0;\n-\tint ret = 0;\n-\tstruct key_value_info default_kvi = KVI_INIT;\n-\n-\tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n-\n-\targc = parse_options(argc, argv, prefix, builtin_config_options,\n-\t\t\t     builtin_config_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n-\n \tif (use_global_config + use_system_config + use_local_config +\n \t    use_worktree_config +\n \t    !!given_config_source.file + !!given_config_source.blob > 1) {\n@@ -690,14 +631,13 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (nongit) {\n+\tif (!startup_info->have_repository) {\n \t\tif (use_local_config)\n \t\t\tdie(_(\"--local can only be used inside a git repository\"));\n \t\tif (given_config_source.blob)\n \t\t\tdie(_(\"--blob can only be used inside a git repository\"));\n \t\tif (use_worktree_config)\n \t\t\tdie(_(\"--worktree can only be used inside a git repository\"));\n-\n \t}\n \n \tif (given_config_source.file &&\n@@ -751,10 +691,73 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tconfig_options.respect_includes = !given_config_source.file;\n \telse\n \t\tconfig_options.respect_includes = respect_includes_opt;\n-\tif (!nongit) {\n+\tif (startup_info->have_repository) {\n \t\tconfig_options.commondir = get_git_common_dir();\n \t\tconfig_options.git_dir = get_git_dir();\n \t}\n+}\n+\n+static struct option builtin_config_options[] = {\n+\tOPT_GROUP(N_(\"Config file location\")),\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tOPT_GROUP(N_(\"Action\")),\n+\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n+\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n+\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n+\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n+\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n+\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n+\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n+\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n+\tOPT_GROUP(N_(\"Type\")),\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tOPT_GROUP(N_(\"Other\")),\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n+\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_END(),\n+};\n+\n+static NORETURN void usage_builtin_config(void)\n+{\n+\tusage_with_options(builtin_config_usage, builtin_config_options);\n+}\n+\n+int cmd_config(int argc, const char **argv, const char *prefix)\n+{\n+\tchar *value = NULL;\n+\tint flags = 0;\n+\tint ret = 0;\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\n+\tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n+\n+\targc = parse_options(argc, argv, prefix, builtin_config_options,\n+\t\t\t     builtin_config_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\thandle_config_location(prefix);\n \n \tif (end_nul) {\n \t\tterm = '\\0';\n@@ -848,7 +851,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tchar *config_file;\n \n \t\tcheck_argc(argc, 0, 0);\n-\t\tif (!given_config_source.file && nongit)\n+\t\tif (!given_config_source.file && !startup_info->have_repository)\n \t\t\tdie(_(\"not in a git directory\"));\n \t\tif (given_config_source.use_stdin)\n \t\t\tdie(_(\"editing stdin is not supported\"));\n-- \n2.44.0\n\n"},{"id":"490421","messageId":"283a199578cfe252204839c55ff83bd23fe2cd9e.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 05/13] builtin/config: pull out function to handle `--null`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:20:24Z","receivedAt":"2024-03-11T23:20:33Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Pull out function to handle the `--null` option, which we are about to\nreuse in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 15 +++++++++------\n 1 file changed, 9 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 693df32526..ce2d3fecd4 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -697,6 +697,14 @@ static void handle_config_location(const char *prefix)\n \t}\n }\n \n+static void handle_nul(void) {\n+\tif (end_nul) {\n+\t\tterm = '\\0';\n+\t\tdelim = '\\n';\n+\t\tkey_delim = '\\n';\n+\t}\n+}\n+\n static struct option builtin_config_options[] = {\n \tOPT_GROUP(N_(\"Config file location\")),\n \tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n@@ -758,12 +766,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n \thandle_config_location(prefix);\n-\n-\tif (end_nul) {\n-\t\tterm = '\\0';\n-\t\tdelim = '\\n';\n-\t\tkey_delim = '\\n';\n-\t}\n+\thandle_nul();\n \n \tif ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {\n \t\terror(_(\"--get-color and variable type are incoherent\"));\n-- \n2.44.0\n\n"},{"id":"490422","messageId":"53401299fa1f51954834e2507a2282cf60b02f20.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 06/13] builtin/config: introduce \"list\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:20:35Z","receivedAt":"2024-03-11T23:20:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-config(1) has several modes, those modes are not exposed with\nsubcommands but instead by specifying e.g. `--unset` or `--list`. This\nuser interface is not really in line with how our more modern commands\nwork, where it is a lot more customary to say e.g. `git remote list`.\nFurthermore, to add to the confusion, git-config(1) also allows the user\nto request modes implicitly by just specifying the correct number of\narguments. Thus, `git config foo.bar` will retrieve the value of\n\"foo.bar\" while `git config foo.bar baz` will set it to \"baz\".\n\nOverall, this makes for a confusing interface that could really use a\nmakeover. It hurts discoverability of what you can do with git-config(1)\nand is comparatively easy to get wrong. Converting the command to have\nsubcommands instead would go a long way to help address these issues.\n\nOne concern in this context is backwards compatibility. Luckily, we can\nintroduce subcommands without breaking backwards compatibility at all.\nThis is because all the implicit modes of git-config(1) require that the\nfirst argument is a properly formatted config key. And as config keys\n_must_ have a dot in their name, any value without a dot would have been\ndiscarded by git-config(1) previous to this change. Thus, given that\nnone of the subcommands do have a dot, they are unambiguous.\n\nIntroduce the first such new subcommand, which is \"git config list\". To\nretain backwards compatibility we only conditionally use subcommands and\nwill fall back to the old syntax in case no subcommand was detected.\nThis should help to transition to the new-style syntax until we\neventually deprecate and remove the old-style syntax.\n\nNote that the way we handle this we're duplicating some functionality\nacross old and new syntax. While this isn't pretty, it helps us to\nensure that there really is no change in behaviour for the old syntax.\n\nAmend tests such that we run them both with old and new style syntax.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt |  26 +++++---\n builtin/config.c             |  90 ++++++++++++++++++++++++----\n t/t1300-config.sh            | 111 +++++++++++++++++++++--------------\n 3 files changed, 163 insertions(+), 64 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex dff39093b5..976ba26757 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -9,6 +9,7 @@ git-config - Get and set repository or global options\n SYNOPSIS\n --------\n [verse]\n+'git config list' [<file-option>] [<display-option>] [--includes]\n 'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n 'git config' [<file-option>] [--type=<type>] --add <name> <value>\n 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n@@ -20,7 +21,6 @@ SYNOPSIS\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n-'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list\n 'git config' [<file-option>] --get-color <name> [<default>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n@@ -74,6 +74,12 @@ On success, the command returns the exit code 0.\n A list of all available configuration variables can be obtained using the\n `git help --config` command.\n \n+COMMANDS\n+--------\n+\n+list::\n+\tList all variables set in config file, along with their values.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -178,10 +184,6 @@ See also <<FILES>>.\n --unset-all::\n \tRemove all lines matching the key from config file.\n \n--l::\n---list::\n-\tList all variables set in config file, along with their values.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -236,7 +238,7 @@ Valid `<type>`'s include:\n \tcontain line breaks.\n \n --name-only::\n-\tOutput only the names of config variables for `--list` or\n+\tOutput only the names of config variables for `list` or\n \t`--get-regexp`.\n \n --show-origin::\n@@ -287,10 +289,20 @@ Valid `<type>`'s include:\n   When using `--get`, and the requested variable is not found, behave as if\n   <value> were the value assigned to the that variable.\n \n+DEPRECATED MODES\n+----------------\n+\n+The following modes have been deprecated in favor of subcommands. It is\n+recommended to migrate to the new syntax.\n+\n+-l::\n+--list::\n+\tReplaced by `git config list`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n-using `--list` or any of the `--get-*` which may return multiple results.\n+using `list` or any of the `--get-*` which may return multiple results.\n The default is to use a pager.\n \n [[FILES]]\ndiff --git a/builtin/config.c b/builtin/config.c\nindex ce2d3fecd4..ee7ac9381e 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -16,10 +16,16 @@\n #include \"worktree.h\"\n \n static const char *const builtin_config_usage[] = {\n+\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config [<options>]\"),\n \tNULL\n };\n \n+static const char *const builtin_config_list_usage[] = {\n+\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -33,6 +39,7 @@ static char delim = '=';\n static char key_delim = ' ';\n static char term = '\\n';\n \n+static parse_opt_subcommand_fn *subcommand;\n static int use_global_config, use_system_config, use_local_config;\n static int use_worktree_config;\n static struct git_config_source given_config_source;\n@@ -705,14 +712,24 @@ static void handle_nul(void) {\n \t}\n }\n \n+#define CONFIG_LOCATION_OPTIONS \\\n+\tOPT_GROUP(N_(\"Config file location\")), \\\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")), \\\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")), \\\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")), \\\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")), \\\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")), \\\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\"))\n+\n+#define CONFIG_DISPLAY_OPTIONS \\\n+\tOPT_GROUP(N_(\"Display options\")), \\\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")), \\\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")), \\\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")), \\\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\"))\n+\n static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tCONFIG_LOCATION_OPTIONS,\n \tOPT_GROUP(N_(\"Action\")),\n \tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n \tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n@@ -736,14 +753,11 @@ static struct option builtin_config_options[] = {\n \tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n \tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n \tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tCONFIG_DISPLAY_OPTIONS,\n \tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n \tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n \tOPT_END(),\n };\n \n@@ -752,6 +766,42 @@ static NORETURN void usage_builtin_config(void)\n \tusage_with_options(builtin_config_usage, builtin_config_options);\n }\n \n+static int cmd_config_list(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_DISPLAY_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);\n+\tcheck_argc(argc, 0, 0);\n+\n+\thandle_config_location(prefix);\n+\thandle_nul();\n+\n+\tsetup_auto_pager(\"config\", 1);\n+\n+\tif (config_with_options(show_all_config, NULL,\n+\t\t\t\t&given_config_source, the_repository,\n+\t\t\t\t&config_options) < 0) {\n+\t\tif (given_config_source.file)\n+\t\t\tdie_errno(_(\"unable to read config file '%s'\"),\n+\t\t\t\t  given_config_source.file);\n+\t\telse\n+\t\t\tdie(_(\"error processing config file(s)\"));\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static struct option builtin_subcommand_options[] = {\n+\tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n+\tOPT_END(),\n+};\n+\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tchar *value = NULL;\n@@ -761,6 +811,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \n \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n \n+\t/*\n+\t * This is somewhat hacky: we first parse the command line while\n+\t * keeping all args intact in order to determine whether a subcommand\n+\t * has been specified. If so, we re-parse it a second time, but this\n+\t * time we drop KEEP_ARGV0. This is so that we don't munge the command\n+\t * line in case no subcommand was given, which would otherwise confuse\n+\t * us when parsing the implicit modes.\n+\t */\n+\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\tif (subcommand) {\n+\t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n+\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\treturn subcommand(argc, argv, prefix);\n+\t}\n+\n \targc = parse_options(argc, argv, prefix, builtin_config_options,\n \t\t\t     builtin_config_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 2d1bc1e27e..720f0ee929 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -11,6 +11,21 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n+for mode in legacy subcommands\n+do\n+\n+case \"$mode\" in\n+legacy)\n+\tmode_prefix=\"--\"\n+\t;;\n+subcommands)\n+\tmode_prefix=\"\"\n+\t;;\n+*)\n+\techo \"unknown mode $mode\" >&2\n+\texit 1;;\n+esac\n+\n test_expect_success 'clear default config' '\n \trm -f .git/config\n '\n@@ -350,11 +365,11 @@ version.1.2.3eX.alpha=beta\n EOF\n \n test_expect_success 'working --list' '\n-\tgit config --list > output &&\n+\tgit config ${mode_prefix}list > output &&\n \ttest_cmp expect output\n '\n test_expect_success '--list without repo produces empty output' '\n-\tgit --git-dir=nonexistent config --list >output &&\n+\tgit --git-dir=nonexistent config ${mode_prefix}list >output &&\n \ttest_must_be_empty output\n '\n \n@@ -366,7 +381,7 @@ version.1.2.3eX.alpha\n EOF\n \n test_expect_success '--name-only --list' '\n-\tgit config --name-only --list >output &&\n+\tgit config ${mode_prefix}list --name-only >output &&\n \ttest_cmp expect output\n '\n \n@@ -504,17 +519,17 @@ ein.bahn=strasse\n EOF\n \n test_expect_success 'alternative GIT_CONFIG' '\n-\tGIT_CONFIG=other-config git config --list >output &&\n+\tGIT_CONFIG=other-config git config ${mode_prefix}list >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'alternative GIT_CONFIG (--file)' '\n-\tgit config --file other-config --list >output &&\n+\tgit config ${mode_prefix}list --file other-config >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'alternative GIT_CONFIG (--file=-)' '\n-\tgit config --file - --list <other-config >output &&\n+\tgit config ${mode_prefix}list --file - <other-config >output &&\n \ttest_cmp expect output\n '\n \n@@ -527,6 +542,7 @@ test_expect_success 'editing stdin is an error' '\n '\n \n test_expect_success 'refer config from subdirectory' '\n+\ttest_when_finished \"rm -r x\" &&\n \tmkdir x &&\n \ttest_cmp_config -C x strasse --file=../other-config --get ein.bahn\n '\n@@ -737,7 +753,7 @@ test_expect_success 'line number is reported correctly' '\n '\n \n test_expect_success 'invalid stdin config' '\n-\techo \"[broken\" | test_must_fail git config --list --file - >output 2>&1 &&\n+\techo \"[broken\" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&\n \ttest_grep \"bad config line 1 in standard input\" output\n '\n \n@@ -1029,7 +1045,7 @@ section.quotecont=cont;inued\n EOF\n \n test_expect_success 'value continued on next line' '\n-\tgit config --list > result &&\n+\tgit config ${mode_prefix}list > result &&\n \ttest_cmp expect result\n '\n \n@@ -1053,7 +1069,7 @@ Qsection.sub=section.val4\n Qsection.sub=section.val5Q\n EOF\n test_expect_success '--null --list' '\n-\tgit config --null --list >result.raw &&\n+\tgit config ${mode_prefix}list --null >result.raw &&\n \tnul_to_q <result.raw >result &&\n \techo >>result &&\n \ttest_cmp expect result\n@@ -1072,6 +1088,7 @@ test_expect_success 'inner whitespace kept verbatim' '\n '\n \n test_expect_success SYMLINKS 'symlinked configuration' '\n+\ttest_when_finished \"rm myconfig\" &&\n \tln -s notyet myconfig &&\n \tgit config --file=myconfig test.frotz nitfol &&\n \ttest -h myconfig &&\n@@ -1092,10 +1109,11 @@ test_expect_success SYMLINKS 'symlinked configuration' '\n '\n \n test_expect_success SYMLINKS 'symlink to nonexistent configuration' '\n+\ttest_when_finished \"rm linktonada linktolinktonada\" &&\n \tln -s doesnotexist linktonada &&\n \tln -s linktonada linktolinktonada &&\n-\ttest_must_fail git config --file=linktonada --list &&\n-\ttest_must_fail git config --file=linktolinktonada --list\n+\ttest_must_fail git config ${mode_prefix}list --file=linktonada &&\n+\ttest_must_fail git config ${mode_prefix}list --file=linktolinktonada\n '\n \n test_expect_success 'check split_cmdline return' '\n@@ -1352,7 +1370,7 @@ do\n done\n \n test_expect_success 'git -c is not confused by empty environment' '\n-\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config --list\n+\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config ${mode_prefix}list\n '\n \n test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n@@ -1543,31 +1561,31 @@ test_expect_success 'git config ignores pairs with empty count' '\n '\n \n test_expect_success 'git config fails with invalid count' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&\n+\ttest_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&\n \ttest_grep \"bogus count\" error &&\n-\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&\n+\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&\n \ttest_grep \"too many entries\" error\n '\n \n test_expect_success 'git config fails with missing config key' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config --list 2>error &&\n+\t\tgit config ${mode_prefix}list 2>error &&\n \ttest_grep \"missing config key\" error\n '\n \n test_expect_success 'git config fails with missing config value' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=\"pair.one\" \\\n-\t\tgit config --list 2>error &&\n+\t\tgit config ${mode_prefix}list 2>error &&\n \ttest_grep \"missing config value\" error\n '\n \n test_expect_success 'git config fails with invalid config pair key' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \\\n-\t\tgit config --list &&\n+\t\tgit config ${mode_prefix}list &&\n \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \\\n-\t\tgit config --list\n+\t\tgit config ${mode_prefix}list\n '\n \n test_expect_success 'environment overrides config file' '\n@@ -1607,7 +1625,7 @@ test_expect_success 'git config --edit works' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n \tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n-\tgit config -f tmp --list >actual &&\n+\tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1616,7 +1634,7 @@ test_expect_success 'git config --edit respects core.editor' '\n \techo test.value=yes >expect &&\n \ttest_config core.editor \"echo [test]value=yes >\" &&\n \tgit config -f tmp --edit &&\n-\tgit config -f tmp --list >actual &&\n+\tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1967,7 +1985,7 @@ test_expect_success '--show-origin with --list' '\n \tcommand line:\tuser.cmdline=true\n \tEOF\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\\\n-\t\tgit -c user.cmdline=true config --list --show-origin >output &&\n+\t\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -1984,7 +2002,7 @@ test_expect_success '--show-origin with --list --null' '\n \tincludeQcommand line:Quser.cmdline\n \ttrueQ\n \tEOF\n-\tgit -c user.cmdline=true config --null --list --show-origin >output.raw &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&\n \tnul_to_q <output.raw >output &&\n \t# The here-doc above adds a newline that the --null output would not\n \t# include. Add it here to make the two comparable.\n@@ -1998,7 +2016,7 @@ test_expect_success '--show-origin with single file' '\n \tfile:.git/config\tuser.override=local\n \tfile:.git/config\tinclude.path=../include/relative.include\n \tEOF\n-\tgit config --local --list --show-origin >output &&\n+\tgit config ${mode_prefix}list --local --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2036,7 +2054,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '\n \tcat >expect <<-\\EOF &&\n \tfile:\"file\\\" (dq) and spaces.conf\"\tuser.custom=true\n \tEOF\n-\tgit config --file \"$WEIRDLY_NAMED_FILE\" --show-origin --list >output &&\n+\tgit config ${mode_prefix}list --file \"$WEIRDLY_NAMED_FILE\" --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2044,7 +2062,7 @@ test_expect_success '--show-origin stdin' '\n \tcat >expect <<-\\EOF &&\n \tstandard input:\tuser.custom=true\n \tEOF\n-\tgit config --file - --show-origin --list <\"$CUSTOM_CONFIG_FILE\" >output &&\n+\tgit config ${mode_prefix}list --file - --show-origin <\"$CUSTOM_CONFIG_FILE\" >output &&\n \ttest_cmp expect output\n '\n \n@@ -2071,7 +2089,7 @@ test_expect_success '--show-origin blob' '\n \t\tcat >expect <<-EOF &&\n \t\tblob:$blob\tuser.custom=true\n \t\tEOF\n-\t\tgit config --blob=$blob --show-origin --list >output &&\n+\t\tgit config ${mode_prefix}list --blob=$blob --show-origin >output &&\n \t\ttest_cmp expect output\n \t)\n '\n@@ -2087,7 +2105,7 @@ test_expect_success '--show-origin blob ref' '\n \t\tcp \"$CUSTOM_CONFIG_FILE\" custom.conf &&\n \t\tgit add custom.conf &&\n \t\tgit commit -m \"new config file\" &&\n-\t\tgit config --blob=main:custom.conf --show-origin --list >output &&\n+\t\tgit config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&\n \t\ttest_cmp expect output\n \t)\n '\n@@ -2113,13 +2131,14 @@ test_expect_success '--show-scope with --list' '\n \tworktree\tuser.worktree=true\n \tcommand\tuser.cmdline=true\n \tEOF\n+\ttest_when_finished \"git worktree remove wt1\" &&\n \tgit worktree add wt1 &&\n \t# We need these to test for worktree scope, but outside of this\n \t# test, this is just noise\n \ttest_config core.repositoryformatversion 1 &&\n \ttest_config extensions.worktreeConfig true &&\n \tgit config --worktree user.worktree true &&\n-\tgit -c user.cmdline=true config --list --show-scope >output &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2128,7 +2147,7 @@ test_expect_success !MINGW '--show-scope with --blob' '\n \tcat >expect <<-EOF &&\n \tcommand\tuser.custom=true\n \tEOF\n-\tgit config --blob=$blob --show-scope --list >output &&\n+\tgit config ${mode_prefix}list --blob=$blob --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2138,7 +2157,7 @@ test_expect_success '--show-scope with --local' '\n \tlocal\tuser.override=local\n \tlocal\tinclude.path=../include/relative.include\n \tEOF\n-\tgit config --local --list --show-scope >output &&\n+\tgit config ${mode_prefix}list --local --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2162,7 +2181,7 @@ test_expect_success '--show-scope with --show-origin' '\n \tlocal\tfile:.git/../include/relative.include\tuser.relative=include\n \tcommand\tcommand line:\tuser.cmdline=true\n \tEOF\n-\tgit -c user.cmdline=true config --list --show-origin --show-scope >output &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2203,7 +2222,7 @@ test_expect_success 'override global and system config' '\n \tglobal\thome.config=true\n \tlocal\tlocal.config=true\n \tEOF\n-\tgit config --show-scope --list >output &&\n+\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output &&\n \n \tcat >expect <<-EOF &&\n@@ -2212,20 +2231,20 @@ test_expect_success 'override global and system config' '\n \tlocal\tlocal.config=true\n \tEOF\n \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \\\n-\t\tgit config --show-scope --list >output &&\n+\t\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output &&\n \n \tcat >expect <<-EOF &&\n \tlocal\tlocal.config=true\n \tEOF\n \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \\\n-\t\tgit config --show-scope --list >output &&\n+\t\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'override global and system config with missing file' '\n-\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&\n-\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&\n+\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&\n+\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&\n \tGIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version\n '\n \n@@ -2352,7 +2371,7 @@ test_expect_success 'set all config with value-pattern' '\n \t# no match => add new entry\n \tcp initial config &&\n \tgit config --file=config abc.key two a+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2365,7 +2384,7 @@ test_expect_success 'set all config with value-pattern' '\n \n \t# multiple values, no match => add\n \tgit config --file=config abc.key three a+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2375,7 +2394,7 @@ test_expect_success 'set all config with value-pattern' '\n \n \t# single match => replace\n \tgit config --file=config abc.key four h+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2390,7 +2409,7 @@ test_expect_success '--replace-all and value-pattern' '\n \tgit config --file=config --add abc.key two &&\n \tgit config --file=config --add abc.key three &&\n \tgit config --file=config --replace-all abc.key four \"o+\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=four\n \tabc.key=three\n@@ -2408,7 +2427,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --rename-section dev null &&\n \ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n-\ttest_must_fail git config --file=config --fixed-value --list &&\n+\ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n \n@@ -2429,7 +2448,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n \tfixed.test=bogus\n@@ -2438,7 +2457,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config --fixed-value fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tfixed.test=bogus\n \tEOF\n@@ -2456,7 +2475,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n \tfixed.test=bogus\n@@ -2464,7 +2483,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \ttest_cmp expect actual &&\n \n \tgit config --file=config --fixed-value --replace-all fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=bogus\n \tfixed.test=bogus\n@@ -2625,4 +2644,6 @@ test_expect_success 'specifying multiple modes causes failure' '\n \ttest_cmp expect err\n '\n \n+done\n+\n test_done\n-- \n2.44.0\n\n"},{"id":"490423","messageId":"8ceced0fc58e9b4cf1c5e5b678e864f82c1b8c01.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 07/13] builtin/config: introduce \"get\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:20:46Z","receivedAt":"2024-03-11T23:20:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"get\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt |  89 +++++++++++++------------\n builtin/config.c             |  69 ++++++++++++++++---\n t/t1300-config.sh            | 125 ++++++++++++++++++++++++-----------\n 3 files changed, 192 insertions(+), 91 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 976ba26757..f3d5e3e613 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -10,18 +10,14 @@ SYNOPSIS\n --------\n [verse]\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' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n 'git config' [<file-option>] [--type=<type>] --add <name> <value>\n 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL>\n 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n-'git config' [<file-option>] --get-color <name> [<default>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n \n@@ -80,6 +76,12 @@ COMMANDS\n list::\n \tList all variables set in config file, along with their values.\n \n+get::\n+\tGet value for one or more config options. Values can be filtered by\n+\tregexes and URLs.Returns error code 1 if the key was not found and the\n+\tlast value if multiple key values were found. If `--all` is set, then\n+\tall values will be shown.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -93,22 +95,16 @@ OPTIONS\n \tvalues.  This is the same as providing '^$' as the `value-pattern`\n \tin `--replace-all`.\n \n---get::\n-\tGet the value for a given key (optionally filtered by a regex\n-\tmatching the value). Returns error code 1 if the key was not\n-\tfound and the last value if multiple key values were found.\n-\n---get-all::\n-\tLike get, but returns all values for a multi-valued key.\n+--all::\n+\tWith \"get\", Return all values for a multi-valued key.\n \n---get-regexp::\n-\tLike --get-all, but interprets the name as a regular expression and\n-\twrites out the key names.  Regular expression matching is currently\n-\tcase-sensitive and done against a canonicalized version of the key\n-\tin which section and variable names are lowercased, but subsection\n-\tnames are not.\n+---regexp::\n+\tWith \"get\", interpret the name as a regular expression. Regular\n+\texpression matching is currently case-sensitive and done against a\n+\tcanonicalized version of the key in which section and variable names\n+\tare lowercased, but subsection names are not.\n \n---get-urlmatch <name> <URL>::\n+--url=<URL>::\n \tWhen given a two-part <name> as <section>.<key>, the value for\n \t<section>.<URL>.<key> whose <URL> part matches the best to the\n \tgiven URL is returned (if no such key exists, the value for\n@@ -239,7 +235,7 @@ Valid `<type>`'s include:\n \n --name-only::\n \tOutput only the names of config variables for `list` or\n-\t`--get-regexp`.\n+\t`get`.\n \n --show-origin::\n \tAugment the output of all queried config options with the\n@@ -263,17 +259,6 @@ Valid `<type>`'s include:\n \tWhen the color setting for `name` is undefined, the command uses\n \t`color.ui` as fallback.\n \n---get-color <name> [<default>]::\n-\n-\tFind the color configured for `name` (e.g. `color.diff.new`) and\n-\toutput it as the ANSI color escape sequence to the standard\n-\toutput.  The optional `default` parameter is used instead, if\n-\tthere is no color configured for `name`.\n-+\n-`--type=color [--default=<default>]` is preferred over `--get-color`\n-(but note that `--get-color` will omit the trailing newline printed by\n-`--type=color`).\n-\n -e::\n --edit::\n \tOpens an editor to modify the specified config file; either\n@@ -286,7 +271,7 @@ Valid `<type>`'s include:\n \tconfig files.\n \n --default <value>::\n-  When using `--get`, and the requested variable is not found, behave as if\n+  When using `get`, and the requested variable is not found, behave as if\n   <value> were the value assigned to the that variable.\n \n DEPRECATED MODES\n@@ -295,15 +280,33 @@ DEPRECATED MODES\n The following modes have been deprecated in favor of subcommands. It is\n recommended to migrate to the new syntax.\n \n+'git config <name>'::\n+\tReplaced by `git config get <name>`.\n+\n -l::\n --list::\n \tReplaced by `git config list`.\n \n+--get <name> [<value-pattern>]::\n+\tReplaced by `git config get [--value=<pattern>] <name>`.\n+\n+--get-all <name> [<value-pattern>]::\n+\tReplaced by `git config get [--value=<pattern>] --all --show-names <name>`.\n+\n+--get-regexp <name-regexp>::\n+\tReplaced by `git config get --all --show-names --regexp <name-regexp>`.\n+\n+--get-urlmatch <name> <URL>::\n+\tReplaced by `git config get --all --show-names --url=<URL> <name>`.\n+\n+--get-color <name> [<default>]::\n+\tReplaced by `git config get --type=color [--default=<default>] <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n-using `list` or any of the `--get-*` which may return multiple results.\n-The default is to use a pager.\n+using `list` or `get` which may return multiple results. The default is to use\n+a pager.\n \n [[FILES]]\n FILES\n@@ -506,25 +509,25 @@ you have to provide a regex matching the value of exactly one line.\n To query the value for a given key, do\n \n ------------\n-% git config --get core.filemode\n+% git config get core.filemode\n ------------\n \n or\n \n ------------\n-% git config core.filemode\n+% git config get core.filemode\n ------------\n \n or, to query a multivar:\n \n ------------\n-% git config --get core.gitproxy \"for kernel.org$\"\n+% git config get --value=\"for kernel.org$\" core.gitproxy\n ------------\n \n If you want to know all the values for a multivar, do:\n \n ------------\n-% git config --get-all core.gitproxy\n+% git config get --all --show-names core.gitproxy\n ------------\n \n If you like to live dangerously, you can replace *all* core.gitproxy by a\n@@ -558,8 +561,8 @@ script:\n \n ------------\n #!/bin/sh\n-WS=$(git config --get-color color.diff.whitespace \"blue reverse\")\n-RESET=$(git config --get-color \"\" \"reset\")\n+WS=$(git config get --type=color --default=\"blue reverse\" color.diff.whitespace)\n+RESET=$(git config get --type=color --default=\"reset\" \"\")\n echo \"${WS}your whitespace color or blue reverse${RESET}\"\n ------------\n \n@@ -567,11 +570,11 @@ For URLs in `https://weak.example.com`, `http.sslVerify` is set to\n false, while it is set to `true` for all others:\n \n ------------\n-% git config --type=bool --get-urlmatch http.sslverify https://good.example.com\n+% git config get --type=bool --url=https://good.example.com http.sslverify\n true\n-% git config --type=bool --get-urlmatch http.sslverify https://weak.example.com\n+% git config get --type=bool --url=https://weak.example.com http.sslverify\n false\n-% git config --get-urlmatch http https://weak.example.com\n+% git config get --url=https://weak.example.com http\n http.cookieFile /tmp/cookie.txt\n http.sslverify false\n ------------\ndiff --git a/builtin/config.c b/builtin/config.c\nindex ee7ac9381e..aaa8b15e86 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -17,7 +17,7 @@\n \n static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n-\tN_(\"git config [<options>]\"),\n+\tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tNULL\n };\n \n@@ -26,6 +26,11 @@ static const char *const builtin_config_list_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_get_usage[] = {\n+\tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -721,6 +726,16 @@ static void handle_nul(void) {\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")), \\\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\"))\n \n+#define CONFIG_TYPE_OPTIONS \\\n+\tOPT_GROUP(N_(\"Type\")), \\\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL), \\\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR), \\\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH), \\\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE)\n+\n #define CONFIG_DISPLAY_OPTIONS \\\n \tOPT_GROUP(N_(\"Display options\")), \\\n \tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")), \\\n@@ -745,14 +760,7 @@ static struct option builtin_config_options[] = {\n \tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n \tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n \tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tCONFIG_TYPE_OPTIONS,\n \tCONFIG_DISPLAY_OPTIONS,\n \tOPT_GROUP(N_(\"Other\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n@@ -797,8 +805,51 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix)\n \treturn 0;\n }\n \n+static int cmd_config_get(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL, *url = NULL;\n+\tint flags = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_TYPE_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter options\")),\n+\t\tOPT_BOOL(0, \"all\", &do_all, N_(\"return all values for multi-valued config options\")),\n+\t\tOPT_BOOL(0, \"regexp\", &use_key_regexp, N_(\"interpret the name as a regular expression\")),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_STRING(0, \"url\", &url, N_(\"URL\"), N_(\"show config matching the given URL\")),\n+\t\tCONFIG_DISPLAY_OPTIONS,\n+\t\tOPT_BOOL(0, \"show-names\", &show_keys, N_(\"show config keys in addition to their values\")),\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\t\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"use default value when missing entry\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_get_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_argc(argc, 1, 1);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with 'value-pattern'\"));\n+\tif (default_value && (do_all || url))\n+\t\tdie(_(\"--default= cannot be used with --all or --url=\"));\n+\tif (url && (do_all || use_key_regexp || value_pattern))\n+\t\tdie(_(\"--url= cannot be used with --all, --regexp or --value\"));\n+\n+\thandle_config_location(prefix);\n+\thandle_nul();\n+\n+\tsetup_auto_pager(\"config\", 1);\n+\n+\tif (url)\n+\t\treturn get_urlmatch(argv[0], url);\n+\treturn get_value(argv[0], value_pattern, flags);\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n+\tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 720f0ee929..7f6746936e 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -17,9 +17,15 @@ do\n case \"$mode\" in\n legacy)\n \tmode_prefix=\"--\"\n+\tmode_get=\"\"\n+\tmode_get_all=\"--get-all\"\n+\tmode_get_regexp=\"--get-regexp\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n+\tmode_get=\"get\"\n+\tmode_get_all=\"get --all\"\n+\tmode_get_regexp=\"get --regexp --all --show-names\"\n \t;;\n *)\n \techo \"unknown mode $mode\" >&2\n@@ -289,7 +295,7 @@ test_expect_success 'multi-valued get-all returns all' '\n \twow\n \twow2 for me\n \tEOF\n-\tgit config --get-all nextsection.nonewline >actual &&\n+\tgit config ${mode_get_all} nextsection.nonewline >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -391,7 +397,7 @@ nextsection.nonewline wow2 for me\n EOF\n \n test_expect_success '--get-regexp' '\n-\tgit config --get-regexp in >output &&\n+\tgit config ${mode_get_regexp} in >output &&\n \ttest_cmp expect output\n '\n \n@@ -401,7 +407,7 @@ nextsection.nonewline\n EOF\n \n test_expect_success '--name-only --get-regexp' '\n-\tgit config --name-only --get-regexp in >output &&\n+\tgit config ${mode_get_regexp} --name-only in >output &&\n \ttest_cmp expect output\n '\n \n@@ -412,7 +418,7 @@ EOF\n \n test_expect_success '--add' '\n \tgit config --add nextsection.nonewline \"wow4 for you\" &&\n-\tgit config --get-all nextsection.nonewline > output &&\n+\tgit config ${mode_get_all} nextsection.nonewline > output &&\n \ttest_cmp expect output\n '\n \n@@ -434,21 +440,21 @@ test_expect_success 'get variable with empty value' '\n echo novalue.variable > expect\n \n test_expect_success 'get-regexp variable with no value' '\n-\tgit config --get-regexp novalue > output &&\n+\tgit config ${mode_get_regexp} novalue > output &&\n \ttest_cmp expect output\n '\n \n echo 'novalue.variable true' > expect\n \n test_expect_success 'get-regexp --bool variable with no value' '\n-\tgit config --bool --get-regexp novalue > output &&\n+\tgit config ${mode_get_regexp} --bool novalue > output &&\n \ttest_cmp expect output\n '\n \n echo 'emptyvalue.variable ' > expect\n \n test_expect_success 'get-regexp variable with empty value' '\n-\tgit config --get-regexp emptyvalue > output &&\n+\tgit config ${mode_get_regexp} emptyvalue > output &&\n \ttest_cmp expect output\n '\n \n@@ -1022,7 +1028,7 @@ test_expect_success 'quoting' '\n '\n \n test_expect_success 'key with newline' '\n-\ttest_must_fail git config \"key.with\n+\ttest_must_fail git config ${mode_get} \"key.with\n newline\" 123'\n \n test_expect_success 'value with newline' 'git config key.sub value.with\\\\\\\n@@ -1076,7 +1082,7 @@ test_expect_success '--null --list' '\n '\n \n test_expect_success '--null --get-regexp' '\n-\tgit config --null --get-regexp \"val[0-9]\" >result.raw &&\n+\tgit config ${mode_get_regexp} --null \"val[0-9]\" >result.raw &&\n \tnul_to_q <result.raw >result &&\n \techo >>result &&\n \ttest_cmp expect result\n@@ -1158,11 +1164,11 @@ test_expect_success 'git -c can represent empty string' '\n '\n \n test_expect_success 'key sanity-checking' '\n-\ttest_must_fail git config foo=bar &&\n-\ttest_must_fail git config foo=.bar &&\n-\ttest_must_fail git config foo.ba=r &&\n-\ttest_must_fail git config foo.1bar &&\n-\ttest_must_fail git config foo.\"ba\n+\ttest_must_fail git config ${mode_get} foo=bar &&\n+\ttest_must_fail git config ${mode_get} foo=.bar &&\n+\ttest_must_fail git config ${mode_get} foo.ba=r &&\n+\ttest_must_fail git config ${mode_get} foo.1bar &&\n+\ttest_must_fail git config ${mode_get} foo.\"ba\n \t\t\t\tz\".bar &&\n \ttest_must_fail git config . false &&\n \ttest_must_fail git config .foo false &&\n@@ -1211,7 +1217,7 @@ test_expect_success 'git -c complains about empty key and value' '\n '\n \n test_expect_success 'multiple git -c appends config' '\n-\ttest_config alias.x \"!git -c x.two=2 config --get-regexp ^x\\.*\" &&\n+\ttest_config alias.x \"!git -c x.two=2 config ${mode_get_regexp} ^x\\.*\" &&\n \tcat >expect <<-\\EOF &&\n \tx.one 1\n \tx.two 2\n@@ -1377,7 +1383,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n \tv=\"${SQ}key.one=foo${SQ}\" &&\n \tv=\"$v  ${SQ}key.two=bar${SQ}\" &&\n \tv=\"$v ${SQ}key.ambiguous=section.whatever=value${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.one foo\n \tkey.two bar\n@@ -1390,7 +1396,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '\n \tv=\"${SQ}key.one${SQ}=${SQ}foo${SQ}\" &&\n \tv=\"$v  ${SQ}key.two${SQ}=${SQ}bar${SQ}\" &&\n \tv=\"$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.one foo\n \tkey.two bar\n@@ -1404,7 +1410,7 @@ test_expect_success 'old and new-style entries can mix' '\n \tv=\"$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}\" &&\n \tv=\"$v ${SQ}key.oldtwo=oldbar${SQ}\" &&\n \tv=\"$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.oldone oldfoo\n \tkey.newone newfoo\n@@ -1417,7 +1423,7 @@ test_expect_success 'old and new-style entries can mix' '\n test_expect_success 'old and new bools with ambiguous subsection' '\n \tv=\"${SQ}key.with=equals.oldbool${SQ}\" &&\n \tv=\"$v ${SQ}key.with=equals.newbool${SQ}=\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.with equals.oldbool\n \tkey.with=equals.newbool\n@@ -1431,7 +1437,7 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '\n \tenv.two two\n \tEOF\n \tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ} ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"env.*\" >actual &&\n \ttest_cmp expect actual &&\n \n \tcat >expect <<-EOF &&\n@@ -1439,12 +1445,12 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '\n \tenv.two two\n \tEOF\n \tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ}\\\\$SQ$SQ$SQ ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"env.*\" >actual &&\n \ttest_cmp expect actual &&\n \n \ttest_must_fail env \\\n \t\tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ}\\\\$SQ ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\"\n+\t\tgit config ${mode_get_regexp} \"env.*\"\n '\n \n test_expect_success 'git --config-env=key=envvar support' '\n@@ -1492,7 +1498,7 @@ test_expect_success 'git -c and --config-env work together' '\n \tENVVAR=env-value git \\\n \t\t-c bar.cmd=cmd-value \\\n \t\t--config-env=bar.env=ENVVAR \\\n-\t\tconfig --get-regexp \"^bar.*\" >actual &&\n+\t\tconfig ${mode_get_regexp} \"^bar.*\" >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1520,7 +1526,7 @@ test_expect_success 'git config handles environment config pairs' '\n \tGIT_CONFIG_COUNT=2 \\\n \t\tGIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"foo\" \\\n \t\tGIT_CONFIG_KEY_1=\"pair.two\" GIT_CONFIG_VALUE_1=\"bar\" \\\n-\t\tgit config --get-regexp \"pair.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"pair.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tpair.one foo\n \tpair.two bar\n@@ -1530,7 +1536,7 @@ test_expect_success 'git config handles environment config pairs' '\n \n test_expect_success 'git config ignores pairs without count' '\n \ttest_must_fail env GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n@@ -1538,7 +1544,7 @@ test_expect_success 'git config ignores pairs exceeding count' '\n \tGIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n \t\tGIT_CONFIG_KEY_1=\"pair.two\" GIT_CONFIG_VALUE_1=\"value\" \\\n-\t\tgit config --get-regexp \"pair.*\" >actual 2>error &&\n+\t\tgit config ${mode_get_regexp} \"pair.*\" >actual 2>error &&\n \tcat >expect <<-EOF &&\n \tpair.one value\n \tEOF\n@@ -1549,14 +1555,14 @@ test_expect_success 'git config ignores pairs exceeding count' '\n test_expect_success 'git config ignores pairs with zero count' '\n \ttest_must_fail env \\\n \t\tGIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n test_expect_success 'git config ignores pairs with empty count' '\n \ttest_must_fail env \\\n \t\tGIT_CONFIG_COUNT= GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n@@ -1595,7 +1601,7 @@ test_expect_success 'environment overrides config file' '\n \tone = value\n \tEOF\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \\\n-\t\tgit config pair.one >actual &&\n+\t\tgit config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1605,7 +1611,7 @@ test_expect_success 'environment overrides config file' '\n test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \\\n \t\tGIT_CONFIG_PARAMETERS=\"${SQ}pair.one=override${SQ}\" \\\n-\t\tgit config pair.one >actual &&\n+\t\tgit config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1680,20 +1686,28 @@ test_expect_success 'urlmatch' '\n \n \ttest_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&\n \ttest_must_be_empty actual &&\n+\ttest_expect_code 1 git config get --url=https://good.example.com --bool doesnt.exist >actual &&\n+\ttest_must_be_empty actual &&\n \n \techo true >expect &&\n \tgit config --bool --get-urlmatch http.SSLverify https://good.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --bool --url=https://good.example.com http.SSLverify >actual &&\n+\ttest_cmp expect actual &&\n \n \techo false >expect &&\n \tgit config --bool --get-urlmatch http.sslverify https://weak.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --bool --url=https://weak.example.com http.sslverify >actual &&\n+\ttest_cmp expect actual &&\n \n \t{\n \t\techo http.cookiefile /tmp/cookie.txt &&\n \t\techo http.sslverify false\n \t} >expect &&\n \tgit config --get-urlmatch HTTP https://weak.example.com >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://weak.example.com HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1709,6 +1723,8 @@ test_expect_success 'urlmatch with --show-scope' '\n \tlocal\thttp.sslverify false\n \tEOF\n \tgit config --get-urlmatch --show-scope HTTP https://weak.example.com >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://weak.example.com --show-scope HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1741,45 +1757,67 @@ test_expect_success 'urlmatch favors more specific URLs' '\n \techo http.cookiefile /tmp/root.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com/subdirectory >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com/subdirectory HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com/subdirectory/nested HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/user.txt >expect &&\n \tgit config --get-urlmatch HTTP https://user@example.com/ >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://user@example.com/ HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://averylonguser@example.com/subdirectory HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/preceding.txt >expect &&\n \tgit config --get-urlmatch HTTP https://preceding.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://preceding.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/wildcard.txt >expect &&\n \tgit config --get-urlmatch HTTP https://wildcard.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://wildcard.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n \tgit config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://sub.example.com/wildcardwithsubdomain HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/trailing.txt >expect &&\n \tgit config --get-urlmatch HTTP https://trailing.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://trailing.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n \tgit config --get-urlmatch HTTP https://user@sub.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://user@sub.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/multiwildcard.txt >expect &&\n \tgit config --get-urlmatch HTTP https://wildcard.example.org >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://wildcard.example.org HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1902,7 +1940,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[one]\n \tEOF\n \tgit config --unset two.subsection.key &&\n-\ttest \"not [two subsection]\" = \"$(git config one.key)\" &&\n+\ttest \"not [two subsection]\" = \"$(git config ${mode_get} one.key)\" &&\n \ttest_line_count = 3 .git/config\n '\n \n@@ -2025,7 +2063,7 @@ test_expect_success '--show-origin with --get-regexp' '\n \tfile:$HOME/.gitconfig\tuser.global true\n \tfile:.git/config\tuser.local true\n \tEOF\n-\tgit config --show-origin --get-regexp \"user\\.[g|l].*\" >output &&\n+\tgit config ${mode_get_regexp} --show-origin \"user\\.[g|l].*\" >output &&\n \ttest_cmp expect output\n '\n \n@@ -2033,7 +2071,7 @@ test_expect_success '--show-origin getting a single key' '\n \tcat >expect <<-\\EOF &&\n \tfile:.git/config\tlocal\n \tEOF\n-\tgit config --show-origin user.override >output &&\n+\tgit config ${mode_get} --show-origin user.override >output &&\n \ttest_cmp expect output\n '\n \n@@ -2165,7 +2203,7 @@ test_expect_success '--show-scope getting a single value' '\n \tcat >expect <<-\\EOF &&\n \tlocal\ttrue\n \tEOF\n-\tgit config --show-scope --get user.local >output &&\n+\tgit config ${mode_get} --show-scope user.local >output &&\n \ttest_cmp expect output\n '\n \n@@ -2434,9 +2472,9 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \t# These modes complain when --fixed-value has no value-pattern\n \ttest_must_fail git config --file=config --fixed-value dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --get dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-all dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-regexp \"dev.*\" &&\n+\ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n \ttest_must_fail git config --file=config --fixed-value --unset dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --unset-all dev.null\n '\n@@ -2466,12 +2504,12 @@ test_expect_success '--fixed-value uses exact string matching' '\n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset-all fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset-all fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n \tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n@@ -2498,18 +2536,27 @@ test_expect_success '--get and --get-all with --fixed-value' '\n \tgit config --file=config --add fixed.test \"$META\" &&\n \n \tgit config --file=config --get fixed.test bogus &&\n+\tgit config get --file=config --value=bogus fixed.test &&\n \ttest_must_fail git config --file=config --get fixed.test \"$META\" &&\n+\ttest_must_fail git config get --file=config --value=\"$META\" fixed.test &&\n \tgit config --file=config --get --fixed-value fixed.test \"$META\" &&\n+\tgit config get --file=config --fixed-value --value=\"$META\" fixed.test &&\n \ttest_must_fail git config --file=config --get --fixed-value fixed.test non-existent &&\n \n \tgit config --file=config --get-all fixed.test bogus &&\n+\tgit config get --all --file=config --value=bogus fixed.test &&\n \ttest_must_fail git config --file=config --get-all fixed.test \"$META\" &&\n+\ttest_must_fail git config get --all --file=config --value=\"$META\" fixed.test &&\n \tgit config --file=config --get-all --fixed-value fixed.test \"$META\" &&\n+\tgit config get --all --file=config --value=\"$META\" --fixed-value fixed.test &&\n \ttest_must_fail git config --file=config --get-all --fixed-value fixed.test non-existent &&\n \n \tgit config --file=config --get-regexp fixed+ bogus &&\n+\tgit config get --regexp --file=config --value=bogus fixed+ &&\n \ttest_must_fail git config --file=config --get-regexp fixed+ \"$META\" &&\n+\ttest_must_fail git config get --regexp --file=config --value=\"$META\" fixed+ &&\n \tgit config --file=config --get-regexp --fixed-value fixed+ \"$META\" &&\n+\tgit config get --regexp --file=config --fixed-value --value=\"$META\" fixed+ &&\n \ttest_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent\n '\n \n-- \n2.44.0\n\n"},{"id":"490424","messageId":"aa5c9743ad2efa17dc96536144bd938fdcfed584.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 08/13] builtin/config: introduce \"set\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:20:56Z","receivedAt":"2024-03-11T23:21:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"set\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 38 +++++++++------\n builtin/config.c             | 57 ++++++++++++++++++++++\n t/t1300-config.sh            | 92 +++++++++++++++++++-----------------\n 3 files changed, 129 insertions(+), 58 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex f3d5e3e613..e540f41b56 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -11,9 +11,7 @@ SYNOPSIS\n [verse]\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' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n-'git config' [<file-option>] [--type=<type>] --add <name> <value>\n-'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n+'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n@@ -27,7 +25,7 @@ You can query/set/replace/unset options with this command. The name is\n actually the section and the key separated by a dot, and the value will be\n escaped.\n \n-Multiple lines can be added to an option by using the `--add` option.\n+Multiple lines can be added to an option by using the `--append` option.\n If you want to update or unset an option which can occur on multiple\n lines, a `value-pattern` (which is an extended regular expression,\n unless the `--fixed-value` option is given) needs to be given.  Only the\n@@ -82,6 +80,13 @@ get::\n \tlast value if multiple key values were found. If `--all` is set, then\n \tall values will be shown.\n \n+set::\n+\tSet value for one or more config options. By default, this command\n+\trefuses to write multi-valued config options. Passing `--all` will\n+\treplace all multi-valued config options with the new value, whereas\n+\t`--value=` will replace all config options whose values match the given\n+\tpattern.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -90,10 +95,9 @@ OPTIONS\n \tDefault behavior is to replace at most one line. This replaces\n \tall lines matching the key (and optionally the `value-pattern`).\n \n---add::\n+--append::\n \tAdds a new line to the option without altering any existing\n-\tvalues.  This is the same as providing '^$' as the `value-pattern`\n-\tin `--replace-all`.\n+\tvalues. This is the same as providing '--value=^$' in `set`.\n \n --all::\n \tWith \"get\", Return all values for a multi-valued key.\n@@ -283,6 +287,9 @@ recommended to migrate to the new syntax.\n 'git config <name>'::\n \tReplaced by `git config get <name>`.\n \n+'git config <name> <value> [<value-pattern>]'::\n+\tReplaced by `git config set [--value=<pattern>] <name> <value>`.\n+\n -l::\n --list::\n \tReplaced by `git config list`.\n@@ -302,6 +309,9 @@ recommended to migrate to the new syntax.\n --get-color <name> [<default>]::\n \tReplaced by `git config get --type=color [--default=<default>] <name>`.\n \n+--add <name> <value>::\n+\tReplaced by `git config set --append <name> <value>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n@@ -348,7 +358,7 @@ precedence over values read earlier.  When multiple values are taken then all\n values of a key from all files will be used.\n \n By default, options are only written to the repository specific\n-configuration file. Note that this also affects options like `--replace-all`\n+configuration file. Note that this also affects options like `set`\n and `--unset`. *'git config' will only ever change one file at a time*.\n \n You can limit which configuration sources are read from or written to by\n@@ -484,7 +494,7 @@ Given a .git/config like this:\n you can set the filemode to true with\n \n ------------\n-% git config core.filemode true\n+% git config set core.filemode true\n ------------\n \n The hypothetical proxy command entries actually have a postfix to discern\n@@ -492,7 +502,7 @@ what URL they apply to. Here is how to change the entry for kernel.org\n to \"ssh\".\n \n ------------\n-% git config core.gitproxy '\"ssh\" for kernel.org' 'for kernel.org$'\n+% git config set --value='for kernel.org$' core.gitproxy '\"ssh\" for kernel.org'\n ------------\n \n This makes sure that only the key/value pair for kernel.org is replaced.\n@@ -534,26 +544,26 @@ If you like to live dangerously, you can replace *all* core.gitproxy by a\n new one with\n \n ------------\n-% git config --replace-all core.gitproxy ssh\n+% git config set --all core.gitproxy ssh\n ------------\n \n However, if you really only want to replace the line for the default proxy,\n i.e. the one without a \"for ...\" postfix, do something like this:\n \n ------------\n-% git config core.gitproxy ssh '! for '\n+% git config set --value='! for ' core.gitproxy ssh\n ------------\n \n To actually match only values with an exclamation mark, you have to\n \n ------------\n-% git config section.key value '[!]'\n+% git config set --value='[!]' section.key value\n ------------\n \n To add a new proxy, without altering any of the existing ones, use\n \n ------------\n-% git config --add core.gitproxy '\"proxy-command\" for example.com'\n+% git config set --append core.gitproxy '\"proxy-command\" for example.com'\n ------------\n \n An example to use customized color from the configuration in your\ndiff --git a/builtin/config.c b/builtin/config.c\nindex aaa8b15e86..18f6dc69d5 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -18,6 +18,7 @@\n static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n+\tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tNULL\n };\n \n@@ -31,6 +32,11 @@ static const char *const builtin_config_get_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_set_usage[] = {\n+\tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -847,9 +853,60 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix)\n \treturn get_value(argv[0], value_pattern, flags);\n }\n \n+static int cmd_config_set(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL;\n+\tint flags = 0, append = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_TYPE_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter\")),\n+\t\tOPT_BIT(0, \"all\", &flags, N_(\"replace multi-valued config option with new value\"), CONFIG_FLAGS_MULTI_REPLACE),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_BOOL(0, \"append\", &append, N_(\"add a new line without altering any existing values\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\tchar *value;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with --value=<pattern>\"));\n+\tif (append && value_pattern)\n+\t\tdie(_(\"--append cannot be used with --value=<pattern>\"));\n+\tif (append)\n+\t\tvalue_pattern = CONFIG_REGEX_NONE;\n+\n+\thandle_config_location(prefix);\n+\n+\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n+\n+\tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {\n+\t\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t\t     argv[0], value, value_pattern,\n+\t\t\t\t\t\t\t     flags);\n+\t} else {\n+\t\tret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);\n+\t\tif (ret == CONFIG_NOTHING_SET)\n+\t\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n+\t\t\t\"       Use a regexp, --add or --replace-all to change %s.\"), argv[0]);\n+\t}\n+\n+\tfree(value);\n+\treturn ret;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n+\tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 7f6746936e..2c2d97e0e9 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -20,12 +20,16 @@ legacy)\n \tmode_get=\"\"\n \tmode_get_all=\"--get-all\"\n \tmode_get_regexp=\"--get-regexp\"\n+\tmode_set=\"\"\n+\tmode_replace_all=\"--replace-all\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n \tmode_get=\"get\"\n \tmode_get_all=\"get --all\"\n \tmode_get_regexp=\"get --regexp --all --show-names\"\n+\tmode_set=\"set\"\n+\tmode_replace_all=\"set --all\"\n \t;;\n *)\n \techo \"unknown mode $mode\" >&2\n@@ -41,7 +45,7 @@ cat > expect << EOF\n \tpenguin = little blue\n EOF\n test_expect_success 'initial' '\n-\tgit config section.penguin \"little blue\" &&\n+\tgit config ${mode_set} section.penguin \"little blue\" &&\n \ttest_cmp expect .git/config\n '\n \n@@ -51,7 +55,7 @@ cat > expect << EOF\n \tMovie = BadPhysics\n EOF\n test_expect_success 'mixed case' '\n-\tgit config Section.Movie BadPhysics &&\n+\tgit config ${mode_set} Section.Movie BadPhysics &&\n \ttest_cmp expect .git/config\n '\n \n@@ -63,7 +67,7 @@ cat > expect << EOF\n \tWhatEver = Second\n EOF\n test_expect_success 'similar section' '\n-\tgit config Sections.WhatEver Second &&\n+\tgit config ${mode_set} Sections.WhatEver Second &&\n \ttest_cmp expect .git/config\n '\n \n@@ -76,7 +80,7 @@ cat > expect << EOF\n \tWhatEver = Second\n EOF\n test_expect_success 'uppercase section' '\n-\tgit config SECTION.UPPERCASE true &&\n+\tgit config ${mode_set} SECTION.UPPERCASE true &&\n \ttest_cmp expect .git/config\n '\n \n@@ -192,14 +196,14 @@ test_expect_success 'multiple unset is correct' '\n cp .git/config2 .git/config\n \n test_expect_success '--replace-all missing value' '\n-\ttest_must_fail git config --replace-all beta.haha &&\n+\ttest_must_fail git config ${mode_replace_all} beta.haha &&\n \ttest_cmp .git/config2 .git/config\n '\n \n rm .git/config2\n \n test_expect_success '--replace-all' '\n-\tgit config --replace-all beta.haha gamma\n+\tgit config ${mode_replace_all} beta.haha gamma\n '\n \n cat > expect << EOF\n@@ -226,7 +230,7 @@ noIndent= sillyValue ; 'nother silly comment\n [nextSection] noNewline = ouch\n EOF\n test_expect_success 'really mean test' '\n-\tgit config beta.haha alpha &&\n+\tgit config ${mode_set} beta.haha alpha &&\n \ttest_cmp expect .git/config\n '\n \n@@ -241,7 +245,7 @@ noIndent= sillyValue ; 'nother silly comment\n \tnonewline = wow\n EOF\n test_expect_success 'really really mean test' '\n-\tgit config nextsection.nonewline wow &&\n+\tgit config ${mode_set} nextsection.nonewline wow &&\n \ttest_cmp expect .git/config\n '\n \n@@ -715,16 +719,16 @@ EOF\n \n test_expect_success 'section ending' '\n \trm -f .git/config &&\n-\tgit config gitcvs.enabled true &&\n-\tgit config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&\n-\tgit config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&\n+\tgit config ${mode_set} gitcvs.enabled true &&\n+\tgit config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&\n+\tgit config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&\n \ttest_cmp expect .git/config\n \n '\n \n test_expect_success numbers '\n-\tgit config kilo.gram 1k &&\n-\tgit config mega.ton 1m &&\n+\tgit config ${mode_set} kilo.gram 1k &&\n+\tgit config ${mode_set} mega.ton 1m &&\n \techo 1024 >expect &&\n \techo 1048576 >>expect &&\n \tgit config --int --get kilo.gram >actual &&\n@@ -733,20 +737,20 @@ test_expect_success numbers '\n '\n \n test_expect_success '--int is at least 64 bits' '\n-\tgit config giga.watts 121g &&\n+\tgit config ${mode_set} giga.watts 121g &&\n \techo  >expect &&\n \ttest_cmp_config 129922760704 --int --get giga.watts\n '\n \n test_expect_success 'invalid unit' '\n-\tgit config aninvalid.unit \"1auto\" &&\n+\tgit config ${mode_set} aninvalid.unit \"1auto\" &&\n \ttest_cmp_config 1auto aninvalid.unit &&\n \ttest_must_fail git config --int --get aninvalid.unit 2>actual &&\n \ttest_grep \"bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit\" actual\n '\n \n test_expect_success 'invalid unit boolean' '\n-\tgit config commit.gpgsign \"1true\" &&\n+\tgit config ${mode_set} commit.gpgsign \"1true\" &&\n \ttest_cmp_config 1true commit.gpgsign &&\n \ttest_must_fail git config --bool --get commit.gpgsign 2>actual &&\n \ttest_grep \"bad boolean config value .1true. for .commit.gpgsign.\" actual\n@@ -776,14 +780,14 @@ EOF\n \n test_expect_success bool '\n \n-\tgit config bool.true1 01 &&\n-\tgit config bool.true2 -1 &&\n-\tgit config bool.true3 YeS &&\n-\tgit config bool.true4 true &&\n-\tgit config bool.false1 000 &&\n-\tgit config bool.false2 \"\" &&\n-\tgit config bool.false3 nO &&\n-\tgit config bool.false4 FALSE &&\n+\tgit config ${mode_set} bool.true1 01 &&\n+\tgit config ${mode_set} bool.true2 -1 &&\n+\tgit config ${mode_set} bool.true3 YeS &&\n+\tgit config ${mode_set} bool.true4 true &&\n+\tgit config ${mode_set} bool.false1 000 &&\n+\tgit config ${mode_set} bool.false2 \"\" &&\n+\tgit config ${mode_set} bool.false3 nO &&\n+\tgit config ${mode_set} bool.false4 FALSE &&\n \trm -f result &&\n \tfor i in 1 2 3 4\n \tdo\n@@ -794,7 +798,7 @@ test_expect_success bool '\n \n test_expect_success 'invalid bool (--get)' '\n \n-\tgit config bool.nobool foobar &&\n+\tgit config ${mode_set} bool.nobool foobar &&\n \ttest_must_fail git config --bool --get bool.nobool'\n \n test_expect_success 'invalid bool (set)' '\n@@ -983,7 +987,7 @@ test_expect_success 'get --expiry-date' '\n \n test_expect_success 'get --type=color' '\n \trm .git/config &&\n-\tgit config foo.color \"red\" &&\n+\tgit config ${mode_set} foo.color \"red\" &&\n \tgit config --get --type=color foo.color >actual.raw &&\n \ttest_decode_color <actual.raw >actual &&\n \techo \"<RED>\" >expect &&\n@@ -1020,10 +1024,10 @@ cat > expect << EOF\n EOF\n test_expect_success 'quoting' '\n \trm -f .git/config &&\n-\tgit config quote.leading \" test\" &&\n-\tgit config quote.ending \"test \" &&\n-\tgit config quote.semicolon \"test;test\" &&\n-\tgit config quote.hash \"test#test\" &&\n+\tgit config ${mode_set} quote.leading \" test\" &&\n+\tgit config ${mode_set} quote.ending \"test \" &&\n+\tgit config ${mode_set} quote.semicolon \"test;test\" &&\n+\tgit config ${mode_set} quote.hash \"test#test\" &&\n \ttest_cmp expect .git/config\n '\n \n@@ -1031,7 +1035,7 @@ test_expect_success 'key with newline' '\n \ttest_must_fail git config ${mode_get} \"key.with\n newline\" 123'\n \n-test_expect_success 'value with newline' 'git config key.sub value.with\\\\\\\n+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\\\\\n newline'\n \n cat > .git/config <<\\EOF\n@@ -1089,7 +1093,7 @@ test_expect_success '--null --get-regexp' '\n '\n \n test_expect_success 'inner whitespace kept verbatim' '\n-\tgit config section.val \"foo \t  bar\" &&\n+\tgit config ${mode_set} section.val \"foo \t  bar\" &&\n \ttest_cmp_config \"foo \t  bar\" section.val\n '\n \n@@ -1127,12 +1131,12 @@ test_expect_success 'check split_cmdline return' '\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tgit config alias.split-cmdline-fix \"echo \\\"\" &&\n+\t\tgit config ${mode_set} alias.split-cmdline-fix \"echo \\\"\" &&\n \t\ttest_must_fail git split-cmdline-fix &&\n \t\techo foo >foo &&\n \t\tgit add foo &&\n \t\tgit commit -m \"initial commit\" &&\n-\t\tgit config branch.main.mergeoptions \"echo \\\"\" &&\n+\t\tgit config ${mode_set} branch.main.mergeoptions \"echo \\\"\" &&\n \t\ttest_must_fail git merge main\n \t)\n '\n@@ -1170,12 +1174,12 @@ test_expect_success 'key sanity-checking' '\n \ttest_must_fail git config ${mode_get} foo.1bar &&\n \ttest_must_fail git config ${mode_get} foo.\"ba\n \t\t\t\tz\".bar &&\n-\ttest_must_fail git config . false &&\n-\ttest_must_fail git config .foo false &&\n-\ttest_must_fail git config foo. false &&\n-\ttest_must_fail git config .foo. false &&\n-\tgit config foo.bar true &&\n-\tgit config foo.\"ba =z\".bar false\n+\ttest_must_fail git config ${mode_set} . false &&\n+\ttest_must_fail git config ${mode_set} .foo false &&\n+\ttest_must_fail git config ${mode_set} foo. false &&\n+\ttest_must_fail git config ${mode_set} .foo. false &&\n+\tgit config ${mode_set} foo.bar true &&\n+\tgit config ${mode_set} foo.\"ba =z\".bar false\n '\n \n test_expect_success 'git -c works with aliases of builtins' '\n@@ -2398,7 +2402,7 @@ test_expect_success '--replace-all does not invent newlines' '\n \t[abc]\n \tQkey = b\n \tEOF\n-\tgit config --replace-all abc.key b &&\n+\tgit config ${mode_replace_all} abc.key b &&\n \ttest_cmp expect .git/config\n '\n \n@@ -2470,8 +2474,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n \n \t# These modes complain when --fixed-value has no value-pattern\n-\ttest_must_fail git config --file=config --fixed-value dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&\n+\ttest_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&\n+\ttest_must_fail git config ${mode_replace_all} --file=config --fixed-value dev.null bogus &&\n \ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n@@ -2512,7 +2516,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n-\tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n+\tgit config --file=config fixed.test bogus \"$META\" &&\n \tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n-- \n2.44.0\n\n"},{"id":"490425","messageId":"c8a44b6189ad247d7b5a4b6e490ffa89c8461ddd.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 09/13] builtin/config: introduce \"unset\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:21:05Z","receivedAt":"2024-03-11T23:21:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"unset\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 25 ++++++++++++--------\n builtin/config.c             | 38 +++++++++++++++++++++++++++++++\n t/t1300-config.sh            | 44 ++++++++++++++++++++++++------------\n 3 files changed, 82 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex e540f41b56..42e659ae63 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -12,8 +12,7 @@ SYNOPSIS\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' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n-'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n+'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n@@ -87,6 +86,12 @@ set::\n \t`--value=` will replace all config options whose values match the given\n \tpattern.\n \n+unset::\n+\tUnset value for one or more config options. By default, this command\n+\trefuses to unset multi-valued keys. Passing `--all` will unset all\n+\tmulti-valued config options, whereas `--value` will unset all config\n+\toptions whose values match the given pattern.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -178,12 +183,6 @@ See also <<FILES>>.\n --rename-section::\n \tRename the given section to a new name.\n \n---unset::\n-\tRemove the line matching the key from config file.\n-\n---unset-all::\n-\tRemove all lines matching the key from config file.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -312,6 +311,12 @@ recommended to migrate to the new syntax.\n --add <name> <value>::\n \tReplaced by `git config set --append <name> <value>`.\n \n+--unset <name> [<value-pattern>]::\n+\tReplaced by `git config unset [--value=<pattern>] <name>`.\n+\n+--unset-all <name> [<value-pattern>]::\n+\tReplaced by `git config unset [--value=<pattern>] --all <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n@@ -359,7 +364,7 @@ values of a key from all files will be used.\n \n By default, options are only written to the repository specific\n configuration file. Note that this also affects options like `set`\n-and `--unset`. *'git config' will only ever change one file at a time*.\n+and `unset`. *'git config' will only ever change one file at a time*.\n \n You can limit which configuration sources are read from or written to by\n specifying the path of a file with the `--file` option, or by specifying a\n@@ -510,7 +515,7 @@ This makes sure that only the key/value pair for kernel.org is replaced.\n To delete the entry for renames, do\n \n ------------\n-% git config --unset diff.renames\n+% git config unset diff.renames\n ------------\n \n If you want to delete an entry for a multivar (like core.gitproxy above),\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 18f6dc69d5..e62a09061f 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -19,6 +19,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tNULL\n };\n \n@@ -37,6 +38,11 @@ static const char *const builtin_config_set_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_unset_usage[] = {\n+\tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -903,10 +909,42 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix)\n \treturn ret;\n }\n \n+static int cmd_config_unset(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL;\n+\tint flags = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter\")),\n+\t\tOPT_BIT(0, \"all\", &flags, N_(\"replace multi-valued config option with new value\"), CONFIG_FLAGS_MULTI_REPLACE),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_unset_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 1);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with 'value-pattern'\"));\n+\n+\thandle_config_location(prefix);\n+\n+\tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)\n+\t\treturn git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t\t      argv[0], NULL, value_pattern,\n+\t\t\t\t\t\t\t      flags);\n+\telse\n+\t\treturn git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n+\tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 2c2d97e0e9..06d34b1d6a 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -22,6 +22,8 @@ legacy)\n \tmode_get_regexp=\"--get-regexp\"\n \tmode_set=\"\"\n \tmode_replace_all=\"--replace-all\"\n+\tmode_unset=\"--unset\"\n+\tmode_unset_all=\"--unset-all\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n@@ -30,6 +32,8 @@ subcommands)\n \tmode_get_regexp=\"get --regexp --all --show-names\"\n \tmode_set=\"set\"\n \tmode_replace_all=\"set --all\"\n+\tmode_unset=\"unset\"\n+\tmode_unset_all=\"unset --all\"\n \t;;\n *)\n \techo \"unknown mode $mode\" >&2\n@@ -150,7 +154,7 @@ foo = bar\n EOF\n \n test_expect_success 'unset with cont. lines' '\n-\tgit config --unset beta.baz\n+\tgit config ${mode_unset} beta.baz\n '\n \n cat > expect <<\\EOF\n@@ -177,7 +181,7 @@ EOF\n cp .git/config .git/config2\n \n test_expect_success 'multiple unset' '\n-\tgit config --unset-all beta.haha\n+\tgit config ${mode_unset_all} beta.haha\n '\n \n cat > expect << EOF\n@@ -263,7 +267,7 @@ noIndent= sillyValue ; 'nother silly comment\n \tnonewline = wow\n EOF\n test_expect_success 'unset' '\n-\tgit config --unset beta.haha &&\n+\tgit config ${mode_unset} beta.haha &&\n \ttest_cmp expect .git/config\n '\n \n@@ -319,11 +323,11 @@ test_expect_success 'multivar replace' '\n '\n \n test_expect_success 'ambiguous unset' '\n-\ttest_must_fail git config --unset nextsection.nonewline\n+\ttest_must_fail git config ${mode_unset} nextsection.nonewline\n '\n \n test_expect_success 'invalid unset' '\n-\ttest_must_fail git config --unset somesection.nonewline\n+\ttest_must_fail git config ${mode_unset} somesection.nonewline\n '\n \n cat > expect << EOF\n@@ -337,7 +341,12 @@ noIndent= sillyValue ; 'nother silly comment\n EOF\n \n test_expect_success 'multivar unset' '\n-\tgit config --unset nextsection.nonewline \"wow3$\" &&\n+\tcase \"$mode\" in\n+\tlegacy)\n+\t\tgit config --unset nextsection.nonewline \"wow3$\";;\n+\tsubcommands)\n+\t\tgit config unset --value=\"wow3$\" nextsection.nonewline;;\n+\tesac &&\n \ttest_cmp expect .git/config\n '\n \n@@ -1888,7 +1897,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t# please be careful when you update the above variable\n \tEOF\n \n-\tgit config --unset section.key &&\n+\tgit config ${mode_unset} section.key &&\n \ttest_cmp expect .git/config &&\n \n \tcat >.git/config <<-\\EOF &&\n@@ -1901,7 +1910,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[next-section]\n \tEOF\n \n-\tgit config --unset section.key &&\n+\tgit config ${mode_unset} section.key &&\n \ttest_cmp expect .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1911,7 +1920,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[two]\n \tkey = true\n \tEOF\n-\tgit config --unset two.key &&\n+\tgit config ${mode_unset} two.key &&\n \t! grep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1921,7 +1930,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[one]\n \tkey = true\n \tEOF\n-\tgit config --unset-all one.key &&\n+\tgit config ${mode_unset_all} one.key &&\n \ttest_line_count = 0 .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1931,7 +1940,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[two]\n \tQkey = true\n \tEOF\n-\tgit config --unset two.key &&\n+\tgit config ${mode_unset} two.key &&\n \tgrep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1943,7 +1952,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[TWO \"subsection\"]\n \t[one]\n \tEOF\n-\tgit config --unset two.subsection.key &&\n+\tgit config ${mode_unset} two.subsection.key &&\n \ttest \"not [two subsection]\" = \"$(git config ${mode_get} one.key)\" &&\n \ttest_line_count = 3 .git/config\n '\n@@ -1955,7 +1964,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' '\n \tkey = value2\n \tEOF\n \n-\tgit config --unset-all section.key &&\n+\tgit config ${mode_unset_all} section.key &&\n \ttest_line_count = 0 .git/config\n '\n \n@@ -2479,8 +2488,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n-\ttest_must_fail git config --file=config --fixed-value --unset dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --unset-all dev.null\n+\ttest_must_fail git config ${mode_unset} --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_unset_all} --file=config --fixed-value dev.null\n '\n \n test_expect_success '--fixed-value uses exact string matching' '\n@@ -2510,6 +2519,11 @@ test_expect_success '--fixed-value uses exact string matching' '\n \tgit config --file=config --fixed-value --unset fixed.test \"$META\" &&\n \ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n+\tcp initial config &&\n+\ttest_must_fail git config unset --file=config --value=\"$META\" fixed.test &&\n+\tgit config unset --file=config --fixed-value --value=\"$META\" fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n+\n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset-all fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset-all fixed.test \"$META\" &&\n-- \n2.44.0\n\n"},{"id":"490426","messageId":"a3c66431b0c3fd71a89417cce98ba42a722d3ea4.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 10/13] builtin/config: introduce \"rename-section\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:21:16Z","receivedAt":"2024-03-11T23:21:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"rename-section\" subcommand to git-config(1). Please\nrefer to preceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 11 +++++++----\n builtin/config.c             | 32 ++++++++++++++++++++++++++++++++\n t/t1300-config.sh            | 22 +++++++++++-----------\n 3 files changed, 50 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 42e659ae63..ebde360c1e 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -13,7 +13,7 @@ SYNOPSIS\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' [<file-option>] --rename-section <old-name> <new-name>\n+'git config rename-section' [<file-option>] <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n@@ -92,6 +92,9 @@ unset::\n \tmulti-valued config options, whereas `--value` will unset all config\n \toptions whose values match the given pattern.\n \n+rename-section::\n+\tRename the given section to a new name.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -180,9 +183,6 @@ See also <<FILES>>.\n --remove-section::\n \tRemove the given section from the configuration file.\n \n---rename-section::\n-\tRename the given section to a new name.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -317,6 +317,9 @@ recommended to migrate to the new syntax.\n --unset-all <name> [<value-pattern>]::\n \tReplaced by `git config unset [--value=<pattern>] --all <name>`.\n \n+--rename-section <old-name> <new-name>::\n+\tReplaced by `git config rename-section <old-name> <new-name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex e62a09061f..09e8f47b8e 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -20,6 +20,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tNULL\n };\n \n@@ -43,6 +44,11 @@ static const char *const builtin_config_unset_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_rename_section_usage[] = {\n+\tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -940,11 +946,37 @@ static int cmd_config_unset(int argc, const char **argv, const char *prefix)\n \t\treturn git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);\n }\n \n+static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_rename_section_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\n+\thandle_config_location(prefix);\n+\n+\tret = git_config_rename_section_in_file(given_config_source.file,\n+\t\t\t\t\t\targv[0], argv[1]);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!ret)\n+\t\tdie(_(\"no such section: %s\"), argv[0]);\n+\n+\treturn 0;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n+\tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 06d34b1d6a..0020296df6 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -590,7 +590,7 @@ weird\n EOF\n \n test_expect_success 'rename section' '\n-\tgit config --rename-section branch.eins branch.zwei\n+\tgit config ${mode_prefix}rename-section branch.eins branch.zwei\n '\n \n cat > expect << EOF\n@@ -609,7 +609,7 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'rename non-existing section' '\n-\ttest_must_fail git config --rename-section \\\n+\ttest_must_fail git config ${mode_prefix}rename-section \\\n \t\tbranch.\"world domination\" branch.drei\n '\n \n@@ -618,7 +618,7 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'rename another section' '\n-\tgit config --rename-section branch.\"1 234 blabl/a\" branch.drei\n+\tgit config ${mode_prefix}rename-section branch.\"1 234 blabl/a\" branch.drei\n '\n \n cat > expect << EOF\n@@ -641,7 +641,7 @@ cat >> .git/config << EOF\n EOF\n \n test_expect_success 'rename a section with a var on the same line' '\n-\tgit config --rename-section branch.vier branch.zwei\n+\tgit config ${mode_prefix}rename-section branch.vier branch.zwei\n '\n \n cat > expect << EOF\n@@ -662,11 +662,11 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'renaming empty section name is rejected' '\n-\ttest_must_fail git config --rename-section branch.zwei \"\"\n+\ttest_must_fail git config ${mode_prefix}rename-section branch.zwei \"\"\n '\n \n test_expect_success 'renaming to bogus section is rejected' '\n-\ttest_must_fail git config --rename-section branch.zwei \"bogus name\"\n+\ttest_must_fail git config ${mode_prefix}rename-section branch.zwei \"bogus name\"\n '\n \n test_expect_success 'renaming a section with a long line' '\n@@ -675,7 +675,7 @@ test_expect_success 'renaming a section with a long line' '\n \t\tprintf \"  c = d %1024s [a] e = f\\\\n\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\tgit config -f y --rename-section a xyz &&\n+\tgit config ${mode_prefix}rename-section -f y a xyz &&\n \ttest_must_fail git config -f y b.e\n '\n \n@@ -685,7 +685,7 @@ test_expect_success 'renaming an embedded section with a long line' '\n \t\tprintf \"  c = d %1024s [a] [foo] e = f\\\\n\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\tgit config -f y --rename-section a xyz &&\n+\tgit config ${mode_prefix}rename-section -f y a xyz &&\n \ttest_must_fail git config -f y foo.e\n '\n \n@@ -695,7 +695,7 @@ test_expect_success 'renaming a section with an overly-long line' '\n \t\tprintf \"  c = d %525000s e\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\ttest_must_fail git config -f y --rename-section a xyz 2>err &&\n+\ttest_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&\n \tgrep \"refusing to work with overly long line in .y. on line 2\" err\n '\n \n@@ -1987,7 +1987,7 @@ test_expect_success POSIXPERM,PERL 'preserves existing permissions' '\n \tgit config imap.pass Hunter2 &&\n \tperl -e \\\n \t  \"die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600\" &&\n-\tgit config --rename-section imap pop &&\n+\tgit config ${mode_prefix}rename-section imap pop &&\n \tperl -e \\\n \t  \"die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600\"\n '\n@@ -2476,7 +2476,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --add dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --rename-section dev null &&\n+\ttest_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&\n \ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n \ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n-- \n2.44.0\n\n"},{"id":"490427","messageId":"6314d51cec67d35b5d3e168d67e9d98790367cf0.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 11/13] builtin/config: introduce \"remove-section\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:21:25Z","receivedAt":"2024-03-11T23:21:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"remove-section\" subcommand to git-config(1). Please\nrefer to preceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 11 +++++++----\n builtin/config.c             | 32 ++++++++++++++++++++++++++++++++\n t/t1300-config.sh            |  4 ++--\n 3 files changed, 41 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex ebde360c1e..615ed44350 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -14,7 +14,7 @@ SYNOPSIS\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' [<file-option>] --remove-section <name>\n+'git config remove-section' [<file-option>] <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n \n@@ -95,6 +95,9 @@ unset::\n rename-section::\n \tRename the given section to a new name.\n \n+remove-section::\n+\tRemove the given section from the configuration file.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -180,9 +183,6 @@ See also <<FILES>>.\n \tsection in linkgit:gitrevisions[7] for a more complete list of\n \tways to spell blob names.\n \n---remove-section::\n-\tRemove the given section from the configuration file.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -320,6 +320,9 @@ recommended to migrate to the new syntax.\n --rename-section <old-name> <new-name>::\n \tReplaced by `git config rename-section <old-name> <new-name>`.\n \n+--remove-section <name>::\n+\tReplaced by `git config remove-section <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 09e8f47b8e..d3aa5bc298 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -21,6 +21,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n+\tN_(\"git config remove-section [<file-option>] <name>\"),\n \tNULL\n };\n \n@@ -49,6 +50,11 @@ static const char *const builtin_config_rename_section_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_remove_section_usage[] = {\n+\tN_(\"git config remove-section [<file-option>] <name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -971,12 +977,38 @@ static int cmd_config_rename_section(int argc, const char **argv, const char *pr\n \treturn 0;\n }\n \n+static int cmd_config_remove_section(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_remove_section_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 1);\n+\n+\thandle_config_location(prefix);\n+\n+\tret = git_config_rename_section_in_file(given_config_source.file,\n+\t\t\t\t\t\targv[0], NULL);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!ret)\n+\t\tdie(_(\"no such section: %s\"), argv[0]);\n+\n+\treturn 0;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n+\tOPT_SUBCOMMAND(\"remove-section\", &subcommand, cmd_config_remove_section),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 0020296df6..eafecf84a4 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -704,7 +704,7 @@ cat >> .git/config << EOF\n EOF\n \n test_expect_success 'remove section' '\n-\tgit config --remove-section branch.zwei\n+\tgit config ${mode_prefix}remove-section branch.zwei\n '\n \n cat > expect << EOF\n@@ -2477,7 +2477,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&\n-\ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n+\ttest_must_fail git config ${mode_prefix}remove-section --file=config --fixed-value dev &&\n \ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n-- \n2.44.0\n\n"},{"id":"490428","messageId":"12011714e3ee1459fb078850f1c1ebd6d275af0e.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 12/13] builtin/config: introduce \"edit\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:21:36Z","receivedAt":"2024-03-11T23:21:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"edit\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 15 ++++---\n builtin/config.c             | 81 ++++++++++++++++++++++++------------\n t/t1300-config.sh            |  6 +--\n 3 files changed, 67 insertions(+), 35 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 615ed44350..9fb94e2fad 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -15,8 +15,8 @@ SYNOPSIS\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>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n-'git config' [<file-option>] -e | --edit\n \n DESCRIPTION\n -----------\n@@ -98,6 +98,10 @@ rename-section::\n remove-section::\n \tRemove the given section from the configuration file.\n \n+edit::\n+\tOpens an editor to modify the specified config file; either\n+\t`--system`, `--global`, or repository (default).\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -262,11 +266,6 @@ Valid `<type>`'s include:\n \tWhen the color setting for `name` is undefined, the command uses\n \t`color.ui` as fallback.\n \n--e::\n---edit::\n-\tOpens an editor to modify the specified config file; either\n-\t`--system`, `--global`, or repository (default).\n-\n --[no-]includes::\n \tRespect `include.*` directives in config files when looking up\n \tvalues. Defaults to `off` when a specific file is given (e.g.,\n@@ -323,6 +322,10 @@ recommended to migrate to the new syntax.\n --remove-section <name>::\n \tReplaced by `git config remove-section <name>`.\n \n+-e::\n+--edit::\n+\tReplaced by `git config edit`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex d3aa5bc298..b1008c487d 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -22,6 +22,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tN_(\"git config remove-section [<file-option>] <name>\"),\n+\tN_(\"git config edit [<file-option>]\"),\n \tNULL\n };\n \n@@ -55,6 +56,11 @@ static const char *const builtin_config_remove_section_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_edit_usage[] = {\n+\tN_(\"git config edit [<file-option>]\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -1002,6 +1008,53 @@ static int cmd_config_remove_section(int argc, const char **argv, const char *pr\n \treturn 0;\n }\n \n+static int show_editor(void)\n+{\n+\tchar *config_file;\n+\n+\tif (!given_config_source.file && !startup_info->have_repository)\n+\t\tdie(_(\"not in a git directory\"));\n+\tif (given_config_source.use_stdin)\n+\t\tdie(_(\"editing stdin is not supported\"));\n+\tif (given_config_source.blob)\n+\t\tdie(_(\"editing blobs is not supported\"));\n+\tgit_config(git_default_config, NULL);\n+\tconfig_file = given_config_source.file ?\n+\t\t\txstrdup(given_config_source.file) :\n+\t\t\tgit_pathdup(\"config\");\n+\tif (use_global_config) {\n+\t\tint fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);\n+\t\tif (fd >= 0) {\n+\t\t\tchar *content = default_user_config();\n+\t\t\twrite_str_in_full(fd, content);\n+\t\t\tfree(content);\n+\t\t\tclose(fd);\n+\t\t}\n+\t\telse if (errno != EEXIST)\n+\t\t\tdie_errno(_(\"cannot create configuration file %s\"), config_file);\n+\t}\n+\tlaunch_editor(config_file, NULL, NULL);\n+\tfree(config_file);\n+\n+\treturn 0;\n+}\n+\n+static int cmd_config_edit(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_edit_usage, 0);\n+\tcheck_write();\n+\tcheck_argc(argc, 0, 0);\n+\n+\thandle_config_location(prefix);\n+\n+\treturn show_editor();\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n@@ -1009,6 +1062,7 @@ static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n \tOPT_SUBCOMMAND(\"remove-section\", &subcommand, cmd_config_remove_section),\n+\tOPT_SUBCOMMAND(\"edit\", &subcommand, cmd_config_edit),\n \tOPT_END(),\n };\n \n@@ -1127,32 +1181,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\t}\n \t}\n \telse if (actions == ACTION_EDIT) {\n-\t\tchar *config_file;\n-\n-\t\tcheck_argc(argc, 0, 0);\n-\t\tif (!given_config_source.file && !startup_info->have_repository)\n-\t\t\tdie(_(\"not in a git directory\"));\n-\t\tif (given_config_source.use_stdin)\n-\t\t\tdie(_(\"editing stdin is not supported\"));\n-\t\tif (given_config_source.blob)\n-\t\t\tdie(_(\"editing blobs is not supported\"));\n-\t\tgit_config(git_default_config, NULL);\n-\t\tconfig_file = given_config_source.file ?\n-\t\t\t\txstrdup(given_config_source.file) :\n-\t\t\t\tgit_pathdup(\"config\");\n-\t\tif (use_global_config) {\n-\t\t\tint fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);\n-\t\t\tif (fd >= 0) {\n-\t\t\t\tchar *content = default_user_config();\n-\t\t\t\twrite_str_in_full(fd, content);\n-\t\t\t\tfree(content);\n-\t\t\t\tclose(fd);\n-\t\t\t}\n-\t\t\telse if (errno != EEXIST)\n-\t\t\t\tdie_errno(_(\"cannot create configuration file %s\"), config_file);\n-\t\t}\n-\t\tlaunch_editor(config_file, NULL, NULL);\n-\t\tfree(config_file);\n+\t\tret = show_editor();\n \t}\n \telse if (actions == ACTION_SET) {\n \t\tcheck_write();\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex eafecf84a4..f820f87619 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -557,7 +557,7 @@ test_expect_success 'setting a value in stdin is an error' '\n '\n \n test_expect_success 'editing stdin is an error' '\n-\ttest_must_fail git config --file - --edit\n+\ttest_must_fail git config ${mode_prefix}edit --file -\n '\n \n test_expect_success 'refer config from subdirectory' '\n@@ -1643,7 +1643,7 @@ test_expect_success 'command line overrides environment config' '\n test_expect_success 'git config --edit works' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n-\tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n+\tGIT_EDITOR=\"echo [test]value=yes >\" git config ${mode_prefix}edit -f tmp &&\n \tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n@@ -1652,7 +1652,7 @@ test_expect_success 'git config --edit respects core.editor' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n \ttest_config core.editor \"echo [test]value=yes >\" &&\n-\tgit config -f tmp --edit &&\n+\tgit config ${mode_prefix}edit -f tmp &&\n \tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n-- \n2.44.0\n\n"},{"id":"490429","messageId":"2046f6652aba21d24fbd55399aabf71f836db011.1710198711.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1710198711.git.ps@pks.im","subject":"[PATCH v2 13/13] builtin/config: display subcommand help","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-11T23:21:45Z","receivedAt":"2024-03-11T23:21:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Until now, `git config -h` would have printed help for the old-style\nsyntax. Now that all modes have proper subcommands though it is\npreferable to instead display the subcommand help.\n\nDrop the `NO_INTERNAL_HELP` flag to do so. While at it, drop the help\nmismatch in t0450 and add the `--get-colorbool` option to the usage such\nthat git-config(1)'s synopsis and `git config -h` match.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c            | 5 +++--\n t/t0450/txt-help-mismatches | 1 -\n 2 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex b1008c487d..8ba8dd77bc 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -23,6 +23,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tN_(\"git config remove-section [<file-option>] <name>\"),\n \tN_(\"git config edit [<file-option>]\"),\n+\tN_(\"git config [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\"),\n \tNULL\n };\n \n@@ -1084,10 +1085,10 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t * us when parsing the implicit modes.\n \t */\n \targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n \tif (subcommand) {\n \t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n-\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);\n \t\treturn subcommand(argc, argv, prefix);\n \t}\n \ndiff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches\nindex a0777acd66..28003f18c9 100644\n--- a/t/t0450/txt-help-mismatches\n+++ b/t/t0450/txt-help-mismatches\n@@ -10,7 +10,6 @@ checkout\n checkout-index\n clone\n column\n-config\n credential\n credential-cache\n credential-store\n-- \n2.44.0\n\n"},{"id":"490525","messageId":"CAPig+cTybDAqZeGXPi+hi5zpPD=7nGpknzM6MForaaff4H4htA@mail.gmail.com","threadId":"61061","inReplyTo":"53401299fa1f51954834e2507a2282cf60b02f20.1710198711.git.ps@pks.im","subject":"Re: [PATCH v2 06/13] builtin/config: introduce \"list\" subcommand","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2024-03-13T02:45:56Z","receivedAt":"2024-03-13T02:46:08Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Mon, Mar 11, 2024 at 7:20 PM Patrick Steinhardt <ps@pks.im> wrote:\n> [...]\n> Introduce the first such new subcommand, which is \"git config list\". To\n> retain backwards compatibility we only conditionally use subcommands and\n> will fall back to the old syntax in case no subcommand was detected.\n> This should help to transition to the new-style syntax until we\n> eventually deprecate and remove the old-style syntax.\n> [...]\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n> diff --git a/builtin/config.c b/builtin/config.c\n> @@ -761,6 +811,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n> +       /*\n> +        * This is somewhat hacky: we first parse the command line while\n> +        * keeping all args intact in order to determine whether a subcommand\n> +        * has been specified. If so, we re-parse it a second time, but this\n> +        * time we drop KEEP_ARGV0. This is so that we don't munge the command\n> +        * line in case no subcommand was given, which would otherwise confuse\n> +        * us when parsing the implicit modes.\n> +        */\n> +       argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n\nUpon reading this, I wasn't quite sure what \"when parsing the implicit\nmodes\" meant. I suppose it is referring to the legacy style command\ninvocation?\n\n> diff --git a/t/t1300-config.sh b/t/t1300-config.sh\n> @@ -11,6 +11,21 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n> +for mode in legacy subcommands\n> +do\n> +\n> +case \"$mode\" in\n> +legacy)\n> +       mode_prefix=\"--\"\n> +       ;;\n> +subcommands)\n> +       mode_prefix=\"\"\n> +       ;;\n> +*)\n> +       echo \"unknown mode $mode\" >&2\n> +       exit 1;;\n> +esac\n\nt/test-lib.sh defines a BUG() function for signaling the sort of\nprogrammer error handled by the \"*\" arm of this `case`.\n\nAn alternative simpler implementation, which wouldn't require any sort\nof programmer-error fallback, would be:\n\n    for mode_prefix in -- \"\" # legacy & subcommand modes\n    do\n        test_expect_success '...' '\n            ...\n        '\n        ...\n    done\n\n> @@ -527,6 +542,7 @@ test_expect_success 'editing stdin is an error' '\n>  test_expect_success 'refer config from subdirectory' '\n> +       test_when_finished \"rm -r x\" &&\n>         mkdir x &&\n>         test_cmp_config -C x strasse --file=../other-config --get ein.bahn\n>  '\n\nIs this an unrelated cleanup?\n\n> @@ -1072,6 +1088,7 @@ test_expect_success 'inner whitespace kept verbatim' '\n>  test_expect_success SYMLINKS 'symlinked configuration' '\n> +       test_when_finished \"rm myconfig\" &&\n>         ln -s notyet myconfig &&\n>         git config --file=myconfig test.frotz nitfol &&\n>         test -h myconfig &&\n\nDitto.\n\nSame question regarding similar changes to several later tests.\n"},{"id":"490526","messageId":"CAPig+cRwh4HCi=Q01tGJ0WOb59iE18HWSCNxGogcreOz+2w1WA@mail.gmail.com","threadId":"61061","inReplyTo":"8ceced0fc58e9b4cf1c5e5b678e864f82c1b8c01.1710198711.git.ps@pks.im","subject":"Re: [PATCH v2 07/13] builtin/config: introduce \"get\" subcommand","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2024-03-13T03:11:07Z","receivedAt":"2024-03-13T03:11:19Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Mon, Mar 11, 2024 at 7:20 PM Patrick Steinhardt <ps@pks.im> wrote:\n> Introduce a new \"get\" subcommand to git-config(1). Please refer to\n> preceding commits regarding the motivation behind this change.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n> diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\n> @@ -80,6 +76,12 @@ COMMANDS\n> +get::\n> +       Get value for one or more config options. Values can be filtered by\n> +       regexes and URLs.Returns error code 1 if the key was not found and the\n> +       last value if multiple key values were found. If `--all` is set, then\n> +       all values will be shown.\n\ns/URLs.Returns/URLs. Returns/\n\nIt's not a new problem with this description (since you're mostly just\nrelocating existing text), but I find the discussion of what is\nreturned quite confusing and difficult to interpret. Breaking it down\ninto simpler sentences might help:\n\n    Emits the value of the specified key. If key is present\n    multiple times in the configuration, emits the last\n    value. If `--all` is specified, emits all values\n    associated with key. Returns error code 1 if key\n    is not present.\n\nBut, doing so may be outside the scope of this patch series and can be\ntackled at a later date (or not at all).\n\n> @@ -93,22 +95,16 @@ OPTIONS\n> +--all::\n> +       With \"get\", Return all values for a multi-valued key.\n\ns/Return/return/\ns/\"get\"/`get`/\n\n> +---regexp::\n> +       With \"get\", interpret the name as a regular expression. Regular\n> +       expression matching is currently case-sensitive and done against a\n> +       canonicalized version of the key in which section and variable names\n> +       are lowercased, but subsection names are not.\n\ns/\"get\"/`get`/\n\n> @@ -286,7 +271,7 @@ Valid `<type>`'s include:\n>  --default <value>::\n> -  When using `--get`, and the requested variable is not found, behave as if\n> +  When using `get`, and the requested variable is not found, behave as if\n>    <value> were the value assigned to the that variable.\n\nNot a fault of this patch (and need not be fixed by this series): \"to\nthe that\" should be either \"to the\" or \"to that\".\n\n> @@ -506,25 +509,25 @@ you have to provide a regex matching the value of exactly one line.\n>  To query the value for a given key, do\n>\n>  ------------\n> -% git config --get core.filemode\n> +% git config get core.filemode\n>  ------------\n>\n>  or\n>\n>  ------------\n> -% git config core.filemode\n> +% git config get core.filemode\n>  ------------\n\nMeh. We only need to retain one of these examples now, not both, right?\n\n> diff --git a/t/t1300-config.sh b/t/t1300-config.sh\n> @@ -17,9 +17,15 @@ do\n>  case \"$mode\" in\n>  legacy)\n>         mode_prefix=\"--\"\n> +       mode_get=\"\"\n> +       mode_get_all=\"--get-all\"\n> +       mode_get_regexp=\"--get-regexp\"\n>         ;;\n>  subcommands)\n>         mode_prefix=\"\"\n> +       mode_get=\"get\"\n> +       mode_get_all=\"get --all\"\n> +       mode_get_regexp=\"get --regexp --all --show-names\"\n>         ;;\n>  *)\n>         echo \"unknown mode $mode\" >&2\n\nThe variables added by this patch to the `case` arms invalidate the\nsuggested simplification in my review of [6/13].\n"},{"id":"491655","messageId":"ZgPb_HB3792jHLto@tanuki","threadId":"61061","inReplyTo":"CAPig+cTybDAqZeGXPi+hi5zpPD=7nGpknzM6MForaaff4H4htA@mail.gmail.com","subject":"Re: [PATCH v2 06/13] builtin/config: introduce \"list\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:42:36Z","receivedAt":"2024-03-27T08:42:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Mar 12, 2024 at 10:45:56PM -0400, Eric Sunshine wrote:\n> On Mon, Mar 11, 2024 at 7:20 PM Patrick Steinhardt <ps@pks.im> wrote:\n> > [...]\n> > Introduce the first such new subcommand, which is \"git config list\". To\n> > retain backwards compatibility we only conditionally use subcommands and\n> > will fall back to the old syntax in case no subcommand was detected.\n> > This should help to transition to the new-style syntax until we\n> > eventually deprecate and remove the old-style syntax.\n> > [...]\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> > diff --git a/builtin/config.c b/builtin/config.c\n> > @@ -761,6 +811,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n> > +       /*\n> > +        * This is somewhat hacky: we first parse the command line while\n> > +        * keeping all args intact in order to determine whether a subcommand\n> > +        * has been specified. If so, we re-parse it a second time, but this\n> > +        * time we drop KEEP_ARGV0. This is so that we don't munge the command\n> > +        * line in case no subcommand was given, which would otherwise confuse\n> > +        * us when parsing the implicit modes.\n> > +        */\n> > +       argc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n> \n> Upon reading this, I wasn't quite sure what \"when parsing the implicit\n> modes\" meant. I suppose it is referring to the legacy style command\n> invocation?\n\nYeah, indeed. Let me rephrase it as \"when parsing the legacy-style\ncommand modes\", which is hopefully a bit clearer.\n\n> > diff --git a/t/t1300-config.sh b/t/t1300-config.sh\n> > @@ -11,6 +11,21 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n> > +for mode in legacy subcommands\n> > +do\n> > +\n> > +case \"$mode\" in\n> > +legacy)\n> > +       mode_prefix=\"--\"\n> > +       ;;\n> > +subcommands)\n> > +       mode_prefix=\"\"\n> > +       ;;\n> > +*)\n> > +       echo \"unknown mode $mode\" >&2\n> > +       exit 1;;\n> > +esac\n> \n> t/test-lib.sh defines a BUG() function for signaling the sort of\n> programmer error handled by the \"*\" arm of this `case`.\n\nAh, right, I'll use that.\n\n> An alternative simpler implementation, which wouldn't require any sort\n> of programmer-error fallback, would be:\n> \n>     for mode_prefix in -- \"\" # legacy & subcommand modes\n>     do\n>         test_expect_success '...' '\n>             ...\n>         '\n>         ...\n>     done\n\nIt works now, but will stop working in subsequent patches. Not all modes\ncan be translated this easily. E.g. \"git config config.key value\" needs\nto be translated to \"git config set config.key value\".\n\n> > @@ -527,6 +542,7 @@ test_expect_success 'editing stdin is an error' '\n> >  test_expect_success 'refer config from subdirectory' '\n> > +       test_when_finished \"rm -r x\" &&\n> >         mkdir x &&\n> >         test_cmp_config -C x strasse --file=../other-config --get ein.bahn\n> >  '\n> \n> Is this an unrelated cleanup?\n> \n> > @@ -1072,6 +1088,7 @@ test_expect_success 'inner whitespace kept verbatim' '\n> >  test_expect_success SYMLINKS 'symlinked configuration' '\n> > +       test_when_finished \"rm myconfig\" &&\n> >         ln -s notyet myconfig &&\n> >         git config --file=myconfig test.frotz nitfol &&\n> >         test -h myconfig &&\n> \n> Ditto.\n> \n> Same question regarding similar changes to several later tests.\n\nThese are required because we now re-run tests twice. Consequently,\nstate from the first run may still be around in the second run. The\ntests here broke because of that, which I've fixed by cleaning up the\nstate. I'll clarify this in the commit messages.\n\nPatrick\n"},{"id":"491656","messageId":"ZgPcCJdHQ8NWeWv6@tanuki","threadId":"61061","inReplyTo":"CAPig+cRwh4HCi=Q01tGJ0WOb59iE18HWSCNxGogcreOz+2w1WA@mail.gmail.com","subject":"Re: [PATCH v2 07/13] builtin/config: introduce \"get\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:42:48Z","receivedAt":"2024-03-27T08:42:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Mar 12, 2024 at 11:11:07PM -0400, Eric Sunshine wrote:\n> On Mon, Mar 11, 2024 at 7:20 PM Patrick Steinhardt <ps@pks.im> wrote:\n> > Introduce a new \"get\" subcommand to git-config(1). Please refer to\n> > preceding commits regarding the motivation behind this change.\n> >\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> > diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\n> > @@ -80,6 +76,12 @@ COMMANDS\n> > +get::\n> > +       Get value for one or more config options. Values can be filtered by\n> > +       regexes and URLs.Returns error code 1 if the key was not found and the\n> > +       last value if multiple key values were found. If `--all` is set, then\n> > +       all values will be shown.\n> \n> s/URLs.Returns/URLs. Returns/\n> \n> It's not a new problem with this description (since you're mostly just\n> relocating existing text), but I find the discussion of what is\n> returned quite confusing and difficult to interpret. Breaking it down\n> into simpler sentences might help:\n> \n>     Emits the value of the specified key. If key is present\n>     multiple times in the configuration, emits the last\n>     value. If `--all` is specified, emits all values\n>     associated with key. Returns error code 1 if key\n>     is not present.\n> \n> But, doing so may be outside the scope of this patch series and can be\n> tackled at a later date (or not at all).\n\nI like this description, so let's just go with it.\n\n> > @@ -93,22 +95,16 @@ OPTIONS\n> > +--all::\n> > +       With \"get\", Return all values for a multi-valued key.\n> \n> s/Return/return/\n> s/\"get\"/`get`/\n> \n> > +---regexp::\n> > +       With \"get\", interpret the name as a regular expression. Regular\n> > +       expression matching is currently case-sensitive and done against a\n> > +       canonicalized version of the key in which section and variable names\n> > +       are lowercased, but subsection names are not.\n> \n> s/\"get\"/`get`/\n> \n> > @@ -286,7 +271,7 @@ Valid `<type>`'s include:\n> >  --default <value>::\n> > -  When using `--get`, and the requested variable is not found, behave as if\n> > +  When using `get`, and the requested variable is not found, behave as if\n> >    <value> were the value assigned to the that variable.\n> \n> Not a fault of this patch (and need not be fixed by this series): \"to\n> the that\" should be either \"to the\" or \"to that\".\n\nThis was fixed via 86f9ce7dd6 (docs: fix typo in git-config `--default`,\n2024-03-16).\n\n> > @@ -506,25 +509,25 @@ you have to provide a regex matching the value of exactly one line.\n> >  To query the value for a given key, do\n> >\n> >  ------------\n> > -% git config --get core.filemode\n> > +% git config get core.filemode\n> >  ------------\n> >\n> >  or\n> >\n> >  ------------\n> > -% git config core.filemode\n> > +% git config get core.filemode\n> >  ------------\n> \n> Meh. We only need to retain one of these examples now, not both, right?\n\nOh, of course.\n\nPatrick\n\n> > diff --git a/t/t1300-config.sh b/t/t1300-config.sh\n> > @@ -17,9 +17,15 @@ do\n> >  case \"$mode\" in\n> >  legacy)\n> >         mode_prefix=\"--\"\n> > +       mode_get=\"\"\n> > +       mode_get_all=\"--get-all\"\n> > +       mode_get_regexp=\"--get-regexp\"\n> >         ;;\n> >  subcommands)\n> >         mode_prefix=\"\"\n> > +       mode_get=\"get\"\n> > +       mode_get_all=\"get --all\"\n> > +       mode_get_regexp=\"get --regexp --all --show-names\"\n> >         ;;\n> >  *)\n> >         echo \"unknown mode $mode\" >&2\n> \n> The variables added by this patch to the `case` arms invalidate the\n> suggested simplification in my review of [6/13].\n"},{"id":"491657","messageId":"cover.1711527811.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH v3 00/13] builtin/config: introduce subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:09Z","receivedAt":"2024-03-27T08:46:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the third version of my patch series that aims to add\nsubcommands to git-config(1) in a backwards-compatible manner.\n\nThere's only been a few changes compared to v2 based on Eric's comments:\n\n    - Touched up a commit message to explain why I had to add cleanups\n      to tests.\n\n    - Reformulate \"implicit modes\" in a comment to clarify that this\n      refers to the legacy-style modes.\n\n    - Reformulated documentation of the \"get\" subcommand.\n\n    - Use `BUG` instead of `echo >&2 && exit 1`.\n\n    - Removed duplicate example in the documentation.\n\n    - Fixed conflicts with 86f9ce7dd6 (docs: fix typo in git-config\n      `--default`, 2024-03-16) and 7823a51203 (docs: clarify file\n      options in git-config `--edit`, 2024-03-16) by rebasing onto\n      latest `master`.\n\nThanks!\n\nPatrick\n\nPatrick Steinhardt (13):\n  builtin/config: move option array around\n  builtin/config: move \"fixed-value\" option to correct group\n  builtin/config: use `OPT_CMDMODE()` to specify modes\n  builtin/config: pull out function to handle config location\n  builtin/config: pull out function to handle `--null`\n  builtin/config: introduce \"list\" subcommand\n  builtin/config: introduce \"get\" subcommand\n  builtin/config: introduce \"set\" subcommand\n  builtin/config: introduce \"unset\" subcommand\n  builtin/config: introduce \"rename-section\" subcommand\n  builtin/config: introduce \"remove-section\" subcommand\n  builtin/config: introduce \"edit\" subcommand\n  builtin/config: display subcommand help\n\n Documentation/git-config.txt | 219 +++++++++-------\n builtin/config.c             | 496 ++++++++++++++++++++++++++++-------\n t/t0450/txt-help-mismatches  |   1 -\n t/t1300-config.sh            | 416 ++++++++++++++++++-----------\n 4 files changed, 785 insertions(+), 347 deletions(-)\n\nRange-diff against v2:\n 1:  1f21e7fa0d =  1:  bfcb50e393 builtin/config: move option array around\n 2:  54e536394a =  2:  ff428d8a22 builtin/config: move \"fixed-value\" option to correct group\n 3:  49d190ac24 =  3:  e049c05713 builtin/config: use `OPT_CMDMODE()` to specify modes\n 4:  80093f3908 =  4:  41585803bf builtin/config: pull out function to handle config location\n 5:  283a199578 =  5:  95f661f267 builtin/config: pull out function to handle `--null`\n 6:  53401299fa !  6:  b50f32d074 builtin/config: introduce \"list\" subcommand\n    @@ Commit message\n         ensure that there really is no change in behaviour for the old syntax.\n     \n         Amend tests such that we run them both with old and new style syntax.\n    +    As tests are now run twice, state from the first run may be still be\n    +    around in the second run and thus cause tests to fail. Add cleanup logic\n    +    as required to fix such tests.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    @@ Documentation/git-config.txt: Valid `<type>`'s include:\n      --show-origin::\n     @@ Documentation/git-config.txt: Valid `<type>`'s include:\n        When using `--get`, and the requested variable is not found, behave as if\n    -   <value> were the value assigned to the that variable.\n    +   <value> were the value assigned to that variable.\n      \n     +DEPRECATED MODES\n     +----------------\n    @@ builtin/config.c: int cmd_config(int argc, const char **argv, const char *prefix\n     +\t * has been specified. If so, we re-parse it a second time, but this\n     +\t * time we drop KEEP_ARGV0. This is so that we don't munge the command\n     +\t * line in case no subcommand was given, which would otherwise confuse\n    -+\t * us when parsing the implicit modes.\n    ++\t * us when parsing the legacy-style modes that don't use subcommands.\n     +\t */\n     +\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n     +\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n    @@ t/t1300-config.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n     +\tmode_prefix=\"\"\n     +\t;;\n     +*)\n    -+\techo \"unknown mode $mode\" >&2\n    -+\texit 1;;\n    ++\tBUG \"unknown mode $mode\";;\n     +esac\n     +\n      test_expect_success 'clear default config' '\n 7:  8ceced0fc5 !  7:  eee1fae50c builtin/config: introduce \"get\" subcommand\n    @@ Documentation/git-config.txt: COMMANDS\n      \tList all variables set in config file, along with their values.\n      \n     +get::\n    -+\tGet value for one or more config options. Values can be filtered by\n    -+\tregexes and URLs.Returns error code 1 if the key was not found and the\n    -+\tlast value if multiple key values were found. If `--all` is set, then\n    -+\tall values will be shown.\n    ++\tEmits the value of the specified key. If key is present multiple times\n    ++\tin the configuration, emits the last value. If `--all` is specified,\n    ++\temits all values associated with key. Returns error code 1 if key is\n    ++\tnot present.\n     +\n      [[OPTIONS]]\n      OPTIONS\n    @@ Documentation/git-config.txt: OPTIONS\n     ---get-all::\n     -\tLike get, but returns all values for a multi-valued key.\n     +--all::\n    -+\tWith \"get\", Return all values for a multi-valued key.\n    ++\tWith `get`, Return all values for a multi-valued key.\n      \n     ---get-regexp::\n     -\tLike --get-all, but interprets the name as a regular expression and\n    @@ Documentation/git-config.txt: OPTIONS\n     -\tin which section and variable names are lowercased, but subsection\n     -\tnames are not.\n     +---regexp::\n    -+\tWith \"get\", interpret the name as a regular expression. Regular\n    ++\tWith `get`, interpret the name as a regular expression. Regular\n     +\texpression matching is currently case-sensitive and done against a\n     +\tcanonicalized version of the key in which section and variable names\n     +\tare lowercased, but subsection names are not.\n    @@ Documentation/git-config.txt: Valid `<type>`'s include:\n      --default <value>::\n     -  When using `--get`, and the requested variable is not found, behave as if\n     +  When using `get`, and the requested variable is not found, behave as if\n    -   <value> were the value assigned to the that variable.\n    +   <value> were the value assigned to that variable.\n      \n      DEPRECATED MODES\n     @@ Documentation/git-config.txt: DEPRECATED MODES\n    @@ Documentation/git-config.txt: you have to provide a regex matching the value of\n      \n      ------------\n     -% git config --get core.filemode\n    -+% git config get core.filemode\n    - ------------\n    - \n    - or\n    - \n    - ------------\n    +-------------\n    +-\n    +-or\n    +-\n    +-------------\n     -% git config core.filemode\n     +% git config get core.filemode\n      ------------\n    @@ t/t1300-config.sh: do\n     +\tmode_get_regexp=\"get --regexp --all --show-names\"\n      \t;;\n      *)\n    - \techo \"unknown mode $mode\" >&2\n    + \tBUG \"unknown mode $mode\";;\n     @@ t/t1300-config.sh: test_expect_success 'multi-valued get-all returns all' '\n      \twow\n      \twow2 for me\n 8:  aa5c9743ad !  8:  e2815affab builtin/config: introduce \"set\" subcommand\n    @@ Documentation/git-config.txt: You can query/set/replace/unset options with this\n      lines, a `value-pattern` (which is an extended regular expression,\n      unless the `--fixed-value` option is given) needs to be given.  Only the\n     @@ Documentation/git-config.txt: get::\n    - \tlast value if multiple key values were found. If `--all` is set, then\n    - \tall values will be shown.\n    + \temits all values associated with key. Returns error code 1 if key is\n    + \tnot present.\n      \n     +set::\n     +\tSet value for one or more config options. By default, this command\n    @@ Documentation/git-config.txt: OPTIONS\n     +\tvalues. This is the same as providing '--value=^$' in `set`.\n      \n      --all::\n    - \tWith \"get\", Return all values for a multi-valued key.\n    + \tWith `get`, Return all values for a multi-valued key.\n     @@ Documentation/git-config.txt: recommended to migrate to the new syntax.\n      'git config <name>'::\n      \tReplaced by `git config get <name>`.\n    @@ t/t1300-config.sh: legacy)\n     +\tmode_replace_all=\"set --all\"\n      \t;;\n      *)\n    - \techo \"unknown mode $mode\" >&2\n    + \tBUG \"unknown mode $mode\";;\n     @@ t/t1300-config.sh: cat > expect << EOF\n      \tpenguin = little blue\n      EOF\n 9:  c8a44b6189 !  9:  90f055ae1d builtin/config: introduce \"unset\" subcommand\n    @@ t/t1300-config.sh: subcommands)\n     +\tmode_unset_all=\"unset --all\"\n      \t;;\n      *)\n    - \techo \"unknown mode $mode\" >&2\n    + \tBUG \"unknown mode $mode\";;\n     @@ t/t1300-config.sh: foo = bar\n      EOF\n      \n10:  a3c66431b0 = 10:  3e360b1f47 builtin/config: introduce \"rename-section\" subcommand\n11:  6314d51cec = 11:  d610b5fda1 builtin/config: introduce \"remove-section\" subcommand\n12:  12011714e3 ! 12:  4a6512c88a builtin/config: introduce \"edit\" subcommand\n    @@ Documentation/git-config.txt: rename-section::\n      \n     +edit::\n     +\tOpens an editor to modify the specified config file; either\n    -+\t`--system`, `--global`, or repository (default).\n    ++\t`--system`, `--global`, `--local` (default), `--worktree`, or\n    ++\t`--file <config-file>`.\n     +\n      [[OPTIONS]]\n      OPTIONS\n    @@ Documentation/git-config.txt: Valid `<type>`'s include:\n     --e::\n     ---edit::\n     -\tOpens an editor to modify the specified config file; either\n    --\t`--system`, `--global`, or repository (default).\n    +-\t`--system`, `--global`, `--local` (default), `--worktree`, or\n    +-\t`--file <config-file>`.\n     -\n      --[no-]includes::\n      \tRespect `include.*` directives in config files when looking up\n13:  2046f6652a ! 13:  657d1355b5 builtin/config: display subcommand help\n    @@ builtin/config.c: static const char *const builtin_config_usage[] = {\n      };\n      \n     @@ builtin/config.c: int cmd_config(int argc, const char **argv, const char *prefix)\n    - \t * us when parsing the implicit modes.\n    + \t * us when parsing the legacy-style modes that don't use subcommands.\n      \t */\n      \targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n     -\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n\nbase-commit: c75fd8d8150afdf836b63a8e0534d9b9e3e111ba\n-- \n2.44.GIT\n\n"},{"id":"491658","messageId":"bfcb50e3934f3aec95d4dd55908e886c8671cfe4.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 01/13] builtin/config: move option array around","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:13Z","receivedAt":"2024-03-27T08:46:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move around the option array. This will help us with a follow-up commit\nthat introduces subcommands to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 94 ++++++++++++++++++++++++------------------------\n 1 file changed, 47 insertions(+), 47 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex b55bfae7d6..6eb6aff917 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -134,53 +134,6 @@ static int option_parse_type(const struct option *opt, const char *arg,\n \treturn 0;\n }\n \n-static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n-\tOPT_GROUP(N_(\"Action\")),\n-\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n-\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n-\tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n-\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_END(),\n-};\n-\n-static NORETURN void usage_builtin_config(void)\n-{\n-\tusage_with_options(builtin_config_usage, builtin_config_options);\n-}\n-\n static void check_argc(int argc, int min, int max)\n {\n \tif (argc >= min && argc <= max)\n@@ -669,6 +622,53 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n+static struct option builtin_config_options[] = {\n+\tOPT_GROUP(N_(\"Config file location\")),\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tOPT_GROUP(N_(\"Action\")),\n+\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n+\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n+\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n+\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n+\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n+\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n+\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n+\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_GROUP(N_(\"Type\")),\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tOPT_GROUP(N_(\"Other\")),\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n+\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_END(),\n+};\n+\n+static NORETURN void usage_builtin_config(void)\n+{\n+\tusage_with_options(builtin_config_usage, builtin_config_options);\n+}\n+\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tint nongit = !startup_info->have_repository;\n-- \n2.44.GIT\n\n"},{"id":"491659","messageId":"ff428d8a2224505cc5b18afb4aaef8bf697da9d8.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 02/13] builtin/config: move \"fixed-value\" option to correct group","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:17Z","receivedAt":"2024-03-27T08:46:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `--fixed-value` option can be used to alter how the value-pattern\nparameter is interpreted for the various submodes of git-config(1). But\nwhile it is an option, it is currently listed as part of the submodes\ngroup the command, which is wrong.\n\nMove the option to the \"Other\" group, which hosts the various options\nknown to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 6eb6aff917..fcd6190f12 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -642,7 +642,6 @@ static struct option builtin_config_options[] = {\n \tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n \tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n \tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n \tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n \tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n \tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n@@ -661,6 +660,7 @@ static struct option builtin_config_options[] = {\n \tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n \tOPT_END(),\n };\n \n-- \n2.44.GIT\n\n"},{"id":"491660","messageId":"e049c05713482b74b80c63ac1326cac81b2c347d.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 03/13] builtin/config: use `OPT_CMDMODE()` to specify modes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:22Z","receivedAt":"2024-03-27T08:46:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The git-config(1) command has various different modes which are\naccessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are\ndeclared with `OPT_BIT()`, which causes two minor issues:\n\n  - The respective modes also have a negated form `--no-get-urlmatch`,\n    which is unintended.\n\n  - We have to manually handle exclusiveness of the modes.\n\nSwitch these options to instead use `OPT_CMDMODE()`, which is made\nexactly for this usecase. Remove the now-unneeded check that only a\nsingle mode is given, which is now handled by the parse-options\ninterface.\n\nWhile at it, format optional placeholders for arguments to conform to\nour style guidelines by using `[<placeholder>]`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c  | 32 ++++++++++++++------------------\n t/t1300-config.sh | 13 +++++++++++++\n 2 files changed, 27 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex fcd6190f12..a293161be4 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -631,20 +631,20 @@ static struct option builtin_config_options[] = {\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n \tOPT_GROUP(N_(\"Action\")),\n-\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n+\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n+\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n+\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n+\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n+\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n+\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n+\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n \tOPT_GROUP(N_(\"Type\")),\n \tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n \tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n@@ -767,10 +767,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (HAS_MULTI_BITS(actions)) {\n-\t\terror(_(\"only one action at a time\"));\n-\t\tusage_builtin_config();\n-\t}\n \tif (actions == 0)\n \t\tswitch (argc) {\n \t\tcase 1: actions = ACTION_GET; break;\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 31c3878687..2d1bc1e27e 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -2612,4 +2612,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such\n \tgrep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n '\n \n+test_expect_success 'negated mode causes failure' '\n+\ttest_must_fail git config --no-get 2>err &&\n+\tgrep \"unknown option \\`no-get${SQ}\" err\n+'\n+\n+test_expect_success 'specifying multiple modes causes failure' '\n+\tcat >expect <<-EOF &&\n+\terror: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together\n+\tEOF\n+\ttest_must_fail git config --get --get-all 2>err &&\n+\ttest_cmp expect err\n+'\n+\n test_done\n-- \n2.44.GIT\n\n"},{"id":"491661","messageId":"41585803bf30f329f546616bd27b10da4df047c5.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 04/13] builtin/config: pull out function to handle config location","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:26Z","receivedAt":"2024-03-27T08:46:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There's quite a bunch of options to git-config(1) that allow the user to\nspecify which config location to use when reading or writing config\noptions. The logic to handle this is thus by necessity also quite\ninvolved.\n\nPull it out into a separate function so that we can reuse it in\nsubsequent commits which introduce proper subcommands.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 131 ++++++++++++++++++++++++-----------------------\n 1 file changed, 67 insertions(+), 64 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex a293161be4..693df32526 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -622,67 +622,8 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n-\tOPT_GROUP(N_(\"Action\")),\n-\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n-\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n-\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n-\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n-\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n-\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n-\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n-\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n-\tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n-\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n-\tOPT_END(),\n-};\n-\n-static NORETURN void usage_builtin_config(void)\n-{\n-\tusage_with_options(builtin_config_usage, builtin_config_options);\n-}\n-\n-int cmd_config(int argc, const char **argv, const char *prefix)\n+static void handle_config_location(const char *prefix)\n {\n-\tint nongit = !startup_info->have_repository;\n-\tchar *value = NULL;\n-\tint flags = 0;\n-\tint ret = 0;\n-\tstruct key_value_info default_kvi = KVI_INIT;\n-\n-\tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n-\n-\targc = parse_options(argc, argv, prefix, builtin_config_options,\n-\t\t\t     builtin_config_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n-\n \tif (use_global_config + use_system_config + use_local_config +\n \t    use_worktree_config +\n \t    !!given_config_source.file + !!given_config_source.blob > 1) {\n@@ -690,14 +631,13 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (nongit) {\n+\tif (!startup_info->have_repository) {\n \t\tif (use_local_config)\n \t\t\tdie(_(\"--local can only be used inside a git repository\"));\n \t\tif (given_config_source.blob)\n \t\t\tdie(_(\"--blob can only be used inside a git repository\"));\n \t\tif (use_worktree_config)\n \t\t\tdie(_(\"--worktree can only be used inside a git repository\"));\n-\n \t}\n \n \tif (given_config_source.file &&\n@@ -751,10 +691,73 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tconfig_options.respect_includes = !given_config_source.file;\n \telse\n \t\tconfig_options.respect_includes = respect_includes_opt;\n-\tif (!nongit) {\n+\tif (startup_info->have_repository) {\n \t\tconfig_options.commondir = get_git_common_dir();\n \t\tconfig_options.git_dir = get_git_dir();\n \t}\n+}\n+\n+static struct option builtin_config_options[] = {\n+\tOPT_GROUP(N_(\"Config file location\")),\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tOPT_GROUP(N_(\"Action\")),\n+\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n+\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n+\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n+\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n+\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n+\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n+\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n+\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n+\tOPT_GROUP(N_(\"Type\")),\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tOPT_GROUP(N_(\"Other\")),\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n+\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_END(),\n+};\n+\n+static NORETURN void usage_builtin_config(void)\n+{\n+\tusage_with_options(builtin_config_usage, builtin_config_options);\n+}\n+\n+int cmd_config(int argc, const char **argv, const char *prefix)\n+{\n+\tchar *value = NULL;\n+\tint flags = 0;\n+\tint ret = 0;\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\n+\tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n+\n+\targc = parse_options(argc, argv, prefix, builtin_config_options,\n+\t\t\t     builtin_config_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\thandle_config_location(prefix);\n \n \tif (end_nul) {\n \t\tterm = '\\0';\n@@ -848,7 +851,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tchar *config_file;\n \n \t\tcheck_argc(argc, 0, 0);\n-\t\tif (!given_config_source.file && nongit)\n+\t\tif (!given_config_source.file && !startup_info->have_repository)\n \t\t\tdie(_(\"not in a git directory\"));\n \t\tif (given_config_source.use_stdin)\n \t\t\tdie(_(\"editing stdin is not supported\"));\n-- \n2.44.GIT\n\n"},{"id":"491662","messageId":"95f661f267ddd55a387b1c5ef078cbad639fd95d.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 05/13] builtin/config: pull out function to handle `--null`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:30Z","receivedAt":"2024-03-27T08:46:34Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Pull out function to handle the `--null` option, which we are about to\nreuse in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 15 +++++++++------\n 1 file changed, 9 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 693df32526..ce2d3fecd4 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -697,6 +697,14 @@ static void handle_config_location(const char *prefix)\n \t}\n }\n \n+static void handle_nul(void) {\n+\tif (end_nul) {\n+\t\tterm = '\\0';\n+\t\tdelim = '\\n';\n+\t\tkey_delim = '\\n';\n+\t}\n+}\n+\n static struct option builtin_config_options[] = {\n \tOPT_GROUP(N_(\"Config file location\")),\n \tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n@@ -758,12 +766,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n \thandle_config_location(prefix);\n-\n-\tif (end_nul) {\n-\t\tterm = '\\0';\n-\t\tdelim = '\\n';\n-\t\tkey_delim = '\\n';\n-\t}\n+\thandle_nul();\n \n \tif ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {\n \t\terror(_(\"--get-color and variable type are incoherent\"));\n-- \n2.44.GIT\n\n"},{"id":"491663","messageId":"b50f32d074a7a15f07140a4ed0b3d06a0d58792d.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 06/13] builtin/config: introduce \"list\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:34Z","receivedAt":"2024-03-27T08:46:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-config(1) has several modes, those modes are not exposed with\nsubcommands but instead by specifying e.g. `--unset` or `--list`. This\nuser interface is not really in line with how our more modern commands\nwork, where it is a lot more customary to say e.g. `git remote list`.\nFurthermore, to add to the confusion, git-config(1) also allows the user\nto request modes implicitly by just specifying the correct number of\narguments. Thus, `git config foo.bar` will retrieve the value of\n\"foo.bar\" while `git config foo.bar baz` will set it to \"baz\".\n\nOverall, this makes for a confusing interface that could really use a\nmakeover. It hurts discoverability of what you can do with git-config(1)\nand is comparatively easy to get wrong. Converting the command to have\nsubcommands instead would go a long way to help address these issues.\n\nOne concern in this context is backwards compatibility. Luckily, we can\nintroduce subcommands without breaking backwards compatibility at all.\nThis is because all the implicit modes of git-config(1) require that the\nfirst argument is a properly formatted config key. And as config keys\n_must_ have a dot in their name, any value without a dot would have been\ndiscarded by git-config(1) previous to this change. Thus, given that\nnone of the subcommands do have a dot, they are unambiguous.\n\nIntroduce the first such new subcommand, which is \"git config list\". To\nretain backwards compatibility we only conditionally use subcommands and\nwill fall back to the old syntax in case no subcommand was detected.\nThis should help to transition to the new-style syntax until we\neventually deprecate and remove the old-style syntax.\n\nNote that the way we handle this we're duplicating some functionality\nacross old and new syntax. While this isn't pretty, it helps us to\nensure that there really is no change in behaviour for the old syntax.\n\nAmend tests such that we run them both with old and new style syntax.\nAs tests are now run twice, state from the first run may be still be\naround in the second run and thus cause tests to fail. Add cleanup logic\nas required to fix such tests.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt |  26 ++++++---\n builtin/config.c             |  90 ++++++++++++++++++++++++----\n t/t1300-config.sh            | 110 +++++++++++++++++++++--------------\n 3 files changed, 162 insertions(+), 64 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex a6e82b871b..485e84cd0f 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -9,6 +9,7 @@ git-config - Get and set repository or global options\n SYNOPSIS\n --------\n [verse]\n+'git config list' [<file-option>] [<display-option>] [--includes]\n 'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n 'git config' [<file-option>] [--type=<type>] --add <name> <value>\n 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n@@ -20,7 +21,6 @@ SYNOPSIS\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n-'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list\n 'git config' [<file-option>] --get-color <name> [<default>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n@@ -74,6 +74,12 @@ On success, the command returns the exit code 0.\n A list of all available configuration variables can be obtained using the\n `git help --config` command.\n \n+COMMANDS\n+--------\n+\n+list::\n+\tList all variables set in config file, along with their values.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -178,10 +184,6 @@ See also <<FILES>>.\n --unset-all::\n \tRemove all lines matching the key from config file.\n \n--l::\n---list::\n-\tList all variables set in config file, along with their values.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -236,7 +238,7 @@ Valid `<type>`'s include:\n \tcontain line breaks.\n \n --name-only::\n-\tOutput only the names of config variables for `--list` or\n+\tOutput only the names of config variables for `list` or\n \t`--get-regexp`.\n \n --show-origin::\n@@ -288,10 +290,20 @@ Valid `<type>`'s include:\n   When using `--get`, and the requested variable is not found, behave as if\n   <value> were the value assigned to that variable.\n \n+DEPRECATED MODES\n+----------------\n+\n+The following modes have been deprecated in favor of subcommands. It is\n+recommended to migrate to the new syntax.\n+\n+-l::\n+--list::\n+\tReplaced by `git config list`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n-using `--list` or any of the `--get-*` which may return multiple results.\n+using `list` or any of the `--get-*` which may return multiple results.\n The default is to use a pager.\n \n [[FILES]]\ndiff --git a/builtin/config.c b/builtin/config.c\nindex ce2d3fecd4..7dd588a622 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -16,10 +16,16 @@\n #include \"worktree.h\"\n \n static const char *const builtin_config_usage[] = {\n+\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config [<options>]\"),\n \tNULL\n };\n \n+static const char *const builtin_config_list_usage[] = {\n+\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -33,6 +39,7 @@ static char delim = '=';\n static char key_delim = ' ';\n static char term = '\\n';\n \n+static parse_opt_subcommand_fn *subcommand;\n static int use_global_config, use_system_config, use_local_config;\n static int use_worktree_config;\n static struct git_config_source given_config_source;\n@@ -705,14 +712,24 @@ static void handle_nul(void) {\n \t}\n }\n \n+#define CONFIG_LOCATION_OPTIONS \\\n+\tOPT_GROUP(N_(\"Config file location\")), \\\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")), \\\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")), \\\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")), \\\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")), \\\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")), \\\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\"))\n+\n+#define CONFIG_DISPLAY_OPTIONS \\\n+\tOPT_GROUP(N_(\"Display options\")), \\\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")), \\\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")), \\\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")), \\\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\"))\n+\n static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tCONFIG_LOCATION_OPTIONS,\n \tOPT_GROUP(N_(\"Action\")),\n \tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n \tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n@@ -736,14 +753,11 @@ static struct option builtin_config_options[] = {\n \tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n \tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n \tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tCONFIG_DISPLAY_OPTIONS,\n \tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n \tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n \tOPT_END(),\n };\n \n@@ -752,6 +766,42 @@ static NORETURN void usage_builtin_config(void)\n \tusage_with_options(builtin_config_usage, builtin_config_options);\n }\n \n+static int cmd_config_list(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_DISPLAY_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);\n+\tcheck_argc(argc, 0, 0);\n+\n+\thandle_config_location(prefix);\n+\thandle_nul();\n+\n+\tsetup_auto_pager(\"config\", 1);\n+\n+\tif (config_with_options(show_all_config, NULL,\n+\t\t\t\t&given_config_source, the_repository,\n+\t\t\t\t&config_options) < 0) {\n+\t\tif (given_config_source.file)\n+\t\t\tdie_errno(_(\"unable to read config file '%s'\"),\n+\t\t\t\t  given_config_source.file);\n+\t\telse\n+\t\t\tdie(_(\"error processing config file(s)\"));\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static struct option builtin_subcommand_options[] = {\n+\tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n+\tOPT_END(),\n+};\n+\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tchar *value = NULL;\n@@ -761,6 +811,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \n \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n \n+\t/*\n+\t * This is somewhat hacky: we first parse the command line while\n+\t * keeping all args intact in order to determine whether a subcommand\n+\t * has been specified. If so, we re-parse it a second time, but this\n+\t * time we drop KEEP_ARGV0. This is so that we don't munge the command\n+\t * line in case no subcommand was given, which would otherwise confuse\n+\t * us when parsing the legacy-style modes that don't use subcommands.\n+\t */\n+\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\tif (subcommand) {\n+\t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n+\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\treturn subcommand(argc, argv, prefix);\n+\t}\n+\n \targc = parse_options(argc, argv, prefix, builtin_config_options,\n \t\t\t     builtin_config_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 2d1bc1e27e..b140674293 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -11,6 +11,20 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n+for mode in legacy subcommands\n+do\n+\n+case \"$mode\" in\n+legacy)\n+\tmode_prefix=\"--\"\n+\t;;\n+subcommands)\n+\tmode_prefix=\"\"\n+\t;;\n+*)\n+\tBUG \"unknown mode $mode\";;\n+esac\n+\n test_expect_success 'clear default config' '\n \trm -f .git/config\n '\n@@ -350,11 +364,11 @@ version.1.2.3eX.alpha=beta\n EOF\n \n test_expect_success 'working --list' '\n-\tgit config --list > output &&\n+\tgit config ${mode_prefix}list > output &&\n \ttest_cmp expect output\n '\n test_expect_success '--list without repo produces empty output' '\n-\tgit --git-dir=nonexistent config --list >output &&\n+\tgit --git-dir=nonexistent config ${mode_prefix}list >output &&\n \ttest_must_be_empty output\n '\n \n@@ -366,7 +380,7 @@ version.1.2.3eX.alpha\n EOF\n \n test_expect_success '--name-only --list' '\n-\tgit config --name-only --list >output &&\n+\tgit config ${mode_prefix}list --name-only >output &&\n \ttest_cmp expect output\n '\n \n@@ -504,17 +518,17 @@ ein.bahn=strasse\n EOF\n \n test_expect_success 'alternative GIT_CONFIG' '\n-\tGIT_CONFIG=other-config git config --list >output &&\n+\tGIT_CONFIG=other-config git config ${mode_prefix}list >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'alternative GIT_CONFIG (--file)' '\n-\tgit config --file other-config --list >output &&\n+\tgit config ${mode_prefix}list --file other-config >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'alternative GIT_CONFIG (--file=-)' '\n-\tgit config --file - --list <other-config >output &&\n+\tgit config ${mode_prefix}list --file - <other-config >output &&\n \ttest_cmp expect output\n '\n \n@@ -527,6 +541,7 @@ test_expect_success 'editing stdin is an error' '\n '\n \n test_expect_success 'refer config from subdirectory' '\n+\ttest_when_finished \"rm -r x\" &&\n \tmkdir x &&\n \ttest_cmp_config -C x strasse --file=../other-config --get ein.bahn\n '\n@@ -737,7 +752,7 @@ test_expect_success 'line number is reported correctly' '\n '\n \n test_expect_success 'invalid stdin config' '\n-\techo \"[broken\" | test_must_fail git config --list --file - >output 2>&1 &&\n+\techo \"[broken\" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&\n \ttest_grep \"bad config line 1 in standard input\" output\n '\n \n@@ -1029,7 +1044,7 @@ section.quotecont=cont;inued\n EOF\n \n test_expect_success 'value continued on next line' '\n-\tgit config --list > result &&\n+\tgit config ${mode_prefix}list > result &&\n \ttest_cmp expect result\n '\n \n@@ -1053,7 +1068,7 @@ Qsection.sub=section.val4\n Qsection.sub=section.val5Q\n EOF\n test_expect_success '--null --list' '\n-\tgit config --null --list >result.raw &&\n+\tgit config ${mode_prefix}list --null >result.raw &&\n \tnul_to_q <result.raw >result &&\n \techo >>result &&\n \ttest_cmp expect result\n@@ -1072,6 +1087,7 @@ test_expect_success 'inner whitespace kept verbatim' '\n '\n \n test_expect_success SYMLINKS 'symlinked configuration' '\n+\ttest_when_finished \"rm myconfig\" &&\n \tln -s notyet myconfig &&\n \tgit config --file=myconfig test.frotz nitfol &&\n \ttest -h myconfig &&\n@@ -1092,10 +1108,11 @@ test_expect_success SYMLINKS 'symlinked configuration' '\n '\n \n test_expect_success SYMLINKS 'symlink to nonexistent configuration' '\n+\ttest_when_finished \"rm linktonada linktolinktonada\" &&\n \tln -s doesnotexist linktonada &&\n \tln -s linktonada linktolinktonada &&\n-\ttest_must_fail git config --file=linktonada --list &&\n-\ttest_must_fail git config --file=linktolinktonada --list\n+\ttest_must_fail git config ${mode_prefix}list --file=linktonada &&\n+\ttest_must_fail git config ${mode_prefix}list --file=linktolinktonada\n '\n \n test_expect_success 'check split_cmdline return' '\n@@ -1352,7 +1369,7 @@ do\n done\n \n test_expect_success 'git -c is not confused by empty environment' '\n-\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config --list\n+\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config ${mode_prefix}list\n '\n \n test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n@@ -1543,31 +1560,31 @@ test_expect_success 'git config ignores pairs with empty count' '\n '\n \n test_expect_success 'git config fails with invalid count' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&\n+\ttest_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&\n \ttest_grep \"bogus count\" error &&\n-\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&\n+\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&\n \ttest_grep \"too many entries\" error\n '\n \n test_expect_success 'git config fails with missing config key' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config --list 2>error &&\n+\t\tgit config ${mode_prefix}list 2>error &&\n \ttest_grep \"missing config key\" error\n '\n \n test_expect_success 'git config fails with missing config value' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=\"pair.one\" \\\n-\t\tgit config --list 2>error &&\n+\t\tgit config ${mode_prefix}list 2>error &&\n \ttest_grep \"missing config value\" error\n '\n \n test_expect_success 'git config fails with invalid config pair key' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \\\n-\t\tgit config --list &&\n+\t\tgit config ${mode_prefix}list &&\n \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \\\n-\t\tgit config --list\n+\t\tgit config ${mode_prefix}list\n '\n \n test_expect_success 'environment overrides config file' '\n@@ -1607,7 +1624,7 @@ test_expect_success 'git config --edit works' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n \tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n-\tgit config -f tmp --list >actual &&\n+\tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1616,7 +1633,7 @@ test_expect_success 'git config --edit respects core.editor' '\n \techo test.value=yes >expect &&\n \ttest_config core.editor \"echo [test]value=yes >\" &&\n \tgit config -f tmp --edit &&\n-\tgit config -f tmp --list >actual &&\n+\tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1967,7 +1984,7 @@ test_expect_success '--show-origin with --list' '\n \tcommand line:\tuser.cmdline=true\n \tEOF\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\\\n-\t\tgit -c user.cmdline=true config --list --show-origin >output &&\n+\t\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -1984,7 +2001,7 @@ test_expect_success '--show-origin with --list --null' '\n \tincludeQcommand line:Quser.cmdline\n \ttrueQ\n \tEOF\n-\tgit -c user.cmdline=true config --null --list --show-origin >output.raw &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&\n \tnul_to_q <output.raw >output &&\n \t# The here-doc above adds a newline that the --null output would not\n \t# include. Add it here to make the two comparable.\n@@ -1998,7 +2015,7 @@ test_expect_success '--show-origin with single file' '\n \tfile:.git/config\tuser.override=local\n \tfile:.git/config\tinclude.path=../include/relative.include\n \tEOF\n-\tgit config --local --list --show-origin >output &&\n+\tgit config ${mode_prefix}list --local --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2036,7 +2053,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '\n \tcat >expect <<-\\EOF &&\n \tfile:\"file\\\" (dq) and spaces.conf\"\tuser.custom=true\n \tEOF\n-\tgit config --file \"$WEIRDLY_NAMED_FILE\" --show-origin --list >output &&\n+\tgit config ${mode_prefix}list --file \"$WEIRDLY_NAMED_FILE\" --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2044,7 +2061,7 @@ test_expect_success '--show-origin stdin' '\n \tcat >expect <<-\\EOF &&\n \tstandard input:\tuser.custom=true\n \tEOF\n-\tgit config --file - --show-origin --list <\"$CUSTOM_CONFIG_FILE\" >output &&\n+\tgit config ${mode_prefix}list --file - --show-origin <\"$CUSTOM_CONFIG_FILE\" >output &&\n \ttest_cmp expect output\n '\n \n@@ -2071,7 +2088,7 @@ test_expect_success '--show-origin blob' '\n \t\tcat >expect <<-EOF &&\n \t\tblob:$blob\tuser.custom=true\n \t\tEOF\n-\t\tgit config --blob=$blob --show-origin --list >output &&\n+\t\tgit config ${mode_prefix}list --blob=$blob --show-origin >output &&\n \t\ttest_cmp expect output\n \t)\n '\n@@ -2087,7 +2104,7 @@ test_expect_success '--show-origin blob ref' '\n \t\tcp \"$CUSTOM_CONFIG_FILE\" custom.conf &&\n \t\tgit add custom.conf &&\n \t\tgit commit -m \"new config file\" &&\n-\t\tgit config --blob=main:custom.conf --show-origin --list >output &&\n+\t\tgit config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&\n \t\ttest_cmp expect output\n \t)\n '\n@@ -2113,13 +2130,14 @@ test_expect_success '--show-scope with --list' '\n \tworktree\tuser.worktree=true\n \tcommand\tuser.cmdline=true\n \tEOF\n+\ttest_when_finished \"git worktree remove wt1\" &&\n \tgit worktree add wt1 &&\n \t# We need these to test for worktree scope, but outside of this\n \t# test, this is just noise\n \ttest_config core.repositoryformatversion 1 &&\n \ttest_config extensions.worktreeConfig true &&\n \tgit config --worktree user.worktree true &&\n-\tgit -c user.cmdline=true config --list --show-scope >output &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2128,7 +2146,7 @@ test_expect_success !MINGW '--show-scope with --blob' '\n \tcat >expect <<-EOF &&\n \tcommand\tuser.custom=true\n \tEOF\n-\tgit config --blob=$blob --show-scope --list >output &&\n+\tgit config ${mode_prefix}list --blob=$blob --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2138,7 +2156,7 @@ test_expect_success '--show-scope with --local' '\n \tlocal\tuser.override=local\n \tlocal\tinclude.path=../include/relative.include\n \tEOF\n-\tgit config --local --list --show-scope >output &&\n+\tgit config ${mode_prefix}list --local --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2162,7 +2180,7 @@ test_expect_success '--show-scope with --show-origin' '\n \tlocal\tfile:.git/../include/relative.include\tuser.relative=include\n \tcommand\tcommand line:\tuser.cmdline=true\n \tEOF\n-\tgit -c user.cmdline=true config --list --show-origin --show-scope >output &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2203,7 +2221,7 @@ test_expect_success 'override global and system config' '\n \tglobal\thome.config=true\n \tlocal\tlocal.config=true\n \tEOF\n-\tgit config --show-scope --list >output &&\n+\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output &&\n \n \tcat >expect <<-EOF &&\n@@ -2212,20 +2230,20 @@ test_expect_success 'override global and system config' '\n \tlocal\tlocal.config=true\n \tEOF\n \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \\\n-\t\tgit config --show-scope --list >output &&\n+\t\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output &&\n \n \tcat >expect <<-EOF &&\n \tlocal\tlocal.config=true\n \tEOF\n \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \\\n-\t\tgit config --show-scope --list >output &&\n+\t\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'override global and system config with missing file' '\n-\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&\n-\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&\n+\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&\n+\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&\n \tGIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version\n '\n \n@@ -2352,7 +2370,7 @@ test_expect_success 'set all config with value-pattern' '\n \t# no match => add new entry\n \tcp initial config &&\n \tgit config --file=config abc.key two a+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2365,7 +2383,7 @@ test_expect_success 'set all config with value-pattern' '\n \n \t# multiple values, no match => add\n \tgit config --file=config abc.key three a+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2375,7 +2393,7 @@ test_expect_success 'set all config with value-pattern' '\n \n \t# single match => replace\n \tgit config --file=config abc.key four h+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2390,7 +2408,7 @@ test_expect_success '--replace-all and value-pattern' '\n \tgit config --file=config --add abc.key two &&\n \tgit config --file=config --add abc.key three &&\n \tgit config --file=config --replace-all abc.key four \"o+\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=four\n \tabc.key=three\n@@ -2408,7 +2426,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --rename-section dev null &&\n \ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n-\ttest_must_fail git config --file=config --fixed-value --list &&\n+\ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n \n@@ -2429,7 +2447,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n \tfixed.test=bogus\n@@ -2438,7 +2456,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config --fixed-value fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tfixed.test=bogus\n \tEOF\n@@ -2456,7 +2474,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n \tfixed.test=bogus\n@@ -2464,7 +2482,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \ttest_cmp expect actual &&\n \n \tgit config --file=config --fixed-value --replace-all fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=bogus\n \tfixed.test=bogus\n@@ -2625,4 +2643,6 @@ test_expect_success 'specifying multiple modes causes failure' '\n \ttest_cmp expect err\n '\n \n+done\n+\n test_done\n-- \n2.44.GIT\n\n"},{"id":"491664","messageId":"eee1fae50cb22eb65ba039aa7ab576ed5faac97f.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 07/13] builtin/config: introduce \"get\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:38Z","receivedAt":"2024-03-27T08:46:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"get\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt |  93 +++++++++++++-------------\n builtin/config.c             |  69 ++++++++++++++++---\n t/t1300-config.sh            | 125 ++++++++++++++++++++++++-----------\n 3 files changed, 191 insertions(+), 96 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 485e84cd0f..4a7d6b7543 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -10,18 +10,14 @@ SYNOPSIS\n --------\n [verse]\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' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n 'git config' [<file-option>] [--type=<type>] --add <name> <value>\n 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL>\n 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n-'git config' [<file-option>] --get-color <name> [<default>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n \n@@ -80,6 +76,12 @@ COMMANDS\n list::\n \tList all variables set in config file, along with their values.\n \n+get::\n+\tEmits the value of the specified key. If key is present multiple times\n+\tin the configuration, emits the last value. If `--all` is specified,\n+\temits all values associated with key. Returns error code 1 if key is\n+\tnot present.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -93,22 +95,16 @@ OPTIONS\n \tvalues.  This is the same as providing '^$' as the `value-pattern`\n \tin `--replace-all`.\n \n---get::\n-\tGet the value for a given key (optionally filtered by a regex\n-\tmatching the value). Returns error code 1 if the key was not\n-\tfound and the last value if multiple key values were found.\n-\n---get-all::\n-\tLike get, but returns all values for a multi-valued key.\n+--all::\n+\tWith `get`, Return all values for a multi-valued key.\n \n---get-regexp::\n-\tLike --get-all, but interprets the name as a regular expression and\n-\twrites out the key names.  Regular expression matching is currently\n-\tcase-sensitive and done against a canonicalized version of the key\n-\tin which section and variable names are lowercased, but subsection\n-\tnames are not.\n+---regexp::\n+\tWith `get`, interpret the name as a regular expression. Regular\n+\texpression matching is currently case-sensitive and done against a\n+\tcanonicalized version of the key in which section and variable names\n+\tare lowercased, but subsection names are not.\n \n---get-urlmatch <name> <URL>::\n+--url=<URL>::\n \tWhen given a two-part <name> as <section>.<key>, the value for\n \t<section>.<URL>.<key> whose <URL> part matches the best to the\n \tgiven URL is returned (if no such key exists, the value for\n@@ -239,7 +235,7 @@ Valid `<type>`'s include:\n \n --name-only::\n \tOutput only the names of config variables for `list` or\n-\t`--get-regexp`.\n+\t`get`.\n \n --show-origin::\n \tAugment the output of all queried config options with the\n@@ -263,17 +259,6 @@ Valid `<type>`'s include:\n \tWhen the color setting for `name` is undefined, the command uses\n \t`color.ui` as fallback.\n \n---get-color <name> [<default>]::\n-\n-\tFind the color configured for `name` (e.g. `color.diff.new`) and\n-\toutput it as the ANSI color escape sequence to the standard\n-\toutput.  The optional `default` parameter is used instead, if\n-\tthere is no color configured for `name`.\n-+\n-`--type=color [--default=<default>]` is preferred over `--get-color`\n-(but note that `--get-color` will omit the trailing newline printed by\n-`--type=color`).\n-\n -e::\n --edit::\n \tOpens an editor to modify the specified config file; either\n@@ -287,7 +272,7 @@ Valid `<type>`'s include:\n \tconfig files.\n \n --default <value>::\n-  When using `--get`, and the requested variable is not found, behave as if\n+  When using `get`, and the requested variable is not found, behave as if\n   <value> were the value assigned to that variable.\n \n DEPRECATED MODES\n@@ -296,15 +281,33 @@ DEPRECATED MODES\n The following modes have been deprecated in favor of subcommands. It is\n recommended to migrate to the new syntax.\n \n+'git config <name>'::\n+\tReplaced by `git config get <name>`.\n+\n -l::\n --list::\n \tReplaced by `git config list`.\n \n+--get <name> [<value-pattern>]::\n+\tReplaced by `git config get [--value=<pattern>] <name>`.\n+\n+--get-all <name> [<value-pattern>]::\n+\tReplaced by `git config get [--value=<pattern>] --all --show-names <name>`.\n+\n+--get-regexp <name-regexp>::\n+\tReplaced by `git config get --all --show-names --regexp <name-regexp>`.\n+\n+--get-urlmatch <name> <URL>::\n+\tReplaced by `git config get --all --show-names --url=<URL> <name>`.\n+\n+--get-color <name> [<default>]::\n+\tReplaced by `git config get --type=color [--default=<default>] <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n-using `list` or any of the `--get-*` which may return multiple results.\n-The default is to use a pager.\n+using `list` or `get` which may return multiple results. The default is to use\n+a pager.\n \n [[FILES]]\n FILES\n@@ -507,25 +510,19 @@ you have to provide a regex matching the value of exactly one line.\n To query the value for a given key, do\n \n ------------\n-% git config --get core.filemode\n-------------\n-\n-or\n-\n-------------\n-% git config core.filemode\n+% git config get core.filemode\n ------------\n \n or, to query a multivar:\n \n ------------\n-% git config --get core.gitproxy \"for kernel.org$\"\n+% git config get --value=\"for kernel.org$\" core.gitproxy\n ------------\n \n If you want to know all the values for a multivar, do:\n \n ------------\n-% git config --get-all core.gitproxy\n+% git config get --all --show-names core.gitproxy\n ------------\n \n If you like to live dangerously, you can replace *all* core.gitproxy by a\n@@ -559,8 +556,8 @@ script:\n \n ------------\n #!/bin/sh\n-WS=$(git config --get-color color.diff.whitespace \"blue reverse\")\n-RESET=$(git config --get-color \"\" \"reset\")\n+WS=$(git config get --type=color --default=\"blue reverse\" color.diff.whitespace)\n+RESET=$(git config get --type=color --default=\"reset\" \"\")\n echo \"${WS}your whitespace color or blue reverse${RESET}\"\n ------------\n \n@@ -568,11 +565,11 @@ For URLs in `https://weak.example.com`, `http.sslVerify` is set to\n false, while it is set to `true` for all others:\n \n ------------\n-% git config --type=bool --get-urlmatch http.sslverify https://good.example.com\n+% git config get --type=bool --url=https://good.example.com http.sslverify\n true\n-% git config --type=bool --get-urlmatch http.sslverify https://weak.example.com\n+% git config get --type=bool --url=https://weak.example.com http.sslverify\n false\n-% git config --get-urlmatch http https://weak.example.com\n+% git config get --url=https://weak.example.com http\n http.cookieFile /tmp/cookie.txt\n http.sslverify false\n ------------\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 7dd588a622..447500dbd2 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -17,7 +17,7 @@\n \n static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n-\tN_(\"git config [<options>]\"),\n+\tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tNULL\n };\n \n@@ -26,6 +26,11 @@ static const char *const builtin_config_list_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_get_usage[] = {\n+\tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -721,6 +726,16 @@ static void handle_nul(void) {\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")), \\\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\"))\n \n+#define CONFIG_TYPE_OPTIONS \\\n+\tOPT_GROUP(N_(\"Type\")), \\\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL), \\\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR), \\\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH), \\\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE)\n+\n #define CONFIG_DISPLAY_OPTIONS \\\n \tOPT_GROUP(N_(\"Display options\")), \\\n \tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")), \\\n@@ -745,14 +760,7 @@ static struct option builtin_config_options[] = {\n \tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n \tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n \tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tCONFIG_TYPE_OPTIONS,\n \tCONFIG_DISPLAY_OPTIONS,\n \tOPT_GROUP(N_(\"Other\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n@@ -797,8 +805,51 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix)\n \treturn 0;\n }\n \n+static int cmd_config_get(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL, *url = NULL;\n+\tint flags = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_TYPE_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter options\")),\n+\t\tOPT_BOOL(0, \"all\", &do_all, N_(\"return all values for multi-valued config options\")),\n+\t\tOPT_BOOL(0, \"regexp\", &use_key_regexp, N_(\"interpret the name as a regular expression\")),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_STRING(0, \"url\", &url, N_(\"URL\"), N_(\"show config matching the given URL\")),\n+\t\tCONFIG_DISPLAY_OPTIONS,\n+\t\tOPT_BOOL(0, \"show-names\", &show_keys, N_(\"show config keys in addition to their values\")),\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\t\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"use default value when missing entry\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_get_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_argc(argc, 1, 1);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with 'value-pattern'\"));\n+\tif (default_value && (do_all || url))\n+\t\tdie(_(\"--default= cannot be used with --all or --url=\"));\n+\tif (url && (do_all || use_key_regexp || value_pattern))\n+\t\tdie(_(\"--url= cannot be used with --all, --regexp or --value\"));\n+\n+\thandle_config_location(prefix);\n+\thandle_nul();\n+\n+\tsetup_auto_pager(\"config\", 1);\n+\n+\tif (url)\n+\t\treturn get_urlmatch(argv[0], url);\n+\treturn get_value(argv[0], value_pattern, flags);\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n+\tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex b140674293..f1547921c6 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -17,9 +17,15 @@ do\n case \"$mode\" in\n legacy)\n \tmode_prefix=\"--\"\n+\tmode_get=\"\"\n+\tmode_get_all=\"--get-all\"\n+\tmode_get_regexp=\"--get-regexp\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n+\tmode_get=\"get\"\n+\tmode_get_all=\"get --all\"\n+\tmode_get_regexp=\"get --regexp --all --show-names\"\n \t;;\n *)\n \tBUG \"unknown mode $mode\";;\n@@ -288,7 +294,7 @@ test_expect_success 'multi-valued get-all returns all' '\n \twow\n \twow2 for me\n \tEOF\n-\tgit config --get-all nextsection.nonewline >actual &&\n+\tgit config ${mode_get_all} nextsection.nonewline >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -390,7 +396,7 @@ nextsection.nonewline wow2 for me\n EOF\n \n test_expect_success '--get-regexp' '\n-\tgit config --get-regexp in >output &&\n+\tgit config ${mode_get_regexp} in >output &&\n \ttest_cmp expect output\n '\n \n@@ -400,7 +406,7 @@ nextsection.nonewline\n EOF\n \n test_expect_success '--name-only --get-regexp' '\n-\tgit config --name-only --get-regexp in >output &&\n+\tgit config ${mode_get_regexp} --name-only in >output &&\n \ttest_cmp expect output\n '\n \n@@ -411,7 +417,7 @@ EOF\n \n test_expect_success '--add' '\n \tgit config --add nextsection.nonewline \"wow4 for you\" &&\n-\tgit config --get-all nextsection.nonewline > output &&\n+\tgit config ${mode_get_all} nextsection.nonewline > output &&\n \ttest_cmp expect output\n '\n \n@@ -433,21 +439,21 @@ test_expect_success 'get variable with empty value' '\n echo novalue.variable > expect\n \n test_expect_success 'get-regexp variable with no value' '\n-\tgit config --get-regexp novalue > output &&\n+\tgit config ${mode_get_regexp} novalue > output &&\n \ttest_cmp expect output\n '\n \n echo 'novalue.variable true' > expect\n \n test_expect_success 'get-regexp --bool variable with no value' '\n-\tgit config --bool --get-regexp novalue > output &&\n+\tgit config ${mode_get_regexp} --bool novalue > output &&\n \ttest_cmp expect output\n '\n \n echo 'emptyvalue.variable ' > expect\n \n test_expect_success 'get-regexp variable with empty value' '\n-\tgit config --get-regexp emptyvalue > output &&\n+\tgit config ${mode_get_regexp} emptyvalue > output &&\n \ttest_cmp expect output\n '\n \n@@ -1021,7 +1027,7 @@ test_expect_success 'quoting' '\n '\n \n test_expect_success 'key with newline' '\n-\ttest_must_fail git config \"key.with\n+\ttest_must_fail git config ${mode_get} \"key.with\n newline\" 123'\n \n test_expect_success 'value with newline' 'git config key.sub value.with\\\\\\\n@@ -1075,7 +1081,7 @@ test_expect_success '--null --list' '\n '\n \n test_expect_success '--null --get-regexp' '\n-\tgit config --null --get-regexp \"val[0-9]\" >result.raw &&\n+\tgit config ${mode_get_regexp} --null \"val[0-9]\" >result.raw &&\n \tnul_to_q <result.raw >result &&\n \techo >>result &&\n \ttest_cmp expect result\n@@ -1157,11 +1163,11 @@ test_expect_success 'git -c can represent empty string' '\n '\n \n test_expect_success 'key sanity-checking' '\n-\ttest_must_fail git config foo=bar &&\n-\ttest_must_fail git config foo=.bar &&\n-\ttest_must_fail git config foo.ba=r &&\n-\ttest_must_fail git config foo.1bar &&\n-\ttest_must_fail git config foo.\"ba\n+\ttest_must_fail git config ${mode_get} foo=bar &&\n+\ttest_must_fail git config ${mode_get} foo=.bar &&\n+\ttest_must_fail git config ${mode_get} foo.ba=r &&\n+\ttest_must_fail git config ${mode_get} foo.1bar &&\n+\ttest_must_fail git config ${mode_get} foo.\"ba\n \t\t\t\tz\".bar &&\n \ttest_must_fail git config . false &&\n \ttest_must_fail git config .foo false &&\n@@ -1210,7 +1216,7 @@ test_expect_success 'git -c complains about empty key and value' '\n '\n \n test_expect_success 'multiple git -c appends config' '\n-\ttest_config alias.x \"!git -c x.two=2 config --get-regexp ^x\\.*\" &&\n+\ttest_config alias.x \"!git -c x.two=2 config ${mode_get_regexp} ^x\\.*\" &&\n \tcat >expect <<-\\EOF &&\n \tx.one 1\n \tx.two 2\n@@ -1376,7 +1382,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n \tv=\"${SQ}key.one=foo${SQ}\" &&\n \tv=\"$v  ${SQ}key.two=bar${SQ}\" &&\n \tv=\"$v ${SQ}key.ambiguous=section.whatever=value${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.one foo\n \tkey.two bar\n@@ -1389,7 +1395,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '\n \tv=\"${SQ}key.one${SQ}=${SQ}foo${SQ}\" &&\n \tv=\"$v  ${SQ}key.two${SQ}=${SQ}bar${SQ}\" &&\n \tv=\"$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.one foo\n \tkey.two bar\n@@ -1403,7 +1409,7 @@ test_expect_success 'old and new-style entries can mix' '\n \tv=\"$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}\" &&\n \tv=\"$v ${SQ}key.oldtwo=oldbar${SQ}\" &&\n \tv=\"$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.oldone oldfoo\n \tkey.newone newfoo\n@@ -1416,7 +1422,7 @@ test_expect_success 'old and new-style entries can mix' '\n test_expect_success 'old and new bools with ambiguous subsection' '\n \tv=\"${SQ}key.with=equals.oldbool${SQ}\" &&\n \tv=\"$v ${SQ}key.with=equals.newbool${SQ}=\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.with equals.oldbool\n \tkey.with=equals.newbool\n@@ -1430,7 +1436,7 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '\n \tenv.two two\n \tEOF\n \tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ} ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"env.*\" >actual &&\n \ttest_cmp expect actual &&\n \n \tcat >expect <<-EOF &&\n@@ -1438,12 +1444,12 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '\n \tenv.two two\n \tEOF\n \tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ}\\\\$SQ$SQ$SQ ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"env.*\" >actual &&\n \ttest_cmp expect actual &&\n \n \ttest_must_fail env \\\n \t\tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ}\\\\$SQ ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\"\n+\t\tgit config ${mode_get_regexp} \"env.*\"\n '\n \n test_expect_success 'git --config-env=key=envvar support' '\n@@ -1491,7 +1497,7 @@ test_expect_success 'git -c and --config-env work together' '\n \tENVVAR=env-value git \\\n \t\t-c bar.cmd=cmd-value \\\n \t\t--config-env=bar.env=ENVVAR \\\n-\t\tconfig --get-regexp \"^bar.*\" >actual &&\n+\t\tconfig ${mode_get_regexp} \"^bar.*\" >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1519,7 +1525,7 @@ test_expect_success 'git config handles environment config pairs' '\n \tGIT_CONFIG_COUNT=2 \\\n \t\tGIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"foo\" \\\n \t\tGIT_CONFIG_KEY_1=\"pair.two\" GIT_CONFIG_VALUE_1=\"bar\" \\\n-\t\tgit config --get-regexp \"pair.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"pair.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tpair.one foo\n \tpair.two bar\n@@ -1529,7 +1535,7 @@ test_expect_success 'git config handles environment config pairs' '\n \n test_expect_success 'git config ignores pairs without count' '\n \ttest_must_fail env GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n@@ -1537,7 +1543,7 @@ test_expect_success 'git config ignores pairs exceeding count' '\n \tGIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n \t\tGIT_CONFIG_KEY_1=\"pair.two\" GIT_CONFIG_VALUE_1=\"value\" \\\n-\t\tgit config --get-regexp \"pair.*\" >actual 2>error &&\n+\t\tgit config ${mode_get_regexp} \"pair.*\" >actual 2>error &&\n \tcat >expect <<-EOF &&\n \tpair.one value\n \tEOF\n@@ -1548,14 +1554,14 @@ test_expect_success 'git config ignores pairs exceeding count' '\n test_expect_success 'git config ignores pairs with zero count' '\n \ttest_must_fail env \\\n \t\tGIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n test_expect_success 'git config ignores pairs with empty count' '\n \ttest_must_fail env \\\n \t\tGIT_CONFIG_COUNT= GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n@@ -1594,7 +1600,7 @@ test_expect_success 'environment overrides config file' '\n \tone = value\n \tEOF\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \\\n-\t\tgit config pair.one >actual &&\n+\t\tgit config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1604,7 +1610,7 @@ test_expect_success 'environment overrides config file' '\n test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \\\n \t\tGIT_CONFIG_PARAMETERS=\"${SQ}pair.one=override${SQ}\" \\\n-\t\tgit config pair.one >actual &&\n+\t\tgit config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1679,20 +1685,28 @@ test_expect_success 'urlmatch' '\n \n \ttest_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&\n \ttest_must_be_empty actual &&\n+\ttest_expect_code 1 git config get --url=https://good.example.com --bool doesnt.exist >actual &&\n+\ttest_must_be_empty actual &&\n \n \techo true >expect &&\n \tgit config --bool --get-urlmatch http.SSLverify https://good.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --bool --url=https://good.example.com http.SSLverify >actual &&\n+\ttest_cmp expect actual &&\n \n \techo false >expect &&\n \tgit config --bool --get-urlmatch http.sslverify https://weak.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --bool --url=https://weak.example.com http.sslverify >actual &&\n+\ttest_cmp expect actual &&\n \n \t{\n \t\techo http.cookiefile /tmp/cookie.txt &&\n \t\techo http.sslverify false\n \t} >expect &&\n \tgit config --get-urlmatch HTTP https://weak.example.com >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://weak.example.com HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1708,6 +1722,8 @@ test_expect_success 'urlmatch with --show-scope' '\n \tlocal\thttp.sslverify false\n \tEOF\n \tgit config --get-urlmatch --show-scope HTTP https://weak.example.com >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://weak.example.com --show-scope HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1740,45 +1756,67 @@ test_expect_success 'urlmatch favors more specific URLs' '\n \techo http.cookiefile /tmp/root.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com/subdirectory >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com/subdirectory HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com/subdirectory/nested HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/user.txt >expect &&\n \tgit config --get-urlmatch HTTP https://user@example.com/ >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://user@example.com/ HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://averylonguser@example.com/subdirectory HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/preceding.txt >expect &&\n \tgit config --get-urlmatch HTTP https://preceding.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://preceding.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/wildcard.txt >expect &&\n \tgit config --get-urlmatch HTTP https://wildcard.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://wildcard.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n \tgit config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://sub.example.com/wildcardwithsubdomain HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/trailing.txt >expect &&\n \tgit config --get-urlmatch HTTP https://trailing.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://trailing.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n \tgit config --get-urlmatch HTTP https://user@sub.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://user@sub.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/multiwildcard.txt >expect &&\n \tgit config --get-urlmatch HTTP https://wildcard.example.org >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://wildcard.example.org HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1901,7 +1939,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[one]\n \tEOF\n \tgit config --unset two.subsection.key &&\n-\ttest \"not [two subsection]\" = \"$(git config one.key)\" &&\n+\ttest \"not [two subsection]\" = \"$(git config ${mode_get} one.key)\" &&\n \ttest_line_count = 3 .git/config\n '\n \n@@ -2024,7 +2062,7 @@ test_expect_success '--show-origin with --get-regexp' '\n \tfile:$HOME/.gitconfig\tuser.global true\n \tfile:.git/config\tuser.local true\n \tEOF\n-\tgit config --show-origin --get-regexp \"user\\.[g|l].*\" >output &&\n+\tgit config ${mode_get_regexp} --show-origin \"user\\.[g|l].*\" >output &&\n \ttest_cmp expect output\n '\n \n@@ -2032,7 +2070,7 @@ test_expect_success '--show-origin getting a single key' '\n \tcat >expect <<-\\EOF &&\n \tfile:.git/config\tlocal\n \tEOF\n-\tgit config --show-origin user.override >output &&\n+\tgit config ${mode_get} --show-origin user.override >output &&\n \ttest_cmp expect output\n '\n \n@@ -2164,7 +2202,7 @@ test_expect_success '--show-scope getting a single value' '\n \tcat >expect <<-\\EOF &&\n \tlocal\ttrue\n \tEOF\n-\tgit config --show-scope --get user.local >output &&\n+\tgit config ${mode_get} --show-scope user.local >output &&\n \ttest_cmp expect output\n '\n \n@@ -2433,9 +2471,9 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \t# These modes complain when --fixed-value has no value-pattern\n \ttest_must_fail git config --file=config --fixed-value dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --get dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-all dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-regexp \"dev.*\" &&\n+\ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n \ttest_must_fail git config --file=config --fixed-value --unset dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --unset-all dev.null\n '\n@@ -2465,12 +2503,12 @@ test_expect_success '--fixed-value uses exact string matching' '\n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset-all fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset-all fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n \tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n@@ -2497,18 +2535,27 @@ test_expect_success '--get and --get-all with --fixed-value' '\n \tgit config --file=config --add fixed.test \"$META\" &&\n \n \tgit config --file=config --get fixed.test bogus &&\n+\tgit config get --file=config --value=bogus fixed.test &&\n \ttest_must_fail git config --file=config --get fixed.test \"$META\" &&\n+\ttest_must_fail git config get --file=config --value=\"$META\" fixed.test &&\n \tgit config --file=config --get --fixed-value fixed.test \"$META\" &&\n+\tgit config get --file=config --fixed-value --value=\"$META\" fixed.test &&\n \ttest_must_fail git config --file=config --get --fixed-value fixed.test non-existent &&\n \n \tgit config --file=config --get-all fixed.test bogus &&\n+\tgit config get --all --file=config --value=bogus fixed.test &&\n \ttest_must_fail git config --file=config --get-all fixed.test \"$META\" &&\n+\ttest_must_fail git config get --all --file=config --value=\"$META\" fixed.test &&\n \tgit config --file=config --get-all --fixed-value fixed.test \"$META\" &&\n+\tgit config get --all --file=config --value=\"$META\" --fixed-value fixed.test &&\n \ttest_must_fail git config --file=config --get-all --fixed-value fixed.test non-existent &&\n \n \tgit config --file=config --get-regexp fixed+ bogus &&\n+\tgit config get --regexp --file=config --value=bogus fixed+ &&\n \ttest_must_fail git config --file=config --get-regexp fixed+ \"$META\" &&\n+\ttest_must_fail git config get --regexp --file=config --value=\"$META\" fixed+ &&\n \tgit config --file=config --get-regexp --fixed-value fixed+ \"$META\" &&\n+\tgit config get --regexp --file=config --fixed-value --value=\"$META\" fixed+ &&\n \ttest_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent\n '\n \n-- \n2.44.GIT\n\n"},{"id":"491665","messageId":"e2815affab8c1dce9e2112920ec94a237bd476e0.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 08/13] builtin/config: introduce \"set\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:43Z","receivedAt":"2024-03-27T08:46:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"set\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 38 +++++++++------\n builtin/config.c             | 57 ++++++++++++++++++++++\n t/t1300-config.sh            | 92 +++++++++++++++++++-----------------\n 3 files changed, 129 insertions(+), 58 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 4a7d6b7543..7fdf9b434e 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -11,9 +11,7 @@ SYNOPSIS\n [verse]\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' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n-'git config' [<file-option>] [--type=<type>] --add <name> <value>\n-'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n+'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n@@ -27,7 +25,7 @@ You can query/set/replace/unset options with this command. The name is\n actually the section and the key separated by a dot, and the value will be\n escaped.\n \n-Multiple lines can be added to an option by using the `--add` option.\n+Multiple lines can be added to an option by using the `--append` option.\n If you want to update or unset an option which can occur on multiple\n lines, a `value-pattern` (which is an extended regular expression,\n unless the `--fixed-value` option is given) needs to be given.  Only the\n@@ -82,6 +80,13 @@ get::\n \temits all values associated with key. Returns error code 1 if key is\n \tnot present.\n \n+set::\n+\tSet value for one or more config options. By default, this command\n+\trefuses to write multi-valued config options. Passing `--all` will\n+\treplace all multi-valued config options with the new value, whereas\n+\t`--value=` will replace all config options whose values match the given\n+\tpattern.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -90,10 +95,9 @@ OPTIONS\n \tDefault behavior is to replace at most one line. This replaces\n \tall lines matching the key (and optionally the `value-pattern`).\n \n---add::\n+--append::\n \tAdds a new line to the option without altering any existing\n-\tvalues.  This is the same as providing '^$' as the `value-pattern`\n-\tin `--replace-all`.\n+\tvalues. This is the same as providing '--value=^$' in `set`.\n \n --all::\n \tWith `get`, Return all values for a multi-valued key.\n@@ -284,6 +288,9 @@ recommended to migrate to the new syntax.\n 'git config <name>'::\n \tReplaced by `git config get <name>`.\n \n+'git config <name> <value> [<value-pattern>]'::\n+\tReplaced by `git config set [--value=<pattern>] <name> <value>`.\n+\n -l::\n --list::\n \tReplaced by `git config list`.\n@@ -303,6 +310,9 @@ recommended to migrate to the new syntax.\n --get-color <name> [<default>]::\n \tReplaced by `git config get --type=color [--default=<default>] <name>`.\n \n+--add <name> <value>::\n+\tReplaced by `git config set --append <name> <value>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n@@ -349,7 +359,7 @@ precedence over values read earlier.  When multiple values are taken then all\n values of a key from all files will be used.\n \n By default, options are only written to the repository specific\n-configuration file. Note that this also affects options like `--replace-all`\n+configuration file. Note that this also affects options like `set`\n and `--unset`. *'git config' will only ever change one file at a time*.\n \n You can limit which configuration sources are read from or written to by\n@@ -485,7 +495,7 @@ Given a .git/config like this:\n you can set the filemode to true with\n \n ------------\n-% git config core.filemode true\n+% git config set core.filemode true\n ------------\n \n The hypothetical proxy command entries actually have a postfix to discern\n@@ -493,7 +503,7 @@ what URL they apply to. Here is how to change the entry for kernel.org\n to \"ssh\".\n \n ------------\n-% git config core.gitproxy '\"ssh\" for kernel.org' 'for kernel.org$'\n+% git config set --value='for kernel.org$' core.gitproxy '\"ssh\" for kernel.org'\n ------------\n \n This makes sure that only the key/value pair for kernel.org is replaced.\n@@ -529,26 +539,26 @@ If you like to live dangerously, you can replace *all* core.gitproxy by a\n new one with\n \n ------------\n-% git config --replace-all core.gitproxy ssh\n+% git config set --all core.gitproxy ssh\n ------------\n \n However, if you really only want to replace the line for the default proxy,\n i.e. the one without a \"for ...\" postfix, do something like this:\n \n ------------\n-% git config core.gitproxy ssh '! for '\n+% git config set --value='! for ' core.gitproxy ssh\n ------------\n \n To actually match only values with an exclamation mark, you have to\n \n ------------\n-% git config section.key value '[!]'\n+% git config set --value='[!]' section.key value\n ------------\n \n To add a new proxy, without altering any of the existing ones, use\n \n ------------\n-% git config --add core.gitproxy '\"proxy-command\" for example.com'\n+% git config set --append core.gitproxy '\"proxy-command\" for example.com'\n ------------\n \n An example to use customized color from the configuration in your\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 447500dbd2..eec57fbd17 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -18,6 +18,7 @@\n static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n+\tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tNULL\n };\n \n@@ -31,6 +32,11 @@ static const char *const builtin_config_get_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_set_usage[] = {\n+\tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -847,9 +853,60 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix)\n \treturn get_value(argv[0], value_pattern, flags);\n }\n \n+static int cmd_config_set(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL;\n+\tint flags = 0, append = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_TYPE_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter\")),\n+\t\tOPT_BIT(0, \"all\", &flags, N_(\"replace multi-valued config option with new value\"), CONFIG_FLAGS_MULTI_REPLACE),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_BOOL(0, \"append\", &append, N_(\"add a new line without altering any existing values\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\tchar *value;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with --value=<pattern>\"));\n+\tif (append && value_pattern)\n+\t\tdie(_(\"--append cannot be used with --value=<pattern>\"));\n+\tif (append)\n+\t\tvalue_pattern = CONFIG_REGEX_NONE;\n+\n+\thandle_config_location(prefix);\n+\n+\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n+\n+\tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {\n+\t\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t\t     argv[0], value, value_pattern,\n+\t\t\t\t\t\t\t     flags);\n+\t} else {\n+\t\tret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);\n+\t\tif (ret == CONFIG_NOTHING_SET)\n+\t\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n+\t\t\t\"       Use a regexp, --add or --replace-all to change %s.\"), argv[0]);\n+\t}\n+\n+\tfree(value);\n+\treturn ret;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n+\tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex f1547921c6..f435b8f194 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -20,12 +20,16 @@ legacy)\n \tmode_get=\"\"\n \tmode_get_all=\"--get-all\"\n \tmode_get_regexp=\"--get-regexp\"\n+\tmode_set=\"\"\n+\tmode_replace_all=\"--replace-all\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n \tmode_get=\"get\"\n \tmode_get_all=\"get --all\"\n \tmode_get_regexp=\"get --regexp --all --show-names\"\n+\tmode_set=\"set\"\n+\tmode_replace_all=\"set --all\"\n \t;;\n *)\n \tBUG \"unknown mode $mode\";;\n@@ -40,7 +44,7 @@ cat > expect << EOF\n \tpenguin = little blue\n EOF\n test_expect_success 'initial' '\n-\tgit config section.penguin \"little blue\" &&\n+\tgit config ${mode_set} section.penguin \"little blue\" &&\n \ttest_cmp expect .git/config\n '\n \n@@ -50,7 +54,7 @@ cat > expect << EOF\n \tMovie = BadPhysics\n EOF\n test_expect_success 'mixed case' '\n-\tgit config Section.Movie BadPhysics &&\n+\tgit config ${mode_set} Section.Movie BadPhysics &&\n \ttest_cmp expect .git/config\n '\n \n@@ -62,7 +66,7 @@ cat > expect << EOF\n \tWhatEver = Second\n EOF\n test_expect_success 'similar section' '\n-\tgit config Sections.WhatEver Second &&\n+\tgit config ${mode_set} Sections.WhatEver Second &&\n \ttest_cmp expect .git/config\n '\n \n@@ -75,7 +79,7 @@ cat > expect << EOF\n \tWhatEver = Second\n EOF\n test_expect_success 'uppercase section' '\n-\tgit config SECTION.UPPERCASE true &&\n+\tgit config ${mode_set} SECTION.UPPERCASE true &&\n \ttest_cmp expect .git/config\n '\n \n@@ -191,14 +195,14 @@ test_expect_success 'multiple unset is correct' '\n cp .git/config2 .git/config\n \n test_expect_success '--replace-all missing value' '\n-\ttest_must_fail git config --replace-all beta.haha &&\n+\ttest_must_fail git config ${mode_replace_all} beta.haha &&\n \ttest_cmp .git/config2 .git/config\n '\n \n rm .git/config2\n \n test_expect_success '--replace-all' '\n-\tgit config --replace-all beta.haha gamma\n+\tgit config ${mode_replace_all} beta.haha gamma\n '\n \n cat > expect << EOF\n@@ -225,7 +229,7 @@ noIndent= sillyValue ; 'nother silly comment\n [nextSection] noNewline = ouch\n EOF\n test_expect_success 'really mean test' '\n-\tgit config beta.haha alpha &&\n+\tgit config ${mode_set} beta.haha alpha &&\n \ttest_cmp expect .git/config\n '\n \n@@ -240,7 +244,7 @@ noIndent= sillyValue ; 'nother silly comment\n \tnonewline = wow\n EOF\n test_expect_success 'really really mean test' '\n-\tgit config nextsection.nonewline wow &&\n+\tgit config ${mode_set} nextsection.nonewline wow &&\n \ttest_cmp expect .git/config\n '\n \n@@ -714,16 +718,16 @@ EOF\n \n test_expect_success 'section ending' '\n \trm -f .git/config &&\n-\tgit config gitcvs.enabled true &&\n-\tgit config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&\n-\tgit config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&\n+\tgit config ${mode_set} gitcvs.enabled true &&\n+\tgit config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&\n+\tgit config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&\n \ttest_cmp expect .git/config\n \n '\n \n test_expect_success numbers '\n-\tgit config kilo.gram 1k &&\n-\tgit config mega.ton 1m &&\n+\tgit config ${mode_set} kilo.gram 1k &&\n+\tgit config ${mode_set} mega.ton 1m &&\n \techo 1024 >expect &&\n \techo 1048576 >>expect &&\n \tgit config --int --get kilo.gram >actual &&\n@@ -732,20 +736,20 @@ test_expect_success numbers '\n '\n \n test_expect_success '--int is at least 64 bits' '\n-\tgit config giga.watts 121g &&\n+\tgit config ${mode_set} giga.watts 121g &&\n \techo  >expect &&\n \ttest_cmp_config 129922760704 --int --get giga.watts\n '\n \n test_expect_success 'invalid unit' '\n-\tgit config aninvalid.unit \"1auto\" &&\n+\tgit config ${mode_set} aninvalid.unit \"1auto\" &&\n \ttest_cmp_config 1auto aninvalid.unit &&\n \ttest_must_fail git config --int --get aninvalid.unit 2>actual &&\n \ttest_grep \"bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit\" actual\n '\n \n test_expect_success 'invalid unit boolean' '\n-\tgit config commit.gpgsign \"1true\" &&\n+\tgit config ${mode_set} commit.gpgsign \"1true\" &&\n \ttest_cmp_config 1true commit.gpgsign &&\n \ttest_must_fail git config --bool --get commit.gpgsign 2>actual &&\n \ttest_grep \"bad boolean config value .1true. for .commit.gpgsign.\" actual\n@@ -775,14 +779,14 @@ EOF\n \n test_expect_success bool '\n \n-\tgit config bool.true1 01 &&\n-\tgit config bool.true2 -1 &&\n-\tgit config bool.true3 YeS &&\n-\tgit config bool.true4 true &&\n-\tgit config bool.false1 000 &&\n-\tgit config bool.false2 \"\" &&\n-\tgit config bool.false3 nO &&\n-\tgit config bool.false4 FALSE &&\n+\tgit config ${mode_set} bool.true1 01 &&\n+\tgit config ${mode_set} bool.true2 -1 &&\n+\tgit config ${mode_set} bool.true3 YeS &&\n+\tgit config ${mode_set} bool.true4 true &&\n+\tgit config ${mode_set} bool.false1 000 &&\n+\tgit config ${mode_set} bool.false2 \"\" &&\n+\tgit config ${mode_set} bool.false3 nO &&\n+\tgit config ${mode_set} bool.false4 FALSE &&\n \trm -f result &&\n \tfor i in 1 2 3 4\n \tdo\n@@ -793,7 +797,7 @@ test_expect_success bool '\n \n test_expect_success 'invalid bool (--get)' '\n \n-\tgit config bool.nobool foobar &&\n+\tgit config ${mode_set} bool.nobool foobar &&\n \ttest_must_fail git config --bool --get bool.nobool'\n \n test_expect_success 'invalid bool (set)' '\n@@ -982,7 +986,7 @@ test_expect_success 'get --expiry-date' '\n \n test_expect_success 'get --type=color' '\n \trm .git/config &&\n-\tgit config foo.color \"red\" &&\n+\tgit config ${mode_set} foo.color \"red\" &&\n \tgit config --get --type=color foo.color >actual.raw &&\n \ttest_decode_color <actual.raw >actual &&\n \techo \"<RED>\" >expect &&\n@@ -1019,10 +1023,10 @@ cat > expect << EOF\n EOF\n test_expect_success 'quoting' '\n \trm -f .git/config &&\n-\tgit config quote.leading \" test\" &&\n-\tgit config quote.ending \"test \" &&\n-\tgit config quote.semicolon \"test;test\" &&\n-\tgit config quote.hash \"test#test\" &&\n+\tgit config ${mode_set} quote.leading \" test\" &&\n+\tgit config ${mode_set} quote.ending \"test \" &&\n+\tgit config ${mode_set} quote.semicolon \"test;test\" &&\n+\tgit config ${mode_set} quote.hash \"test#test\" &&\n \ttest_cmp expect .git/config\n '\n \n@@ -1030,7 +1034,7 @@ test_expect_success 'key with newline' '\n \ttest_must_fail git config ${mode_get} \"key.with\n newline\" 123'\n \n-test_expect_success 'value with newline' 'git config key.sub value.with\\\\\\\n+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\\\\\n newline'\n \n cat > .git/config <<\\EOF\n@@ -1088,7 +1092,7 @@ test_expect_success '--null --get-regexp' '\n '\n \n test_expect_success 'inner whitespace kept verbatim' '\n-\tgit config section.val \"foo \t  bar\" &&\n+\tgit config ${mode_set} section.val \"foo \t  bar\" &&\n \ttest_cmp_config \"foo \t  bar\" section.val\n '\n \n@@ -1126,12 +1130,12 @@ test_expect_success 'check split_cmdline return' '\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tgit config alias.split-cmdline-fix \"echo \\\"\" &&\n+\t\tgit config ${mode_set} alias.split-cmdline-fix \"echo \\\"\" &&\n \t\ttest_must_fail git split-cmdline-fix &&\n \t\techo foo >foo &&\n \t\tgit add foo &&\n \t\tgit commit -m \"initial commit\" &&\n-\t\tgit config branch.main.mergeoptions \"echo \\\"\" &&\n+\t\tgit config ${mode_set} branch.main.mergeoptions \"echo \\\"\" &&\n \t\ttest_must_fail git merge main\n \t)\n '\n@@ -1169,12 +1173,12 @@ test_expect_success 'key sanity-checking' '\n \ttest_must_fail git config ${mode_get} foo.1bar &&\n \ttest_must_fail git config ${mode_get} foo.\"ba\n \t\t\t\tz\".bar &&\n-\ttest_must_fail git config . false &&\n-\ttest_must_fail git config .foo false &&\n-\ttest_must_fail git config foo. false &&\n-\ttest_must_fail git config .foo. false &&\n-\tgit config foo.bar true &&\n-\tgit config foo.\"ba =z\".bar false\n+\ttest_must_fail git config ${mode_set} . false &&\n+\ttest_must_fail git config ${mode_set} .foo false &&\n+\ttest_must_fail git config ${mode_set} foo. false &&\n+\ttest_must_fail git config ${mode_set} .foo. false &&\n+\tgit config ${mode_set} foo.bar true &&\n+\tgit config ${mode_set} foo.\"ba =z\".bar false\n '\n \n test_expect_success 'git -c works with aliases of builtins' '\n@@ -2397,7 +2401,7 @@ test_expect_success '--replace-all does not invent newlines' '\n \t[abc]\n \tQkey = b\n \tEOF\n-\tgit config --replace-all abc.key b &&\n+\tgit config ${mode_replace_all} abc.key b &&\n \ttest_cmp expect .git/config\n '\n \n@@ -2469,8 +2473,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n \n \t# These modes complain when --fixed-value has no value-pattern\n-\ttest_must_fail git config --file=config --fixed-value dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&\n+\ttest_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&\n+\ttest_must_fail git config ${mode_replace_all} --file=config --fixed-value dev.null bogus &&\n \ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n@@ -2511,7 +2515,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n-\tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n+\tgit config --file=config fixed.test bogus \"$META\" &&\n \tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n-- \n2.44.GIT\n\n"},{"id":"491666","messageId":"90f055ae1d9e735560b66dc9aac534fafaf27c23.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 09/13] builtin/config: introduce \"unset\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:47Z","receivedAt":"2024-03-27T08:46:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"unset\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 25 ++++++++++++--------\n builtin/config.c             | 38 +++++++++++++++++++++++++++++++\n t/t1300-config.sh            | 44 ++++++++++++++++++++++++------------\n 3 files changed, 82 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 7fdf9b434e..4fbc73a19e 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -12,8 +12,7 @@ SYNOPSIS\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' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n-'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n+'git config unset' [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n@@ -87,6 +86,12 @@ set::\n \t`--value=` will replace all config options whose values match the given\n \tpattern.\n \n+unset::\n+\tUnset value for one or more config options. By default, this command\n+\trefuses to unset multi-valued keys. Passing `--all` will unset all\n+\tmulti-valued config options, whereas `--value` will unset all config\n+\toptions whose values match the given pattern.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -178,12 +183,6 @@ See also <<FILES>>.\n --rename-section::\n \tRename the given section to a new name.\n \n---unset::\n-\tRemove the line matching the key from config file.\n-\n---unset-all::\n-\tRemove all lines matching the key from config file.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -313,6 +312,12 @@ recommended to migrate to the new syntax.\n --add <name> <value>::\n \tReplaced by `git config set --append <name> <value>`.\n \n+--unset <name> [<value-pattern>]::\n+\tReplaced by `git config unset [--value=<pattern>] <name>`.\n+\n+--unset-all <name> [<value-pattern>]::\n+\tReplaced by `git config unset [--value=<pattern>] --all <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n@@ -360,7 +365,7 @@ values of a key from all files will be used.\n \n By default, options are only written to the repository specific\n configuration file. Note that this also affects options like `set`\n-and `--unset`. *'git config' will only ever change one file at a time*.\n+and `unset`. *'git config' will only ever change one file at a time*.\n \n You can limit which configuration sources are read from or written to by\n specifying the path of a file with the `--file` option, or by specifying a\n@@ -511,7 +516,7 @@ This makes sure that only the key/value pair for kernel.org is replaced.\n To delete the entry for renames, do\n \n ------------\n-% git config --unset diff.renames\n+% git config unset diff.renames\n ------------\n \n If you want to delete an entry for a multivar (like core.gitproxy above),\ndiff --git a/builtin/config.c b/builtin/config.c\nindex eec57fbd17..c53b68887e 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -19,6 +19,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tNULL\n };\n \n@@ -37,6 +38,11 @@ static const char *const builtin_config_set_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_unset_usage[] = {\n+\tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -903,10 +909,42 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix)\n \treturn ret;\n }\n \n+static int cmd_config_unset(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL;\n+\tint flags = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter\")),\n+\t\tOPT_BIT(0, \"all\", &flags, N_(\"replace multi-valued config option with new value\"), CONFIG_FLAGS_MULTI_REPLACE),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_unset_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 1);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with 'value-pattern'\"));\n+\n+\thandle_config_location(prefix);\n+\n+\tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)\n+\t\treturn git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t\t      argv[0], NULL, value_pattern,\n+\t\t\t\t\t\t\t      flags);\n+\telse\n+\t\treturn git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n+\tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex f435b8f194..c9610a1f5b 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -22,6 +22,8 @@ legacy)\n \tmode_get_regexp=\"--get-regexp\"\n \tmode_set=\"\"\n \tmode_replace_all=\"--replace-all\"\n+\tmode_unset=\"--unset\"\n+\tmode_unset_all=\"--unset-all\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n@@ -30,6 +32,8 @@ subcommands)\n \tmode_get_regexp=\"get --regexp --all --show-names\"\n \tmode_set=\"set\"\n \tmode_replace_all=\"set --all\"\n+\tmode_unset=\"unset\"\n+\tmode_unset_all=\"unset --all\"\n \t;;\n *)\n \tBUG \"unknown mode $mode\";;\n@@ -149,7 +153,7 @@ foo = bar\n EOF\n \n test_expect_success 'unset with cont. lines' '\n-\tgit config --unset beta.baz\n+\tgit config ${mode_unset} beta.baz\n '\n \n cat > expect <<\\EOF\n@@ -176,7 +180,7 @@ EOF\n cp .git/config .git/config2\n \n test_expect_success 'multiple unset' '\n-\tgit config --unset-all beta.haha\n+\tgit config ${mode_unset_all} beta.haha\n '\n \n cat > expect << EOF\n@@ -262,7 +266,7 @@ noIndent= sillyValue ; 'nother silly comment\n \tnonewline = wow\n EOF\n test_expect_success 'unset' '\n-\tgit config --unset beta.haha &&\n+\tgit config ${mode_unset} beta.haha &&\n \ttest_cmp expect .git/config\n '\n \n@@ -318,11 +322,11 @@ test_expect_success 'multivar replace' '\n '\n \n test_expect_success 'ambiguous unset' '\n-\ttest_must_fail git config --unset nextsection.nonewline\n+\ttest_must_fail git config ${mode_unset} nextsection.nonewline\n '\n \n test_expect_success 'invalid unset' '\n-\ttest_must_fail git config --unset somesection.nonewline\n+\ttest_must_fail git config ${mode_unset} somesection.nonewline\n '\n \n cat > expect << EOF\n@@ -336,7 +340,12 @@ noIndent= sillyValue ; 'nother silly comment\n EOF\n \n test_expect_success 'multivar unset' '\n-\tgit config --unset nextsection.nonewline \"wow3$\" &&\n+\tcase \"$mode\" in\n+\tlegacy)\n+\t\tgit config --unset nextsection.nonewline \"wow3$\";;\n+\tsubcommands)\n+\t\tgit config unset --value=\"wow3$\" nextsection.nonewline;;\n+\tesac &&\n \ttest_cmp expect .git/config\n '\n \n@@ -1887,7 +1896,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t# please be careful when you update the above variable\n \tEOF\n \n-\tgit config --unset section.key &&\n+\tgit config ${mode_unset} section.key &&\n \ttest_cmp expect .git/config &&\n \n \tcat >.git/config <<-\\EOF &&\n@@ -1900,7 +1909,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[next-section]\n \tEOF\n \n-\tgit config --unset section.key &&\n+\tgit config ${mode_unset} section.key &&\n \ttest_cmp expect .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1910,7 +1919,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[two]\n \tkey = true\n \tEOF\n-\tgit config --unset two.key &&\n+\tgit config ${mode_unset} two.key &&\n \t! grep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1920,7 +1929,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[one]\n \tkey = true\n \tEOF\n-\tgit config --unset-all one.key &&\n+\tgit config ${mode_unset_all} one.key &&\n \ttest_line_count = 0 .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1930,7 +1939,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[two]\n \tQkey = true\n \tEOF\n-\tgit config --unset two.key &&\n+\tgit config ${mode_unset} two.key &&\n \tgrep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -1942,7 +1951,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[TWO \"subsection\"]\n \t[one]\n \tEOF\n-\tgit config --unset two.subsection.key &&\n+\tgit config ${mode_unset} two.subsection.key &&\n \ttest \"not [two subsection]\" = \"$(git config ${mode_get} one.key)\" &&\n \ttest_line_count = 3 .git/config\n '\n@@ -1954,7 +1963,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' '\n \tkey = value2\n \tEOF\n \n-\tgit config --unset-all section.key &&\n+\tgit config ${mode_unset_all} section.key &&\n \ttest_line_count = 0 .git/config\n '\n \n@@ -2478,8 +2487,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n-\ttest_must_fail git config --file=config --fixed-value --unset dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --unset-all dev.null\n+\ttest_must_fail git config ${mode_unset} --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_unset_all} --file=config --fixed-value dev.null\n '\n \n test_expect_success '--fixed-value uses exact string matching' '\n@@ -2509,6 +2518,11 @@ test_expect_success '--fixed-value uses exact string matching' '\n \tgit config --file=config --fixed-value --unset fixed.test \"$META\" &&\n \ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n+\tcp initial config &&\n+\ttest_must_fail git config unset --file=config --value=\"$META\" fixed.test &&\n+\tgit config unset --file=config --fixed-value --value=\"$META\" fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n+\n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset-all fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset-all fixed.test \"$META\" &&\n-- \n2.44.GIT\n\n"},{"id":"491667","messageId":"3e360b1f47864ebe738889fd46ea414829f8daf4.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 10/13] builtin/config: introduce \"rename-section\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:51Z","receivedAt":"2024-03-27T08:46:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"rename-section\" subcommand to git-config(1). Please\nrefer to preceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 11 +++++++----\n builtin/config.c             | 32 ++++++++++++++++++++++++++++++++\n t/t1300-config.sh            | 22 +++++++++++-----------\n 3 files changed, 50 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 4fbc73a19e..0695eeae22 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -13,7 +13,7 @@ SYNOPSIS\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' [<file-option>] --rename-section <old-name> <new-name>\n+'git config rename-section' [<file-option>] <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n@@ -92,6 +92,9 @@ unset::\n \tmulti-valued config options, whereas `--value` will unset all config\n \toptions whose values match the given pattern.\n \n+rename-section::\n+\tRename the given section to a new name.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -180,9 +183,6 @@ See also <<FILES>>.\n --remove-section::\n \tRemove the given section from the configuration file.\n \n---rename-section::\n-\tRename the given section to a new name.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -318,6 +318,9 @@ recommended to migrate to the new syntax.\n --unset-all <name> [<value-pattern>]::\n \tReplaced by `git config unset [--value=<pattern>] --all <name>`.\n \n+--rename-section <old-name> <new-name>::\n+\tReplaced by `git config rename-section <old-name> <new-name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex c53b68887e..99d9ac6341 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -20,6 +20,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tNULL\n };\n \n@@ -43,6 +44,11 @@ static const char *const builtin_config_unset_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_rename_section_usage[] = {\n+\tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -940,11 +946,37 @@ static int cmd_config_unset(int argc, const char **argv, const char *prefix)\n \t\treturn git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);\n }\n \n+static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_rename_section_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\n+\thandle_config_location(prefix);\n+\n+\tret = git_config_rename_section_in_file(given_config_source.file,\n+\t\t\t\t\t\targv[0], argv[1]);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!ret)\n+\t\tdie(_(\"no such section: %s\"), argv[0]);\n+\n+\treturn 0;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n+\tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex c9610a1f5b..4818c495bd 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -589,7 +589,7 @@ weird\n EOF\n \n test_expect_success 'rename section' '\n-\tgit config --rename-section branch.eins branch.zwei\n+\tgit config ${mode_prefix}rename-section branch.eins branch.zwei\n '\n \n cat > expect << EOF\n@@ -608,7 +608,7 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'rename non-existing section' '\n-\ttest_must_fail git config --rename-section \\\n+\ttest_must_fail git config ${mode_prefix}rename-section \\\n \t\tbranch.\"world domination\" branch.drei\n '\n \n@@ -617,7 +617,7 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'rename another section' '\n-\tgit config --rename-section branch.\"1 234 blabl/a\" branch.drei\n+\tgit config ${mode_prefix}rename-section branch.\"1 234 blabl/a\" branch.drei\n '\n \n cat > expect << EOF\n@@ -640,7 +640,7 @@ cat >> .git/config << EOF\n EOF\n \n test_expect_success 'rename a section with a var on the same line' '\n-\tgit config --rename-section branch.vier branch.zwei\n+\tgit config ${mode_prefix}rename-section branch.vier branch.zwei\n '\n \n cat > expect << EOF\n@@ -661,11 +661,11 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'renaming empty section name is rejected' '\n-\ttest_must_fail git config --rename-section branch.zwei \"\"\n+\ttest_must_fail git config ${mode_prefix}rename-section branch.zwei \"\"\n '\n \n test_expect_success 'renaming to bogus section is rejected' '\n-\ttest_must_fail git config --rename-section branch.zwei \"bogus name\"\n+\ttest_must_fail git config ${mode_prefix}rename-section branch.zwei \"bogus name\"\n '\n \n test_expect_success 'renaming a section with a long line' '\n@@ -674,7 +674,7 @@ test_expect_success 'renaming a section with a long line' '\n \t\tprintf \"  c = d %1024s [a] e = f\\\\n\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\tgit config -f y --rename-section a xyz &&\n+\tgit config ${mode_prefix}rename-section -f y a xyz &&\n \ttest_must_fail git config -f y b.e\n '\n \n@@ -684,7 +684,7 @@ test_expect_success 'renaming an embedded section with a long line' '\n \t\tprintf \"  c = d %1024s [a] [foo] e = f\\\\n\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\tgit config -f y --rename-section a xyz &&\n+\tgit config ${mode_prefix}rename-section -f y a xyz &&\n \ttest_must_fail git config -f y foo.e\n '\n \n@@ -694,7 +694,7 @@ test_expect_success 'renaming a section with an overly-long line' '\n \t\tprintf \"  c = d %525000s e\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\ttest_must_fail git config -f y --rename-section a xyz 2>err &&\n+\ttest_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&\n \tgrep \"refusing to work with overly long line in .y. on line 2\" err\n '\n \n@@ -1986,7 +1986,7 @@ test_expect_success POSIXPERM,PERL 'preserves existing permissions' '\n \tgit config imap.pass Hunter2 &&\n \tperl -e \\\n \t  \"die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600\" &&\n-\tgit config --rename-section imap pop &&\n+\tgit config ${mode_prefix}rename-section imap pop &&\n \tperl -e \\\n \t  \"die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600\"\n '\n@@ -2475,7 +2475,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --add dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --rename-section dev null &&\n+\ttest_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&\n \ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n \ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n-- \n2.44.GIT\n\n"},{"id":"491668","messageId":"d610b5fda1c6e92cb5ecaf451e3f2c66a7253f73.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 11/13] builtin/config: introduce \"remove-section\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:55Z","receivedAt":"2024-03-27T08:46:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"remove-section\" subcommand to git-config(1). Please\nrefer to preceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 11 +++++++----\n builtin/config.c             | 32 ++++++++++++++++++++++++++++++++\n t/t1300-config.sh            |  4 ++--\n 3 files changed, 41 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 0695eeae22..b9ca5e83a2 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -14,7 +14,7 @@ SYNOPSIS\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' [<file-option>] --remove-section <name>\n+'git config remove-section' [<file-option>] <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n \n@@ -95,6 +95,9 @@ unset::\n rename-section::\n \tRename the given section to a new name.\n \n+remove-section::\n+\tRemove the given section from the configuration file.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -180,9 +183,6 @@ See also <<FILES>>.\n \tsection in linkgit:gitrevisions[7] for a more complete list of\n \tways to spell blob names.\n \n---remove-section::\n-\tRemove the given section from the configuration file.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -321,6 +321,9 @@ recommended to migrate to the new syntax.\n --rename-section <old-name> <new-name>::\n \tReplaced by `git config rename-section <old-name> <new-name>`.\n \n+--remove-section <name>::\n+\tReplaced by `git config remove-section <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 99d9ac6341..6be6717f26 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -21,6 +21,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n+\tN_(\"git config remove-section [<file-option>] <name>\"),\n \tNULL\n };\n \n@@ -49,6 +50,11 @@ static const char *const builtin_config_rename_section_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_remove_section_usage[] = {\n+\tN_(\"git config remove-section [<file-option>] <name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -971,12 +977,38 @@ static int cmd_config_rename_section(int argc, const char **argv, const char *pr\n \treturn 0;\n }\n \n+static int cmd_config_remove_section(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_remove_section_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 1);\n+\n+\thandle_config_location(prefix);\n+\n+\tret = git_config_rename_section_in_file(given_config_source.file,\n+\t\t\t\t\t\targv[0], NULL);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!ret)\n+\t\tdie(_(\"no such section: %s\"), argv[0]);\n+\n+\treturn 0;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n+\tOPT_SUBCOMMAND(\"remove-section\", &subcommand, cmd_config_remove_section),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 4818c495bd..2f8d0310b1 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -703,7 +703,7 @@ cat >> .git/config << EOF\n EOF\n \n test_expect_success 'remove section' '\n-\tgit config --remove-section branch.zwei\n+\tgit config ${mode_prefix}remove-section branch.zwei\n '\n \n cat > expect << EOF\n@@ -2476,7 +2476,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&\n-\ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n+\ttest_must_fail git config ${mode_prefix}remove-section --file=config --fixed-value dev &&\n \ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n-- \n2.44.GIT\n\n"},{"id":"491669","messageId":"4a6512c88af82013b8a08767515f5715af05c6bb.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 12/13] builtin/config: introduce \"edit\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:46:59Z","receivedAt":"2024-03-27T08:47:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"edit\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 17 ++++----\n builtin/config.c             | 81 ++++++++++++++++++++++++------------\n t/t1300-config.sh            |  6 +--\n 3 files changed, 68 insertions(+), 36 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex b9ca5e83a2..448bf43bb3 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -15,8 +15,8 @@ SYNOPSIS\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>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n-'git config' [<file-option>] -e | --edit\n \n DESCRIPTION\n -----------\n@@ -98,6 +98,11 @@ rename-section::\n remove-section::\n \tRemove the given section from the configuration file.\n \n+edit::\n+\tOpens an editor to modify the specified config file; either\n+\t`--system`, `--global`, `--local` (default), `--worktree`, or\n+\t`--file <config-file>`.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -262,12 +267,6 @@ Valid `<type>`'s include:\n \tWhen the color setting for `name` is undefined, the command uses\n \t`color.ui` as fallback.\n \n--e::\n---edit::\n-\tOpens an editor to modify the specified config file; either\n-\t`--system`, `--global`, `--local` (default), `--worktree`, or\n-\t`--file <config-file>`.\n-\n --[no-]includes::\n \tRespect `include.*` directives in config files when looking up\n \tvalues. Defaults to `off` when a specific file is given (e.g.,\n@@ -324,6 +323,10 @@ recommended to migrate to the new syntax.\n --remove-section <name>::\n \tReplaced by `git config remove-section <name>`.\n \n+-e::\n+--edit::\n+\tReplaced by `git config edit`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 6be6717f26..b8cb940b1e 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -22,6 +22,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tN_(\"git config remove-section [<file-option>] <name>\"),\n+\tN_(\"git config edit [<file-option>]\"),\n \tNULL\n };\n \n@@ -55,6 +56,11 @@ static const char *const builtin_config_remove_section_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_edit_usage[] = {\n+\tN_(\"git config edit [<file-option>]\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -1002,6 +1008,53 @@ static int cmd_config_remove_section(int argc, const char **argv, const char *pr\n \treturn 0;\n }\n \n+static int show_editor(void)\n+{\n+\tchar *config_file;\n+\n+\tif (!given_config_source.file && !startup_info->have_repository)\n+\t\tdie(_(\"not in a git directory\"));\n+\tif (given_config_source.use_stdin)\n+\t\tdie(_(\"editing stdin is not supported\"));\n+\tif (given_config_source.blob)\n+\t\tdie(_(\"editing blobs is not supported\"));\n+\tgit_config(git_default_config, NULL);\n+\tconfig_file = given_config_source.file ?\n+\t\t\txstrdup(given_config_source.file) :\n+\t\t\tgit_pathdup(\"config\");\n+\tif (use_global_config) {\n+\t\tint fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);\n+\t\tif (fd >= 0) {\n+\t\t\tchar *content = default_user_config();\n+\t\t\twrite_str_in_full(fd, content);\n+\t\t\tfree(content);\n+\t\t\tclose(fd);\n+\t\t}\n+\t\telse if (errno != EEXIST)\n+\t\t\tdie_errno(_(\"cannot create configuration file %s\"), config_file);\n+\t}\n+\tlaunch_editor(config_file, NULL, NULL);\n+\tfree(config_file);\n+\n+\treturn 0;\n+}\n+\n+static int cmd_config_edit(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_edit_usage, 0);\n+\tcheck_write();\n+\tcheck_argc(argc, 0, 0);\n+\n+\thandle_config_location(prefix);\n+\n+\treturn show_editor();\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n@@ -1009,6 +1062,7 @@ static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n \tOPT_SUBCOMMAND(\"remove-section\", &subcommand, cmd_config_remove_section),\n+\tOPT_SUBCOMMAND(\"edit\", &subcommand, cmd_config_edit),\n \tOPT_END(),\n };\n \n@@ -1127,32 +1181,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\t}\n \t}\n \telse if (actions == ACTION_EDIT) {\n-\t\tchar *config_file;\n-\n-\t\tcheck_argc(argc, 0, 0);\n-\t\tif (!given_config_source.file && !startup_info->have_repository)\n-\t\t\tdie(_(\"not in a git directory\"));\n-\t\tif (given_config_source.use_stdin)\n-\t\t\tdie(_(\"editing stdin is not supported\"));\n-\t\tif (given_config_source.blob)\n-\t\t\tdie(_(\"editing blobs is not supported\"));\n-\t\tgit_config(git_default_config, NULL);\n-\t\tconfig_file = given_config_source.file ?\n-\t\t\t\txstrdup(given_config_source.file) :\n-\t\t\t\tgit_pathdup(\"config\");\n-\t\tif (use_global_config) {\n-\t\t\tint fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);\n-\t\t\tif (fd >= 0) {\n-\t\t\t\tchar *content = default_user_config();\n-\t\t\t\twrite_str_in_full(fd, content);\n-\t\t\t\tfree(content);\n-\t\t\t\tclose(fd);\n-\t\t\t}\n-\t\t\telse if (errno != EEXIST)\n-\t\t\t\tdie_errno(_(\"cannot create configuration file %s\"), config_file);\n-\t\t}\n-\t\tlaunch_editor(config_file, NULL, NULL);\n-\t\tfree(config_file);\n+\t\tret = show_editor();\n \t}\n \telse if (actions == ACTION_SET) {\n \t\tcheck_write();\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 2f8d0310b1..3049b61533 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -556,7 +556,7 @@ test_expect_success 'setting a value in stdin is an error' '\n '\n \n test_expect_success 'editing stdin is an error' '\n-\ttest_must_fail git config --file - --edit\n+\ttest_must_fail git config ${mode_prefix}edit --file -\n '\n \n test_expect_success 'refer config from subdirectory' '\n@@ -1642,7 +1642,7 @@ test_expect_success 'command line overrides environment config' '\n test_expect_success 'git config --edit works' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n-\tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n+\tGIT_EDITOR=\"echo [test]value=yes >\" git config ${mode_prefix}edit -f tmp &&\n \tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n@@ -1651,7 +1651,7 @@ test_expect_success 'git config --edit respects core.editor' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n \ttest_config core.editor \"echo [test]value=yes >\" &&\n-\tgit config -f tmp --edit &&\n+\tgit config ${mode_prefix}edit -f tmp &&\n \tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n-- \n2.44.GIT\n\n"},{"id":"491670","messageId":"657d1355b5d33056644effc5d170d323a58ebdbc.1711527812.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"[PATCH v3 13/13] builtin/config: display subcommand help","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T08:47:03Z","receivedAt":"2024-03-27T08:47:07Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Until now, `git config -h` would have printed help for the old-style\nsyntax. Now that all modes have proper subcommands though it is\npreferable to instead display the subcommand help.\n\nDrop the `NO_INTERNAL_HELP` flag to do so. While at it, drop the help\nmismatch in t0450 and add the `--get-colorbool` option to the usage such\nthat git-config(1)'s synopsis and `git config -h` match.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c            | 5 +++--\n t/t0450/txt-help-mismatches | 1 -\n 2 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex b8cb940b1e..18b1f7014a 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -23,6 +23,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tN_(\"git config remove-section [<file-option>] <name>\"),\n \tN_(\"git config edit [<file-option>]\"),\n+\tN_(\"git config [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\"),\n \tNULL\n };\n \n@@ -1084,10 +1085,10 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t * us when parsing the legacy-style modes that don't use subcommands.\n \t */\n \targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n \tif (subcommand) {\n \t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n-\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);\n \t\treturn subcommand(argc, argv, prefix);\n \t}\n \ndiff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches\nindex a0777acd66..28003f18c9 100644\n--- a/t/t0450/txt-help-mismatches\n+++ b/t/t0450/txt-help-mismatches\n@@ -10,7 +10,6 @@ checkout\n checkout-index\n clone\n column\n-config\n credential\n credential-cache\n credential-store\n-- \n2.44.GIT\n\n"},{"id":"491671","messageId":"CAPig+cTX72Jrc2=9jo23rJfnNK4VKdOuKrCeo9YQPfELBHn1YQ@mail.gmail.com","threadId":"61061","inReplyTo":"cover.1711527811.git.ps@pks.im","subject":"Re: [PATCH v3 00/13] builtin/config: introduce subcommands","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2024-03-27T08:53:35Z","receivedAt":"2024-03-27T08:53:47Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Wed, Mar 27, 2024 at 4:46 AM Patrick Steinhardt <ps@pks.im> wrote:\n> this is the third version of my patch series that aims to add\n> subcommands to git-config(1) in a backwards-compatible manner.\n>\n> There's only been a few changes compared to v2 based on Eric's comments:\n>\n>  7:  8ceced0fc5 !  7:  eee1fae50c builtin/config: introduce \"get\" subcommand\n>     -+  With \"get\", Return all values for a multi-valued key.\n>     ++  With `get`, Return all values for a multi-valued key.\n\nBetter. This fixes the typesetting problem but misses the\ns/Return/return/ mentioned earlier[1].\n\n[1]: https://lore.kernel.org/git/CAPig+cRwh4HCi=Q01tGJ0WOb59iE18HWSCNxGogcreOz+2w1WA@mail.gmail.com/\n"},{"id":"491674","messageId":"ZgPj7_QaiIqD0yiS@tanuki","threadId":"61061","inReplyTo":"CAPig+cTX72Jrc2=9jo23rJfnNK4VKdOuKrCeo9YQPfELBHn1YQ@mail.gmail.com","subject":"Re: [PATCH v3 00/13] builtin/config: introduce subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-03-27T09:16:31Z","receivedAt":"2024-03-27T09:16:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Mar 27, 2024 at 04:53:35AM -0400, Eric Sunshine wrote:\n> On Wed, Mar 27, 2024 at 4:46 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > this is the third version of my patch series that aims to add\n> > subcommands to git-config(1) in a backwards-compatible manner.\n> >\n> > There's only been a few changes compared to v2 based on Eric's comments:\n> >\n> >  7:  8ceced0fc5 !  7:  eee1fae50c builtin/config: introduce \"get\" subcommand\n> >     -+  With \"get\", Return all values for a multi-valued key.\n> >     ++  With `get`, Return all values for a multi-valued key.\n> \n> Better. This fixes the typesetting problem but misses the\n> s/Return/return/ mentioned earlier[1].\n> \n> [1]: https://lore.kernel.org/git/CAPig+cRwh4HCi=Q01tGJ0WOb59iE18HWSCNxGogcreOz+2w1WA@mail.gmail.com/\n\nAh, sorry, I missed that one. Fixed locally, will send out with the\ninevitable v4 of this patch series. Thanks!\n\nPatrick\n"},{"id":"493974","messageId":"cover.1714730169.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH v4 00/14] builtin/config: introduce subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:56:40Z","receivedAt":"2024-05-03T09:56:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the fourth version of my patch series that introduces\nsubcommands for git-config(1).\n\nChanges compared to v3:\n\n    - Rebased on top of d4cc1ec35f (Start the 2.46 cycle, 2024-04-30).\n\n    - Implemented support for `git config set --comment`. This switch\n      has been added since the last version of this patch series.\n\nHere's hoping that there's a bit more interest in this patch series at\nthe beginning of the release cycle :)\n\nPatrick\n\nPatrick Steinhardt (14):\n  config: clarify memory ownership when preparing comment strings\n  builtin/config: move option array around\n  builtin/config: move \"fixed-value\" option to correct group\n  builtin/config: use `OPT_CMDMODE()` to specify modes\n  builtin/config: pull out function to handle config location\n  builtin/config: pull out function to handle `--null`\n  builtin/config: introduce \"list\" subcommand\n  builtin/config: introduce \"get\" subcommand\n  builtin/config: introduce \"set\" subcommand\n  builtin/config: introduce \"unset\" subcommand\n  builtin/config: introduce \"rename-section\" subcommand\n  builtin/config: introduce \"remove-section\" subcommand\n  builtin/config: introduce \"edit\" subcommand\n  builtin/config: display subcommand help\n\n Documentation/git-config.txt | 219 ++++++++-------\n builtin/config.c             | 512 ++++++++++++++++++++++++++++-------\n config.c                     |  16 +-\n config.h                     |   2 +-\n t/t0450/txt-help-mismatches  |   1 -\n t/t1300-config.sh            | 432 +++++++++++++++++------------\n 6 files changed, 812 insertions(+), 370 deletions(-)\n\nRange-diff against v3:\n -:  ---------- >  1:  3aa26d5bff config: clarify memory ownership when preparing comment strings\n 1:  bfcb50e393 !  2:  8f0804ab48 builtin/config: move option array around\n    @@ builtin/config.c: static int option_parse_type(const struct option *opt, const c\n     -\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n     -\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n     -\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n    +-\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n     -\tOPT_END(),\n     -};\n     -\n    @@ builtin/config.c: static char *default_user_config(void)\n     +\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n     +\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n     +\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n    ++\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n     +\tOPT_END(),\n     +};\n     +\n 2:  ff428d8a22 !  3:  ddcd8031d7 builtin/config: move \"fixed-value\" option to correct group\n    @@ builtin/config.c: static struct option builtin_config_options[] = {\n      \tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n      \tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n     @@ builtin/config.c: static struct option builtin_config_options[] = {\n    - \tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n      \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n      \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n    + \tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n     +\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n      \tOPT_END(),\n      };\n 3:  e049c05713 =  4:  1bc3918840 builtin/config: use `OPT_CMDMODE()` to specify modes\n 4:  41585803bf !  5:  3754812309 builtin/config: pull out function to handle config location\n    @@ builtin/config.c: static char *default_user_config(void)\n     -\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n     -\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n     -\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n    +-\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n     -\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n     -\tOPT_END(),\n     -};\n     -\n     -static NORETURN void usage_builtin_config(void)\n    --{\n    ++static void handle_config_location(const char *prefix)\n    + {\n     -\tusage_with_options(builtin_config_usage, builtin_config_options);\n     -}\n     -\n     -int cmd_config(int argc, const char **argv, const char *prefix)\n    -+static void handle_config_location(const char *prefix)\n    - {\n    +-{\n     -\tint nongit = !startup_info->have_repository;\n    --\tchar *value = NULL;\n    +-\tchar *value = NULL, *comment = NULL;\n     -\tint flags = 0;\n     -\tint ret = 0;\n     -\tstruct key_value_info default_kvi = KVI_INIT;\n    @@ builtin/config.c: int cmd_config(int argc, const char **argv, const char *prefix\n     +\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n     +\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n     +\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n    ++\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n     +\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n     +\tOPT_END(),\n     +};\n    @@ builtin/config.c: int cmd_config(int argc, const char **argv, const char *prefix\n     +\n     +int cmd_config(int argc, const char **argv, const char *prefix)\n     +{\n    -+\tchar *value = NULL;\n    ++\tchar *value = NULL, *comment = NULL;\n     +\tint flags = 0;\n     +\tint ret = 0;\n     +\tstruct key_value_info default_kvi = KVI_INIT;\n 5:  95f661f267 =  6:  cb1714c493 builtin/config: pull out function to handle `--null`\n 6:  b50f32d074 !  7:  b3f3c3ba6a builtin/config: introduce \"list\" subcommand\n    @@ Documentation/git-config.txt: git-config - Get and set repository or global opti\n      --------\n      [verse]\n     +'git config list' [<file-option>] [<display-option>] [--includes]\n    - 'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n    - 'git config' [<file-option>] [--type=<type>] --add <name> <value>\n    - 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>\n    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n     @@ Documentation/git-config.txt: SYNOPSIS\n      'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n      'git config' [<file-option>] --rename-section <old-name> <new-name>\n    @@ builtin/config.c: static struct option builtin_config_options[] = {\n     -\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n     -\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n      \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n    + \tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n      \tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n     +\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n      \tOPT_END(),\n    @@ builtin/config.c: static NORETURN void usage_builtin_config(void)\n     +\n      int cmd_config(int argc, const char **argv, const char *prefix)\n      {\n    - \tchar *value = NULL;\n    + \tchar *value = NULL, *comment = NULL;\n     @@ builtin/config.c: int cmd_config(int argc, const char **argv, const char *prefix)\n      \n      \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n    @@ t/t1300-config.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n     +\tBUG \"unknown mode $mode\";;\n     +esac\n     +\n    - test_expect_success 'clear default config' '\n    - \trm -f .git/config\n    - '\n    + test_expect_success 'setup whitespace config' '\n    + \tsed -e \"s/^|//\" \\\n    + \t    -e \"s/[$]$//\" \\\n     @@ t/t1300-config.sh: version.1.2.3eX.alpha=beta\n      EOF\n      \n    @@ t/t1300-config.sh: Qsection.sub=section.val4\n      \tnul_to_q <result.raw >result &&\n      \techo >>result &&\n      \ttest_cmp expect result\n    -@@ t/t1300-config.sh: test_expect_success 'inner whitespace kept verbatim' '\n    +@@ t/t1300-config.sh: test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces'\n      '\n      \n      test_expect_success SYMLINKS 'symlinked configuration' '\n 7:  eee1fae50c !  8:  0e6da909ac builtin/config: introduce \"get\" subcommand\n    @@ Documentation/git-config.txt: SYNOPSIS\n      [verse]\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' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n    - 'git config' [<file-option>] [--type=<type>] --add <name> <value>\n    - 'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>\n    + 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n     -'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]\n     -'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]\n     -'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]\n    @@ Documentation/git-config.txt: COMMANDS\n      OPTIONS\n      -------\n     @@ Documentation/git-config.txt: OPTIONS\n    - \tvalues.  This is the same as providing '^$' as the `value-pattern`\n    - \tin `--replace-all`.\n    + \tnot contain linefeed characters (no multi-line comments are\n    + \tpermitted).\n      \n     ---get::\n     -\tGet the value for a given key (optionally filtered by a regex\n    @@ Documentation/git-config.txt: OPTIONS\n     ---get-all::\n     -\tLike get, but returns all values for a multi-valued key.\n     +--all::\n    -+\tWith `get`, Return all values for a multi-valued key.\n    ++\tWith `get`, return all values for a multi-valued key.\n      \n     ---get-regexp::\n     -\tLike --get-all, but interprets the name as a regular expression and\n    @@ t/t1300-config.sh: test_expect_success '--null --list' '\n      \tnul_to_q <result.raw >result &&\n      \techo >>result &&\n      \ttest_cmp expect result\n    +@@ t/t1300-config.sh: test_expect_success '--null --get-regexp' '\n    + test_expect_success 'inner whitespace kept verbatim, spaces only' '\n    + \techo \"foo   bar\" >expect &&\n    + \tgit config section.val \"foo   bar\" &&\n    +-\tgit config --get section.val >actual &&\n    ++\tgit config ${mode_get} section.val >actual &&\n    + \ttest_cmp expect actual\n    + '\n    + \n    + test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '\n    + \techo \"fooQQbar\" | q_to_tab >expect &&\n    + \tgit config section.val \"$(cat expect)\" &&\n    +-\tgit config --get section.val >actual &&\n    ++\tgit config ${mode_get} section.val >actual &&\n    + \ttest_cmp expect actual\n    + '\n    + \n    + test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '\n    + \techo \"foo Q  bar\" | q_to_tab >expect &&\n    + \tgit config section.val \"$(cat expect)\" &&\n    +-\tgit config --get section.val >actual &&\n    ++\tgit config ${mode_get} section.val >actual &&\n    + \ttest_cmp expect actual\n    + '\n    + \n     @@ t/t1300-config.sh: test_expect_success 'git -c can represent empty string' '\n      '\n      \n 8:  e2815affab !  9:  8a623a31b9 builtin/config: introduce \"set\" subcommand\n    @@ Documentation/git-config.txt: SYNOPSIS\n      [verse]\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' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n    --'git config' [<file-option>] [--type=<type>] --add <name> <value>\n    --'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n    -+'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n    +-'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n    +-'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>\n    +-'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n    ++'git config set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n      'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n      'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n      'git config' [<file-option>] --rename-section <old-name> <new-name>\n    @@ Documentation/git-config.txt: OPTIONS\n     -\tin `--replace-all`.\n     +\tvalues. This is the same as providing '--value=^$' in `set`.\n      \n    - --all::\n    - \tWith `get`, Return all values for a multi-valued key.\n    + --comment <message>::\n    + \tAppend a comment at the end of new or modified lines.\n     @@ Documentation/git-config.txt: recommended to migrate to the new syntax.\n      'git config <name>'::\n      \tReplaced by `git config get <name>`.\n    @@ builtin/config.c: static const char *const builtin_config_get_usage[] = {\n      };\n      \n     +static const char *const builtin_config_set_usage[] = {\n    -+\tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n    ++\tN_(\"git config set [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n     +\tNULL\n     +};\n     +\n    @@ builtin/config.c: static int cmd_config_get(int argc, const char **argv, const c\n      \n     +static int cmd_config_set(int argc, const char **argv, const char *prefix)\n     +{\n    -+\tconst char *value_pattern = NULL;\n    ++\tconst char *value_pattern = NULL, *comment_arg = NULL;\n    ++\tchar *comment = NULL;\n     +\tint flags = 0, append = 0;\n     +\tstruct option opts[] = {\n     +\t\tCONFIG_LOCATION_OPTIONS,\n    @@ builtin/config.c: static int cmd_config_get(int argc, const char **argv, const c\n     +\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n     +\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n     +\t\tOPT_GROUP(N_(\"Other\")),\n    ++\t\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n     +\t\tOPT_BOOL(0, \"append\", &append, N_(\"add a new line without altering any existing values\")),\n     +\t\tOPT_END(),\n     +\t};\n    @@ builtin/config.c: static int cmd_config_get(int argc, const char **argv, const c\n     +\tif (append)\n     +\t\tvalue_pattern = CONFIG_REGEX_NONE;\n     +\n    ++\tcomment = git_config_prepare_comment_string(comment_arg);\n    ++\n     +\thandle_config_location(prefix);\n     +\n     +\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n    @@ builtin/config.c: static int cmd_config_get(int argc, const char **argv, const c\n     +\tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {\n     +\t\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n     +\t\t\t\t\t\t\t     argv[0], value, value_pattern,\n    -+\t\t\t\t\t\t\t     flags);\n    ++\t\t\t\t\t\t\t     comment, flags);\n     +\t} else {\n    -+\t\tret = git_config_set_in_file_gently(given_config_source.file, argv[0], value);\n    ++\t\tret = git_config_set_in_file_gently(given_config_source.file,\n    ++\t\t\t\t\t\t    argv[0], comment, value);\n     +\t\tif (ret == CONFIG_NOTHING_SET)\n     +\t\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n     +\t\t\t\"       Use a regexp, --add or --replace-all to change %s.\"), argv[0]);\n     +\t}\n     +\n    ++\tfree(comment);\n     +\tfree(value);\n     +\treturn ret;\n     +}\n    @@ t/t1300-config.sh: cat > expect << EOF\n      \ttest_cmp expect .git/config\n      '\n      \n    +@@ t/t1300-config.sh: EOF\n    + \n    + test_expect_success 'append comments' '\n    + \tgit config --replace-all --comment=\"Pygoscelis papua\" section.penguin gentoo &&\n    +-\tgit config --comment=\"find fish\" section.disposition peckish &&\n    +-\tgit config --comment=\"#abc\" section.foo bar &&\n    ++\tgit config ${mode_set} --comment=\"find fish\" section.disposition peckish &&\n    ++\tgit config ${mode_set} --comment=\"#abc\" section.foo bar &&\n    + \n    + \tgit config --comment=\"and comment\" section.spsp value &&\n    + \tgit config --comment=\"\t# and comment\" section.htsp value &&\n    +@@ t/t1300-config.sh: test_expect_success 'append comments' '\n    + '\n    + \n    + test_expect_success 'Prohibited LF in comment' '\n    +-\ttest_must_fail git config --comment=\"a${LF}b\" section.k v\n    ++\ttest_must_fail git config ${mode_set} --comment=\"a${LF}b\" section.k v\n    + '\n    + \n    + test_expect_success 'non-match result' 'test_cmp expect .git/config'\n     @@ t/t1300-config.sh: test_expect_success 'multiple unset is correct' '\n      cp .git/config2 .git/config\n      \n    @@ t/t1300-config.sh: test_expect_success 'key with newline' '\n      \n      cat > .git/config <<\\EOF\n     @@ t/t1300-config.sh: test_expect_success '--null --get-regexp' '\n    + \n    + test_expect_success 'inner whitespace kept verbatim, spaces only' '\n    + \techo \"foo   bar\" >expect &&\n    +-\tgit config section.val \"foo   bar\" &&\n    ++\tgit config ${mode_set} section.val \"foo   bar\" &&\n    + \tgit config ${mode_get} section.val >actual &&\n    + \ttest_cmp expect actual\n      '\n      \n    - test_expect_success 'inner whitespace kept verbatim' '\n    --\tgit config section.val \"foo \t  bar\" &&\n    -+\tgit config ${mode_set} section.val \"foo \t  bar\" &&\n    - \ttest_cmp_config \"foo \t  bar\" section.val\n    + test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '\n    + \techo \"fooQQbar\" | q_to_tab >expect &&\n    +-\tgit config section.val \"$(cat expect)\" &&\n    ++\tgit config ${mode_set} section.val \"$(cat expect)\" &&\n    + \tgit config ${mode_get} section.val >actual &&\n    + \ttest_cmp expect actual\n      '\n      \n    + test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '\n    + \techo \"foo Q  bar\" | q_to_tab >expect &&\n    +-\tgit config section.val \"$(cat expect)\" &&\n    ++\tgit config ${mode_set} section.val \"$(cat expect)\" &&\n    + \tgit config ${mode_get} section.val >actual &&\n    + \ttest_cmp expect actual\n    + '\n     @@ t/t1300-config.sh: test_expect_success 'check split_cmdline return' '\n      \tgit init repo &&\n      \t(\n 9:  90f055ae1d ! 10:  e25e5b69cd builtin/config: introduce \"unset\" subcommand\n    @@ Commit message\n     \n      ## Documentation/git-config.txt ##\n     @@ Documentation/git-config.txt: SYNOPSIS\n    + [verse]\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 set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n     -'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n     -'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\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' [<file-option>] --rename-section <old-name> <new-name>\n      'git config' [<file-option>] --remove-section <name>\n    @@ builtin/config.c: static int cmd_config_set(int argc, const char **argv, const c\n     +\tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)\n     +\t\treturn git_config_set_multivar_in_file_gently(given_config_source.file,\n     +\t\t\t\t\t\t\t      argv[0], NULL, value_pattern,\n    -+\t\t\t\t\t\t\t      flags);\n    ++\t\t\t\t\t\t\t      NULL, flags);\n     +\telse\n    -+\t\treturn git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);\n    ++\t\treturn git_config_set_in_file_gently(given_config_source.file, argv[0],\n    ++\t\t\t\t\t\t     NULL, NULL);\n     +}\n     +\n      static struct option builtin_subcommand_options[] = {\n10:  3e360b1f47 ! 11:  f24008d356 builtin/config: introduce \"rename-section\" subcommand\n    @@ builtin/config.c: static const char *const builtin_config_unset_usage[] = {\n      static regex_t *key_regexp;\n      static const char *value_pattern;\n     @@ builtin/config.c: static int cmd_config_unset(int argc, const char **argv, const char *prefix)\n    - \t\treturn git_config_set_in_file_gently(given_config_source.file, argv[0], NULL);\n    + \t\t\t\t\t\t     NULL, NULL);\n      }\n      \n     +static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)\n11:  d610b5fda1 = 12:  fc2ddd3201 builtin/config: introduce \"remove-section\" subcommand\n12:  4a6512c88a = 13:  4c2d817eff builtin/config: introduce \"edit\" subcommand\n13:  657d1355b5 = 14:  4c351b12b8 builtin/config: display subcommand help\n-- \n2.45.0\n\n"},{"id":"493975","messageId":"3aa26d5bff21e0b93736b1502fa90694f6b2cedc.1714730169.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 01/14] config: clarify memory ownership when preparing comment strings","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:56:44Z","receivedAt":"2024-05-03T09:56:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The ownership of memory returned when preparing a comment string is\nquite intricate: when the returned value is different than the passed\nvalue, then the caller is responsible to free the memory. This is quite\nsubtle, and it's even easier to miss because the returned value is in\nfact a `const char *`.\n\nAdapt the function to always return either `NULL` or a newly allocated\nstring. The function is called at most once per git-config(1), so it's\nnot like this micro-optimization really matters. Thus, callers are now\nalways responsible for freeing the value.\n---\n builtin/config.c | 11 ++++++-----\n config.c         | 16 ++++++----------\n config.h         |  2 +-\n 3 files changed, 13 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 0015620dde..40456c0770 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -44,7 +44,7 @@ static struct config_options config_options;\n static int show_origin;\n static int show_scope;\n static int fixed_value;\n-static const char *comment;\n+static const char *comment_arg;\n \n #define ACTION_GET (1<<0)\n #define ACTION_GET_ALL (1<<1)\n@@ -174,7 +174,7 @@ static struct option builtin_config_options[] = {\n \tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_STRING(0, \"comment\", &comment, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n+\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n \tOPT_END(),\n };\n \n@@ -674,7 +674,7 @@ static char *default_user_config(void)\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tint nongit = !startup_info->have_repository;\n-\tchar *value = NULL;\n+\tchar *value = NULL, *comment = NULL;\n \tint flags = 0;\n \tint ret = 0;\n \tstruct key_value_info default_kvi = KVI_INIT;\n@@ -799,7 +799,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (comment &&\n+\tif (comment_arg &&\n \t    !(actions & (ACTION_ADD|ACTION_SET|ACTION_SET_ALL|ACTION_REPLACE_ALL))) {\n \t\terror(_(\"--comment is only applicable to add/set/replace operations\"));\n \t\tusage_builtin_config();\n@@ -841,7 +841,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tflags |= CONFIG_FLAGS_FIXED_VALUE;\n \t}\n \n-\tcomment = git_config_prepare_comment_string(comment);\n+\tcomment = git_config_prepare_comment_string(comment_arg);\n \n \tif (actions & PAGING_ACTIONS)\n \t\tsetup_auto_pager(\"config\", 1);\n@@ -993,6 +993,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\treturn get_colorbool(argv[0], argc == 2);\n \t}\n \n+\tfree(comment);\n \tfree(value);\n \treturn ret;\n }\ndiff --git a/config.c b/config.c\nindex ae3652b08f..13cf9eeb16 100644\n--- a/config.c\n+++ b/config.c\n@@ -3182,14 +3182,10 @@ void git_config_set(const char *key, const char *value)\n \ttrace2_cmd_set_config(key, value);\n }\n \n-/*\n- * The ownership rule is that the caller will own the string\n- * if it receives a piece of memory different from what it passed\n- * as the parameter.\n- */\n-const char *git_config_prepare_comment_string(const char *comment)\n+char *git_config_prepare_comment_string(const char *comment)\n {\n \tsize_t leading_blanks;\n+\tchar *prepared;\n \n \tif (!comment)\n \t\treturn NULL;\n@@ -3210,13 +3206,13 @@ const char *git_config_prepare_comment_string(const char *comment)\n \n \tleading_blanks = strspn(comment, \" \\t\");\n \tif (leading_blanks && comment[leading_blanks] == '#')\n-\t\t; /* use it as-is */\n+\t\tprepared = xstrdup(comment); /* use it as-is */\n \telse if (comment[0] == '#')\n-\t\tcomment = xstrfmt(\" %s\", comment);\n+\t\tprepared = xstrfmt(\" %s\", comment);\n \telse\n-\t\tcomment = xstrfmt(\" # %s\", comment);\n+\t\tprepared = xstrfmt(\" # %s\", comment);\n \n-\treturn comment;\n+\treturn prepared;\n }\n \n static void validate_comment_string(const char *comment)\ndiff --git a/config.h b/config.h\nindex f4966e3749..db8b608064 100644\n--- a/config.h\n+++ b/config.h\n@@ -338,7 +338,7 @@ void git_config_set_multivar(const char *, const char *, const char *, unsigned)\n int repo_config_set_multivar_gently(struct repository *, const char *, const char *, const char *, unsigned);\n int git_config_set_multivar_in_file_gently(const char *, const char *, const char *, const char *, const char *, unsigned);\n \n-const char *git_config_prepare_comment_string(const char *);\n+char *git_config_prepare_comment_string(const char *);\n \n /**\n  * takes four parameters:\n-- \n2.45.0\n\n"},{"id":"493976","messageId":"8f0804ab4864438bd34a8bab5ff44f504aa8d443.1714730169.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 02/14] builtin/config: move option array around","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:56:49Z","receivedAt":"2024-05-03T09:56:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move around the option array. This will help us with a follow-up commit\nthat introduces subcommands to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 96 ++++++++++++++++++++++++------------------------\n 1 file changed, 48 insertions(+), 48 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 40456c0770..59ae5996eb 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -135,54 +135,6 @@ static int option_parse_type(const struct option *opt, const char *arg,\n \treturn 0;\n }\n \n-static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n-\tOPT_GROUP(N_(\"Action\")),\n-\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n-\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n-\tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n-\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n-\tOPT_END(),\n-};\n-\n-static NORETURN void usage_builtin_config(void)\n-{\n-\tusage_with_options(builtin_config_usage, builtin_config_options);\n-}\n-\n static void check_argc(int argc, int min, int max)\n {\n \tif (argc >= min && argc <= max)\n@@ -671,6 +623,54 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n+static struct option builtin_config_options[] = {\n+\tOPT_GROUP(N_(\"Config file location\")),\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tOPT_GROUP(N_(\"Action\")),\n+\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n+\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n+\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n+\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n+\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n+\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n+\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n+\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_GROUP(N_(\"Type\")),\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tOPT_GROUP(N_(\"Other\")),\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n+\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n+\tOPT_END(),\n+};\n+\n+static NORETURN void usage_builtin_config(void)\n+{\n+\tusage_with_options(builtin_config_usage, builtin_config_options);\n+}\n+\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tint nongit = !startup_info->have_repository;\n-- \n2.45.0\n\n"},{"id":"493977","messageId":"ddcd8031d7e399b126344d82d3373a2b2ed7fb8f.1714730169.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 03/14] builtin/config: move \"fixed-value\" option to correct group","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:56:55Z","receivedAt":"2024-05-03T09:57:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `--fixed-value` option can be used to alter how the value-pattern\nparameter is interpreted for the various submodes of git-config(1). But\nwhile it is an option, it is currently listed as part of the submodes\ngroup the command, which is wrong.\n\nMove the option to the \"Other\" group, which hosts the various options\nknown to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 59ae5996eb..054019b70c 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -643,7 +643,6 @@ static struct option builtin_config_options[] = {\n \tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n \tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n \tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n \tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n \tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n \tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n@@ -663,6 +662,7 @@ static struct option builtin_config_options[] = {\n \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n \tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n \tOPT_END(),\n };\n \n-- \n2.45.0\n\n"},{"id":"493978","messageId":"1bc39188409c8239c95555148487cfba7550475f.1714730169.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 04/14] builtin/config: use `OPT_CMDMODE()` to specify modes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:00Z","receivedAt":"2024-05-03T09:57:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The git-config(1) command has various different modes which are\naccessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are\ndeclared with `OPT_BIT()`, which causes two minor issues:\n\n  - The respective modes also have a negated form `--no-get-urlmatch`,\n    which is unintended.\n\n  - We have to manually handle exclusiveness of the modes.\n\nSwitch these options to instead use `OPT_CMDMODE()`, which is made\nexactly for this usecase. Remove the now-unneeded check that only a\nsingle mode is given, which is now handled by the parse-options\ninterface.\n\nWhile at it, format optional placeholders for arguments to conform to\nour style guidelines by using `[<placeholder>]`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c  | 32 ++++++++++++++------------------\n t/t1300-config.sh | 13 +++++++++++++\n 2 files changed, 27 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 054019b70c..8991533e08 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -632,20 +632,20 @@ static struct option builtin_config_options[] = {\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n \tOPT_GROUP(N_(\"Action\")),\n-\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n+\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n+\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n+\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n+\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n+\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n+\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n+\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n \tOPT_GROUP(N_(\"Type\")),\n \tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n \tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n@@ -769,10 +769,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (HAS_MULTI_BITS(actions)) {\n-\t\terror(_(\"only one action at a time\"));\n-\t\tusage_builtin_config();\n-\t}\n \tif (actions == 0)\n \t\tswitch (argc) {\n \t\tcase 1: actions = ACTION_GET; break;\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 9b65d9eaf5..86dc70769a 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -2738,4 +2738,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such\n \tgrep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n '\n \n+test_expect_success 'negated mode causes failure' '\n+\ttest_must_fail git config --no-get 2>err &&\n+\tgrep \"unknown option \\`no-get${SQ}\" err\n+'\n+\n+test_expect_success 'specifying multiple modes causes failure' '\n+\tcat >expect <<-EOF &&\n+\terror: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together\n+\tEOF\n+\ttest_must_fail git config --get --get-all 2>err &&\n+\ttest_cmp expect err\n+'\n+\n test_done\n-- \n2.45.0\n\n"},{"id":"493979","messageId":"3754812309ceb1dd0564e2cc7096b487acaf484e.1714730170.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 05/14] builtin/config: pull out function to handle config location","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:04Z","receivedAt":"2024-05-03T09:57:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There's quite a bunch of options to git-config(1) that allow the user to\nspecify which config location to use when reading or writing config\noptions. The logic to handle this is thus by necessity also quite\ninvolved.\n\nPull it out into a separate function so that we can reuse it in\nsubsequent commits which introduce proper subcommands.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 133 ++++++++++++++++++++++++-----------------------\n 1 file changed, 68 insertions(+), 65 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 8991533e08..8c7cd30cb4 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -623,68 +623,8 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n-\tOPT_GROUP(N_(\"Action\")),\n-\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n-\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n-\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n-\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n-\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n-\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n-\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n-\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n-\tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n-\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n-\tOPT_END(),\n-};\n-\n-static NORETURN void usage_builtin_config(void)\n+static void handle_config_location(const char *prefix)\n {\n-\tusage_with_options(builtin_config_usage, builtin_config_options);\n-}\n-\n-int cmd_config(int argc, const char **argv, const char *prefix)\n-{\n-\tint nongit = !startup_info->have_repository;\n-\tchar *value = NULL, *comment = NULL;\n-\tint flags = 0;\n-\tint ret = 0;\n-\tstruct key_value_info default_kvi = KVI_INIT;\n-\n-\tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n-\n-\targc = parse_options(argc, argv, prefix, builtin_config_options,\n-\t\t\t     builtin_config_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n-\n \tif (use_global_config + use_system_config + use_local_config +\n \t    use_worktree_config +\n \t    !!given_config_source.file + !!given_config_source.blob > 1) {\n@@ -692,14 +632,13 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (nongit) {\n+\tif (!startup_info->have_repository) {\n \t\tif (use_local_config)\n \t\t\tdie(_(\"--local can only be used inside a git repository\"));\n \t\tif (given_config_source.blob)\n \t\t\tdie(_(\"--blob can only be used inside a git repository\"));\n \t\tif (use_worktree_config)\n \t\t\tdie(_(\"--worktree can only be used inside a git repository\"));\n-\n \t}\n \n \tif (given_config_source.file &&\n@@ -753,10 +692,74 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tconfig_options.respect_includes = !given_config_source.file;\n \telse\n \t\tconfig_options.respect_includes = respect_includes_opt;\n-\tif (!nongit) {\n+\tif (startup_info->have_repository) {\n \t\tconfig_options.commondir = get_git_common_dir();\n \t\tconfig_options.git_dir = get_git_dir();\n \t}\n+}\n+\n+static struct option builtin_config_options[] = {\n+\tOPT_GROUP(N_(\"Config file location\")),\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tOPT_GROUP(N_(\"Action\")),\n+\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n+\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n+\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n+\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n+\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n+\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n+\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n+\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n+\tOPT_GROUP(N_(\"Type\")),\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tOPT_GROUP(N_(\"Other\")),\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n+\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_END(),\n+};\n+\n+static NORETURN void usage_builtin_config(void)\n+{\n+\tusage_with_options(builtin_config_usage, builtin_config_options);\n+}\n+\n+int cmd_config(int argc, const char **argv, const char *prefix)\n+{\n+\tchar *value = NULL, *comment = NULL;\n+\tint flags = 0;\n+\tint ret = 0;\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\n+\tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n+\n+\targc = parse_options(argc, argv, prefix, builtin_config_options,\n+\t\t\t     builtin_config_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\thandle_config_location(prefix);\n \n \tif (end_nul) {\n \t\tterm = '\\0';\n@@ -858,7 +861,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tchar *config_file;\n \n \t\tcheck_argc(argc, 0, 0);\n-\t\tif (!given_config_source.file && nongit)\n+\t\tif (!given_config_source.file && !startup_info->have_repository)\n \t\t\tdie(_(\"not in a git directory\"));\n \t\tif (given_config_source.use_stdin)\n \t\t\tdie(_(\"editing stdin is not supported\"));\n-- \n2.45.0\n\n"},{"id":"493980","messageId":"cb1714c493fe556c7a55fada2e83d97e04bf4748.1714730170.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 06/14] builtin/config: pull out function to handle `--null`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:09Z","receivedAt":"2024-05-03T09:57:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Pull out function to handle the `--null` option, which we are about to\nreuse in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 15 +++++++++------\n 1 file changed, 9 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 8c7cd30cb4..59877065f8 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -698,6 +698,14 @@ static void handle_config_location(const char *prefix)\n \t}\n }\n \n+static void handle_nul(void) {\n+\tif (end_nul) {\n+\t\tterm = '\\0';\n+\t\tdelim = '\\n';\n+\t\tkey_delim = '\\n';\n+\t}\n+}\n+\n static struct option builtin_config_options[] = {\n \tOPT_GROUP(N_(\"Config file location\")),\n \tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n@@ -760,12 +768,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n \thandle_config_location(prefix);\n-\n-\tif (end_nul) {\n-\t\tterm = '\\0';\n-\t\tdelim = '\\n';\n-\t\tkey_delim = '\\n';\n-\t}\n+\thandle_nul();\n \n \tif ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {\n \t\terror(_(\"--get-color and variable type are incoherent\"));\n-- \n2.45.0\n\n"},{"id":"493981","messageId":"b3f3c3ba6ab33d269f7a707eae1456c87cdfeddb.1714730170.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:14Z","receivedAt":"2024-05-03T09:57:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-config(1) has several modes, those modes are not exposed with\nsubcommands but instead by specifying e.g. `--unset` or `--list`. This\nuser interface is not really in line with how our more modern commands\nwork, where it is a lot more customary to say e.g. `git remote list`.\nFurthermore, to add to the confusion, git-config(1) also allows the user\nto request modes implicitly by just specifying the correct number of\narguments. Thus, `git config foo.bar` will retrieve the value of\n\"foo.bar\" while `git config foo.bar baz` will set it to \"baz\".\n\nOverall, this makes for a confusing interface that could really use a\nmakeover. It hurts discoverability of what you can do with git-config(1)\nand is comparatively easy to get wrong. Converting the command to have\nsubcommands instead would go a long way to help address these issues.\n\nOne concern in this context is backwards compatibility. Luckily, we can\nintroduce subcommands without breaking backwards compatibility at all.\nThis is because all the implicit modes of git-config(1) require that the\nfirst argument is a properly formatted config key. And as config keys\n_must_ have a dot in their name, any value without a dot would have been\ndiscarded by git-config(1) previous to this change. Thus, given that\nnone of the subcommands do have a dot, they are unambiguous.\n\nIntroduce the first such new subcommand, which is \"git config list\". To\nretain backwards compatibility we only conditionally use subcommands and\nwill fall back to the old syntax in case no subcommand was detected.\nThis should help to transition to the new-style syntax until we\neventually deprecate and remove the old-style syntax.\n\nNote that the way we handle this we're duplicating some functionality\nacross old and new syntax. While this isn't pretty, it helps us to\nensure that there really is no change in behaviour for the old syntax.\n\nAmend tests such that we run them both with old and new style syntax.\nAs tests are now run twice, state from the first run may be still be\naround in the second run and thus cause tests to fail. Add cleanup logic\nas required to fix such tests.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt |  26 ++++++---\n builtin/config.c             |  90 ++++++++++++++++++++++++----\n t/t1300-config.sh            | 110 +++++++++++++++++++++--------------\n 3 files changed, 162 insertions(+), 64 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex ac61113fcc..c83c97cb7e 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -9,6 +9,7 @@ git-config - Get and set repository or global options\n SYNOPSIS\n --------\n [verse]\n+'git config list' [<file-option>] [<display-option>] [--includes]\n 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>\n 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n@@ -20,7 +21,6 @@ SYNOPSIS\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n-'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list\n 'git config' [<file-option>] --get-color <name> [<default>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n@@ -74,6 +74,12 @@ On success, the command returns the exit code 0.\n A list of all available configuration variables can be obtained using the\n `git help --config` command.\n \n+COMMANDS\n+--------\n+\n+list::\n+\tList all variables set in config file, along with their values.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -190,10 +196,6 @@ See also <<FILES>>.\n --unset-all::\n \tRemove all lines matching the key from config file.\n \n--l::\n---list::\n-\tList all variables set in config file, along with their values.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -248,7 +250,7 @@ Valid `<type>`'s include:\n \tcontain line breaks.\n \n --name-only::\n-\tOutput only the names of config variables for `--list` or\n+\tOutput only the names of config variables for `list` or\n \t`--get-regexp`.\n \n --show-origin::\n@@ -300,10 +302,20 @@ Valid `<type>`'s include:\n   When using `--get`, and the requested variable is not found, behave as if\n   <value> were the value assigned to that variable.\n \n+DEPRECATED MODES\n+----------------\n+\n+The following modes have been deprecated in favor of subcommands. It is\n+recommended to migrate to the new syntax.\n+\n+-l::\n+--list::\n+\tReplaced by `git config list`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n-using `--list` or any of the `--get-*` which may return multiple results.\n+using `list` or any of the `--get-*` which may return multiple results.\n The default is to use a pager.\n \n [[FILES]]\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 59877065f8..f89ddce8da 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -16,10 +16,16 @@\n #include \"worktree.h\"\n \n static const char *const builtin_config_usage[] = {\n+\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config [<options>]\"),\n \tNULL\n };\n \n+static const char *const builtin_config_list_usage[] = {\n+\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -33,6 +39,7 @@ static char delim = '=';\n static char key_delim = ' ';\n static char term = '\\n';\n \n+static parse_opt_subcommand_fn *subcommand;\n static int use_global_config, use_system_config, use_local_config;\n static int use_worktree_config;\n static struct git_config_source given_config_source;\n@@ -706,14 +713,24 @@ static void handle_nul(void) {\n \t}\n }\n \n+#define CONFIG_LOCATION_OPTIONS \\\n+\tOPT_GROUP(N_(\"Config file location\")), \\\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")), \\\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")), \\\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")), \\\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")), \\\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")), \\\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\"))\n+\n+#define CONFIG_DISPLAY_OPTIONS \\\n+\tOPT_GROUP(N_(\"Display options\")), \\\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")), \\\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")), \\\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")), \\\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\"))\n+\n static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tCONFIG_LOCATION_OPTIONS,\n \tOPT_GROUP(N_(\"Action\")),\n \tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n \tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n@@ -737,15 +754,12 @@ static struct option builtin_config_options[] = {\n \tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n \tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n \tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tCONFIG_DISPLAY_OPTIONS,\n \tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n \tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n \tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n \tOPT_END(),\n };\n \n@@ -754,6 +768,42 @@ static NORETURN void usage_builtin_config(void)\n \tusage_with_options(builtin_config_usage, builtin_config_options);\n }\n \n+static int cmd_config_list(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_DISPLAY_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);\n+\tcheck_argc(argc, 0, 0);\n+\n+\thandle_config_location(prefix);\n+\thandle_nul();\n+\n+\tsetup_auto_pager(\"config\", 1);\n+\n+\tif (config_with_options(show_all_config, NULL,\n+\t\t\t\t&given_config_source, the_repository,\n+\t\t\t\t&config_options) < 0) {\n+\t\tif (given_config_source.file)\n+\t\t\tdie_errno(_(\"unable to read config file '%s'\"),\n+\t\t\t\t  given_config_source.file);\n+\t\telse\n+\t\t\tdie(_(\"error processing config file(s)\"));\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static struct option builtin_subcommand_options[] = {\n+\tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n+\tOPT_END(),\n+};\n+\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tchar *value = NULL, *comment = NULL;\n@@ -763,6 +813,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \n \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n \n+\t/*\n+\t * This is somewhat hacky: we first parse the command line while\n+\t * keeping all args intact in order to determine whether a subcommand\n+\t * has been specified. If so, we re-parse it a second time, but this\n+\t * time we drop KEEP_ARGV0. This is so that we don't munge the command\n+\t * line in case no subcommand was given, which would otherwise confuse\n+\t * us when parsing the legacy-style modes that don't use subcommands.\n+\t */\n+\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\tif (subcommand) {\n+\t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n+\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\treturn subcommand(argc, argv, prefix);\n+\t}\n+\n \targc = parse_options(argc, argv, prefix, builtin_config_options,\n \t\t\t     builtin_config_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 86dc70769a..f77d2f7847 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -11,6 +11,20 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n+for mode in legacy subcommands\n+do\n+\n+case \"$mode\" in\n+legacy)\n+\tmode_prefix=\"--\"\n+\t;;\n+subcommands)\n+\tmode_prefix=\"\"\n+\t;;\n+*)\n+\tBUG \"unknown mode $mode\";;\n+esac\n+\n test_expect_success 'setup whitespace config' '\n \tsed -e \"s/^|//\" \\\n \t    -e \"s/[$]$//\" \\\n@@ -460,11 +474,11 @@ version.1.2.3eX.alpha=beta\n EOF\n \n test_expect_success 'working --list' '\n-\tgit config --list > output &&\n+\tgit config ${mode_prefix}list > output &&\n \ttest_cmp expect output\n '\n test_expect_success '--list without repo produces empty output' '\n-\tgit --git-dir=nonexistent config --list >output &&\n+\tgit --git-dir=nonexistent config ${mode_prefix}list >output &&\n \ttest_must_be_empty output\n '\n \n@@ -476,7 +490,7 @@ version.1.2.3eX.alpha\n EOF\n \n test_expect_success '--name-only --list' '\n-\tgit config --name-only --list >output &&\n+\tgit config ${mode_prefix}list --name-only >output &&\n \ttest_cmp expect output\n '\n \n@@ -614,17 +628,17 @@ ein.bahn=strasse\n EOF\n \n test_expect_success 'alternative GIT_CONFIG' '\n-\tGIT_CONFIG=other-config git config --list >output &&\n+\tGIT_CONFIG=other-config git config ${mode_prefix}list >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'alternative GIT_CONFIG (--file)' '\n-\tgit config --file other-config --list >output &&\n+\tgit config ${mode_prefix}list --file other-config >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'alternative GIT_CONFIG (--file=-)' '\n-\tgit config --file - --list <other-config >output &&\n+\tgit config ${mode_prefix}list --file - <other-config >output &&\n \ttest_cmp expect output\n '\n \n@@ -637,6 +651,7 @@ test_expect_success 'editing stdin is an error' '\n '\n \n test_expect_success 'refer config from subdirectory' '\n+\ttest_when_finished \"rm -r x\" &&\n \tmkdir x &&\n \ttest_cmp_config -C x strasse --file=../other-config --get ein.bahn\n '\n@@ -847,7 +862,7 @@ test_expect_success 'line number is reported correctly' '\n '\n \n test_expect_success 'invalid stdin config' '\n-\techo \"[broken\" | test_must_fail git config --list --file - >output 2>&1 &&\n+\techo \"[broken\" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&\n \ttest_grep \"bad config line 1 in standard input\" output\n '\n \n@@ -1139,7 +1154,7 @@ section.quotecont=cont;inued\n EOF\n \n test_expect_success 'value continued on next line' '\n-\tgit config --list > result &&\n+\tgit config ${mode_prefix}list > result &&\n \ttest_cmp expect result\n '\n \n@@ -1163,7 +1178,7 @@ Qsection.sub=section.val4\n Qsection.sub=section.val5Q\n EOF\n test_expect_success '--null --list' '\n-\tgit config --null --list >result.raw &&\n+\tgit config ${mode_prefix}list --null >result.raw &&\n \tnul_to_q <result.raw >result &&\n \techo >>result &&\n \ttest_cmp expect result\n@@ -1198,6 +1213,7 @@ test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces'\n '\n \n test_expect_success SYMLINKS 'symlinked configuration' '\n+\ttest_when_finished \"rm myconfig\" &&\n \tln -s notyet myconfig &&\n \tgit config --file=myconfig test.frotz nitfol &&\n \ttest -h myconfig &&\n@@ -1218,10 +1234,11 @@ test_expect_success SYMLINKS 'symlinked configuration' '\n '\n \n test_expect_success SYMLINKS 'symlink to nonexistent configuration' '\n+\ttest_when_finished \"rm linktonada linktolinktonada\" &&\n \tln -s doesnotexist linktonada &&\n \tln -s linktonada linktolinktonada &&\n-\ttest_must_fail git config --file=linktonada --list &&\n-\ttest_must_fail git config --file=linktolinktonada --list\n+\ttest_must_fail git config ${mode_prefix}list --file=linktonada &&\n+\ttest_must_fail git config ${mode_prefix}list --file=linktolinktonada\n '\n \n test_expect_success 'check split_cmdline return' '\n@@ -1478,7 +1495,7 @@ do\n done\n \n test_expect_success 'git -c is not confused by empty environment' '\n-\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config --list\n+\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config ${mode_prefix}list\n '\n \n test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n@@ -1669,31 +1686,31 @@ test_expect_success 'git config ignores pairs with empty count' '\n '\n \n test_expect_success 'git config fails with invalid count' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&\n+\ttest_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&\n \ttest_grep \"bogus count\" error &&\n-\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&\n+\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&\n \ttest_grep \"too many entries\" error\n '\n \n test_expect_success 'git config fails with missing config key' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config --list 2>error &&\n+\t\tgit config ${mode_prefix}list 2>error &&\n \ttest_grep \"missing config key\" error\n '\n \n test_expect_success 'git config fails with missing config value' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=\"pair.one\" \\\n-\t\tgit config --list 2>error &&\n+\t\tgit config ${mode_prefix}list 2>error &&\n \ttest_grep \"missing config value\" error\n '\n \n test_expect_success 'git config fails with invalid config pair key' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \\\n-\t\tgit config --list &&\n+\t\tgit config ${mode_prefix}list &&\n \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \\\n-\t\tgit config --list\n+\t\tgit config ${mode_prefix}list\n '\n \n test_expect_success 'environment overrides config file' '\n@@ -1733,7 +1750,7 @@ test_expect_success 'git config --edit works' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n \tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n-\tgit config -f tmp --list >actual &&\n+\tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1742,7 +1759,7 @@ test_expect_success 'git config --edit respects core.editor' '\n \techo test.value=yes >expect &&\n \ttest_config core.editor \"echo [test]value=yes >\" &&\n \tgit config -f tmp --edit &&\n-\tgit config -f tmp --list >actual &&\n+\tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -2093,7 +2110,7 @@ test_expect_success '--show-origin with --list' '\n \tcommand line:\tuser.cmdline=true\n \tEOF\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\\\n-\t\tgit -c user.cmdline=true config --list --show-origin >output &&\n+\t\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2110,7 +2127,7 @@ test_expect_success '--show-origin with --list --null' '\n \tincludeQcommand line:Quser.cmdline\n \ttrueQ\n \tEOF\n-\tgit -c user.cmdline=true config --null --list --show-origin >output.raw &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&\n \tnul_to_q <output.raw >output &&\n \t# The here-doc above adds a newline that the --null output would not\n \t# include. Add it here to make the two comparable.\n@@ -2124,7 +2141,7 @@ test_expect_success '--show-origin with single file' '\n \tfile:.git/config\tuser.override=local\n \tfile:.git/config\tinclude.path=../include/relative.include\n \tEOF\n-\tgit config --local --list --show-origin >output &&\n+\tgit config ${mode_prefix}list --local --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2162,7 +2179,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '\n \tcat >expect <<-\\EOF &&\n \tfile:\"file\\\" (dq) and spaces.conf\"\tuser.custom=true\n \tEOF\n-\tgit config --file \"$WEIRDLY_NAMED_FILE\" --show-origin --list >output &&\n+\tgit config ${mode_prefix}list --file \"$WEIRDLY_NAMED_FILE\" --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2170,7 +2187,7 @@ test_expect_success '--show-origin stdin' '\n \tcat >expect <<-\\EOF &&\n \tstandard input:\tuser.custom=true\n \tEOF\n-\tgit config --file - --show-origin --list <\"$CUSTOM_CONFIG_FILE\" >output &&\n+\tgit config ${mode_prefix}list --file - --show-origin <\"$CUSTOM_CONFIG_FILE\" >output &&\n \ttest_cmp expect output\n '\n \n@@ -2197,7 +2214,7 @@ test_expect_success '--show-origin blob' '\n \t\tcat >expect <<-EOF &&\n \t\tblob:$blob\tuser.custom=true\n \t\tEOF\n-\t\tgit config --blob=$blob --show-origin --list >output &&\n+\t\tgit config ${mode_prefix}list --blob=$blob --show-origin >output &&\n \t\ttest_cmp expect output\n \t)\n '\n@@ -2213,7 +2230,7 @@ test_expect_success '--show-origin blob ref' '\n \t\tcp \"$CUSTOM_CONFIG_FILE\" custom.conf &&\n \t\tgit add custom.conf &&\n \t\tgit commit -m \"new config file\" &&\n-\t\tgit config --blob=main:custom.conf --show-origin --list >output &&\n+\t\tgit config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&\n \t\ttest_cmp expect output\n \t)\n '\n@@ -2239,13 +2256,14 @@ test_expect_success '--show-scope with --list' '\n \tworktree\tuser.worktree=true\n \tcommand\tuser.cmdline=true\n \tEOF\n+\ttest_when_finished \"git worktree remove wt1\" &&\n \tgit worktree add wt1 &&\n \t# We need these to test for worktree scope, but outside of this\n \t# test, this is just noise\n \ttest_config core.repositoryformatversion 1 &&\n \ttest_config extensions.worktreeConfig true &&\n \tgit config --worktree user.worktree true &&\n-\tgit -c user.cmdline=true config --list --show-scope >output &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2254,7 +2272,7 @@ test_expect_success !MINGW '--show-scope with --blob' '\n \tcat >expect <<-EOF &&\n \tcommand\tuser.custom=true\n \tEOF\n-\tgit config --blob=$blob --show-scope --list >output &&\n+\tgit config ${mode_prefix}list --blob=$blob --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2264,7 +2282,7 @@ test_expect_success '--show-scope with --local' '\n \tlocal\tuser.override=local\n \tlocal\tinclude.path=../include/relative.include\n \tEOF\n-\tgit config --local --list --show-scope >output &&\n+\tgit config ${mode_prefix}list --local --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2288,7 +2306,7 @@ test_expect_success '--show-scope with --show-origin' '\n \tlocal\tfile:.git/../include/relative.include\tuser.relative=include\n \tcommand\tcommand line:\tuser.cmdline=true\n \tEOF\n-\tgit -c user.cmdline=true config --list --show-origin --show-scope >output &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2329,7 +2347,7 @@ test_expect_success 'override global and system config' '\n \tglobal\thome.config=true\n \tlocal\tlocal.config=true\n \tEOF\n-\tgit config --show-scope --list >output &&\n+\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output &&\n \n \tcat >expect <<-EOF &&\n@@ -2338,20 +2356,20 @@ test_expect_success 'override global and system config' '\n \tlocal\tlocal.config=true\n \tEOF\n \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \\\n-\t\tgit config --show-scope --list >output &&\n+\t\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output &&\n \n \tcat >expect <<-EOF &&\n \tlocal\tlocal.config=true\n \tEOF\n \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \\\n-\t\tgit config --show-scope --list >output &&\n+\t\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'override global and system config with missing file' '\n-\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&\n-\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&\n+\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&\n+\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&\n \tGIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version\n '\n \n@@ -2478,7 +2496,7 @@ test_expect_success 'set all config with value-pattern' '\n \t# no match => add new entry\n \tcp initial config &&\n \tgit config --file=config abc.key two a+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2491,7 +2509,7 @@ test_expect_success 'set all config with value-pattern' '\n \n \t# multiple values, no match => add\n \tgit config --file=config abc.key three a+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2501,7 +2519,7 @@ test_expect_success 'set all config with value-pattern' '\n \n \t# single match => replace\n \tgit config --file=config abc.key four h+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2516,7 +2534,7 @@ test_expect_success '--replace-all and value-pattern' '\n \tgit config --file=config --add abc.key two &&\n \tgit config --file=config --add abc.key three &&\n \tgit config --file=config --replace-all abc.key four \"o+\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=four\n \tabc.key=three\n@@ -2534,7 +2552,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --rename-section dev null &&\n \ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n-\ttest_must_fail git config --file=config --fixed-value --list &&\n+\ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n \n@@ -2555,7 +2573,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n \tfixed.test=bogus\n@@ -2564,7 +2582,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config --fixed-value fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tfixed.test=bogus\n \tEOF\n@@ -2582,7 +2600,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n \tfixed.test=bogus\n@@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \ttest_cmp expect actual &&\n \n \tgit config --file=config --fixed-value --replace-all fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=bogus\n \tfixed.test=bogus\n@@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes failure' '\n \ttest_cmp expect err\n '\n \n+done\n+\n test_done\n-- \n2.45.0\n\n"},{"id":"493982","messageId":"0e6da909ac75b9cdf5b32c3b6a29a1a2adbed9bd.1714730170.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 08/14] builtin/config: introduce \"get\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:18Z","receivedAt":"2024-05-03T09:57:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"get\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt |  93 ++++++++++++-------------\n builtin/config.c             |  69 +++++++++++++++---\n t/t1300-config.sh            | 131 ++++++++++++++++++++++++-----------\n 3 files changed, 194 insertions(+), 99 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex c83c97cb7e..d0878663db 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -10,18 +10,14 @@ SYNOPSIS\n --------\n [verse]\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' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>\n 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL>\n 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n-'git config' [<file-option>] --get-color <name> [<default>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n \n@@ -80,6 +76,12 @@ COMMANDS\n list::\n \tList all variables set in config file, along with their values.\n \n+get::\n+\tEmits the value of the specified key. If key is present multiple times\n+\tin the configuration, emits the last value. If `--all` is specified,\n+\temits all values associated with key. Returns error code 1 if key is\n+\tnot present.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -105,22 +107,16 @@ OPTIONS\n \tnot contain linefeed characters (no multi-line comments are\n \tpermitted).\n \n---get::\n-\tGet the value for a given key (optionally filtered by a regex\n-\tmatching the value). Returns error code 1 if the key was not\n-\tfound and the last value if multiple key values were found.\n-\n---get-all::\n-\tLike get, but returns all values for a multi-valued key.\n+--all::\n+\tWith `get`, return all values for a multi-valued key.\n \n---get-regexp::\n-\tLike --get-all, but interprets the name as a regular expression and\n-\twrites out the key names.  Regular expression matching is currently\n-\tcase-sensitive and done against a canonicalized version of the key\n-\tin which section and variable names are lowercased, but subsection\n-\tnames are not.\n+---regexp::\n+\tWith `get`, interpret the name as a regular expression. Regular\n+\texpression matching is currently case-sensitive and done against a\n+\tcanonicalized version of the key in which section and variable names\n+\tare lowercased, but subsection names are not.\n \n---get-urlmatch <name> <URL>::\n+--url=<URL>::\n \tWhen given a two-part <name> as <section>.<key>, the value for\n \t<section>.<URL>.<key> whose <URL> part matches the best to the\n \tgiven URL is returned (if no such key exists, the value for\n@@ -251,7 +247,7 @@ Valid `<type>`'s include:\n \n --name-only::\n \tOutput only the names of config variables for `list` or\n-\t`--get-regexp`.\n+\t`get`.\n \n --show-origin::\n \tAugment the output of all queried config options with the\n@@ -275,17 +271,6 @@ Valid `<type>`'s include:\n \tWhen the color setting for `name` is undefined, the command uses\n \t`color.ui` as fallback.\n \n---get-color <name> [<default>]::\n-\n-\tFind the color configured for `name` (e.g. `color.diff.new`) and\n-\toutput it as the ANSI color escape sequence to the standard\n-\toutput.  The optional `default` parameter is used instead, if\n-\tthere is no color configured for `name`.\n-+\n-`--type=color [--default=<default>]` is preferred over `--get-color`\n-(but note that `--get-color` will omit the trailing newline printed by\n-`--type=color`).\n-\n -e::\n --edit::\n \tOpens an editor to modify the specified config file; either\n@@ -299,7 +284,7 @@ Valid `<type>`'s include:\n \tconfig files.\n \n --default <value>::\n-  When using `--get`, and the requested variable is not found, behave as if\n+  When using `get`, and the requested variable is not found, behave as if\n   <value> were the value assigned to that variable.\n \n DEPRECATED MODES\n@@ -308,15 +293,33 @@ DEPRECATED MODES\n The following modes have been deprecated in favor of subcommands. It is\n recommended to migrate to the new syntax.\n \n+'git config <name>'::\n+\tReplaced by `git config get <name>`.\n+\n -l::\n --list::\n \tReplaced by `git config list`.\n \n+--get <name> [<value-pattern>]::\n+\tReplaced by `git config get [--value=<pattern>] <name>`.\n+\n+--get-all <name> [<value-pattern>]::\n+\tReplaced by `git config get [--value=<pattern>] --all --show-names <name>`.\n+\n+--get-regexp <name-regexp>::\n+\tReplaced by `git config get --all --show-names --regexp <name-regexp>`.\n+\n+--get-urlmatch <name> <URL>::\n+\tReplaced by `git config get --all --show-names --url=<URL> <name>`.\n+\n+--get-color <name> [<default>]::\n+\tReplaced by `git config get --type=color [--default=<default>] <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n-using `list` or any of the `--get-*` which may return multiple results.\n-The default is to use a pager.\n+using `list` or `get` which may return multiple results. The default is to use\n+a pager.\n \n [[FILES]]\n FILES\n@@ -519,25 +522,19 @@ you have to provide a regex matching the value of exactly one line.\n To query the value for a given key, do\n \n ------------\n-% git config --get core.filemode\n-------------\n-\n-or\n-\n-------------\n-% git config core.filemode\n+% git config get core.filemode\n ------------\n \n or, to query a multivar:\n \n ------------\n-% git config --get core.gitproxy \"for kernel.org$\"\n+% git config get --value=\"for kernel.org$\" core.gitproxy\n ------------\n \n If you want to know all the values for a multivar, do:\n \n ------------\n-% git config --get-all core.gitproxy\n+% git config get --all --show-names core.gitproxy\n ------------\n \n If you like to live dangerously, you can replace *all* core.gitproxy by a\n@@ -571,8 +568,8 @@ script:\n \n ------------\n #!/bin/sh\n-WS=$(git config --get-color color.diff.whitespace \"blue reverse\")\n-RESET=$(git config --get-color \"\" \"reset\")\n+WS=$(git config get --type=color --default=\"blue reverse\" color.diff.whitespace)\n+RESET=$(git config get --type=color --default=\"reset\" \"\")\n echo \"${WS}your whitespace color or blue reverse${RESET}\"\n ------------\n \n@@ -580,11 +577,11 @@ For URLs in `https://weak.example.com`, `http.sslVerify` is set to\n false, while it is set to `true` for all others:\n \n ------------\n-% git config --type=bool --get-urlmatch http.sslverify https://good.example.com\n+% git config get --type=bool --url=https://good.example.com http.sslverify\n true\n-% git config --type=bool --get-urlmatch http.sslverify https://weak.example.com\n+% git config get --type=bool --url=https://weak.example.com http.sslverify\n false\n-% git config --get-urlmatch http https://weak.example.com\n+% git config get --url=https://weak.example.com http\n http.cookieFile /tmp/cookie.txt\n http.sslverify false\n ------------\ndiff --git a/builtin/config.c b/builtin/config.c\nindex f89ddce8da..9e7ae49c02 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -17,7 +17,7 @@\n \n static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n-\tN_(\"git config [<options>]\"),\n+\tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tNULL\n };\n \n@@ -26,6 +26,11 @@ static const char *const builtin_config_list_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_get_usage[] = {\n+\tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -722,6 +727,16 @@ static void handle_nul(void) {\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")), \\\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\"))\n \n+#define CONFIG_TYPE_OPTIONS \\\n+\tOPT_GROUP(N_(\"Type\")), \\\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL), \\\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR), \\\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH), \\\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE)\n+\n #define CONFIG_DISPLAY_OPTIONS \\\n \tOPT_GROUP(N_(\"Display options\")), \\\n \tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")), \\\n@@ -746,14 +761,7 @@ static struct option builtin_config_options[] = {\n \tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n \tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n \tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tCONFIG_TYPE_OPTIONS,\n \tCONFIG_DISPLAY_OPTIONS,\n \tOPT_GROUP(N_(\"Other\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n@@ -799,8 +807,51 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix)\n \treturn 0;\n }\n \n+static int cmd_config_get(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL, *url = NULL;\n+\tint flags = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_TYPE_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter options\")),\n+\t\tOPT_BOOL(0, \"all\", &do_all, N_(\"return all values for multi-valued config options\")),\n+\t\tOPT_BOOL(0, \"regexp\", &use_key_regexp, N_(\"interpret the name as a regular expression\")),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_STRING(0, \"url\", &url, N_(\"URL\"), N_(\"show config matching the given URL\")),\n+\t\tCONFIG_DISPLAY_OPTIONS,\n+\t\tOPT_BOOL(0, \"show-names\", &show_keys, N_(\"show config keys in addition to their values\")),\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\t\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"use default value when missing entry\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_get_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_argc(argc, 1, 1);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with 'value-pattern'\"));\n+\tif (default_value && (do_all || url))\n+\t\tdie(_(\"--default= cannot be used with --all or --url=\"));\n+\tif (url && (do_all || use_key_regexp || value_pattern))\n+\t\tdie(_(\"--url= cannot be used with --all, --regexp or --value\"));\n+\n+\thandle_config_location(prefix);\n+\thandle_nul();\n+\n+\tsetup_auto_pager(\"config\", 1);\n+\n+\tif (url)\n+\t\treturn get_urlmatch(argv[0], url);\n+\treturn get_value(argv[0], value_pattern, flags);\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n+\tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex f77d2f7847..7ce13ed6cd 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -17,9 +17,15 @@ do\n case \"$mode\" in\n legacy)\n \tmode_prefix=\"--\"\n+\tmode_get=\"\"\n+\tmode_get_all=\"--get-all\"\n+\tmode_get_regexp=\"--get-regexp\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n+\tmode_get=\"get\"\n+\tmode_get_all=\"get --all\"\n+\tmode_get_regexp=\"get --regexp --all --show-names\"\n \t;;\n *)\n \tBUG \"unknown mode $mode\";;\n@@ -398,7 +404,7 @@ test_expect_success 'multi-valued get-all returns all' '\n \twow\n \twow2 for me\n \tEOF\n-\tgit config --get-all nextsection.nonewline >actual &&\n+\tgit config ${mode_get_all} nextsection.nonewline >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -500,7 +506,7 @@ nextsection.nonewline wow2 for me\n EOF\n \n test_expect_success '--get-regexp' '\n-\tgit config --get-regexp in >output &&\n+\tgit config ${mode_get_regexp} in >output &&\n \ttest_cmp expect output\n '\n \n@@ -510,7 +516,7 @@ nextsection.nonewline\n EOF\n \n test_expect_success '--name-only --get-regexp' '\n-\tgit config --name-only --get-regexp in >output &&\n+\tgit config ${mode_get_regexp} --name-only in >output &&\n \ttest_cmp expect output\n '\n \n@@ -521,7 +527,7 @@ EOF\n \n test_expect_success '--add' '\n \tgit config --add nextsection.nonewline \"wow4 for you\" &&\n-\tgit config --get-all nextsection.nonewline > output &&\n+\tgit config ${mode_get_all} nextsection.nonewline > output &&\n \ttest_cmp expect output\n '\n \n@@ -543,21 +549,21 @@ test_expect_success 'get variable with empty value' '\n echo novalue.variable > expect\n \n test_expect_success 'get-regexp variable with no value' '\n-\tgit config --get-regexp novalue > output &&\n+\tgit config ${mode_get_regexp} novalue > output &&\n \ttest_cmp expect output\n '\n \n echo 'novalue.variable true' > expect\n \n test_expect_success 'get-regexp --bool variable with no value' '\n-\tgit config --bool --get-regexp novalue > output &&\n+\tgit config ${mode_get_regexp} --bool novalue > output &&\n \ttest_cmp expect output\n '\n \n echo 'emptyvalue.variable ' > expect\n \n test_expect_success 'get-regexp variable with empty value' '\n-\tgit config --get-regexp emptyvalue > output &&\n+\tgit config ${mode_get_regexp} emptyvalue > output &&\n \ttest_cmp expect output\n '\n \n@@ -1131,7 +1137,7 @@ test_expect_success 'quoting' '\n '\n \n test_expect_success 'key with newline' '\n-\ttest_must_fail git config \"key.with\n+\ttest_must_fail git config ${mode_get} \"key.with\n newline\" 123'\n \n test_expect_success 'value with newline' 'git config key.sub value.with\\\\\\\n@@ -1185,7 +1191,7 @@ test_expect_success '--null --list' '\n '\n \n test_expect_success '--null --get-regexp' '\n-\tgit config --null --get-regexp \"val[0-9]\" >result.raw &&\n+\tgit config ${mode_get_regexp} --null \"val[0-9]\" >result.raw &&\n \tnul_to_q <result.raw >result &&\n \techo >>result &&\n \ttest_cmp expect result\n@@ -1194,21 +1200,21 @@ test_expect_success '--null --get-regexp' '\n test_expect_success 'inner whitespace kept verbatim, spaces only' '\n \techo \"foo   bar\" >expect &&\n \tgit config section.val \"foo   bar\" &&\n-\tgit config --get section.val >actual &&\n+\tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '\n \techo \"fooQQbar\" | q_to_tab >expect &&\n \tgit config section.val \"$(cat expect)\" &&\n-\tgit config --get section.val >actual &&\n+\tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '\n \techo \"foo Q  bar\" | q_to_tab >expect &&\n \tgit config section.val \"$(cat expect)\" &&\n-\tgit config --get section.val >actual &&\n+\tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1283,11 +1289,11 @@ test_expect_success 'git -c can represent empty string' '\n '\n \n test_expect_success 'key sanity-checking' '\n-\ttest_must_fail git config foo=bar &&\n-\ttest_must_fail git config foo=.bar &&\n-\ttest_must_fail git config foo.ba=r &&\n-\ttest_must_fail git config foo.1bar &&\n-\ttest_must_fail git config foo.\"ba\n+\ttest_must_fail git config ${mode_get} foo=bar &&\n+\ttest_must_fail git config ${mode_get} foo=.bar &&\n+\ttest_must_fail git config ${mode_get} foo.ba=r &&\n+\ttest_must_fail git config ${mode_get} foo.1bar &&\n+\ttest_must_fail git config ${mode_get} foo.\"ba\n \t\t\t\tz\".bar &&\n \ttest_must_fail git config . false &&\n \ttest_must_fail git config .foo false &&\n@@ -1336,7 +1342,7 @@ test_expect_success 'git -c complains about empty key and value' '\n '\n \n test_expect_success 'multiple git -c appends config' '\n-\ttest_config alias.x \"!git -c x.two=2 config --get-regexp ^x\\.*\" &&\n+\ttest_config alias.x \"!git -c x.two=2 config ${mode_get_regexp} ^x\\.*\" &&\n \tcat >expect <<-\\EOF &&\n \tx.one 1\n \tx.two 2\n@@ -1502,7 +1508,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n \tv=\"${SQ}key.one=foo${SQ}\" &&\n \tv=\"$v  ${SQ}key.two=bar${SQ}\" &&\n \tv=\"$v ${SQ}key.ambiguous=section.whatever=value${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.one foo\n \tkey.two bar\n@@ -1515,7 +1521,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '\n \tv=\"${SQ}key.one${SQ}=${SQ}foo${SQ}\" &&\n \tv=\"$v  ${SQ}key.two${SQ}=${SQ}bar${SQ}\" &&\n \tv=\"$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.one foo\n \tkey.two bar\n@@ -1529,7 +1535,7 @@ test_expect_success 'old and new-style entries can mix' '\n \tv=\"$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}\" &&\n \tv=\"$v ${SQ}key.oldtwo=oldbar${SQ}\" &&\n \tv=\"$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.oldone oldfoo\n \tkey.newone newfoo\n@@ -1542,7 +1548,7 @@ test_expect_success 'old and new-style entries can mix' '\n test_expect_success 'old and new bools with ambiguous subsection' '\n \tv=\"${SQ}key.with=equals.oldbool${SQ}\" &&\n \tv=\"$v ${SQ}key.with=equals.newbool${SQ}=\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.with equals.oldbool\n \tkey.with=equals.newbool\n@@ -1556,7 +1562,7 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '\n \tenv.two two\n \tEOF\n \tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ} ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"env.*\" >actual &&\n \ttest_cmp expect actual &&\n \n \tcat >expect <<-EOF &&\n@@ -1564,12 +1570,12 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '\n \tenv.two two\n \tEOF\n \tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ}\\\\$SQ$SQ$SQ ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"env.*\" >actual &&\n \ttest_cmp expect actual &&\n \n \ttest_must_fail env \\\n \t\tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ}\\\\$SQ ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\"\n+\t\tgit config ${mode_get_regexp} \"env.*\"\n '\n \n test_expect_success 'git --config-env=key=envvar support' '\n@@ -1617,7 +1623,7 @@ test_expect_success 'git -c and --config-env work together' '\n \tENVVAR=env-value git \\\n \t\t-c bar.cmd=cmd-value \\\n \t\t--config-env=bar.env=ENVVAR \\\n-\t\tconfig --get-regexp \"^bar.*\" >actual &&\n+\t\tconfig ${mode_get_regexp} \"^bar.*\" >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1645,7 +1651,7 @@ test_expect_success 'git config handles environment config pairs' '\n \tGIT_CONFIG_COUNT=2 \\\n \t\tGIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"foo\" \\\n \t\tGIT_CONFIG_KEY_1=\"pair.two\" GIT_CONFIG_VALUE_1=\"bar\" \\\n-\t\tgit config --get-regexp \"pair.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"pair.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tpair.one foo\n \tpair.two bar\n@@ -1655,7 +1661,7 @@ test_expect_success 'git config handles environment config pairs' '\n \n test_expect_success 'git config ignores pairs without count' '\n \ttest_must_fail env GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n@@ -1663,7 +1669,7 @@ test_expect_success 'git config ignores pairs exceeding count' '\n \tGIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n \t\tGIT_CONFIG_KEY_1=\"pair.two\" GIT_CONFIG_VALUE_1=\"value\" \\\n-\t\tgit config --get-regexp \"pair.*\" >actual 2>error &&\n+\t\tgit config ${mode_get_regexp} \"pair.*\" >actual 2>error &&\n \tcat >expect <<-EOF &&\n \tpair.one value\n \tEOF\n@@ -1674,14 +1680,14 @@ test_expect_success 'git config ignores pairs exceeding count' '\n test_expect_success 'git config ignores pairs with zero count' '\n \ttest_must_fail env \\\n \t\tGIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n test_expect_success 'git config ignores pairs with empty count' '\n \ttest_must_fail env \\\n \t\tGIT_CONFIG_COUNT= GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n@@ -1720,7 +1726,7 @@ test_expect_success 'environment overrides config file' '\n \tone = value\n \tEOF\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \\\n-\t\tgit config pair.one >actual &&\n+\t\tgit config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1730,7 +1736,7 @@ test_expect_success 'environment overrides config file' '\n test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \\\n \t\tGIT_CONFIG_PARAMETERS=\"${SQ}pair.one=override${SQ}\" \\\n-\t\tgit config pair.one >actual &&\n+\t\tgit config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1805,20 +1811,28 @@ test_expect_success 'urlmatch' '\n \n \ttest_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&\n \ttest_must_be_empty actual &&\n+\ttest_expect_code 1 git config get --url=https://good.example.com --bool doesnt.exist >actual &&\n+\ttest_must_be_empty actual &&\n \n \techo true >expect &&\n \tgit config --bool --get-urlmatch http.SSLverify https://good.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --bool --url=https://good.example.com http.SSLverify >actual &&\n+\ttest_cmp expect actual &&\n \n \techo false >expect &&\n \tgit config --bool --get-urlmatch http.sslverify https://weak.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --bool --url=https://weak.example.com http.sslverify >actual &&\n+\ttest_cmp expect actual &&\n \n \t{\n \t\techo http.cookiefile /tmp/cookie.txt &&\n \t\techo http.sslverify false\n \t} >expect &&\n \tgit config --get-urlmatch HTTP https://weak.example.com >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://weak.example.com HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1834,6 +1848,8 @@ test_expect_success 'urlmatch with --show-scope' '\n \tlocal\thttp.sslverify false\n \tEOF\n \tgit config --get-urlmatch --show-scope HTTP https://weak.example.com >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://weak.example.com --show-scope HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1866,45 +1882,67 @@ test_expect_success 'urlmatch favors more specific URLs' '\n \techo http.cookiefile /tmp/root.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com/subdirectory >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com/subdirectory HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com/subdirectory/nested HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/user.txt >expect &&\n \tgit config --get-urlmatch HTTP https://user@example.com/ >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://user@example.com/ HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://averylonguser@example.com/subdirectory HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/preceding.txt >expect &&\n \tgit config --get-urlmatch HTTP https://preceding.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://preceding.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/wildcard.txt >expect &&\n \tgit config --get-urlmatch HTTP https://wildcard.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://wildcard.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n \tgit config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://sub.example.com/wildcardwithsubdomain HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/trailing.txt >expect &&\n \tgit config --get-urlmatch HTTP https://trailing.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://trailing.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n \tgit config --get-urlmatch HTTP https://user@sub.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://user@sub.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/multiwildcard.txt >expect &&\n \tgit config --get-urlmatch HTTP https://wildcard.example.org >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://wildcard.example.org HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -2027,7 +2065,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[one]\n \tEOF\n \tgit config --unset two.subsection.key &&\n-\ttest \"not [two subsection]\" = \"$(git config one.key)\" &&\n+\ttest \"not [two subsection]\" = \"$(git config ${mode_get} one.key)\" &&\n \ttest_line_count = 3 .git/config\n '\n \n@@ -2150,7 +2188,7 @@ test_expect_success '--show-origin with --get-regexp' '\n \tfile:$HOME/.gitconfig\tuser.global true\n \tfile:.git/config\tuser.local true\n \tEOF\n-\tgit config --show-origin --get-regexp \"user\\.[g|l].*\" >output &&\n+\tgit config ${mode_get_regexp} --show-origin \"user\\.[g|l].*\" >output &&\n \ttest_cmp expect output\n '\n \n@@ -2158,7 +2196,7 @@ test_expect_success '--show-origin getting a single key' '\n \tcat >expect <<-\\EOF &&\n \tfile:.git/config\tlocal\n \tEOF\n-\tgit config --show-origin user.override >output &&\n+\tgit config ${mode_get} --show-origin user.override >output &&\n \ttest_cmp expect output\n '\n \n@@ -2290,7 +2328,7 @@ test_expect_success '--show-scope getting a single value' '\n \tcat >expect <<-\\EOF &&\n \tlocal\ttrue\n \tEOF\n-\tgit config --show-scope --get user.local >output &&\n+\tgit config ${mode_get} --show-scope user.local >output &&\n \ttest_cmp expect output\n '\n \n@@ -2559,9 +2597,9 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \t# These modes complain when --fixed-value has no value-pattern\n \ttest_must_fail git config --file=config --fixed-value dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --get dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-all dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-regexp \"dev.*\" &&\n+\ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n \ttest_must_fail git config --file=config --fixed-value --unset dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --unset-all dev.null\n '\n@@ -2591,12 +2629,12 @@ test_expect_success '--fixed-value uses exact string matching' '\n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset-all fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset-all fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n \tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n@@ -2623,18 +2661,27 @@ test_expect_success '--get and --get-all with --fixed-value' '\n \tgit config --file=config --add fixed.test \"$META\" &&\n \n \tgit config --file=config --get fixed.test bogus &&\n+\tgit config get --file=config --value=bogus fixed.test &&\n \ttest_must_fail git config --file=config --get fixed.test \"$META\" &&\n+\ttest_must_fail git config get --file=config --value=\"$META\" fixed.test &&\n \tgit config --file=config --get --fixed-value fixed.test \"$META\" &&\n+\tgit config get --file=config --fixed-value --value=\"$META\" fixed.test &&\n \ttest_must_fail git config --file=config --get --fixed-value fixed.test non-existent &&\n \n \tgit config --file=config --get-all fixed.test bogus &&\n+\tgit config get --all --file=config --value=bogus fixed.test &&\n \ttest_must_fail git config --file=config --get-all fixed.test \"$META\" &&\n+\ttest_must_fail git config get --all --file=config --value=\"$META\" fixed.test &&\n \tgit config --file=config --get-all --fixed-value fixed.test \"$META\" &&\n+\tgit config get --all --file=config --value=\"$META\" --fixed-value fixed.test &&\n \ttest_must_fail git config --file=config --get-all --fixed-value fixed.test non-existent &&\n \n \tgit config --file=config --get-regexp fixed+ bogus &&\n+\tgit config get --regexp --file=config --value=bogus fixed+ &&\n \ttest_must_fail git config --file=config --get-regexp fixed+ \"$META\" &&\n+\ttest_must_fail git config get --regexp --file=config --value=\"$META\" fixed+ &&\n \tgit config --file=config --get-regexp --fixed-value fixed+ \"$META\" &&\n+\tgit config get --regexp --file=config --fixed-value --value=\"$META\" fixed+ &&\n \ttest_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent\n '\n \n-- \n2.45.0\n\n"},{"id":"493983","messageId":"8a623a31b9390377a9efbc8ca97e575568a8da4f.1714730170.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 09/14] builtin/config: introduce \"set\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:24Z","receivedAt":"2024-05-03T09:57:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"set\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt |  38 ++++++++-----\n builtin/config.c             |  63 ++++++++++++++++++++++\n t/t1300-config.sh            | 102 ++++++++++++++++++-----------------\n 3 files changed, 140 insertions(+), 63 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex d0878663db..f57fa01085 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -11,9 +11,7 @@ SYNOPSIS\n [verse]\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' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n-'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>\n-'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n+'git config set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n@@ -27,7 +25,7 @@ You can query/set/replace/unset options with this command. The name is\n actually the section and the key separated by a dot, and the value will be\n escaped.\n \n-Multiple lines can be added to an option by using the `--add` option.\n+Multiple lines can be added to an option by using the `--append` option.\n If you want to update or unset an option which can occur on multiple\n lines, a `value-pattern` (which is an extended regular expression,\n unless the `--fixed-value` option is given) needs to be given.  Only the\n@@ -82,6 +80,13 @@ get::\n \temits all values associated with key. Returns error code 1 if key is\n \tnot present.\n \n+set::\n+\tSet value for one or more config options. By default, this command\n+\trefuses to write multi-valued config options. Passing `--all` will\n+\treplace all multi-valued config options with the new value, whereas\n+\t`--value=` will replace all config options whose values match the given\n+\tpattern.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -90,10 +95,9 @@ OPTIONS\n \tDefault behavior is to replace at most one line. This replaces\n \tall lines matching the key (and optionally the `value-pattern`).\n \n---add::\n+--append::\n \tAdds a new line to the option without altering any existing\n-\tvalues.  This is the same as providing '^$' as the `value-pattern`\n-\tin `--replace-all`.\n+\tvalues. This is the same as providing '--value=^$' in `set`.\n \n --comment <message>::\n \tAppend a comment at the end of new or modified lines.\n@@ -296,6 +300,9 @@ recommended to migrate to the new syntax.\n 'git config <name>'::\n \tReplaced by `git config get <name>`.\n \n+'git config <name> <value> [<value-pattern>]'::\n+\tReplaced by `git config set [--value=<pattern>] <name> <value>`.\n+\n -l::\n --list::\n \tReplaced by `git config list`.\n@@ -315,6 +322,9 @@ recommended to migrate to the new syntax.\n --get-color <name> [<default>]::\n \tReplaced by `git config get --type=color [--default=<default>] <name>`.\n \n+--add <name> <value>::\n+\tReplaced by `git config set --append <name> <value>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n@@ -361,7 +371,7 @@ precedence over values read earlier.  When multiple values are taken then all\n values of a key from all files will be used.\n \n By default, options are only written to the repository specific\n-configuration file. Note that this also affects options like `--replace-all`\n+configuration file. Note that this also affects options like `set`\n and `--unset`. *'git config' will only ever change one file at a time*.\n \n You can limit which configuration sources are read from or written to by\n@@ -497,7 +507,7 @@ Given a .git/config like this:\n you can set the filemode to true with\n \n ------------\n-% git config core.filemode true\n+% git config set core.filemode true\n ------------\n \n The hypothetical proxy command entries actually have a postfix to discern\n@@ -505,7 +515,7 @@ what URL they apply to. Here is how to change the entry for kernel.org\n to \"ssh\".\n \n ------------\n-% git config core.gitproxy '\"ssh\" for kernel.org' 'for kernel.org$'\n+% git config set --value='for kernel.org$' core.gitproxy '\"ssh\" for kernel.org'\n ------------\n \n This makes sure that only the key/value pair for kernel.org is replaced.\n@@ -541,26 +551,26 @@ If you like to live dangerously, you can replace *all* core.gitproxy by a\n new one with\n \n ------------\n-% git config --replace-all core.gitproxy ssh\n+% git config set --all core.gitproxy ssh\n ------------\n \n However, if you really only want to replace the line for the default proxy,\n i.e. the one without a \"for ...\" postfix, do something like this:\n \n ------------\n-% git config core.gitproxy ssh '! for '\n+% git config set --value='! for ' core.gitproxy ssh\n ------------\n \n To actually match only values with an exclamation mark, you have to\n \n ------------\n-% git config section.key value '[!]'\n+% git config set --value='[!]' section.key value\n ------------\n \n To add a new proxy, without altering any of the existing ones, use\n \n ------------\n-% git config --add core.gitproxy '\"proxy-command\" for example.com'\n+% git config set --append core.gitproxy '\"proxy-command\" for example.com'\n ------------\n \n An example to use customized color from the configuration in your\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 9e7ae49c02..87fd12fda2 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -18,6 +18,7 @@\n static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n+\tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tNULL\n };\n \n@@ -31,6 +32,11 @@ static const char *const builtin_config_get_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_set_usage[] = {\n+\tN_(\"git config set [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -849,9 +855,66 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix)\n \treturn get_value(argv[0], value_pattern, flags);\n }\n \n+static int cmd_config_set(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL, *comment_arg = NULL;\n+\tchar *comment = NULL;\n+\tint flags = 0, append = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_TYPE_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter\")),\n+\t\tOPT_BIT(0, \"all\", &flags, N_(\"replace multi-valued config option with new value\"), CONFIG_FLAGS_MULTI_REPLACE),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n+\t\tOPT_BOOL(0, \"append\", &append, N_(\"add a new line without altering any existing values\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\tchar *value;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with --value=<pattern>\"));\n+\tif (append && value_pattern)\n+\t\tdie(_(\"--append cannot be used with --value=<pattern>\"));\n+\tif (append)\n+\t\tvalue_pattern = CONFIG_REGEX_NONE;\n+\n+\tcomment = git_config_prepare_comment_string(comment_arg);\n+\n+\thandle_config_location(prefix);\n+\n+\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n+\n+\tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {\n+\t\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t\t     argv[0], value, value_pattern,\n+\t\t\t\t\t\t\t     comment, flags);\n+\t} else {\n+\t\tret = git_config_set_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t    argv[0], comment, value);\n+\t\tif (ret == CONFIG_NOTHING_SET)\n+\t\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n+\t\t\t\"       Use a regexp, --add or --replace-all to change %s.\"), argv[0]);\n+\t}\n+\n+\tfree(comment);\n+\tfree(value);\n+\treturn ret;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n+\tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 7ce13ed6cd..aa3f179be5 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -20,12 +20,16 @@ legacy)\n \tmode_get=\"\"\n \tmode_get_all=\"--get-all\"\n \tmode_get_regexp=\"--get-regexp\"\n+\tmode_set=\"\"\n+\tmode_replace_all=\"--replace-all\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n \tmode_get=\"get\"\n \tmode_get_all=\"get --all\"\n \tmode_get_regexp=\"get --regexp --all --show-names\"\n+\tmode_set=\"set\"\n+\tmode_replace_all=\"set --all\"\n \t;;\n *)\n \tBUG \"unknown mode $mode\";;\n@@ -132,7 +136,7 @@ cat > expect << EOF\n \tpenguin = little blue\n EOF\n test_expect_success 'initial' '\n-\tgit config section.penguin \"little blue\" &&\n+\tgit config ${mode_set} section.penguin \"little blue\" &&\n \ttest_cmp expect .git/config\n '\n \n@@ -142,7 +146,7 @@ cat > expect << EOF\n \tMovie = BadPhysics\n EOF\n test_expect_success 'mixed case' '\n-\tgit config Section.Movie BadPhysics &&\n+\tgit config ${mode_set} Section.Movie BadPhysics &&\n \ttest_cmp expect .git/config\n '\n \n@@ -154,7 +158,7 @@ cat > expect << EOF\n \tWhatEver = Second\n EOF\n test_expect_success 'similar section' '\n-\tgit config Sections.WhatEver Second &&\n+\tgit config ${mode_set} Sections.WhatEver Second &&\n \ttest_cmp expect .git/config\n '\n \n@@ -167,7 +171,7 @@ cat > expect << EOF\n \tWhatEver = Second\n EOF\n test_expect_success 'uppercase section' '\n-\tgit config SECTION.UPPERCASE true &&\n+\tgit config ${mode_set} SECTION.UPPERCASE true &&\n \ttest_cmp expect .git/config\n '\n \n@@ -194,8 +198,8 @@ EOF\n \n test_expect_success 'append comments' '\n \tgit config --replace-all --comment=\"Pygoscelis papua\" section.penguin gentoo &&\n-\tgit config --comment=\"find fish\" section.disposition peckish &&\n-\tgit config --comment=\"#abc\" section.foo bar &&\n+\tgit config ${mode_set} --comment=\"find fish\" section.disposition peckish &&\n+\tgit config ${mode_set} --comment=\"#abc\" section.foo bar &&\n \n \tgit config --comment=\"and comment\" section.spsp value &&\n \tgit config --comment=\"\t# and comment\" section.htsp value &&\n@@ -204,7 +208,7 @@ test_expect_success 'append comments' '\n '\n \n test_expect_success 'Prohibited LF in comment' '\n-\ttest_must_fail git config --comment=\"a${LF}b\" section.k v\n+\ttest_must_fail git config ${mode_set} --comment=\"a${LF}b\" section.k v\n '\n \n test_expect_success 'non-match result' 'test_cmp expect .git/config'\n@@ -301,14 +305,14 @@ test_expect_success 'multiple unset is correct' '\n cp .git/config2 .git/config\n \n test_expect_success '--replace-all missing value' '\n-\ttest_must_fail git config --replace-all beta.haha &&\n+\ttest_must_fail git config ${mode_replace_all} beta.haha &&\n \ttest_cmp .git/config2 .git/config\n '\n \n rm .git/config2\n \n test_expect_success '--replace-all' '\n-\tgit config --replace-all beta.haha gamma\n+\tgit config ${mode_replace_all} beta.haha gamma\n '\n \n cat > expect << EOF\n@@ -335,7 +339,7 @@ noIndent= sillyValue ; 'nother silly comment\n [nextSection] noNewline = ouch\n EOF\n test_expect_success 'really mean test' '\n-\tgit config beta.haha alpha &&\n+\tgit config ${mode_set} beta.haha alpha &&\n \ttest_cmp expect .git/config\n '\n \n@@ -350,7 +354,7 @@ noIndent= sillyValue ; 'nother silly comment\n \tnonewline = wow\n EOF\n test_expect_success 'really really mean test' '\n-\tgit config nextsection.nonewline wow &&\n+\tgit config ${mode_set} nextsection.nonewline wow &&\n \ttest_cmp expect .git/config\n '\n \n@@ -824,16 +828,16 @@ EOF\n \n test_expect_success 'section ending' '\n \trm -f .git/config &&\n-\tgit config gitcvs.enabled true &&\n-\tgit config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&\n-\tgit config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&\n+\tgit config ${mode_set} gitcvs.enabled true &&\n+\tgit config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&\n+\tgit config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&\n \ttest_cmp expect .git/config\n \n '\n \n test_expect_success numbers '\n-\tgit config kilo.gram 1k &&\n-\tgit config mega.ton 1m &&\n+\tgit config ${mode_set} kilo.gram 1k &&\n+\tgit config ${mode_set} mega.ton 1m &&\n \techo 1024 >expect &&\n \techo 1048576 >>expect &&\n \tgit config --int --get kilo.gram >actual &&\n@@ -842,20 +846,20 @@ test_expect_success numbers '\n '\n \n test_expect_success '--int is at least 64 bits' '\n-\tgit config giga.watts 121g &&\n+\tgit config ${mode_set} giga.watts 121g &&\n \techo  >expect &&\n \ttest_cmp_config 129922760704 --int --get giga.watts\n '\n \n test_expect_success 'invalid unit' '\n-\tgit config aninvalid.unit \"1auto\" &&\n+\tgit config ${mode_set} aninvalid.unit \"1auto\" &&\n \ttest_cmp_config 1auto aninvalid.unit &&\n \ttest_must_fail git config --int --get aninvalid.unit 2>actual &&\n \ttest_grep \"bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit\" actual\n '\n \n test_expect_success 'invalid unit boolean' '\n-\tgit config commit.gpgsign \"1true\" &&\n+\tgit config ${mode_set} commit.gpgsign \"1true\" &&\n \ttest_cmp_config 1true commit.gpgsign &&\n \ttest_must_fail git config --bool --get commit.gpgsign 2>actual &&\n \ttest_grep \"bad boolean config value .1true. for .commit.gpgsign.\" actual\n@@ -885,14 +889,14 @@ EOF\n \n test_expect_success bool '\n \n-\tgit config bool.true1 01 &&\n-\tgit config bool.true2 -1 &&\n-\tgit config bool.true3 YeS &&\n-\tgit config bool.true4 true &&\n-\tgit config bool.false1 000 &&\n-\tgit config bool.false2 \"\" &&\n-\tgit config bool.false3 nO &&\n-\tgit config bool.false4 FALSE &&\n+\tgit config ${mode_set} bool.true1 01 &&\n+\tgit config ${mode_set} bool.true2 -1 &&\n+\tgit config ${mode_set} bool.true3 YeS &&\n+\tgit config ${mode_set} bool.true4 true &&\n+\tgit config ${mode_set} bool.false1 000 &&\n+\tgit config ${mode_set} bool.false2 \"\" &&\n+\tgit config ${mode_set} bool.false3 nO &&\n+\tgit config ${mode_set} bool.false4 FALSE &&\n \trm -f result &&\n \tfor i in 1 2 3 4\n \tdo\n@@ -903,7 +907,7 @@ test_expect_success bool '\n \n test_expect_success 'invalid bool (--get)' '\n \n-\tgit config bool.nobool foobar &&\n+\tgit config ${mode_set} bool.nobool foobar &&\n \ttest_must_fail git config --bool --get bool.nobool'\n \n test_expect_success 'invalid bool (set)' '\n@@ -1092,7 +1096,7 @@ test_expect_success 'get --expiry-date' '\n \n test_expect_success 'get --type=color' '\n \trm .git/config &&\n-\tgit config foo.color \"red\" &&\n+\tgit config ${mode_set} foo.color \"red\" &&\n \tgit config --get --type=color foo.color >actual.raw &&\n \ttest_decode_color <actual.raw >actual &&\n \techo \"<RED>\" >expect &&\n@@ -1129,10 +1133,10 @@ cat > expect << EOF\n EOF\n test_expect_success 'quoting' '\n \trm -f .git/config &&\n-\tgit config quote.leading \" test\" &&\n-\tgit config quote.ending \"test \" &&\n-\tgit config quote.semicolon \"test;test\" &&\n-\tgit config quote.hash \"test#test\" &&\n+\tgit config ${mode_set} quote.leading \" test\" &&\n+\tgit config ${mode_set} quote.ending \"test \" &&\n+\tgit config ${mode_set} quote.semicolon \"test;test\" &&\n+\tgit config ${mode_set} quote.hash \"test#test\" &&\n \ttest_cmp expect .git/config\n '\n \n@@ -1140,7 +1144,7 @@ test_expect_success 'key with newline' '\n \ttest_must_fail git config ${mode_get} \"key.with\n newline\" 123'\n \n-test_expect_success 'value with newline' 'git config key.sub value.with\\\\\\\n+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\\\\\n newline'\n \n cat > .git/config <<\\EOF\n@@ -1199,21 +1203,21 @@ test_expect_success '--null --get-regexp' '\n \n test_expect_success 'inner whitespace kept verbatim, spaces only' '\n \techo \"foo   bar\" >expect &&\n-\tgit config section.val \"foo   bar\" &&\n+\tgit config ${mode_set} section.val \"foo   bar\" &&\n \tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '\n \techo \"fooQQbar\" | q_to_tab >expect &&\n-\tgit config section.val \"$(cat expect)\" &&\n+\tgit config ${mode_set} section.val \"$(cat expect)\" &&\n \tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '\n \techo \"foo Q  bar\" | q_to_tab >expect &&\n-\tgit config section.val \"$(cat expect)\" &&\n+\tgit config ${mode_set} section.val \"$(cat expect)\" &&\n \tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n@@ -1252,12 +1256,12 @@ test_expect_success 'check split_cmdline return' '\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tgit config alias.split-cmdline-fix \"echo \\\"\" &&\n+\t\tgit config ${mode_set} alias.split-cmdline-fix \"echo \\\"\" &&\n \t\ttest_must_fail git split-cmdline-fix &&\n \t\techo foo >foo &&\n \t\tgit add foo &&\n \t\tgit commit -m \"initial commit\" &&\n-\t\tgit config branch.main.mergeoptions \"echo \\\"\" &&\n+\t\tgit config ${mode_set} branch.main.mergeoptions \"echo \\\"\" &&\n \t\ttest_must_fail git merge main\n \t)\n '\n@@ -1295,12 +1299,12 @@ test_expect_success 'key sanity-checking' '\n \ttest_must_fail git config ${mode_get} foo.1bar &&\n \ttest_must_fail git config ${mode_get} foo.\"ba\n \t\t\t\tz\".bar &&\n-\ttest_must_fail git config . false &&\n-\ttest_must_fail git config .foo false &&\n-\ttest_must_fail git config foo. false &&\n-\ttest_must_fail git config .foo. false &&\n-\tgit config foo.bar true &&\n-\tgit config foo.\"ba =z\".bar false\n+\ttest_must_fail git config ${mode_set} . false &&\n+\ttest_must_fail git config ${mode_set} .foo false &&\n+\ttest_must_fail git config ${mode_set} foo. false &&\n+\ttest_must_fail git config ${mode_set} .foo. false &&\n+\tgit config ${mode_set} foo.bar true &&\n+\tgit config ${mode_set} foo.\"ba =z\".bar false\n '\n \n test_expect_success 'git -c works with aliases of builtins' '\n@@ -2523,7 +2527,7 @@ test_expect_success '--replace-all does not invent newlines' '\n \t[abc]\n \tQkey = b\n \tEOF\n-\tgit config --replace-all abc.key b &&\n+\tgit config ${mode_replace_all} abc.key b &&\n \ttest_cmp expect .git/config\n '\n \n@@ -2595,8 +2599,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n \n \t# These modes complain when --fixed-value has no value-pattern\n-\ttest_must_fail git config --file=config --fixed-value dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&\n+\ttest_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&\n+\ttest_must_fail git config ${mode_replace_all} --file=config --fixed-value dev.null bogus &&\n \ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n@@ -2637,7 +2641,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n-\tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n+\tgit config --file=config fixed.test bogus \"$META\" &&\n \tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n-- \n2.45.0\n\n"},{"id":"493984","messageId":"e25e5b69cd60f5e6f6779158ddfc67cda51da52e.1714730170.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 10/14] builtin/config: introduce \"unset\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:28Z","receivedAt":"2024-05-03T09:57:34Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"unset\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 27 +++++++++++++---------\n builtin/config.c             | 39 ++++++++++++++++++++++++++++++++\n t/t1300-config.sh            | 44 ++++++++++++++++++++++++------------\n 3 files changed, 84 insertions(+), 26 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex f57fa01085..5575fe55ea 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -11,9 +11,8 @@ SYNOPSIS\n [verse]\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>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n-'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n-'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\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' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n@@ -87,6 +86,12 @@ set::\n \t`--value=` will replace all config options whose values match the given\n \tpattern.\n \n+unset::\n+\tUnset value for one or more config options. By default, this command\n+\trefuses to unset multi-valued keys. Passing `--all` will unset all\n+\tmulti-valued config options, whereas `--value` will unset all config\n+\toptions whose values match the given pattern.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -190,12 +195,6 @@ See also <<FILES>>.\n --rename-section::\n \tRename the given section to a new name.\n \n---unset::\n-\tRemove the line matching the key from config file.\n-\n---unset-all::\n-\tRemove all lines matching the key from config file.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -325,6 +324,12 @@ recommended to migrate to the new syntax.\n --add <name> <value>::\n \tReplaced by `git config set --append <name> <value>`.\n \n+--unset <name> [<value-pattern>]::\n+\tReplaced by `git config unset [--value=<pattern>] <name>`.\n+\n+--unset-all <name> [<value-pattern>]::\n+\tReplaced by `git config unset [--value=<pattern>] --all <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n@@ -372,7 +377,7 @@ values of a key from all files will be used.\n \n By default, options are only written to the repository specific\n configuration file. Note that this also affects options like `set`\n-and `--unset`. *'git config' will only ever change one file at a time*.\n+and `unset`. *'git config' will only ever change one file at a time*.\n \n You can limit which configuration sources are read from or written to by\n specifying the path of a file with the `--file` option, or by specifying a\n@@ -523,7 +528,7 @@ This makes sure that only the key/value pair for kernel.org is replaced.\n To delete the entry for renames, do\n \n ------------\n-% git config --unset diff.renames\n+% git config unset diff.renames\n ------------\n \n If you want to delete an entry for a multivar (like core.gitproxy above),\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 87fd12fda2..2578d4c930 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -19,6 +19,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tNULL\n };\n \n@@ -37,6 +38,11 @@ static const char *const builtin_config_set_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_unset_usage[] = {\n+\tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -911,10 +917,43 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix)\n \treturn ret;\n }\n \n+static int cmd_config_unset(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL;\n+\tint flags = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter\")),\n+\t\tOPT_BIT(0, \"all\", &flags, N_(\"replace multi-valued config option with new value\"), CONFIG_FLAGS_MULTI_REPLACE),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_unset_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 1);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with 'value-pattern'\"));\n+\n+\thandle_config_location(prefix);\n+\n+\tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)\n+\t\treturn git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t\t      argv[0], NULL, value_pattern,\n+\t\t\t\t\t\t\t      NULL, flags);\n+\telse\n+\t\treturn git_config_set_in_file_gently(given_config_source.file, argv[0],\n+\t\t\t\t\t\t     NULL, NULL);\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n+\tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex aa3f179be5..2ab58bbd95 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -22,6 +22,8 @@ legacy)\n \tmode_get_regexp=\"--get-regexp\"\n \tmode_set=\"\"\n \tmode_replace_all=\"--replace-all\"\n+\tmode_unset=\"--unset\"\n+\tmode_unset_all=\"--unset-all\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n@@ -30,6 +32,8 @@ subcommands)\n \tmode_get_regexp=\"get --regexp --all --show-names\"\n \tmode_set=\"set\"\n \tmode_replace_all=\"set --all\"\n+\tmode_unset=\"unset\"\n+\tmode_unset_all=\"unset --all\"\n \t;;\n *)\n \tBUG \"unknown mode $mode\";;\n@@ -259,7 +263,7 @@ foo = bar\n EOF\n \n test_expect_success 'unset with cont. lines' '\n-\tgit config --unset beta.baz\n+\tgit config ${mode_unset} beta.baz\n '\n \n cat > expect <<\\EOF\n@@ -286,7 +290,7 @@ EOF\n cp .git/config .git/config2\n \n test_expect_success 'multiple unset' '\n-\tgit config --unset-all beta.haha\n+\tgit config ${mode_unset_all} beta.haha\n '\n \n cat > expect << EOF\n@@ -372,7 +376,7 @@ noIndent= sillyValue ; 'nother silly comment\n \tnonewline = wow\n EOF\n test_expect_success 'unset' '\n-\tgit config --unset beta.haha &&\n+\tgit config ${mode_unset} beta.haha &&\n \ttest_cmp expect .git/config\n '\n \n@@ -428,11 +432,11 @@ test_expect_success 'multivar replace' '\n '\n \n test_expect_success 'ambiguous unset' '\n-\ttest_must_fail git config --unset nextsection.nonewline\n+\ttest_must_fail git config ${mode_unset} nextsection.nonewline\n '\n \n test_expect_success 'invalid unset' '\n-\ttest_must_fail git config --unset somesection.nonewline\n+\ttest_must_fail git config ${mode_unset} somesection.nonewline\n '\n \n cat > expect << EOF\n@@ -446,7 +450,12 @@ noIndent= sillyValue ; 'nother silly comment\n EOF\n \n test_expect_success 'multivar unset' '\n-\tgit config --unset nextsection.nonewline \"wow3$\" &&\n+\tcase \"$mode\" in\n+\tlegacy)\n+\t\tgit config --unset nextsection.nonewline \"wow3$\";;\n+\tsubcommands)\n+\t\tgit config unset --value=\"wow3$\" nextsection.nonewline;;\n+\tesac &&\n \ttest_cmp expect .git/config\n '\n \n@@ -2013,7 +2022,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t# please be careful when you update the above variable\n \tEOF\n \n-\tgit config --unset section.key &&\n+\tgit config ${mode_unset} section.key &&\n \ttest_cmp expect .git/config &&\n \n \tcat >.git/config <<-\\EOF &&\n@@ -2026,7 +2035,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[next-section]\n \tEOF\n \n-\tgit config --unset section.key &&\n+\tgit config ${mode_unset} section.key &&\n \ttest_cmp expect .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -2036,7 +2045,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[two]\n \tkey = true\n \tEOF\n-\tgit config --unset two.key &&\n+\tgit config ${mode_unset} two.key &&\n \t! grep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -2046,7 +2055,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[one]\n \tkey = true\n \tEOF\n-\tgit config --unset-all one.key &&\n+\tgit config ${mode_unset_all} one.key &&\n \ttest_line_count = 0 .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -2056,7 +2065,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[two]\n \tQkey = true\n \tEOF\n-\tgit config --unset two.key &&\n+\tgit config ${mode_unset} two.key &&\n \tgrep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -2068,7 +2077,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[TWO \"subsection\"]\n \t[one]\n \tEOF\n-\tgit config --unset two.subsection.key &&\n+\tgit config ${mode_unset} two.subsection.key &&\n \ttest \"not [two subsection]\" = \"$(git config ${mode_get} one.key)\" &&\n \ttest_line_count = 3 .git/config\n '\n@@ -2080,7 +2089,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' '\n \tkey = value2\n \tEOF\n \n-\tgit config --unset-all section.key &&\n+\tgit config ${mode_unset_all} section.key &&\n \ttest_line_count = 0 .git/config\n '\n \n@@ -2604,8 +2613,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n-\ttest_must_fail git config --file=config --fixed-value --unset dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --unset-all dev.null\n+\ttest_must_fail git config ${mode_unset} --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_unset_all} --file=config --fixed-value dev.null\n '\n \n test_expect_success '--fixed-value uses exact string matching' '\n@@ -2635,6 +2644,11 @@ test_expect_success '--fixed-value uses exact string matching' '\n \tgit config --file=config --fixed-value --unset fixed.test \"$META\" &&\n \ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n+\tcp initial config &&\n+\ttest_must_fail git config unset --file=config --value=\"$META\" fixed.test &&\n+\tgit config unset --file=config --fixed-value --value=\"$META\" fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n+\n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset-all fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset-all fixed.test \"$META\" &&\n-- \n2.45.0\n\n"},{"id":"493985","messageId":"f24008d3563a07445ebf8d63ec3a86f4c0983010.1714730170.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 11/14] builtin/config: introduce \"rename-section\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:33Z","receivedAt":"2024-05-03T09:57:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"rename-section\" subcommand to git-config(1). Please\nrefer to preceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 11 +++++++----\n builtin/config.c             | 32 ++++++++++++++++++++++++++++++++\n t/t1300-config.sh            | 22 +++++++++++-----------\n 3 files changed, 50 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 5575fe55ea..ede86ad085 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -13,7 +13,7 @@ SYNOPSIS\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' [<file-option>] --rename-section <old-name> <new-name>\n+'git config rename-section' [<file-option>] <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n@@ -92,6 +92,9 @@ unset::\n \tmulti-valued config options, whereas `--value` will unset all config\n \toptions whose values match the given pattern.\n \n+rename-section::\n+\tRename the given section to a new name.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -192,9 +195,6 @@ See also <<FILES>>.\n --remove-section::\n \tRemove the given section from the configuration file.\n \n---rename-section::\n-\tRename the given section to a new name.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -330,6 +330,9 @@ recommended to migrate to the new syntax.\n --unset-all <name> [<value-pattern>]::\n \tReplaced by `git config unset [--value=<pattern>] --all <name>`.\n \n+--rename-section <old-name> <new-name>::\n+\tReplaced by `git config rename-section <old-name> <new-name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 2578d4c930..a60dca9b2b 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -20,6 +20,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tNULL\n };\n \n@@ -43,6 +44,11 @@ static const char *const builtin_config_unset_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_rename_section_usage[] = {\n+\tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -949,11 +955,37 @@ static int cmd_config_unset(int argc, const char **argv, const char *prefix)\n \t\t\t\t\t\t     NULL, NULL);\n }\n \n+static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_rename_section_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\n+\thandle_config_location(prefix);\n+\n+\tret = git_config_rename_section_in_file(given_config_source.file,\n+\t\t\t\t\t\targv[0], argv[1]);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!ret)\n+\t\tdie(_(\"no such section: %s\"), argv[0]);\n+\n+\treturn 0;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n+\tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 2ab58bbd95..5d7b08fa4c 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -699,7 +699,7 @@ weird\n EOF\n \n test_expect_success 'rename section' '\n-\tgit config --rename-section branch.eins branch.zwei\n+\tgit config ${mode_prefix}rename-section branch.eins branch.zwei\n '\n \n cat > expect << EOF\n@@ -718,7 +718,7 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'rename non-existing section' '\n-\ttest_must_fail git config --rename-section \\\n+\ttest_must_fail git config ${mode_prefix}rename-section \\\n \t\tbranch.\"world domination\" branch.drei\n '\n \n@@ -727,7 +727,7 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'rename another section' '\n-\tgit config --rename-section branch.\"1 234 blabl/a\" branch.drei\n+\tgit config ${mode_prefix}rename-section branch.\"1 234 blabl/a\" branch.drei\n '\n \n cat > expect << EOF\n@@ -750,7 +750,7 @@ cat >> .git/config << EOF\n EOF\n \n test_expect_success 'rename a section with a var on the same line' '\n-\tgit config --rename-section branch.vier branch.zwei\n+\tgit config ${mode_prefix}rename-section branch.vier branch.zwei\n '\n \n cat > expect << EOF\n@@ -771,11 +771,11 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'renaming empty section name is rejected' '\n-\ttest_must_fail git config --rename-section branch.zwei \"\"\n+\ttest_must_fail git config ${mode_prefix}rename-section branch.zwei \"\"\n '\n \n test_expect_success 'renaming to bogus section is rejected' '\n-\ttest_must_fail git config --rename-section branch.zwei \"bogus name\"\n+\ttest_must_fail git config ${mode_prefix}rename-section branch.zwei \"bogus name\"\n '\n \n test_expect_success 'renaming a section with a long line' '\n@@ -784,7 +784,7 @@ test_expect_success 'renaming a section with a long line' '\n \t\tprintf \"  c = d %1024s [a] e = f\\\\n\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\tgit config -f y --rename-section a xyz &&\n+\tgit config ${mode_prefix}rename-section -f y a xyz &&\n \ttest_must_fail git config -f y b.e\n '\n \n@@ -794,7 +794,7 @@ test_expect_success 'renaming an embedded section with a long line' '\n \t\tprintf \"  c = d %1024s [a] [foo] e = f\\\\n\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\tgit config -f y --rename-section a xyz &&\n+\tgit config ${mode_prefix}rename-section -f y a xyz &&\n \ttest_must_fail git config -f y foo.e\n '\n \n@@ -804,7 +804,7 @@ test_expect_success 'renaming a section with an overly-long line' '\n \t\tprintf \"  c = d %525000s e\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\ttest_must_fail git config -f y --rename-section a xyz 2>err &&\n+\ttest_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&\n \tgrep \"refusing to work with overly long line in .y. on line 2\" err\n '\n \n@@ -2112,7 +2112,7 @@ test_expect_success POSIXPERM,PERL 'preserves existing permissions' '\n \tgit config imap.pass Hunter2 &&\n \tperl -e \\\n \t  \"die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600\" &&\n-\tgit config --rename-section imap pop &&\n+\tgit config ${mode_prefix}rename-section imap pop &&\n \tperl -e \\\n \t  \"die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600\"\n '\n@@ -2601,7 +2601,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --add dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --rename-section dev null &&\n+\ttest_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&\n \ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n \ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n-- \n2.45.0\n\n"},{"id":"493986","messageId":"fc2ddd3201c01939a52293a80c134cae96adee23.1714730170.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 12/14] builtin/config: introduce \"remove-section\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:38Z","receivedAt":"2024-05-03T09:57:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"remove-section\" subcommand to git-config(1). Please\nrefer to preceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 11 +++++++----\n builtin/config.c             | 32 ++++++++++++++++++++++++++++++++\n t/t1300-config.sh            |  4 ++--\n 3 files changed, 41 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex ede86ad085..ef46520c15 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -14,7 +14,7 @@ SYNOPSIS\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' [<file-option>] --remove-section <name>\n+'git config remove-section' [<file-option>] <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n \n@@ -95,6 +95,9 @@ unset::\n rename-section::\n \tRename the given section to a new name.\n \n+remove-section::\n+\tRemove the given section from the configuration file.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -192,9 +195,6 @@ See also <<FILES>>.\n \tsection in linkgit:gitrevisions[7] for a more complete list of\n \tways to spell blob names.\n \n---remove-section::\n-\tRemove the given section from the configuration file.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -333,6 +333,9 @@ recommended to migrate to the new syntax.\n --rename-section <old-name> <new-name>::\n \tReplaced by `git config rename-section <old-name> <new-name>`.\n \n+--remove-section <name>::\n+\tReplaced by `git config remove-section <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex a60dca9b2b..cea03fb517 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -21,6 +21,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n+\tN_(\"git config remove-section [<file-option>] <name>\"),\n \tNULL\n };\n \n@@ -49,6 +50,11 @@ static const char *const builtin_config_rename_section_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_remove_section_usage[] = {\n+\tN_(\"git config remove-section [<file-option>] <name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -980,12 +986,38 @@ static int cmd_config_rename_section(int argc, const char **argv, const char *pr\n \treturn 0;\n }\n \n+static int cmd_config_remove_section(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_remove_section_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 1);\n+\n+\thandle_config_location(prefix);\n+\n+\tret = git_config_rename_section_in_file(given_config_source.file,\n+\t\t\t\t\t\targv[0], NULL);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!ret)\n+\t\tdie(_(\"no such section: %s\"), argv[0]);\n+\n+\treturn 0;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n+\tOPT_SUBCOMMAND(\"remove-section\", &subcommand, cmd_config_remove_section),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 5d7b08fa4c..9d71ec1260 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -813,7 +813,7 @@ cat >> .git/config << EOF\n EOF\n \n test_expect_success 'remove section' '\n-\tgit config --remove-section branch.zwei\n+\tgit config ${mode_prefix}remove-section branch.zwei\n '\n \n cat > expect << EOF\n@@ -2602,7 +2602,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&\n-\ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n+\ttest_must_fail git config ${mode_prefix}remove-section --file=config --fixed-value dev &&\n \ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n-- \n2.45.0\n\n"},{"id":"493987","messageId":"4c2d817eff2ceb29725ef101269216dcd632f489.1714730170.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 13/14] builtin/config: introduce \"edit\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:43Z","receivedAt":"2024-05-03T09:57:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"edit\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 17 ++++----\n builtin/config.c             | 81 ++++++++++++++++++++++++------------\n t/t1300-config.sh            |  6 +--\n 3 files changed, 68 insertions(+), 36 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex ef46520c15..65c645d461 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -15,8 +15,8 @@ SYNOPSIS\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>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n-'git config' [<file-option>] -e | --edit\n \n DESCRIPTION\n -----------\n@@ -98,6 +98,11 @@ rename-section::\n remove-section::\n \tRemove the given section from the configuration file.\n \n+edit::\n+\tOpens an editor to modify the specified config file; either\n+\t`--system`, `--global`, `--local` (default), `--worktree`, or\n+\t`--file <config-file>`.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -274,12 +279,6 @@ Valid `<type>`'s include:\n \tWhen the color setting for `name` is undefined, the command uses\n \t`color.ui` as fallback.\n \n--e::\n---edit::\n-\tOpens an editor to modify the specified config file; either\n-\t`--system`, `--global`, `--local` (default), `--worktree`, or\n-\t`--file <config-file>`.\n-\n --[no-]includes::\n \tRespect `include.*` directives in config files when looking up\n \tvalues. Defaults to `off` when a specific file is given (e.g.,\n@@ -336,6 +335,10 @@ recommended to migrate to the new syntax.\n --remove-section <name>::\n \tReplaced by `git config remove-section <name>`.\n \n+-e::\n+--edit::\n+\tReplaced by `git config edit`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex cea03fb517..8f7fa8f31a 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -22,6 +22,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tN_(\"git config remove-section [<file-option>] <name>\"),\n+\tN_(\"git config edit [<file-option>]\"),\n \tNULL\n };\n \n@@ -55,6 +56,11 @@ static const char *const builtin_config_remove_section_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_edit_usage[] = {\n+\tN_(\"git config edit [<file-option>]\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -1011,6 +1017,53 @@ static int cmd_config_remove_section(int argc, const char **argv, const char *pr\n \treturn 0;\n }\n \n+static int show_editor(void)\n+{\n+\tchar *config_file;\n+\n+\tif (!given_config_source.file && !startup_info->have_repository)\n+\t\tdie(_(\"not in a git directory\"));\n+\tif (given_config_source.use_stdin)\n+\t\tdie(_(\"editing stdin is not supported\"));\n+\tif (given_config_source.blob)\n+\t\tdie(_(\"editing blobs is not supported\"));\n+\tgit_config(git_default_config, NULL);\n+\tconfig_file = given_config_source.file ?\n+\t\t\txstrdup(given_config_source.file) :\n+\t\t\tgit_pathdup(\"config\");\n+\tif (use_global_config) {\n+\t\tint fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);\n+\t\tif (fd >= 0) {\n+\t\t\tchar *content = default_user_config();\n+\t\t\twrite_str_in_full(fd, content);\n+\t\t\tfree(content);\n+\t\t\tclose(fd);\n+\t\t}\n+\t\telse if (errno != EEXIST)\n+\t\t\tdie_errno(_(\"cannot create configuration file %s\"), config_file);\n+\t}\n+\tlaunch_editor(config_file, NULL, NULL);\n+\tfree(config_file);\n+\n+\treturn 0;\n+}\n+\n+static int cmd_config_edit(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_edit_usage, 0);\n+\tcheck_write();\n+\tcheck_argc(argc, 0, 0);\n+\n+\thandle_config_location(prefix);\n+\n+\treturn show_editor();\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n@@ -1018,6 +1071,7 @@ static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n \tOPT_SUBCOMMAND(\"remove-section\", &subcommand, cmd_config_remove_section),\n+\tOPT_SUBCOMMAND(\"edit\", &subcommand, cmd_config_edit),\n \tOPT_END(),\n };\n \n@@ -1144,32 +1198,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\t}\n \t}\n \telse if (actions == ACTION_EDIT) {\n-\t\tchar *config_file;\n-\n-\t\tcheck_argc(argc, 0, 0);\n-\t\tif (!given_config_source.file && !startup_info->have_repository)\n-\t\t\tdie(_(\"not in a git directory\"));\n-\t\tif (given_config_source.use_stdin)\n-\t\t\tdie(_(\"editing stdin is not supported\"));\n-\t\tif (given_config_source.blob)\n-\t\t\tdie(_(\"editing blobs is not supported\"));\n-\t\tgit_config(git_default_config, NULL);\n-\t\tconfig_file = given_config_source.file ?\n-\t\t\t\txstrdup(given_config_source.file) :\n-\t\t\t\tgit_pathdup(\"config\");\n-\t\tif (use_global_config) {\n-\t\t\tint fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);\n-\t\t\tif (fd >= 0) {\n-\t\t\t\tchar *content = default_user_config();\n-\t\t\t\twrite_str_in_full(fd, content);\n-\t\t\t\tfree(content);\n-\t\t\t\tclose(fd);\n-\t\t\t}\n-\t\t\telse if (errno != EEXIST)\n-\t\t\t\tdie_errno(_(\"cannot create configuration file %s\"), config_file);\n-\t\t}\n-\t\tlaunch_editor(config_file, NULL, NULL);\n-\t\tfree(config_file);\n+\t\tret = show_editor();\n \t}\n \telse if (actions == ACTION_SET) {\n \t\tcheck_write();\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 9d71ec1260..f3c4d28e06 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -666,7 +666,7 @@ test_expect_success 'setting a value in stdin is an error' '\n '\n \n test_expect_success 'editing stdin is an error' '\n-\ttest_must_fail git config --file - --edit\n+\ttest_must_fail git config ${mode_prefix}edit --file -\n '\n \n test_expect_success 'refer config from subdirectory' '\n@@ -1768,7 +1768,7 @@ test_expect_success 'command line overrides environment config' '\n test_expect_success 'git config --edit works' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n-\tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n+\tGIT_EDITOR=\"echo [test]value=yes >\" git config ${mode_prefix}edit -f tmp &&\n \tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n@@ -1777,7 +1777,7 @@ test_expect_success 'git config --edit respects core.editor' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n \ttest_config core.editor \"echo [test]value=yes >\" &&\n-\tgit config -f tmp --edit &&\n+\tgit config ${mode_prefix}edit -f tmp &&\n \tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n-- \n2.45.0\n\n"},{"id":"493988","messageId":"4c351b12b825ecea04f1ccf85958dd0e2f718ebf.1714730170.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"[PATCH v4 14/14] builtin/config: display subcommand help","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-03T09:57:48Z","receivedAt":"2024-05-03T09:57:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Until now, `git config -h` would have printed help for the old-style\nsyntax. Now that all modes have proper subcommands though it is\npreferable to instead display the subcommand help.\n\nDrop the `NO_INTERNAL_HELP` flag to do so. While at it, drop the help\nmismatch in t0450 and add the `--get-colorbool` option to the usage such\nthat git-config(1)'s synopsis and `git config -h` match.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c            | 5 +++--\n t/t0450/txt-help-mismatches | 1 -\n 2 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 8f7fa8f31a..80aa9d8a66 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -23,6 +23,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tN_(\"git config remove-section [<file-option>] <name>\"),\n \tN_(\"git config edit [<file-option>]\"),\n+\tN_(\"git config [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\"),\n \tNULL\n };\n \n@@ -1093,10 +1094,10 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t * us when parsing the legacy-style modes that don't use subcommands.\n \t */\n \targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n \tif (subcommand) {\n \t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n-\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);\n \t\treturn subcommand(argc, argv, prefix);\n \t}\n \ndiff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches\nindex a0777acd66..28003f18c9 100644\n--- a/t/t0450/txt-help-mismatches\n+++ b/t/t0450/txt-help-mismatches\n@@ -10,7 +10,6 @@ checkout\n checkout-index\n clone\n column\n-config\n credential\n credential-cache\n credential-store\n-- \n2.45.0\n\n"},{"id":"493989","messageId":"6d8af89c-614d-43a1-a8b5-83a0fc17ad94@app.fastmail.com","threadId":"61061","inReplyTo":"3aa26d5bff21e0b93736b1502fa90694f6b2cedc.1714730169.git.ps@pks.im","subject":"Re: [PATCH v4 01/14] config: clarify memory ownership when preparing comment strings","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-05-03T10:13:17Z","receivedAt":"2024-05-03T10:13:44Z","isPatch":true,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"On Fri, May 3, 2024, at 11:56, Patrick Steinhardt wrote:\n> The ownership of memory returned when preparing a comment string is\n> quite intricate: when the returned value is different than the passed\n> value, then the caller is responsible to free the memory. This is quite\n> subtle, and it's even easier to miss because the returned value is in\n> fact a `const char *`.\n>\n> Adapt the function to always return either `NULL` or a newly allocated\n> string. The function is called at most once per git-config(1), so it's\n> not like this micro-optimization really matters. Thus, callers are now\n> always responsible for freeing the value.\n> ---\n\nMissing a signoff on this one. The only one in the series.\n\n-- \nKristoffer Haugsbakk\n"},{"id":"493990","messageId":"CAOLa=ZR02Jf3-4zT9gMzVmzS1JNR1MCLDHCV+utR3B-N_acK=w@mail.gmail.com","threadId":"61061","inReplyTo":"ddcd8031d7e399b126344d82d3373a2b2ed7fb8f.1714730169.git.ps@pks.im","subject":"Re: [PATCH v4 03/14] builtin/config: move \"fixed-value\" option to correct group","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-03T12:28:38Z","receivedAt":"2024-05-03T12:28:39Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The `--fixed-value` option can be used to alter how the value-pattern\n> parameter is interpreted for the various submodes of git-config(1). But\n> while it is an option, it is currently listed as part of the submodes\n> group the command, which is wrong.\n>\n\nIsn't it currently listed as part of the 'Action' group? Also I'm not\nsure if the last sentence needs to be corrected here.\n\n>\n> Move the option to the \"Other\" group, which hosts the various options\n> known to git-config(1).\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/config.c | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n>\n> diff --git a/builtin/config.c b/builtin/config.c\n> index 59ae5996eb..054019b70c 100644\n> --- a/builtin/config.c\n> +++ b/builtin/config.c\n> @@ -643,7 +643,6 @@ static struct option builtin_config_options[] = {\n>  \tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n>  \tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n>  \tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n> -\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n>  \tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n>  \tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n>  \tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n> @@ -663,6 +662,7 @@ static struct option builtin_config_options[] = {\n>  \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n>  \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n>  \tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n> +\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n>  \tOPT_END(),\n>  };\n>\n> --\n> 2.45.0\n"},{"id":"493999","messageId":"CAOLa=ZSNbZPByO9QyeAGaR1pWXMB7ge_GF7M5fydxP-cse-X3g@mail.gmail.com","threadId":"61061","inReplyTo":"b3f3c3ba6ab33d269f7a707eae1456c87cdfeddb.1714730170.git.ps@pks.im","subject":"Re: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-03T13:08:57Z","receivedAt":"2024-05-03T13:08:59Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> While git-config(1) has several modes, those modes are not exposed with\n> subcommands but instead by specifying e.g. `--unset` or `--list`. This\n\ns/specifying/specifying flags/ perhaps?\n\n> user interface is not really in line with how our more modern commands\n> work, where it is a lot more customary to say e.g. `git remote list`.\n\nTangent: I totally agree with the patch, but it would be nice to have a\n'DesigningCommands' document which would highlight UX do's and don'ts.\nIt would be nice to add that as reference in discussions.\n\n> Furthermore, to add to the confusion, git-config(1) also allows the user\n> to request modes implicitly by just specifying the correct number of\n> arguments. Thus, `git config foo.bar` will retrieve the value of\n> \"foo.bar\" while `git config foo.bar baz` will set it to \"baz\".\n>\n> Overall, this makes for a confusing interface that could really use a\n> makeover. It hurts discoverability of what you can do with git-config(1)\n> and is comparatively easy to get wrong. Converting the command to have\n> subcommands instead would go a long way to help address these issues.\n>\n> One concern in this context is backwards compatibility. Luckily, we can\n> introduce subcommands without breaking backwards compatibility at all.\n> This is because all the implicit modes of git-config(1) require that the\n> first argument is a properly formatted config key. And as config keys\n> _must_ have a dot in their name, any value without a dot would have been\n> discarded by git-config(1) previous to this change. Thus, given that\n> none of the subcommands do have a dot, they are unambiguous.\n>\n> Introduce the first such new subcommand, which is \"git config list\". To\n> retain backwards compatibility we only conditionally use subcommands and\n> will fall back to the old syntax in case no subcommand was detected.\n> This should help to transition to the new-style syntax until we\n> eventually deprecate and remove the old-style syntax.\n>\n> Note that the way we handle this we're duplicating some functionality\n> across old and new syntax. While this isn't pretty, it helps us to\n> ensure that there really is no change in behaviour for the old syntax.\n>\n> Amend tests such that we run them both with old and new style syntax.\n> As tests are now run twice, state from the first run may be still be\n> around in the second run and thus cause tests to fail. Add cleanup logic\n> as required to fix such tests.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-config.txt |  26 ++++++---\n>  builtin/config.c             |  90 ++++++++++++++++++++++++----\n>  t/t1300-config.sh            | 110 +++++++++++++++++++++--------------\n>  3 files changed, 162 insertions(+), 64 deletions(-)\n>\n> diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\n> index ac61113fcc..c83c97cb7e 100644\n> --- a/Documentation/git-config.txt\n> +++ b/Documentation/git-config.txt\n> @@ -9,6 +9,7 @@ git-config - Get and set repository or global options\n>  SYNOPSIS\n>  --------\n>  [verse]\n> +'git config list' [<file-option>] [<display-option>] [--includes]\n>  'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n>  'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>\n>  'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n> @@ -20,7 +21,6 @@ SYNOPSIS\n>  'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n>  'git config' [<file-option>] --rename-section <old-name> <new-name>\n>  'git config' [<file-option>] --remove-section <name>\n> -'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list\n>  'git config' [<file-option>] --get-color <name> [<default>]\n>  'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n>  'git config' [<file-option>] -e | --edit\n> @@ -74,6 +74,12 @@ On success, the command returns the exit code 0.\n>  A list of all available configuration variables can be obtained using the\n>  `git help --config` command.\n>\n> +COMMANDS\n> +--------\n> +\n> +list::\n> +\tList all variables set in config file, along with their values.\n> +\n>  [[OPTIONS]]\n>  OPTIONS\n>  -------\n> @@ -190,10 +196,6 @@ See also <<FILES>>.\n>  --unset-all::\n>  \tRemove all lines matching the key from config file.\n>\n> --l::\n> ---list::\n> -\tList all variables set in config file, along with their values.\n> -\n>  --fixed-value::\n>  \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n>  \tan exact string instead of a regular expression. This will restrict\n> @@ -248,7 +250,7 @@ Valid `<type>`'s include:\n>  \tcontain line breaks.\n>\n>  --name-only::\n> -\tOutput only the names of config variables for `--list` or\n> +\tOutput only the names of config variables for `list` or\n>  \t`--get-regexp`.\n>\n>  --show-origin::\n> @@ -300,10 +302,20 @@ Valid `<type>`'s include:\n>    When using `--get`, and the requested variable is not found, behave as if\n>    <value> were the value assigned to that variable.\n>\n> +DEPRECATED MODES\n> +----------------\n> +\n> +The following modes have been deprecated in favor of subcommands. It is\n> +recommended to migrate to the new syntax.\n> +\n> +-l::\n> +--list::\n> +\tReplaced by `git config list`.\n> +\n>  CONFIGURATION\n>  -------------\n>  `pager.config` is only respected when listing configuration, i.e., when\n> -using `--list` or any of the `--get-*` which may return multiple results.\n> +using `list` or any of the `--get-*` which may return multiple results.\n>  The default is to use a pager.\n>\n>  [[FILES]]\n> diff --git a/builtin/config.c b/builtin/config.c\n> index 59877065f8..f89ddce8da 100644\n> --- a/builtin/config.c\n> +++ b/builtin/config.c\n> @@ -16,10 +16,16 @@\n>  #include \"worktree.h\"\n>\n>  static const char *const builtin_config_usage[] = {\n> +\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n>  \tN_(\"git config [<options>]\"),\n>  \tNULL\n>  };\n>\n> +static const char *const builtin_config_list_usage[] = {\n> +\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n> +\tNULL\n> +};\n> +\n>  static char *key;\n>  static regex_t *key_regexp;\n>  static const char *value_pattern;\n> @@ -33,6 +39,7 @@ static char delim = '=';\n>  static char key_delim = ' ';\n>  static char term = '\\n';\n>\n> +static parse_opt_subcommand_fn *subcommand;\n>  static int use_global_config, use_system_config, use_local_config;\n>  static int use_worktree_config;\n>  static struct git_config_source given_config_source;\n> @@ -706,14 +713,24 @@ static void handle_nul(void) {\n>  \t}\n>  }\n>\n> +#define CONFIG_LOCATION_OPTIONS \\\n> +\tOPT_GROUP(N_(\"Config file location\")), \\\n> +\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")), \\\n> +\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")), \\\n> +\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")), \\\n> +\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")), \\\n> +\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")), \\\n> +\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\"))\n> +\n> +#define CONFIG_DISPLAY_OPTIONS \\\n> +\tOPT_GROUP(N_(\"Display options\")), \\\n> +\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")), \\\n> +\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")), \\\n> +\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")), \\\n> +\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\"))\n> +\n>  static struct option builtin_config_options[] = {\n> -\tOPT_GROUP(N_(\"Config file location\")),\n> -\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n> -\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n> -\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n> -\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n> -\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n> -\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n> +\tCONFIG_LOCATION_OPTIONS,\n>  \tOPT_GROUP(N_(\"Action\")),\n>  \tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n>  \tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n> @@ -737,15 +754,12 @@ static struct option builtin_config_options[] = {\n>  \tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n>  \tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n>  \tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n> +\tCONFIG_DISPLAY_OPTIONS,\n>  \tOPT_GROUP(N_(\"Other\")),\n> -\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n> -\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n> -\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n> -\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n> -\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n>  \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n>  \tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n>  \tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n> +\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n>  \tOPT_END(),\n>  };\n>\n> @@ -754,6 +768,42 @@ static NORETURN void usage_builtin_config(void)\n>  \tusage_with_options(builtin_config_usage, builtin_config_options);\n>  }\n>\n> +static int cmd_config_list(int argc, const char **argv, const char *prefix)\n> +{\n> +\tstruct option opts[] = {\n> +\t\tCONFIG_LOCATION_OPTIONS,\n> +\t\tCONFIG_DISPLAY_OPTIONS,\n> +\t\tOPT_GROUP(N_(\"Other\")),\n> +\t\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n> +\t\tOPT_END(),\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);\n> +\tcheck_argc(argc, 0, 0);\n> +\n> +\thandle_config_location(prefix);\n> +\thandle_nul();\n> +\n> +\tsetup_auto_pager(\"config\", 1);\n> +\n> +\tif (config_with_options(show_all_config, NULL,\n> +\t\t\t\t&given_config_source, the_repository,\n> +\t\t\t\t&config_options) < 0) {\n> +\t\tif (given_config_source.file)\n> +\t\t\tdie_errno(_(\"unable to read config file '%s'\"),\n> +\t\t\t\t  given_config_source.file);\n> +\t\telse\n> +\t\t\tdie(_(\"error processing config file(s)\"));\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +static struct option builtin_subcommand_options[] = {\n> +\tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n> +\tOPT_END(),\n> +};\n> +\n>  int cmd_config(int argc, const char **argv, const char *prefix)\n>  {\n>  \tchar *value = NULL, *comment = NULL;\n> @@ -763,6 +813,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n>\n>  \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n>\n> +\t/*\n> +\t * This is somewhat hacky: we first parse the command line while\n> +\t * keeping all args intact in order to determine whether a subcommand\n> +\t * has been specified. If so, we re-parse it a second time, but this\n> +\t * time we drop KEEP_ARGV0. This is so that we don't munge the command\n> +\t * line in case no subcommand was given, which would otherwise confuse\n> +\t * us when parsing the legacy-style modes that don't use subcommands.\n> +\t */\n> +\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n> +\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n> +\tif (subcommand) {\n> +\t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n> +\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);\n> +\t\treturn subcommand(argc, argv, prefix);\n> +\t}\n> +\n>  \targc = parse_options(argc, argv, prefix, builtin_config_options,\n>  \t\t\t     builtin_config_usage,\n>  \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n> diff --git a/t/t1300-config.sh b/t/t1300-config.sh\n> index 86dc70769a..f77d2f7847 100755\n> --- a/t/t1300-config.sh\n> +++ b/t/t1300-config.sh\n> @@ -11,6 +11,20 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>  TEST_PASSES_SANITIZE_LEAK=true\n>  . ./test-lib.sh\n>\n> +for mode in legacy subcommands\n> +do\n> +\n> +case \"$mode\" in\n> +legacy)\n> +\tmode_prefix=\"--\"\n> +\t;;\n> +subcommands)\n> +\tmode_prefix=\"\"\n> +\t;;\n> +*)\n> +\tBUG \"unknown mode $mode\";;\n> +esac\n> +\n>  test_expect_success 'setup whitespace config' '\n>  \tsed -e \"s/^|//\" \\\n>  \t    -e \"s/[$]$//\" \\\n> @@ -460,11 +474,11 @@ version.1.2.3eX.alpha=beta\n>  EOF\n>\n>  test_expect_success 'working --list' '\n> -\tgit config --list > output &&\n> +\tgit config ${mode_prefix}list > output &&\n>  \ttest_cmp expect output\n>  '\n>  test_expect_success '--list without repo produces empty output' '\n> -\tgit --git-dir=nonexistent config --list >output &&\n> +\tgit --git-dir=nonexistent config ${mode_prefix}list >output &&\n>  \ttest_must_be_empty output\n>  '\n>\n> @@ -476,7 +490,7 @@ version.1.2.3eX.alpha\n>  EOF\n>\n>  test_expect_success '--name-only --list' '\n> -\tgit config --name-only --list >output &&\n> +\tgit config ${mode_prefix}list --name-only >output &&\n>  \ttest_cmp expect output\n>  '\n>\n> @@ -614,17 +628,17 @@ ein.bahn=strasse\n>  EOF\n>\n>  test_expect_success 'alternative GIT_CONFIG' '\n> -\tGIT_CONFIG=other-config git config --list >output &&\n> +\tGIT_CONFIG=other-config git config ${mode_prefix}list >output &&\n>  \ttest_cmp expect output\n>  '\n>\n>  test_expect_success 'alternative GIT_CONFIG (--file)' '\n> -\tgit config --file other-config --list >output &&\n> +\tgit config ${mode_prefix}list --file other-config >output &&\n>  \ttest_cmp expect output\n>  '\n>\n>  test_expect_success 'alternative GIT_CONFIG (--file=-)' '\n> -\tgit config --file - --list <other-config >output &&\n> +\tgit config ${mode_prefix}list --file - <other-config >output &&\n>  \ttest_cmp expect output\n>  '\n>\n> @@ -637,6 +651,7 @@ test_expect_success 'editing stdin is an error' '\n>  '\n>\n>  test_expect_success 'refer config from subdirectory' '\n> +\ttest_when_finished \"rm -r x\" &&\n>  \tmkdir x &&\n>  \ttest_cmp_config -C x strasse --file=../other-config --get ein.bahn\n>  '\n> @@ -847,7 +862,7 @@ test_expect_success 'line number is reported correctly' '\n>  '\n>\n>  test_expect_success 'invalid stdin config' '\n> -\techo \"[broken\" | test_must_fail git config --list --file - >output 2>&1 &&\n> +\techo \"[broken\" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&\n>  \ttest_grep \"bad config line 1 in standard input\" output\n>  '\n>\n> @@ -1139,7 +1154,7 @@ section.quotecont=cont;inued\n>  EOF\n>\n>  test_expect_success 'value continued on next line' '\n> -\tgit config --list > result &&\n> +\tgit config ${mode_prefix}list > result &&\n>  \ttest_cmp expect result\n>  '\n>\n> @@ -1163,7 +1178,7 @@ Qsection.sub=section.val4\n>  Qsection.sub=section.val5Q\n>  EOF\n>  test_expect_success '--null --list' '\n> -\tgit config --null --list >result.raw &&\n> +\tgit config ${mode_prefix}list --null >result.raw &&\n>  \tnul_to_q <result.raw >result &&\n>  \techo >>result &&\n>  \ttest_cmp expect result\n> @@ -1198,6 +1213,7 @@ test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces'\n>  '\n>\n>  test_expect_success SYMLINKS 'symlinked configuration' '\n> +\ttest_when_finished \"rm myconfig\" &&\n>  \tln -s notyet myconfig &&\n>  \tgit config --file=myconfig test.frotz nitfol &&\n>  \ttest -h myconfig &&\n> @@ -1218,10 +1234,11 @@ test_expect_success SYMLINKS 'symlinked configuration' '\n>  '\n>\n>  test_expect_success SYMLINKS 'symlink to nonexistent configuration' '\n> +\ttest_when_finished \"rm linktonada linktolinktonada\" &&\n>  \tln -s doesnotexist linktonada &&\n>  \tln -s linktonada linktolinktonada &&\n> -\ttest_must_fail git config --file=linktonada --list &&\n> -\ttest_must_fail git config --file=linktolinktonada --list\n> +\ttest_must_fail git config ${mode_prefix}list --file=linktonada &&\n> +\ttest_must_fail git config ${mode_prefix}list --file=linktolinktonada\n>  '\n>\n>  test_expect_success 'check split_cmdline return' '\n> @@ -1478,7 +1495,7 @@ do\n>  done\n>\n>  test_expect_success 'git -c is not confused by empty environment' '\n> -\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config --list\n> +\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config ${mode_prefix}list\n>  '\n>\n>  test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n> @@ -1669,31 +1686,31 @@ test_expect_success 'git config ignores pairs with empty count' '\n>  '\n>\n>  test_expect_success 'git config fails with invalid count' '\n> -\ttest_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&\n> +\ttest_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&\n>  \ttest_grep \"bogus count\" error &&\n> -\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&\n> +\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&\n>  \ttest_grep \"too many entries\" error\n>  '\n>\n>  test_expect_success 'git config fails with missing config key' '\n>  \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0=\"value\" \\\n> -\t\tgit config --list 2>error &&\n> +\t\tgit config ${mode_prefix}list 2>error &&\n>  \ttest_grep \"missing config key\" error\n>  '\n>\n>  test_expect_success 'git config fails with missing config value' '\n>  \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=\"pair.one\" \\\n> -\t\tgit config --list 2>error &&\n> +\t\tgit config ${mode_prefix}list 2>error &&\n>  \ttest_grep \"missing config value\" error\n>  '\n>\n>  test_expect_success 'git config fails with invalid config pair key' '\n>  \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n>  \t\tGIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \\\n> -\t\tgit config --list &&\n> +\t\tgit config ${mode_prefix}list &&\n>  \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n>  \t\tGIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \\\n> -\t\tgit config --list\n> +\t\tgit config ${mode_prefix}list\n>  '\n>\n>  test_expect_success 'environment overrides config file' '\n> @@ -1733,7 +1750,7 @@ test_expect_success 'git config --edit works' '\n>  \tgit config -f tmp test.value no &&\n>  \techo test.value=yes >expect &&\n>  \tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n> -\tgit config -f tmp --list >actual &&\n> +\tgit config ${mode_prefix}list -f tmp >actual &&\n>  \ttest_cmp expect actual\n>  '\n>\n> @@ -1742,7 +1759,7 @@ test_expect_success 'git config --edit respects core.editor' '\n>  \techo test.value=yes >expect &&\n>  \ttest_config core.editor \"echo [test]value=yes >\" &&\n>  \tgit config -f tmp --edit &&\n> -\tgit config -f tmp --list >actual &&\n> +\tgit config ${mode_prefix}list -f tmp >actual &&\n>  \ttest_cmp expect actual\n>  '\n>\n> @@ -2093,7 +2110,7 @@ test_expect_success '--show-origin with --list' '\n>  \tcommand line:\tuser.cmdline=true\n>  \tEOF\n>  \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\\\n> -\t\tgit -c user.cmdline=true config --list --show-origin >output &&\n> +\t\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&\n>  \ttest_cmp expect output\n>  '\n>\n> @@ -2110,7 +2127,7 @@ test_expect_success '--show-origin with --list --null' '\n>  \tincludeQcommand line:Quser.cmdline\n>  \ttrueQ\n>  \tEOF\n> -\tgit -c user.cmdline=true config --null --list --show-origin >output.raw &&\n> +\tgit -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&\n>  \tnul_to_q <output.raw >output &&\n>  \t# The here-doc above adds a newline that the --null output would not\n>  \t# include. Add it here to make the two comparable.\n> @@ -2124,7 +2141,7 @@ test_expect_success '--show-origin with single file' '\n>  \tfile:.git/config\tuser.override=local\n>  \tfile:.git/config\tinclude.path=../include/relative.include\n>  \tEOF\n> -\tgit config --local --list --show-origin >output &&\n> +\tgit config ${mode_prefix}list --local --show-origin >output &&\n>  \ttest_cmp expect output\n>  '\n>\n> @@ -2162,7 +2179,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '\n>  \tcat >expect <<-\\EOF &&\n>  \tfile:\"file\\\" (dq) and spaces.conf\"\tuser.custom=true\n>  \tEOF\n> -\tgit config --file \"$WEIRDLY_NAMED_FILE\" --show-origin --list >output &&\n> +\tgit config ${mode_prefix}list --file \"$WEIRDLY_NAMED_FILE\" --show-origin >output &&\n>  \ttest_cmp expect output\n>  '\n>\n> @@ -2170,7 +2187,7 @@ test_expect_success '--show-origin stdin' '\n>  \tcat >expect <<-\\EOF &&\n>  \tstandard input:\tuser.custom=true\n>  \tEOF\n> -\tgit config --file - --show-origin --list <\"$CUSTOM_CONFIG_FILE\" >output &&\n> +\tgit config ${mode_prefix}list --file - --show-origin <\"$CUSTOM_CONFIG_FILE\" >output &&\n>  \ttest_cmp expect output\n>  '\n>\n> @@ -2197,7 +2214,7 @@ test_expect_success '--show-origin blob' '\n>  \t\tcat >expect <<-EOF &&\n>  \t\tblob:$blob\tuser.custom=true\n>  \t\tEOF\n> -\t\tgit config --blob=$blob --show-origin --list >output &&\n> +\t\tgit config ${mode_prefix}list --blob=$blob --show-origin >output &&\n>  \t\ttest_cmp expect output\n>  \t)\n>  '\n> @@ -2213,7 +2230,7 @@ test_expect_success '--show-origin blob ref' '\n>  \t\tcp \"$CUSTOM_CONFIG_FILE\" custom.conf &&\n>  \t\tgit add custom.conf &&\n>  \t\tgit commit -m \"new config file\" &&\n> -\t\tgit config --blob=main:custom.conf --show-origin --list >output &&\n> +\t\tgit config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&\n>  \t\ttest_cmp expect output\n>  \t)\n>  '\n> @@ -2239,13 +2256,14 @@ test_expect_success '--show-scope with --list' '\n>  \tworktree\tuser.worktree=true\n>  \tcommand\tuser.cmdline=true\n>  \tEOF\n> +\ttest_when_finished \"git worktree remove wt1\" &&\n>  \tgit worktree add wt1 &&\n>  \t# We need these to test for worktree scope, but outside of this\n>  \t# test, this is just noise\n>  \ttest_config core.repositoryformatversion 1 &&\n>  \ttest_config extensions.worktreeConfig true &&\n>  \tgit config --worktree user.worktree true &&\n> -\tgit -c user.cmdline=true config --list --show-scope >output &&\n> +\tgit -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&\n>  \ttest_cmp expect output\n>  '\n>\n> @@ -2254,7 +2272,7 @@ test_expect_success !MINGW '--show-scope with --blob' '\n>  \tcat >expect <<-EOF &&\n>  \tcommand\tuser.custom=true\n>  \tEOF\n> -\tgit config --blob=$blob --show-scope --list >output &&\n> +\tgit config ${mode_prefix}list --blob=$blob --show-scope >output &&\n>  \ttest_cmp expect output\n>  '\n>\n> @@ -2264,7 +2282,7 @@ test_expect_success '--show-scope with --local' '\n>  \tlocal\tuser.override=local\n>  \tlocal\tinclude.path=../include/relative.include\n>  \tEOF\n> -\tgit config --local --list --show-scope >output &&\n> +\tgit config ${mode_prefix}list --local --show-scope >output &&\n>  \ttest_cmp expect output\n>  '\n>\n> @@ -2288,7 +2306,7 @@ test_expect_success '--show-scope with --show-origin' '\n>  \tlocal\tfile:.git/../include/relative.include\tuser.relative=include\n>  \tcommand\tcommand line:\tuser.cmdline=true\n>  \tEOF\n> -\tgit -c user.cmdline=true config --list --show-origin --show-scope >output &&\n> +\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&\n>  \ttest_cmp expect output\n>  '\n>\n> @@ -2329,7 +2347,7 @@ test_expect_success 'override global and system config' '\n>  \tglobal\thome.config=true\n>  \tlocal\tlocal.config=true\n>  \tEOF\n> -\tgit config --show-scope --list >output &&\n> +\tgit config ${mode_prefix}list --show-scope >output &&\n>  \ttest_cmp expect output &&\n>\n>  \tcat >expect <<-EOF &&\n> @@ -2338,20 +2356,20 @@ test_expect_success 'override global and system config' '\n>  \tlocal\tlocal.config=true\n>  \tEOF\n>  \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \\\n> -\t\tgit config --show-scope --list >output &&\n> +\t\tgit config ${mode_prefix}list --show-scope >output &&\n>  \ttest_cmp expect output &&\n>\n>  \tcat >expect <<-EOF &&\n>  \tlocal\tlocal.config=true\n>  \tEOF\n>  \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \\\n> -\t\tgit config --show-scope --list >output &&\n> +\t\tgit config ${mode_prefix}list --show-scope >output &&\n>  \ttest_cmp expect output\n>  '\n>\n>  test_expect_success 'override global and system config with missing file' '\n> -\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&\n> -\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&\n> +\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&\n> +\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&\n>  \tGIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version\n>  '\n>\n> @@ -2478,7 +2496,7 @@ test_expect_success 'set all config with value-pattern' '\n>  \t# no match => add new entry\n>  \tcp initial config &&\n>  \tgit config --file=config abc.key two a+ &&\n> -\tgit config --file=config --list >actual &&\n> +\tgit config ${mode_prefix}list --file=config >actual &&\n>  \tcat >expect <<-\\EOF &&\n>  \tabc.key=one\n>  \tabc.key=two\n> @@ -2491,7 +2509,7 @@ test_expect_success 'set all config with value-pattern' '\n>\n>  \t# multiple values, no match => add\n>  \tgit config --file=config abc.key three a+ &&\n> -\tgit config --file=config --list >actual &&\n> +\tgit config ${mode_prefix}list --file=config >actual &&\n>  \tcat >expect <<-\\EOF &&\n>  \tabc.key=one\n>  \tabc.key=two\n> @@ -2501,7 +2519,7 @@ test_expect_success 'set all config with value-pattern' '\n>\n>  \t# single match => replace\n>  \tgit config --file=config abc.key four h+ &&\n> -\tgit config --file=config --list >actual &&\n> +\tgit config ${mode_prefix}list --file=config >actual &&\n>  \tcat >expect <<-\\EOF &&\n>  \tabc.key=one\n>  \tabc.key=two\n> @@ -2516,7 +2534,7 @@ test_expect_success '--replace-all and value-pattern' '\n>  \tgit config --file=config --add abc.key two &&\n>  \tgit config --file=config --add abc.key three &&\n>  \tgit config --file=config --replace-all abc.key four \"o+\" &&\n> -\tgit config --file=config --list >actual &&\n> +\tgit config ${mode_prefix}list --file=config >actual &&\n>  \tcat >expect <<-\\EOF &&\n>  \tabc.key=four\n>  \tabc.key=three\n> @@ -2534,7 +2552,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n>  \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n>  \ttest_must_fail git config --file=config --fixed-value --rename-section dev null &&\n>  \ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n> -\ttest_must_fail git config --file=config --fixed-value --list &&\n> +\ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n>  \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n>  \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n>\n> @@ -2555,7 +2573,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n>\n>  \tcp initial config &&\n>  \tgit config --file=config fixed.test bogus \"$META\" &&\n> -\tgit config --file=config --list >actual &&\n> +\tgit config ${mode_prefix}list --file=config >actual &&\n>  \tcat >expect <<-EOF &&\n>  \tfixed.test=$META\n>  \tfixed.test=bogus\n> @@ -2564,7 +2582,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n>\n>  \tcp initial config &&\n>  \tgit config --file=config --fixed-value fixed.test bogus \"$META\" &&\n> -\tgit config --file=config --list >actual &&\n> +\tgit config ${mode_prefix}list --file=config >actual &&\n>  \tcat >expect <<-\\EOF &&\n>  \tfixed.test=bogus\n>  \tEOF\n> @@ -2582,7 +2600,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n>\n>  \tcp initial config &&\n>  \tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n> -\tgit config --file=config --list >actual &&\n> +\tgit config ${mode_prefix}list --file=config >actual &&\n>  \tcat >expect <<-EOF &&\n>  \tfixed.test=$META\n>  \tfixed.test=bogus\n> @@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n>  \ttest_cmp expect actual &&\n>\n>  \tgit config --file=config --fixed-value --replace-all fixed.test bogus \"$META\" &&\n> -\tgit config --file=config --list >actual &&\n> +\tgit config ${mode_prefix}list --file=config >actual &&\n>  \tcat >expect <<-EOF &&\n>  \tfixed.test=bogus\n>  \tfixed.test=bogus\n> @@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes failure' '\n>  \ttest_cmp expect err\n>  '\n>\n> +done\n> +\n>\nNit: Wouldn't it be better if the tests are indented here? That way you\nknow it's part of a loop.\n"},{"id":"494000","messageId":"015b01da9d5b$bbe59120$33b0b360$@nexbridge.com","threadId":"61061","inReplyTo":"CAOLa=ZSNbZPByO9QyeAGaR1pWXMB7ge_GF7M5fydxP-cse-X3g@mail.gmail.com","subject":"RE: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-05-03T13:13:42Z","receivedAt":"2024-05-03T13:16:50Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Friday, May 3, 2024 9:09 AM. Karthik Nayak wrote:\n>Patrick Steinhardt <ps@pks.im> writes:\n>\n>> While git-config(1) has several modes, those modes are not exposed\n>> with subcommands but instead by specifying e.g. `--unset` or `--list`.\n>> This\n>\n>s/specifying/specifying flags/ perhaps?\n>\n>> user interface is not really in line with how our more modern commands\n>> work, where it is a lot more customary to say e.g. `git remote list`.\n>\n>Tangent: I totally agree with the patch, but it would be nice to have a\n>'DesigningCommands' document which would highlight UX do's and don'ts.\n>It would be nice to add that as reference in discussions.\n>\n>> Furthermore, to add to the confusion, git-config(1) also allows the\n>> user to request modes implicitly by just specifying the correct number\n>> of arguments. Thus, `git config foo.bar` will retrieve the value of\n>> \"foo.bar\" while `git config foo.bar baz` will set it to \"baz\".\n>>\n>> Overall, this makes for a confusing interface that could really use a\n>> makeover. It hurts discoverability of what you can do with\n>> git-config(1) and is comparatively easy to get wrong. Converting the\n>> command to have subcommands instead would go a long way to help address\n>these issues.\n>>\n>> One concern in this context is backwards compatibility. Luckily, we\n>> can introduce subcommands without breaking backwards compatibility at all.\n>> This is because all the implicit modes of git-config(1) require that\n>> the first argument is a properly formatted config key. And as config\n>> keys _must_ have a dot in their name, any value without a dot would\n>> have been discarded by git-config(1) previous to this change. Thus,\n>> given that none of the subcommands do have a dot, they are unambiguous.\n>>\n>> Introduce the first such new subcommand, which is \"git config list\".\n>> To retain backwards compatibility we only conditionally use\n>> subcommands and will fall back to the old syntax in case no subcommand was\n>detected.\n>> This should help to transition to the new-style syntax until we\n>> eventually deprecate and remove the old-style syntax.\n>>\n>> Note that the way we handle this we're duplicating some functionality\n>> across old and new syntax. While this isn't pretty, it helps us to\n>> ensure that there really is no change in behaviour for the old syntax.\n>>\n>> Amend tests such that we run them both with old and new style syntax.\n>> As tests are now run twice, state from the first run may be still be\n>> around in the second run and thus cause tests to fail. Add cleanup\n>> logic as required to fix such tests.\n>>\n>> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>> ---\n>>  Documentation/git-config.txt |  26 ++++++---\n>>  builtin/config.c             |  90 ++++++++++++++++++++++++----\n>>  t/t1300-config.sh            | 110 +++++++++++++++++++++--------------\n>>  3 files changed, 162 insertions(+), 64 deletions(-)\n>>\n>> diff --git a/Documentation/git-config.txt\n>> b/Documentation/git-config.txt index ac61113fcc..c83c97cb7e 100644\n>> --- a/Documentation/git-config.txt\n>> +++ b/Documentation/git-config.txt\n>> @@ -9,6 +9,7 @@ git-config - Get and set repository or global options\n>> SYNOPSIS\n>>  --------\n>>  [verse]\n>> +'git config list' [<file-option>] [<display-option>] [--includes]\n>>  'git config' [<file-option>] [--type=<type>] [--comment=<message>]\n>> [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name>\n>> [<value> [<value-pattern>]]  'git config' [<file-option>]\n>> [--type=<type>] [--comment=<message>] --add <name> <value>  'git\n>> config' [<file-option>] [--type=<type>] [--comment=<message>]\n>> [--fixed-value] --replace-all <name> <value> [<value-pattern>] @@\n>> -20,7 +21,6 @@ SYNOPSIS  'git config' [<file-option>] [--fixed-value]\n>> --unset-all <name> [<value-pattern>]  'git config' [<file-option>]\n>> --rename-section <old-name> <new-name>  'git config' [<file-option>]\n>> --remove-section <name> -'git config' [<file-option>] [--show-origin]\n>> [--show-scope] [-z|--null] [--name-only] -l | --list  'git config'\n>> [<file-option>] --get-color <name> [<default>]  'git config' [<file-option>] --get-\n>colorbool <name> [<stdout-is-tty>]  'git config' [<file-option>] -e | --edit @@ -74,6\n>+74,12 @@ On success, the command returns the exit code 0.\n>>  A list of all available configuration variables can be obtained using\n>> the  `git help --config` command.\n>>\n>> +COMMANDS\n>> +--------\n>> +\n>> +list::\n>> +\tList all variables set in config file, along with their values.\n>> +\n>>  [[OPTIONS]]\n>>  OPTIONS\n>>  -------\n>> @@ -190,10 +196,6 @@ See also <<FILES>>.\n>>  --unset-all::\n>>  \tRemove all lines matching the key from config file.\n>>\n>> --l::\n>> ---list::\n>> -\tList all variables set in config file, along with their values.\n>> -\n>>  --fixed-value::\n>>  \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n>>  \tan exact string instead of a regular expression. This will restrict\n>> @@ -248,7 +250,7 @@ Valid `<type>`'s include:\n>>  \tcontain line breaks.\n>>\n>>  --name-only::\n>> -\tOutput only the names of config variables for `--list` or\n>> +\tOutput only the names of config variables for `list` or\n>>  \t`--get-regexp`.\n>>\n>>  --show-origin::\n>> @@ -300,10 +302,20 @@ Valid `<type>`'s include:\n>>    When using `--get`, and the requested variable is not found, behave as if\n>>    <value> were the value assigned to that variable.\n>>\n>> +DEPRECATED MODES\n>> +----------------\n>> +\n>> +The following modes have been deprecated in favor of subcommands. It\n>> +is recommended to migrate to the new syntax.\n>> +\n>> +-l::\n>> +--list::\n>> +\tReplaced by `git config list`.\n>> +\n>>  CONFIGURATION\n>>  -------------\n>>  `pager.config` is only respected when listing configuration, i.e.,\n>> when -using `--list` or any of the `--get-*` which may return multiple results.\n>> +using `list` or any of the `--get-*` which may return multiple results.\n>>  The default is to use a pager.\n>>\n>>  [[FILES]]\n>> diff --git a/builtin/config.c b/builtin/config.c index\n>> 59877065f8..f89ddce8da 100644\n>> --- a/builtin/config.c\n>> +++ b/builtin/config.c\n>> @@ -16,10 +16,16 @@\n>>  #include \"worktree.h\"\n>>\n>>  static const char *const builtin_config_usage[] = {\n>> +\tN_(\"git config list [<file-option>] [<display-option>]\n>> +[--includes]\"),\n>>  \tN_(\"git config [<options>]\"),\n>>  \tNULL\n>>  };\n>>\n>> +static const char *const builtin_config_list_usage[] = {\n>> +\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n>> +\tNULL\n>> +};\n>> +\n>>  static char *key;\n>>  static regex_t *key_regexp;\n>>  static const char *value_pattern;\n>> @@ -33,6 +39,7 @@ static char delim = '=';  static char key_delim = '\n>> ';  static char term = '\\n';\n>>\n>> +static parse_opt_subcommand_fn *subcommand;\n>>  static int use_global_config, use_system_config, use_local_config;\n>> static int use_worktree_config;  static struct git_config_source\n>> given_config_source; @@ -706,14 +713,24 @@ static void\n>> handle_nul(void) {\n>>  \t}\n>>  }\n>>\n>> +#define CONFIG_LOCATION_OPTIONS \\\n>> +\tOPT_GROUP(N_(\"Config file location\")), \\\n>> +\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")), \\\n>> +\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config\n>file\")), \\\n>> +\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")), \\\n>> +\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree\n>config file\")), \\\n>> +\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given\n>config file\")), \\\n>> +\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"),\n>> +N_(\"read config from given blob object\"))\n>> +\n>> +#define CONFIG_DISPLAY_OPTIONS \\\n>> +\tOPT_GROUP(N_(\"Display options\")), \\\n>> +\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")), \\\n>> +\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names\n>only\")), \\\n>> +\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file,\n>standard input, blob, command line)\")), \\\n>> +\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config\n>> +(worktree, local, global, system, command)\"))\n>> +\n>>  static struct option builtin_config_options[] = {\n>> -\tOPT_GROUP(N_(\"Config file location\")),\n>> -\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n>> -\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config\n>file\")),\n>> -\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n>> -\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree\n>config file\")),\n>> -\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given\n>config file\")),\n>> -\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"),\n>N_(\"read config from given blob object\")),\n>> +\tCONFIG_LOCATION_OPTIONS,\n>>  \tOPT_GROUP(N_(\"Action\")),\n>>  \tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-\n>pattern>]\"), ACTION_GET),\n>>  \tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key\n>> [<value-pattern>]\"), ACTION_GET_ALL), @@ -737,15 +754,12 @@ static struct\n>option builtin_config_options[] = {\n>>  \tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or\n>string\"), TYPE_BOOL_OR_STR),\n>>  \tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or\n>directory name)\"), TYPE_PATH),\n>>  \tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry\n>> date\"), TYPE_EXPIRY_DATE),\n>> +\tCONFIG_DISPLAY_OPTIONS,\n>>  \tOPT_GROUP(N_(\"Other\")),\n>> -\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n>> -\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names\n>only\")),\n>> -\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include\n>directives on lookup\")),\n>> -\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file,\n>standard input, blob, command line)\")),\n>> -\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config\n>(worktree, local, global, system, command)\")),\n>>  \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use\n>default value when missing entry\")),\n>>  \tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-\n>readable comment string (# will be prepended as needed)\")),\n>>  \tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality\n>> when comparing values to 'value-pattern'\")),\n>> +\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include\n>> +directives on lookup\")),\n>>  \tOPT_END(),\n>>  };\n>>\n>> @@ -754,6 +768,42 @@ static NORETURN void usage_builtin_config(void)\n>>  \tusage_with_options(builtin_config_usage, builtin_config_options);  }\n>>\n>> +static int cmd_config_list(int argc, const char **argv, const char\n>> +*prefix) {\n>> +\tstruct option opts[] = {\n>> +\t\tCONFIG_LOCATION_OPTIONS,\n>> +\t\tCONFIG_DISPLAY_OPTIONS,\n>> +\t\tOPT_GROUP(N_(\"Other\")),\n>> +\t\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect\n>include directives on lookup\")),\n>> +\t\tOPT_END(),\n>> +\t};\n>> +\n>> +\targc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);\n>> +\tcheck_argc(argc, 0, 0);\n>> +\n>> +\thandle_config_location(prefix);\n>> +\thandle_nul();\n>> +\n>> +\tsetup_auto_pager(\"config\", 1);\n>> +\n>> +\tif (config_with_options(show_all_config, NULL,\n>> +\t\t\t\t&given_config_source, the_repository,\n>> +\t\t\t\t&config_options) < 0) {\n>> +\t\tif (given_config_source.file)\n>> +\t\t\tdie_errno(_(\"unable to read config file '%s'\"),\n>> +\t\t\t\t  given_config_source.file);\n>> +\t\telse\n>> +\t\t\tdie(_(\"error processing config file(s)\"));\n>> +\t}\n>> +\n>> +\treturn 0;\n>> +}\n>> +\n>> +static struct option builtin_subcommand_options[] = {\n>> +\tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n>> +\tOPT_END(),\n>> +};\n>> +\n>>  int cmd_config(int argc, const char **argv, const char *prefix)  {\n>>  \tchar *value = NULL, *comment = NULL; @@ -763,6 +813,22 @@ int\n>> cmd_config(int argc, const char **argv, const char *prefix)\n>>\n>>  \tgiven_config_source.file =\n>> xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n>>\n>> +\t/*\n>> +\t * This is somewhat hacky: we first parse the command line while\n>> +\t * keeping all args intact in order to determine whether a subcommand\n>> +\t * has been specified. If so, we re-parse it a second time, but this\n>> +\t * time we drop KEEP_ARGV0. This is so that we don't munge the command\n>> +\t * line in case no subcommand was given, which would otherwise confuse\n>> +\t * us when parsing the legacy-style modes that don't use subcommands.\n>> +\t */\n>> +\targc = parse_options(argc, argv, prefix, builtin_subcommand_options,\n>builtin_config_usage,\n>> +\n>PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE\n>_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n>> +\tif (subcommand) {\n>> +\t\targc = parse_options(argc, argv, prefix,\n>builtin_subcommand_options, builtin_config_usage,\n>> +\n>PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE\n>_OPT_KEEP_UNKNOWN_OPT);\n>> +\t\treturn subcommand(argc, argv, prefix);\n>> +\t}\n>> +\n>>  \targc = parse_options(argc, argv, prefix, builtin_config_options,\n>>  \t\t\t     builtin_config_usage,\n>>  \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION); diff --git\n>a/t/t1300-config.sh\n>> b/t/t1300-config.sh index 86dc70769a..f77d2f7847 100755\n>> --- a/t/t1300-config.sh\n>> +++ b/t/t1300-config.sh\n>> @@ -11,6 +11,20 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>>  TEST_PASSES_SANITIZE_LEAK=true\n>>  . ./test-lib.sh\n>>\n>> +for mode in legacy subcommands\n>> +do\n>> +\n>> +case \"$mode\" in\n>> +legacy)\n>> +\tmode_prefix=\"--\"\n>> +\t;;\n>> +subcommands)\n>> +\tmode_prefix=\"\"\n>> +\t;;\n>> +*)\n>> +\tBUG \"unknown mode $mode\";;\n>> +esac\n>> +\n>>  test_expect_success 'setup whitespace config' '\n>>  \tsed -e \"s/^|//\" \\\n>>  \t    -e \"s/[$]$//\" \\\n>> @@ -460,11 +474,11 @@ version.1.2.3eX.alpha=beta  EOF\n>>\n>>  test_expect_success 'working --list' '\n>> -\tgit config --list > output &&\n>> +\tgit config ${mode_prefix}list > output &&\n>>  \ttest_cmp expect output\n>>  '\n>>  test_expect_success '--list without repo produces empty output' '\n>> -\tgit --git-dir=nonexistent config --list >output &&\n>> +\tgit --git-dir=nonexistent config ${mode_prefix}list >output &&\n>>  \ttest_must_be_empty output\n>>  '\n>>\n>> @@ -476,7 +490,7 @@ version.1.2.3eX.alpha  EOF\n>>\n>>  test_expect_success '--name-only --list' '\n>> -\tgit config --name-only --list >output &&\n>> +\tgit config ${mode_prefix}list --name-only >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>> @@ -614,17 +628,17 @@ ein.bahn=strasse  EOF\n>>\n>>  test_expect_success 'alternative GIT_CONFIG' '\n>> -\tGIT_CONFIG=other-config git config --list >output &&\n>> +\tGIT_CONFIG=other-config git config ${mode_prefix}list >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>>  test_expect_success 'alternative GIT_CONFIG (--file)' '\n>> -\tgit config --file other-config --list >output &&\n>> +\tgit config ${mode_prefix}list --file other-config >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>>  test_expect_success 'alternative GIT_CONFIG (--file=-)' '\n>> -\tgit config --file - --list <other-config >output &&\n>> +\tgit config ${mode_prefix}list --file - <other-config >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>> @@ -637,6 +651,7 @@ test_expect_success 'editing stdin is an error' '\n>>  '\n>>\n>>  test_expect_success 'refer config from subdirectory' '\n>> +\ttest_when_finished \"rm -r x\" &&\n>>  \tmkdir x &&\n>>  \ttest_cmp_config -C x strasse --file=../other-config --get ein.bahn\n>> '\n>> @@ -847,7 +862,7 @@ test_expect_success 'line number is reported correctly' '\n>>  '\n>>\n>>  test_expect_success 'invalid stdin config' '\n>> -\techo \"[broken\" | test_must_fail git config --list --file - >output 2>&1 &&\n>> +\techo \"[broken\" | test_must_fail git config ${mode_prefix}list --file\n>> +- >output 2>&1 &&\n>>  \ttest_grep \"bad config line 1 in standard input\" output  '\n>>\n>> @@ -1139,7 +1154,7 @@ section.quotecont=cont;inued  EOF\n>>\n>>  test_expect_success 'value continued on next line' '\n>> -\tgit config --list > result &&\n>> +\tgit config ${mode_prefix}list > result &&\n>>  \ttest_cmp expect result\n>>  '\n>>\n>> @@ -1163,7 +1178,7 @@ Qsection.sub=section.val4\n>> Qsection.sub=section.val5Q  EOF  test_expect_success '--null --list' '\n>> -\tgit config --null --list >result.raw &&\n>> +\tgit config ${mode_prefix}list --null >result.raw &&\n>>  \tnul_to_q <result.raw >result &&\n>>  \techo >>result &&\n>>  \ttest_cmp expect result\n>> @@ -1198,6 +1213,7 @@ test_expect_success 'inner whitespace kept verbatim,\n>horizontal tabs and spaces'\n>>  '\n>>\n>>  test_expect_success SYMLINKS 'symlinked configuration' '\n>> +\ttest_when_finished \"rm myconfig\" &&\n>>  \tln -s notyet myconfig &&\n>>  \tgit config --file=myconfig test.frotz nitfol &&\n>>  \ttest -h myconfig &&\n>> @@ -1218,10 +1234,11 @@ test_expect_success SYMLINKS 'symlinked\n>configuration' '\n>>  '\n>>\n>>  test_expect_success SYMLINKS 'symlink to nonexistent configuration' '\n>> +\ttest_when_finished \"rm linktonada linktolinktonada\" &&\n>>  \tln -s doesnotexist linktonada &&\n>>  \tln -s linktonada linktolinktonada &&\n>> -\ttest_must_fail git config --file=linktonada --list &&\n>> -\ttest_must_fail git config --file=linktolinktonada --list\n>> +\ttest_must_fail git config ${mode_prefix}list --file=linktonada &&\n>> +\ttest_must_fail git config ${mode_prefix}list --file=linktolinktonada\n>>  '\n>>\n>>  test_expect_success 'check split_cmdline return' '\n>> @@ -1478,7 +1495,7 @@ do\n>>  done\n>>\n>>  test_expect_success 'git -c is not confused by empty environment' '\n>> -\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config --list\n>> +\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config ${mode_prefix}list\n>>  '\n>>\n>>  test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n>> @@ -1669,31 +1686,31 @@ test_expect_success 'git config ignores pairs with\n>empty count' '\n>>  '\n>>\n>>  test_expect_success 'git config fails with invalid count' '\n>> -\ttest_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&\n>> +\ttest_must_fail env GIT_CONFIG_COUNT=10a git config\n>> +${mode_prefix}list 2>error &&\n>>  \ttest_grep \"bogus count\" error &&\n>> -\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --\n>list 2>error &&\n>> +\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config\n>> +${mode_prefix}list 2>error &&\n>>  \ttest_grep \"too many entries\" error\n>>  '\n>>\n>>  test_expect_success 'git config fails with missing config key' '\n>>  \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0=\"value\"\n>\\\n>> -\t\tgit config --list 2>error &&\n>> +\t\tgit config ${mode_prefix}list 2>error &&\n>>  \ttest_grep \"missing config key\" error  '\n>>\n>>  test_expect_success 'git config fails with missing config value' '\n>>  \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=\"pair.one\" \\\n>> -\t\tgit config --list 2>error &&\n>> +\t\tgit config ${mode_prefix}list 2>error &&\n>>  \ttest_grep \"missing config value\" error  '\n>>\n>>  test_expect_success 'git config fails with invalid config pair key' '\n>>  \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n>>  \t\tGIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \\\n>> -\t\tgit config --list &&\n>> +\t\tgit config ${mode_prefix}list &&\n>>  \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n>>  \t\tGIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value\n>\\\n>> -\t\tgit config --list\n>> +\t\tgit config ${mode_prefix}list\n>>  '\n>>\n>>  test_expect_success 'environment overrides config file' '\n>> @@ -1733,7 +1750,7 @@ test_expect_success 'git config --edit works' '\n>>  \tgit config -f tmp test.value no &&\n>>  \techo test.value=yes >expect &&\n>>  \tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n>> -\tgit config -f tmp --list >actual &&\n>> +\tgit config ${mode_prefix}list -f tmp >actual &&\n>>  \ttest_cmp expect actual\n>>  '\n>>\n>> @@ -1742,7 +1759,7 @@ test_expect_success 'git config --edit respects\n>core.editor' '\n>>  \techo test.value=yes >expect &&\n>>  \ttest_config core.editor \"echo [test]value=yes >\" &&\n>>  \tgit config -f tmp --edit &&\n>> -\tgit config -f tmp --list >actual &&\n>> +\tgit config ${mode_prefix}list -f tmp >actual &&\n>>  \ttest_cmp expect actual\n>>  '\n>>\n>> @@ -2093,7 +2110,7 @@ test_expect_success '--show-origin with --list' '\n>>  \tcommand line:\tuser.cmdline=true\n>>  \tEOF\n>>  \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ\n>GIT_CONFIG_VALUE_0=true\\\n>> -\t\tgit -c user.cmdline=true config --list --show-origin >output &&\n>> +\t\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin\n>> +>output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>> @@ -2110,7 +2127,7 @@ test_expect_success '--show-origin with --list --null' '\n>>  \tincludeQcommand line:Quser.cmdline\n>>  \ttrueQ\n>>  \tEOF\n>> -\tgit -c user.cmdline=true config --null --list --show-origin >output.raw &&\n>> +\tgit -c user.cmdline=true config ${mode_prefix}list --null\n>> +--show-origin >output.raw &&\n>>  \tnul_to_q <output.raw >output &&\n>>  \t# The here-doc above adds a newline that the --null output would not\n>>  \t# include. Add it here to make the two comparable.\n>> @@ -2124,7 +2141,7 @@ test_expect_success '--show-origin with single file' '\n>>  \tfile:.git/config\tuser.override=local\n>>  \tfile:.git/config\tinclude.path=../include/relative.include\n>>  \tEOF\n>> -\tgit config --local --list --show-origin >output &&\n>> +\tgit config ${mode_prefix}list --local --show-origin >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>> @@ -2162,7 +2179,7 @@ test_expect_success !MINGW '--show-origin escape\n>special file name characters' '\n>>  \tcat >expect <<-\\EOF &&\n>>  \tfile:\"file\\\" (dq) and spaces.conf\"\tuser.custom=true\n>>  \tEOF\n>> -\tgit config --file \"$WEIRDLY_NAMED_FILE\" --show-origin --list >output &&\n>> +\tgit config ${mode_prefix}list --file \"$WEIRDLY_NAMED_FILE\"\n>> +--show-origin >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>> @@ -2170,7 +2187,7 @@ test_expect_success '--show-origin stdin' '\n>>  \tcat >expect <<-\\EOF &&\n>>  \tstandard input:\tuser.custom=true\n>>  \tEOF\n>> -\tgit config --file - --show-origin --list <\"$CUSTOM_CONFIG_FILE\" >output\n>&&\n>> +\tgit config ${mode_prefix}list --file - --show-origin\n>> +<\"$CUSTOM_CONFIG_FILE\" >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>> @@ -2197,7 +2214,7 @@ test_expect_success '--show-origin blob' '\n>>  \t\tcat >expect <<-EOF &&\n>>  \t\tblob:$blob\tuser.custom=true\n>>  \t\tEOF\n>> -\t\tgit config --blob=$blob --show-origin --list >output &&\n>> +\t\tgit config ${mode_prefix}list --blob=$blob --show-origin >output\n>&&\n>>  \t\ttest_cmp expect output\n>>  \t)\n>>  '\n>> @@ -2213,7 +2230,7 @@ test_expect_success '--show-origin blob ref' '\n>>  \t\tcp \"$CUSTOM_CONFIG_FILE\" custom.conf &&\n>>  \t\tgit add custom.conf &&\n>>  \t\tgit commit -m \"new config file\" &&\n>> -\t\tgit config --blob=main:custom.conf --show-origin --list >output &&\n>> +\t\tgit config ${mode_prefix}list --blob=main:custom.conf --show-\n>origin\n>> +>output &&\n>>  \t\ttest_cmp expect output\n>>  \t)\n>>  '\n>> @@ -2239,13 +2256,14 @@ test_expect_success '--show-scope with --list' '\n>>  \tworktree\tuser.worktree=true\n>>  \tcommand\tuser.cmdline=true\n>>  \tEOF\n>> +\ttest_when_finished \"git worktree remove wt1\" &&\n>>  \tgit worktree add wt1 &&\n>>  \t# We need these to test for worktree scope, but outside of this\n>>  \t# test, this is just noise\n>>  \ttest_config core.repositoryformatversion 1 &&\n>>  \ttest_config extensions.worktreeConfig true &&\n>>  \tgit config --worktree user.worktree true &&\n>> -\tgit -c user.cmdline=true config --list --show-scope >output &&\n>> +\tgit -c user.cmdline=true config ${mode_prefix}list --show-scope\n>> +>output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>> @@ -2254,7 +2272,7 @@ test_expect_success !MINGW '--show-scope with --\n>blob' '\n>>  \tcat >expect <<-EOF &&\n>>  \tcommand\tuser.custom=true\n>>  \tEOF\n>> -\tgit config --blob=$blob --show-scope --list >output &&\n>> +\tgit config ${mode_prefix}list --blob=$blob --show-scope >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>> @@ -2264,7 +2282,7 @@ test_expect_success '--show-scope with --local' '\n>>  \tlocal\tuser.override=local\n>>  \tlocal\tinclude.path=../include/relative.include\n>>  \tEOF\n>> -\tgit config --local --list --show-scope >output &&\n>> +\tgit config ${mode_prefix}list --local --show-scope >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>> @@ -2288,7 +2306,7 @@ test_expect_success '--show-scope with --show-origin'\n>'\n>>  \tlocal\tfile:.git/../include/relative.include\tuser.relative=include\n>>  \tcommand\tcommand line:\tuser.cmdline=true\n>>  \tEOF\n>> -\tgit -c user.cmdline=true config --list --show-origin --show-scope >output\n>&&\n>> +\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin\n>> +--show-scope >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>> @@ -2329,7 +2347,7 @@ test_expect_success 'override global and system\n>config' '\n>>  \tglobal\thome.config=true\n>>  \tlocal\tlocal.config=true\n>>  \tEOF\n>> -\tgit config --show-scope --list >output &&\n>> +\tgit config ${mode_prefix}list --show-scope >output &&\n>>  \ttest_cmp expect output &&\n>>\n>>  \tcat >expect <<-EOF &&\n>> @@ -2338,20 +2356,20 @@ test_expect_success 'override global and system\n>config' '\n>>  \tlocal\tlocal.config=true\n>>  \tEOF\n>>  \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-\n>config GIT_CONFIG_GLOBAL=custom-global-config \\\n>> -\t\tgit config --show-scope --list >output &&\n>> +\t\tgit config ${mode_prefix}list --show-scope >output &&\n>>  \ttest_cmp expect output &&\n>>\n>>  \tcat >expect <<-EOF &&\n>>  \tlocal\tlocal.config=true\n>>  \tEOF\n>>  \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null\n>GIT_CONFIG_GLOBAL=/dev/null \\\n>> -\t\tgit config --show-scope --list >output &&\n>> +\t\tgit config ${mode_prefix}list --show-scope >output &&\n>>  \ttest_cmp expect output\n>>  '\n>>\n>>  test_expect_success 'override global and system config with missing file' '\n>> -\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist\n>GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&\n>> -\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null\n>GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&\n>> +\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist\n>GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&\n>> +\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null\n>> +GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list\n>> +--system &&\n>>  \tGIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist\n>> git version  '\n>>\n>> @@ -2478,7 +2496,7 @@ test_expect_success 'set all config with value-pattern' '\n>>  \t# no match => add new entry\n>>  \tcp initial config &&\n>>  \tgit config --file=config abc.key two a+ &&\n>> -\tgit config --file=config --list >actual &&\n>> +\tgit config ${mode_prefix}list --file=config >actual &&\n>>  \tcat >expect <<-\\EOF &&\n>>  \tabc.key=one\n>>  \tabc.key=two\n>> @@ -2491,7 +2509,7 @@ test_expect_success 'set all config with value-pattern' '\n>>\n>>  \t# multiple values, no match => add\n>>  \tgit config --file=config abc.key three a+ &&\n>> -\tgit config --file=config --list >actual &&\n>> +\tgit config ${mode_prefix}list --file=config >actual &&\n>>  \tcat >expect <<-\\EOF &&\n>>  \tabc.key=one\n>>  \tabc.key=two\n>> @@ -2501,7 +2519,7 @@ test_expect_success 'set all config with value-pattern' '\n>>\n>>  \t# single match => replace\n>>  \tgit config --file=config abc.key four h+ &&\n>> -\tgit config --file=config --list >actual &&\n>> +\tgit config ${mode_prefix}list --file=config >actual &&\n>>  \tcat >expect <<-\\EOF &&\n>>  \tabc.key=one\n>>  \tabc.key=two\n>> @@ -2516,7 +2534,7 @@ test_expect_success '--replace-all and value-pattern' '\n>>  \tgit config --file=config --add abc.key two &&\n>>  \tgit config --file=config --add abc.key three &&\n>>  \tgit config --file=config --replace-all abc.key four \"o+\" &&\n>> -\tgit config --file=config --list >actual &&\n>> +\tgit config ${mode_prefix}list --file=config >actual &&\n>>  \tcat >expect <<-\\EOF &&\n>>  \tabc.key=four\n>>  \tabc.key=three\n>> @@ -2534,7 +2552,7 @@ test_expect_success 'refuse --fixed-value for\n>incompatible actions' '\n>>  \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null\n>bogus &&\n>>  \ttest_must_fail git config --file=config --fixed-value --rename-section dev null\n>&&\n>>  \ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n>> -\ttest_must_fail git config --file=config --fixed-value --list &&\n>> +\ttest_must_fail git config ${mode_prefix}list --file=config\n>> +--fixed-value &&\n>>  \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n>>  \ttest_must_fail git config --file=config --fixed-value\n>> --get-colorbool dev.null &&\n>>\n>> @@ -2555,7 +2573,7 @@ test_expect_success '--fixed-value uses exact string\n>matching' '\n>>\n>>  \tcp initial config &&\n>>  \tgit config --file=config fixed.test bogus \"$META\" &&\n>> -\tgit config --file=config --list >actual &&\n>> +\tgit config ${mode_prefix}list --file=config >actual &&\n>>  \tcat >expect <<-EOF &&\n>>  \tfixed.test=$META\n>>  \tfixed.test=bogus\n>> @@ -2564,7 +2582,7 @@ test_expect_success '--fixed-value uses exact string\n>matching' '\n>>\n>>  \tcp initial config &&\n>>  \tgit config --file=config --fixed-value fixed.test bogus \"$META\" &&\n>> -\tgit config --file=config --list >actual &&\n>> +\tgit config ${mode_prefix}list --file=config >actual &&\n>>  \tcat >expect <<-\\EOF &&\n>>  \tfixed.test=bogus\n>>  \tEOF\n>> @@ -2582,7 +2600,7 @@ test_expect_success '--fixed-value uses exact string\n>matching' '\n>>\n>>  \tcp initial config &&\n>>  \tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n>> -\tgit config --file=config --list >actual &&\n>> +\tgit config ${mode_prefix}list --file=config >actual &&\n>>  \tcat >expect <<-EOF &&\n>>  \tfixed.test=$META\n>>  \tfixed.test=bogus\n>> @@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string\n>matching' '\n>>  \ttest_cmp expect actual &&\n>>\n>>  \tgit config --file=config --fixed-value --replace-all fixed.test bogus \"$META\"\n>&&\n>> -\tgit config --file=config --list >actual &&\n>> +\tgit config ${mode_prefix}list --file=config >actual &&\n>>  \tcat >expect <<-EOF &&\n>>  \tfixed.test=bogus\n>>  \tfixed.test=bogus\n>> @@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes\n>failure' '\n>>  \ttest_cmp expect err\n>>  '\n>>\n>> +done\n>> +\n>>\n>Nit: Wouldn't it be better if the tests are indented here? That way you know it's part\n>of a loop.\n\nRemoving 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.\n\nThanks,\nRandall\n\n"},{"id":"494001","messageId":"ae41e7b58ad156e0c035c6eb120a32dd@manjaro.org","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"Re: [PATCH v4 00/14] builtin/config: introduce subcommands","fromName":"Dragan Simic","fromEmail":"dsimic@manjaro.org","sentAt":"2024-05-03T13:36:50Z","receivedAt":"2024-05-03T13:36:52Z","isPatch":true,"sender":{"key":"dsimic@manjaro.org","avatar":null},"body":"Hello Patrick,\n\nOn 2024-05-03 11:56, Patrick Steinhardt wrote:\n> this is the fourth version of my patch series that introduces\n> subcommands for git-config(1).\n> \n> Changes compared to v3:\n> \n>     - Rebased on top of d4cc1ec35f (Start the 2.46 cycle, 2024-04-30).\n> \n>     - Implemented support for `git config set --comment`. This switch\n>       has been added since the last version of this patch series.\n> \n> Here's hoping that there's a bit more interest in this patch series at\n> the beginning of the release cycle :)\n\nFWIW, I find the introduction of the subcommands really great.\nIt makes the way git-config(1) works much more intuitive.\n"},{"id":"494012","messageId":"xmqqjzkaanr1.fsf@gitster.g","threadId":"61061","inReplyTo":"015b01da9d5b$bbe59120$33b0b360$@nexbridge.com","subject":"Re: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-03T16:01:06Z","receivedAt":"2024-05-03T16:01:11Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"<rsbecker@nexbridge.com> writes:\n\n>>> +DEPRECATED MODES\n>>> +----------------\n>>> +\n>>> +The following modes have been deprecated in favor of subcommands. It\n>>> +is recommended to migrate to the new syntax.\n>>> +\n>>> +-l::\n>>> +--list::\n>>> +\tReplaced by `git config list`.\n\nCan you trim quotes in your response that has no relevance to what\nyou are responding to?\n\n> Removing the --list option is going to break backward\n> compatibility for users who script the use of config for things\n> like setup, clone automation, etc.\n\nYes, that is why this is merely a deprecation.\n\n> Adding list as a sub-command could (but should not) cause\n> ambiguities between a list and configuration value.\n\nBecause there is no sectionless variable, there exists no ambiguity.\n\nIn other words,\n\n    $ git config list\n\ncannot be asking for the value of the variable \"list\", because such\na variable cannot exist.  When you start your command with\n\n    $ git config list ...\n\nwhatever other tokens follow that token \"list\" on the command line,\nit is a \"--list\" subcommand.  A traditional and lazy \"get a single\nvariable\" without an explicit subcommand would look like\n\n    $ git config section.variable\n\nthat is a short-hand to\n\n    $ git config --get section.variable\n\nand would become\n\n    $ git config get section.variable\n\nin the new world order.\n\nThanks.\n"},{"id":"494013","messageId":"xmqqbk5mandn.fsf@gitster.g","threadId":"61061","inReplyTo":"cover.1714730169.git.ps@pks.im","subject":"Re: [PATCH v4 00/14] builtin/config: introduce subcommands","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-03T16:09:08Z","receivedAt":"2024-05-03T16:09:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> this is the fourth version of my patch series that introduces\n> subcommands for git-config(1).\n>\n> Changes compared to v3:\n>\n>     - Rebased on top of d4cc1ec35f (Start the 2.46 cycle, 2024-04-30).\n>\n>     - Implemented support for `git config set --comment`. This switch\n>       has been added since the last version of this patch series.\n>\n> Here's hoping that there's a bit more interest in this patch series at\n> the beginning of the release cycle :)\n\nAs you made the cover letter of v4 a response to the cover letter of\nv1, and because the primary motivation of the series has not changed\nsince the very beginning, it was very easy to find the cover letter\nof the original series that outlined the motivation (including why\nit does not risk ambiguity) quite well.  People cannot go directly\nback to v2 and v3 with the information alone, but at that point, we\ncan visit https://lore.kernel.org/git/cover.1709724089.git.ps@pks.im/\nand everything is there on one page.\n\nThanks for not forgetting the \"comment\" thing that was added since\nthe last round.\n"},{"id":"494121","messageId":"ZjiL7vu5kCVwpsLd@tanuki","threadId":"61061","inReplyTo":"xmqqjzkaanr1.fsf@gitster.g","subject":"Re: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T07:51:10Z","receivedAt":"2024-05-06T07:51:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, May 03, 2024 at 09:01:06AM -0700, Junio C Hamano wrote:\n> <rsbecker@nexbridge.com> writes:\n> \n> >>> +DEPRECATED MODES\n> >>> +----------------\n> >>> +\n> >>> +The following modes have been deprecated in favor of subcommands. It\n> >>> +is recommended to migrate to the new syntax.\n> >>> +\n> >>> +-l::\n> >>> +--list::\n> >>> +\tReplaced by `git config list`.\n> \n> Can you trim quotes in your response that has no relevance to what\n> you are responding to?\n> \n> > Removing the --list option is going to break backward\n> > compatibility for users who script the use of config for things\n> > like setup, clone automation, etc.\n> \n> Yes, that is why this is merely a deprecation.\n\nI will probably send a follow-up patch series that converts other tests\nto use the new syntax and that does some more cleanups. I was pondering\nwhether we want to introduce a document as part of that patch series\nthat starts to keep track of upcoming removals for a potential Git 3.0\nrelease.\n\nThere are multiple items that could be added:\n\n  - Removal of the old syntax of git-config(1).\n\n  - Removal of the dumb HTTP transport.\n\n  - Removal of `info/grafts`.\n\nThere are probably other items.\n\nIn any case, the old actions are here to stay for the foreseeable future\nuntil we commit to a breaking major release.\n\n> > Adding list as a sub-command could (but should not) cause\n> > ambiguities between a list and configuration value.\n> \n> Because there is no sectionless variable, there exists no ambiguity.\n> \n> In other words,\n> \n>     $ git config list\n> \n> cannot be asking for the value of the variable \"list\", because such\n> a variable cannot exist.  When you start your command with\n> \n>     $ git config list ...\n> \n> whatever other tokens follow that token \"list\" on the command line,\n> it is a \"--list\" subcommand.  A traditional and lazy \"get a single\n> variable\" without an explicit subcommand would look like\n> \n>     $ git config section.variable\n> \n> that is a short-hand to\n> \n>     $ git config --get section.variable\n> \n> and would become\n> \n>     $ git config get section.variable\n> \n> in the new world order.\n\nThanks for the thorough explanation, I have nothing to add!\n\nPatrick\n"},{"id":"494122","messageId":"ZjiNr-PUue2_mJGr@tanuki","threadId":"61061","inReplyTo":"CAOLa=ZSNbZPByO9QyeAGaR1pWXMB7ge_GF7M5fydxP-cse-X3g@mail.gmail.com","subject":"Re: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T07:58:39Z","receivedAt":"2024-05-06T07:58:46Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, May 03, 2024 at 06:08:57AM -0700, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > While git-config(1) has several modes, those modes are not exposed with\n> > subcommands but instead by specifying e.g. `--unset` or `--list`. This\n> \n> s/specifying/specifying flags/ perhaps?\n> \n> > user interface is not really in line with how our more modern commands\n> > work, where it is a lot more customary to say e.g. `git remote list`.\n> \n> Tangent: I totally agree with the patch, but it would be nice to have a\n> 'DesigningCommands' document which would highlight UX do's and don'ts.\n> It would be nice to add that as reference in discussions.\n\nI agree that we should have that discussion, but feel like it should be\npart of a separate patch series. If I were to adopt that into this\nseries I very much think that the resulting discussions would take quite\na while to settle.\n\nI'll start the discussion soonish in a separate thread.\n\n[snip]\n> > @@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n> >  \ttest_cmp expect actual &&\n> >\n> >  \tgit config --file=config --fixed-value --replace-all fixed.test bogus \"$META\" &&\n> > -\tgit config --file=config --list >actual &&\n> > +\tgit config ${mode_prefix}list --file=config >actual &&\n> >  \tcat >expect <<-EOF &&\n> >  \tfixed.test=bogus\n> >  \tfixed.test=bogus\n> > @@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes failure' '\n> >  \ttest_cmp expect err\n> >  '\n> >\n> > +done\n> > +\n> >\n> Nit: Wouldn't it be better if the tests are indented here? That way you\n> know it's part of a loop.\n\nWe would basically have to reindent 2500 lines of code. I don't think\nthat'd be a helpful to reviewers :)\n\nPatrick\n"},{"id":"494123","messageId":"cover.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1709724089.git.ps@pks.im","subject":"[PATCH v5 00/14] builtin/config: introduce subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:55:51Z","receivedAt":"2024-05-06T08:55:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the fifth and hopefully last version of my patch sthat\nintroduces subcommands into git-config(1).\n\nThe only changes compared to v4 are some fixes to commit messages.\nOtherwise I'm not aware of any other feedback that would need to be\naddressed.\n\nPatrick\n\nPatrick Steinhardt (14):\n  config: clarify memory ownership when preparing comment strings\n  builtin/config: move option array around\n  builtin/config: move \"fixed-value\" option to correct group\n  builtin/config: use `OPT_CMDMODE()` to specify modes\n  builtin/config: pull out function to handle config location\n  builtin/config: pull out function to handle `--null`\n  builtin/config: introduce \"list\" subcommand\n  builtin/config: introduce \"get\" subcommand\n  builtin/config: introduce \"set\" subcommand\n  builtin/config: introduce \"unset\" subcommand\n  builtin/config: introduce \"rename-section\" subcommand\n  builtin/config: introduce \"remove-section\" subcommand\n  builtin/config: introduce \"edit\" subcommand\n  builtin/config: display subcommand help\n\n Documentation/git-config.txt | 219 ++++++++-------\n builtin/config.c             | 512 ++++++++++++++++++++++++++++-------\n config.c                     |  16 +-\n config.h                     |   2 +-\n t/t0450/txt-help-mismatches  |   1 -\n t/t1300-config.sh            | 432 +++++++++++++++++------------\n 6 files changed, 812 insertions(+), 370 deletions(-)\n\nRange-diff against v4:\n 1:  3aa26d5bff !  1:  881d2b5426 config: clarify memory ownership when preparing comment strings\n    @@ Commit message\n         not like this micro-optimization really matters. Thus, callers are now\n         always responsible for freeing the value.\n     \n    +    Signed-off-by: Patrick Steinhardt <ps@pks.im>\n    +\n      ## builtin/config.c ##\n     @@ builtin/config.c: static struct config_options config_options;\n      static int show_origin;\n 2:  8f0804ab48 =  2:  66dffaa8f2 builtin/config: move option array around\n 3:  ddcd8031d7 !  3:  36abda0e02 builtin/config: move \"fixed-value\" option to correct group\n    @@ Commit message\n         builtin/config: move \"fixed-value\" option to correct group\n     \n         The `--fixed-value` option can be used to alter how the value-pattern\n    -    parameter is interpreted for the various submodes of git-config(1). But\n    -    while it is an option, it is currently listed as part of the submodes\n    -    group the command, which is wrong.\n    +    parameter is interpreted for the various actions of git-config(1). But\n    +    while it is an option, it is currently listed as part of the actions\n    +    group, which is wrong.\n     \n         Move the option to the \"Other\" group, which hosts the various options\n         known to git-config(1).\n 4:  1bc3918840 =  4:  34b66f9c87 builtin/config: use `OPT_CMDMODE()` to specify modes\n 5:  3754812309 =  5:  4f90f206e7 builtin/config: pull out function to handle config location\n 6:  cb1714c493 =  6:  df1a6f14e6 builtin/config: pull out function to handle `--null`\n 7:  b3f3c3ba6a !  7:  1df76a9970 builtin/config: introduce \"list\" subcommand\n    @@ Commit message\n         builtin/config: introduce \"list\" subcommand\n     \n         While git-config(1) has several modes, those modes are not exposed with\n    -    subcommands but instead by specifying e.g. `--unset` or `--list`. This\n    -    user interface is not really in line with how our more modern commands\n    -    work, where it is a lot more customary to say e.g. `git remote list`.\n    -    Furthermore, to add to the confusion, git-config(1) also allows the user\n    -    to request modes implicitly by just specifying the correct number of\n    -    arguments. Thus, `git config foo.bar` will retrieve the value of\n    -    \"foo.bar\" while `git config foo.bar baz` will set it to \"baz\".\n    +    subcommands but instead by specifying action flags like `--unset` or\n    +    `--list`. This user interface is not really in line with how our more\n    +    modern commands work, where it is a lot more customary to say e.g. `git\n    +    remote list`. Furthermore, to add to the confusion, git-config(1) also\n    +    allows the user to request modes implicitly by just specifying the\n    +    correct number of arguments. Thus, `git config foo.bar` will retrieve\n    +    the value of \"foo.bar\" while `git config foo.bar baz` will set it to\n    +    \"baz\".\n     \n         Overall, this makes for a confusing interface that could really use a\n         makeover. It hurts discoverability of what you can do with git-config(1)\n 8:  0e6da909ac =  8:  29676b81e0 builtin/config: introduce \"get\" subcommand\n 9:  8a623a31b9 =  9:  94afb5a5b7 builtin/config: introduce \"set\" subcommand\n10:  e25e5b69cd = 10:  e525c2326a builtin/config: introduce \"unset\" subcommand\n11:  f24008d356 = 11:  a797889890 builtin/config: introduce \"rename-section\" subcommand\n12:  fc2ddd3201 = 12:  8ec214755e builtin/config: introduce \"remove-section\" subcommand\n13:  4c2d817eff = 13:  1893c23afc builtin/config: introduce \"edit\" subcommand\n14:  4c351b12b8 = 14:  97a48ab81d builtin/config: display subcommand help\n\nbase-commit: d4cc1ec35f3bcce816b69986ca41943f6ce21377\n-- \n2.45.0\n\n"},{"id":"494124","messageId":"881d2b5426e2ee690e5ef37cd795b7141064d05f.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 01/14] config: clarify memory ownership when preparing comment strings","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:55:56Z","receivedAt":"2024-05-06T08:56:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The ownership of memory returned when preparing a comment string is\nquite intricate: when the returned value is different than the passed\nvalue, then the caller is responsible to free the memory. This is quite\nsubtle, and it's even easier to miss because the returned value is in\nfact a `const char *`.\n\nAdapt the function to always return either `NULL` or a newly allocated\nstring. The function is called at most once per git-config(1), so it's\nnot like this micro-optimization really matters. Thus, callers are now\nalways responsible for freeing the value.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 11 ++++++-----\n config.c         | 16 ++++++----------\n config.h         |  2 +-\n 3 files changed, 13 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 0015620dde..40456c0770 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -44,7 +44,7 @@ static struct config_options config_options;\n static int show_origin;\n static int show_scope;\n static int fixed_value;\n-static const char *comment;\n+static const char *comment_arg;\n \n #define ACTION_GET (1<<0)\n #define ACTION_GET_ALL (1<<1)\n@@ -174,7 +174,7 @@ static struct option builtin_config_options[] = {\n \tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_STRING(0, \"comment\", &comment, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n+\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n \tOPT_END(),\n };\n \n@@ -674,7 +674,7 @@ static char *default_user_config(void)\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tint nongit = !startup_info->have_repository;\n-\tchar *value = NULL;\n+\tchar *value = NULL, *comment = NULL;\n \tint flags = 0;\n \tint ret = 0;\n \tstruct key_value_info default_kvi = KVI_INIT;\n@@ -799,7 +799,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (comment &&\n+\tif (comment_arg &&\n \t    !(actions & (ACTION_ADD|ACTION_SET|ACTION_SET_ALL|ACTION_REPLACE_ALL))) {\n \t\terror(_(\"--comment is only applicable to add/set/replace operations\"));\n \t\tusage_builtin_config();\n@@ -841,7 +841,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tflags |= CONFIG_FLAGS_FIXED_VALUE;\n \t}\n \n-\tcomment = git_config_prepare_comment_string(comment);\n+\tcomment = git_config_prepare_comment_string(comment_arg);\n \n \tif (actions & PAGING_ACTIONS)\n \t\tsetup_auto_pager(\"config\", 1);\n@@ -993,6 +993,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\treturn get_colorbool(argv[0], argc == 2);\n \t}\n \n+\tfree(comment);\n \tfree(value);\n \treturn ret;\n }\ndiff --git a/config.c b/config.c\nindex ae3652b08f..13cf9eeb16 100644\n--- a/config.c\n+++ b/config.c\n@@ -3182,14 +3182,10 @@ void git_config_set(const char *key, const char *value)\n \ttrace2_cmd_set_config(key, value);\n }\n \n-/*\n- * The ownership rule is that the caller will own the string\n- * if it receives a piece of memory different from what it passed\n- * as the parameter.\n- */\n-const char *git_config_prepare_comment_string(const char *comment)\n+char *git_config_prepare_comment_string(const char *comment)\n {\n \tsize_t leading_blanks;\n+\tchar *prepared;\n \n \tif (!comment)\n \t\treturn NULL;\n@@ -3210,13 +3206,13 @@ const char *git_config_prepare_comment_string(const char *comment)\n \n \tleading_blanks = strspn(comment, \" \\t\");\n \tif (leading_blanks && comment[leading_blanks] == '#')\n-\t\t; /* use it as-is */\n+\t\tprepared = xstrdup(comment); /* use it as-is */\n \telse if (comment[0] == '#')\n-\t\tcomment = xstrfmt(\" %s\", comment);\n+\t\tprepared = xstrfmt(\" %s\", comment);\n \telse\n-\t\tcomment = xstrfmt(\" # %s\", comment);\n+\t\tprepared = xstrfmt(\" # %s\", comment);\n \n-\treturn comment;\n+\treturn prepared;\n }\n \n static void validate_comment_string(const char *comment)\ndiff --git a/config.h b/config.h\nindex f4966e3749..db8b608064 100644\n--- a/config.h\n+++ b/config.h\n@@ -338,7 +338,7 @@ void git_config_set_multivar(const char *, const char *, const char *, unsigned)\n int repo_config_set_multivar_gently(struct repository *, const char *, const char *, const char *, unsigned);\n int git_config_set_multivar_in_file_gently(const char *, const char *, const char *, const char *, const char *, unsigned);\n \n-const char *git_config_prepare_comment_string(const char *);\n+char *git_config_prepare_comment_string(const char *);\n \n /**\n  * takes four parameters:\n-- \n2.45.0\n\n"},{"id":"494125","messageId":"66dffaa8f2e4c717ca08123bad70093b25360a2a.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 02/14] builtin/config: move option array around","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:00Z","receivedAt":"2024-05-06T08:56:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move around the option array. This will help us with a follow-up commit\nthat introduces subcommands to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 96 ++++++++++++++++++++++++------------------------\n 1 file changed, 48 insertions(+), 48 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 40456c0770..59ae5996eb 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -135,54 +135,6 @@ static int option_parse_type(const struct option *opt, const char *arg,\n \treturn 0;\n }\n \n-static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n-\tOPT_GROUP(N_(\"Action\")),\n-\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n-\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n-\tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n-\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n-\tOPT_END(),\n-};\n-\n-static NORETURN void usage_builtin_config(void)\n-{\n-\tusage_with_options(builtin_config_usage, builtin_config_options);\n-}\n-\n static void check_argc(int argc, int min, int max)\n {\n \tif (argc >= min && argc <= max)\n@@ -671,6 +623,54 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n+static struct option builtin_config_options[] = {\n+\tOPT_GROUP(N_(\"Config file location\")),\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tOPT_GROUP(N_(\"Action\")),\n+\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n+\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n+\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n+\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n+\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n+\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n+\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n+\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_GROUP(N_(\"Type\")),\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tOPT_GROUP(N_(\"Other\")),\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n+\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n+\tOPT_END(),\n+};\n+\n+static NORETURN void usage_builtin_config(void)\n+{\n+\tusage_with_options(builtin_config_usage, builtin_config_options);\n+}\n+\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tint nongit = !startup_info->have_repository;\n-- \n2.45.0\n\n"},{"id":"494126","messageId":"36abda0e02016eac027f4fb59cd912f2c5b60e05.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 03/14] builtin/config: move \"fixed-value\" option to correct group","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:05Z","receivedAt":"2024-05-06T08:56:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `--fixed-value` option can be used to alter how the value-pattern\nparameter is interpreted for the various actions of git-config(1). But\nwhile it is an option, it is currently listed as part of the actions\ngroup, which is wrong.\n\nMove the option to the \"Other\" group, which hosts the various options\nknown to git-config(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 59ae5996eb..054019b70c 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -643,7 +643,6 @@ static struct option builtin_config_options[] = {\n \tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n \tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n \tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n \tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n \tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n \tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n@@ -663,6 +662,7 @@ static struct option builtin_config_options[] = {\n \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n \tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n \tOPT_END(),\n };\n \n-- \n2.45.0\n\n"},{"id":"494127","messageId":"34b66f9c8754e4cd8df934aa4d6ffdc51129e402.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 04/14] builtin/config: use `OPT_CMDMODE()` to specify modes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:10Z","receivedAt":"2024-05-06T08:56:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The git-config(1) command has various different modes which are\naccessible via e.g. `--get-urlmatch` or `--unset-all`. These modes are\ndeclared with `OPT_BIT()`, which causes two minor issues:\n\n  - The respective modes also have a negated form `--no-get-urlmatch`,\n    which is unintended.\n\n  - We have to manually handle exclusiveness of the modes.\n\nSwitch these options to instead use `OPT_CMDMODE()`, which is made\nexactly for this usecase. Remove the now-unneeded check that only a\nsingle mode is given, which is now handled by the parse-options\ninterface.\n\nWhile at it, format optional placeholders for arguments to conform to\nour style guidelines by using `[<placeholder>]`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c  | 32 ++++++++++++++------------------\n t/t1300-config.sh | 13 +++++++++++++\n 2 files changed, 27 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 054019b70c..8991533e08 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -632,20 +632,20 @@ static struct option builtin_config_options[] = {\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n \tOPT_GROUP(N_(\"Action\")),\n-\tOPT_BIT(0, \"get\", &actions, N_(\"get value: name [value-pattern]\"), ACTION_GET),\n-\tOPT_BIT(0, \"get-all\", &actions, N_(\"get all values: key [value-pattern]\"), ACTION_GET_ALL),\n-\tOPT_BIT(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [value-pattern]\"), ACTION_GET_REGEXP),\n-\tOPT_BIT(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_BIT(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [value-pattern]\"), ACTION_REPLACE_ALL),\n-\tOPT_BIT(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_BIT(0, \"unset\", &actions, N_(\"remove a variable: name [value-pattern]\"), ACTION_UNSET),\n-\tOPT_BIT(0, \"unset-all\", &actions, N_(\"remove all matches: name [value-pattern]\"), ACTION_UNSET_ALL),\n-\tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n-\tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n+\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n+\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n+\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n+\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n+\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n+\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n+\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n+\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n \tOPT_GROUP(N_(\"Type\")),\n \tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n \tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n@@ -769,10 +769,6 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (HAS_MULTI_BITS(actions)) {\n-\t\terror(_(\"only one action at a time\"));\n-\t\tusage_builtin_config();\n-\t}\n \tif (actions == 0)\n \t\tswitch (argc) {\n \t\tcase 1: actions = ACTION_GET; break;\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 9b65d9eaf5..86dc70769a 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -2738,4 +2738,17 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such\n \tgrep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n '\n \n+test_expect_success 'negated mode causes failure' '\n+\ttest_must_fail git config --no-get 2>err &&\n+\tgrep \"unknown option \\`no-get${SQ}\" err\n+'\n+\n+test_expect_success 'specifying multiple modes causes failure' '\n+\tcat >expect <<-EOF &&\n+\terror: options ${SQ}--get-all${SQ} and ${SQ}--get${SQ} cannot be used together\n+\tEOF\n+\ttest_must_fail git config --get --get-all 2>err &&\n+\ttest_cmp expect err\n+'\n+\n test_done\n-- \n2.45.0\n\n"},{"id":"494128","messageId":"4f90f206e70ca86f26a96c2db7cffd0e97a8c39a.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 05/14] builtin/config: pull out function to handle config location","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:14Z","receivedAt":"2024-05-06T08:56:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"There's quite a bunch of options to git-config(1) that allow the user to\nspecify which config location to use when reading or writing config\noptions. The logic to handle this is thus by necessity also quite\ninvolved.\n\nPull it out into a separate function so that we can reuse it in\nsubsequent commits which introduce proper subcommands.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 133 ++++++++++++++++++++++++-----------------------\n 1 file changed, 68 insertions(+), 65 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 8991533e08..8c7cd30cb4 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -623,68 +623,8 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n-\tOPT_GROUP(N_(\"Action\")),\n-\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n-\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n-\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n-\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n-\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n-\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n-\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n-\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n-\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n-\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n-\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n-\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n-\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n-\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n-\tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n-\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n-\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n-\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n-\tOPT_END(),\n-};\n-\n-static NORETURN void usage_builtin_config(void)\n+static void handle_config_location(const char *prefix)\n {\n-\tusage_with_options(builtin_config_usage, builtin_config_options);\n-}\n-\n-int cmd_config(int argc, const char **argv, const char *prefix)\n-{\n-\tint nongit = !startup_info->have_repository;\n-\tchar *value = NULL, *comment = NULL;\n-\tint flags = 0;\n-\tint ret = 0;\n-\tstruct key_value_info default_kvi = KVI_INIT;\n-\n-\tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n-\n-\targc = parse_options(argc, argv, prefix, builtin_config_options,\n-\t\t\t     builtin_config_usage,\n-\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n-\n \tif (use_global_config + use_system_config + use_local_config +\n \t    use_worktree_config +\n \t    !!given_config_source.file + !!given_config_source.blob > 1) {\n@@ -692,14 +632,13 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tusage_builtin_config();\n \t}\n \n-\tif (nongit) {\n+\tif (!startup_info->have_repository) {\n \t\tif (use_local_config)\n \t\t\tdie(_(\"--local can only be used inside a git repository\"));\n \t\tif (given_config_source.blob)\n \t\t\tdie(_(\"--blob can only be used inside a git repository\"));\n \t\tif (use_worktree_config)\n \t\t\tdie(_(\"--worktree can only be used inside a git repository\"));\n-\n \t}\n \n \tif (given_config_source.file &&\n@@ -753,10 +692,74 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tconfig_options.respect_includes = !given_config_source.file;\n \telse\n \t\tconfig_options.respect_includes = respect_includes_opt;\n-\tif (!nongit) {\n+\tif (startup_info->have_repository) {\n \t\tconfig_options.commondir = get_git_common_dir();\n \t\tconfig_options.git_dir = get_git_dir();\n \t}\n+}\n+\n+static struct option builtin_config_options[] = {\n+\tOPT_GROUP(N_(\"Config file location\")),\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tOPT_GROUP(N_(\"Action\")),\n+\tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n+\tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n+\tOPT_CMDMODE(0, \"get-regexp\", &actions, N_(\"get values for regexp: name-regex [<value-pattern>]\"), ACTION_GET_REGEXP),\n+\tOPT_CMDMODE(0, \"get-urlmatch\", &actions, N_(\"get value specific for the URL: section[.var] URL\"), ACTION_GET_URLMATCH),\n+\tOPT_CMDMODE(0, \"replace-all\", &actions, N_(\"replace all matching variables: name value [<value-pattern>]\"), ACTION_REPLACE_ALL),\n+\tOPT_CMDMODE(0, \"add\", &actions, N_(\"add a new variable: name value\"), ACTION_ADD),\n+\tOPT_CMDMODE(0, \"unset\", &actions, N_(\"remove a variable: name [<value-pattern>]\"), ACTION_UNSET),\n+\tOPT_CMDMODE(0, \"unset-all\", &actions, N_(\"remove all matches: name [<value-pattern>]\"), ACTION_UNSET_ALL),\n+\tOPT_CMDMODE(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n+\tOPT_CMDMODE(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n+\tOPT_CMDMODE('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n+\tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n+\tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n+\tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n+\tOPT_GROUP(N_(\"Type\")),\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tOPT_GROUP(N_(\"Other\")),\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n+\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n+\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n+\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_END(),\n+};\n+\n+static NORETURN void usage_builtin_config(void)\n+{\n+\tusage_with_options(builtin_config_usage, builtin_config_options);\n+}\n+\n+int cmd_config(int argc, const char **argv, const char *prefix)\n+{\n+\tchar *value = NULL, *comment = NULL;\n+\tint flags = 0;\n+\tint ret = 0;\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\n+\tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n+\n+\targc = parse_options(argc, argv, prefix, builtin_config_options,\n+\t\t\t     builtin_config_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\thandle_config_location(prefix);\n \n \tif (end_nul) {\n \t\tterm = '\\0';\n@@ -858,7 +861,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\tchar *config_file;\n \n \t\tcheck_argc(argc, 0, 0);\n-\t\tif (!given_config_source.file && nongit)\n+\t\tif (!given_config_source.file && !startup_info->have_repository)\n \t\t\tdie(_(\"not in a git directory\"));\n \t\tif (given_config_source.use_stdin)\n \t\t\tdie(_(\"editing stdin is not supported\"));\n-- \n2.45.0\n\n"},{"id":"494129","messageId":"df1a6f14e6a267b8867d9784531ec8327a170d6d.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 06/14] builtin/config: pull out function to handle `--null`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:19Z","receivedAt":"2024-05-06T08:56:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Pull out function to handle the `--null` option, which we are about to\nreuse in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c | 15 +++++++++------\n 1 file changed, 9 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 8c7cd30cb4..59877065f8 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -698,6 +698,14 @@ static void handle_config_location(const char *prefix)\n \t}\n }\n \n+static void handle_nul(void) {\n+\tif (end_nul) {\n+\t\tterm = '\\0';\n+\t\tdelim = '\\n';\n+\t\tkey_delim = '\\n';\n+\t}\n+}\n+\n static struct option builtin_config_options[] = {\n \tOPT_GROUP(N_(\"Config file location\")),\n \tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n@@ -760,12 +768,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n \thandle_config_location(prefix);\n-\n-\tif (end_nul) {\n-\t\tterm = '\\0';\n-\t\tdelim = '\\n';\n-\t\tkey_delim = '\\n';\n-\t}\n+\thandle_nul();\n \n \tif ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) {\n \t\terror(_(\"--get-color and variable type are incoherent\"));\n-- \n2.45.0\n\n"},{"id":"494130","messageId":"1df76a99704fec6d5306db73744007278e97157b.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:24Z","receivedAt":"2024-05-06T08:56:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-config(1) has several modes, those modes are not exposed with\nsubcommands but instead by specifying action flags like `--unset` or\n`--list`. This user interface is not really in line with how our more\nmodern commands work, where it is a lot more customary to say e.g. `git\nremote list`. Furthermore, to add to the confusion, git-config(1) also\nallows the user to request modes implicitly by just specifying the\ncorrect number of arguments. Thus, `git config foo.bar` will retrieve\nthe value of \"foo.bar\" while `git config foo.bar baz` will set it to\n\"baz\".\n\nOverall, this makes for a confusing interface that could really use a\nmakeover. It hurts discoverability of what you can do with git-config(1)\nand is comparatively easy to get wrong. Converting the command to have\nsubcommands instead would go a long way to help address these issues.\n\nOne concern in this context is backwards compatibility. Luckily, we can\nintroduce subcommands without breaking backwards compatibility at all.\nThis is because all the implicit modes of git-config(1) require that the\nfirst argument is a properly formatted config key. And as config keys\n_must_ have a dot in their name, any value without a dot would have been\ndiscarded by git-config(1) previous to this change. Thus, given that\nnone of the subcommands do have a dot, they are unambiguous.\n\nIntroduce the first such new subcommand, which is \"git config list\". To\nretain backwards compatibility we only conditionally use subcommands and\nwill fall back to the old syntax in case no subcommand was detected.\nThis should help to transition to the new-style syntax until we\neventually deprecate and remove the old-style syntax.\n\nNote that the way we handle this we're duplicating some functionality\nacross old and new syntax. While this isn't pretty, it helps us to\nensure that there really is no change in behaviour for the old syntax.\n\nAmend tests such that we run them both with old and new style syntax.\nAs tests are now run twice, state from the first run may be still be\naround in the second run and thus cause tests to fail. Add cleanup logic\nas required to fix such tests.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt |  26 ++++++---\n builtin/config.c             |  90 ++++++++++++++++++++++++----\n t/t1300-config.sh            | 110 +++++++++++++++++++++--------------\n 3 files changed, 162 insertions(+), 64 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex ac61113fcc..c83c97cb7e 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -9,6 +9,7 @@ git-config - Get and set repository or global options\n SYNOPSIS\n --------\n [verse]\n+'git config list' [<file-option>] [<display-option>] [--includes]\n 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>\n 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n@@ -20,7 +21,6 @@ SYNOPSIS\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n-'git config' [<file-option>] [--show-origin] [--show-scope] [-z|--null] [--name-only] -l | --list\n 'git config' [<file-option>] --get-color <name> [<default>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n@@ -74,6 +74,12 @@ On success, the command returns the exit code 0.\n A list of all available configuration variables can be obtained using the\n `git help --config` command.\n \n+COMMANDS\n+--------\n+\n+list::\n+\tList all variables set in config file, along with their values.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -190,10 +196,6 @@ See also <<FILES>>.\n --unset-all::\n \tRemove all lines matching the key from config file.\n \n--l::\n---list::\n-\tList all variables set in config file, along with their values.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -248,7 +250,7 @@ Valid `<type>`'s include:\n \tcontain line breaks.\n \n --name-only::\n-\tOutput only the names of config variables for `--list` or\n+\tOutput only the names of config variables for `list` or\n \t`--get-regexp`.\n \n --show-origin::\n@@ -300,10 +302,20 @@ Valid `<type>`'s include:\n   When using `--get`, and the requested variable is not found, behave as if\n   <value> were the value assigned to that variable.\n \n+DEPRECATED MODES\n+----------------\n+\n+The following modes have been deprecated in favor of subcommands. It is\n+recommended to migrate to the new syntax.\n+\n+-l::\n+--list::\n+\tReplaced by `git config list`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n-using `--list` or any of the `--get-*` which may return multiple results.\n+using `list` or any of the `--get-*` which may return multiple results.\n The default is to use a pager.\n \n [[FILES]]\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 59877065f8..f89ddce8da 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -16,10 +16,16 @@\n #include \"worktree.h\"\n \n static const char *const builtin_config_usage[] = {\n+\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config [<options>]\"),\n \tNULL\n };\n \n+static const char *const builtin_config_list_usage[] = {\n+\tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -33,6 +39,7 @@ static char delim = '=';\n static char key_delim = ' ';\n static char term = '\\n';\n \n+static parse_opt_subcommand_fn *subcommand;\n static int use_global_config, use_system_config, use_local_config;\n static int use_worktree_config;\n static struct git_config_source given_config_source;\n@@ -706,14 +713,24 @@ static void handle_nul(void) {\n \t}\n }\n \n+#define CONFIG_LOCATION_OPTIONS \\\n+\tOPT_GROUP(N_(\"Config file location\")), \\\n+\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")), \\\n+\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")), \\\n+\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")), \\\n+\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")), \\\n+\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")), \\\n+\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\"))\n+\n+#define CONFIG_DISPLAY_OPTIONS \\\n+\tOPT_GROUP(N_(\"Display options\")), \\\n+\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")), \\\n+\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")), \\\n+\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")), \\\n+\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\"))\n+\n static struct option builtin_config_options[] = {\n-\tOPT_GROUP(N_(\"Config file location\")),\n-\tOPT_BOOL(0, \"global\", &use_global_config, N_(\"use global config file\")),\n-\tOPT_BOOL(0, \"system\", &use_system_config, N_(\"use system config file\")),\n-\tOPT_BOOL(0, \"local\", &use_local_config, N_(\"use repository config file\")),\n-\tOPT_BOOL(0, \"worktree\", &use_worktree_config, N_(\"use per-worktree config file\")),\n-\tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")),\n-\tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\")),\n+\tCONFIG_LOCATION_OPTIONS,\n \tOPT_GROUP(N_(\"Action\")),\n \tOPT_CMDMODE(0, \"get\", &actions, N_(\"get value: name [<value-pattern>]\"), ACTION_GET),\n \tOPT_CMDMODE(0, \"get-all\", &actions, N_(\"get all values: key [<value-pattern>]\"), ACTION_GET_ALL),\n@@ -737,15 +754,12 @@ static struct option builtin_config_options[] = {\n \tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n \tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n \tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tCONFIG_DISPLAY_OPTIONS,\n \tOPT_GROUP(N_(\"Other\")),\n-\tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n-\tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n-\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n-\tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n-\tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n \tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n \tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n+\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n \tOPT_END(),\n };\n \n@@ -754,6 +768,42 @@ static NORETURN void usage_builtin_config(void)\n \tusage_with_options(builtin_config_usage, builtin_config_options);\n }\n \n+static int cmd_config_list(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_DISPLAY_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);\n+\tcheck_argc(argc, 0, 0);\n+\n+\thandle_config_location(prefix);\n+\thandle_nul();\n+\n+\tsetup_auto_pager(\"config\", 1);\n+\n+\tif (config_with_options(show_all_config, NULL,\n+\t\t\t\t&given_config_source, the_repository,\n+\t\t\t\t&config_options) < 0) {\n+\t\tif (given_config_source.file)\n+\t\t\tdie_errno(_(\"unable to read config file '%s'\"),\n+\t\t\t\t  given_config_source.file);\n+\t\telse\n+\t\t\tdie(_(\"error processing config file(s)\"));\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static struct option builtin_subcommand_options[] = {\n+\tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n+\tOPT_END(),\n+};\n+\n int cmd_config(int argc, const char **argv, const char *prefix)\n {\n \tchar *value = NULL, *comment = NULL;\n@@ -763,6 +813,22 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \n \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n \n+\t/*\n+\t * This is somewhat hacky: we first parse the command line while\n+\t * keeping all args intact in order to determine whether a subcommand\n+\t * has been specified. If so, we re-parse it a second time, but this\n+\t * time we drop KEEP_ARGV0. This is so that we don't munge the command\n+\t * line in case no subcommand was given, which would otherwise confuse\n+\t * us when parsing the legacy-style modes that don't use subcommands.\n+\t */\n+\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\tif (subcommand) {\n+\t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n+\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\treturn subcommand(argc, argv, prefix);\n+\t}\n+\n \targc = parse_options(argc, argv, prefix, builtin_config_options,\n \t\t\t     builtin_config_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 86dc70769a..f77d2f7847 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -11,6 +11,20 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n+for mode in legacy subcommands\n+do\n+\n+case \"$mode\" in\n+legacy)\n+\tmode_prefix=\"--\"\n+\t;;\n+subcommands)\n+\tmode_prefix=\"\"\n+\t;;\n+*)\n+\tBUG \"unknown mode $mode\";;\n+esac\n+\n test_expect_success 'setup whitespace config' '\n \tsed -e \"s/^|//\" \\\n \t    -e \"s/[$]$//\" \\\n@@ -460,11 +474,11 @@ version.1.2.3eX.alpha=beta\n EOF\n \n test_expect_success 'working --list' '\n-\tgit config --list > output &&\n+\tgit config ${mode_prefix}list > output &&\n \ttest_cmp expect output\n '\n test_expect_success '--list without repo produces empty output' '\n-\tgit --git-dir=nonexistent config --list >output &&\n+\tgit --git-dir=nonexistent config ${mode_prefix}list >output &&\n \ttest_must_be_empty output\n '\n \n@@ -476,7 +490,7 @@ version.1.2.3eX.alpha\n EOF\n \n test_expect_success '--name-only --list' '\n-\tgit config --name-only --list >output &&\n+\tgit config ${mode_prefix}list --name-only >output &&\n \ttest_cmp expect output\n '\n \n@@ -614,17 +628,17 @@ ein.bahn=strasse\n EOF\n \n test_expect_success 'alternative GIT_CONFIG' '\n-\tGIT_CONFIG=other-config git config --list >output &&\n+\tGIT_CONFIG=other-config git config ${mode_prefix}list >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'alternative GIT_CONFIG (--file)' '\n-\tgit config --file other-config --list >output &&\n+\tgit config ${mode_prefix}list --file other-config >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'alternative GIT_CONFIG (--file=-)' '\n-\tgit config --file - --list <other-config >output &&\n+\tgit config ${mode_prefix}list --file - <other-config >output &&\n \ttest_cmp expect output\n '\n \n@@ -637,6 +651,7 @@ test_expect_success 'editing stdin is an error' '\n '\n \n test_expect_success 'refer config from subdirectory' '\n+\ttest_when_finished \"rm -r x\" &&\n \tmkdir x &&\n \ttest_cmp_config -C x strasse --file=../other-config --get ein.bahn\n '\n@@ -847,7 +862,7 @@ test_expect_success 'line number is reported correctly' '\n '\n \n test_expect_success 'invalid stdin config' '\n-\techo \"[broken\" | test_must_fail git config --list --file - >output 2>&1 &&\n+\techo \"[broken\" | test_must_fail git config ${mode_prefix}list --file - >output 2>&1 &&\n \ttest_grep \"bad config line 1 in standard input\" output\n '\n \n@@ -1139,7 +1154,7 @@ section.quotecont=cont;inued\n EOF\n \n test_expect_success 'value continued on next line' '\n-\tgit config --list > result &&\n+\tgit config ${mode_prefix}list > result &&\n \ttest_cmp expect result\n '\n \n@@ -1163,7 +1178,7 @@ Qsection.sub=section.val4\n Qsection.sub=section.val5Q\n EOF\n test_expect_success '--null --list' '\n-\tgit config --null --list >result.raw &&\n+\tgit config ${mode_prefix}list --null >result.raw &&\n \tnul_to_q <result.raw >result &&\n \techo >>result &&\n \ttest_cmp expect result\n@@ -1198,6 +1213,7 @@ test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces'\n '\n \n test_expect_success SYMLINKS 'symlinked configuration' '\n+\ttest_when_finished \"rm myconfig\" &&\n \tln -s notyet myconfig &&\n \tgit config --file=myconfig test.frotz nitfol &&\n \ttest -h myconfig &&\n@@ -1218,10 +1234,11 @@ test_expect_success SYMLINKS 'symlinked configuration' '\n '\n \n test_expect_success SYMLINKS 'symlink to nonexistent configuration' '\n+\ttest_when_finished \"rm linktonada linktolinktonada\" &&\n \tln -s doesnotexist linktonada &&\n \tln -s linktonada linktolinktonada &&\n-\ttest_must_fail git config --file=linktonada --list &&\n-\ttest_must_fail git config --file=linktolinktonada --list\n+\ttest_must_fail git config ${mode_prefix}list --file=linktonada &&\n+\ttest_must_fail git config ${mode_prefix}list --file=linktolinktonada\n '\n \n test_expect_success 'check split_cmdline return' '\n@@ -1478,7 +1495,7 @@ do\n done\n \n test_expect_success 'git -c is not confused by empty environment' '\n-\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config --list\n+\tGIT_CONFIG_PARAMETERS=\"\" git -c x.one=1 config ${mode_prefix}list\n '\n \n test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n@@ -1669,31 +1686,31 @@ test_expect_success 'git config ignores pairs with empty count' '\n '\n \n test_expect_success 'git config fails with invalid count' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&\n+\ttest_must_fail env GIT_CONFIG_COUNT=10a git config ${mode_prefix}list 2>error &&\n \ttest_grep \"bogus count\" error &&\n-\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&\n+\ttest_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config ${mode_prefix}list 2>error &&\n \ttest_grep \"too many entries\" error\n '\n \n test_expect_success 'git config fails with missing config key' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config --list 2>error &&\n+\t\tgit config ${mode_prefix}list 2>error &&\n \ttest_grep \"missing config key\" error\n '\n \n test_expect_success 'git config fails with missing config value' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=\"pair.one\" \\\n-\t\tgit config --list 2>error &&\n+\t\tgit config ${mode_prefix}list 2>error &&\n \ttest_grep \"missing config value\" error\n '\n \n test_expect_success 'git config fails with invalid config pair key' '\n \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \\\n-\t\tgit config --list &&\n+\t\tgit config ${mode_prefix}list &&\n \ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \\\n-\t\tgit config --list\n+\t\tgit config ${mode_prefix}list\n '\n \n test_expect_success 'environment overrides config file' '\n@@ -1733,7 +1750,7 @@ test_expect_success 'git config --edit works' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n \tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n-\tgit config -f tmp --list >actual &&\n+\tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1742,7 +1759,7 @@ test_expect_success 'git config --edit respects core.editor' '\n \techo test.value=yes >expect &&\n \ttest_config core.editor \"echo [test]value=yes >\" &&\n \tgit config -f tmp --edit &&\n-\tgit config -f tmp --list >actual &&\n+\tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -2093,7 +2110,7 @@ test_expect_success '--show-origin with --list' '\n \tcommand line:\tuser.cmdline=true\n \tEOF\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\\\n-\t\tgit -c user.cmdline=true config --list --show-origin >output &&\n+\t\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2110,7 +2127,7 @@ test_expect_success '--show-origin with --list --null' '\n \tincludeQcommand line:Quser.cmdline\n \ttrueQ\n \tEOF\n-\tgit -c user.cmdline=true config --null --list --show-origin >output.raw &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --null --show-origin >output.raw &&\n \tnul_to_q <output.raw >output &&\n \t# The here-doc above adds a newline that the --null output would not\n \t# include. Add it here to make the two comparable.\n@@ -2124,7 +2141,7 @@ test_expect_success '--show-origin with single file' '\n \tfile:.git/config\tuser.override=local\n \tfile:.git/config\tinclude.path=../include/relative.include\n \tEOF\n-\tgit config --local --list --show-origin >output &&\n+\tgit config ${mode_prefix}list --local --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2162,7 +2179,7 @@ test_expect_success !MINGW '--show-origin escape special file name characters' '\n \tcat >expect <<-\\EOF &&\n \tfile:\"file\\\" (dq) and spaces.conf\"\tuser.custom=true\n \tEOF\n-\tgit config --file \"$WEIRDLY_NAMED_FILE\" --show-origin --list >output &&\n+\tgit config ${mode_prefix}list --file \"$WEIRDLY_NAMED_FILE\" --show-origin >output &&\n \ttest_cmp expect output\n '\n \n@@ -2170,7 +2187,7 @@ test_expect_success '--show-origin stdin' '\n \tcat >expect <<-\\EOF &&\n \tstandard input:\tuser.custom=true\n \tEOF\n-\tgit config --file - --show-origin --list <\"$CUSTOM_CONFIG_FILE\" >output &&\n+\tgit config ${mode_prefix}list --file - --show-origin <\"$CUSTOM_CONFIG_FILE\" >output &&\n \ttest_cmp expect output\n '\n \n@@ -2197,7 +2214,7 @@ test_expect_success '--show-origin blob' '\n \t\tcat >expect <<-EOF &&\n \t\tblob:$blob\tuser.custom=true\n \t\tEOF\n-\t\tgit config --blob=$blob --show-origin --list >output &&\n+\t\tgit config ${mode_prefix}list --blob=$blob --show-origin >output &&\n \t\ttest_cmp expect output\n \t)\n '\n@@ -2213,7 +2230,7 @@ test_expect_success '--show-origin blob ref' '\n \t\tcp \"$CUSTOM_CONFIG_FILE\" custom.conf &&\n \t\tgit add custom.conf &&\n \t\tgit commit -m \"new config file\" &&\n-\t\tgit config --blob=main:custom.conf --show-origin --list >output &&\n+\t\tgit config ${mode_prefix}list --blob=main:custom.conf --show-origin >output &&\n \t\ttest_cmp expect output\n \t)\n '\n@@ -2239,13 +2256,14 @@ test_expect_success '--show-scope with --list' '\n \tworktree\tuser.worktree=true\n \tcommand\tuser.cmdline=true\n \tEOF\n+\ttest_when_finished \"git worktree remove wt1\" &&\n \tgit worktree add wt1 &&\n \t# We need these to test for worktree scope, but outside of this\n \t# test, this is just noise\n \ttest_config core.repositoryformatversion 1 &&\n \ttest_config extensions.worktreeConfig true &&\n \tgit config --worktree user.worktree true &&\n-\tgit -c user.cmdline=true config --list --show-scope >output &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2254,7 +2272,7 @@ test_expect_success !MINGW '--show-scope with --blob' '\n \tcat >expect <<-EOF &&\n \tcommand\tuser.custom=true\n \tEOF\n-\tgit config --blob=$blob --show-scope --list >output &&\n+\tgit config ${mode_prefix}list --blob=$blob --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2264,7 +2282,7 @@ test_expect_success '--show-scope with --local' '\n \tlocal\tuser.override=local\n \tlocal\tinclude.path=../include/relative.include\n \tEOF\n-\tgit config --local --list --show-scope >output &&\n+\tgit config ${mode_prefix}list --local --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2288,7 +2306,7 @@ test_expect_success '--show-scope with --show-origin' '\n \tlocal\tfile:.git/../include/relative.include\tuser.relative=include\n \tcommand\tcommand line:\tuser.cmdline=true\n \tEOF\n-\tgit -c user.cmdline=true config --list --show-origin --show-scope >output &&\n+\tgit -c user.cmdline=true config ${mode_prefix}list --show-origin --show-scope >output &&\n \ttest_cmp expect output\n '\n \n@@ -2329,7 +2347,7 @@ test_expect_success 'override global and system config' '\n \tglobal\thome.config=true\n \tlocal\tlocal.config=true\n \tEOF\n-\tgit config --show-scope --list >output &&\n+\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output &&\n \n \tcat >expect <<-EOF &&\n@@ -2338,20 +2356,20 @@ test_expect_success 'override global and system config' '\n \tlocal\tlocal.config=true\n \tEOF\n \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=custom-system-config GIT_CONFIG_GLOBAL=custom-global-config \\\n-\t\tgit config --show-scope --list >output &&\n+\t\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output &&\n \n \tcat >expect <<-EOF &&\n \tlocal\tlocal.config=true\n \tEOF\n \tGIT_CONFIG_NOSYSTEM=false GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_GLOBAL=/dev/null \\\n-\t\tgit config --show-scope --list >output &&\n+\t\tgit config ${mode_prefix}list --show-scope >output &&\n \ttest_cmp expect output\n '\n \n test_expect_success 'override global and system config with missing file' '\n-\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config --global --list &&\n-\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config --system --list &&\n+\ttest_must_fail env GIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=/dev/null git config ${mode_prefix}list --global &&\n+\ttest_must_fail env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=does-not-exist git config ${mode_prefix}list --system &&\n \tGIT_CONFIG_GLOBAL=does-not-exist GIT_CONFIG_SYSTEM=does-not-exist git version\n '\n \n@@ -2478,7 +2496,7 @@ test_expect_success 'set all config with value-pattern' '\n \t# no match => add new entry\n \tcp initial config &&\n \tgit config --file=config abc.key two a+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2491,7 +2509,7 @@ test_expect_success 'set all config with value-pattern' '\n \n \t# multiple values, no match => add\n \tgit config --file=config abc.key three a+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2501,7 +2519,7 @@ test_expect_success 'set all config with value-pattern' '\n \n \t# single match => replace\n \tgit config --file=config abc.key four h+ &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=one\n \tabc.key=two\n@@ -2516,7 +2534,7 @@ test_expect_success '--replace-all and value-pattern' '\n \tgit config --file=config --add abc.key two &&\n \tgit config --file=config --add abc.key three &&\n \tgit config --file=config --replace-all abc.key four \"o+\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tabc.key=four\n \tabc.key=three\n@@ -2534,7 +2552,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --rename-section dev null &&\n \ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n-\ttest_must_fail git config --file=config --fixed-value --list &&\n+\ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n \n@@ -2555,7 +2573,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n \tfixed.test=bogus\n@@ -2564,7 +2582,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config --fixed-value fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-\\EOF &&\n \tfixed.test=bogus\n \tEOF\n@@ -2582,7 +2600,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \n \tcp initial config &&\n \tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n \tfixed.test=bogus\n@@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \ttest_cmp expect actual &&\n \n \tgit config --file=config --fixed-value --replace-all fixed.test bogus \"$META\" &&\n-\tgit config --file=config --list >actual &&\n+\tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=bogus\n \tfixed.test=bogus\n@@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes failure' '\n \ttest_cmp expect err\n '\n \n+done\n+\n test_done\n-- \n2.45.0\n\n"},{"id":"494131","messageId":"29676b81e0151e2982326663f8b64e2a70bec0f8.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 08/14] builtin/config: introduce \"get\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:29Z","receivedAt":"2024-05-06T08:56:34Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"get\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt |  93 ++++++++++++-------------\n builtin/config.c             |  69 +++++++++++++++---\n t/t1300-config.sh            | 131 ++++++++++++++++++++++++-----------\n 3 files changed, 194 insertions(+), 99 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex c83c97cb7e..d0878663db 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -10,18 +10,14 @@ SYNOPSIS\n --------\n [verse]\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' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n 'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>\n 'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get <name> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] --get-all <name> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [--show-origin] [--show-scope] [-z|--null] [--fixed-value] [--name-only] --get-regexp <name-regex> [<value-pattern>]\n-'git config' [<file-option>] [--type=<type>] [-z|--null] --get-urlmatch <name> <URL>\n 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n-'git config' [<file-option>] --get-color <name> [<default>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n \n@@ -80,6 +76,12 @@ COMMANDS\n list::\n \tList all variables set in config file, along with their values.\n \n+get::\n+\tEmits the value of the specified key. If key is present multiple times\n+\tin the configuration, emits the last value. If `--all` is specified,\n+\temits all values associated with key. Returns error code 1 if key is\n+\tnot present.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -105,22 +107,16 @@ OPTIONS\n \tnot contain linefeed characters (no multi-line comments are\n \tpermitted).\n \n---get::\n-\tGet the value for a given key (optionally filtered by a regex\n-\tmatching the value). Returns error code 1 if the key was not\n-\tfound and the last value if multiple key values were found.\n-\n---get-all::\n-\tLike get, but returns all values for a multi-valued key.\n+--all::\n+\tWith `get`, return all values for a multi-valued key.\n \n---get-regexp::\n-\tLike --get-all, but interprets the name as a regular expression and\n-\twrites out the key names.  Regular expression matching is currently\n-\tcase-sensitive and done against a canonicalized version of the key\n-\tin which section and variable names are lowercased, but subsection\n-\tnames are not.\n+---regexp::\n+\tWith `get`, interpret the name as a regular expression. Regular\n+\texpression matching is currently case-sensitive and done against a\n+\tcanonicalized version of the key in which section and variable names\n+\tare lowercased, but subsection names are not.\n \n---get-urlmatch <name> <URL>::\n+--url=<URL>::\n \tWhen given a two-part <name> as <section>.<key>, the value for\n \t<section>.<URL>.<key> whose <URL> part matches the best to the\n \tgiven URL is returned (if no such key exists, the value for\n@@ -251,7 +247,7 @@ Valid `<type>`'s include:\n \n --name-only::\n \tOutput only the names of config variables for `list` or\n-\t`--get-regexp`.\n+\t`get`.\n \n --show-origin::\n \tAugment the output of all queried config options with the\n@@ -275,17 +271,6 @@ Valid `<type>`'s include:\n \tWhen the color setting for `name` is undefined, the command uses\n \t`color.ui` as fallback.\n \n---get-color <name> [<default>]::\n-\n-\tFind the color configured for `name` (e.g. `color.diff.new`) and\n-\toutput it as the ANSI color escape sequence to the standard\n-\toutput.  The optional `default` parameter is used instead, if\n-\tthere is no color configured for `name`.\n-+\n-`--type=color [--default=<default>]` is preferred over `--get-color`\n-(but note that `--get-color` will omit the trailing newline printed by\n-`--type=color`).\n-\n -e::\n --edit::\n \tOpens an editor to modify the specified config file; either\n@@ -299,7 +284,7 @@ Valid `<type>`'s include:\n \tconfig files.\n \n --default <value>::\n-  When using `--get`, and the requested variable is not found, behave as if\n+  When using `get`, and the requested variable is not found, behave as if\n   <value> were the value assigned to that variable.\n \n DEPRECATED MODES\n@@ -308,15 +293,33 @@ DEPRECATED MODES\n The following modes have been deprecated in favor of subcommands. It is\n recommended to migrate to the new syntax.\n \n+'git config <name>'::\n+\tReplaced by `git config get <name>`.\n+\n -l::\n --list::\n \tReplaced by `git config list`.\n \n+--get <name> [<value-pattern>]::\n+\tReplaced by `git config get [--value=<pattern>] <name>`.\n+\n+--get-all <name> [<value-pattern>]::\n+\tReplaced by `git config get [--value=<pattern>] --all --show-names <name>`.\n+\n+--get-regexp <name-regexp>::\n+\tReplaced by `git config get --all --show-names --regexp <name-regexp>`.\n+\n+--get-urlmatch <name> <URL>::\n+\tReplaced by `git config get --all --show-names --url=<URL> <name>`.\n+\n+--get-color <name> [<default>]::\n+\tReplaced by `git config get --type=color [--default=<default>] <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n-using `list` or any of the `--get-*` which may return multiple results.\n-The default is to use a pager.\n+using `list` or `get` which may return multiple results. The default is to use\n+a pager.\n \n [[FILES]]\n FILES\n@@ -519,25 +522,19 @@ you have to provide a regex matching the value of exactly one line.\n To query the value for a given key, do\n \n ------------\n-% git config --get core.filemode\n-------------\n-\n-or\n-\n-------------\n-% git config core.filemode\n+% git config get core.filemode\n ------------\n \n or, to query a multivar:\n \n ------------\n-% git config --get core.gitproxy \"for kernel.org$\"\n+% git config get --value=\"for kernel.org$\" core.gitproxy\n ------------\n \n If you want to know all the values for a multivar, do:\n \n ------------\n-% git config --get-all core.gitproxy\n+% git config get --all --show-names core.gitproxy\n ------------\n \n If you like to live dangerously, you can replace *all* core.gitproxy by a\n@@ -571,8 +568,8 @@ script:\n \n ------------\n #!/bin/sh\n-WS=$(git config --get-color color.diff.whitespace \"blue reverse\")\n-RESET=$(git config --get-color \"\" \"reset\")\n+WS=$(git config get --type=color --default=\"blue reverse\" color.diff.whitespace)\n+RESET=$(git config get --type=color --default=\"reset\" \"\")\n echo \"${WS}your whitespace color or blue reverse${RESET}\"\n ------------\n \n@@ -580,11 +577,11 @@ For URLs in `https://weak.example.com`, `http.sslVerify` is set to\n false, while it is set to `true` for all others:\n \n ------------\n-% git config --type=bool --get-urlmatch http.sslverify https://good.example.com\n+% git config get --type=bool --url=https://good.example.com http.sslverify\n true\n-% git config --type=bool --get-urlmatch http.sslverify https://weak.example.com\n+% git config get --type=bool --url=https://weak.example.com http.sslverify\n false\n-% git config --get-urlmatch http https://weak.example.com\n+% git config get --url=https://weak.example.com http\n http.cookieFile /tmp/cookie.txt\n http.sslverify false\n ------------\ndiff --git a/builtin/config.c b/builtin/config.c\nindex f89ddce8da..9e7ae49c02 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -17,7 +17,7 @@\n \n static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n-\tN_(\"git config [<options>]\"),\n+\tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tNULL\n };\n \n@@ -26,6 +26,11 @@ static const char *const builtin_config_list_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_get_usage[] = {\n+\tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -722,6 +727,16 @@ static void handle_nul(void) {\n \tOPT_STRING('f', \"file\", &given_config_source.file, N_(\"file\"), N_(\"use given config file\")), \\\n \tOPT_STRING(0, \"blob\", &given_config_source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\"))\n \n+#define CONFIG_TYPE_OPTIONS \\\n+\tOPT_GROUP(N_(\"Type\")), \\\n+\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL), \\\n+\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT), \\\n+\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR), \\\n+\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH), \\\n+\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE)\n+\n #define CONFIG_DISPLAY_OPTIONS \\\n \tOPT_GROUP(N_(\"Display options\")), \\\n \tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")), \\\n@@ -746,14 +761,7 @@ static struct option builtin_config_options[] = {\n \tOPT_CMDMODE('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n \tOPT_CMDMODE(0, \"get-color\", &actions, N_(\"find the color configured: slot [<default>]\"), ACTION_GET_COLOR),\n \tOPT_CMDMODE(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [<stdout-is-tty>]\"), ACTION_GET_COLORBOOL),\n-\tOPT_GROUP(N_(\"Type\")),\n-\tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type),\n-\tOPT_CALLBACK_VALUE(0, \"bool\", &type, N_(\"value is \\\"true\\\" or \\\"false\\\"\"), TYPE_BOOL),\n-\tOPT_CALLBACK_VALUE(0, \"int\", &type, N_(\"value is decimal number\"), TYPE_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-int\", &type, N_(\"value is --bool or --int\"), TYPE_BOOL_OR_INT),\n-\tOPT_CALLBACK_VALUE(0, \"bool-or-str\", &type, N_(\"value is --bool or string\"), TYPE_BOOL_OR_STR),\n-\tOPT_CALLBACK_VALUE(0, \"path\", &type, N_(\"value is a path (file or directory name)\"), TYPE_PATH),\n-\tOPT_CALLBACK_VALUE(0, \"expiry-date\", &type, N_(\"value is an expiry date\"), TYPE_EXPIRY_DATE),\n+\tCONFIG_TYPE_OPTIONS,\n \tCONFIG_DISPLAY_OPTIONS,\n \tOPT_GROUP(N_(\"Other\")),\n \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n@@ -799,8 +807,51 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix)\n \treturn 0;\n }\n \n+static int cmd_config_get(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL, *url = NULL;\n+\tint flags = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_TYPE_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter options\")),\n+\t\tOPT_BOOL(0, \"all\", &do_all, N_(\"return all values for multi-valued config options\")),\n+\t\tOPT_BOOL(0, \"regexp\", &use_key_regexp, N_(\"interpret the name as a regular expression\")),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_STRING(0, \"url\", &url, N_(\"URL\"), N_(\"show config matching the given URL\")),\n+\t\tCONFIG_DISPLAY_OPTIONS,\n+\t\tOPT_BOOL(0, \"show-names\", &show_keys, N_(\"show config keys in addition to their values\")),\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n+\t\tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"use default value when missing entry\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_get_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_argc(argc, 1, 1);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with 'value-pattern'\"));\n+\tif (default_value && (do_all || url))\n+\t\tdie(_(\"--default= cannot be used with --all or --url=\"));\n+\tif (url && (do_all || use_key_regexp || value_pattern))\n+\t\tdie(_(\"--url= cannot be used with --all, --regexp or --value\"));\n+\n+\thandle_config_location(prefix);\n+\thandle_nul();\n+\n+\tsetup_auto_pager(\"config\", 1);\n+\n+\tif (url)\n+\t\treturn get_urlmatch(argv[0], url);\n+\treturn get_value(argv[0], value_pattern, flags);\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n+\tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex f77d2f7847..7ce13ed6cd 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -17,9 +17,15 @@ do\n case \"$mode\" in\n legacy)\n \tmode_prefix=\"--\"\n+\tmode_get=\"\"\n+\tmode_get_all=\"--get-all\"\n+\tmode_get_regexp=\"--get-regexp\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n+\tmode_get=\"get\"\n+\tmode_get_all=\"get --all\"\n+\tmode_get_regexp=\"get --regexp --all --show-names\"\n \t;;\n *)\n \tBUG \"unknown mode $mode\";;\n@@ -398,7 +404,7 @@ test_expect_success 'multi-valued get-all returns all' '\n \twow\n \twow2 for me\n \tEOF\n-\tgit config --get-all nextsection.nonewline >actual &&\n+\tgit config ${mode_get_all} nextsection.nonewline >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -500,7 +506,7 @@ nextsection.nonewline wow2 for me\n EOF\n \n test_expect_success '--get-regexp' '\n-\tgit config --get-regexp in >output &&\n+\tgit config ${mode_get_regexp} in >output &&\n \ttest_cmp expect output\n '\n \n@@ -510,7 +516,7 @@ nextsection.nonewline\n EOF\n \n test_expect_success '--name-only --get-regexp' '\n-\tgit config --name-only --get-regexp in >output &&\n+\tgit config ${mode_get_regexp} --name-only in >output &&\n \ttest_cmp expect output\n '\n \n@@ -521,7 +527,7 @@ EOF\n \n test_expect_success '--add' '\n \tgit config --add nextsection.nonewline \"wow4 for you\" &&\n-\tgit config --get-all nextsection.nonewline > output &&\n+\tgit config ${mode_get_all} nextsection.nonewline > output &&\n \ttest_cmp expect output\n '\n \n@@ -543,21 +549,21 @@ test_expect_success 'get variable with empty value' '\n echo novalue.variable > expect\n \n test_expect_success 'get-regexp variable with no value' '\n-\tgit config --get-regexp novalue > output &&\n+\tgit config ${mode_get_regexp} novalue > output &&\n \ttest_cmp expect output\n '\n \n echo 'novalue.variable true' > expect\n \n test_expect_success 'get-regexp --bool variable with no value' '\n-\tgit config --bool --get-regexp novalue > output &&\n+\tgit config ${mode_get_regexp} --bool novalue > output &&\n \ttest_cmp expect output\n '\n \n echo 'emptyvalue.variable ' > expect\n \n test_expect_success 'get-regexp variable with empty value' '\n-\tgit config --get-regexp emptyvalue > output &&\n+\tgit config ${mode_get_regexp} emptyvalue > output &&\n \ttest_cmp expect output\n '\n \n@@ -1131,7 +1137,7 @@ test_expect_success 'quoting' '\n '\n \n test_expect_success 'key with newline' '\n-\ttest_must_fail git config \"key.with\n+\ttest_must_fail git config ${mode_get} \"key.with\n newline\" 123'\n \n test_expect_success 'value with newline' 'git config key.sub value.with\\\\\\\n@@ -1185,7 +1191,7 @@ test_expect_success '--null --list' '\n '\n \n test_expect_success '--null --get-regexp' '\n-\tgit config --null --get-regexp \"val[0-9]\" >result.raw &&\n+\tgit config ${mode_get_regexp} --null \"val[0-9]\" >result.raw &&\n \tnul_to_q <result.raw >result &&\n \techo >>result &&\n \ttest_cmp expect result\n@@ -1194,21 +1200,21 @@ test_expect_success '--null --get-regexp' '\n test_expect_success 'inner whitespace kept verbatim, spaces only' '\n \techo \"foo   bar\" >expect &&\n \tgit config section.val \"foo   bar\" &&\n-\tgit config --get section.val >actual &&\n+\tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '\n \techo \"fooQQbar\" | q_to_tab >expect &&\n \tgit config section.val \"$(cat expect)\" &&\n-\tgit config --get section.val >actual &&\n+\tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '\n \techo \"foo Q  bar\" | q_to_tab >expect &&\n \tgit config section.val \"$(cat expect)\" &&\n-\tgit config --get section.val >actual &&\n+\tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1283,11 +1289,11 @@ test_expect_success 'git -c can represent empty string' '\n '\n \n test_expect_success 'key sanity-checking' '\n-\ttest_must_fail git config foo=bar &&\n-\ttest_must_fail git config foo=.bar &&\n-\ttest_must_fail git config foo.ba=r &&\n-\ttest_must_fail git config foo.1bar &&\n-\ttest_must_fail git config foo.\"ba\n+\ttest_must_fail git config ${mode_get} foo=bar &&\n+\ttest_must_fail git config ${mode_get} foo=.bar &&\n+\ttest_must_fail git config ${mode_get} foo.ba=r &&\n+\ttest_must_fail git config ${mode_get} foo.1bar &&\n+\ttest_must_fail git config ${mode_get} foo.\"ba\n \t\t\t\tz\".bar &&\n \ttest_must_fail git config . false &&\n \ttest_must_fail git config .foo false &&\n@@ -1336,7 +1342,7 @@ test_expect_success 'git -c complains about empty key and value' '\n '\n \n test_expect_success 'multiple git -c appends config' '\n-\ttest_config alias.x \"!git -c x.two=2 config --get-regexp ^x\\.*\" &&\n+\ttest_config alias.x \"!git -c x.two=2 config ${mode_get_regexp} ^x\\.*\" &&\n \tcat >expect <<-\\EOF &&\n \tx.one 1\n \tx.two 2\n@@ -1502,7 +1508,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '\n \tv=\"${SQ}key.one=foo${SQ}\" &&\n \tv=\"$v  ${SQ}key.two=bar${SQ}\" &&\n \tv=\"$v ${SQ}key.ambiguous=section.whatever=value${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.one foo\n \tkey.two bar\n@@ -1515,7 +1521,7 @@ test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '\n \tv=\"${SQ}key.one${SQ}=${SQ}foo${SQ}\" &&\n \tv=\"$v  ${SQ}key.two${SQ}=${SQ}bar${SQ}\" &&\n \tv=\"$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.one foo\n \tkey.two bar\n@@ -1529,7 +1535,7 @@ test_expect_success 'old and new-style entries can mix' '\n \tv=\"$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}\" &&\n \tv=\"$v ${SQ}key.oldtwo=oldbar${SQ}\" &&\n \tv=\"$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.oldone oldfoo\n \tkey.newone newfoo\n@@ -1542,7 +1548,7 @@ test_expect_success 'old and new-style entries can mix' '\n test_expect_success 'old and new bools with ambiguous subsection' '\n \tv=\"${SQ}key.with=equals.oldbool${SQ}\" &&\n \tv=\"$v ${SQ}key.with=equals.newbool${SQ}=\" &&\n-\tGIT_CONFIG_PARAMETERS=$v git config --get-regexp \"key.*\" >actual &&\n+\tGIT_CONFIG_PARAMETERS=$v git config ${mode_get_regexp} \"key.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tkey.with equals.oldbool\n \tkey.with=equals.newbool\n@@ -1556,7 +1562,7 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '\n \tenv.two two\n \tEOF\n \tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ} ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"env.*\" >actual &&\n \ttest_cmp expect actual &&\n \n \tcat >expect <<-EOF &&\n@@ -1564,12 +1570,12 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '\n \tenv.two two\n \tEOF\n \tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ}\\\\$SQ$SQ$SQ ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"env.*\" >actual &&\n \ttest_cmp expect actual &&\n \n \ttest_must_fail env \\\n \t\tGIT_CONFIG_PARAMETERS=\"${SQ}env.one=one${SQ}\\\\$SQ ${SQ}env.two=two${SQ}\" \\\n-\t\tgit config --get-regexp \"env.*\"\n+\t\tgit config ${mode_get_regexp} \"env.*\"\n '\n \n test_expect_success 'git --config-env=key=envvar support' '\n@@ -1617,7 +1623,7 @@ test_expect_success 'git -c and --config-env work together' '\n \tENVVAR=env-value git \\\n \t\t-c bar.cmd=cmd-value \\\n \t\t--config-env=bar.env=ENVVAR \\\n-\t\tconfig --get-regexp \"^bar.*\" >actual &&\n+\t\tconfig ${mode_get_regexp} \"^bar.*\" >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1645,7 +1651,7 @@ test_expect_success 'git config handles environment config pairs' '\n \tGIT_CONFIG_COUNT=2 \\\n \t\tGIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"foo\" \\\n \t\tGIT_CONFIG_KEY_1=\"pair.two\" GIT_CONFIG_VALUE_1=\"bar\" \\\n-\t\tgit config --get-regexp \"pair.*\" >actual &&\n+\t\tgit config ${mode_get_regexp} \"pair.*\" >actual &&\n \tcat >expect <<-EOF &&\n \tpair.one foo\n \tpair.two bar\n@@ -1655,7 +1661,7 @@ test_expect_success 'git config handles environment config pairs' '\n \n test_expect_success 'git config ignores pairs without count' '\n \ttest_must_fail env GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n@@ -1663,7 +1669,7 @@ test_expect_success 'git config ignores pairs exceeding count' '\n \tGIT_CONFIG_COUNT=1 \\\n \t\tGIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n \t\tGIT_CONFIG_KEY_1=\"pair.two\" GIT_CONFIG_VALUE_1=\"value\" \\\n-\t\tgit config --get-regexp \"pair.*\" >actual 2>error &&\n+\t\tgit config ${mode_get_regexp} \"pair.*\" >actual 2>error &&\n \tcat >expect <<-EOF &&\n \tpair.one value\n \tEOF\n@@ -1674,14 +1680,14 @@ test_expect_success 'git config ignores pairs exceeding count' '\n test_expect_success 'git config ignores pairs with zero count' '\n \ttest_must_fail env \\\n \t\tGIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n test_expect_success 'git config ignores pairs with empty count' '\n \ttest_must_fail env \\\n \t\tGIT_CONFIG_COUNT= GIT_CONFIG_KEY_0=\"pair.one\" GIT_CONFIG_VALUE_0=\"value\" \\\n-\t\tgit config pair.one 2>error &&\n+\t\tgit config ${mode_get} pair.one 2>error &&\n \ttest_must_be_empty error\n '\n \n@@ -1720,7 +1726,7 @@ test_expect_success 'environment overrides config file' '\n \tone = value\n \tEOF\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \\\n-\t\tgit config pair.one >actual &&\n+\t\tgit config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1730,7 +1736,7 @@ test_expect_success 'environment overrides config file' '\n test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '\n \tGIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \\\n \t\tGIT_CONFIG_PARAMETERS=\"${SQ}pair.one=override${SQ}\" \\\n-\t\tgit config pair.one >actual &&\n+\t\tgit config ${mode_get} pair.one >actual &&\n \tcat >expect <<-EOF &&\n \toverride\n \tEOF\n@@ -1805,20 +1811,28 @@ test_expect_success 'urlmatch' '\n \n \ttest_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&\n \ttest_must_be_empty actual &&\n+\ttest_expect_code 1 git config get --url=https://good.example.com --bool doesnt.exist >actual &&\n+\ttest_must_be_empty actual &&\n \n \techo true >expect &&\n \tgit config --bool --get-urlmatch http.SSLverify https://good.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --bool --url=https://good.example.com http.SSLverify >actual &&\n+\ttest_cmp expect actual &&\n \n \techo false >expect &&\n \tgit config --bool --get-urlmatch http.sslverify https://weak.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --bool --url=https://weak.example.com http.sslverify >actual &&\n+\ttest_cmp expect actual &&\n \n \t{\n \t\techo http.cookiefile /tmp/cookie.txt &&\n \t\techo http.sslverify false\n \t} >expect &&\n \tgit config --get-urlmatch HTTP https://weak.example.com >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://weak.example.com HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1834,6 +1848,8 @@ test_expect_success 'urlmatch with --show-scope' '\n \tlocal\thttp.sslverify false\n \tEOF\n \tgit config --get-urlmatch --show-scope HTTP https://weak.example.com >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://weak.example.com --show-scope HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -1866,45 +1882,67 @@ test_expect_success 'urlmatch favors more specific URLs' '\n \techo http.cookiefile /tmp/root.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com/subdirectory >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com/subdirectory HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://example.com/subdirectory/nested HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/user.txt >expect &&\n \tgit config --get-urlmatch HTTP https://user@example.com/ >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://user@example.com/ HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/subdirectory.txt >expect &&\n \tgit config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://averylonguser@example.com/subdirectory HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/preceding.txt >expect &&\n \tgit config --get-urlmatch HTTP https://preceding.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://preceding.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/wildcard.txt >expect &&\n \tgit config --get-urlmatch HTTP https://wildcard.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://wildcard.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n \tgit config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://sub.example.com/wildcardwithsubdomain HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/trailing.txt >expect &&\n \tgit config --get-urlmatch HTTP https://trailing.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://trailing.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n \tgit config --get-urlmatch HTTP https://user@sub.example.com >actual &&\n \ttest_cmp expect actual &&\n+\tgit config get --url=https://user@sub.example.com HTTP >actual &&\n+\ttest_cmp expect actual &&\n \n \techo http.cookiefile /tmp/multiwildcard.txt >expect &&\n \tgit config --get-urlmatch HTTP https://wildcard.example.org >actual &&\n+\ttest_cmp expect actual &&\n+\tgit config get --url=https://wildcard.example.org HTTP >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -2027,7 +2065,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[one]\n \tEOF\n \tgit config --unset two.subsection.key &&\n-\ttest \"not [two subsection]\" = \"$(git config one.key)\" &&\n+\ttest \"not [two subsection]\" = \"$(git config ${mode_get} one.key)\" &&\n \ttest_line_count = 3 .git/config\n '\n \n@@ -2150,7 +2188,7 @@ test_expect_success '--show-origin with --get-regexp' '\n \tfile:$HOME/.gitconfig\tuser.global true\n \tfile:.git/config\tuser.local true\n \tEOF\n-\tgit config --show-origin --get-regexp \"user\\.[g|l].*\" >output &&\n+\tgit config ${mode_get_regexp} --show-origin \"user\\.[g|l].*\" >output &&\n \ttest_cmp expect output\n '\n \n@@ -2158,7 +2196,7 @@ test_expect_success '--show-origin getting a single key' '\n \tcat >expect <<-\\EOF &&\n \tfile:.git/config\tlocal\n \tEOF\n-\tgit config --show-origin user.override >output &&\n+\tgit config ${mode_get} --show-origin user.override >output &&\n \ttest_cmp expect output\n '\n \n@@ -2290,7 +2328,7 @@ test_expect_success '--show-scope getting a single value' '\n \tcat >expect <<-\\EOF &&\n \tlocal\ttrue\n \tEOF\n-\tgit config --show-scope --get user.local >output &&\n+\tgit config ${mode_get} --show-scope user.local >output &&\n \ttest_cmp expect output\n '\n \n@@ -2559,9 +2597,9 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \t# These modes complain when --fixed-value has no value-pattern\n \ttest_must_fail git config --file=config --fixed-value dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --get dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-all dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --get-regexp \"dev.*\" &&\n+\ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n \ttest_must_fail git config --file=config --fixed-value --unset dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --unset-all dev.null\n '\n@@ -2591,12 +2629,12 @@ test_expect_success '--fixed-value uses exact string matching' '\n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset-all fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset-all fixed.test \"$META\" &&\n-\ttest_must_fail git config --file=config fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n \tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n@@ -2623,18 +2661,27 @@ test_expect_success '--get and --get-all with --fixed-value' '\n \tgit config --file=config --add fixed.test \"$META\" &&\n \n \tgit config --file=config --get fixed.test bogus &&\n+\tgit config get --file=config --value=bogus fixed.test &&\n \ttest_must_fail git config --file=config --get fixed.test \"$META\" &&\n+\ttest_must_fail git config get --file=config --value=\"$META\" fixed.test &&\n \tgit config --file=config --get --fixed-value fixed.test \"$META\" &&\n+\tgit config get --file=config --fixed-value --value=\"$META\" fixed.test &&\n \ttest_must_fail git config --file=config --get --fixed-value fixed.test non-existent &&\n \n \tgit config --file=config --get-all fixed.test bogus &&\n+\tgit config get --all --file=config --value=bogus fixed.test &&\n \ttest_must_fail git config --file=config --get-all fixed.test \"$META\" &&\n+\ttest_must_fail git config get --all --file=config --value=\"$META\" fixed.test &&\n \tgit config --file=config --get-all --fixed-value fixed.test \"$META\" &&\n+\tgit config get --all --file=config --value=\"$META\" --fixed-value fixed.test &&\n \ttest_must_fail git config --file=config --get-all --fixed-value fixed.test non-existent &&\n \n \tgit config --file=config --get-regexp fixed+ bogus &&\n+\tgit config get --regexp --file=config --value=bogus fixed+ &&\n \ttest_must_fail git config --file=config --get-regexp fixed+ \"$META\" &&\n+\ttest_must_fail git config get --regexp --file=config --value=\"$META\" fixed+ &&\n \tgit config --file=config --get-regexp --fixed-value fixed+ \"$META\" &&\n+\tgit config get --regexp --file=config --fixed-value --value=\"$META\" fixed+ &&\n \ttest_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent\n '\n \n-- \n2.45.0\n\n"},{"id":"494132","messageId":"94afb5a5b7295de7ce7b22a0a4c612b431377eda.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 09/14] builtin/config: introduce \"set\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:33Z","receivedAt":"2024-05-06T08:56:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"set\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt |  38 ++++++++-----\n builtin/config.c             |  63 ++++++++++++++++++++++\n t/t1300-config.sh            | 102 ++++++++++++++++++-----------------\n 3 files changed, 140 insertions(+), 63 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex d0878663db..f57fa01085 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -11,9 +11,7 @@ SYNOPSIS\n [verse]\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' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]\n-'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>\n-'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]\n+'git config set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\n 'git config' [<file-option>] --rename-section <old-name> <new-name>\n@@ -27,7 +25,7 @@ You can query/set/replace/unset options with this command. The name is\n actually the section and the key separated by a dot, and the value will be\n escaped.\n \n-Multiple lines can be added to an option by using the `--add` option.\n+Multiple lines can be added to an option by using the `--append` option.\n If you want to update or unset an option which can occur on multiple\n lines, a `value-pattern` (which is an extended regular expression,\n unless the `--fixed-value` option is given) needs to be given.  Only the\n@@ -82,6 +80,13 @@ get::\n \temits all values associated with key. Returns error code 1 if key is\n \tnot present.\n \n+set::\n+\tSet value for one or more config options. By default, this command\n+\trefuses to write multi-valued config options. Passing `--all` will\n+\treplace all multi-valued config options with the new value, whereas\n+\t`--value=` will replace all config options whose values match the given\n+\tpattern.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -90,10 +95,9 @@ OPTIONS\n \tDefault behavior is to replace at most one line. This replaces\n \tall lines matching the key (and optionally the `value-pattern`).\n \n---add::\n+--append::\n \tAdds a new line to the option without altering any existing\n-\tvalues.  This is the same as providing '^$' as the `value-pattern`\n-\tin `--replace-all`.\n+\tvalues. This is the same as providing '--value=^$' in `set`.\n \n --comment <message>::\n \tAppend a comment at the end of new or modified lines.\n@@ -296,6 +300,9 @@ recommended to migrate to the new syntax.\n 'git config <name>'::\n \tReplaced by `git config get <name>`.\n \n+'git config <name> <value> [<value-pattern>]'::\n+\tReplaced by `git config set [--value=<pattern>] <name> <value>`.\n+\n -l::\n --list::\n \tReplaced by `git config list`.\n@@ -315,6 +322,9 @@ recommended to migrate to the new syntax.\n --get-color <name> [<default>]::\n \tReplaced by `git config get --type=color [--default=<default>] <name>`.\n \n+--add <name> <value>::\n+\tReplaced by `git config set --append <name> <value>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n@@ -361,7 +371,7 @@ precedence over values read earlier.  When multiple values are taken then all\n values of a key from all files will be used.\n \n By default, options are only written to the repository specific\n-configuration file. Note that this also affects options like `--replace-all`\n+configuration file. Note that this also affects options like `set`\n and `--unset`. *'git config' will only ever change one file at a time*.\n \n You can limit which configuration sources are read from or written to by\n@@ -497,7 +507,7 @@ Given a .git/config like this:\n you can set the filemode to true with\n \n ------------\n-% git config core.filemode true\n+% git config set core.filemode true\n ------------\n \n The hypothetical proxy command entries actually have a postfix to discern\n@@ -505,7 +515,7 @@ what URL they apply to. Here is how to change the entry for kernel.org\n to \"ssh\".\n \n ------------\n-% git config core.gitproxy '\"ssh\" for kernel.org' 'for kernel.org$'\n+% git config set --value='for kernel.org$' core.gitproxy '\"ssh\" for kernel.org'\n ------------\n \n This makes sure that only the key/value pair for kernel.org is replaced.\n@@ -541,26 +551,26 @@ If you like to live dangerously, you can replace *all* core.gitproxy by a\n new one with\n \n ------------\n-% git config --replace-all core.gitproxy ssh\n+% git config set --all core.gitproxy ssh\n ------------\n \n However, if you really only want to replace the line for the default proxy,\n i.e. the one without a \"for ...\" postfix, do something like this:\n \n ------------\n-% git config core.gitproxy ssh '! for '\n+% git config set --value='! for ' core.gitproxy ssh\n ------------\n \n To actually match only values with an exclamation mark, you have to\n \n ------------\n-% git config section.key value '[!]'\n+% git config set --value='[!]' section.key value\n ------------\n \n To add a new proxy, without altering any of the existing ones, use\n \n ------------\n-% git config --add core.gitproxy '\"proxy-command\" for example.com'\n+% git config set --append core.gitproxy '\"proxy-command\" for example.com'\n ------------\n \n An example to use customized color from the configuration in your\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 9e7ae49c02..87fd12fda2 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -18,6 +18,7 @@\n static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n+\tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tNULL\n };\n \n@@ -31,6 +32,11 @@ static const char *const builtin_config_get_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_set_usage[] = {\n+\tN_(\"git config set [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -849,9 +855,66 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix)\n \treturn get_value(argv[0], value_pattern, flags);\n }\n \n+static int cmd_config_set(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL, *comment_arg = NULL;\n+\tchar *comment = NULL;\n+\tint flags = 0, append = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tCONFIG_TYPE_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter\")),\n+\t\tOPT_BIT(0, \"all\", &flags, N_(\"replace multi-valued config option with new value\"), CONFIG_FLAGS_MULTI_REPLACE),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_GROUP(N_(\"Other\")),\n+\t\tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n+\t\tOPT_BOOL(0, \"append\", &append, N_(\"add a new line without altering any existing values\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct key_value_info default_kvi = KVI_INIT;\n+\tchar *value;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with --value=<pattern>\"));\n+\tif (append && value_pattern)\n+\t\tdie(_(\"--append cannot be used with --value=<pattern>\"));\n+\tif (append)\n+\t\tvalue_pattern = CONFIG_REGEX_NONE;\n+\n+\tcomment = git_config_prepare_comment_string(comment_arg);\n+\n+\thandle_config_location(prefix);\n+\n+\tvalue = normalize_value(argv[0], argv[1], &default_kvi);\n+\n+\tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {\n+\t\tret = git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t\t     argv[0], value, value_pattern,\n+\t\t\t\t\t\t\t     comment, flags);\n+\t} else {\n+\t\tret = git_config_set_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t    argv[0], comment, value);\n+\t\tif (ret == CONFIG_NOTHING_SET)\n+\t\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n+\t\t\t\"       Use a regexp, --add or --replace-all to change %s.\"), argv[0]);\n+\t}\n+\n+\tfree(comment);\n+\tfree(value);\n+\treturn ret;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n+\tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 7ce13ed6cd..aa3f179be5 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -20,12 +20,16 @@ legacy)\n \tmode_get=\"\"\n \tmode_get_all=\"--get-all\"\n \tmode_get_regexp=\"--get-regexp\"\n+\tmode_set=\"\"\n+\tmode_replace_all=\"--replace-all\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n \tmode_get=\"get\"\n \tmode_get_all=\"get --all\"\n \tmode_get_regexp=\"get --regexp --all --show-names\"\n+\tmode_set=\"set\"\n+\tmode_replace_all=\"set --all\"\n \t;;\n *)\n \tBUG \"unknown mode $mode\";;\n@@ -132,7 +136,7 @@ cat > expect << EOF\n \tpenguin = little blue\n EOF\n test_expect_success 'initial' '\n-\tgit config section.penguin \"little blue\" &&\n+\tgit config ${mode_set} section.penguin \"little blue\" &&\n \ttest_cmp expect .git/config\n '\n \n@@ -142,7 +146,7 @@ cat > expect << EOF\n \tMovie = BadPhysics\n EOF\n test_expect_success 'mixed case' '\n-\tgit config Section.Movie BadPhysics &&\n+\tgit config ${mode_set} Section.Movie BadPhysics &&\n \ttest_cmp expect .git/config\n '\n \n@@ -154,7 +158,7 @@ cat > expect << EOF\n \tWhatEver = Second\n EOF\n test_expect_success 'similar section' '\n-\tgit config Sections.WhatEver Second &&\n+\tgit config ${mode_set} Sections.WhatEver Second &&\n \ttest_cmp expect .git/config\n '\n \n@@ -167,7 +171,7 @@ cat > expect << EOF\n \tWhatEver = Second\n EOF\n test_expect_success 'uppercase section' '\n-\tgit config SECTION.UPPERCASE true &&\n+\tgit config ${mode_set} SECTION.UPPERCASE true &&\n \ttest_cmp expect .git/config\n '\n \n@@ -194,8 +198,8 @@ EOF\n \n test_expect_success 'append comments' '\n \tgit config --replace-all --comment=\"Pygoscelis papua\" section.penguin gentoo &&\n-\tgit config --comment=\"find fish\" section.disposition peckish &&\n-\tgit config --comment=\"#abc\" section.foo bar &&\n+\tgit config ${mode_set} --comment=\"find fish\" section.disposition peckish &&\n+\tgit config ${mode_set} --comment=\"#abc\" section.foo bar &&\n \n \tgit config --comment=\"and comment\" section.spsp value &&\n \tgit config --comment=\"\t# and comment\" section.htsp value &&\n@@ -204,7 +208,7 @@ test_expect_success 'append comments' '\n '\n \n test_expect_success 'Prohibited LF in comment' '\n-\ttest_must_fail git config --comment=\"a${LF}b\" section.k v\n+\ttest_must_fail git config ${mode_set} --comment=\"a${LF}b\" section.k v\n '\n \n test_expect_success 'non-match result' 'test_cmp expect .git/config'\n@@ -301,14 +305,14 @@ test_expect_success 'multiple unset is correct' '\n cp .git/config2 .git/config\n \n test_expect_success '--replace-all missing value' '\n-\ttest_must_fail git config --replace-all beta.haha &&\n+\ttest_must_fail git config ${mode_replace_all} beta.haha &&\n \ttest_cmp .git/config2 .git/config\n '\n \n rm .git/config2\n \n test_expect_success '--replace-all' '\n-\tgit config --replace-all beta.haha gamma\n+\tgit config ${mode_replace_all} beta.haha gamma\n '\n \n cat > expect << EOF\n@@ -335,7 +339,7 @@ noIndent= sillyValue ; 'nother silly comment\n [nextSection] noNewline = ouch\n EOF\n test_expect_success 'really mean test' '\n-\tgit config beta.haha alpha &&\n+\tgit config ${mode_set} beta.haha alpha &&\n \ttest_cmp expect .git/config\n '\n \n@@ -350,7 +354,7 @@ noIndent= sillyValue ; 'nother silly comment\n \tnonewline = wow\n EOF\n test_expect_success 'really really mean test' '\n-\tgit config nextsection.nonewline wow &&\n+\tgit config ${mode_set} nextsection.nonewline wow &&\n \ttest_cmp expect .git/config\n '\n \n@@ -824,16 +828,16 @@ EOF\n \n test_expect_success 'section ending' '\n \trm -f .git/config &&\n-\tgit config gitcvs.enabled true &&\n-\tgit config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&\n-\tgit config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&\n+\tgit config ${mode_set} gitcvs.enabled true &&\n+\tgit config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&\n+\tgit config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&\n \ttest_cmp expect .git/config\n \n '\n \n test_expect_success numbers '\n-\tgit config kilo.gram 1k &&\n-\tgit config mega.ton 1m &&\n+\tgit config ${mode_set} kilo.gram 1k &&\n+\tgit config ${mode_set} mega.ton 1m &&\n \techo 1024 >expect &&\n \techo 1048576 >>expect &&\n \tgit config --int --get kilo.gram >actual &&\n@@ -842,20 +846,20 @@ test_expect_success numbers '\n '\n \n test_expect_success '--int is at least 64 bits' '\n-\tgit config giga.watts 121g &&\n+\tgit config ${mode_set} giga.watts 121g &&\n \techo  >expect &&\n \ttest_cmp_config 129922760704 --int --get giga.watts\n '\n \n test_expect_success 'invalid unit' '\n-\tgit config aninvalid.unit \"1auto\" &&\n+\tgit config ${mode_set} aninvalid.unit \"1auto\" &&\n \ttest_cmp_config 1auto aninvalid.unit &&\n \ttest_must_fail git config --int --get aninvalid.unit 2>actual &&\n \ttest_grep \"bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit\" actual\n '\n \n test_expect_success 'invalid unit boolean' '\n-\tgit config commit.gpgsign \"1true\" &&\n+\tgit config ${mode_set} commit.gpgsign \"1true\" &&\n \ttest_cmp_config 1true commit.gpgsign &&\n \ttest_must_fail git config --bool --get commit.gpgsign 2>actual &&\n \ttest_grep \"bad boolean config value .1true. for .commit.gpgsign.\" actual\n@@ -885,14 +889,14 @@ EOF\n \n test_expect_success bool '\n \n-\tgit config bool.true1 01 &&\n-\tgit config bool.true2 -1 &&\n-\tgit config bool.true3 YeS &&\n-\tgit config bool.true4 true &&\n-\tgit config bool.false1 000 &&\n-\tgit config bool.false2 \"\" &&\n-\tgit config bool.false3 nO &&\n-\tgit config bool.false4 FALSE &&\n+\tgit config ${mode_set} bool.true1 01 &&\n+\tgit config ${mode_set} bool.true2 -1 &&\n+\tgit config ${mode_set} bool.true3 YeS &&\n+\tgit config ${mode_set} bool.true4 true &&\n+\tgit config ${mode_set} bool.false1 000 &&\n+\tgit config ${mode_set} bool.false2 \"\" &&\n+\tgit config ${mode_set} bool.false3 nO &&\n+\tgit config ${mode_set} bool.false4 FALSE &&\n \trm -f result &&\n \tfor i in 1 2 3 4\n \tdo\n@@ -903,7 +907,7 @@ test_expect_success bool '\n \n test_expect_success 'invalid bool (--get)' '\n \n-\tgit config bool.nobool foobar &&\n+\tgit config ${mode_set} bool.nobool foobar &&\n \ttest_must_fail git config --bool --get bool.nobool'\n \n test_expect_success 'invalid bool (set)' '\n@@ -1092,7 +1096,7 @@ test_expect_success 'get --expiry-date' '\n \n test_expect_success 'get --type=color' '\n \trm .git/config &&\n-\tgit config foo.color \"red\" &&\n+\tgit config ${mode_set} foo.color \"red\" &&\n \tgit config --get --type=color foo.color >actual.raw &&\n \ttest_decode_color <actual.raw >actual &&\n \techo \"<RED>\" >expect &&\n@@ -1129,10 +1133,10 @@ cat > expect << EOF\n EOF\n test_expect_success 'quoting' '\n \trm -f .git/config &&\n-\tgit config quote.leading \" test\" &&\n-\tgit config quote.ending \"test \" &&\n-\tgit config quote.semicolon \"test;test\" &&\n-\tgit config quote.hash \"test#test\" &&\n+\tgit config ${mode_set} quote.leading \" test\" &&\n+\tgit config ${mode_set} quote.ending \"test \" &&\n+\tgit config ${mode_set} quote.semicolon \"test;test\" &&\n+\tgit config ${mode_set} quote.hash \"test#test\" &&\n \ttest_cmp expect .git/config\n '\n \n@@ -1140,7 +1144,7 @@ test_expect_success 'key with newline' '\n \ttest_must_fail git config ${mode_get} \"key.with\n newline\" 123'\n \n-test_expect_success 'value with newline' 'git config key.sub value.with\\\\\\\n+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\\\\\n newline'\n \n cat > .git/config <<\\EOF\n@@ -1199,21 +1203,21 @@ test_expect_success '--null --get-regexp' '\n \n test_expect_success 'inner whitespace kept verbatim, spaces only' '\n \techo \"foo   bar\" >expect &&\n-\tgit config section.val \"foo   bar\" &&\n+\tgit config ${mode_set} section.val \"foo   bar\" &&\n \tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '\n \techo \"fooQQbar\" | q_to_tab >expect &&\n-\tgit config section.val \"$(cat expect)\" &&\n+\tgit config ${mode_set} section.val \"$(cat expect)\" &&\n \tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '\n \techo \"foo Q  bar\" | q_to_tab >expect &&\n-\tgit config section.val \"$(cat expect)\" &&\n+\tgit config ${mode_set} section.val \"$(cat expect)\" &&\n \tgit config ${mode_get} section.val >actual &&\n \ttest_cmp expect actual\n '\n@@ -1252,12 +1256,12 @@ test_expect_success 'check split_cmdline return' '\n \tgit init repo &&\n \t(\n \t\tcd repo &&\n-\t\tgit config alias.split-cmdline-fix \"echo \\\"\" &&\n+\t\tgit config ${mode_set} alias.split-cmdline-fix \"echo \\\"\" &&\n \t\ttest_must_fail git split-cmdline-fix &&\n \t\techo foo >foo &&\n \t\tgit add foo &&\n \t\tgit commit -m \"initial commit\" &&\n-\t\tgit config branch.main.mergeoptions \"echo \\\"\" &&\n+\t\tgit config ${mode_set} branch.main.mergeoptions \"echo \\\"\" &&\n \t\ttest_must_fail git merge main\n \t)\n '\n@@ -1295,12 +1299,12 @@ test_expect_success 'key sanity-checking' '\n \ttest_must_fail git config ${mode_get} foo.1bar &&\n \ttest_must_fail git config ${mode_get} foo.\"ba\n \t\t\t\tz\".bar &&\n-\ttest_must_fail git config . false &&\n-\ttest_must_fail git config .foo false &&\n-\ttest_must_fail git config foo. false &&\n-\ttest_must_fail git config .foo. false &&\n-\tgit config foo.bar true &&\n-\tgit config foo.\"ba =z\".bar false\n+\ttest_must_fail git config ${mode_set} . false &&\n+\ttest_must_fail git config ${mode_set} .foo false &&\n+\ttest_must_fail git config ${mode_set} foo. false &&\n+\ttest_must_fail git config ${mode_set} .foo. false &&\n+\tgit config ${mode_set} foo.bar true &&\n+\tgit config ${mode_set} foo.\"ba =z\".bar false\n '\n \n test_expect_success 'git -c works with aliases of builtins' '\n@@ -2523,7 +2527,7 @@ test_expect_success '--replace-all does not invent newlines' '\n \t[abc]\n \tQkey = b\n \tEOF\n-\tgit config --replace-all abc.key b &&\n+\tgit config ${mode_replace_all} abc.key b &&\n \ttest_cmp expect .git/config\n '\n \n@@ -2595,8 +2599,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n \n \t# These modes complain when --fixed-value has no value-pattern\n-\ttest_must_fail git config --file=config --fixed-value dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&\n+\ttest_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&\n+\ttest_must_fail git config ${mode_replace_all} --file=config --fixed-value dev.null bogus &&\n \ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n@@ -2637,7 +2641,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n \ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n \tcp initial config &&\n-\tgit config --file=config --replace-all fixed.test bogus \"$META\" &&\n+\tgit config --file=config fixed.test bogus \"$META\" &&\n \tgit config ${mode_prefix}list --file=config >actual &&\n \tcat >expect <<-EOF &&\n \tfixed.test=$META\n-- \n2.45.0\n\n"},{"id":"494133","messageId":"e525c2326a3908bf77edf1e5e0752a34a28b4449.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 10/14] builtin/config: introduce \"unset\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:38Z","receivedAt":"2024-05-06T08:56:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"unset\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 27 +++++++++++++---------\n builtin/config.c             | 39 ++++++++++++++++++++++++++++++++\n t/t1300-config.sh            | 44 ++++++++++++++++++++++++------------\n 3 files changed, 84 insertions(+), 26 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex f57fa01085..5575fe55ea 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -11,9 +11,8 @@ SYNOPSIS\n [verse]\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>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>\n-'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]\n-'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]\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' [<file-option>] --rename-section <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n@@ -87,6 +86,12 @@ set::\n \t`--value=` will replace all config options whose values match the given\n \tpattern.\n \n+unset::\n+\tUnset value for one or more config options. By default, this command\n+\trefuses to unset multi-valued keys. Passing `--all` will unset all\n+\tmulti-valued config options, whereas `--value` will unset all config\n+\toptions whose values match the given pattern.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -190,12 +195,6 @@ See also <<FILES>>.\n --rename-section::\n \tRename the given section to a new name.\n \n---unset::\n-\tRemove the line matching the key from config file.\n-\n---unset-all::\n-\tRemove all lines matching the key from config file.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -325,6 +324,12 @@ recommended to migrate to the new syntax.\n --add <name> <value>::\n \tReplaced by `git config set --append <name> <value>`.\n \n+--unset <name> [<value-pattern>]::\n+\tReplaced by `git config unset [--value=<pattern>] <name>`.\n+\n+--unset-all <name> [<value-pattern>]::\n+\tReplaced by `git config unset [--value=<pattern>] --all <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\n@@ -372,7 +377,7 @@ values of a key from all files will be used.\n \n By default, options are only written to the repository specific\n configuration file. Note that this also affects options like `set`\n-and `--unset`. *'git config' will only ever change one file at a time*.\n+and `unset`. *'git config' will only ever change one file at a time*.\n \n You can limit which configuration sources are read from or written to by\n specifying the path of a file with the `--file` option, or by specifying a\n@@ -523,7 +528,7 @@ This makes sure that only the key/value pair for kernel.org is replaced.\n To delete the entry for renames, do\n \n ------------\n-% git config --unset diff.renames\n+% git config unset diff.renames\n ------------\n \n If you want to delete an entry for a multivar (like core.gitproxy above),\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 87fd12fda2..2578d4c930 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -19,6 +19,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config list [<file-option>] [<display-option>] [--includes]\"),\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tNULL\n };\n \n@@ -37,6 +38,11 @@ static const char *const builtin_config_set_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_unset_usage[] = {\n+\tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -911,10 +917,43 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix)\n \treturn ret;\n }\n \n+static int cmd_config_unset(int argc, const char **argv, const char *prefix)\n+{\n+\tconst char *value_pattern = NULL;\n+\tint flags = 0;\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_GROUP(N_(\"Filter\")),\n+\t\tOPT_BIT(0, \"all\", &flags, N_(\"replace multi-valued config option with new value\"), CONFIG_FLAGS_MULTI_REPLACE),\n+\t\tOPT_STRING(0, \"value\", &value_pattern, N_(\"pattern\"), N_(\"show config with values matching the pattern\")),\n+\t\tOPT_BIT(0, \"fixed-value\", &flags, N_(\"use string equality when comparing values to value pattern\"), CONFIG_FLAGS_FIXED_VALUE),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_unset_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 1);\n+\n+\tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n+\t\tdie(_(\"--fixed-value only applies with 'value-pattern'\"));\n+\n+\thandle_config_location(prefix);\n+\n+\tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)\n+\t\treturn git_config_set_multivar_in_file_gently(given_config_source.file,\n+\t\t\t\t\t\t\t      argv[0], NULL, value_pattern,\n+\t\t\t\t\t\t\t      NULL, flags);\n+\telse\n+\t\treturn git_config_set_in_file_gently(given_config_source.file, argv[0],\n+\t\t\t\t\t\t     NULL, NULL);\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n+\tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex aa3f179be5..2ab58bbd95 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -22,6 +22,8 @@ legacy)\n \tmode_get_regexp=\"--get-regexp\"\n \tmode_set=\"\"\n \tmode_replace_all=\"--replace-all\"\n+\tmode_unset=\"--unset\"\n+\tmode_unset_all=\"--unset-all\"\n \t;;\n subcommands)\n \tmode_prefix=\"\"\n@@ -30,6 +32,8 @@ subcommands)\n \tmode_get_regexp=\"get --regexp --all --show-names\"\n \tmode_set=\"set\"\n \tmode_replace_all=\"set --all\"\n+\tmode_unset=\"unset\"\n+\tmode_unset_all=\"unset --all\"\n \t;;\n *)\n \tBUG \"unknown mode $mode\";;\n@@ -259,7 +263,7 @@ foo = bar\n EOF\n \n test_expect_success 'unset with cont. lines' '\n-\tgit config --unset beta.baz\n+\tgit config ${mode_unset} beta.baz\n '\n \n cat > expect <<\\EOF\n@@ -286,7 +290,7 @@ EOF\n cp .git/config .git/config2\n \n test_expect_success 'multiple unset' '\n-\tgit config --unset-all beta.haha\n+\tgit config ${mode_unset_all} beta.haha\n '\n \n cat > expect << EOF\n@@ -372,7 +376,7 @@ noIndent= sillyValue ; 'nother silly comment\n \tnonewline = wow\n EOF\n test_expect_success 'unset' '\n-\tgit config --unset beta.haha &&\n+\tgit config ${mode_unset} beta.haha &&\n \ttest_cmp expect .git/config\n '\n \n@@ -428,11 +432,11 @@ test_expect_success 'multivar replace' '\n '\n \n test_expect_success 'ambiguous unset' '\n-\ttest_must_fail git config --unset nextsection.nonewline\n+\ttest_must_fail git config ${mode_unset} nextsection.nonewline\n '\n \n test_expect_success 'invalid unset' '\n-\ttest_must_fail git config --unset somesection.nonewline\n+\ttest_must_fail git config ${mode_unset} somesection.nonewline\n '\n \n cat > expect << EOF\n@@ -446,7 +450,12 @@ noIndent= sillyValue ; 'nother silly comment\n EOF\n \n test_expect_success 'multivar unset' '\n-\tgit config --unset nextsection.nonewline \"wow3$\" &&\n+\tcase \"$mode\" in\n+\tlegacy)\n+\t\tgit config --unset nextsection.nonewline \"wow3$\";;\n+\tsubcommands)\n+\t\tgit config unset --value=\"wow3$\" nextsection.nonewline;;\n+\tesac &&\n \ttest_cmp expect .git/config\n '\n \n@@ -2013,7 +2022,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t# please be careful when you update the above variable\n \tEOF\n \n-\tgit config --unset section.key &&\n+\tgit config ${mode_unset} section.key &&\n \ttest_cmp expect .git/config &&\n \n \tcat >.git/config <<-\\EOF &&\n@@ -2026,7 +2035,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[next-section]\n \tEOF\n \n-\tgit config --unset section.key &&\n+\tgit config ${mode_unset} section.key &&\n \ttest_cmp expect .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -2036,7 +2045,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[two]\n \tkey = true\n \tEOF\n-\tgit config --unset two.key &&\n+\tgit config ${mode_unset} two.key &&\n \t! grep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -2046,7 +2055,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[one]\n \tkey = true\n \tEOF\n-\tgit config --unset-all one.key &&\n+\tgit config ${mode_unset_all} one.key &&\n \ttest_line_count = 0 .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -2056,7 +2065,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[two]\n \tQkey = true\n \tEOF\n-\tgit config --unset two.key &&\n+\tgit config ${mode_unset} two.key &&\n \tgrep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n@@ -2068,7 +2077,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \t[TWO \"subsection\"]\n \t[one]\n \tEOF\n-\tgit config --unset two.subsection.key &&\n+\tgit config ${mode_unset} two.subsection.key &&\n \ttest \"not [two subsection]\" = \"$(git config ${mode_get} one.key)\" &&\n \ttest_line_count = 3 .git/config\n '\n@@ -2080,7 +2089,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' '\n \tkey = value2\n \tEOF\n \n-\tgit config --unset-all section.key &&\n+\tgit config ${mode_unset_all} section.key &&\n \ttest_line_count = 0 .git/config\n '\n \n@@ -2604,8 +2613,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&\n \ttest_must_fail git config ${mode_get_regexp} --file=config --fixed-value \"dev.*\" &&\n-\ttest_must_fail git config --file=config --fixed-value --unset dev.null &&\n-\ttest_must_fail git config --file=config --fixed-value --unset-all dev.null\n+\ttest_must_fail git config ${mode_unset} --file=config --fixed-value dev.null &&\n+\ttest_must_fail git config ${mode_unset_all} --file=config --fixed-value dev.null\n '\n \n test_expect_success '--fixed-value uses exact string matching' '\n@@ -2635,6 +2644,11 @@ test_expect_success '--fixed-value uses exact string matching' '\n \tgit config --file=config --fixed-value --unset fixed.test \"$META\" &&\n \ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n \n+\tcp initial config &&\n+\ttest_must_fail git config unset --file=config --value=\"$META\" fixed.test &&\n+\tgit config unset --file=config --fixed-value --value=\"$META\" fixed.test &&\n+\ttest_must_fail git config ${mode_get} --file=config fixed.test &&\n+\n \tcp initial config &&\n \ttest_must_fail git config --file=config --unset-all fixed.test \"$META\" &&\n \tgit config --file=config --fixed-value --unset-all fixed.test \"$META\" &&\n-- \n2.45.0\n\n"},{"id":"494134","messageId":"a79788989049f41053b985307f35441519e6b3b9.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 11/14] builtin/config: introduce \"rename-section\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:43Z","receivedAt":"2024-05-06T08:56:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"rename-section\" subcommand to git-config(1). Please\nrefer to preceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 11 +++++++----\n builtin/config.c             | 32 ++++++++++++++++++++++++++++++++\n t/t1300-config.sh            | 22 +++++++++++-----------\n 3 files changed, 50 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 5575fe55ea..ede86ad085 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -13,7 +13,7 @@ SYNOPSIS\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' [<file-option>] --rename-section <old-name> <new-name>\n+'git config rename-section' [<file-option>] <old-name> <new-name>\n 'git config' [<file-option>] --remove-section <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n@@ -92,6 +92,9 @@ unset::\n \tmulti-valued config options, whereas `--value` will unset all config\n \toptions whose values match the given pattern.\n \n+rename-section::\n+\tRename the given section to a new name.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -192,9 +195,6 @@ See also <<FILES>>.\n --remove-section::\n \tRemove the given section from the configuration file.\n \n---rename-section::\n-\tRename the given section to a new name.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -330,6 +330,9 @@ recommended to migrate to the new syntax.\n --unset-all <name> [<value-pattern>]::\n \tReplaced by `git config unset [--value=<pattern>] --all <name>`.\n \n+--rename-section <old-name> <new-name>::\n+\tReplaced by `git config rename-section <old-name> <new-name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 2578d4c930..a60dca9b2b 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -20,6 +20,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>\"),\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n+\tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tNULL\n };\n \n@@ -43,6 +44,11 @@ static const char *const builtin_config_unset_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_rename_section_usage[] = {\n+\tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -949,11 +955,37 @@ static int cmd_config_unset(int argc, const char **argv, const char *prefix)\n \t\t\t\t\t\t     NULL, NULL);\n }\n \n+static int cmd_config_rename_section(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_rename_section_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 2, 2);\n+\n+\thandle_config_location(prefix);\n+\n+\tret = git_config_rename_section_in_file(given_config_source.file,\n+\t\t\t\t\t\targv[0], argv[1]);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!ret)\n+\t\tdie(_(\"no such section: %s\"), argv[0]);\n+\n+\treturn 0;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n+\tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 2ab58bbd95..5d7b08fa4c 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -699,7 +699,7 @@ weird\n EOF\n \n test_expect_success 'rename section' '\n-\tgit config --rename-section branch.eins branch.zwei\n+\tgit config ${mode_prefix}rename-section branch.eins branch.zwei\n '\n \n cat > expect << EOF\n@@ -718,7 +718,7 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'rename non-existing section' '\n-\ttest_must_fail git config --rename-section \\\n+\ttest_must_fail git config ${mode_prefix}rename-section \\\n \t\tbranch.\"world domination\" branch.drei\n '\n \n@@ -727,7 +727,7 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'rename another section' '\n-\tgit config --rename-section branch.\"1 234 blabl/a\" branch.drei\n+\tgit config ${mode_prefix}rename-section branch.\"1 234 blabl/a\" branch.drei\n '\n \n cat > expect << EOF\n@@ -750,7 +750,7 @@ cat >> .git/config << EOF\n EOF\n \n test_expect_success 'rename a section with a var on the same line' '\n-\tgit config --rename-section branch.vier branch.zwei\n+\tgit config ${mode_prefix}rename-section branch.vier branch.zwei\n '\n \n cat > expect << EOF\n@@ -771,11 +771,11 @@ test_expect_success 'rename succeeded' '\n '\n \n test_expect_success 'renaming empty section name is rejected' '\n-\ttest_must_fail git config --rename-section branch.zwei \"\"\n+\ttest_must_fail git config ${mode_prefix}rename-section branch.zwei \"\"\n '\n \n test_expect_success 'renaming to bogus section is rejected' '\n-\ttest_must_fail git config --rename-section branch.zwei \"bogus name\"\n+\ttest_must_fail git config ${mode_prefix}rename-section branch.zwei \"bogus name\"\n '\n \n test_expect_success 'renaming a section with a long line' '\n@@ -784,7 +784,7 @@ test_expect_success 'renaming a section with a long line' '\n \t\tprintf \"  c = d %1024s [a] e = f\\\\n\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\tgit config -f y --rename-section a xyz &&\n+\tgit config ${mode_prefix}rename-section -f y a xyz &&\n \ttest_must_fail git config -f y b.e\n '\n \n@@ -794,7 +794,7 @@ test_expect_success 'renaming an embedded section with a long line' '\n \t\tprintf \"  c = d %1024s [a] [foo] e = f\\\\n\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\tgit config -f y --rename-section a xyz &&\n+\tgit config ${mode_prefix}rename-section -f y a xyz &&\n \ttest_must_fail git config -f y foo.e\n '\n \n@@ -804,7 +804,7 @@ test_expect_success 'renaming a section with an overly-long line' '\n \t\tprintf \"  c = d %525000s e\" \" \" &&\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n-\ttest_must_fail git config -f y --rename-section a xyz 2>err &&\n+\ttest_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&\n \tgrep \"refusing to work with overly long line in .y. on line 2\" err\n '\n \n@@ -2112,7 +2112,7 @@ test_expect_success POSIXPERM,PERL 'preserves existing permissions' '\n \tgit config imap.pass Hunter2 &&\n \tperl -e \\\n \t  \"die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600\" &&\n-\tgit config --rename-section imap pop &&\n+\tgit config ${mode_prefix}rename-section imap pop &&\n \tperl -e \\\n \t  \"die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600\"\n '\n@@ -2601,7 +2601,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --add dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n-\ttest_must_fail git config --file=config --fixed-value --rename-section dev null &&\n+\ttest_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&\n \ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n \ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n-- \n2.45.0\n\n"},{"id":"494135","messageId":"8ec214755ea1e37f2b49e9d5f88a8d719ed84996.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 12/14] builtin/config: introduce \"remove-section\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:47Z","receivedAt":"2024-05-06T08:56:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"remove-section\" subcommand to git-config(1). Please\nrefer to preceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 11 +++++++----\n builtin/config.c             | 32 ++++++++++++++++++++++++++++++++\n t/t1300-config.sh            |  4 ++--\n 3 files changed, 41 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex ede86ad085..ef46520c15 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -14,7 +14,7 @@ SYNOPSIS\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' [<file-option>] --remove-section <name>\n+'git config remove-section' [<file-option>] <name>\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n 'git config' [<file-option>] -e | --edit\n \n@@ -95,6 +95,9 @@ unset::\n rename-section::\n \tRename the given section to a new name.\n \n+remove-section::\n+\tRemove the given section from the configuration file.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -192,9 +195,6 @@ See also <<FILES>>.\n \tsection in linkgit:gitrevisions[7] for a more complete list of\n \tways to spell blob names.\n \n---remove-section::\n-\tRemove the given section from the configuration file.\n-\n --fixed-value::\n \tWhen used with the `value-pattern` argument, treat `value-pattern` as\n \tan exact string instead of a regular expression. This will restrict\n@@ -333,6 +333,9 @@ recommended to migrate to the new syntax.\n --rename-section <old-name> <new-name>::\n \tReplaced by `git config rename-section <old-name> <new-name>`.\n \n+--remove-section <name>::\n+\tReplaced by `git config remove-section <name>`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex a60dca9b2b..cea03fb517 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -21,6 +21,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n+\tN_(\"git config remove-section [<file-option>] <name>\"),\n \tNULL\n };\n \n@@ -49,6 +50,11 @@ static const char *const builtin_config_rename_section_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_remove_section_usage[] = {\n+\tN_(\"git config remove-section [<file-option>] <name>\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -980,12 +986,38 @@ static int cmd_config_rename_section(int argc, const char **argv, const char *pr\n \treturn 0;\n }\n \n+static int cmd_config_remove_section(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_remove_section_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tcheck_write();\n+\tcheck_argc(argc, 1, 1);\n+\n+\thandle_config_location(prefix);\n+\n+\tret = git_config_rename_section_in_file(given_config_source.file,\n+\t\t\t\t\t\targv[0], NULL);\n+\tif (ret < 0)\n+\t\treturn ret;\n+\telse if (!ret)\n+\t\tdie(_(\"no such section: %s\"), argv[0]);\n+\n+\treturn 0;\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n \tOPT_SUBCOMMAND(\"set\", &subcommand, cmd_config_set),\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n+\tOPT_SUBCOMMAND(\"remove-section\", &subcommand, cmd_config_remove_section),\n \tOPT_END(),\n };\n \ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 5d7b08fa4c..9d71ec1260 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -813,7 +813,7 @@ cat >> .git/config << EOF\n EOF\n \n test_expect_success 'remove section' '\n-\tgit config --remove-section branch.zwei\n+\tgit config ${mode_prefix}remove-section branch.zwei\n '\n \n cat > expect << EOF\n@@ -2602,7 +2602,7 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config --file=config --fixed-value --get-urlmatch dev.null bogus &&\n \ttest_must_fail git config ${mode_prefix}rename-section --file=config --fixed-value dev null &&\n-\ttest_must_fail git config --file=config --fixed-value --remove-section dev &&\n+\ttest_must_fail git config ${mode_prefix}remove-section --file=config --fixed-value dev &&\n \ttest_must_fail git config ${mode_prefix}list --file=config --fixed-value &&\n \ttest_must_fail git config --file=config --fixed-value --get-color dev.null &&\n \ttest_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&\n-- \n2.45.0\n\n"},{"id":"494136","messageId":"1893c23afc982e1d442f50b5e4b8ab0073aaa05d.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 13/14] builtin/config: introduce \"edit\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:52Z","receivedAt":"2024-05-06T08:56:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new \"edit\" subcommand to git-config(1). Please refer to\npreceding commits regarding the motivation behind this change.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-config.txt | 17 ++++----\n builtin/config.c             | 81 ++++++++++++++++++++++++------------\n t/t1300-config.sh            |  6 +--\n 3 files changed, 68 insertions(+), 36 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex ef46520c15..65c645d461 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -15,8 +15,8 @@ SYNOPSIS\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>]\n 'git config' [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\n-'git config' [<file-option>] -e | --edit\n \n DESCRIPTION\n -----------\n@@ -98,6 +98,11 @@ rename-section::\n remove-section::\n \tRemove the given section from the configuration file.\n \n+edit::\n+\tOpens an editor to modify the specified config file; either\n+\t`--system`, `--global`, `--local` (default), `--worktree`, or\n+\t`--file <config-file>`.\n+\n [[OPTIONS]]\n OPTIONS\n -------\n@@ -274,12 +279,6 @@ Valid `<type>`'s include:\n \tWhen the color setting for `name` is undefined, the command uses\n \t`color.ui` as fallback.\n \n--e::\n---edit::\n-\tOpens an editor to modify the specified config file; either\n-\t`--system`, `--global`, `--local` (default), `--worktree`, or\n-\t`--file <config-file>`.\n-\n --[no-]includes::\n \tRespect `include.*` directives in config files when looking up\n \tvalues. Defaults to `off` when a specific file is given (e.g.,\n@@ -336,6 +335,10 @@ recommended to migrate to the new syntax.\n --remove-section <name>::\n \tReplaced by `git config remove-section <name>`.\n \n+-e::\n+--edit::\n+\tReplaced by `git config edit`.\n+\n CONFIGURATION\n -------------\n `pager.config` is only respected when listing configuration, i.e., when\ndiff --git a/builtin/config.c b/builtin/config.c\nindex cea03fb517..8f7fa8f31a 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -22,6 +22,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config unset [<file-option>] [--all] [--value=<value>] [--fixed-value] <name> <value>\"),\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tN_(\"git config remove-section [<file-option>] <name>\"),\n+\tN_(\"git config edit [<file-option>]\"),\n \tNULL\n };\n \n@@ -55,6 +56,11 @@ static const char *const builtin_config_remove_section_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_config_edit_usage[] = {\n+\tN_(\"git config edit [<file-option>]\"),\n+\tNULL\n+};\n+\n static char *key;\n static regex_t *key_regexp;\n static const char *value_pattern;\n@@ -1011,6 +1017,53 @@ static int cmd_config_remove_section(int argc, const char **argv, const char *pr\n \treturn 0;\n }\n \n+static int show_editor(void)\n+{\n+\tchar *config_file;\n+\n+\tif (!given_config_source.file && !startup_info->have_repository)\n+\t\tdie(_(\"not in a git directory\"));\n+\tif (given_config_source.use_stdin)\n+\t\tdie(_(\"editing stdin is not supported\"));\n+\tif (given_config_source.blob)\n+\t\tdie(_(\"editing blobs is not supported\"));\n+\tgit_config(git_default_config, NULL);\n+\tconfig_file = given_config_source.file ?\n+\t\t\txstrdup(given_config_source.file) :\n+\t\t\tgit_pathdup(\"config\");\n+\tif (use_global_config) {\n+\t\tint fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);\n+\t\tif (fd >= 0) {\n+\t\t\tchar *content = default_user_config();\n+\t\t\twrite_str_in_full(fd, content);\n+\t\t\tfree(content);\n+\t\t\tclose(fd);\n+\t\t}\n+\t\telse if (errno != EEXIST)\n+\t\t\tdie_errno(_(\"cannot create configuration file %s\"), config_file);\n+\t}\n+\tlaunch_editor(config_file, NULL, NULL);\n+\tfree(config_file);\n+\n+\treturn 0;\n+}\n+\n+static int cmd_config_edit(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option opts[] = {\n+\t\tCONFIG_LOCATION_OPTIONS,\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, builtin_config_edit_usage, 0);\n+\tcheck_write();\n+\tcheck_argc(argc, 0, 0);\n+\n+\thandle_config_location(prefix);\n+\n+\treturn show_editor();\n+}\n+\n static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"list\", &subcommand, cmd_config_list),\n \tOPT_SUBCOMMAND(\"get\", &subcommand, cmd_config_get),\n@@ -1018,6 +1071,7 @@ static struct option builtin_subcommand_options[] = {\n \tOPT_SUBCOMMAND(\"unset\", &subcommand, cmd_config_unset),\n \tOPT_SUBCOMMAND(\"rename-section\", &subcommand, cmd_config_rename_section),\n \tOPT_SUBCOMMAND(\"remove-section\", &subcommand, cmd_config_remove_section),\n+\tOPT_SUBCOMMAND(\"edit\", &subcommand, cmd_config_edit),\n \tOPT_END(),\n };\n \n@@ -1144,32 +1198,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t\t}\n \t}\n \telse if (actions == ACTION_EDIT) {\n-\t\tchar *config_file;\n-\n-\t\tcheck_argc(argc, 0, 0);\n-\t\tif (!given_config_source.file && !startup_info->have_repository)\n-\t\t\tdie(_(\"not in a git directory\"));\n-\t\tif (given_config_source.use_stdin)\n-\t\t\tdie(_(\"editing stdin is not supported\"));\n-\t\tif (given_config_source.blob)\n-\t\t\tdie(_(\"editing blobs is not supported\"));\n-\t\tgit_config(git_default_config, NULL);\n-\t\tconfig_file = given_config_source.file ?\n-\t\t\t\txstrdup(given_config_source.file) :\n-\t\t\t\tgit_pathdup(\"config\");\n-\t\tif (use_global_config) {\n-\t\t\tint fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666);\n-\t\t\tif (fd >= 0) {\n-\t\t\t\tchar *content = default_user_config();\n-\t\t\t\twrite_str_in_full(fd, content);\n-\t\t\t\tfree(content);\n-\t\t\t\tclose(fd);\n-\t\t\t}\n-\t\t\telse if (errno != EEXIST)\n-\t\t\t\tdie_errno(_(\"cannot create configuration file %s\"), config_file);\n-\t\t}\n-\t\tlaunch_editor(config_file, NULL, NULL);\n-\t\tfree(config_file);\n+\t\tret = show_editor();\n \t}\n \telse if (actions == ACTION_SET) {\n \t\tcheck_write();\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 9d71ec1260..f3c4d28e06 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -666,7 +666,7 @@ test_expect_success 'setting a value in stdin is an error' '\n '\n \n test_expect_success 'editing stdin is an error' '\n-\ttest_must_fail git config --file - --edit\n+\ttest_must_fail git config ${mode_prefix}edit --file -\n '\n \n test_expect_success 'refer config from subdirectory' '\n@@ -1768,7 +1768,7 @@ test_expect_success 'command line overrides environment config' '\n test_expect_success 'git config --edit works' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n-\tGIT_EDITOR=\"echo [test]value=yes >\" git config -f tmp --edit &&\n+\tGIT_EDITOR=\"echo [test]value=yes >\" git config ${mode_prefix}edit -f tmp &&\n \tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n@@ -1777,7 +1777,7 @@ test_expect_success 'git config --edit respects core.editor' '\n \tgit config -f tmp test.value no &&\n \techo test.value=yes >expect &&\n \ttest_config core.editor \"echo [test]value=yes >\" &&\n-\tgit config -f tmp --edit &&\n+\tgit config ${mode_prefix}edit -f tmp &&\n \tgit config ${mode_prefix}list -f tmp >actual &&\n \ttest_cmp expect actual\n '\n-- \n2.45.0\n\n"},{"id":"494137","messageId":"97a48ab81dba0ab03cc3c1b648555a5fab79ee0a.1714982328.git.ps@pks.im","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"[PATCH v5 14/14] builtin/config: display subcommand help","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T08:56:57Z","receivedAt":"2024-05-06T08:57:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Until now, `git config -h` would have printed help for the old-style\nsyntax. Now that all modes have proper subcommands though it is\npreferable to instead display the subcommand help.\n\nDrop the `NO_INTERNAL_HELP` flag to do so. While at it, drop the help\nmismatch in t0450 and add the `--get-colorbool` option to the usage such\nthat git-config(1)'s synopsis and `git config -h` match.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/config.c            | 5 +++--\n t/t0450/txt-help-mismatches | 1 -\n 2 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 8f7fa8f31a..80aa9d8a66 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -23,6 +23,7 @@ static const char *const builtin_config_usage[] = {\n \tN_(\"git config rename-section [<file-option>] <old-name> <new-name>\"),\n \tN_(\"git config remove-section [<file-option>] <name>\"),\n \tN_(\"git config edit [<file-option>]\"),\n+\tN_(\"git config [<file-option>] --get-colorbool <name> [<stdout-is-tty>]\"),\n \tNULL\n };\n \n@@ -1093,10 +1094,10 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n \t * us when parsing the legacy-style modes that don't use subcommands.\n \t */\n \targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n-\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\t\t     PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_ARGV0|PARSE_OPT_KEEP_UNKNOWN_OPT);\n \tif (subcommand) {\n \t\targc = parse_options(argc, argv, prefix, builtin_subcommand_options, builtin_config_usage,\n-\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_NO_INTERNAL_HELP|PARSE_OPT_KEEP_UNKNOWN_OPT);\n+\t\t       PARSE_OPT_SUBCOMMAND_OPTIONAL|PARSE_OPT_KEEP_UNKNOWN_OPT);\n \t\treturn subcommand(argc, argv, prefix);\n \t}\n \ndiff --git a/t/t0450/txt-help-mismatches b/t/t0450/txt-help-mismatches\nindex a0777acd66..28003f18c9 100644\n--- a/t/t0450/txt-help-mismatches\n+++ b/t/t0450/txt-help-mismatches\n@@ -10,7 +10,6 @@ checkout\n checkout-index\n clone\n column\n-config\n credential\n credential-cache\n credential-store\n-- \n2.45.0\n\n"},{"id":"494139","messageId":"ZjikMpvHnTK01YM6@tanuki","threadId":"61061","inReplyTo":"CAOLa=ZR02Jf3-4zT9gMzVmzS1JNR1MCLDHCV+utR3B-N_acK=w@mail.gmail.com","subject":"Re: [PATCH v4 03/14] builtin/config: move \"fixed-value\" option to correct group","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-06T09:34:42Z","receivedAt":"2024-05-06T09:34:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, May 03, 2024 at 05:28:38AM -0700, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > The `--fixed-value` option can be used to alter how the value-pattern\n> > parameter is interpreted for the various submodes of git-config(1). But\n> > while it is an option, it is currently listed as part of the submodes\n> > group the command, which is wrong.\n> >\n> \n> Isn't it currently listed as part of the 'Action' group? Also I'm not\n> sure if the last sentence needs to be corrected here.\n\nIt's what I actually intended to say, yes. But I agree with you that\n\"action\" feels like the more reasonable way to put it given that we also\ncall them \"actions\" in code.\n\nAnd yes, the last sentence needs to be corrected :)\n\nPatrick\n\n> >\n> > Move the option to the \"Other\" group, which hosts the various options\n> > known to git-config(1).\n> >\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >  builtin/config.c | 2 +-\n> >  1 file changed, 1 insertion(+), 1 deletion(-)\n> >\n> > diff --git a/builtin/config.c b/builtin/config.c\n> > index 59ae5996eb..054019b70c 100644\n> > --- a/builtin/config.c\n> > +++ b/builtin/config.c\n> > @@ -643,7 +643,6 @@ static struct option builtin_config_options[] = {\n> >  \tOPT_BIT(0, \"rename-section\", &actions, N_(\"rename section: old-name new-name\"), ACTION_RENAME_SECTION),\n> >  \tOPT_BIT(0, \"remove-section\", &actions, N_(\"remove a section: name\"), ACTION_REMOVE_SECTION),\n> >  \tOPT_BIT('l', \"list\", &actions, N_(\"list all\"), ACTION_LIST),\n> > -\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n> >  \tOPT_BIT('e', \"edit\", &actions, N_(\"open an editor\"), ACTION_EDIT),\n> >  \tOPT_BIT(0, \"get-color\", &actions, N_(\"find the color configured: slot [default]\"), ACTION_GET_COLOR),\n> >  \tOPT_BIT(0, \"get-colorbool\", &actions, N_(\"find the color setting: slot [stdout-is-tty]\"), ACTION_GET_COLORBOOL),\n> > @@ -663,6 +662,7 @@ static struct option builtin_config_options[] = {\n> >  \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n> >  \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n> >  \tOPT_STRING(0, \"comment\", &comment_arg, N_(\"value\"), N_(\"human-readable comment string (# will be prepended as needed)\")),\n> > +\tOPT_BOOL(0, \"fixed-value\", &fixed_value, N_(\"use string equality when comparing values to 'value-pattern'\")),\n> >  \tOPT_END(),\n> >  };\n> >\n> > --\n> > 2.45.0\n\n\n"},{"id":"494146","messageId":"CAOLa=ZRSM6=qFgzMMh=yEK+jPPGknaPHG7u5ah5OZ24ZdSG1cA@mail.gmail.com","threadId":"61061","inReplyTo":"ZjiNr-PUue2_mJGr@tanuki","subject":"Re: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-06T11:26:55Z","receivedAt":"2024-05-06T11:26:57Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Fri, May 03, 2024 at 06:08:57AM -0700, Karthik Nayak wrote:\n>> Patrick Steinhardt <ps@pks.im> writes:\n>>\n>> > While git-config(1) has several modes, those modes are not exposed with\n>> > subcommands but instead by specifying e.g. `--unset` or `--list`. This\n>>\n>> s/specifying/specifying flags/ perhaps?\n>>\n>> > user interface is not really in line with how our more modern commands\n>> > work, where it is a lot more customary to say e.g. `git remote list`.\n>>\n>> Tangent: I totally agree with the patch, but it would be nice to have a\n>> 'DesigningCommands' document which would highlight UX do's and don'ts.\n>> It would be nice to add that as reference in discussions.\n>\n> I agree that we should have that discussion, but feel like it should be\n> part of a separate patch series. If I were to adopt that into this\n> series I very much think that the resulting discussions would take quite\n> a while to settle.\n>\n> I'll start the discussion soonish in a separate thread.\n>\n\nOfcourse, not part of this series. I was merely using this patch to\nbring up that topic. Thanks, will stay tuned.\n\n> [snip]\n>> > @@ -2590,7 +2608,7 @@ test_expect_success '--fixed-value uses exact string matching' '\n>> >  \ttest_cmp expect actual &&\n>> >\n>> >  \tgit config --file=config --fixed-value --replace-all fixed.test bogus \"$META\" &&\n>> > -\tgit config --file=config --list >actual &&\n>> > +\tgit config ${mode_prefix}list --file=config >actual &&\n>> >  \tcat >expect <<-EOF &&\n>> >  \tfixed.test=bogus\n>> >  \tfixed.test=bogus\n>> > @@ -2751,4 +2769,6 @@ test_expect_success 'specifying multiple modes causes failure' '\n>> >  \ttest_cmp expect err\n>> >  '\n>> >\n>> > +done\n>> > +\n>> >\n>> Nit: Wouldn't it be better if the tests are indented here? That way you\n>> know it's part of a loop.\n>\n> We would basically have to reindent 2500 lines of code. I don't think\n> that'd be a helpful to reviewers :)\n>\n> Patrick\n\nFair enough.\n\nKarthik\n"},{"id":"494147","messageId":"CAOLa=ZSK-DxT1J3ZwvaS1kSQS3f-B11yftb5-F5Wu718VQYUxw@mail.gmail.com","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"Re: [PATCH v5 00/14] builtin/config: introduce subcommands","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2024-05-06T11:30:21Z","receivedAt":"2024-05-06T11:30:23Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Hi,\n>\n> this is the fifth and hopefully last version of my patch sthat\n> introduces subcommands into git-config(1).\n>\n> The only changes compared to v4 are some fixes to commit messages.\n> Otherwise I'm not aware of any other feedback that would need to be\n> addressed.\n>\n> Patrick\n>\n> Patrick Steinhardt (14):\n>   config: clarify memory ownership when preparing comment strings\n>   builtin/config: move option array around\n>   builtin/config: move \"fixed-value\" option to correct group\n>   builtin/config: use `OPT_CMDMODE()` to specify modes\n>   builtin/config: pull out function to handle config location\n>   builtin/config: pull out function to handle `--null`\n>   builtin/config: introduce \"list\" subcommand\n>   builtin/config: introduce \"get\" subcommand\n>   builtin/config: introduce \"set\" subcommand\n>   builtin/config: introduce \"unset\" subcommand\n>   builtin/config: introduce \"rename-section\" subcommand\n>   builtin/config: introduce \"remove-section\" subcommand\n>   builtin/config: introduce \"edit\" subcommand\n>   builtin/config: display subcommand help\n>\n>  Documentation/git-config.txt | 219 ++++++++-------\n>  builtin/config.c             | 512 ++++++++++++++++++++++++++++-------\n>  config.c                     |  16 +-\n>  config.h                     |   2 +-\n>  t/t0450/txt-help-mismatches  |   1 -\n>  t/t1300-config.sh            | 432 +++++++++++++++++------------\n>  6 files changed, 812 insertions(+), 370 deletions(-)\n>\n> Range-diff against v4:\n>  1:  3aa26d5bff !  1:  881d2b5426 config: clarify memory ownership when preparing comment strings\n>     @@ Commit message\n>          not like this micro-optimization really matters. Thus, callers are now\n>          always responsible for freeing the value.\n>\n>     +    Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>     +\n>       ## builtin/config.c ##\n>      @@ builtin/config.c: static struct config_options config_options;\n>       static int show_origin;\n>  2:  8f0804ab48 =  2:  66dffaa8f2 builtin/config: move option array around\n>  3:  ddcd8031d7 !  3:  36abda0e02 builtin/config: move \"fixed-value\" option to correct group\n>     @@ Commit message\n>          builtin/config: move \"fixed-value\" option to correct group\n>\n>          The `--fixed-value` option can be used to alter how the value-pattern\n>     -    parameter is interpreted for the various submodes of git-config(1). But\n>     -    while it is an option, it is currently listed as part of the submodes\n>     -    group the command, which is wrong.\n>     +    parameter is interpreted for the various actions of git-config(1). But\n>     +    while it is an option, it is currently listed as part of the actions\n>     +    group, which is wrong.\n>\n>          Move the option to the \"Other\" group, which hosts the various options\n>          known to git-config(1).\n>  4:  1bc3918840 =  4:  34b66f9c87 builtin/config: use `OPT_CMDMODE()` to specify modes\n>  5:  3754812309 =  5:  4f90f206e7 builtin/config: pull out function to handle config location\n>  6:  cb1714c493 =  6:  df1a6f14e6 builtin/config: pull out function to handle `--null`\n>  7:  b3f3c3ba6a !  7:  1df76a9970 builtin/config: introduce \"list\" subcommand\n>     @@ Commit message\n>          builtin/config: introduce \"list\" subcommand\n>\n>          While git-config(1) has several modes, those modes are not exposed with\n>     -    subcommands but instead by specifying e.g. `--unset` or `--list`. This\n>     -    user interface is not really in line with how our more modern commands\n>     -    work, where it is a lot more customary to say e.g. `git remote list`.\n>     -    Furthermore, to add to the confusion, git-config(1) also allows the user\n>     -    to request modes implicitly by just specifying the correct number of\n>     -    arguments. Thus, `git config foo.bar` will retrieve the value of\n>     -    \"foo.bar\" while `git config foo.bar baz` will set it to \"baz\".\n>     +    subcommands but instead by specifying action flags like `--unset` or\n>     +    `--list`. This user interface is not really in line with how our more\n>     +    modern commands work, where it is a lot more customary to say e.g. `git\n>     +    remote list`. Furthermore, to add to the confusion, git-config(1) also\n>     +    allows the user to request modes implicitly by just specifying the\n>     +    correct number of arguments. Thus, `git config foo.bar` will retrieve\n>     +    the value of \"foo.bar\" while `git config foo.bar baz` will set it to\n>     +    \"baz\".\n>\n>          Overall, this makes for a confusing interface that could really use a\n>          makeover. It hurts discoverability of what you can do with git-config(1)\n>  8:  0e6da909ac =  8:  29676b81e0 builtin/config: introduce \"get\" subcommand\n>  9:  8a623a31b9 =  9:  94afb5a5b7 builtin/config: introduce \"set\" subcommand\n> 10:  e25e5b69cd = 10:  e525c2326a builtin/config: introduce \"unset\" subcommand\n> 11:  f24008d356 = 11:  a797889890 builtin/config: introduce \"rename-section\" subcommand\n> 12:  fc2ddd3201 = 12:  8ec214755e builtin/config: introduce \"remove-section\" subcommand\n> 13:  4c2d817eff = 13:  1893c23afc builtin/config: introduce \"edit\" subcommand\n> 14:  4c351b12b8 = 14:  97a48ab81d builtin/config: display subcommand help\n>\n> base-commit: d4cc1ec35f3bcce816b69986ca41943f6ce21377\n> --\n> 2.45.0\n\nThe range diff looks good. Overall I have nothing more to add to this\nseries.\n\nThanks\nKarthik\n"},{"id":"494166","messageId":"xmqqttjazwwa.fsf@gitster.g","threadId":"61061","inReplyTo":"ZjiL7vu5kCVwpsLd@tanuki","subject":"Re: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-06T17:13:25Z","receivedAt":"2024-05-06T17:13:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> ... I was pondering\n> whether we want to introduce a document as part of that patch series\n> that starts to keep track of upcoming removals for a potential Git 3.0\n> release.\n\nFinally somebody has bit it ;-)  In the 2.44 cycle, I wrote\n\n    The RelNotes symbolic link says we are now working towards Git 2.44.\n    It may not be a bad idea to reflect on what technical debt and UI\n    warts we have accumulated so far to see if we have enough of them to\n    start planning for a breaking Git 3.0 release (or, of course, keep\n    incrementally improve the system, which is much more preferrable---\n    continuity and stability is good).  End of year being a relatively\n    quiet period, it may be a good time to think about your favorite pet\n    peeve, to be discussed early next year.\n\nin a few of the \"What's cooking\" reports.\n\n> There are multiple items that could be added:\n>\n>   - Removal of the old syntax of git-config(1).\n>\n>   - Removal of the dumb HTTP transport.\n>\n>   - Removal of `info/grafts`.\n>\n> There are probably other items.\n\nA list of things I can think of that I won't be the primary advocate\nfor but I do not mind too terribly if we had champions for the\ntopics are attached at the end.\n\n> In any case, the old actions are here to stay for the foreseeable future\n> until we commit to a breaking major release.\n\nTrue.\n\n> Thanks for the thorough explanation, I have nothing to add!\n\nYou could have avoided it if you copied some from the initial cover\nletter to each round (i.e. preparing the series to be read by some\nfolks who did not read an earlier round).\n\n\nPossible additional Git 3.0 items:\n\n - Removing \"git http-push\" to push over HTTP/DAV.\n\n - Removing support of `$GIT_DIR/branches/` from remote.c API.\n\n - Removing \"git update-server-info\".\n\n - Removing \"git annotate\".\n\n - Removing \"gitweb\" and \"git instaweb\".\n\n - Removing \"git filter-branch\", now we have a better alternative\n   \"git filter-repo\".\n\n - Removing discovery of hook script in \"$GIT_DIR/hooks/\", in favor\n   of the configuration variables that point at them.\n\n - Switching to SHA-256 as the default hash algorithm.\n\n - Switching to reftable as the default ref backend.\n\n - Switching the hardcoded default branch name away from \"master\" to\n   \"main\".\n\n - Declaring that \"git restore\" and \"git switch\" were failed\n   experiments and deprecating them.\n\n - Declaring that \"git submodule\" was a failed experiment and\n   deprecating it.\n"},{"id":"494176","messageId":"04d001da9fe3$e9c2eef0$bd48ccd0$@nexbridge.com","threadId":"61061","inReplyTo":"xmqqttjazwwa.fsf@gitster.g","subject":"RE: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-05-06T18:33:32Z","receivedAt":"2024-05-06T18:33:53Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Monday, May 6, 2024 1:13 PM, Junio C Hamano wrote:\n>Patrick Steinhardt <ps@pks.im> writes:\n>\n>> ... I was pondering\n>> whether we want to introduce a document as part of that patch series\n>> that starts to keep track of upcoming removals for a potential Git 3.0\n>> release.\n>\n>Finally somebody has bit it ;-)  In the 2.44 cycle, I wrote\n>\n>    The RelNotes symbolic link says we are now working towards Git 2.44.\n>    It may not be a bad idea to reflect on what technical debt and UI\n>    warts we have accumulated so far to see if we have enough of them to\n>    start planning for a breaking Git 3.0 release (or, of course, keep\n>    incrementally improve the system, which is much more preferrable---\n>    continuity and stability is good).  End of year being a relatively\n>    quiet period, it may be a good time to think about your favorite pet\n>    peeve, to be discussed early next year.\n>\n>in a few of the \"What's cooking\" reports.\n>\n>> There are multiple items that could be added:\n>>\n>>   - Removal of the old syntax of git-config(1).\n>>\n>>   - Removal of the dumb HTTP transport.\n>>\n>>   - Removal of `info/grafts`.\n>>\n>> There are probably other items.\n>\n>A list of things I can think of that I won't be the primary advocate for but I do not\n>mind too terribly if we had champions for the topics are attached at the end.\n>\n>> In any case, the old actions are here to stay for the foreseeable\n>> future until we commit to a breaking major release.\n>\n>True.\n>\n>> Thanks for the thorough explanation, I have nothing to add!\n>\n>You could have avoided it if you copied some from the initial cover letter to each\n>round (i.e. preparing the series to be read by some folks who did not read an earlier\n>round).\n>\n>\n>Possible additional Git 3.0 items:\n>\n> - Removing \"git http-push\" to push over HTTP/DAV.\n>\n> - Removing support of `$GIT_DIR/branches/` from remote.c API.\n>\n> - Removing \"git update-server-info\".\n>\n> - Removing \"git annotate\".\n\nI must have missed this... I thought annotate was replacing blame.\n\n> - Removing \"gitweb\" and \"git instaweb\".\n>\n> - Removing \"git filter-branch\", now we have a better alternative\n>   \"git filter-repo\".\n>\n> - Removing discovery of hook script in \"$GIT_DIR/hooks/\", in favor\n>   of the configuration variables that point at them.\n>\n> - Switching to SHA-256 as the default hash algorithm.\n>\n> - Switching to reftable as the default ref backend.\n>\n> - Switching the hardcoded default branch name away from \"master\" to\n>   \"main\".\n>\n> - Declaring that \"git restore\" and \"git switch\" were failed\n>   experiments and deprecating them.\n\nThis 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.\n\n> - Declaring that \"git submodule\" was a failed experiment and\n>   deprecating it.\n\nThis 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.\n\nIf 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.\n\n--Randall\n\n"},{"id":"494177","messageId":"3d84f2eac9b03eda814332d73b5d5ba6@manjaro.org","threadId":"61061","inReplyTo":"04d001da9fe3$e9c2eef0$bd48ccd0$@nexbridge.com","subject":"Re: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Dragan Simic","fromEmail":"dsimic@manjaro.org","sentAt":"2024-05-06T18:45:00Z","receivedAt":"2024-05-06T18:45:03Z","isPatch":true,"sender":{"key":"dsimic@manjaro.org","avatar":null},"body":"On 2024-05-06 20:33, rsbecker@nexbridge.com wrote:\n> On Monday, May 6, 2024 1:13 PM, Junio C Hamano wrote:\n>> - Declaring that \"git restore\" and \"git switch\" were failed\n>>   experiments and deprecating them.\n> \n> This restore and switch have broad use in my community. I do not\n> consider them failed experiments at all, but essential in scripts and\n> usage. Removing these would block migration to git 3, in my view. I\n> have actually been thinking about doing a YouTube video on these\n> commands.\n\nHuh, I was also really surprised to see \"git switch\" and \"git restore\"\non the planned deprecated feature list.  For example, I use \"git switch\"\nliterally all the time, and \"git restore\" also sees a fair amount of \nuse.\nThus, I think that deprecating these two features is a really bad idea.\n\n>> - Declaring that \"git submodule\" was a failed experiment and\n>>   deprecating it.\n> \n> This would be a very high-impact decision. Aside from my community's\n> use (and my own company's dependence on submodules), this will break\n> large numbers of GNU-based projects that use submodules for\n> distribution, including build-aux. I would suggest staying away from\n> this decision. Submodules have definite value. Please keep them.\n\nI agree that deprecating \"git submodule\" isn't a good idea.\n"},{"id":"494201","messageId":"Zjk73l5l6AqQOz6N@nand.local","threadId":"61061","inReplyTo":"cover.1714982328.git.ps@pks.im","subject":"Re: [PATCH v5 00/14] builtin/config: introduce subcommands","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-05-06T20:21:50Z","receivedAt":"2024-05-06T20:21:52Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Mon, May 06, 2024 at 10:55:51AM +0200, Patrick Steinhardt wrote:\n> Hi,\n>\n> this is the fifth and hopefully last version of my patch sthat\n> introduces subcommands into git-config(1).\n>\n> The only changes compared to v4 are some fixes to commit messages.\n> Otherwise I'm not aware of any other feedback that would need to be\n> addressed.\n\nThanks for the new round. I took a close look through the series, and\ncouldn't see any remaining issues. I appreciate the translation guide\nyou included in the documentation to indicate \"git config --add\" is\nreplaced by things like \"git config set --append\".\n\nThanks,\nTaylor\n"},{"id":"494203","messageId":"04ef01da9ff5$700fab90$502f02b0$@nexbridge.com","threadId":"61061","inReplyTo":"Zjk73l5l6AqQOz6N@nand.local","subject":"RE: [PATCH v5 00/14] builtin/config: introduce subcommands","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-05-06T20:38:59Z","receivedAt":"2024-05-06T20:39:15Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Monday, May 6, 2024 4:22 PM, Taylor Blau wrote:\n>On Mon, May 06, 2024 at 10:55:51AM +0200, Patrick Steinhardt wrote:\n>> Hi,\n>>\n>> this is the fifth and hopefully last version of my patch sthat\n>> introduces subcommands into git-config(1).\n>>\n>> The only changes compared to v4 are some fixes to commit messages.\n>> Otherwise I'm not aware of any other feedback that would need to be\n>> addressed.\n>\n>Thanks for the new round. I took a close look through the series, and couldn't see\n>any remaining issues. I appreciate the translation guide you included in the\n>documentation to indicate \"git config --add\" is replaced by things like \"git config set\n>--append\".\n\nPlease 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.\n\n"},{"id":"494206","messageId":"xmqqa5l2y6aa.fsf_-_@gitster.g","threadId":"61061","inReplyTo":"xmqqttjazwwa.fsf@gitster.g","subject":"Git 3.0?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-06T21:33:33Z","receivedAt":"2024-05-06T21:33:43Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> ... I was pondering\n>> whether we want to introduce a document as part of that patch series\n>> that starts to keep track of upcoming removals for a potential Git 3.0\n>> release.\n>\n> Finally somebody has bit it ;-)  In the 2.44 cycle, I wrote\n>\n>     The RelNotes symbolic link says we are now working towards Git 2.44.\n>     It may not be a bad idea to reflect on what technical debt and UI\n>     warts we have accumulated so far to see if we have enough of them to\n>     start planning for a breaking Git 3.0 release (or, of course, keep\n>     incrementally improve the system, which is much more preferrable---\n>     continuity and stability is good).  End of year being a relatively\n>     quiet period, it may be a good time to think about your favorite pet\n>     peeve, to be discussed early next year.\n>\n> in a few of the \"What's cooking\" reports.\n>\n>> There are multiple items that could be added:\n>>\n>>   - Removal of the old syntax of git-config(1).\n>>\n>>   - Removal of the dumb HTTP transport.\n>>\n>>   - Removal of `info/grafts`.\n>>\n>> There are probably other items.\n> \n> A list of things I can think of that I won't be the primary advocate\n> for but I do not mind too terribly if we had champions for the\n> topics are attached at the end.\n\nJust to avoid unnecessary misunderstanding, as I said, what I listed\nare not my proposals.  Rather, take them together with Patrick's list\nthat they are invitations for others to let their imagination go wild,\ntrying to come up as many ideas regardless of how good they are, sort\nof the initial \"brain-storming\" phase of a discussion.  We will need\nto cull bad ideas, pick good ones, and refine what we are going to\nimplement in later stages, but this is not a time to shoot down what\nyou do not like, yet.  It is instead for you to add your pet peeve.\n"},{"id":"494215","messageId":"Zjmn7_C_eiSo0rBL@tanuki","threadId":"61061","inReplyTo":"xmqqttjazwwa.fsf@gitster.g","subject":"Re: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-07T04:02:55Z","receivedAt":"2024-05-07T04:03:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, May 06, 2024 at 10:13:25AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > ... I was pondering\n> > whether we want to introduce a document as part of that patch series\n> > that starts to keep track of upcoming removals for a potential Git 3.0\n> > release.\n> \n> Finally somebody has bit it ;-)  In the 2.44 cycle, I wrote\n> \n>     The RelNotes symbolic link says we are now working towards Git 2.44.\n>     It may not be a bad idea to reflect on what technical debt and UI\n>     warts we have accumulated so far to see if we have enough of them to\n>     start planning for a breaking Git 3.0 release (or, of course, keep\n>     incrementally improve the system, which is much more preferrable---\n>     continuity and stability is good).  End of year being a relatively\n>     quiet period, it may be a good time to think about your favorite pet\n>     peeve, to be discussed early next year.\n> \n> in a few of the \"What's cooking\" reports.\n\nI know :) I have been thinking about this on and off, but never felt\nlike pushing for it yet.\n\n> > There are multiple items that could be added:\n> >\n> >   - Removal of the old syntax of git-config(1).\n> >\n> >   - Removal of the dumb HTTP transport.\n> >\n> >   - Removal of `info/grafts`.\n> >\n> > There are probably other items.\n> \n> A list of things I can think of that I won't be the primary advocate\n> for but I do not mind too terribly if we had champions for the\n> topics are attached at the end.\n\nI'd be happy to champion for each of those.\n\n> > In any case, the old actions are here to stay for the foreseeable future\n> > until we commit to a breaking major release.\n> \n> True.\n> \n> > Thanks for the thorough explanation, I have nothing to add!\n> \n> You could have avoided it if you copied some from the initial cover\n> letter to each round (i.e. preparing the series to be read by some\n> folks who did not read an earlier round).\n\nFair enough. I should have known that this part is indeed quite\nimportant to the whole series and included it in the newer cover\nletters.\n\n> \n> Possible additional Git 3.0 items:\n\nSome pretty controversial takes in here ;) I guess that's by design.\n\n>  - Removing \"git http-push\" to push over HTTP/DAV.\n> \n>  - Removing support of `$GIT_DIR/branches/` from remote.c API.\n> \n>  - Removing \"git update-server-info\".\n\nYes, all of these are quite sensible.\n\n>  - Removing \"git annotate\".\n\nI don't care much about this one, but it's nice indeed to get rid of\nduplicate functionality.\n\n>  - Removing \"gitweb\" and \"git instaweb\".\n\nI don't care about this one, to be honest. It's basically unmaintained\nthough as far as I know, so we might just be accepting the status quo.\n\n>  - Removing \"git filter-branch\", now we have a better alternative\n>    \"git filter-repo\".\n\nThis one I'm sceptical about. The one upside of git-filter-branch(1) is\nthat it's part of Git itself, whereas git-filter-repo(1) is not. I thus\nthink that a prerequisite here should be that we first land the new\nscript in Git before actually deprecating the old tool such that things\nremain discoverable. And upstreaming git-filter-repo(1) would be a\nworthy goal by itself already.\n\n>  - Removing discovery of hook script in \"$GIT_DIR/hooks/\", in favor\n>    of the configuration variables that point at them.\n\nThose have been an attack vector in the past, and I think that using\nconfig to set up hooks is quite a bit saner. But of course, we first\nneed to land this topic :)\n\n>  - Switching to SHA-256 as the default hash algorithm.\n> \n>  - Switching to reftable as the default ref backend.\n> \n>  - Switching the hardcoded default branch name away from \"master\" to\n>    \"main\".\n\nAll three of those may be nice. The first one is going to be hardest as\nit requires support in forges. GitLab started to support SHA256 a month\nago, but it's still experimental. But to the best of my knowledge GitHub\ndoes not yet support SHA256 at all.\n\n>  - Declaring that \"git restore\" and \"git switch\" were failed\n>    experiments and deprecating them.\n\nI use those quite a lot, so it'd be a shame if those went away.\n\n>  - Declaring that \"git submodule\" was a failed experiment and\n>    deprecating it.\n\nWell. I know that most people think that submodules don't work and\nshould just go away, and I count myself as part of that group. But\nrealistically I don't see that as a feasible goal, even more so because\nwe don't actually have a proper replacement. They may be somewhat\nbroken, but there is no better substitute.\n\nYou know, let me take this list and propose it as something like\n\"Documents/UpcomingDeprecations\". This will make the whole discussion\nhere a lot more discoverable.\n\nPatrick\n"},{"id":"494216","messageId":"Zjmo8xMkN-hypRHz@tanuki","threadId":"61061","inReplyTo":"04ef01da9ff5$700fab90$502f02b0$@nexbridge.com","subject":"Re: [PATCH v5 00/14] builtin/config: introduce subcommands","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-07T04:07:15Z","receivedAt":"2024-05-07T04:07:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, May 06, 2024 at 04:38:59PM -0400, rsbecker@nexbridge.com wrote:\n> On Monday, May 6, 2024 4:22 PM, Taylor Blau wrote:\n> >On Mon, May 06, 2024 at 10:55:51AM +0200, Patrick Steinhardt wrote:\n> >> Hi,\n> >>\n> >> this is the fifth and hopefully last version of my patch sthat\n> >> introduces subcommands into git-config(1).\n> >>\n> >> The only changes compared to v4 are some fixes to commit messages.\n> >> Otherwise I'm not aware of any other feedback that would need to be\n> >> addressed.\n> >\n> >Thanks for the new round. I took a close look through the series, and couldn't see\n> >any remaining issues. I appreciate the translation guide you included in the\n> >documentation to indicate \"git config --add\" is replaced by things like \"git config set\n> >--append\".\n> \n> Please make sure that there is a compatibility mode available for some\n> large period. There are huge numbers of scripts in the customer base I\n> deal with that use config extensively. Changing the CLI for this will\n> be dire consequences.\n\nYes, that's a very sensible thing to ask for. I think the bare minimum\nwe should aim for is a year (4 releases), where two years (8 releases)\nfeels a bit more sensible. Do you feel like that is reasonable, or do\nyou think it would be too short?\n\nPatrick\n"},{"id":"494219","messageId":"ZjmrjMN-AxW9rCC-@tanuki","threadId":"61061","inReplyTo":"xmqqa5l2y6aa.fsf_-_@gitster.g","subject":"Re: Git 3.0?","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-05-07T04:18:20Z","receivedAt":"2024-05-07T04:18:27Z","isPatch":false,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, May 06, 2024 at 02:33:33PM -0700, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > Patrick Steinhardt <ps@pks.im> writes:\n> >\n> >> ... I was pondering\n> >> whether we want to introduce a document as part of that patch series\n> >> that starts to keep track of upcoming removals for a potential Git 3.0\n> >> release.\n> >\n> > Finally somebody has bit it ;-)  In the 2.44 cycle, I wrote\n> >\n> >     The RelNotes symbolic link says we are now working towards Git 2.44.\n> >     It may not be a bad idea to reflect on what technical debt and UI\n> >     warts we have accumulated so far to see if we have enough of them to\n> >     start planning for a breaking Git 3.0 release (or, of course, keep\n> >     incrementally improve the system, which is much more preferrable---\n> >     continuity and stability is good).  End of year being a relatively\n> >     quiet period, it may be a good time to think about your favorite pet\n> >     peeve, to be discussed early next year.\n> >\n> > in a few of the \"What's cooking\" reports.\n> >\n> >> There are multiple items that could be added:\n> >>\n> >>   - Removal of the old syntax of git-config(1).\n> >>\n> >>   - Removal of the dumb HTTP transport.\n> >>\n> >>   - Removal of `info/grafts`.\n> >>\n> >> There are probably other items.\n> > \n> > A list of things I can think of that I won't be the primary advocate\n> > for but I do not mind too terribly if we had champions for the\n> > topics are attached at the end.\n> \n> Just to avoid unnecessary misunderstanding, as I said, what I listed\n> are not my proposals.  Rather, take them together with Patrick's list\n> that they are invitations for others to let their imagination go wild,\n> trying to come up as many ideas regardless of how good they are, sort\n> of the initial \"brain-storming\" phase of a discussion.  We will need\n> to cull bad ideas, pick good ones, and refine what we are going to\n> implement in later stages, but this is not a time to shoot down what\n> you do not like, yet.  It is instead for you to add your pet peeve.\n\nJust to avoid starting this discussion deep down in this thread, I plan\nto send an RFC-style patch later today that proposes several potential\ndeprecations for Git 3.0. The intent of that patch will be to trigger\ndiscussion and it is thus by design going to be controversial :)\n\nPatrick\n"},{"id":"494251","messageId":"e364b795-7920-438c-a2e0-ed5940065623@app.fastmail.com","threadId":"61061","inReplyTo":"04d001da9fe3$e9c2eef0$bd48ccd0$@nexbridge.com","subject":"Re: [PATCH v4 07/14] builtin/config: introduce \"list\" subcommand","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2024-05-07T06:20:56Z","receivedAt":"2024-05-07T06:21:19Z","isPatch":true,"sender":{"key":"code@khaugsbakk.name","avatar":"https://avatars.githubusercontent.com/u/2229597?v=4"},"body":"On Mon, May 6, 2024, at 20:33, rsbecker@nexbridge.com wrote:\n> If you really want to get rid of stuff that has limited use instead of\n> submodules, it is worktrees, the benefit of which is reduced given\n> sparce checkouts and fetch depth.\n\nGit-worktrees is very useful. One use-case is having a dedicated\nworktree for an old version of the program/app that you need to work on\nfor whatever reason (using the same worktree for very different versions\ncan confuse build systems and IDEs). These things aren’t covered by\nthose other supposed alternatives (which I never use because I don’t\nwork on big repositories).\n\nMaybe the idea is to use a fetch depth of 1 on N clones instead of N\nworktrees. But then you still have clones laying around in an ad hoc\nmanner compared to having on overview of all the worktrees that you are\nusing from one repository.\n\n-- \nKristoffer Haugsbakk\n\n"},{"id":"523354","messageId":"aI+oQvQgnNtC6DVw@szeder.dev","threadId":"61061","inReplyTo":"29676b81e0151e2982326663f8b64e2a70bec0f8.1714982328.git.ps@pks.im","subject":"Re: [PATCH v5 08/14] builtin/config: introduce \"get\" subcommand","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2025-08-03T18:19:46Z","receivedAt":"2025-08-03T18:19:50Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, May 06, 2024 at 10:56:29AM +0200, Patrick Steinhardt wrote:\n> Introduce a new \"get\" subcommand to git-config(1). Please refer to\n> preceding commits regarding the motivation behind this change.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n\n> @@ -519,25 +522,19 @@ you have to provide a regex matching the value of exactly one line.\n>  To query the value for a given key, do\n>  \n>  ------------\n> -% git config --get core.filemode\n> -------------\n> -\n> -or\n> -\n> -------------\n> -% git config core.filemode\n> +% git config get core.filemode\n>  ------------\n>  \n>  or, to query a multivar:\n>  \n>  ------------\n> -% git config --get core.gitproxy \"for kernel.org$\"\n> +% git config get --value=\"for kernel.org$\" core.gitproxy\n>  ------------\n>  \n>  If you want to know all the values for a multivar, do:\n>  \n>  ------------\n> -% git config --get-all core.gitproxy\n> +% git config get --all --show-names core.gitproxy\n>  ------------\n>  \n>  If you like to live dangerously, you can replace *all* core.gitproxy by a\n> @@ -571,8 +568,8 @@ script:\n>  \n>  ------------\n>  #!/bin/sh\n> -WS=$(git config --get-color color.diff.whitespace \"blue reverse\")\n> -RESET=$(git config --get-color \"\" \"reset\")\n> +WS=$(git config get --type=color --default=\"blue reverse\" color.diff.whitespace)\n> +RESET=$(git config get --type=color --default=\"reset\" \"\")\n>  echo \"${WS}your whitespace color or blue reverse${RESET}\"\n\nIn this example the second command in the postimage doesn't work:\n\n  $ RESET=$(git config get --type=color --default=\"reset\" \"\")\n  error: key does not contain a section: \n\nConsequently, the last command doesn't work as expected anymore,\nbecause the color is not reset at the end of the output.\n\nLuckily, the commands in the preimage still work.\n\n"},{"id":"523362","messageId":"xmqqjz3kdomv.fsf@gitster.g","threadId":"61061","inReplyTo":"aI+oQvQgnNtC6DVw@szeder.dev","subject":"Re: [PATCH v5 08/14] builtin/config: introduce \"get\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-03T22:30:32Z","receivedAt":"2025-08-03T22:30:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n>>  #!/bin/sh\n>> -WS=$(git config --get-color color.diff.whitespace \"blue reverse\")\n>> -RESET=$(git config --get-color \"\" \"reset\")\n>> +WS=$(git config get --type=color --default=\"blue reverse\" color.diff.whitespace)\n>> +RESET=$(git config get --type=color --default=\"reset\" \"\")\n>>  echo \"${WS}your whitespace color or blue reverse${RESET}\"\n>\n> In this example the second command in the postimage doesn't work:\n>\n>   $ RESET=$(git config get --type=color --default=\"reset\" \"\")\n>   error: key does not contain a section: \n>\n> Consequently, the last command doesn't work as expected anymore,\n> because the color is not reset at the end of the output.\n>\n> Luckily, the commands in the preimage still work.\n\nInstead of an empty string as a configuration variable, using\nsomething like no.such.variable that does not exist would also be\na workaround.  I.e.,\n\n  RESET=$(git config get --type=color --default=reset no.such.variable)\n\nWhat is bad about this is that there is no generic variable that is\nguaranteed to be unset.\n\n"},{"id":"526119","messageId":"aMKs4qcEI4RrP_6U@pks.im","threadId":"61061","inReplyTo":"xmqqjz3kdomv.fsf@gitster.g","subject":"Re: [PATCH v5 08/14] builtin/config: introduce \"get\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-11T11:05:06Z","receivedAt":"2025-09-11T11:05:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sun, Aug 03, 2025 at 03:30:32PM -0700, Junio C Hamano wrote:\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n> \n> >>  #!/bin/sh\n> >> -WS=$(git config --get-color color.diff.whitespace \"blue reverse\")\n> >> -RESET=$(git config --get-color \"\" \"reset\")\n> >> +WS=$(git config get --type=color --default=\"blue reverse\" color.diff.whitespace)\n> >> +RESET=$(git config get --type=color --default=\"reset\" \"\")\n> >>  echo \"${WS}your whitespace color or blue reverse${RESET}\"\n> >\n> > In this example the second command in the postimage doesn't work:\n> >\n> >   $ RESET=$(git config get --type=color --default=\"reset\" \"\")\n> >   error: key does not contain a section: \n> >\n> > Consequently, the last command doesn't work as expected anymore,\n> > because the color is not reset at the end of the output.\n> >\n> > Luckily, the commands in the preimage still work.\n> \n> Instead of an empty string as a configuration variable, using\n> something like no.such.variable that does not exist would also be\n> a workaround.  I.e.,\n> \n>   RESET=$(git config get --type=color --default=reset no.such.variable)\n> \n> What is bad about this is that there is no generic variable that is\n> guaranteed to be unset.\n\nIn fact that's even what our tests in t4026 do:\n\n    color()\n    {\n        actual=$(git config --get-color no.such.slot \"$1\") &&\n        test \"$actual\" = \"${2:+$ESC}$2\"\n    }\n\nBut that's certainly not great.\n\nI'd say that the current behaviour is just a plain bug. In fact, there's\neven two bugs:\n\n  1. We set up the pager when asked to print color codes. This does not\n     make any sense whatsoever, as these should never go to the pager\n     but should always be printed to stdout directly.\n\n  2. We don't recognize the special case where we have are asked for a\n     color, have an empty key but a default value.\n\nI'll send a patch series to fix these.\n\nPatrick\n"}]}