{"thread":{"id":"65631","subject":"[PATCH] config: suggest the correct form when key contains \"=\"","startedAt":"2026-05-13T13:58:47Z","lastAt":"2026-06-04T01:09:35Z","messageCount":26,"participants":["Harald Nordgren via GitGitGadget","Junio C Hamano","Harald Nordgren","Kristoffer Haugsbakk"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"543241","messageId":"pull.2302.git.git.1778680725459.gitgitgadget@gmail.com","threadId":"65631","inReplyTo":null,"subject":"[PATCH] config: suggest the correct form when key contains \"=\"","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T13:58:45Z","receivedAt":"2026-05-13T13:58:47Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nWhen a user types \"git config foo.bar=baz\", git_config_parse_key()\nrejects the key with \"error: invalid key: foo.bar=baz\" but gives no\nindication of what the user should have written.  The mistake is a\ncommon one for users who reach for INI-file syntax or for the\n\"--flag=value\" convention used by other command-line tools.\n\nSince \"=\" is never a valid character in a config key, treat its\npresence as a strong signal of this specific mistake and follow the\nerror with a one-line suggestion in the \"(did you mean ...)\" style\nused elsewhere in git, e.g.:\n\n    $ git config pull.rebase=false\n    error: invalid key: pull.rebase=false\n      (did you mean \"git config set pull.rebase false\"?)\n\nThe hint is emitted only when the offending character is \"=\"; other\ninvalid characters (newlines, \"@\", etc.) keep their existing error\nunchanged.\n\nSigned-off-by: Harald Nordgren <harald.nordgren@kostdoktorn.se>\n---\n    config: suggest the correct form when key contains \"=\"\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2302%2FHaraldNordgren%2Fconfig-hint-equals-key-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2302/HaraldNordgren/config-hint-equals-key-v1\nPull-Request: https://github.com/git/git/pull/2302\n\n config.c | 4 ++++\n 1 file changed, 4 insertions(+)\n\ndiff --git a/config.c b/config.c\nindex a1b92fe083..6e658d71d1 100644\n--- a/config.c\n+++ b/config.c\n@@ -580,6 +580,10 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n \t\t\tif (!iskeychar(c) ||\n \t\t\t    (i == baselen + 1 && !isalpha(c))) {\n \t\t\t\terror(_(\"invalid key: %s\"), key);\n+\t\t\t\tif (c == '=')\n+\t\t\t\t\tfprintf_ln(stderr,\n+\t\t\t\t\t\t   _(\"  (did you mean \\\"git config set %.*s %s\\\"?)\"),\n+\t\t\t\t\t\t   (int)i, key, key + i + 1);\n \t\t\t\tgoto out_free_ret_1;\n \t\t\t}\n \t\t\tc = tolower(c);\n\nbase-commit: 59ff4886a579f4bc91e976fe18590b9ae02c7a08\n-- \ngitgitgadget\n"},{"id":"543362","messageId":"xmqqqzndel8c.fsf@gitster.g","threadId":"65631","inReplyTo":"pull.2302.git.git.1778680725459.gitgitgadget@gmail.com","subject":"Re: [PATCH] config: suggest the correct form when key contains \"=\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-14T21:26:27Z","receivedAt":"2026-05-14T21:26:29Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> When a user types \"git config foo.bar=baz\", git_config_parse_key()\n> rejects the key with \"error: invalid key: foo.bar=baz\" but gives no\n> indication of what the user should have written.  The mistake is a\n> common one for users who reach for INI-file syntax or for the\n> \"--flag=value\" convention used by other command-line tools.\n>\n> Since \"=\" is never a valid character in a config key, treat its\n> presence as a strong signal of this specific mistake and follow the\n> error with a one-line suggestion in the \"(did you mean ...)\" style\n> used elsewhere in git, e.g.:\n>\n>     $ git config pull.rebase=false\n>     error: invalid key: pull.rebase=false\n>       (did you mean \"git config set pull.rebase false\"?)\n\nIf the command line were\n\n\tgit config get foo.bar=baz\n\tgit config set foo.bar=baz nitfol\n\nwe shouldn't give an extra \"did you mean?\" at all.\n\nThe only cases you may want to do the \"did you mean?\" I think are\n\n\tgit config foo.bar=baz\n\tgit config set foo.bar=baz\n\nAnd I think git_config_parse_key() is at a way too low level to tell\nin what context we are seeing this faulty key to guess end-user's\nintention to limit our \"did you mean?\"\n\nI also wonder if, given that \"=\" in anywhere other than three-level\nnames, is invalid, we should just start accept\n\n\tgit config foo.bar=baz\n\tgit config set foo.bar=baz\n\nand interpret them as\n\n\tgit config set foo.bar baz\n\nWe of course need to be careful about non-invalid keys, i.e.\n\n\tgit config foo.bar=baz.boo\n\nis a request to read the value of that named variable, i.e.\n\n\t[foo \"bar=baz\"]\n\t\tboo = its value\n\nso either you start offering unsolicited \"did you mean?\" or accepting\ntokens with '=' in them as new style \"set\", you need to be extra\ncareful not to trigger a false positive.\n"},{"id":"543365","messageId":"20260514221602.9918-1-haraldnordgren@gmail.com","threadId":"65631","inReplyTo":"xmqqqzndel8c.fsf@gitster.g","subject":"[PATCH] fetch: add fetch.pruneLocalBranches config","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-05-14T22:16:02Z","receivedAt":"2026-05-14T22:16:06Z","isPatch":true,"body":"> I also wonder if, given that \"=\" in anywhere other than three-level\n> names, is invalid, we should just start accept\n> \n> \tgit config foo.bar=baz\n> \tgit config set foo.bar=baz\n> \n> and interpret them as\n> \n> \tgit config set foo.bar baz\n\nThat sounds good too! Probably even better.\n\n\nHarald\n"},{"id":"543367","messageId":"xmqqecjdea13.fsf@gitster.g","threadId":"65631","inReplyTo":"20260514221602.9918-1-haraldnordgren@gmail.com","subject":"Re: [PATCH] fetch: add fetch.pruneLocalBranches config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-15T01:28:24Z","receivedAt":"2026-05-15T01:28:27Z","isPatch":true,"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> I also wonder if, given that \"=\" in anywhere other than three-level\n>> names, is invalid, we should just start accept\n>> \n>> \tgit config foo.bar=baz\n>> \tgit config set foo.bar=baz\n>> \n>> and interpret them as\n>> \n>> \tgit config set foo.bar baz\n>\n> That sounds good too! Probably even better.\n>\n>\n> Harald\n\nWhy do I get the above, which apparently is a response to my review\nfor\n\n    [PATCH] config: suggest the correct form when key contains \"=\"\n\nunder this thread?  Am I dealing with some sort of mechanical slop?\n"},{"id":"543384","messageId":"20260515075611.59535-1-haraldnordgren@gmail.com","threadId":"65631","inReplyTo":"xmqqecjdea13.fsf@gitster.g","subject":"Email issues","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-05-15T07:56:11Z","receivedAt":"2026-05-15T07:56:15Z","isPatch":false,"body":"> Why do I get the above, which apparently is a response to my review\n> for\n> \n>     [PATCH] config: suggest the correct form when key contains \"=\"\n> \n> under this thread?  Am I dealing with some sort of mechanical slop?\n\nI think the problem here is my email sending process is not good. I edit\nall the emails in Sublime text, where I keep the same file for all\ndifferent threads.\n\nI have the subject line as the first line of the file and like you notice I\nforget to change it sometimes.\n\nI keep each of the topics bookmarked like this, \nhttps://lore.kernel.org/git/xmqqecjdea13.fsf@gitster.g/, and then utilize\nthat like to send the email\n\n```\n  git send-email \\\n    --in-reply-to=xmqqecjdea13.fsf@gitster.g \\\n    --to=gitster@pobox.com \\\n    --cc=git@vger.kernel.org \\\n    --cc=gitgitgadget@gmail.com \\\n    --cc=haraldnordgren@gmail.com \\\n    /path/to/YOUR_REPLY\n```\n\nI tried playing with neomutt and and email client replacement, but that\nadds the complexity of downloading a new mbox file for each reply, it\ndidn't seem easier, but maybe it is.\n\nHow do you handle emails?\n\n\nHarald\n"},{"id":"543393","messageId":"CAHwyqnUmUMrpuuyiRbSrjbOzHVViYhpekn33WwuqeZpzAy_W4w@mail.gmail.com","threadId":"65631","inReplyTo":"xmqqecjdea13.fsf@gitster.g","subject":"Re: [PATCH] fetch: add fetch.pruneLocalBranches config","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-05-15T09:39:01Z","receivedAt":"2026-05-15T09:39:41Z","isPatch":true,"body":"> Why do I get the above, which apparently is a response to my review\n> for\n>\n>     [PATCH] config: suggest the correct form when key contains \"=\"\n>\n> under this thread?  Am I dealing with some sort of mechanical slop?\n\n(Testing plain text email sending via Gmail for a less error-prone\nworkflow, does it still add the CC's correctly?)\n\n\nHarald\n"},{"id":"543397","messageId":"f1421148-dcae-49ee-aff0-a98731558d51@app.fastmail.com","threadId":"65631","inReplyTo":"20260515075611.59535-1-haraldnordgren@gmail.com","subject":"Re: Email issues","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-15T12:02:21Z","receivedAt":"2026-05-15T12:02:42Z","isPatch":false,"body":"On Fri, May 15, 2026, at 09:56, Harald Nordgren wrote:\n>> Why do I get the above, which apparently is a response to my review\n>> for\n>> \n>>     [PATCH] config: suggest the correct form when key contains \"=\"\n>> \n>> under this thread?  Am I dealing with some sort of mechanical slop?\n>\n> I think the problem here is my email sending process is not good. I edit\n> all the emails in Sublime text, where I keep the same file for all\n> different threads.\n>\n> I have the subject line as the first line of the file and like you notice I\n> forget to change it sometimes.\n>\n> I keep each of the topics bookmarked like this, \n> https://lore.kernel.org/git/xmqqecjdea13.fsf@gitster.g/, and then utilize\n> that like to send the email\n>\n> ```\n>   git send-email \\\n>     --in-reply-to=xmqqecjdea13.fsf@gitster.g \\\n>     --to=gitster@pobox.com \\\n>     --cc=git@vger.kernel.org \\\n>     --cc=gitgitgadget@gmail.com \\\n>     --cc=haraldnordgren@gmail.com \\\n>     /path/to/YOUR_REPLY\n> ```\n>\n> I tried playing with neomutt and and email client replacement, but that\n> adds the complexity of downloading a new mbox file for each reply, it\n> didn't seem easier, but maybe it is.\n>\n> How do you handle emails?\n\nI use the Fastmail webmail client for\nregular non-patch emails. The only\nthings it messes up so far is long lines\nin replies to patches.\n\nI edit the emails in a text editor. And sometimes\nI have left multiple drafts before sending them\nand switched them around. Only to see my mistake on the Lore archive later. :)\n\nBut by and large it works just fine. I haven't had\nthe need for a more ergonomic setup.\n\n-- \nSent from mobile\n"},{"id":"543448","messageId":"CAHwyqnWC5z5ioxOFew-kV_8tA-eO_o7KvmM4XzNBu8VCtpx+uQ@mail.gmail.com","threadId":"65631","inReplyTo":"xmqqqzndel8c.fsf@gitster.g","subject":"Re: [PATCH] config: suggest the correct form when key contains \"=\"","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-05-16T12:51:46Z","receivedAt":"2026-05-16T12:52:25Z","isPatch":true,"body":"> And I think git_config_parse_key() is at a way too low level to tell\n> in what context we are seeing this faulty key to guess end-user's\n> intention to limit our \"did you mean?\"\n>\n> I also wonder if, given that \"=\" in anywhere other than three-level\n> names, is invalid, we should just start accept\n>\n>         git config foo.bar=baz\n>         git config set foo.bar=baz\n>\n> and interpret them as\n>\n>         git config set foo.bar baz\n\nI tried implementing a version to be more liberal in what to accept, but\nthe implementation became very complex.\n\nMoving in the other direction: show the warning, but try to make it more\ncorrect.\n\n(Also switching over to replying to emails with Gmail with 'plain text\nmode'), hopefully there will be less miss-sends that end up on the wrong\ntopic from now on.)\n\n\nHarald\n"},{"id":"543449","messageId":"pull.2302.v2.git.git.1778935976330.gitgitgadget@gmail.com","threadId":"65631","inReplyTo":"pull.2302.git.git.1778680725459.gitgitgadget@gmail.com","subject":"[PATCH v2] config: suggest the correct form when key contains \"=\" in set context","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-16T12:52:56Z","receivedAt":"2026-05-16T12:52:58Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nA user who types \"git config pull.rebase=false\" gets only \"error:\ninvalid key: pull.rebase=false\" with no clue what went wrong.\n\nEmit a \"did you mean ...\" hint suggesting the split form.  Restrict it\nto plausible-set contexts (\"git config set\", bare \"git config <key>\",\nand their 2-arg forms); explicit \"get\"/\"unset\" keep the existing error.\n\n\"=\" is legal inside a subsection, so only fire when \"=\" lands after\nthe last \".\".  When the user supplied a separate value, use it in the\nsuggestion instead of the suffix after \"=\":\n\n    $ git config set pull.rebase=false true\n    error: invalid key: pull.rebase=false\n    hint: did you mean \"git config set pull.rebase true\"?\n\nSigned-off-by: Harald Nordgren <harald.nordgren@kostdoktorn.se>\n---\n    config: suggest the correct form when key contains \"=\"\n    \n     * Hint moved from git_config_parse_key() to a new\n       advise_setting_with_equals() in builtin/config.c; wired only into set\n       and bare paths.\n     * Only fires when = is after the last .; 2-arg forms use the user's\n       value.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2302%2FHaraldNordgren%2Fconfig-hint-equals-key-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2302/HaraldNordgren/config-hint-equals-key-v2\nPull-Request: https://github.com/git/git/pull/2302\n\nRange-diff vs v1:\n\n 1:  56eb3ce6fd < -:  ---------- config: suggest the correct form when key contains \"=\"\n -:  ---------- > 1:  40d9eb3e5c config: suggest the correct form when key contains \"=\" in set context\n\n\n builtin/config.c  | 30 ++++++++++++++++++++++++++++++\n t/t1300-config.sh | 47 +++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 77 insertions(+)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex cf4ba0f7cc..f14a30e720 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -1,6 +1,7 @@\n #define USE_THE_REPOSITORY_VARIABLE\n #include \"builtin.h\"\n #include \"abspath.h\"\n+#include \"advice.h\"\n #include \"config.h\"\n #include \"color.h\"\n #include \"date.h\"\n@@ -210,6 +211,22 @@ static void check_argc(int argc, int min, int max)\n \texit(129);\n }\n \n+static void advise_setting_with_equals(const char *key, const char *value)\n+{\n+\tconst char *last_dot = strrchr(key, '.');\n+\tconst char *eq;\n+\n+\tif (!last_dot)\n+\t\treturn;\n+\teq = strchr(last_dot + 1, '=');\n+\tif (!eq)\n+\t\treturn;\n+\tif (!value)\n+\t\tvalue = eq + 1;\n+\tadvise(_(\"did you mean \\\"git config set %.*s %s\\\"?\"),\n+\t       (int)(eq - key), key, value);\n+}\n+\n static void show_config_origin(const struct config_display_options *opts,\n \t\t\t       const struct key_value_info *kvi,\n \t\t\t       struct strbuf *buf)\n@@ -1133,6 +1150,11 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,\n \n \targc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tif (argc == 1 && strchr(argv[0], '=')) {\n+\t\terror(_(\"wrong number of arguments, should be 2\"));\n+\t\tadvise_setting_with_equals(argv[0], NULL);\n+\t\texit(129);\n+\t}\n \tcheck_argc(argc, 2, 2);\n \n \tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n@@ -1160,6 +1182,8 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,\n \t\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n \t\t\t\"       Use --value=<pattern>, --append or --all to change %s.\"), argv[0]);\n \t}\n+\tif (ret == CONFIG_INVALID_KEY)\n+\t\tadvise_setting_with_equals(argv[0], argv[1]);\n \n \tlocation_options_release(&location_opts);\n \tfree(comment);\n@@ -1371,6 +1395,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t};\n \tchar *value = NULL, *comment = NULL;\n \tint ret = 0;\n+\tint actions_implicit;\n \tstruct key_value_info default_kvi = KVI_INIT;\n \n \targc = parse_options(argc, argv, prefix, opts,\n@@ -1385,6 +1410,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\texit(129);\n \t}\n \n+\tactions_implicit = (actions == 0);\n \tif (actions == 0)\n \t\tswitch (argc) {\n \t\tcase 1: actions = ACTION_GET; break;\n@@ -1485,6 +1511,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\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\telse if (ret == CONFIG_INVALID_KEY)\n+\t\t\tadvise_setting_with_equals(argv[0], argv[1]);\n \t}\n \telse if (actions == ACTION_SET_ALL) {\n \t\tcheck_write(&location_opts.source);\n@@ -1515,6 +1543,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\tcheck_argc(argc, 1, 2);\n \t\tret = get_value(&location_opts, &display_opts, argv[0], argv[1],\n \t\t\t\t0, flags);\n+\t\tif (ret == CONFIG_INVALID_KEY && actions_implicit)\n+\t\t\tadvise_setting_with_equals(argv[0], NULL);\n \t}\n \telse if (actions == ACTION_GET_ALL) {\n \t\tcheck_argc(argc, 1, 2);\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 128971ee12..f46c081413 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -462,6 +462,53 @@ test_expect_success 'invalid key' '\n \ttest_must_fail git config inval.2key blabla\n '\n \n+test_expect_success 'misplaced \"=\" in key: bare 1-arg form hints' '\n+\ttest_must_fail git config pull.rebase=false 2>err &&\n+\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: bare 2-arg form uses given value' '\n+\ttest_must_fail git config pull.rebase=false true 2>err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase true.\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: set subcommand uses given value' '\n+\ttest_must_fail git config set pull.rebase=false true 2>err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase true.\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: set with single arg hints' '\n+\ttest_must_fail git config set pull.rebase=false 2>err &&\n+\ttest_grep \"wrong number of arguments\" err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: explicit --get does not hint' '\n+\ttest_must_fail git config --get pull.rebase=false 2>err &&\n+\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: get subcommand does not hint' '\n+\ttest_must_fail git config get pull.rebase=false 2>err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: unset subcommand does not hint' '\n+\ttest_must_fail git config unset pull.rebase=false 2>err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success '\"=\" inside subsection is valid, no hint' '\n+\ttest_when_finished \"rm -f subsection.cfg\" &&\n+\tgit config set -f subsection.cfg foo.bar=baz.boo qux 2>err &&\n+\ttest_grep ! \"did you mean\" err &&\n+\techo qux >expect &&\n+\tgit config get -f subsection.cfg foo.bar=baz.boo >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'correct key' '\n \tgit config 123456.a123 987\n '\n\nbase-commit: 59ff4886a579f4bc91e976fe18590b9ae02c7a08\n-- \ngitgitgadget\n"},{"id":"544044","messageId":"pull.2302.v3.git.git.1779697995418.gitgitgadget@gmail.com","threadId":"65631","inReplyTo":"pull.2302.v2.git.git.1778935976330.gitgitgadget@gmail.com","subject":"[PATCH v3] config: suggest the correct form when key contains \"=\" in set context","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-25T08:33:15Z","receivedAt":"2026-05-25T08:33:18Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nA user who types \"git config pull.rebase=false\" gets only \"error:\ninvalid key: pull.rebase=false\" with no clue what went wrong.\n\nEmit a \"did you mean ...\" hint suggesting the split form.  Restrict it\nto plausible-set contexts (\"git config set\", bare \"git config <key>\",\nand their 2-arg forms); explicit \"get\"/\"unset\" keep the existing error.\n\n\"=\" is legal inside a subsection, so only fire when \"=\" lands after\nthe last \".\".  When the user supplied a separate value, use it in the\nsuggestion instead of the suffix after \"=\":\n\n    $ git config set pull.rebase=false true\n    error: invalid key: pull.rebase=false\n    hint: did you mean \"git config set pull.rebase true\"?\n\nSigned-off-by: Harald Nordgren <harald.nordgren@kostdoktorn.se>\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    config: suggest the correct form when key contains \"=\"\n    \n     * Skip the hint when the inferred value contains whitespace, so git\n       config set pull.rebase=false \"hello world\" no longer suggests a\n       malformed command.\n     * Replace the inline actions == 0 check with a named actions_implicit\n       flag, simplfied the code.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2302%2FHaraldNordgren%2Fconfig-hint-equals-key-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2302/HaraldNordgren/config-hint-equals-key-v3\nPull-Request: https://github.com/git/git/pull/2302\n\nRange-diff vs v2:\n\n 1:  40d9eb3e5c ! 1:  6b9d66361d config: suggest the correct form when key contains \"=\" in set context\n     @@ Commit message\n              hint: did you mean \"git config set pull.rebase true\"?\n      \n          Signed-off-by: Harald Nordgren <harald.nordgren@kostdoktorn.se>\n     +    Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## builtin/config.c ##\n      @@\n     @@ builtin/config.c: static void check_argc(int argc, int min, int max)\n      +\t\treturn;\n      +\tif (!value)\n      +\t\tvalue = eq + 1;\n     ++\tif (!*value || strpbrk(value, \" \\t\\n\"))\n     ++\t\treturn;\n      +\tadvise(_(\"did you mean \\\"git config set %.*s %s\\\"?\"),\n      +\t       (int)(eq - key), key, value);\n      +}\n     @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, con\n       \t\texit(129);\n       \t}\n       \n     +-\tif (actions == 0)\n      +\tactions_implicit = (actions == 0);\n     - \tif (actions == 0)\n     ++\tif (actions_implicit)\n       \t\tswitch (argc) {\n       \t\tcase 1: actions = ACTION_GET; break;\n     + \t\tcase 2: actions = ACTION_SET; break;\n      @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n       \t\tif (ret == CONFIG_NOTHING_SET)\n       \t\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n     @@ t/t1300-config.sh: test_expect_success 'invalid key' '\n      +\ttest_grep ! \"did you mean\" err\n      +'\n      +\n     ++test_expect_success 'misplaced \"=\" in key: value with whitespace skips hint' '\n     ++\ttest_must_fail git config set pull.rebase=false \"hello world\" 2>err &&\n     ++\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n     ++\ttest_grep ! \"did you mean\" err\n     ++'\n     ++\n      +test_expect_success '\"=\" inside subsection is valid, no hint' '\n      +\ttest_when_finished \"rm -f subsection.cfg\" &&\n      +\tgit config set -f subsection.cfg foo.bar=baz.boo qux 2>err &&\n\n\n builtin/config.c  | 34 +++++++++++++++++++++++++++++-\n t/t1300-config.sh | 53 +++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 86 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex cf4ba0f7cc..8c7ab36fcb 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -1,6 +1,7 @@\n #define USE_THE_REPOSITORY_VARIABLE\n #include \"builtin.h\"\n #include \"abspath.h\"\n+#include \"advice.h\"\n #include \"config.h\"\n #include \"color.h\"\n #include \"date.h\"\n@@ -210,6 +211,24 @@ static void check_argc(int argc, int min, int max)\n \texit(129);\n }\n \n+static void advise_setting_with_equals(const char *key, const char *value)\n+{\n+\tconst char *last_dot = strrchr(key, '.');\n+\tconst char *eq;\n+\n+\tif (!last_dot)\n+\t\treturn;\n+\teq = strchr(last_dot + 1, '=');\n+\tif (!eq)\n+\t\treturn;\n+\tif (!value)\n+\t\tvalue = eq + 1;\n+\tif (!*value || strpbrk(value, \" \\t\\n\"))\n+\t\treturn;\n+\tadvise(_(\"did you mean \\\"git config set %.*s %s\\\"?\"),\n+\t       (int)(eq - key), key, value);\n+}\n+\n static void show_config_origin(const struct config_display_options *opts,\n \t\t\t       const struct key_value_info *kvi,\n \t\t\t       struct strbuf *buf)\n@@ -1133,6 +1152,11 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,\n \n \targc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tif (argc == 1 && strchr(argv[0], '=')) {\n+\t\terror(_(\"wrong number of arguments, should be 2\"));\n+\t\tadvise_setting_with_equals(argv[0], NULL);\n+\t\texit(129);\n+\t}\n \tcheck_argc(argc, 2, 2);\n \n \tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n@@ -1160,6 +1184,8 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,\n \t\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n \t\t\t\"       Use --value=<pattern>, --append or --all to change %s.\"), argv[0]);\n \t}\n+\tif (ret == CONFIG_INVALID_KEY)\n+\t\tadvise_setting_with_equals(argv[0], argv[1]);\n \n \tlocation_options_release(&location_opts);\n \tfree(comment);\n@@ -1371,6 +1397,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t};\n \tchar *value = NULL, *comment = NULL;\n \tint ret = 0;\n+\tint actions_implicit;\n \tstruct key_value_info default_kvi = KVI_INIT;\n \n \targc = parse_options(argc, argv, prefix, opts,\n@@ -1385,7 +1412,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\texit(129);\n \t}\n \n-\tif (actions == 0)\n+\tactions_implicit = (actions == 0);\n+\tif (actions_implicit)\n \t\tswitch (argc) {\n \t\tcase 1: actions = ACTION_GET; break;\n \t\tcase 2: actions = ACTION_SET; break;\n@@ -1485,6 +1513,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\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\telse if (ret == CONFIG_INVALID_KEY)\n+\t\t\tadvise_setting_with_equals(argv[0], argv[1]);\n \t}\n \telse if (actions == ACTION_SET_ALL) {\n \t\tcheck_write(&location_opts.source);\n@@ -1515,6 +1545,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\tcheck_argc(argc, 1, 2);\n \t\tret = get_value(&location_opts, &display_opts, argv[0], argv[1],\n \t\t\t\t0, flags);\n+\t\tif (ret == CONFIG_INVALID_KEY && actions_implicit)\n+\t\t\tadvise_setting_with_equals(argv[0], NULL);\n \t}\n \telse if (actions == ACTION_GET_ALL) {\n \t\tcheck_argc(argc, 1, 2);\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 11fc976f3a..4e12b78536 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -469,6 +469,59 @@ test_expect_success 'invalid key' '\n \ttest_must_fail git config inval.2key blabla\n '\n \n+test_expect_success 'misplaced \"=\" in key: bare 1-arg form hints' '\n+\ttest_must_fail git config pull.rebase=false 2>err &&\n+\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: bare 2-arg form uses given value' '\n+\ttest_must_fail git config pull.rebase=false true 2>err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase true.\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: set subcommand uses given value' '\n+\ttest_must_fail git config set pull.rebase=false true 2>err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase true.\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: set with single arg hints' '\n+\ttest_must_fail git config set pull.rebase=false 2>err &&\n+\ttest_grep \"wrong number of arguments\" err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: explicit --get does not hint' '\n+\ttest_must_fail git config --get pull.rebase=false 2>err &&\n+\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: get subcommand does not hint' '\n+\ttest_must_fail git config get pull.rebase=false 2>err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: unset subcommand does not hint' '\n+\ttest_must_fail git config unset pull.rebase=false 2>err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'misplaced \"=\" in key: value with whitespace skips hint' '\n+\ttest_must_fail git config set pull.rebase=false \"hello world\" 2>err &&\n+\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success '\"=\" inside subsection is valid, no hint' '\n+\ttest_when_finished \"rm -f subsection.cfg\" &&\n+\tgit config set -f subsection.cfg foo.bar=baz.boo qux 2>err &&\n+\ttest_grep ! \"did you mean\" err &&\n+\techo qux >expect &&\n+\tgit config get -f subsection.cfg foo.bar=baz.boo >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'correct key' '\n \tgit config 123456.a123 987\n '\n\nbase-commit: 6a4418c36d6bad69a599044b3cf49dcbd049cb45\n-- \ngitgitgadget\n"},{"id":"544048","messageId":"xmqqecizetoh.fsf@gitster.g","threadId":"65631","inReplyTo":"pull.2302.v3.git.git.1779697995418.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] config: suggest the correct form when key contains \"=\" in set context","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-25T09:15:42Z","receivedAt":"2026-05-25T09:15:45Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Emit a \"did you mean ...\" hint suggesting the split form.  Restrict it\n> to plausible-set contexts (\"git config set\", bare \"git config <key>\",\n> and their 2-arg forms); explicit \"get\"/\"unset\" keep the existing error.\n\nI understand that it would be a good idea to give this warning\nagainst these two where $A is an arbitrary string with at least one\ndot in it (making it a likely variable name), and $B is an arbitrary\nstring that may contain anything:\n\n    git config set \"$A=$B\"\n    git config \"$A=$B\"\n\nIt is plausible that the user wanted to make the value of the\nvariable \"$A\" to \"$B\", so telling them the right syntax would be\nvaluable.\n\nIf \"$A\" is a syntactically valid variable name, then I would imagine\nthat we want to say something like this:\n\n    $ git config set \"$A=$B\"\n    error: missing value to set to the variable \"$A=$B\"\n    hint: did you mean 'git config set \"$A\" \"$B\"'?\n\nIf \"$A\" is *not* a syntactically valid variable name, then giving a\nhint to try to assing to it is a counter-productive.  Ideally we\nprobably want something like:\n\n    $ git config set \"foo=bar\"\n    error: missing value to set to a variable with an invalid name 'foo=bar'\n\nIt is pointless to say the user may have meant \"git config set foo bar\",\nas \"foo\" is clearly not a valid variable.\n\nI do not understand what you mean by \"their 2-arg forms\".  Do you\nmean\n\n    git config set \"$A=$B\" \"$C\"\n\nby that?  If so, I doubt that user meant an assignment to \"$A\" by\nthis form with explicit \"set\".  If \"$A=$B\" is a variable whose name\nis valid (i.e. three-level name whose the second level component\ncontains a \"=\"), we should just take it as asked.  E.g.,\n\n    git config set \"foo.bar=baz.boo\" \"some-string\"\n\nneeds no hand holding.  But\nif \"$A=$B\" is not a valid variable name, we should just complain\nthat the user is trying to assign to a variable with an invalid\nname.\n\n    $ git config set \"foo.bar=baz\" \"some-string\"\n    error: setting to a variable with invalid name 'foo.bar=baz'\n\nI think\n\n    git config \"$A=$B\" \"$C\"\n\nthat implicitly uses the 'set' verb can be left as an exercise to\nreaders.  If \"$A=$B\" is a valid name, we shouldn't do any complaint.\nIf it is not, \n\n    $ git config \"foo.bar=baz\" \"some-string\"\n    error: setting to a variable with invalid name 'foo.bar=baz'\n\nIt makes it clear to the user that (1) we interpreted the command\nline to be \"implicit set\", (2) we interpreted the command line to\nset variable 'foo.bar=baz', and (3) 'foo.bar=baz' is not a valid\nname.  I do not think there is anything more needed for this case.\n\n> \"=\" is legal inside a subsection, so only fire when \"=\" lands after\n> the last \".\".  When the user supplied a separate value, use it in the\n> suggestion instead of the suffix after \"=\":\n>\n>     $ git config set pull.rebase=false true\n>     error: invalid key: pull.rebase=false\n>     hint: did you mean \"git config set pull.rebase true\"?\n\nI really do not think '=' needs *any* special casing in this case.\nIf we used \"pull.rebase*false\" as the variable instead, the message\nwould say that \"pull.rebase*false\" is an invalid key.  Two important\nthings for this message to convey are (1) the command correctly\nparsed the command line to mean that the user wants to assign to a\nvariable whose name is 'pull.rebase*false' and (2) that variable\nname *is* invalid.\n\nIf you find the current message suboptimal, I think we should try to\nclarify the message, as '=' or '*' or any letter that makes the\nvariable name invalid would benefit from the same improvement.\nPerhaps something like:\n\n    $ git config set pull.rebase*false true\n    error: setting to a variable with invalid name: 'pull.rebase*false'\n\nperhaps?\n\n\n> Signed-off-by: Harald Nordgren <harald.nordgren@kostdoktorn.se>\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n\nInteresting.  We typically do not do this.\n"},{"id":"544120","messageId":"pull.2302.v4.git.git.1779823288005.gitgitgadget@gmail.com","threadId":"65631","inReplyTo":"pull.2302.v3.git.git.1779697995418.gitgitgadget@gmail.com","subject":"[PATCH v4] config: improve diagnostic for \"set\" with missing value","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-26T19:21:27Z","receivedAt":"2026-05-26T19:21:30Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git config set pull.rebase=false\" currently fails with \"wrong\nnumber of arguments\", and the implicit form \"git config\npull.rebase=false\" fails with \"invalid key\". Neither points at\nthe real problem: the value is missing.\n\nReport that directly, and when the argument has the shape\n\"<valid-key>=<value>\", also suggest the split form:\n\n    $ git config set pull.rebase=false\n    error: missing value to set to the variable 'pull.rebase=false'\n    hint: did you mean \"git config set pull.rebase false\"?\n\nWhen the prefix before \"=\" is not a valid key, drop the hint:\n\n    $ git config set foo=bar\n    error: missing value to set to a variable with an invalid name 'foo=bar'\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    config: suggest the correct form when key contains \"=\"\n    \n     * Skip the hint when the inferred value contains whitespace, so git\n       config set pull.rebase=false \"hello world\" no longer suggests a\n       malformed command.\n     * Replace the inline actions == 0 check with a named actions_implicit\n       flag, simplfied the code.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2302%2FHaraldNordgren%2Fconfig-hint-equals-key-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2302/HaraldNordgren/config-hint-equals-key-v4\nPull-Request: https://github.com/git/git/pull/2302\n\nRange-diff vs v3:\n\n 1:  6b9d66361d ! 1:  780b99409c config: suggest the correct form when key contains \"=\" in set context\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    config: suggest the correct form when key contains \"=\" in set context\n     +    config: improve diagnostic for \"set\" with missing value\n      \n     -    A user who types \"git config pull.rebase=false\" gets only \"error:\n     -    invalid key: pull.rebase=false\" with no clue what went wrong.\n     +    \"git config set pull.rebase=false\" currently fails with \"wrong\n     +    number of arguments\", and the implicit form \"git config\n     +    pull.rebase=false\" fails with \"invalid key\". Neither points at\n     +    the real problem: the value is missing.\n      \n     -    Emit a \"did you mean ...\" hint suggesting the split form.  Restrict it\n     -    to plausible-set contexts (\"git config set\", bare \"git config <key>\",\n     -    and their 2-arg forms); explicit \"get\"/\"unset\" keep the existing error.\n     +    Report that directly, and when the argument has the shape\n     +    \"<valid-key>=<value>\", also suggest the split form:\n      \n     -    \"=\" is legal inside a subsection, so only fire when \"=\" lands after\n     -    the last \".\".  When the user supplied a separate value, use it in the\n     -    suggestion instead of the suffix after \"=\":\n     +        $ git config set pull.rebase=false\n     +        error: missing value to set to the variable 'pull.rebase=false'\n     +        hint: did you mean \"git config set pull.rebase false\"?\n      \n     -        $ git config set pull.rebase=false true\n     -        error: invalid key: pull.rebase=false\n     -        hint: did you mean \"git config set pull.rebase true\"?\n     +    When the prefix before \"=\" is not a valid key, drop the hint:\n     +\n     +        $ git config set foo=bar\n     +        error: missing value to set to a variable with an invalid name 'foo=bar'\n      \n     -    Signed-off-by: Harald Nordgren <harald.nordgren@kostdoktorn.se>\n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## builtin/config.c ##\n     @@ builtin/config.c: static void check_argc(int argc, int min, int max)\n       \texit(129);\n       }\n       \n     -+static void advise_setting_with_equals(const char *key, const char *value)\n     ++static int is_valid_key(const char *key)\n      +{\n      +\tconst char *last_dot = strrchr(key, '.');\n     -+\tconst char *eq;\n      +\n     -+\tif (!last_dot)\n     -+\t\treturn;\n     -+\teq = strchr(last_dot + 1, '=');\n     -+\tif (!eq)\n     -+\t\treturn;\n     -+\tif (!value)\n     -+\t\tvalue = eq + 1;\n     -+\tif (!*value || strpbrk(value, \" \\t\\n\"))\n     -+\t\treturn;\n     -+\tadvise(_(\"did you mean \\\"git config set %.*s %s\\\"?\"),\n     -+\t       (int)(eq - key), key, value);\n     ++\treturn last_dot && isalpha(last_dot[1]);\n     ++}\n     ++\n     ++static NORETURN void die_missing_set_value(const char *arg)\n     ++{\n     ++\tconst char *last_dot = strrchr(arg, '.');\n     ++\tconst char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;\n     ++\tchar *prefix = eq ? xstrndup(arg, eq - arg) : NULL;\n     ++\n     ++\tif (prefix && is_valid_key(prefix)) {\n     ++\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n     ++\t\tadvise(_(\"did you mean \\\"git config set %s %s\\\"?\"),\n     ++\t\t       prefix, eq + 1);\n     ++\t} else if (is_valid_key(arg)) {\n     ++\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n     ++\t} else {\n     ++\t\terror(_(\"missing value to set to a variable with an invalid name '%s'\"),\n     ++\t\t      arg);\n     ++\t}\n     ++\tfree(prefix);\n     ++\texit(129);\n      +}\n      +\n       static void show_config_origin(const struct config_display_options *opts,\n     @@ builtin/config.c: static int cmd_config_set(int argc, const char **argv, const c\n       \n       \targc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n       \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n     -+\tif (argc == 1 && strchr(argv[0], '=')) {\n     -+\t\terror(_(\"wrong number of arguments, should be 2\"));\n     -+\t\tadvise_setting_with_equals(argv[0], NULL);\n     -+\t\texit(129);\n     -+\t}\n     ++\tif (argc == 1)\n     ++\t\tdie_missing_set_value(argv[0]);\n       \tcheck_argc(argc, 2, 2);\n       \n       \tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n     -@@ builtin/config.c: static int cmd_config_set(int argc, const char **argv, const char *prefix,\n     - \t\t\terror(_(\"cannot overwrite multiple values with a single value\\n\"\n     - \t\t\t\"       Use --value=<pattern>, --append or --all to change %s.\"), argv[0]);\n     - \t}\n     -+\tif (ret == CONFIG_INVALID_KEY)\n     -+\t\tadvise_setting_with_equals(argv[0], argv[1]);\n     - \n     - \tlocation_options_release(&location_opts);\n     - \tfree(comment);\n      @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n       \t};\n       \tchar *value = NULL, *comment = NULL;\n     @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, con\n       \t\tcase 1: actions = ACTION_GET; break;\n       \t\tcase 2: actions = ACTION_SET; break;\n      @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)\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\telse if (ret == CONFIG_INVALID_KEY)\n     -+\t\t\tadvise_setting_with_equals(argv[0], argv[1]);\n     - \t}\n     - \telse if (actions == ACTION_SET_ALL) {\n     - \t\tcheck_write(&location_opts.source);\n     -@@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n     - \t\tcheck_argc(argc, 1, 2);\n     - \t\tret = get_value(&location_opts, &display_opts, argv[0], argv[1],\n     - \t\t\t\t0, flags);\n     -+\t\tif (ret == CONFIG_INVALID_KEY && actions_implicit)\n     -+\t\t\tadvise_setting_with_equals(argv[0], NULL);\n     - \t}\n     - \telse if (actions == ACTION_GET_ALL) {\n     - \t\tcheck_argc(argc, 1, 2);\n     + \t\t\terror(_(\"no action specified\"));\n     + \t\t\texit(129);\n     + \t\t}\n     ++\tif (actions_implicit && argc == 1) {\n     ++\t\tconst char *last_dot = strrchr(argv[0], '.');\n     ++\t\tif (last_dot && strchr(last_dot + 1, '='))\n     ++\t\t\tdie_missing_set_value(argv[0]);\n     ++\t}\n     + \tif (display_opts.omit_values &&\n     + \t    !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {\n     + \t\terror(_(\"--name-only is only applicable to --list or --get-regexp\"));\n      \n       ## t/t1300-config.sh ##\n      @@ t/t1300-config.sh: test_expect_success 'invalid key' '\n       \ttest_must_fail git config inval.2key blabla\n       '\n       \n     -+test_expect_success 'misplaced \"=\" in key: bare 1-arg form hints' '\n     -+\ttest_must_fail git config pull.rebase=false 2>err &&\n     -+\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n     ++test_expect_success 'set with 1 arg of \"key=value\": valid key suggests split form' '\n     ++\ttest_must_fail git config set pull.rebase=false 2>err &&\n     ++\ttest_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n      +\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n      +'\n      +\n     -+test_expect_success 'misplaced \"=\" in key: bare 2-arg form uses given value' '\n     -+\ttest_must_fail git config pull.rebase=false true 2>err &&\n     -+\ttest_grep \"did you mean .git config set pull\\\\.rebase true.\" err\n     -+'\n     -+\n     -+test_expect_success 'misplaced \"=\" in key: set subcommand uses given value' '\n     -+\ttest_must_fail git config set pull.rebase=false true 2>err &&\n     -+\ttest_grep \"did you mean .git config set pull\\\\.rebase true.\" err\n     -+'\n     -+\n     -+test_expect_success 'misplaced \"=\" in key: set with single arg hints' '\n     -+\ttest_must_fail git config set pull.rebase=false 2>err &&\n     -+\ttest_grep \"wrong number of arguments\" err &&\n     ++test_expect_success 'set with 1 arg of \"key=value\": implicit form suggests split form' '\n     ++\ttest_must_fail git config pull.rebase=false 2>err &&\n     ++\ttest_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n      +\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n      +'\n      +\n     -+test_expect_success 'misplaced \"=\" in key: explicit --get does not hint' '\n     -+\ttest_must_fail git config --get pull.rebase=false 2>err &&\n     -+\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n     ++test_expect_success 'set with 1 arg of \"key=value\": invalid key does not suggest split form' '\n     ++\ttest_must_fail git config set foo=bar 2>err &&\n     ++\ttest_grep \"missing value to set to a variable with an invalid name .foo=bar.\" err &&\n      +\ttest_grep ! \"did you mean\" err\n      +'\n      +\n     -+test_expect_success 'misplaced \"=\" in key: get subcommand does not hint' '\n     -+\ttest_must_fail git config get pull.rebase=false 2>err &&\n     ++test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '\n     ++\ttest_must_fail git config set foo.1bar=baz 2>err &&\n     ++\ttest_grep \"missing value to set to a variable with an invalid name .foo\\\\.1bar=baz.\" err &&\n      +\ttest_grep ! \"did you mean\" err\n      +'\n      +\n     -+test_expect_success 'misplaced \"=\" in key: unset subcommand does not hint' '\n     -+\ttest_must_fail git config unset pull.rebase=false 2>err &&\n     ++test_expect_success 'set with 1 arg of valid key reports missing value' '\n     ++\ttest_must_fail git config set pull.rebase 2>err &&\n     ++\ttest_grep \"missing value to set to the variable .pull\\\\.rebase.\" err &&\n      +\ttest_grep ! \"did you mean\" err\n      +'\n      +\n     -+test_expect_success 'misplaced \"=\" in key: value with whitespace skips hint' '\n     -+\ttest_must_fail git config set pull.rebase=false \"hello world\" 2>err &&\n     -+\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n     ++test_expect_success 'set with 2 args including \"=\" in invalid key does not suggest' '\n     ++\ttest_must_fail git config set pull.rebase=false true 2>err &&\n      +\ttest_grep ! \"did you mean\" err\n      +'\n      +\n     -+test_expect_success '\"=\" inside subsection is valid, no hint' '\n     ++test_expect_success '\"=\" inside subsection is valid' '\n      +\ttest_when_finished \"rm -f subsection.cfg\" &&\n     -+\tgit config set -f subsection.cfg foo.bar=baz.boo qux 2>err &&\n     -+\ttest_grep ! \"did you mean\" err &&\n     ++\tgit config set -f subsection.cfg foo.bar=baz.boo qux &&\n      +\techo qux >expect &&\n      +\tgit config get -f subsection.cfg foo.bar=baz.boo >actual &&\n      +\ttest_cmp expect actual\n\n\n builtin/config.c  | 39 ++++++++++++++++++++++++++++++++++++++-\n t/t1300-config.sh | 43 +++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 81 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex cf4ba0f7cc..6fe2d85814 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -1,6 +1,7 @@\n #define USE_THE_REPOSITORY_VARIABLE\n #include \"builtin.h\"\n #include \"abspath.h\"\n+#include \"advice.h\"\n #include \"config.h\"\n #include \"color.h\"\n #include \"date.h\"\n@@ -210,6 +211,33 @@ static void check_argc(int argc, int min, int max)\n \texit(129);\n }\n \n+static int is_valid_key(const char *key)\n+{\n+\tconst char *last_dot = strrchr(key, '.');\n+\n+\treturn last_dot && isalpha(last_dot[1]);\n+}\n+\n+static NORETURN void die_missing_set_value(const char *arg)\n+{\n+\tconst char *last_dot = strrchr(arg, '.');\n+\tconst char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;\n+\tchar *prefix = eq ? xstrndup(arg, eq - arg) : NULL;\n+\n+\tif (prefix && is_valid_key(prefix)) {\n+\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n+\t\tadvise(_(\"did you mean \\\"git config set %s %s\\\"?\"),\n+\t\t       prefix, eq + 1);\n+\t} else if (is_valid_key(arg)) {\n+\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n+\t} else {\n+\t\terror(_(\"missing value to set to a variable with an invalid name '%s'\"),\n+\t\t      arg);\n+\t}\n+\tfree(prefix);\n+\texit(129);\n+}\n+\n static void show_config_origin(const struct config_display_options *opts,\n \t\t\t       const struct key_value_info *kvi,\n \t\t\t       struct strbuf *buf)\n@@ -1133,6 +1161,8 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,\n \n \targc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tif (argc == 1)\n+\t\tdie_missing_set_value(argv[0]);\n \tcheck_argc(argc, 2, 2);\n \n \tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n@@ -1371,6 +1401,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t};\n \tchar *value = NULL, *comment = NULL;\n \tint ret = 0;\n+\tint actions_implicit;\n \tstruct key_value_info default_kvi = KVI_INIT;\n \n \targc = parse_options(argc, argv, prefix, opts,\n@@ -1385,7 +1416,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\texit(129);\n \t}\n \n-\tif (actions == 0)\n+\tactions_implicit = (actions == 0);\n+\tif (actions_implicit)\n \t\tswitch (argc) {\n \t\tcase 1: actions = ACTION_GET; break;\n \t\tcase 2: actions = ACTION_SET; break;\n@@ -1394,6 +1426,11 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\t\terror(_(\"no action specified\"));\n \t\t\texit(129);\n \t\t}\n+\tif (actions_implicit && argc == 1) {\n+\t\tconst char *last_dot = strrchr(argv[0], '.');\n+\t\tif (last_dot && strchr(last_dot + 1, '='))\n+\t\t\tdie_missing_set_value(argv[0]);\n+\t}\n \tif (display_opts.omit_values &&\n \t    !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {\n \t\terror(_(\"--name-only is only applicable to --list or --get-regexp\"));\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 11fc976f3a..4a8a381bd8 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -469,6 +469,49 @@ test_expect_success 'invalid key' '\n \ttest_must_fail git config inval.2key blabla\n '\n \n+test_expect_success 'set with 1 arg of \"key=value\": valid key suggests split form' '\n+\ttest_must_fail git config set pull.rebase=false 2>err &&\n+\ttest_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n+'\n+\n+test_expect_success 'set with 1 arg of \"key=value\": implicit form suggests split form' '\n+\ttest_must_fail git config pull.rebase=false 2>err &&\n+\ttest_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n+'\n+\n+test_expect_success 'set with 1 arg of \"key=value\": invalid key does not suggest split form' '\n+\ttest_must_fail git config set foo=bar 2>err &&\n+\ttest_grep \"missing value to set to a variable with an invalid name .foo=bar.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '\n+\ttest_must_fail git config set foo.1bar=baz 2>err &&\n+\ttest_grep \"missing value to set to a variable with an invalid name .foo\\\\.1bar=baz.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 1 arg of valid key reports missing value' '\n+\ttest_must_fail git config set pull.rebase 2>err &&\n+\ttest_grep \"missing value to set to the variable .pull\\\\.rebase.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 2 args including \"=\" in invalid key does not suggest' '\n+\ttest_must_fail git config set pull.rebase=false true 2>err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success '\"=\" inside subsection is valid' '\n+\ttest_when_finished \"rm -f subsection.cfg\" &&\n+\tgit config set -f subsection.cfg foo.bar=baz.boo qux &&\n+\techo qux >expect &&\n+\tgit config get -f subsection.cfg foo.bar=baz.boo >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'correct key' '\n \tgit config 123456.a123 987\n '\n\nbase-commit: 56a4f3c3a221adf1df9b39da69b8a6890f803157\n-- \ngitgitgadget\n"},{"id":"544121","messageId":"CAHwyqnVxpBv1CEru4sYKu3TtXDLjK-ZZoYnqfdhFf8_Kh1Ostg@mail.gmail.com","threadId":"65631","inReplyTo":"pull.2302.v4.git.git.1779823288005.gitgitgadget@gmail.com","subject":"Re: [PATCH v4] config: improve diagnostic for \"set\" with missing value","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-05-26T19:24:40Z","receivedAt":"2026-05-26T19:25:19Z","isPatch":true,"body":"I forgot to update the PR description on GitHub, it should have read:\n\n- Diagnose the 1-arg set form (explicit and implicit) directly: report\nthe missing value, and suggest the split form only when the prefix\nbefore `=` is a valid key.\n- Did not act on Junio's secondary suggestion to reword the 2-arg\n`error: invalid key: <key>`, fix seemed to become too big.\n\n\nHarald\n\nOn Tue, May 26, 2026 at 9:21 PM Harald Nordgren via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n>\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> \"git config set pull.rebase=false\" currently fails with \"wrong\n> number of arguments\", and the implicit form \"git config\n> pull.rebase=false\" fails with \"invalid key\". Neither points at\n> the real problem: the value is missing.\n>\n> Report that directly, and when the argument has the shape\n> \"<valid-key>=<value>\", also suggest the split form:\n>\n>     $ git config set pull.rebase=false\n>     error: missing value to set to the variable 'pull.rebase=false'\n>     hint: did you mean \"git config set pull.rebase false\"?\n>\n> When the prefix before \"=\" is not a valid key, drop the hint:\n>\n>     $ git config set foo=bar\n>     error: missing value to set to a variable with an invalid name 'foo=bar'\n>\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n>     config: suggest the correct form when key contains \"=\"\n>\n>      * Skip the hint when the inferred value contains whitespace, so git\n>        config set pull.rebase=false \"hello world\" no longer suggests a\n>        malformed command.\n>      * Replace the inline actions == 0 check with a named actions_implicit\n>        flag, simplfied the code.\n>\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2302%2FHaraldNordgren%2Fconfig-hint-equals-key-v4\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2302/HaraldNordgren/config-hint-equals-key-v4\n> Pull-Request: https://github.com/git/git/pull/2302\n>\n> Range-diff vs v3:\n>\n>  1:  6b9d66361d ! 1:  780b99409c config: suggest the correct form when key contains \"=\" in set context\n>      @@ Metadata\n>       Author: Harald Nordgren <haraldnordgren@gmail.com>\n>\n>        ## Commit message ##\n>      -    config: suggest the correct form when key contains \"=\" in set context\n>      +    config: improve diagnostic for \"set\" with missing value\n>\n>      -    A user who types \"git config pull.rebase=false\" gets only \"error:\n>      -    invalid key: pull.rebase=false\" with no clue what went wrong.\n>      +    \"git config set pull.rebase=false\" currently fails with \"wrong\n>      +    number of arguments\", and the implicit form \"git config\n>      +    pull.rebase=false\" fails with \"invalid key\". Neither points at\n>      +    the real problem: the value is missing.\n>\n>      -    Emit a \"did you mean ...\" hint suggesting the split form.  Restrict it\n>      -    to plausible-set contexts (\"git config set\", bare \"git config <key>\",\n>      -    and their 2-arg forms); explicit \"get\"/\"unset\" keep the existing error.\n>      +    Report that directly, and when the argument has the shape\n>      +    \"<valid-key>=<value>\", also suggest the split form:\n>\n>      -    \"=\" is legal inside a subsection, so only fire when \"=\" lands after\n>      -    the last \".\".  When the user supplied a separate value, use it in the\n>      -    suggestion instead of the suffix after \"=\":\n>      +        $ git config set pull.rebase=false\n>      +        error: missing value to set to the variable 'pull.rebase=false'\n>      +        hint: did you mean \"git config set pull.rebase false\"?\n>\n>      -        $ git config set pull.rebase=false true\n>      -        error: invalid key: pull.rebase=false\n>      -        hint: did you mean \"git config set pull.rebase true\"?\n>      +    When the prefix before \"=\" is not a valid key, drop the hint:\n>      +\n>      +        $ git config set foo=bar\n>      +        error: missing value to set to a variable with an invalid name 'foo=bar'\n>\n>      -    Signed-off-by: Harald Nordgren <harald.nordgren@kostdoktorn.se>\n>           Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n>\n>        ## builtin/config.c ##\n>      @@ builtin/config.c: static void check_argc(int argc, int min, int max)\n>         exit(129);\n>        }\n>\n>      -+static void advise_setting_with_equals(const char *key, const char *value)\n>      ++static int is_valid_key(const char *key)\n>       +{\n>       + const char *last_dot = strrchr(key, '.');\n>      -+ const char *eq;\n>       +\n>      -+ if (!last_dot)\n>      -+         return;\n>      -+ eq = strchr(last_dot + 1, '=');\n>      -+ if (!eq)\n>      -+         return;\n>      -+ if (!value)\n>      -+         value = eq + 1;\n>      -+ if (!*value || strpbrk(value, \" \\t\\n\"))\n>      -+         return;\n>      -+ advise(_(\"did you mean \\\"git config set %.*s %s\\\"?\"),\n>      -+        (int)(eq - key), key, value);\n>      ++ return last_dot && isalpha(last_dot[1]);\n>      ++}\n>      ++\n>      ++static NORETURN void die_missing_set_value(const char *arg)\n>      ++{\n>      ++ const char *last_dot = strrchr(arg, '.');\n>      ++ const char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;\n>      ++ char *prefix = eq ? xstrndup(arg, eq - arg) : NULL;\n>      ++\n>      ++ if (prefix && is_valid_key(prefix)) {\n>      ++         error(_(\"missing value to set to the variable '%s'\"), arg);\n>      ++         advise(_(\"did you mean \\\"git config set %s %s\\\"?\"),\n>      ++                prefix, eq + 1);\n>      ++ } else if (is_valid_key(arg)) {\n>      ++         error(_(\"missing value to set to the variable '%s'\"), arg);\n>      ++ } else {\n>      ++         error(_(\"missing value to set to a variable with an invalid name '%s'\"),\n>      ++               arg);\n>      ++ }\n>      ++ free(prefix);\n>      ++ exit(129);\n>       +}\n>       +\n>        static void show_config_origin(const struct config_display_options *opts,\n>      @@ builtin/config.c: static int cmd_config_set(int argc, const char **argv, const c\n>\n>         argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n>                              PARSE_OPT_STOP_AT_NON_OPTION);\n>      -+ if (argc == 1 && strchr(argv[0], '=')) {\n>      -+         error(_(\"wrong number of arguments, should be 2\"));\n>      -+         advise_setting_with_equals(argv[0], NULL);\n>      -+         exit(129);\n>      -+ }\n>      ++ if (argc == 1)\n>      ++         die_missing_set_value(argv[0]);\n>         check_argc(argc, 2, 2);\n>\n>         if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n>      -@@ builtin/config.c: static int cmd_config_set(int argc, const char **argv, const char *prefix,\n>      -                  error(_(\"cannot overwrite multiple values with a single value\\n\"\n>      -                  \"       Use --value=<pattern>, --append or --all to change %s.\"), argv[0]);\n>      -  }\n>      -+ if (ret == CONFIG_INVALID_KEY)\n>      -+         advise_setting_with_equals(argv[0], argv[1]);\n>      -\n>      -  location_options_release(&location_opts);\n>      -  free(comment);\n>       @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n>         };\n>         char *value = NULL, *comment = NULL;\n>      @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, con\n>                 case 1: actions = ACTION_GET; break;\n>                 case 2: actions = ACTION_SET; break;\n>       @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n>      -          if (ret == CONFIG_NOTHING_SET)\n>      -                  error(_(\"cannot overwrite multiple values with a single value\\n\"\n>      -                  \"       Use a regexp, --add or --replace-all to change %s.\"), argv[0]);\n>      -+         else if (ret == CONFIG_INVALID_KEY)\n>      -+                 advise_setting_with_equals(argv[0], argv[1]);\n>      -  }\n>      -  else if (actions == ACTION_SET_ALL) {\n>      -          check_write(&location_opts.source);\n>      -@@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n>      -          check_argc(argc, 1, 2);\n>      -          ret = get_value(&location_opts, &display_opts, argv[0], argv[1],\n>      -                          0, flags);\n>      -+         if (ret == CONFIG_INVALID_KEY && actions_implicit)\n>      -+                 advise_setting_with_equals(argv[0], NULL);\n>      -  }\n>      -  else if (actions == ACTION_GET_ALL) {\n>      -          check_argc(argc, 1, 2);\n>      +                  error(_(\"no action specified\"));\n>      +                  exit(129);\n>      +          }\n>      ++ if (actions_implicit && argc == 1) {\n>      ++         const char *last_dot = strrchr(argv[0], '.');\n>      ++         if (last_dot && strchr(last_dot + 1, '='))\n>      ++                 die_missing_set_value(argv[0]);\n>      ++ }\n>      +  if (display_opts.omit_values &&\n>      +      !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {\n>      +          error(_(\"--name-only is only applicable to --list or --get-regexp\"));\n>\n>        ## t/t1300-config.sh ##\n>       @@ t/t1300-config.sh: test_expect_success 'invalid key' '\n>         test_must_fail git config inval.2key blabla\n>        '\n>\n>      -+test_expect_success 'misplaced \"=\" in key: bare 1-arg form hints' '\n>      -+ test_must_fail git config pull.rebase=false 2>err &&\n>      -+ test_grep \"invalid key: pull\\\\.rebase=false\" err &&\n>      ++test_expect_success 'set with 1 arg of \"key=value\": valid key suggests split form' '\n>      ++ test_must_fail git config set pull.rebase=false 2>err &&\n>      ++ test_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n>       + test_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n>       +'\n>       +\n>      -+test_expect_success 'misplaced \"=\" in key: bare 2-arg form uses given value' '\n>      -+ test_must_fail git config pull.rebase=false true 2>err &&\n>      -+ test_grep \"did you mean .git config set pull\\\\.rebase true.\" err\n>      -+'\n>      -+\n>      -+test_expect_success 'misplaced \"=\" in key: set subcommand uses given value' '\n>      -+ test_must_fail git config set pull.rebase=false true 2>err &&\n>      -+ test_grep \"did you mean .git config set pull\\\\.rebase true.\" err\n>      -+'\n>      -+\n>      -+test_expect_success 'misplaced \"=\" in key: set with single arg hints' '\n>      -+ test_must_fail git config set pull.rebase=false 2>err &&\n>      -+ test_grep \"wrong number of arguments\" err &&\n>      ++test_expect_success 'set with 1 arg of \"key=value\": implicit form suggests split form' '\n>      ++ test_must_fail git config pull.rebase=false 2>err &&\n>      ++ test_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n>       + test_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n>       +'\n>       +\n>      -+test_expect_success 'misplaced \"=\" in key: explicit --get does not hint' '\n>      -+ test_must_fail git config --get pull.rebase=false 2>err &&\n>      -+ test_grep \"invalid key: pull\\\\.rebase=false\" err &&\n>      ++test_expect_success 'set with 1 arg of \"key=value\": invalid key does not suggest split form' '\n>      ++ test_must_fail git config set foo=bar 2>err &&\n>      ++ test_grep \"missing value to set to a variable with an invalid name .foo=bar.\" err &&\n>       + test_grep ! \"did you mean\" err\n>       +'\n>       +\n>      -+test_expect_success 'misplaced \"=\" in key: get subcommand does not hint' '\n>      -+ test_must_fail git config get pull.rebase=false 2>err &&\n>      ++test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '\n>      ++ test_must_fail git config set foo.1bar=baz 2>err &&\n>      ++ test_grep \"missing value to set to a variable with an invalid name .foo\\\\.1bar=baz.\" err &&\n>       + test_grep ! \"did you mean\" err\n>       +'\n>       +\n>      -+test_expect_success 'misplaced \"=\" in key: unset subcommand does not hint' '\n>      -+ test_must_fail git config unset pull.rebase=false 2>err &&\n>      ++test_expect_success 'set with 1 arg of valid key reports missing value' '\n>      ++ test_must_fail git config set pull.rebase 2>err &&\n>      ++ test_grep \"missing value to set to the variable .pull\\\\.rebase.\" err &&\n>       + test_grep ! \"did you mean\" err\n>       +'\n>       +\n>      -+test_expect_success 'misplaced \"=\" in key: value with whitespace skips hint' '\n>      -+ test_must_fail git config set pull.rebase=false \"hello world\" 2>err &&\n>      -+ test_grep \"invalid key: pull\\\\.rebase=false\" err &&\n>      ++test_expect_success 'set with 2 args including \"=\" in invalid key does not suggest' '\n>      ++ test_must_fail git config set pull.rebase=false true 2>err &&\n>       + test_grep ! \"did you mean\" err\n>       +'\n>       +\n>      -+test_expect_success '\"=\" inside subsection is valid, no hint' '\n>      ++test_expect_success '\"=\" inside subsection is valid' '\n>       + test_when_finished \"rm -f subsection.cfg\" &&\n>      -+ git config set -f subsection.cfg foo.bar=baz.boo qux 2>err &&\n>      -+ test_grep ! \"did you mean\" err &&\n>      ++ git config set -f subsection.cfg foo.bar=baz.boo qux &&\n>       + echo qux >expect &&\n>       + git config get -f subsection.cfg foo.bar=baz.boo >actual &&\n>       + test_cmp expect actual\n>\n>\n>  builtin/config.c  | 39 ++++++++++++++++++++++++++++++++++++++-\n>  t/t1300-config.sh | 43 +++++++++++++++++++++++++++++++++++++++++++\n>  2 files changed, 81 insertions(+), 1 deletion(-)\n>\n> diff --git a/builtin/config.c b/builtin/config.c\n> index cf4ba0f7cc..6fe2d85814 100644\n> --- a/builtin/config.c\n> +++ b/builtin/config.c\n> @@ -1,6 +1,7 @@\n>  #define USE_THE_REPOSITORY_VARIABLE\n>  #include \"builtin.h\"\n>  #include \"abspath.h\"\n> +#include \"advice.h\"\n>  #include \"config.h\"\n>  #include \"color.h\"\n>  #include \"date.h\"\n> @@ -210,6 +211,33 @@ static void check_argc(int argc, int min, int max)\n>         exit(129);\n>  }\n>\n> +static int is_valid_key(const char *key)\n> +{\n> +       const char *last_dot = strrchr(key, '.');\n> +\n> +       return last_dot && isalpha(last_dot[1]);\n> +}\n> +\n> +static NORETURN void die_missing_set_value(const char *arg)\n> +{\n> +       const char *last_dot = strrchr(arg, '.');\n> +       const char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;\n> +       char *prefix = eq ? xstrndup(arg, eq - arg) : NULL;\n> +\n> +       if (prefix && is_valid_key(prefix)) {\n> +               error(_(\"missing value to set to the variable '%s'\"), arg);\n> +               advise(_(\"did you mean \\\"git config set %s %s\\\"?\"),\n> +                      prefix, eq + 1);\n> +       } else if (is_valid_key(arg)) {\n> +               error(_(\"missing value to set to the variable '%s'\"), arg);\n> +       } else {\n> +               error(_(\"missing value to set to a variable with an invalid name '%s'\"),\n> +                     arg);\n> +       }\n> +       free(prefix);\n> +       exit(129);\n> +}\n> +\n>  static void show_config_origin(const struct config_display_options *opts,\n>                                const struct key_value_info *kvi,\n>                                struct strbuf *buf)\n> @@ -1133,6 +1161,8 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,\n>\n>         argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n>                              PARSE_OPT_STOP_AT_NON_OPTION);\n> +       if (argc == 1)\n> +               die_missing_set_value(argv[0]);\n>         check_argc(argc, 2, 2);\n>\n>         if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n> @@ -1371,6 +1401,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n>         };\n>         char *value = NULL, *comment = NULL;\n>         int ret = 0;\n> +       int actions_implicit;\n>         struct key_value_info default_kvi = KVI_INIT;\n>\n>         argc = parse_options(argc, argv, prefix, opts,\n> @@ -1385,7 +1416,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n>                 exit(129);\n>         }\n>\n> -       if (actions == 0)\n> +       actions_implicit = (actions == 0);\n> +       if (actions_implicit)\n>                 switch (argc) {\n>                 case 1: actions = ACTION_GET; break;\n>                 case 2: actions = ACTION_SET; break;\n> @@ -1394,6 +1426,11 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n>                         error(_(\"no action specified\"));\n>                         exit(129);\n>                 }\n> +       if (actions_implicit && argc == 1) {\n> +               const char *last_dot = strrchr(argv[0], '.');\n> +               if (last_dot && strchr(last_dot + 1, '='))\n> +                       die_missing_set_value(argv[0]);\n> +       }\n>         if (display_opts.omit_values &&\n>             !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {\n>                 error(_(\"--name-only is only applicable to --list or --get-regexp\"));\n> diff --git a/t/t1300-config.sh b/t/t1300-config.sh\n> index 11fc976f3a..4a8a381bd8 100755\n> --- a/t/t1300-config.sh\n> +++ b/t/t1300-config.sh\n> @@ -469,6 +469,49 @@ test_expect_success 'invalid key' '\n>         test_must_fail git config inval.2key blabla\n>  '\n>\n> +test_expect_success 'set with 1 arg of \"key=value\": valid key suggests split form' '\n> +       test_must_fail git config set pull.rebase=false 2>err &&\n> +       test_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n> +       test_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n> +'\n> +\n> +test_expect_success 'set with 1 arg of \"key=value\": implicit form suggests split form' '\n> +       test_must_fail git config pull.rebase=false 2>err &&\n> +       test_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n> +       test_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n> +'\n> +\n> +test_expect_success 'set with 1 arg of \"key=value\": invalid key does not suggest split form' '\n> +       test_must_fail git config set foo=bar 2>err &&\n> +       test_grep \"missing value to set to a variable with an invalid name .foo=bar.\" err &&\n> +       test_grep ! \"did you mean\" err\n> +'\n> +\n> +test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '\n> +       test_must_fail git config set foo.1bar=baz 2>err &&\n> +       test_grep \"missing value to set to a variable with an invalid name .foo\\\\.1bar=baz.\" err &&\n> +       test_grep ! \"did you mean\" err\n> +'\n> +\n> +test_expect_success 'set with 1 arg of valid key reports missing value' '\n> +       test_must_fail git config set pull.rebase 2>err &&\n> +       test_grep \"missing value to set to the variable .pull\\\\.rebase.\" err &&\n> +       test_grep ! \"did you mean\" err\n> +'\n> +\n> +test_expect_success 'set with 2 args including \"=\" in invalid key does not suggest' '\n> +       test_must_fail git config set pull.rebase=false true 2>err &&\n> +       test_grep ! \"did you mean\" err\n> +'\n> +\n> +test_expect_success '\"=\" inside subsection is valid' '\n> +       test_when_finished \"rm -f subsection.cfg\" &&\n> +       git config set -f subsection.cfg foo.bar=baz.boo qux &&\n> +       echo qux >expect &&\n> +       git config get -f subsection.cfg foo.bar=baz.boo >actual &&\n> +       test_cmp expect actual\n> +'\n> +\n>  test_expect_success 'correct key' '\n>         git config 123456.a123 987\n>  '\n>\n> base-commit: 56a4f3c3a221adf1df9b39da69b8a6890f803157\n> --\n> gitgitgadget\n"},{"id":"544446","messageId":"xmqqa4td25bl.fsf@gitster.g","threadId":"65631","inReplyTo":"pull.2302.v4.git.git.1779823288005.gitgitgadget@gmail.com","subject":"Re: [PATCH v4] config: improve diagnostic for \"set\" with missing value","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-01T23:45:18Z","receivedAt":"2026-06-01T23:45:21Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> +static int is_valid_key(const char *key)\n> +{\n> +\tconst char *last_dot = strrchr(key, '.');\n> +\n> +\treturn last_dot && isalpha(last_dot[1]);\n> +}\n\nNone of these are valid configuration variable names, but this\nfunction would allow any of them, no?\n\n    1foo.bar\n    1foo.some.bar\n    foo.b_r\n    foo.some.b_r\n\nor does the caller reject such \"key\" before calling us?\n\n> +static NORETURN void die_missing_set_value(const char *arg)\n> +{\n> +\tconst char *last_dot = strrchr(arg, '.');\n> +\tconst char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;\n\nOK, the intention is to see \"foo.bar=baz\" and guess that assinging\nto \"foo.bar\" might be what the user wanted.  eq here would point at\nthat '='.  And ...\n\n> +\tchar *prefix = eq ? xstrndup(arg, eq - arg) : NULL;\n\n... prefix is our own copy of \"foo.bar\".\n\n> +\tif (prefix && is_valid_key(prefix)) {\n> +\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n> +\t\tadvise(_(\"did you mean \\\"git config set %s %s\\\"?\"),\n> +\t\t       prefix, eq + 1);\n\nOK.  If is_valid_key() rejected invalid variable names correctly,\nthis would catch $A=$B where $A is a plausible-looking name.\n\n> +\t} else if (is_valid_key(arg)) {\n> +\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n> +\t} else {\n> +\t\terror(_(\"missing value to set to a variable with an invalid name '%s'\"),\n> +\t\t      arg);\n> +\t}\n\nThe distinction among these three messages does look reasonable,\nprovided if is_valid_key() gives the correct result.\n\nI wonder if it is too hard to refactor existing logic (perhaps it is\nused in git_config_parse_key(), no?) to give us a less noisy version\nof it that we can use as is_valid_key() here?\n\nOther than that, the remainder of the code changes looked reasonable\nto me.  Thanks.\n"},{"id":"544447","messageId":"xmqq33z524yh.fsf@gitster.g","threadId":"65631","inReplyTo":"pull.2302.v4.git.git.1779823288005.gitgitgadget@gmail.com","subject":"Re: [PATCH v4] config: improve diagnostic for \"set\" with missing value","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-01T23:53:10Z","receivedAt":"2026-06-01T23:53:12Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> +static int is_valid_key(const char *key)\n> +{\n> +\tconst char *last_dot = strrchr(key, '.');\n> +\n> +\treturn last_dot && isalpha(last_dot[1]);\n> +}\n\nNone of these are valid configuration variable names, but this\nfunction would allow any of them, no?\n\n    1foo.bar\n    1foo.some.bar\n    foo.b_r\n    foo.some.b_r\n\nor does the caller reject such \"key\" before calling us?\n\n> +static NORETURN void die_missing_set_value(const char *arg)\n> +{\n> +\tconst char *last_dot = strrchr(arg, '.');\n> +\tconst char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;\n\nOK, the intention is to see \"foo.bar=baz\" and guess that assinging\nto \"foo.bar\" might be what the user wanted.  eq here would point at\nthat '='.  And ...\n\n> +\tchar *prefix = eq ? xstrndup(arg, eq - arg) : NULL;\n\n... prefix is our own copy of \"foo.bar\".\n\n> +\tif (prefix && is_valid_key(prefix)) {\n> +\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n> +\t\tadvise(_(\"did you mean \\\"git config set %s %s\\\"?\"),\n> +\t\t       prefix, eq + 1);\n\nOK.  If is_valid_key() rejected invalid variable names correctly,\nthis would catch $A=$B where $A is a plausible-looking name.\n\n> +\t} else if (is_valid_key(arg)) {\n> +\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n> +\t} else {\n> +\t\terror(_(\"missing value to set to a variable with an invalid name '%s'\"),\n> +\t\t      arg);\n> +\t}\n\nThe distinction among these three messages does look reasonable,\nprovided if is_valid_key() gives the correct result.\n\nI wonder if it is too hard to refactor existing logic (perhaps it is\nused in git_config_parse_key(), no?) to give us a less noisy version\nof it that we can use as is_valid_key() here?\n\nOther than that, the remainder of the code changes looked reasonable\nto me.  Thanks.\n"},{"id":"544509","messageId":"pull.2302.v5.git.git.1780407557.gitgitgadget@gmail.com","threadId":"65631","inReplyTo":"pull.2302.v4.git.git.1779823288005.gitgitgadget@gmail.com","subject":"[PATCH v5 0/2] config: suggest the correct form when key contains \"=\"","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-02T13:39:15Z","receivedAt":"2026-06-02T13:39:19Z","isPatch":true,"body":" * New commit config: let git_config_parse_key() validate quietly adds a\n   quiet parameter (and an optional store_key) so callers can validate\n   without writing to stderr.\n * Validation in die_missing_set_value() now routes through\n   git_config_parse_key(key, NULL, NULL, 1) instead of the previous local\n   helper.\n * Added tests for 1foo.bar=baz and foo.some.b_r=baz.\n\nHarald Nordgren (2):\n  config: let git_config_parse_key() validate quietly\n  config: improve diagnostic for \"set\" with missing value\n\n builtin/config.c   | 34 ++++++++++++++++++++++++++--\n config.c           | 34 ++++++++++++++++++----------\n config.h           |  2 +-\n submodule-config.c |  2 +-\n t/t1300-config.sh  | 55 ++++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 111 insertions(+), 16 deletions(-)\n\n\nbase-commit: 9ac3f193c05c2237e2b14ebaa1149e9fc8a1abe0\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2302%2FHaraldNordgren%2Fconfig-hint-equals-key-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2302/HaraldNordgren/config-hint-equals-key-v5\nPull-Request: https://github.com/git/git/pull/2302\n\nRange-diff vs v4:\n\n -:  ---------- > 1:  d938ebf95a config: let git_config_parse_key() validate quietly\n 1:  780b99409c ! 2:  e5a2070ee1 config: improve diagnostic for \"set\" with missing value\n     @@ builtin/config.c: static void check_argc(int argc, int min, int max)\n       \texit(129);\n       }\n       \n     -+static int is_valid_key(const char *key)\n     -+{\n     -+\tconst char *last_dot = strrchr(key, '.');\n     -+\n     -+\treturn last_dot && isalpha(last_dot[1]);\n     -+}\n     -+\n      +static NORETURN void die_missing_set_value(const char *arg)\n      +{\n      +\tconst char *last_dot = strrchr(arg, '.');\n      +\tconst char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;\n      +\tchar *prefix = eq ? xstrndup(arg, eq - arg) : NULL;\n      +\n     -+\tif (prefix && is_valid_key(prefix)) {\n     ++\tif (prefix && !git_config_parse_key(prefix, NULL, NULL, 1)) {\n      +\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n      +\t\tadvise(_(\"did you mean \\\"git config set %s %s\\\"?\"),\n      +\t\t       prefix, eq + 1);\n     -+\t} else if (is_valid_key(arg)) {\n     ++\t} else if (!git_config_parse_key(arg, NULL, NULL, 1)) {\n      +\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n      +\t} else {\n      +\t\terror(_(\"missing value to set to a variable with an invalid name '%s'\"),\n     @@ t/t1300-config.sh: test_expect_success 'invalid key' '\n      +\ttest_grep ! \"did you mean\" err\n      +'\n      +\n     ++test_expect_success 'set with 1 arg: digit-led section name is valid' '\n     ++\ttest_must_fail git config set 1foo.bar=baz 2>err &&\n     ++\ttest_grep \"missing value to set to the variable .1foo\\\\.bar=baz.\" err &&\n     ++\ttest_grep \"did you mean .git config set 1foo\\\\.bar baz.\" err\n     ++'\n     ++\n     ++test_expect_success 'set with 1 arg: subsection plus invalid variable name' '\n     ++\ttest_must_fail git config set foo.some.b_r=baz 2>err &&\n     ++\ttest_grep \"missing value to set to a variable with an invalid name .foo\\\\.some\\\\.b_r=baz.\" err &&\n     ++\ttest_grep ! \"did you mean\" err\n     ++'\n     ++\n      +test_expect_success 'set with 1 arg of valid key reports missing value' '\n      +\ttest_must_fail git config set pull.rebase 2>err &&\n      +\ttest_grep \"missing value to set to the variable .pull\\\\.rebase.\" err &&\n\n-- \ngitgitgadget\n"},{"id":"544510","messageId":"d938ebf95a817c00a415670c08b839747d711d29.1780407557.git.gitgitgadget@gmail.com","threadId":"65631","inReplyTo":"pull.2302.v5.git.git.1780407557.gitgitgadget@gmail.com","subject":"[PATCH v5 1/2] config: let git_config_parse_key() validate quietly","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-02T13:39:16Z","receivedAt":"2026-06-02T13:39:20Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a \"quiet\" parameter that suppresses the error() calls, and let\nstore_key be NULL to skip the canonical-copy allocation.  Existing\ncallers pass 0 for quiet.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/config.c   |  2 +-\n config.c           | 34 ++++++++++++++++++++++------------\n config.h           |  2 +-\n submodule-config.c |  2 +-\n 4 files changed, 25 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex cf4ba0f7cc..b3188cd8d4 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -555,7 +555,7 @@ static int get_value(const struct config_location_options *opts,\n \t\t\tgoto free_strings;\n \t\t}\n \t} else {\n-\t\tif (git_config_parse_key(key_, &key, NULL)) {\n+\t\tif (git_config_parse_key(key_, &key, NULL, 0)) {\n \t\t\tret = CONFIG_INVALID_KEY;\n \t\t\tgoto free_strings;\n \t\t}\ndiff --git a/config.c b/config.c\nindex a1b92fe083..81b31c5155 100644\n--- a/config.c\n+++ b/config.c\n@@ -536,11 +536,14 @@ static inline int iskeychar(int c)\n  * -2 if there is no section name in the key.\n  *\n  * store_key - pointer to char* which will hold a copy of the key with\n- *             lowercase section and variable name\n+ *             lowercase section and variable name, can be NULL to skip\n+ *             allocation when only validation is needed\n  * baselen - pointer to size_t which will hold the length of the\n  *           section + subsection part, can be NULL\n+ * quiet - when non-zero, suppress error() reports on rejection\n  */\n-int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n+int git_config_parse_key(const char *key, char **store_key, size_t *baselen_,\n+\t\t\t int quiet)\n {\n \tsize_t i, baselen;\n \tint dot;\n@@ -552,12 +555,14 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n \t */\n \n \tif (last_dot == NULL || last_dot == key) {\n-\t\terror(_(\"key does not contain a section: %s\"), key);\n+\t\tif (!quiet)\n+\t\t\terror(_(\"key does not contain a section: %s\"), key);\n \t\treturn -CONFIG_NO_SECTION_OR_NAME;\n \t}\n \n \tif (!last_dot[1]) {\n-\t\terror(_(\"key does not contain variable name: %s\"), key);\n+\t\tif (!quiet)\n+\t\t\terror(_(\"key does not contain variable name: %s\"), key);\n \t\treturn -CONFIG_NO_SECTION_OR_NAME;\n \t}\n \n@@ -568,7 +573,8 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n \t/*\n \t * Validate the key and while at it, lower case it for matching.\n \t */\n-\t*store_key = xmallocz(strlen(key));\n+\tif (store_key)\n+\t\t*store_key = xmallocz(strlen(key));\n \n \tdot = 0;\n \tfor (i = 0; key[i]; i++) {\n@@ -579,21 +585,25 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n \t\tif (!dot || i > baselen) {\n \t\t\tif (!iskeychar(c) ||\n \t\t\t    (i == baselen + 1 && !isalpha(c))) {\n-\t\t\t\terror(_(\"invalid key: %s\"), key);\n+\t\t\t\tif (!quiet)\n+\t\t\t\t\terror(_(\"invalid key: %s\"), key);\n \t\t\t\tgoto out_free_ret_1;\n \t\t\t}\n \t\t\tc = tolower(c);\n \t\t} else if (c == '\\n') {\n-\t\t\terror(_(\"invalid key (newline): %s\"), key);\n+\t\t\tif (!quiet)\n+\t\t\t\terror(_(\"invalid key (newline): %s\"), key);\n \t\t\tgoto out_free_ret_1;\n \t\t}\n-\t\t(*store_key)[i] = c;\n+\t\tif (store_key)\n+\t\t\t(*store_key)[i] = c;\n \t}\n \n \treturn 0;\n \n out_free_ret_1:\n-\tFREE_AND_NULL(*store_key);\n+\tif (store_key)\n+\t\tFREE_AND_NULL(*store_key);\n \treturn -CONFIG_INVALID_KEY;\n }\n \n@@ -609,7 +619,7 @@ static int config_parse_pair(const char *key, const char *value,\n \n \tif (!strlen(key))\n \t\treturn error(_(\"empty config key\"));\n-\tif (git_config_parse_key(key, &canonical_name, NULL))\n+\tif (git_config_parse_key(key, &canonical_name, NULL, 0))\n \t\treturn -1;\n \n \tret = (fn(canonical_name, value, &ctx, data) < 0) ? -1 : 0;\n@@ -1708,7 +1718,7 @@ static int configset_find_element(struct config_set *set, const char *key,\n \t * `key` may come from the user, so normalize it before using it\n \t * for querying entries from the hashmap.\n \t */\n-\tret = git_config_parse_key(key, &normalized_key, NULL);\n+\tret = git_config_parse_key(key, &normalized_key, NULL, 0);\n \tif (ret)\n \t\treturn ret;\n \n@@ -3001,7 +3011,7 @@ int repo_config_set_multivar_in_file_gently(struct repository *r,\n \tvalidate_comment_string(comment);\n \n \t/* parse-key returns negative; flip the sign to feed exit(3) */\n-\tret = 0 - git_config_parse_key(key, &store.key, &store.baselen);\n+\tret = 0 - git_config_parse_key(key, &store.key, &store.baselen, 0);\n \tif (ret)\n \t\tgoto out_free;\n \ndiff --git a/config.h b/config.h\nindex bf47fb3afc..2c66d334c1 100644\n--- a/config.h\n+++ b/config.h\n@@ -341,7 +341,7 @@ int repo_config_set_worktree_gently(struct repository *, const char *, const cha\n  */\n void repo_config_set(struct repository *, const char *, const char *);\n \n-int git_config_parse_key(const char *, char **, size_t *);\n+int git_config_parse_key(const char *, char **, size_t *, int quiet);\n \n /*\n  * The following macros specify flag bits that alter the behavior\ndiff --git a/submodule-config.c b/submodule-config.c\nindex a81897b4e0..a319956f7a 100644\n--- a/submodule-config.c\n+++ b/submodule-config.c\n@@ -970,7 +970,7 @@ int print_config_from_gitmodules(struct repository *repo, const char *key)\n \tint ret;\n \tchar *store_key;\n \n-\tret = git_config_parse_key(key, &store_key, NULL);\n+\tret = git_config_parse_key(key, &store_key, NULL, 0);\n \tif (ret < 0)\n \t\treturn CONFIG_INVALID_KEY;\n \n-- \ngitgitgadget\n\n"},{"id":"544511","messageId":"e5a2070ee1598bc345556b4afd01ae6d40fab633.1780407557.git.gitgitgadget@gmail.com","threadId":"65631","inReplyTo":"pull.2302.v5.git.git.1780407557.gitgitgadget@gmail.com","subject":"[PATCH v5 2/2] config: improve diagnostic for \"set\" with missing value","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-02T13:39:17Z","receivedAt":"2026-06-02T13:39:22Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git config set pull.rebase=false\" currently fails with \"wrong\nnumber of arguments\", and the implicit form \"git config\npull.rebase=false\" fails with \"invalid key\". Neither points at\nthe real problem: the value is missing.\n\nReport that directly, and when the argument has the shape\n\"<valid-key>=<value>\", also suggest the split form:\n\n    $ git config set pull.rebase=false\n    error: missing value to set to the variable 'pull.rebase=false'\n    hint: did you mean \"git config set pull.rebase false\"?\n\nWhen the prefix before \"=\" is not a valid key, drop the hint:\n\n    $ git config set foo=bar\n    error: missing value to set to a variable with an invalid name 'foo=bar'\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/config.c  | 32 ++++++++++++++++++++++++++-\n t/t1300-config.sh | 55 +++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 86 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex b3188cd8d4..a2d46d0ce1 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -1,6 +1,7 @@\n #define USE_THE_REPOSITORY_VARIABLE\n #include \"builtin.h\"\n #include \"abspath.h\"\n+#include \"advice.h\"\n #include \"config.h\"\n #include \"color.h\"\n #include \"date.h\"\n@@ -210,6 +211,26 @@ static void check_argc(int argc, int min, int max)\n \texit(129);\n }\n \n+static NORETURN void die_missing_set_value(const char *arg)\n+{\n+\tconst char *last_dot = strrchr(arg, '.');\n+\tconst char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;\n+\tchar *prefix = eq ? xstrndup(arg, eq - arg) : NULL;\n+\n+\tif (prefix && !git_config_parse_key(prefix, NULL, NULL, 1)) {\n+\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n+\t\tadvise(_(\"did you mean \\\"git config set %s %s\\\"?\"),\n+\t\t       prefix, eq + 1);\n+\t} else if (!git_config_parse_key(arg, NULL, NULL, 1)) {\n+\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n+\t} else {\n+\t\terror(_(\"missing value to set to a variable with an invalid name '%s'\"),\n+\t\t      arg);\n+\t}\n+\tfree(prefix);\n+\texit(129);\n+}\n+\n static void show_config_origin(const struct config_display_options *opts,\n \t\t\t       const struct key_value_info *kvi,\n \t\t\t       struct strbuf *buf)\n@@ -1133,6 +1154,8 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,\n \n \targc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tif (argc == 1)\n+\t\tdie_missing_set_value(argv[0]);\n \tcheck_argc(argc, 2, 2);\n \n \tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n@@ -1371,6 +1394,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t};\n \tchar *value = NULL, *comment = NULL;\n \tint ret = 0;\n+\tint actions_implicit;\n \tstruct key_value_info default_kvi = KVI_INIT;\n \n \targc = parse_options(argc, argv, prefix, opts,\n@@ -1385,7 +1409,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\texit(129);\n \t}\n \n-\tif (actions == 0)\n+\tactions_implicit = (actions == 0);\n+\tif (actions_implicit)\n \t\tswitch (argc) {\n \t\tcase 1: actions = ACTION_GET; break;\n \t\tcase 2: actions = ACTION_SET; break;\n@@ -1394,6 +1419,11 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\t\terror(_(\"no action specified\"));\n \t\t\texit(129);\n \t\t}\n+\tif (actions_implicit && argc == 1) {\n+\t\tconst char *last_dot = strrchr(argv[0], '.');\n+\t\tif (last_dot && strchr(last_dot + 1, '='))\n+\t\t\tdie_missing_set_value(argv[0]);\n+\t}\n \tif (display_opts.omit_values &&\n \t    !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {\n \t\terror(_(\"--name-only is only applicable to --list or --get-regexp\"));\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 11fc976f3a..ed122d1100 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -469,6 +469,61 @@ test_expect_success 'invalid key' '\n \ttest_must_fail git config inval.2key blabla\n '\n \n+test_expect_success 'set with 1 arg of \"key=value\": valid key suggests split form' '\n+\ttest_must_fail git config set pull.rebase=false 2>err &&\n+\ttest_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n+'\n+\n+test_expect_success 'set with 1 arg of \"key=value\": implicit form suggests split form' '\n+\ttest_must_fail git config pull.rebase=false 2>err &&\n+\ttest_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n+'\n+\n+test_expect_success 'set with 1 arg of \"key=value\": invalid key does not suggest split form' '\n+\ttest_must_fail git config set foo=bar 2>err &&\n+\ttest_grep \"missing value to set to a variable with an invalid name .foo=bar.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '\n+\ttest_must_fail git config set foo.1bar=baz 2>err &&\n+\ttest_grep \"missing value to set to a variable with an invalid name .foo\\\\.1bar=baz.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 1 arg: digit-led section name is valid' '\n+\ttest_must_fail git config set 1foo.bar=baz 2>err &&\n+\ttest_grep \"missing value to set to the variable .1foo\\\\.bar=baz.\" err &&\n+\ttest_grep \"did you mean .git config set 1foo\\\\.bar baz.\" err\n+'\n+\n+test_expect_success 'set with 1 arg: subsection plus invalid variable name' '\n+\ttest_must_fail git config set foo.some.b_r=baz 2>err &&\n+\ttest_grep \"missing value to set to a variable with an invalid name .foo\\\\.some\\\\.b_r=baz.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 1 arg of valid key reports missing value' '\n+\ttest_must_fail git config set pull.rebase 2>err &&\n+\ttest_grep \"missing value to set to the variable .pull\\\\.rebase.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 2 args including \"=\" in invalid key does not suggest' '\n+\ttest_must_fail git config set pull.rebase=false true 2>err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success '\"=\" inside subsection is valid' '\n+\ttest_when_finished \"rm -f subsection.cfg\" &&\n+\tgit config set -f subsection.cfg foo.bar=baz.boo qux &&\n+\techo qux >expect &&\n+\tgit config get -f subsection.cfg foo.bar=baz.boo >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'correct key' '\n \tgit config 123456.a123 987\n '\n-- \ngitgitgadget\n"},{"id":"544516","messageId":"xmqqtsrlujah.fsf@gitster.g","threadId":"65631","inReplyTo":"d938ebf95a817c00a415670c08b839747d711d29.1780407557.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 1/2] config: let git_config_parse_key() validate quietly","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-02T14:08:22Z","receivedAt":"2026-06-02T14:08:25Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> Add a \"quiet\" parameter that suppresses the error() calls, and let\n> store_key be NULL to skip the canonical-copy allocation.  Existing\n> callers pass 0 for quiet.\n\nHmph.\n\nThe way this patch did this may have been easier to implement, but\nis a bit different from what I had in mind when I suggested to\n\"refactor\" the existing logic.\n\nPerhaps the updated \"git_config_parse_key()\" in this patch should be\nrenamed to be a file-scape static internal helper, and the existing\n\"git_config_parse_key()\" should become a thin wrapper around that\nnew helper function, retaining the current external interface,\nrequiring no changes to existing callers.\n\nThen in the next step, config.[ch] can add a new entry point that\nserves the purpose of is_valid_key() in the previous iteration,\nperhaps call it is_valid_git_config_key() or something like that\n(Patrick or others may want to suggest a better word order in its\nname).  That way, we do not have to sprinkle many calls into\nthis (rather ugly) version of git_config_parse_key() with overly\nwide interface that repeats meaningless NULL/0/1 parameters that no\ncallers want to use (other than for the purpose of differenciating\nthe real git_config_parse_key() calls from the new calls made to the\nsame function to ask \"is this a valid key or not, yes/no?\".\n\nThanks.\n"},{"id":"544517","messageId":"xmqqfr35uiu5.fsf@gitster.g","threadId":"65631","inReplyTo":"e5a2070ee1598bc345556b4afd01ae6d40fab633.1780407557.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 2/2] config: improve diagnostic for \"set\" with missing value","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-02T14:18:10Z","receivedAt":"2026-06-02T14:18:13Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> diff --git a/t/t1300-config.sh b/t/t1300-config.sh\n> index 11fc976f3a..ed122d1100 100755\n> --- a/t/t1300-config.sh\n> +++ b/t/t1300-config.sh\n> @@ -469,6 +469,61 @@ test_expect_success 'invalid key' '\n>  \ttest_must_fail git config inval.2key blabla\n>  '\n>  \n> +test_expect_success 'set with 1 arg of \"key=value\": valid key suggests split form' '\n> +\ttest_must_fail git config set pull.rebase=false 2>err &&\n> +\ttest_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n> +\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n> +'\n\nThis is a syntax error of the command line, but the lhs of '=' makes\nus suspect that the user may have meant to assign to that variable.\nMakes perfect sense.\n\n> +test_expect_success 'set with 1 arg of \"key=value\": implicit form suggests split form' '\n> +\ttest_must_fail git config pull.rebase=false 2>err &&\n> +\ttest_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n> +\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n> +'\n\nDitto, the syntax may be an implicit \"get\" with bogus variable name,\nor an implicit \"set\" with variable name and its value concatenated\ninto one argument with '='.  The message seems to be assuming the\nlatter, which is OK to me.\n\n> +test_expect_success 'set with 1 arg of \"key=value\": invalid key does not suggest split form' '\n> +\ttest_must_fail git config set foo=bar 2>err &&\n> +\ttest_grep \"missing value to set to a variable with an invalid name .foo=bar.\" err &&\n> +\ttest_grep ! \"did you mean\" err\n> +'\n\nOK.\n\n> +test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '\n> +\ttest_must_fail git config set foo.1bar=baz 2>err &&\n> +\ttest_grep \"missing value to set to a variable with an invalid name .foo\\\\.1bar=baz.\" err &&\n> +\ttest_grep ! \"did you mean\" err\n> +'\n\nOK.  The above two should always give us the same error (except for\nthe actual bogus names given by the user to the command).\n\n> +test_expect_success 'set with 1 arg: digit-led section name is valid' '\n> +\ttest_must_fail git config set 1foo.bar=baz 2>err &&\n> +\ttest_grep \"missing value to set to the variable .1foo\\\\.bar=baz.\" err &&\n> +\ttest_grep \"did you mean .git config set 1foo\\\\.bar baz.\" err\n> +'\n\nOK.\n\n> +test_expect_success 'set with 1 arg: subsection plus invalid variable name' '\n> +\ttest_must_fail git config set foo.some.b_r=baz 2>err &&\n> +\ttest_grep \"missing value to set to a variable with an invalid name .foo\\\\.some\\\\.b_r=baz.\" err &&\n> +\ttest_grep ! \"did you mean\" err\n> +'\n\nThis is the third one that should be identical to earlier two that\ngave a bogus variable name.\n\n> +test_expect_success 'set with 1 arg of valid key reports missing value' '\n> +\ttest_must_fail git config set pull.rebase 2>err &&\n> +\ttest_grep \"missing value to set to the variable .pull\\\\.rebase.\" err &&\n> +\ttest_grep ! \"did you mean\" err\n> +'\n\nDid we see this already?  No, this is different from the earlier one\nthat had \"=false\".  This is a bog standard \"you said set but did not\nsay what value to set to\".  Good.\n\n> +test_expect_success 'set with 2 args including \"=\" in invalid key does not suggest' '\n> +\ttest_must_fail git config set pull.rebase=false true 2>err &&\n> +\ttest_grep ! \"did you mean\" err\n> +'\n\nOK.  Do we want to see that the bogus variable name reported?\n\n> +test_expect_success '\"=\" inside subsection is valid' '\n> +\ttest_when_finished \"rm -f subsection.cfg\" &&\n> +\tgit config set -f subsection.cfg foo.bar=baz.boo qux &&\n> +\techo qux >expect &&\n> +\tgit config get -f subsection.cfg foo.bar=baz.boo >actual &&\n> +\ttest_cmp expect actual\n> +'\n\nExcellent.\n"},{"id":"544533","messageId":"CAHwyqnXC=F-ewFy3nejzKZcSNNe5L73PcaH+b30wg_BKNpStYA@mail.gmail.com","threadId":"65631","inReplyTo":"xmqqtsrlujah.fsf@gitster.g","subject":"Re: [PATCH v5 1/2] config: let git_config_parse_key() validate quietly","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-06-02T16:31:36Z","receivedAt":"2026-06-02T16:32:14Z","isPatch":true,"body":"> Perhaps the updated \"git_config_parse_key()\" in this patch should be\n> renamed to be a file-scape static internal helper, and the existing\n> \"git_config_parse_key()\" should become a thin wrapper around that\n> new helper function, retaining the current external interface,\n> requiring no changes to existing callers.\n\nI want to remember a discussion on one of my earlier topics, a few\nmonths back, where someone else suggested instead of introducing two\nthin wrappers over a helper, we should update the callers instead.\n\nBut for me either way is fine, maybe here it makes more sense, because\nof the repeated NULL/0/1 parameters.\n\n\nHarald\n"},{"id":"544549","messageId":"pull.2302.v6.git.git.1780425808.gitgitgadget@gmail.com","threadId":"65631","inReplyTo":"pull.2302.v5.git.git.1780407557.gitgitgadget@gmail.com","subject":"[PATCH v6 0/2] config: suggest the correct form when key contains \"=\"","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-02T18:43:26Z","receivedAt":"2026-06-02T18:43:30Z","isPatch":true,"body":" * The quiet parameter now lives on a static do_parse_config_key() instead\n   of git_config_parse_key() itself. git_config_parse_key() is back to its\n   three-argument signature; existing callers don't change.\n * New public git_config_key_is_valid() for callers that only need a yes/no\n   check.\n\nHarald Nordgren (2):\n  config: add git_config_key_is_valid() for quiet validation\n  config: improve diagnostic for \"set\" with missing value\n\n builtin/config.c  | 32 ++++++++++++++++++++++++++-\n config.c          | 38 ++++++++++++++++++++++++--------\n config.h          |  2 ++\n t/t1300-config.sh | 56 +++++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 118 insertions(+), 10 deletions(-)\n\n\nbase-commit: 9ac3f193c05c2237e2b14ebaa1149e9fc8a1abe0\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2302%2FHaraldNordgren%2Fconfig-hint-equals-key-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2302/HaraldNordgren/config-hint-equals-key-v6\nPull-Request: https://github.com/git/git/pull/2302\n\nRange-diff vs v5:\n\n 1:  d938ebf95a ! 1:  7400ca41bb config: let git_config_parse_key() validate quietly\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    config: let git_config_parse_key() validate quietly\n     +    config: add git_config_key_is_valid() for quiet validation\n      \n     -    Add a \"quiet\" parameter that suppresses the error() calls, and let\n     -    store_key be NULL to skip the canonical-copy allocation.  Existing\n     -    callers pass 0 for quiet.\n     +    Move the body of git_config_parse_key() into a static helper\n     +    do_parse_config_key() that takes a \"quiet\" flag and treats\n     +    store_key as optional.  git_config_parse_key() becomes a thin\n     +    wrapper.\n      \n     -    Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n     +    Add git_config_key_is_valid() for callers that only need to\n     +    know whether a key is well-formed.\n      \n     - ## builtin/config.c ##\n     -@@ builtin/config.c: static int get_value(const struct config_location_options *opts,\n     - \t\t\tgoto free_strings;\n     - \t\t}\n     - \t} else {\n     --\t\tif (git_config_parse_key(key_, &key, NULL)) {\n     -+\t\tif (git_config_parse_key(key_, &key, NULL, 0)) {\n     - \t\t\tret = CONFIG_INVALID_KEY;\n     - \t\t\tgoto free_strings;\n     - \t\t}\n     +    Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## config.c ##\n      @@ config.c: static inline int iskeychar(int c)\n     @@ config.c: static inline int iskeychar(int c)\n      + * quiet - when non-zero, suppress error() reports on rejection\n        */\n      -int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n     -+int git_config_parse_key(const char *key, char **store_key, size_t *baselen_,\n     -+\t\t\t int quiet)\n     ++static int do_parse_config_key(const char *key, char **store_key,\n     ++\t\t\t       size_t *baselen_, int quiet)\n       {\n       \tsize_t i, baselen;\n       \tint dot;\n     @@ config.c: int git_config_parse_key(const char *key, char **store_key, size_t *ba\n       \treturn -CONFIG_INVALID_KEY;\n       }\n       \n     -@@ config.c: static int config_parse_pair(const char *key, const char *value,\n     - \n     - \tif (!strlen(key))\n     - \t\treturn error(_(\"empty config key\"));\n     --\tif (git_config_parse_key(key, &canonical_name, NULL))\n     -+\tif (git_config_parse_key(key, &canonical_name, NULL, 0))\n     - \t\treturn -1;\n     - \n     - \tret = (fn(canonical_name, value, &ctx, data) < 0) ? -1 : 0;\n     -@@ config.c: static int configset_find_element(struct config_set *set, const char *key,\n     - \t * `key` may come from the user, so normalize it before using it\n     - \t * for querying entries from the hashmap.\n     - \t */\n     --\tret = git_config_parse_key(key, &normalized_key, NULL);\n     -+\tret = git_config_parse_key(key, &normalized_key, NULL, 0);\n     - \tif (ret)\n     - \t\treturn ret;\n     - \n     -@@ config.c: int repo_config_set_multivar_in_file_gently(struct repository *r,\n     - \tvalidate_comment_string(comment);\n     - \n     - \t/* parse-key returns negative; flip the sign to feed exit(3) */\n     --\tret = 0 - git_config_parse_key(key, &store.key, &store.baselen);\n     -+\tret = 0 - git_config_parse_key(key, &store.key, &store.baselen, 0);\n     - \tif (ret)\n     - \t\tgoto out_free;\n     - \n     ++int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n     ++{\n     ++\treturn do_parse_config_key(key, store_key, baselen_, 0);\n     ++}\n     ++\n     ++int git_config_key_is_valid(const char *key)\n     ++{\n     ++\treturn !do_parse_config_key(key, NULL, NULL, 1);\n     ++}\n     ++\n     + static int config_parse_pair(const char *key, const char *value,\n     + \t\t\t     struct key_value_info *kvi,\n     + \t\t\t     config_fn_t fn, void *data)\n      \n       ## config.h ##\n     -@@ config.h: int repo_config_set_worktree_gently(struct repository *, const char *, const cha\n     -  */\n     - void repo_config_set(struct repository *, const char *, const char *);\n     +@@ config.h: void repo_config_set(struct repository *, const char *, const char *);\n       \n     --int git_config_parse_key(const char *, char **, size_t *);\n     -+int git_config_parse_key(const char *, char **, size_t *, int quiet);\n     + int git_config_parse_key(const char *, char **, size_t *);\n       \n     ++int git_config_key_is_valid(const char *);\n     ++\n       /*\n        * The following macros specify flag bits that alter the behavior\n     -\n     - ## submodule-config.c ##\n     -@@ submodule-config.c: int print_config_from_gitmodules(struct repository *repo, const char *key)\n     - \tint ret;\n     - \tchar *store_key;\n     - \n     --\tret = git_config_parse_key(key, &store_key, NULL);\n     -+\tret = git_config_parse_key(key, &store_key, NULL, 0);\n     - \tif (ret < 0)\n     - \t\treturn CONFIG_INVALID_KEY;\n     - \n     +  * of the repo_config_set_multivar*() methods.\n 2:  e5a2070ee1 ! 2:  a7f8a084c7 config: improve diagnostic for \"set\" with missing value\n     @@ builtin/config.c: static void check_argc(int argc, int min, int max)\n      +\tconst char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;\n      +\tchar *prefix = eq ? xstrndup(arg, eq - arg) : NULL;\n      +\n     -+\tif (prefix && !git_config_parse_key(prefix, NULL, NULL, 1)) {\n     ++\tif (prefix && git_config_key_is_valid(prefix)) {\n      +\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n      +\t\tadvise(_(\"did you mean \\\"git config set %s %s\\\"?\"),\n      +\t\t       prefix, eq + 1);\n     -+\t} else if (!git_config_parse_key(arg, NULL, NULL, 1)) {\n     ++\t} else if (git_config_key_is_valid(arg)) {\n      +\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n      +\t} else {\n      +\t\terror(_(\"missing value to set to a variable with an invalid name '%s'\"),\n     @@ t/t1300-config.sh: test_expect_success 'invalid key' '\n      +\n      +test_expect_success 'set with 2 args including \"=\" in invalid key does not suggest' '\n      +\ttest_must_fail git config set pull.rebase=false true 2>err &&\n     ++\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n      +\ttest_grep ! \"did you mean\" err\n      +'\n      +\n\n-- \ngitgitgadget\n"},{"id":"544550","messageId":"7400ca41bbacab40c28f21f63327a9808e568e05.1780425808.git.gitgitgadget@gmail.com","threadId":"65631","inReplyTo":"pull.2302.v6.git.git.1780425808.gitgitgadget@gmail.com","subject":"[PATCH v6 1/2] config: add git_config_key_is_valid() for quiet validation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-02T18:43:27Z","receivedAt":"2026-06-02T18:43:32Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nMove the body of git_config_parse_key() into a static helper\ndo_parse_config_key() that takes a \"quiet\" flag and treats\nstore_key as optional.  git_config_parse_key() becomes a thin\nwrapper.\n\nAdd git_config_key_is_valid() for callers that only need to\nknow whether a key is well-formed.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n config.c | 38 +++++++++++++++++++++++++++++---------\n config.h |  2 ++\n 2 files changed, 31 insertions(+), 9 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex a1b92fe083..45144f73c5 100644\n--- a/config.c\n+++ b/config.c\n@@ -536,11 +536,14 @@ static inline int iskeychar(int c)\n  * -2 if there is no section name in the key.\n  *\n  * store_key - pointer to char* which will hold a copy of the key with\n- *             lowercase section and variable name\n+ *             lowercase section and variable name, can be NULL to skip\n+ *             allocation when only validation is needed\n  * baselen - pointer to size_t which will hold the length of the\n  *           section + subsection part, can be NULL\n+ * quiet - when non-zero, suppress error() reports on rejection\n  */\n-int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n+static int do_parse_config_key(const char *key, char **store_key,\n+\t\t\t       size_t *baselen_, int quiet)\n {\n \tsize_t i, baselen;\n \tint dot;\n@@ -552,12 +555,14 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n \t */\n \n \tif (last_dot == NULL || last_dot == key) {\n-\t\terror(_(\"key does not contain a section: %s\"), key);\n+\t\tif (!quiet)\n+\t\t\terror(_(\"key does not contain a section: %s\"), key);\n \t\treturn -CONFIG_NO_SECTION_OR_NAME;\n \t}\n \n \tif (!last_dot[1]) {\n-\t\terror(_(\"key does not contain variable name: %s\"), key);\n+\t\tif (!quiet)\n+\t\t\terror(_(\"key does not contain variable name: %s\"), key);\n \t\treturn -CONFIG_NO_SECTION_OR_NAME;\n \t}\n \n@@ -568,7 +573,8 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n \t/*\n \t * Validate the key and while at it, lower case it for matching.\n \t */\n-\t*store_key = xmallocz(strlen(key));\n+\tif (store_key)\n+\t\t*store_key = xmallocz(strlen(key));\n \n \tdot = 0;\n \tfor (i = 0; key[i]; i++) {\n@@ -579,24 +585,38 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n \t\tif (!dot || i > baselen) {\n \t\t\tif (!iskeychar(c) ||\n \t\t\t    (i == baselen + 1 && !isalpha(c))) {\n-\t\t\t\terror(_(\"invalid key: %s\"), key);\n+\t\t\t\tif (!quiet)\n+\t\t\t\t\terror(_(\"invalid key: %s\"), key);\n \t\t\t\tgoto out_free_ret_1;\n \t\t\t}\n \t\t\tc = tolower(c);\n \t\t} else if (c == '\\n') {\n-\t\t\terror(_(\"invalid key (newline): %s\"), key);\n+\t\t\tif (!quiet)\n+\t\t\t\terror(_(\"invalid key (newline): %s\"), key);\n \t\t\tgoto out_free_ret_1;\n \t\t}\n-\t\t(*store_key)[i] = c;\n+\t\tif (store_key)\n+\t\t\t(*store_key)[i] = c;\n \t}\n \n \treturn 0;\n \n out_free_ret_1:\n-\tFREE_AND_NULL(*store_key);\n+\tif (store_key)\n+\t\tFREE_AND_NULL(*store_key);\n \treturn -CONFIG_INVALID_KEY;\n }\n \n+int git_config_parse_key(const char *key, char **store_key, size_t *baselen_)\n+{\n+\treturn do_parse_config_key(key, store_key, baselen_, 0);\n+}\n+\n+int git_config_key_is_valid(const char *key)\n+{\n+\treturn !do_parse_config_key(key, NULL, NULL, 1);\n+}\n+\n static int config_parse_pair(const char *key, const char *value,\n \t\t\t     struct key_value_info *kvi,\n \t\t\t     config_fn_t fn, void *data)\ndiff --git a/config.h b/config.h\nindex bf47fb3afc..31fe3e2961 100644\n--- a/config.h\n+++ b/config.h\n@@ -343,6 +343,8 @@ void repo_config_set(struct repository *, const char *, const char *);\n \n int git_config_parse_key(const char *, char **, size_t *);\n \n+int git_config_key_is_valid(const char *);\n+\n /*\n  * The following macros specify flag bits that alter the behavior\n  * of the repo_config_set_multivar*() methods.\n-- \ngitgitgadget\n\n"},{"id":"544551","messageId":"a7f8a084c7c2858638b8cdd145cb21ffcdfd9c57.1780425808.git.gitgitgadget@gmail.com","threadId":"65631","inReplyTo":"pull.2302.v6.git.git.1780425808.gitgitgadget@gmail.com","subject":"[PATCH v6 2/2] config: improve diagnostic for \"set\" with missing value","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-02T18:43:28Z","receivedAt":"2026-06-02T18:43:33Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git config set pull.rebase=false\" currently fails with \"wrong\nnumber of arguments\", and the implicit form \"git config\npull.rebase=false\" fails with \"invalid key\". Neither points at\nthe real problem: the value is missing.\n\nReport that directly, and when the argument has the shape\n\"<valid-key>=<value>\", also suggest the split form:\n\n    $ git config set pull.rebase=false\n    error: missing value to set to the variable 'pull.rebase=false'\n    hint: did you mean \"git config set pull.rebase false\"?\n\nWhen the prefix before \"=\" is not a valid key, drop the hint:\n\n    $ git config set foo=bar\n    error: missing value to set to a variable with an invalid name 'foo=bar'\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/config.c  | 32 ++++++++++++++++++++++++++-\n t/t1300-config.sh | 56 +++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 87 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex cf4ba0f7cc..8d8ec0beea 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -1,6 +1,7 @@\n #define USE_THE_REPOSITORY_VARIABLE\n #include \"builtin.h\"\n #include \"abspath.h\"\n+#include \"advice.h\"\n #include \"config.h\"\n #include \"color.h\"\n #include \"date.h\"\n@@ -210,6 +211,26 @@ static void check_argc(int argc, int min, int max)\n \texit(129);\n }\n \n+static NORETURN void die_missing_set_value(const char *arg)\n+{\n+\tconst char *last_dot = strrchr(arg, '.');\n+\tconst char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;\n+\tchar *prefix = eq ? xstrndup(arg, eq - arg) : NULL;\n+\n+\tif (prefix && git_config_key_is_valid(prefix)) {\n+\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n+\t\tadvise(_(\"did you mean \\\"git config set %s %s\\\"?\"),\n+\t\t       prefix, eq + 1);\n+\t} else if (git_config_key_is_valid(arg)) {\n+\t\terror(_(\"missing value to set to the variable '%s'\"), arg);\n+\t} else {\n+\t\terror(_(\"missing value to set to a variable with an invalid name '%s'\"),\n+\t\t      arg);\n+\t}\n+\tfree(prefix);\n+\texit(129);\n+}\n+\n static void show_config_origin(const struct config_display_options *opts,\n \t\t\t       const struct key_value_info *kvi,\n \t\t\t       struct strbuf *buf)\n@@ -1133,6 +1154,8 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,\n \n \targc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tif (argc == 1)\n+\t\tdie_missing_set_value(argv[0]);\n \tcheck_argc(argc, 2, 2);\n \n \tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n@@ -1371,6 +1394,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t};\n \tchar *value = NULL, *comment = NULL;\n \tint ret = 0;\n+\tint actions_implicit;\n \tstruct key_value_info default_kvi = KVI_INIT;\n \n \targc = parse_options(argc, argv, prefix, opts,\n@@ -1385,7 +1409,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\texit(129);\n \t}\n \n-\tif (actions == 0)\n+\tactions_implicit = (actions == 0);\n+\tif (actions_implicit)\n \t\tswitch (argc) {\n \t\tcase 1: actions = ACTION_GET; break;\n \t\tcase 2: actions = ACTION_SET; break;\n@@ -1394,6 +1419,11 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\t\terror(_(\"no action specified\"));\n \t\t\texit(129);\n \t\t}\n+\tif (actions_implicit && argc == 1) {\n+\t\tconst char *last_dot = strrchr(argv[0], '.');\n+\t\tif (last_dot && strchr(last_dot + 1, '='))\n+\t\t\tdie_missing_set_value(argv[0]);\n+\t}\n \tif (display_opts.omit_values &&\n \t    !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {\n \t\terror(_(\"--name-only is only applicable to --list or --get-regexp\"));\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 11fc976f3a..87ca11a127 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -469,6 +469,62 @@ test_expect_success 'invalid key' '\n \ttest_must_fail git config inval.2key blabla\n '\n \n+test_expect_success 'set with 1 arg of \"key=value\": valid key suggests split form' '\n+\ttest_must_fail git config set pull.rebase=false 2>err &&\n+\ttest_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n+'\n+\n+test_expect_success 'set with 1 arg of \"key=value\": implicit form suggests split form' '\n+\ttest_must_fail git config pull.rebase=false 2>err &&\n+\ttest_grep \"missing value to set to the variable .pull\\\\.rebase=false.\" err &&\n+\ttest_grep \"did you mean .git config set pull\\\\.rebase false.\" err\n+'\n+\n+test_expect_success 'set with 1 arg of \"key=value\": invalid key does not suggest split form' '\n+\ttest_must_fail git config set foo=bar 2>err &&\n+\ttest_grep \"missing value to set to a variable with an invalid name .foo=bar.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '\n+\ttest_must_fail git config set foo.1bar=baz 2>err &&\n+\ttest_grep \"missing value to set to a variable with an invalid name .foo\\\\.1bar=baz.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 1 arg: digit-led section name is valid' '\n+\ttest_must_fail git config set 1foo.bar=baz 2>err &&\n+\ttest_grep \"missing value to set to the variable .1foo\\\\.bar=baz.\" err &&\n+\ttest_grep \"did you mean .git config set 1foo\\\\.bar baz.\" err\n+'\n+\n+test_expect_success 'set with 1 arg: subsection plus invalid variable name' '\n+\ttest_must_fail git config set foo.some.b_r=baz 2>err &&\n+\ttest_grep \"missing value to set to a variable with an invalid name .foo\\\\.some\\\\.b_r=baz.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 1 arg of valid key reports missing value' '\n+\ttest_must_fail git config set pull.rebase 2>err &&\n+\ttest_grep \"missing value to set to the variable .pull\\\\.rebase.\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success 'set with 2 args including \"=\" in invalid key does not suggest' '\n+\ttest_must_fail git config set pull.rebase=false true 2>err &&\n+\ttest_grep \"invalid key: pull\\\\.rebase=false\" err &&\n+\ttest_grep ! \"did you mean\" err\n+'\n+\n+test_expect_success '\"=\" inside subsection is valid' '\n+\ttest_when_finished \"rm -f subsection.cfg\" &&\n+\tgit config set -f subsection.cfg foo.bar=baz.boo qux &&\n+\techo qux >expect &&\n+\tgit config get -f subsection.cfg foo.bar=baz.boo >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'correct key' '\n \tgit config 123456.a123 987\n '\n-- \ngitgitgadget\n"},{"id":"544652","messageId":"xmqq7bofqfga.fsf@gitster.g","threadId":"65631","inReplyTo":"CAHwyqnXC=F-ewFy3nejzKZcSNNe5L73PcaH+b30wg_BKNpStYA@mail.gmail.com","subject":"Re: [PATCH v5 1/2] config: let git_config_parse_key() validate quietly","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-04T01:09:25Z","receivedAt":"2026-06-04T01:09:27Z","isPatch":true,"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> Perhaps the updated \"git_config_parse_key()\" in this patch should be\n>> renamed to be a file-scape static internal helper, and the existing\n>> \"git_config_parse_key()\" should become a thin wrapper around that\n>> new helper function, retaining the current external interface,\n>> requiring no changes to existing callers.\n>\n> I want to remember a discussion on one of my earlier topics, a few\n> months back, where someone else suggested instead of introducing two\n> thin wrappers over a helper, we should update the callers instead.\n>\n> But for me either way is fine, maybe here it makes more sense, because\n> of the repeated NULL/0/1 parameters.\n\nIf the \"quiet\" and \"store_key\" setting were independent, then I\nwouldn't have made such a suggestion.  But I got an impression that\nwith the updated code, there wasn't a valid use case to ask to\nquietly store the discovered key.\n\nAn ideal refactoring would have been a low level helper function\nthat only yields error code, and git_config_parse_key() would call\nit and react to the returned error code, stores the discovered key,\nand produces error message on its own.  Then such an \"always quiet\"\nhelper can be used for the purpose of the new caller, without having\nto have \"if (!quiet)\" sprinkled all over.  But that is certainly\ncumbersome to arrange.\n\n"},{"id":"544653","messageId":"xmqq1penqfg2.fsf@gitster.g","threadId":"65631","inReplyTo":"pull.2302.v6.git.git.1780425808.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 0/2] config: suggest the correct form when key contains \"=\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-04T01:09:33Z","receivedAt":"2026-06-04T01:09:35Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  * The quiet parameter now lives on a static do_parse_config_key() instead\n>    of git_config_parse_key() itself. git_config_parse_key() is back to its\n>    three-argument signature; existing callers don't change.\n>  * New public git_config_key_is_valid() for callers that only need a yes/no\n>    check.\n>\n> Harald Nordgren (2):\n>   config: add git_config_key_is_valid() for quiet validation\n>   config: improve diagnostic for \"set\" with missing value\n>\n>  builtin/config.c  | 32 ++++++++++++++++++++++++++-\n>  config.c          | 38 ++++++++++++++++++++++++--------\n>  config.h          |  2 ++\n>  t/t1300-config.sh | 56 +++++++++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 118 insertions(+), 10 deletions(-)\n\nLooking good.  Thanks.  Will queue.\n"}]}