{"thread":{"id":"65895","subject":"[PATCH v1] config: fix case-insensitive match for old-style [section.subsection]","startedAt":"2026-06-30T12:49:15Z","lastAt":"2026-06-30T20:12:50Z","messageCount":2,"participants":["RISHAV DEWAN","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"546777","messageId":"20260630124850.2498-1-rishavdewan10@gmail.com","threadId":"65895","inReplyTo":null,"subject":"[PATCH v1] config: fix case-insensitive match for old-style [section.subsection]","fromName":"RISHAV DEWAN","fromEmail":"rishavdewan10@gmail.com","sentAt":"2026-06-30T12:48:50Z","receivedAt":"2026-06-30T12:49:15Z","isPatch":true,"body":"When writing to an old-style \"[section.subsection]\" header, the\nconfig file parser always folds the subsection to lower case\n(get_base_var() lowercases unconditionally), while the key given on\nthe command line keeps whatever case the caller typed\n(do_parse_config_key() deliberately leaves the subsection segment\nuntouched). matches() compared these two forms with a plain strcmp(),\nso a caller passing an upper-cased subsection (e.g. 'git config\nsection.Subsection.key value2' against a file containing\n'[section.subsection]') never matched the existing key and a\nduplicate line was appended instead of replacing the value. This was\na known, documented limitation (see the now-removed BUGS section of\ngit-config.adoc).\n\nTrack whether the currently active section was parsed case-\ninsensitively (old-style) or case-sensitively (new-style, quoted) in\nstruct config_store_data, and have matches() use that information to\ncompare the subsection segment of the key accordingly, instead of an\nunconditional case-sensitive strcmp().\n\nSigned-off-by: RISHAV DEWAN <rishavdewan10@gmail.com>\n---\n Documentation/git-config.adoc | 21 ---------------------\n config.c                      | 18 +++++++++++++++++-\n t/t1300-config.sh             |  2 --\n 3 files changed, 17 insertions(+), 24 deletions(-)\n\ndiff --git a/Documentation/git-config.adoc b/Documentation/git-config.adoc\nindex 57af010ade..11dfd802b4 100644\n--- a/Documentation/git-config.adoc\n+++ b/Documentation/git-config.adoc\n@@ -634,27 +634,6 @@ http.sslverify false\n \n include::config.adoc[]\n \n-BUGS\n-----\n-When using the deprecated `[section.subsection]` syntax, changing a value\n-will result in adding a multi-line key instead of a change, if the subsection\n-is given with at least one uppercase character. For example when the config\n-looks like\n-\n---------\n-  [section.subsection]\n-    key = value1\n---------\n-\n-and running `git config section.Subsection.key value2` will result in\n-\n---------\n-  [section.subsection]\n-    key = value1\n-    key = value2\n---------\n-\n-\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/config.c b/config.c\nindex 6a0de86e3a..7f086fcd75 100644\n--- a/config.c\n+++ b/config.c\n@@ -2594,6 +2594,7 @@ struct config_store_data {\n \t} *parsed;\n \tunsigned int parsed_nr, parsed_alloc, *seen, seen_nr, seen_alloc;\n \tunsigned int key_seen:1, section_seen:1, is_keys_section:1;\n+\tunsigned int subsection_case_sensitive:1;\n };\n #define CONFIG_STORE_INIT { 0 }\n \n@@ -2613,7 +2614,21 @@ static void config_store_data_clear(struct config_store_data *store)\n static int matches(const char *key, const char *value,\n \t\t   const struct config_store_data *store)\n {\n-\tif (strcmp(key, store->key))\n+\t/*\n+\t * The subsection part of \"key\" (key[0..store->baselen)) was parsed\n+\t * out of the config file using the case sensitivity of whichever\n+\t * section header it came from (see store_aux_event()): old-style\n+\t * \"[section.subsection]\" headers are folded to lower case while\n+\t * parsing, so they must be compared case-insensitively against\n+\t * store->key, which preserves whatever case the caller passed on\n+\t * the command line. New-style \"[section \"Subsection\"]\" headers keep\n+\t * their case, so they need an exact, case-sensitive comparison.\n+\t */\n+\tint (*cmpfn)(const char *, const char *, size_t) =\n+\t\tstore->subsection_case_sensitive ? strncasecmp : strncmp;\n+\n+\tif (cmpfn(key, store->key, store->baselen) ||\n+\t    strcmp(key + store->baselen, store->key + store->baselen))\n \t\treturn 0; /* not ours */\n \tif (store->fixed_value && value)\n \t\treturn !strcmp(store->fixed_value, value);\n@@ -2654,6 +2669,7 @@ static int store_aux_event(enum config_event_t type, size_t begin, size_t end,\n \t\t\t!cmpfn(cs->var.buf, store->key, store->baselen);\n \t\tif (store->is_keys_section) {\n \t\t\tstore->section_seen = 1;\n+\t\t\tstore->subsection_case_sensitive = cs->subsection_case_sensitive;\n \t\t\tALLOC_GROW(store->seen, store->seen_nr + 1,\n \t\t\t\t   store->seen_alloc);\n \t\t\tstore->seen[store->seen_nr] = store->parsed_nr;\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 87ca11a127..eaa3b83990 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -1499,7 +1499,6 @@ test_expect_success 'old-fashioned settings are case insensitive' '\n \tEOF\n \tq_to_tab >testConfig_expect <<-EOF &&\n \t[V.A]\n-\tr = value1\n \tQr = value2\n \tEOF\n \tgit config -f testConfig_actual \"V.A.r\" value2 &&\n@@ -1511,7 +1510,6 @@ test_expect_success 'old-fashioned settings are case insensitive' '\n \tEOF\n \tq_to_tab >testConfig_expect <<-EOF &&\n \t[V.A]\n-\tr = value1\n \tQr = value2\n \tEOF\n \tgit config -f testConfig_actual \"v.A.r\" value2 &&\n-- \n2.50.1 (Apple Git-155)\n\n"},{"id":"546809","messageId":"xmqqldbvrdmn.fsf@gitster.g","threadId":"65895","inReplyTo":"20260630124850.2498-1-rishavdewan10@gmail.com","subject":"Re: [PATCH v1] config: fix case-insensitive match for old-style [section.subsection]","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-30T20:12:48Z","receivedAt":"2026-06-30T20:12:50Z","isPatch":true,"body":"RISHAV DEWAN <rishavdewan10@gmail.com> writes:\n\nThanks.\n\nAlthough this was originally CC'ed to me, according to \"git blame\",\nmost of the code involved in the \"bug\" (i.e., matches() and its\ncaller) seems to be work by Dscho around 2018, so I'll redirect this\nreview in that direction ;-0.\n\n> When writing to an old-style \"[section.subsection]\" header, the\n> config file parser always folds the subsection to lower case\n> (get_base_var() lowercases unconditionally), while the key given on\n> the command line keeps whatever case the caller typed\n> (do_parse_config_key() deliberately leaves the subsection segment\n> untouched). matches() compared these two forms with a plain strcmp(),\n> so a caller passing an upper-cased subsection (e.g. 'git config\n> section.Subsection.key value2' against a file containing\n> '[section.subsection]') never matched the existing key and a\n> duplicate line was appended instead of replacing the value. This was\n> a known, documented limitation (see the now-removed BUGS section of\n> git-config.adoc).\n>\n> Track whether the currently active section was parsed case-\n> insensitively (old-style) or case-sensitively (new-style, quoted) in\n> struct config_store_data, and have matches() use that information to\n> compare the subsection segment of the key accordingly, instead of an\n> unconditional case-sensitive strcmp().\n>\n> Signed-off-by: RISHAV DEWAN <rishavdewan10@gmail.com>\n> ---\n>  Documentation/git-config.adoc | 21 ---------------------\n>  config.c                      | 18 +++++++++++++++++-\n>  t/t1300-config.sh             |  2 --\n>  3 files changed, 17 insertions(+), 24 deletions(-)\n>\n> diff --git a/Documentation/git-config.adoc b/Documentation/git-config.adoc\n> index 57af010ade..11dfd802b4 100644\n> --- a/Documentation/git-config.adoc\n> +++ b/Documentation/git-config.adoc\n> @@ -634,27 +634,6 @@ http.sslverify false\n>  \n>  include::config.adoc[]\n>  \n> -BUGS\n> -----\n> -When using the deprecated `[section.subsection]` syntax, changing a value\n> -will result in adding a multi-line key instead of a change, if the subsection\n> -is given with at least one uppercase character. For example when the config\n> -looks like\n> -\n> ---------\n> -  [section.subsection]\n> -    key = value1\n> ---------\n> -\n> -and running `git config section.Subsection.key value2` will result in\n> -\n> ---------\n> -  [section.subsection]\n> -    key = value1\n> -    key = value2\n> ---------\n> -\n> -\n>  GIT\n>  ---\n>  Part of the linkgit:git[1] suite\n> diff --git a/config.c b/config.c\n> index 6a0de86e3a..7f086fcd75 100644\n> --- a/config.c\n> +++ b/config.c\n> @@ -2594,6 +2594,7 @@ struct config_store_data {\n>  \t} *parsed;\n>  \tunsigned int parsed_nr, parsed_alloc, *seen, seen_nr, seen_alloc;\n>  \tunsigned int key_seen:1, section_seen:1, is_keys_section:1;\n> +\tunsigned int subsection_case_sensitive:1;\n>  };\n>  #define CONFIG_STORE_INIT { 0 }\n>  \n> @@ -2613,7 +2614,21 @@ static void config_store_data_clear(struct config_store_data *store)\n>  static int matches(const char *key, const char *value,\n>  \t\t   const struct config_store_data *store)\n>  {\n> -\tif (strcmp(key, store->key))\n> +\t/*\n> +\t * The subsection part of \"key\" (key[0..store->baselen)) was parsed\n> +\t * out of the config file using the case sensitivity of whichever\n> +\t * section header it came from (see store_aux_event()): old-style\n> +\t * \"[section.subsection]\" headers are folded to lower case while\n> +\t * parsing, so they must be compared case-insensitively against\n> +\t * store->key, which preserves whatever case the caller passed on\n> +\t * the command line. New-style \"[section \"Subsection\"]\" headers keep\n> +\t * their case, so they need an exact, case-sensitive comparison.\n> +\t */\n> +\tint (*cmpfn)(const char *, const char *, size_t) =\n> +\t\tstore->subsection_case_sensitive ? strncasecmp : strncmp;\n> +\n> +\tif (cmpfn(key, store->key, store->baselen) ||\n> +\t    strcmp(key + store->baselen, store->key + store->baselen))\n>  \t\treturn 0; /* not ours */\n>  \tif (store->fixed_value && value)\n>  \t\treturn !strcmp(store->fixed_value, value);\n> @@ -2654,6 +2669,7 @@ static int store_aux_event(enum config_event_t type, size_t begin, size_t end,\n>  \t\t\t!cmpfn(cs->var.buf, store->key, store->baselen);\n>  \t\tif (store->is_keys_section) {\n>  \t\t\tstore->section_seen = 1;\n> +\t\t\tstore->subsection_case_sensitive = cs->subsection_case_sensitive;\n>  \t\t\tALLOC_GROW(store->seen, store->seen_nr + 1,\n>  \t\t\t\t   store->seen_alloc);\n>  \t\t\tstore->seen[store->seen_nr] = store->parsed_nr;\n> diff --git a/t/t1300-config.sh b/t/t1300-config.sh\n> index 87ca11a127..eaa3b83990 100755\n> --- a/t/t1300-config.sh\n> +++ b/t/t1300-config.sh\n> @@ -1499,7 +1499,6 @@ test_expect_success 'old-fashioned settings are case insensitive' '\n>  \tEOF\n>  \tq_to_tab >testConfig_expect <<-EOF &&\n>  \t[V.A]\n> -\tr = value1\n>  \tQr = value2\n>  \tEOF\n>  \tgit config -f testConfig_actual \"V.A.r\" value2 &&\n> @@ -1511,7 +1510,6 @@ test_expect_success 'old-fashioned settings are case insensitive' '\n>  \tEOF\n>  \tq_to_tab >testConfig_expect <<-EOF &&\n>  \t[V.A]\n> -\tr = value1\n>  \tQr = value2\n>  \tEOF\n>  \tgit config -f testConfig_actual \"v.A.r\" value2 &&\n"}]}