{"thread":{"id":"64949","subject":"[RFC] Support UTF-8 characters in Git alias names","startedAt":"2026-02-08T15:36:46Z","lastAt":"2026-03-03T15:20:19Z","messageCount":88,"participants":["Jonatan Holmgren","D. Ben Knoble","brian m. carlson","Jeff King","Theodore Tso","Junio C Hamano","Ben Knoble","Torsten Bögershausen","Richard Kerry","Kristoffer Haugsbakk","Patrick Steinhardt","Jacob Keller"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"535480","messageId":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","threadId":"64949","inReplyTo":null,"subject":"[RFC] Support UTF-8 characters in Git alias names","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-08T15:30:02Z","receivedAt":"2026-02-08T15:36:46Z","isPatch":false,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Hi Git developers,\n\nI'd like to propose adding support for UTF-8 characters in Git alias \nnames to better support non-English speaking users as these are \ncurrently restricted to [a-zA-Z0-9-] which prevents users from creating \naliases in languages with other characters. Shell aliases do not solve \nthis as the best you could do is \"git-förgrena\".\n\n\nThis would allow users to set aliases such as:\n\n     git config alias.förgrena branch # Swedish\n     git config alias.分支 branch # Chinese\n\netc.\n\nThe restriction comes from iskeychar() in config.c:526-529:\n\n     static inline int iskeychar(int c)\n     {\n\treturn isalnum(c) || c == '-';\n     }\n\nThe function in question validates all Git config keys, not just \naliases. The git_config_parse_key() function (same file, 543-598) \nenforces this validation and uses tolower(), which only works for ASCII.\n\nI think the best approach is to support UTF-8 specifically for alias.* \nvariables, which would mean modifying the git_config_parse_key() fn to \nallow UTF-8 bytes and make non-ascii aliases case-sensitive to avoid \ncomplex locale-dependent case folding.\n\nThe main pain point would be making sure all platforms handle this \nnicely, esp since mac uses NFD and not NFC Unicode.\n\nBefore implementing this, I'd like to hear:\n\n1. Is this a feature the project would like?\n2. Is my implementation approach reasonable?\n3. What concerns should be addressed in said design?\n4. Any compat requirements I should be aware of?\n\nThank you so much for your consideration!\n\nJonatan Holmgren\n"},{"id":"535481","messageId":"CALnO6CCDazixno6Of8ufEV9=EYcGFdawmv5X2J9TNerW6CZMJw@mail.gmail.com","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-02-08T16:07:03Z","receivedAt":"2026-02-08T16:07:15Z","isPatch":false,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Sun, Feb 8, 2026 at 10:37 AM Jonatan Holmgren <jonatan@jontes.page> wrote:\n>\n> Hi Git developers,\n>\n> I'd like to propose adding support for UTF-8 characters in Git alias\n> names to better support non-English speaking users as these are\n> currently restricted to [a-zA-Z0-9-] which prevents users from creating\n> aliases in languages with other characters. Shell aliases do not solve\n> this as the best you could do is \"git-förgrena\".\n>\n>\n> This would allow users to set aliases such as:\n>\n>      git config alias.förgrena branch # Swedish\n>      git config alias.分支 branch # Chinese\n>\n> etc.\n>\n> The restriction comes from iskeychar() in config.c:526-529:\n>\n>      static inline int iskeychar(int c)\n>      {\n>         return isalnum(c) || c == '-';\n>      }\n>\n> The function in question validates all Git config keys, not just\n> aliases. The git_config_parse_key() function (same file, 543-598)\n> enforces this validation and uses tolower(), which only works for ASCII.\n\nIn particular, this comes from sane-ctype.h and ctype.c, which I could\nnot easily rip out of the build :/\n\nIt looks like Git's ctype definitions are ASCII-specific even though\nisalnum(3) on my system is documented as using the locale, so I was\nhoping that if I could build Git without it's own ctype.c I could test\nyour aliases. Bummer.\n\nIt looks like this goes back to 4546738b58 (Unlocalized isspace and\nfriends, 2005-10-13). I don't have an amlog note for it, but searching\nall of lore turned up\n<https://lore.kernel.org/all/Pine.LNX.4.64.0510130838240.15297@g5.osdl.org/>.\nIn the same thread, [1] suggests we'd have to use more complex logic\nto parse UTF-8 config, right?\n\n[1]: https://lore.kernel.org/all/434E8650.7060604@zytor.com/\n\nAnyway, my opinion (not that it holds much weight) is that user-facing\nparts of Git ought to support international languages. (If the `diff`\nline for git-apply(1) has to be ASCII, that's probably fine [2]; it\nshould definitely be parsed as a byte-stream.)\n\n[2]: https://lore.kernel.org/all/7vachd6hdx.fsf@assigned-by-dhcp.cox.net/\n\n-- \nD. Ben Knoble\n"},{"id":"535491","messageId":"aYkaepCu4lwT3xNl@fruit.crustytoothpaste.net","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-02-08T23:21:30Z","receivedAt":"2026-02-08T23:21:32Z","isPatch":false,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2026-02-08 at 15:30:02, Jonatan Holmgren wrote:\n> I think the best approach is to support UTF-8 specifically for alias.*\n> variables, which would mean modifying the git_config_parse_key() fn to allow\n> UTF-8 bytes and make non-ascii aliases case-sensitive to avoid complex\n> locale-dependent case folding.\n\nYes, I don't think anyone should be relying on case folding for aliases.\nNot doing case folding also avoids locale problems with Turkic\nlanguages.\n\nOur mailmap also doesn't do case-folding for non-ASCII characters,\nso there is some precedent, although I wish we were consistent by not\ndoing case folding at all.\n\n> The main pain point would be making sure all platforms handle this nicely,\n> esp since mac uses NFD and not NFC Unicode.\n\nI don't think this affects command-line arguments, though, which are\njust bytes.  Since running `git paramétrer` would just be passing\n`paramétrer` as a command-line argument, then it should pass that value\nas an unnormalized UTF-8 byte string.  It would only be normalized if\nGit invoked `git-paramétrer` as a binary or script.\n\nI don't think we have any Unicode normalization code at all in Git,\nthough, so if you want a quality implementation, that may be a thing we\nneed.\n\n> Before implementing this, I'd like to hear:\n> \n> 1. Is this a feature the project would like?\n\nI think this would be useful.  I don't personally plan to use it, but I\ncan imagine a lot of other people would, and in general I'm in favour of\nbetter i18n and l10n support.\n\n> 2. Is my implementation approach reasonable?\n> 3. What concerns should be addressed in said design?\n\nAs I said, Unicode normalization may be a thing you want to support\nhere.  Not having it isn't a complete dealbreaker, but it would prevent\nhard-to-debug breakage.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"535500","messageId":"20260209073602.GC585828@coredump.intra.peff.net","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-02-09T07:36:02Z","receivedAt":"2026-02-09T07:36:03Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sun, Feb 08, 2026 at 04:30:02PM +0100, Jonatan Holmgren wrote:\n\n> I think the best approach is to support UTF-8 specifically for alias.*\n> variables, which would mean modifying the git_config_parse_key() fn to allow\n> UTF-8 bytes and make non-ascii aliases case-sensitive to avoid complex\n> locale-dependent case folding.\n> \n> The main pain point would be making sure all platforms handle this nicely,\n> esp since mac uses NFD and not NFC Unicode.\n> \n> Before implementing this, I'd like to hear:\n> \n> 1. Is this a feature the project would like?\n> 2. Is my implementation approach reasonable?\n> 3. What concerns should be addressed in said design?\n> 4. Any compat requirements I should be aware of?\n\nI think supporting non-ascii aliases is a good goal.\n\nHowever, I'm not sure that special-casing the parsing of alias config\nkeys is the best direction. Since it's a syntactic change, the special\ncase would have to be understand by all code that reads or writes\nconfig, not just git_config_parse_key(). And then you'd potentially run\ninto problems with older versions of Git, or alternate implementations\n(of which there are several).\n\nPlus it doesn't solve all of the issues. E.g., should we allow new\ncharacters like \"_\" (for a potential \"git foo_bar\")? That is doable, but\nwhat about \".\" (for \"git foo.bar\")? I think that introduces new\nambiguities into the syntax.\n\nTaking a step back, I think the root of the issue is that the schema for\nalias keys is poorly designed. Git's config syntax allows for three\nlevels: section, subsection, and key. The section and key fields are\nrestricted to alnum and dash, but the subsection is designed to be\nunrestricted (modulo NUL bytes).\n\nAnd that's why we have:\n\n  [branch \"foo/bar\"]\n  remote = origin\n\nfor example, because branch names don't follow the same syntax rules as\nconfig keys. And it's the same issue here: the alias.* schema is trying\nto use one syntax (alnum config keys) to store another (command names).\nThey _usually_ overlap, but not always. The pager.* config has the same\nproblem.\n\nWe've discussed this before, e.g., in:\n\n  https://lore.kernel.org/git/20150206124528.GA18859@inner.h.apk.li/\n\nThere the immediate problem was that \"git foo_bar\" caused an error\nmessage. We hacked around it by suppressing the error, but it was still\nimpossible to add an alias or pager config. We knew that was a\nlimitation, but punted until somebody came along who actually cared\nabout making it work. Now you get to be that somebody. ;)\n\nSo what I'd propose instead is introducing a new schema like:\n\n  - setting \"alias.foo.command\" to \"bar\" would alias \"git foo\" to \"bar\";\n    this should work for any command name, as it is just a byte stream\n\n  - a given command subsection is matched verbatim. So alias.foo.command\n    matches \"git foo\" but not \"git Foo\". Likewise, we do not do any\n    normalization. You put what you want into your config, and it should\n    match the command you invoke. This is perhaps less friendly, but it\n    punts on any normalization or case-folding that we have to do, and\n    matches how the rest of Git works (paths are likewise streams of\n    bytes, and it is mostly up to the user to use them consistently).\n\n  - leave \"alias.foo\" as a historical synonym for \"alias.foo.command\",\n    so that existing config continues working\n\n  - optionally add new keys within alias.foo.* sections. For example, we\n    could allow alias.foo.help to provide text shown during \"git help\n    foo\". For the most part that could come later, so I'm just\n    illustrating possible eventual directions that the new schema would\n    allow. But it might be worth pondering a little now to avoid\n    painting ourselves into a corner. E.g., you could imagine a schema\n    where alias.foo.shell is set to \"true\" instead of sticking a \"!\" at\n    the front of the value of alias.foo.command. I don't know if that's\n    a good idea or not, but if we were going to do stuff like that, we'd\n    want to decide now before setting the alias.foo.command behavior in\n    stone.\n\n  - likewise, optionally do the same for pager.*\n\nI hacked together some illustrative code below. Note that we do use\nstrcasecmp() currently to match command names (which kind of makes\nsense, since if you had \"alias.Foo\" in your config, the parser would\ndowncase it to \"alias.foo\"). So probably that historical code should\ncontinue to behave like that, but the new \"alias.Foo.command\" should be\nmore verbatim (the patch below just feeds them both to strcasecmp).\n\n-Peff\n\n---\ndiff --git a/alias.c b/alias.c\nindex 1a1a141a0a..44bdde58af 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -17,19 +17,30 @@ static int config_alias_cb(const char *key, const char *value,\n \t\t\t   const struct config_context *ctx UNUSED, void *d)\n {\n \tstruct config_alias_data *data = d;\n-\tconst char *p;\n+\tconst char *cmd, *p;\n+\tsize_t cmd_len;\n \n-\tif (!skip_prefix(key, \"alias.\", &p))\n+\tif (parse_config_key(key, \"alias\", &cmd, &cmd_len, &p) < 0)\n \t\treturn 0;\n \n+\tif (cmd) {\n+\t\t/* The only 3-level key we understand is alias.*.command */\n+\t\tif (strcmp(p, \"command\"))\n+\t\t\treturn 0;\n+\t} else {\n+\t\t/* alias.foo is the same as alias.foo.command */\n+\t\tcmd = p;\n+\t\tcmd_len = strlen(p);\n+\t}\n+\n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias)) {\n+\t\tif (!strncasecmp(cmd, data->alias, cmd_len)) {\n \t\t\tFREE_AND_NULL(data->v);\n \t\t\treturn git_config_string(&data->v,\n \t\t\t\t\t\t key, value);\n \t\t}\n \t} else if (data->list) {\n-\t\tstring_list_append(data->list, p);\n+\t\tstring_list_append_nodup(data->list, xmemdupz(cmd, cmd_len));\n \t}\n \n \treturn 0;\n"},{"id":"535532","messageId":"20260209135917.GD27241@macsyma.lan","threadId":"64949","inReplyTo":"20260209073602.GC585828@coredump.intra.peff.net","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"Theodore Tso","fromEmail":"tytso@mit.edu","sentAt":"2026-02-09T13:59:17Z","receivedAt":"2026-02-09T14:00:34Z","isPatch":false,"sender":{"key":"tytso@mit.edu","avatar":"https://avatars.githubusercontent.com/u/51416?v=4"},"body":"I would just note that Unicode normalization can be *tricky*.  The\nfact that ext4 case folding would do Unicode normaliation was\nsufficient to trigger a CVE that resulted a security vulnerability in\ngit. Of course, who would be insane enough to use git on a case\nfolded file system (aside from someone running git instead Termux on\nan Android device, that is....)?\n\nSo I'd suggest that git folks think very carefully before trying to\ndive into the insanity which is Unicode.  It might be a very strong\nnice-to-have for certain user communities, but it's also a mess.\n\n\t     \t \t      \t\t   - Ted\n"},{"id":"535543","messageId":"xmqqikc66k5k.fsf@gitster.g","threadId":"64949","inReplyTo":"aYkaepCu4lwT3xNl@fruit.crustytoothpaste.net","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-09T14:55:51Z","receivedAt":"2026-02-09T14:55:54Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> I don't think we have any Unicode normalization code at all in Git,\n> though, so if you want a quality implementation, that may be a thing we\n> need.\n\nIsn't NKC/NKD a macOS-only issue in practice?  Anything on the\ncommand line \"git\" potty and \"git-blah\" built-in commands receive\ngoes through precompose_argv_prefix() to be normalized on that\nplatform.\n\n>> Before implementing this, I'd like to hear:\n>> \n>> 1. Is this a feature the project would like?\n>\n> I think this would be useful.  I don't personally plan to use it, but I\n> can imagine a lot of other people would, and in general I'm in favour of\n> better i18n and l10n support.\n\nI am not fundamentally against this, as long as such an addition\ndoes not introduce unnecessary bugs and ambiguities.  IOW, do not\nforce me to read bug reports in this area after it is done.\n\n>> 2. Is my implementation approach reasonable?\n>> 3. What concerns should be addressed in said design?\n>\n> As I said, Unicode normalization may be a thing you want to support\n> here.  Not having it isn't a complete dealbreaker, but it would prevent\n> hard-to-debug breakage.\n\nA buggy normalization implementation would be also a source of\nunnecessary bugs.  We cannot have and eat that cake so easily ;-).\n"},{"id":"535549","messageId":"c3445fa2-a217-4e48-b0d0-ad41a563c6c4@jontes.page","threadId":"64949","inReplyTo":"xmqqikc66k5k.fsf@gitster.g","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-09T15:19:55Z","receivedAt":"2026-02-09T15:29:43Z","isPatch":false,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Thanks for chiming in!\n\n > Isn't NKC/NKD a macOS-only issue in practice?  Anything on the\n > command line \"git\" potty and \"git-blah\" built-in commands receive\n > goes through precompose_argv_prefix() to be normalized on that\n > platform.\n\nIf we use Jeff's proposed alias.*.{keyname} approach with literal byte\nmatching macOS should already handle the normalization at the argv level \nbefore Git even sees it, correct? I'm not very familiar with how macOS \nhandles this.\n\n > I am not fundamentally against this, as long as such an addition\n > does not introduce unnecessary bugs and ambiguities.  IOW, do not\n > force me to read bug reports in this area after it is done.\n\nUnderstood. Jeff's subsection approach seems safest, it uses existing\nconfig infrastructure that already supports arbitrary bytes in\nsubsections. This would enable\n\n     [alias \"förgrena\"]\n         command = branch\n\nas an alternative to (not a replacement for) the current ASCII-only:\n\n     [alias]\n         forgrena = branch\n\nIs this sound?\n\nJonatan\n\nOn 2026-02-09 15:55, Junio C Hamano wrote:\n> \"brian m. carlson\"<sandals@crustytoothpaste.net> writes:\n> \n>> I don't think we have any Unicode normalization code at all in Git,\n>> though, so if you want a quality implementation, that may be a thing we\n>> need.\n> Isn't NKC/NKD a macOS-only issue in practice?  Anything on the\n> command line \"git\" potty and \"git-blah\" built-in commands receive\n> goes through precompose_argv_prefix() to be normalized on that\n> platform.\n> \n>>> Before implementing this, I'd like to hear:\n>>>\n>>> 1. Is this a feature the project would like?\n>> I think this would be useful.  I don't personally plan to use it, but I\n>> can imagine a lot of other people would, and in general I'm in favour of\n>> better i18n and l10n support.\n> I am not fundamentally against this, as long as such an addition\n> does not introduce unnecessary bugs and ambiguities.  IOW, do not\n> force me to read bug reports in this area after it is done.\n> \n>>> 2. Is my implementation approach reasonable?\n>>> 3. What concerns should be addressed in said design?\n>> As I said, Unicode normalization may be a thing you want to support\n>> here.  Not having it isn't a complete dealbreaker, but it would prevent\n>> hard-to-debug breakage.\n> A buggy normalization implementation would be also a source of\n> unnecessary bugs.  We cannot have and eat that cake so easily 😉.\n\n"},{"id":"535581","messageId":"xmqqy0l14x30.fsf@gitster.g","threadId":"64949","inReplyTo":"c3445fa2-a217-4e48-b0d0-ad41a563c6c4@jontes.page","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-09T17:59:31Z","receivedAt":"2026-02-09T17:59:33Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> Thanks for chiming in!\n>\n>  > Isn't NKC/NKD a macOS-only issue in practice?  Anything on the\n>  > command line \"git\" potty and \"git-blah\" built-in commands receive\n>  > goes through precompose_argv_prefix() to be normalized on that\n>  > platform.\n>\n> If we use Jeff's proposed alias.*.{keyname} approach with literal byte\n> matching macOS should already handle the normalization at the argv level \n> before Git even sees it, correct? I'm not very familiar with how macOS \n> handles this.\n\nI may have phrased it poorly, but the above (with Peff's syntax\nchange, which I think is independently a good thing to do) is\nexactly what I meant.  That way, we do not have to worry about\nnormalization ourselves at all in this area.\n\nThanks.\n"},{"id":"535609","messageId":"20260209220115.461109-1-jonatan@jontes.page","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"[PATCH v1] alias: support UTF-8 characters via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-09T22:01:15Z","receivedAt":"2026-02-09T22:02:08Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Git aliases are currently restricted to ASCII characters due to\nconfig key syntax limitations. This prevents non-English speakers\nfrom creating aliases in their native languages.\n\nAdd support for UTF-8 alias names using config subsections:\n\n    [alias \"förgrena\"]\n        command = branch\n\nThe subsection name is matched verbatim (case-sensitive), while the\nexisting flat syntax (alias.name) remains case-insensitive for\nbackward compatibility. This approach uses existing config\ninfrastructure and avoids complex Unicode normalization.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n Documentation/RelNotes/2.54.0.adoc |  8 +++++\n Documentation/config/alias.adoc    | 53 +++++++++++++++++++++++++-----\n alias.c                            | 38 +++++++++++++++++----\n t/t0014-alias-utf8.sh              | 44 +++++++++++++++++++++++++\n t/t0014-alias.sh                   | 32 ++++++++++++++++++\n 5 files changed, 161 insertions(+), 14 deletions(-)\n create mode 100755 t/t0014-alias-utf8.sh\n\ndiff --git a/Documentation/RelNotes/2.54.0.adoc b/Documentation/RelNotes/2.54.0.adoc\nindex 20c660d82a..8cb00a21bb 100644\n--- a/Documentation/RelNotes/2.54.0.adoc\n+++ b/Documentation/RelNotes/2.54.0.adoc\n@@ -7,6 +7,14 @@ UI, Workflows & Features\n  * \"git add -p\" and friends note what the current status of the hunk\n    being shown is.\n \n+ * Git aliases now support UTF-8 characters in alias names through\n+   subsection syntax: `[alias \"name\"] command = value`. This enables\n+   aliases in non-English languages. The flat syntax continues\n+   to work for backward compatibility.\n+\n+ * The new subsection syntax uses case-sensitive matching and\n+   the flat syntax remains case-insensitive for backward compatibility.\n+\n \n Performance, Internal Implementation, Development Support etc.\n --------------------------------------------------------------\ndiff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\nindex 80ce17d2de..feba1e2022 100644\n--- a/Documentation/config/alias.adoc\n+++ b/Documentation/config/alias.adoc\n@@ -1,12 +1,49 @@\n alias.*::\n-\tCommand aliases for the linkgit:git[1] command wrapper - e.g.\n-\tafter defining `alias.last = cat-file commit HEAD`, the invocation\n-\t`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n-\tconfusion and troubles with script usage, aliases that\n-\thide existing Git commands are ignored except for deprecated\n-\tcommands.  Arguments are split by\n-\tspaces, the usual shell quoting and escaping are supported.\n-\tA quote pair or a backslash can be used to quote them.\n+alias.*.command::\n+\tCommand aliases for the linkgit:git[1] command wrapper. Aliases\n+\tcan be defined using two syntaxes:\n++\n+--\n+1. **Simple syntax** (case-insensitive): `alias.name = value`\n+2. **Subsection syntax** (recommended for UTF-8/special characters):\n+   `[alias \"name\"]` with `command = value`\n+\n+The subsection syntax allows alias names containing UTF-8 characters,\n+spaces, or special characters, as the subsection preserves the exact\n+bytes including case.\n+--\n++\n+Examples:\n++\n+----\n+# Simple syntax (ASCII names)\n+[alias]\n+    co = checkout\n+    st = status\n+\n+# Subsection syntax (UTF-8 and special characters)\n+[alias \"hämta\"]\n+    command = fetch\n+[alias \"gömma\"]\n+    command = stash\n+[alias \"my alias with whitespace\"]\n+    command = \"status --short\"\n+----\n++\n+After defining these, you can run `git co`, `git hämta`, `git gömma`,\n+and `git \"my alias with whitespace\"` (note the quotes for aliases with spaces).\n++\n+**Note:** The flat syntax `alias.name` remains case-insensitive for\n+backward compatibility. The new subsection syntax is case-sensitive:\n+`[alias \"Foo\"]` and `[alias \"foo\"]` are different aliases.\n++\n+E.g. after defining `alias.last = cat-file commit HEAD`, the invocation\n+`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n+confusion and troubles with script usage, aliases that\n+hide existing Git commands are ignored except for deprecated\n+commands.  Arguments are split by\n+spaces, the usual shell quoting and escaping are supported.\n+A quote pair or a backslash can be used to quote them.\n +\n Note that the first word of an alias does not necessarily have to be a\n command. It can be a command-line option that will be passed into the\ndiff --git a/alias.c b/alias.c\nindex 1a1a141a0a..f0c5f12fdd 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -17,19 +17,45 @@ static int config_alias_cb(const char *key, const char *value,\n \t\t\t   const struct config_context *ctx UNUSED, void *d)\n {\n \tstruct config_alias_data *data = d;\n-\tconst char *p;\n+\tconst char *cmd, *subkey;\n+\tsize_t cmd_len;\n+\tint is_subsection;\n \n-\tif (!skip_prefix(key, \"alias.\", &p))\n+\t/* Use parse_config_key() to handle both 2-level and 3-level keys */\n+\tif (parse_config_key(key, \"alias\", &cmd, &cmd_len, &subkey) < 0)\n \t\treturn 0;\n \n+\t/*\n+\t * Support two syntaxes:\n+\t * 1. alias.name = value (simple, 2-level key)\n+\t * 2. [alias \"name\"] command = value (new, 3-level key)\n+\t */\n+\tif (cmd) {\n+\t\tif (strcmp(subkey, \"command\"))\n+\t\t\treturn 0;\n+\t\tis_subsection = 1;\n+\t} else {\n+\t\tcmd = subkey;\n+\t\tcmd_len = strlen(cmd);\n+\t\tis_subsection = 0;\n+\t}\n+\n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias)) {\n+\t\tint match;\n+\t\tif (is_subsection) {\n+\t\t\tmatch = (strlen(data->alias) == cmd_len &&\n+\t\t\t\t !strncmp(data->alias, cmd, cmd_len));\n+\t\t} else {\n+\t\t\tmatch = (strlen(data->alias) == cmd_len &&\n+\t\t\t\t !strncasecmp(data->alias, cmd, cmd_len));\n+\t\t}\n+\n+\t\tif (match) {\n \t\t\tFREE_AND_NULL(data->v);\n-\t\t\treturn git_config_string(&data->v,\n-\t\t\t\t\t\t key, value);\n+\t\t\treturn git_config_string(&data->v, key, value);\n \t\t}\n \t} else if (data->list) {\n-\t\tstring_list_append(data->list, p);\n+\t\tstring_list_append_nodup(data->list, xmemdupz(cmd, cmd_len));\n \t}\n \n \treturn 0;\ndiff --git a/t/t0014-alias-utf8.sh b/t/t0014-alias-utf8.sh\nnew file mode 100755\nindex 0000000000..d55b5a7213\n--- /dev/null\n+++ b/t/t0014-alias-utf8.sh\n@@ -0,0 +1,44 @@\n+#!/bin/sh\n+\n+test_description='UTF-8 support in git aliases via subsection syntax'\n+\n+. ./test-lib.sh\n+\n+# Skip if filesystem/locale doesn't support UTF-8\n+test_lazy_prereq UTF8_LOCALE '\n+\ttest_have_prereq !MINGW &&\n+\ttest_set_prereq UTF8_LOCALE\n+'\n+\n+test_expect_success 'setup test repository' '\n+\tgit init &&\n+\ttest_commit initial\n+'\n+\n+test_expect_success UTF8_LOCALE 'setup UTF-8 aliases' '\n+\tgit config alias.\"förgrena\".command branch &&\n+\tgit config alias.\"分支\".command \"branch --list\" &&\n+\tgit config alias.\"test name\".command status\n+'\n+\n+test_expect_success UTF8_LOCALE 'UTF-8 alias with Swedish characters' '\n+\tgit förgrena >output &&\n+\ttest_grep -E \"^(\\* )?(main|master)\" output\n+'\n+\n+test_expect_success UTF8_LOCALE 'UTF-8 alias with CJK characters' '\n+\tgit 分支 >output &&\n+\ttest_grep -E \"^(\\* )?(main|master)\" output\n+'\n+\n+test_expect_success UTF8_LOCALE 'alias with spaces in name' '\n+\tgit \"test name\" >output &&\n+\ttest_grep \"On branch\" output\n+'\n+\n+test_expect_success 'list UTF-8 aliases' '\n+\tgit config --get-regexp \"^alias\\\\..*\\\\.command\" >output &&\n+\ttest_line_count -ge 3 output\n+'\n+\n+test_done\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex 07a53e7366..b19a8a5061 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -111,5 +111,37 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '\n \t\tcannot_alias_regular_builtin \"$cmd\" || return 1\n \tdone\n '\n+test_expect_success 'flat syntax still works' '\n+\tgit config alias.testlegacy status &&\n+\tgit testlegacy >output &&\n+\ttest_grep \"On branch\" output\n+'\n+\n+test_expect_success 'new subsection syntax works' '\n+\tgit config alias.testnew.command status &&\n+\tgit testnew >output &&\n+\ttest_grep \"On branch\" output\n+'\n+\n+test_expect_success 'subsection syntax only accepts command key' '\n+\tgit config alias.invalid.notcommand \"value\" &&\n+\ttest_must_fail git invalid 2>error &&\n+\ttest_grep -i \"not a git command\" error\n+'\n+\n+test_expect_success 'simple syntax is case-insensitive' '\n+\tgit config alias.LegacyCase status &&\n+\tgit legacycase >output 2>&1 &&\n+\ttest_grep \"On branch\" output\n+'\n+\n+test_expect_success 'subsection syntax is case-sensitive' '\n+\ttest_commit case-test &&\n+\tgit config alias.SubCase.command \"log --oneline\" &&\n+\tgit config alias.subcase.command status &&\n+\tgit SubCase >upper.out 2>&1 &&\n+\tgit subcase >lower.out 2>&1 &&\n+\t! test_cmp upper.out lower.out\n+'\n \n test_done\n-- \n2.53.0\n\n"},{"id":"535616","messageId":"aYpiSHSxYS2fUUS1@fruit.crustytoothpaste.net","threadId":"64949","inReplyTo":"xmqqikc66k5k.fsf@gitster.g","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-02-09T22:40:08Z","receivedAt":"2026-02-09T22:40:10Z","isPatch":false,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2026-02-09 at 14:55:51, Junio C Hamano wrote:\n> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n> \n> > I don't think we have any Unicode normalization code at all in Git,\n> > though, so if you want a quality implementation, that may be a thing we\n> > need.\n> \n> Isn't NKC/NKD a macOS-only issue in practice?  Anything on the\n> command line \"git\" potty and \"git-blah\" built-in commands receive\n> goes through precompose_argv_prefix() to be normalized on that\n> platform.\n\nNormalization is not a macOS-only issue.  Many accented characters can\nbe written in multiple ways, one composed and one decomposed.  If the\nalias in the file is composed and what's on the command line is\ndecomposed, they will not match bytewise even though they are logically\nand graphically identical.\n\nFor instance, here is the word for \"where\" in French, first composed,\nthen decomposed:\n\noù\noù\n\nThe former is U+006F U+00F9 and the latter is U+006F U+0075 U+0300.\nObviously, if I write one of those in my config file and the other on\nthe command line, I intended to execute the same alias, but they are not\nbytewise identical unless both are normalized identically.\n\nThis is why many websites don't accept Unicode in passwords: because\nlogging in on different systems can produce different sequences and they\nmust be properly normalized to avoid hard-to-reproduce problems.\n\nThere are also canonical (NFC and NFD) and compatibility (NFKC and NFKD)\nnormalizations.  For instance, a Greek question mark looks like an\nEnglish semicolon.  Canonical normalizations preserve this distinction,\nbut compatibility ones do not.\n\nI'll note that the Mac-native normalizations do not match any standard\nUnicode normalizations for any version, so we'd need separate\nnormalization code.  I also don't think UTF-8-MAC is available on all\nversions of libiconv, either.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"535621","messageId":"xmqqecmt33xk.fsf@gitster.g","threadId":"64949","inReplyTo":"aYpiSHSxYS2fUUS1@fruit.crustytoothpaste.net","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-09T23:14:31Z","receivedAt":"2026-02-09T23:14:34Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> On 2026-02-09 at 14:55:51, Junio C Hamano wrote:\n>> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n>> \n>> > I don't think we have any Unicode normalization code at all in Git,\n>> > though, so if you want a quality implementation, that may be a thing we\n>> > need.\n>> \n>> Isn't NKC/NKD a macOS-only issue in practice?  Anything on the\n>> command line \"git\" potty and \"git-blah\" built-in commands receive\n>> goes through precompose_argv_prefix() to be normalized on that\n>> platform.\n>\n> Normalization is not a macOS-only issue.  Many accented characters can\n> be written in multiple ways,...\n\nYup, but that wasn't what I brought up macOS for.  No sane person\nwould write the same string in multiple ways on purpose and\neverybody would want to stick to one, so that byte-for-byte\ncomparison can decide paths they created in the filesystem can be\nmatched with a list of paths they added in .gitignore, for example.\nAnd for that everybody uses normalization form C, no?\n\nBut the macOS makes it harder to stick to a single way when it\ninvolves filesystem entities; the pathname you gave to a new file\nwith your creat/open(2) may be normalized in macOS specific way when\nit comes back from readdir(2).  Ahd for that glitch, we massage the\nstrings we got from the command line and readdir(), which are in the\nnormalization form D, into normalization form C.\n\nI think the suggestion here is to assume that the users are doing\nthe right thing and treat alias names (which eventually end up being\npathname components) as bytes, and everything should be happy, even\non macOS.\n"},{"id":"535631","messageId":"D96BCAD5-5482-4BD5-B22E-82D34EBC1F0A@gmail.com","threadId":"64949","inReplyTo":"xmqqecmt33xk.fsf@gitster.g","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-02-10T00:45:30Z","receivedAt":"2026-02-10T00:45:42Z","isPatch":false,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> \n> Le 9 févr. 2026 à 18:14, Junio C Hamano <gitster@pobox.com> a écrit :\n> \n> ﻿\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n> \n>>> On 2026-02-09 at 14:55:51, Junio C Hamano wrote:\n>>> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n>>> \n>>>> I don't think we have any Unicode normalization code at all in Git,\n>>>> though, so if you want a quality implementation, that may be a thing we\n>>>> need.\n>>> \n>>> Isn't NKC/NKD a macOS-only issue in practice?  Anything on the\n>>> command line \"git\" potty and \"git-blah\" built-in commands receive\n>>> goes through precompose_argv_prefix() to be normalized on that\n>>> platform.\n>> \n>> Normalization is not a macOS-only issue.  Many accented characters can\n>> be written in multiple ways,...\n> \n> Yup, but that wasn't what I brought up macOS for.  No sane person\n> would write the same string in multiple ways on purpose and\n> everybody would want to stick to one, so that byte-for-byte\n> comparison can decide paths they created in the filesystem can be\n> matched with a list of paths they added in .gitignore, for example.\n> And for that everybody uses normalization form C, no?\n> \n> But the macOS makes it harder to stick to a single way when it\n> involves filesystem entities; the pathname you gave to a new file\n> with your creat/open(2) may be normalized in macOS specific way when\n> it comes back from readdir(2).  Ahd for that glitch, we massage the\n> strings we got from the command line and readdir(), which are in the\n> normalization form D, into normalization form C.\n> \n> I think the suggestion here is to assume that the users are doing\n> the right thing and treat alias names (which eventually end up being\n> pathname components) as bytes, and everything should be happy, even\n> on macOS.\n\nIn what way do alias names end up being pathname components? Or did you mean to insert « treated like » (as in, normalized as command arguments)?"},{"id":"535633","messageId":"xmqqzf5h1ka5.fsf@gitster.g","threadId":"64949","inReplyTo":"D96BCAD5-5482-4BD5-B22E-82D34EBC1F0A@gmail.com","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-10T01:04:18Z","receivedAt":"2026-02-10T01:04:21Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ben Knoble <ben.knoble@gmail.com> writes:\n\n> In what way do alias names end up being pathname components? Or\n> did you mean to insert « treated like » (as in, normalized as\n> command arguments)?\n\nAh, they don't.  I somehow was confusing those custom commands you\nwould throw into your ~/bin as \"git-something\" ;-).\n\nBut you're correct to point out that for a command line \"git\nsomething\", \"something\" would go through the same \"undo macOS NFD\nfor all argv[] elements\".\n\n"},{"id":"535654","messageId":"20260210065901.GD1756549@coredump.intra.peff.net","threadId":"64949","inReplyTo":"xmqqzf5h1ka5.fsf@gitster.g","subject":"Re: [RFC] Support UTF-8 characters in Git alias names","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-02-10T06:59:01Z","receivedAt":"2026-02-10T06:59:08Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Feb 09, 2026 at 05:04:18PM -0800, Junio C Hamano wrote:\n\n> Ben Knoble <ben.knoble@gmail.com> writes:\n> \n> > In what way do alias names end up being pathname components? Or\n> > did you mean to insert « treated like » (as in, normalized as\n> > command arguments)?\n> \n> Ah, they don't.  I somehow was confusing those custom commands you\n> would throw into your ~/bin as \"git-something\" ;-).\n> \n> But you're correct to point out that for a command line \"git\n> something\", \"something\" would go through the same \"undo macOS NFD\n> for all argv[] elements\".\n\nIt's not an issue for aliases, but pager.* (and a hypothetical\npager.*.command) does care about the on-disk representation of program\nnames (and that it matches what is fed from the command-line).\n\n-Peff\n"},{"id":"535655","messageId":"20260210074441.GE1756549@coredump.intra.peff.net","threadId":"64949","inReplyTo":"20260209220115.461109-1-jonatan@jontes.page","subject":"Re: [PATCH v1] alias: support UTF-8 characters via subsection syntax","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-02-10T07:44:41Z","receivedAt":"2026-02-10T07:44:43Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Feb 09, 2026 at 11:01:15PM +0100, Jonatan Holmgren wrote:\n\n> Git aliases are currently restricted to ASCII characters due to\n> config key syntax limitations. This prevents non-English speakers\n> from creating aliases in their native languages.\n> \n> Add support for UTF-8 alias names using config subsections:\n> \n>     [alias \"förgrena\"]\n>         command = branch\n> \n> The subsection name is matched verbatim (case-sensitive), while the\n> existing flat syntax (alias.name) remains case-insensitive for\n> backward compatibility. This approach uses existing config\n> infrastructure and avoids complex Unicode normalization.\n\nThanks, this mostly looks quite good. I have a few comments below that\nrange from a few small bugs to project-specific gotchas to\nstyle/readability suggestions.\n\n> diff --git a/Documentation/RelNotes/2.54.0.adoc b/Documentation/RelNotes/2.54.0.adoc\n> [...]\n\nUsually patches do not fill out their own release notes, as it would\ncreate many conflicts when merging topics in arbitrary order. Instead,\nthe maintainer generally writes up a blurb that goes in the merge\ncommits and is eventually placed in the release notes by script.\nSuggesting a blurb does save work for the maintainer, but it should go\nin the cover letter (so for a single patch, after the \"---\" lines).\n\nThere's a bit on this in Documentation/SubmittingPatches, under the\nsection \"the-topic-summary\".\n\n> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\n> index 80ce17d2de..feba1e2022 100644\n> --- a/Documentation/config/alias.adoc\n> +++ b/Documentation/config/alias.adoca\n\nYay, I was happy to see nice comprehensive documentation here. And you\ndo not seem to have gotten caught by any of the (many) formatting\ngotchas.\n\n> +After defining these, you can run `git co`, `git hämta`, `git gömma`,\n> +and `git \"my alias with whitespace\"` (note the quotes for aliases with spaces).\n\nThe use of literal backticks here makes sense, and if you haven't disabled it\nwith a build-time knob, they should be shown in bold. Curiously in my\nbuild of the docs this doesn't quite work, and I get \"git h\" in bold,\nbut \"ämta\" unbolded. Weird, and presumably a bug in asciidoc (the\ngenerated docbook xml shows the same thing, as does html). I get the\nsame with asciidoctor, too. I'm not sure if it is worth working around\nit (or even how we would do so).\n\n> diff --git a/alias.c b/alias.c\n> index 1a1a141a0a..f0c5f12fdd 100644\n> --- a/alias.c\n> +++ b/alias.c\n> @@ -17,19 +17,45 @@ static int config_alias_cb(const char *key, const char *value,\n>  \t\t\t   const struct config_context *ctx UNUSED, void *d)\n>  {\n>  \tstruct config_alias_data *data = d;\n> -\tconst char *p;\n> +\tconst char *cmd, *subkey;\n> +\tsize_t cmd_len;\n> +\tint is_subsection;\n>  \n> -\tif (!skip_prefix(key, \"alias.\", &p))\n> +\t/* Use parse_config_key() to handle both 2-level and 3-level keys */\n> +\tif (parse_config_key(key, \"alias\", &cmd, &cmd_len, &subkey) < 0)\n>  \t\treturn 0;\n>  \n> +\t/*\n> +\t * Support two syntaxes:\n> +\t * 1. alias.name = value (simple, 2-level key)\n> +\t * 2. [alias \"name\"] command = value (new, 3-level key)\n> +\t */\n> +\tif (cmd) {\n> +\t\tif (strcmp(subkey, \"command\"))\n> +\t\t\treturn 0;\n> +\t\tis_subsection = 1;\n> +\t} else {\n> +\t\tcmd = subkey;\n> +\t\tcmd_len = strlen(cmd);\n> +\t\tis_subsection = 0;\n> +\t}\n\nOK, so versus what I showed earlier, we are keeping an is_subsection\nflag in order to differentiate the cases. And then we use that below...\n\n>  \tif (data->alias) {\n> -\t\tif (!strcasecmp(p, data->alias)) {\n> +\t\tint match;\n> +\t\tif (is_subsection) {\n> +\t\t\tmatch = (strlen(data->alias) == cmd_len &&\n> +\t\t\t\t !strncmp(data->alias, cmd, cmd_len));\n> +\t\t} else {\n> +\t\t\tmatch = (strlen(data->alias) == cmd_len &&\n> +\t\t\t\t !strncasecmp(data->alias, cmd, cmd_len));\n> +\t\t}\n\n...to decide whether to be case-insensitive or not. Makes sense.\n\nI was going to suggest some simplifications here: if is_subsection is\nfalse, then we know that cmd is a NUL-terminated string, and we could\njust use strcasecmp() as before. And in fact, with that conditional we\ndo not need the \"cmd\" aliasing at all. Which also means we don't need a\nseparate flag to distinguish the cases.\n\nYou could just write:\n\n  if (cmd) {\n\tmatch = (strlen(data->alias) == cmd_len &&\n\t         !strncmp(data->alias, cmd, cmd_len));\n  } else {\n\tmatch = !strcasecmp(data->alias, subkey);\n  }\n\nBut I guess we do use the redirection in the listing path later anyway:\n\n>  \t} else if (data->list) {\n> -\t\tstring_list_append(data->list, p);\n> +\t\tstring_list_append_nodup(data->list, xmemdupz(cmd, cmd_len));\n>  \t}\n\nSo maybe it is better as-is. I dunno. I have a feeling this would all\nneed even more refactoring even if we ever added any more alias.foo.*\nconfig entries. So we can probably just leave it until then for more\nrearranging.\n\n\nOne thing I did find a little funny is the use of the word \"subkey\" for\none variable. Usually we just call this the \"key\", but that name is\nannoyingly already taken by the function parameter (well before your\npatch). Usually we call that \"var\" instead. I don't know if it's worth\nchanging or not. It's probably only confusing to me because I've stared\nat so many other Git config callback functions.\n\n> diff --git a/t/t0014-alias-utf8.sh b/t/t0014-alias-utf8.sh\n\nIs there any reason to add a new script here, and not just put these in\nthe existing t0014? If you were bailing from the script when the prereq\nwas not met, we'd need a separate script. But since you are using\nper-test prereqs, they can just exist alongside the other alias tests.\n\nAnd also, you can't have two scripts with the same number. ;) Running\n\"make test\" should complain about that (I'll assume you ran your tests,\nbut manually as ./t0014).\n\n> +# Skip if filesystem/locale doesn't support UTF-8\n> +test_lazy_prereq UTF8_LOCALE '\n> +\ttest_have_prereq !MINGW &&\n> +\ttest_set_prereq UTF8_LOCALE\n> +'\n\nThis is a slight mis-use of set_prereq. You should just need to return\nsuccess from the lazy-prereq function, and it will be set automatically.\nSo more like:\n\n  test_lazy_prereq UTF8_LOCALE '\n\ttest_have_prereq !MINGW\n  '\n\nAt which point I wonder if there is much value in having UTF8_LOCALE at\nall, and not just putting \"!MINGW\" in each of the tests.\n\nBeyond that, my larger question is: what are we asking of the platform\nand why does MINGW not work? We do not care about actual utf8 filesystem\nsupport, since these are just aliases. As long as the config file can\nstore them and we can pass them on the command-line, that should be\nenough.\n\n> +test_expect_success UTF8_LOCALE 'setup UTF-8 aliases' '\n> +\tgit config alias.\"förgrena\".command branch &&\n> +\tgit config alias.\"分支\".command \"branch --list\" &&\n> +\tgit config alias.\"test name\".command status\n> +'\n\nUsually we'd just define these in the tests that use them, which keeps\neach individual test a bit more self-contained. So more like:\n\n  test_expect_success 'UTF-8 alias with Swedish characters' '\n\ttest_config alias.\"förgrena\".command branch &&\n\tgit förgrena >output &&\n\t...\n  '\n\nBut we use them in the listing test, too, so they are each used twice. I\ndunno. It might make sense for the listing test to just define its own\nexpected set.\n\n> +test_expect_success UTF8_LOCALE 'UTF-8 alias with Swedish characters' '\n> +\tgit förgrena >output &&\n> +\ttest_grep -E \"^(\\* )?(main|master)\" output\n> +'\n\nSo I see we're aliasing \"branch\" here, which gives us this complicated\nregex for checking the output. Would something like \"!echo ran my alias\"\nmake the test easier to understand?\n\n> +test_expect_success 'list UTF-8 aliases' '\n> +\tgit config --get-regexp \"^alias\\\\..*\\\\.command\" >output &&\n> +\ttest_line_count -ge 3 output\n> +'\n\nThis one needs the UTF8_LOCALE prereq, too (since we'd only have set up\nthose aliases if we had it).\n\nIs doing --get-regexp here all that interesting? It's not testing your\nnew code at all, and would have passed already. I expected us to look at\nthe output of \"git help -a\" to see that the entries are listed.\n\nAnd trying that with your patch:\n\n  ./git -c alias.föo.command=branch help -a\n\nshows some possible issues:\n\n  - we show it as \"föo.command\"\n\n  - the alignment with other aliases is not quite right. Probably it is\n    using a byte-count instead of utf8_strwidth()\n\nI was puzzled that we would not parse it as föo correctly, since your\npatch modified the listing code. It looks like help.c has its own\nseparate parser for this. Yuck. :( I think this also means that \"git\nhelp föo\" will fail.\n\nSo probably we want a preparatory patch to teach help.c to rely on\nlist_aliases() and alias_lookup() from alias.[ch].\n\n> +test_expect_success 'flat syntax still works' '\n> +\tgit config alias.testlegacy status &&\n> +\tgit testlegacy >output &&\n> +\ttest_grep \"On branch\" output\n> +'\n\nThis probably is covered elsewhere already, but OK. :)\n\n> +test_expect_success 'new subsection syntax works' '\n> +\tgit config alias.testnew.command status &&\n> +\tgit testnew >output &&\n> +\ttest_grep \"On branch\" output\n> +'\n\nNice basic test without utf8. Good.\n\n> +test_expect_success 'subsection syntax only accepts command key' '\n> +\tgit config alias.invalid.notcommand \"value\" &&\n> +\ttest_must_fail git invalid 2>error &&\n> +\ttest_grep -i \"not a git command\" error\n> +'\n\nGood thinking.\n\n> +test_expect_success 'simple syntax is case-insensitive' '\n> +\tgit config alias.LegacyCase status &&\n> +\tgit legacycase >output 2>&1 &&\n> +\ttest_grep \"On branch\" output\n> +'\n> +\n> +test_expect_success 'subsection syntax is case-sensitive' '\n> +\ttest_commit case-test &&\n> +\tgit config alias.SubCase.command \"log --oneline\" &&\n> +\tgit config alias.subcase.command status &&\n> +\tgit SubCase >upper.out 2>&1 &&\n> +\tgit subcase >lower.out 2>&1 &&\n> +\t! test_cmp upper.out lower.out\n> +'\n\nAnd these are both very nice to see.\n\n-Peff\n"},{"id":"535657","messageId":"20260210083047.GA23958@tb-raspi4","threadId":"64949","inReplyTo":"20260209220115.461109-1-jonatan@jontes.page","subject":"Re: [PATCH v1] alias: support UTF-8 characters via subsection syntax","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2026-02-10T08:30:47Z","receivedAt":"2026-02-10T08:31:12Z","isPatch":true,"sender":{"key":"tboegi@web.de","avatar":"https://avatars.githubusercontent.com/u/7138363?v=4"},"body":"[]\nThanks for working on this.\nTrying to review it: Is there a chance to split it into\n2 workpackages/commits/ ?\n\nFor me it seems as if the introduction of the new subsection is\none big thing. With documentation, code review, tests.\n\nAnd handling UTF-8 is another big thing, opening questions like\nDo we need to set a locale ?\nDoes mingw handle UTF-8 on the command line, or why is it exlcuded ?\n"},{"id":"535689","messageId":"xmqqy0l0zhdx.fsf@gitster.g","threadId":"64949","inReplyTo":"20260209220115.461109-1-jonatan@jontes.page","subject":"Re: [PATCH v1] alias: support UTF-8 characters via subsection syntax","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-10T16:35:06Z","receivedAt":"2026-02-10T16:35:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> diff --git a/Documentation/RelNotes/2.54.0.adoc b/Documentation/RelNotes/2.54.0.adoc\n> index 20c660d82a..8cb00a21bb 100644\n> --- a/Documentation/RelNotes/2.54.0.adoc\n> +++ b/Documentation/RelNotes/2.54.0.adoc\n> @@ -7,6 +7,14 @@ UI, Workflows & Features\n>   * \"git add -p\" and friends note what the current status of the hunk\n>     being shown is.\n>  \n> + * Git aliases now support UTF-8 characters in alias names through\n> +   subsection syntax: `[alias \"name\"] command = value`. This enables\n> +   aliases in non-English languages. The flat syntax continues\n> +   to work for backward compatibility.\n> +\n> + * The new subsection syntax uses case-sensitive matching and\n> +   the flat syntax remains case-insensitive for backward compatibility.\n\nThanks for summrizing the topic well.  \"The flat syntax\" is a new\nphrase to us, I think.  Do we have a term to call two-level\nconfiguration variables in contrast to threee-level ones defined in\nthe glossary?  I've seen phrases like two- and three- level names to\ndistinguish them in the past, but that is no way \"official\".\n\n\"git config --help\" has Syntax section that calls \"alias\" a\n\"section\", and \"foo\" a \"subsection\", and \"command\" a \"variable name\"\nin \"alias.foo.command\".  Perhaps \"the alias definition without\nsubsection, e.g., \"[alias] co = checkout\", continues to work\", or\nsomething, perhaps?\n\nIn addition to what Peff already mentioned, we would not want the\nsecond bullet point; the fact that the traditional two-level config\nis still supported is very much worth mentioning, which is already\ndone in the previous point.  So all it remains in this bullet point\nis that this topic did not change anything in the three-level\nconfiguration case, which is not noteworty.\n\n\n> +# Simple syntax (ASCII names)\n> +[alias]\n> +    co = checkout\n> +    st = status\n\nIt is somewhat misleading to call this \"ASCII\" as it is stricter\nthan that (e.g., there are '.' and other ASCII characters that you\ncannot have in the name).  \n\n    \"Limited to alphanumeric and '-' letters (the same limitation as\n    configuration variable names), case insensitive\"\n\nor something.\n\n"},{"id":"535698","messageId":"20260210183110.1151072-1-jonatan@jontes.page","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"[PATCH v2 0/2] support UTF-8 in alias names","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-10T18:31:08Z","receivedAt":"2026-02-10T18:33:11Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Hi all,\n\nThanks for the feedback on my first patch! This iteration (to my understanding) \naddresses all the points raised by Jeff, Junio, and Brian.\n\nRelevant since RFC/v1:\n- Split into two commits: a preparatory refactoring and the main feature\n- Removed release notes from commits \n  (suggested blurb below, first time doing this, bear with me :))\n- Fixed documentation to use \"without subsection\" terminology instead of\n  \"flat syntax\"\n- Consolidated help.c to use list_aliases() from alias.c, eliminating\n  duplicate parsing logic\n- Added utf8_strwidth() for proper column alignment in help output\n- Improved test coverage with case-sensitivity tests, subsection validation,\n  and help listing verification\n- Tests now use test_config helper and simpler output verification\n\nThe implementation follows Peff's suggestion to use config subsections\nrather than modifying the config key syntax. This allows arbitrary bytes\nin alias names while maintaining backward compatibility.\n\nSuggested release note blurb:\n\n * Git aliases now support UTF-8 characters and special characters\n   in alias names through subsection syntax: `[alias \"name\"] command = value`.\n   This enables aliases in non-English languages. The traditional syntax\n   (without subsection, e.g., `[alias] co = checkout`) continues to work.\n\n\nJonatan Holmgren (2):\n  help: use list_aliases() for alias listing and lookup\n  alias: support non-alphanumeric names via subsection syntax\n\n Documentation/config/alias.adoc | 44 +++++++++++++++++++++-----\n alias.c                         | 43 ++++++++++++++++++++++----\n help.c                          | 36 ++++++++++-----------\n t/t0014-alias.sh                | 55 +++++++++++++++++++++++++++++++++\n 4 files changed, 144 insertions(+), 34 deletions(-)\n\n-- \n2.53.0\n\n"},{"id":"535700","messageId":"20260210183110.1151072-2-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260210183110.1151072-1-jonatan@jontes.page","subject":"[PATCH v2 1/2] help: use list_aliases() for alias listing and lookup","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-10T18:31:09Z","receivedAt":"2026-02-10T18:40:19Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"help.c has its own get_alias() config callback that duplicates the\nparsing logic in alias.c. Consolidate by teaching list_aliases() to\nalso store the alias values (via the string_list util field), then\nuse it in list_all_cmds_help_aliases() instead of the private\ncallback.\n\nWhile at it, switch git_unknown_cmd_config() from skip_prefix() to\nparse_config_key() for alias parsing, which properly handles the\nconfig key structure and prepares for multi-level alias config keys\nin a subsequent commit.\n\nNo functional change intended.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n alias.c |  4 +++-\n help.c  | 26 ++++++++------------------\n 2 files changed, 11 insertions(+), 19 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 1a1a141a0a..c66a6095bb 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -29,7 +29,9 @@ static int config_alias_cb(const char *key, const char *value,\n \t\t\t\t\t\t key, value);\n \t\t}\n \t} else if (data->list) {\n-\t\tstring_list_append(data->list, p);\n+\t\tif (value)\n+\t\t\tstring_list_append(data->list, p)->util =\n+\t\t\t\txstrdup(value);\n \t}\n \n \treturn 0;\ndiff --git a/help.c b/help.c\nindex fefd811f7a..a450d57987 100644\n--- a/help.c\n+++ b/help.c\n@@ -20,6 +20,7 @@\n #include \"prompt.h\"\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n+#include \"alias.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)\n \tputchar('\\n');\n }\n \n-static int get_alias(const char *var, const char *value,\n-\t\t     const struct config_context *ctx UNUSED, void *data)\n-{\n-\tstruct string_list *list = data;\n-\n-\tif (skip_prefix(var, \"alias.\", &var)) {\n-\t\tif (!value)\n-\t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(list, var)->util = xstrdup(value);\n-\t}\n-\n-\treturn 0;\n-}\n-\n static void list_all_cmds_help_external_commands(void)\n {\n \tstruct string_list others = STRING_LIST_INIT_DUP;\n@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstruct cmdname_help *aliases;\n \tint i;\n \n-\trepo_config(the_repository, get_alias, &alias_list);\n+\tlist_aliases(&alias_list);\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\n@@ -586,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t\t\t\t  void *cb)\n {\n \tstruct help_unknown_cmd_config *cfg = cb;\n-\tconst char *p;\n+\tconst char *subsection, *key;\n+\tsize_t subsection_len;\n \n \tif (!strcmp(var, \"help.autocorrect\")) {\n \t\tint v = parse_autocorrect(value);\n@@ -601,8 +589,10 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t}\n \n \t/* Also use aliases for command lookup */\n-\tif (skip_prefix(var, \"alias.\", &p))\n-\t\tadd_cmdname(&cfg->aliases, p, strlen(p));\n+\tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len, &key)) {\n+\t\tif (!subsection)\n+\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t}\n \n \treturn 0;\n }\n-- \n2.53.0\n\n"},{"id":"535701","messageId":"20260210183110.1151072-3-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260210183110.1151072-1-jonatan@jontes.page","subject":"[PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-10T18:31:10Z","receivedAt":"2026-02-10T18:42:01Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Git alias names are limited to alphanumeric characters and dashes\nbecause config variable names are validated by iskeychar(). This\nprevents non-English speakers from creating aliases in their native\nlanguages.\n\nAdd support for arbitrary alias names by using config subsections:\n\n    [alias \"förgrena\"]\n        command = branch\n\nThe subsection name is matched as-is (case-sensitive byte comparison),\nwhile the existing definition without a subsection (e.g.,\n\"[alias] co = checkout\") remains case-insensitive for backward\ncompatibility. This uses existing config infrastructure since\nsubsections already support arbitrary bytes, and avoids introducing\nUnicode normalization.\n\nAlso teach the help subsystem about the new syntax so that \"git help\n-a\" properly lists subsection aliases and the autocorrect feature can\nsuggest them. Use utf8_strwidth() instead of strlen() for column\nalignment so that non-alphanumeric alias names display correctly.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n Documentation/config/alias.adoc | 44 +++++++++++++++++++++-----\n alias.c                         | 45 ++++++++++++++++++++++-----\n help.c                          | 12 +++++--\n t/t0014-alias.sh                | 55 +++++++++++++++++++++++++++++++++\n 4 files changed, 137 insertions(+), 19 deletions(-)\n\ndiff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\nindex 80ce17d2de..17a548cd64 100644\n--- a/Documentation/config/alias.adoc\n+++ b/Documentation/config/alias.adoc\n@@ -1,12 +1,40 @@\n alias.*::\n-\tCommand aliases for the linkgit:git[1] command wrapper - e.g.\n-\tafter defining `alias.last = cat-file commit HEAD`, the invocation\n-\t`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n-\tconfusion and troubles with script usage, aliases that\n-\thide existing Git commands are ignored except for deprecated\n-\tcommands.  Arguments are split by\n-\tspaces, the usual shell quoting and escaping are supported.\n-\tA quote pair or a backslash can be used to quote them.\n+alias.*.command::\n+\tCommand aliases for the linkgit:git[1] command wrapper. Aliases\n+\tcan be defined using two syntaxes:\n++\n+--\n+1. Without a subsection, e.g., `[alias] co = checkout`. The alias\n+   name is limited to alphanumeric characters and `-` (the same\n+   limitation as configuration variable names), and is matched\n+   case-insensitively.\n+2. With a subsection, e.g., `[alias \"name\"] command = value`. The\n+   alias name can contain any characters including UTF-8, and is\n+   matched case-sensitively as raw bytes.\n+--\n++\n+Examples:\n++\n+----\n+# Without subsection\n+[alias]\n+    co = checkout\n+    st = status\n+\n+# With subsection (allows UTF-8 and special characters)\n+[alias \"hämta\"]\n+    command = fetch\n+[alias \"gömma\"]\n+    command = stash\n+----\n++\n+E.g. after defining `alias.last = cat-file commit HEAD`, the invocation\n+`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n+confusion and troubles with script usage, aliases that\n+hide existing Git commands are ignored except for deprecated\n+commands.  Arguments are split by\n+spaces, the usual shell quoting and escaping are supported.\n+A quote pair or a backslash can be used to quote them.\n +\n Note that the first word of an alias does not necessarily have to be a\n command. It can be a command-line option that will be passed into the\ndiff --git a/alias.c b/alias.c\nindex c66a6095bb..cfd313ce5d 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -17,21 +17,50 @@ static int config_alias_cb(const char *key, const char *value,\n \t\t\t   const struct config_context *ctx UNUSED, void *d)\n {\n \tstruct config_alias_data *data = d;\n-\tconst char *p;\n+\tconst char *subsection, *subkey;\n+\tsize_t subsection_len;\n \n-\tif (!skip_prefix(key, \"alias.\", &p))\n+\tif (parse_config_key(key, \"alias\", &subsection, &subsection_len,\n+\t\t\t     &subkey) < 0)\n \t\treturn 0;\n \n+\t/*\n+\t * Two config syntaxes:\n+\t * - alias.name = value        (without subsection, case-insensitive)\n+\t * - [alias \"name\"]\n+\t *       command = value       (with subsection, case-sensitive)\n+\t */\n+\tif (subsection) {\n+\t\tif (strcmp(subkey, \"command\"))\n+\t\t\treturn 0;\n+\t}\n+\n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias)) {\n+\t\tint match;\n+\n+\t\tif (subsection)\n+\t\t\tmatch = (strlen(data->alias) == subsection_len &&\n+\t\t\t\t !strncmp(data->alias, subsection,\n+\t\t\t\t\t  subsection_len));\n+\t\telse\n+\t\t\tmatch = !strcasecmp(data->alias, subkey);\n+\n+\t\tif (match) {\n \t\t\tFREE_AND_NULL(data->v);\n-\t\t\treturn git_config_string(&data->v,\n-\t\t\t\t\t\t key, value);\n+\t\t\treturn git_config_string(&data->v, key, value);\n \t\t}\n \t} else if (data->list) {\n-\t\tif (value)\n-\t\t\tstring_list_append(data->list, p)->util =\n-\t\t\t\txstrdup(value);\n+\t\tstruct string_list_item *item;\n+\n+\t\tif (!value)\n+\t\t\treturn 0;\n+\n+\t\tif (subsection)\n+\t\t\titem = string_list_append_nodup(data->list,\n+\t\t\t\txmemdupz(subsection, subsection_len));\n+\t\telse\n+\t\t\titem = string_list_append(data->list, subkey);\n+\t\titem->util = xstrdup(value);\n \t}\n \n \treturn 0;\ndiff --git a/help.c b/help.c\nindex a450d57987..16abfe7bf3 100644\n--- a/help.c\n+++ b/help.c\n@@ -21,6 +21,7 @@\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n #include \"alias.h\"\n+#include \"utf8.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,\n \n \tfor (i = 0; cmds[i].name; i++) {\n \t\tif (cmds[i].category & mask) {\n-\t\t\tsize_t len = strlen(cmds[i].name);\n+\t\t\tsize_t len = utf8_strwidth(cmds[i].name);\n \t\t\tprintf(\"   %s   \", cmds[i].name);\n \t\t\tif (longest > len)\n \t\t\t\tmput_char(' ', longest - len);\n@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\n-\t\tsize_t len = strlen(alias_list.items[i].string);\n+\t\tsize_t len = utf8_strwidth(alias_list.items[i].string);\n \t\tif (longest < len)\n \t\t\tlongest = len;\n \t}\n@@ -590,8 +591,13 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len, &key)) {\n-\t\tif (!subsection)\n+\t\tif (subsection) {\n+\t\t\tif (!strcmp(key, \"command\"))\n+\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\t\t    subsection_len);\n+\t\t} else {\n \t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t}\n \t}\n \n \treturn 0;\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex 07a53e7366..66631ad40f 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -112,4 +112,59 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '\n \tdone\n '\n \n+test_expect_success 'subsection syntax works' '\n+\ttest_config alias.testnew.command \"!echo ran-subsection\" &&\n+\tgit testnew >output &&\n+\ttest_grep \"ran-subsection\" output\n+'\n+\n+test_expect_success 'subsection syntax only accepts command key' '\n+\ttest_config alias.invalid.notcommand value &&\n+\ttest_must_fail git invalid 2>error &&\n+\ttest_grep -i \"not a git command\" error\n+'\n+\n+test_expect_success 'simple syntax is case-insensitive' '\n+\ttest_config alias.LegacyCase \"!echo ran-legacy\" &&\n+\tgit legacycase >output &&\n+\ttest_grep \"ran-legacy\" output\n+'\n+\n+test_expect_success 'subsection syntax is case-sensitive' '\n+\ttest_config alias.SubCase.command \"!echo ran-upper\" &&\n+\ttest_config alias.subcase.command \"!echo ran-lower\" &&\n+\tgit SubCase >upper.out &&\n+\tgit subcase >lower.out &&\n+\ttest_grep \"ran-upper\" upper.out &&\n+\ttest_grep \"ran-lower\" lower.out\n+'\n+\n+test_expect_success 'UTF-8 alias with Swedish characters' '\n+\ttest_config alias.\"förgrena\".command \"!echo ran-swedish\" &&\n+\tgit förgrena >output &&\n+\ttest_grep \"ran-swedish\" output\n+'\n+\n+test_expect_success 'UTF-8 alias with CJK characters' '\n+\ttest_config alias.\"分支\".command \"!echo ran-cjk\" &&\n+\tgit 分支 >output &&\n+\ttest_grep \"ran-cjk\" output\n+'\n+\n+test_expect_success 'alias with spaces in name' '\n+\ttest_config alias.\"test name\".command \"!echo ran-spaces\" &&\n+\tgit \"test name\" >output &&\n+\ttest_grep \"ran-spaces\" output\n+'\n+\n+test_expect_success 'subsection aliases listed in help -a' '\n+\ttest_config alias.\"förgrena\".command \"!echo test\" &&\n+\ttest_config alias.\"分支\".command \"!echo test\" &&\n+\ttest_config alias.regular \"!echo test\" &&\n+\tgit help -a >output &&\n+\ttest_grep \"förgrena\" output &&\n+\ttest_grep \"分支\" output &&\n+\ttest_grep \"regular\" output\n+'\n+\n test_done\n-- \n2.53.0\n\n"},{"id":"535706","messageId":"xmqq3438z9ds.fsf@gitster.g","threadId":"64949","inReplyTo":"20260210183110.1151072-2-jonatan@jontes.page","subject":"Re: [PATCH v2 1/2] help: use list_aliases() for alias listing and lookup","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-10T19:27:59Z","receivedAt":"2026-02-10T19:28:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> help.c has its own get_alias() config callback that duplicates the\n> parsing logic in alias.c. Consolidate by teaching list_aliases() to\n> also store the alias values (via the string_list util field), then\n> use it in list_all_cmds_help_aliases() instead of the private\n> callback.\n>\n> While at it, switch git_unknown_cmd_config() from skip_prefix() to\n> parse_config_key() for alias parsing, which properly handles the\n> config key structure and prepares for multi-level alias config keys\n> in a subsequent commit.\n>\n> No functional change intended.\n>\n> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n> ---\n>  alias.c |  4 +++-\n>  help.c  | 26 ++++++++------------------\n>  2 files changed, 11 insertions(+), 19 deletions(-)\n>\n> diff --git a/alias.c b/alias.c\n> index 1a1a141a0a..c66a6095bb 100644\n> --- a/alias.c\n> +++ b/alias.c\n> @@ -29,7 +29,9 @@ static int config_alias_cb(const char *key, const char *value,\n>  \t\t\t\t\t\t key, value);\n>  \t\t}\n>  \t} else if (data->list) {\n> -\t\tstring_list_append(data->list, p);\n> +\t\tif (value)\n> +\t\t\tstring_list_append(data->list, p)->util =\n> +\t\t\t\txstrdup(value);\n>  \t}\n\nIf !value, the original still added p to data->list, but the updated\ncode discards p when value is not there.  Is that an intended change?\n\nIf not,\n\n\t} else if (data->list) {\n\t\tstruct string_list_item *item;\n\n                item = string_list_append(data->list, p);\n\t\tif (value)\n\t\t\titem->util = xstrdup(value);\n\t}\n\nperhaps.\n\n> -static int get_alias(const char *var, const char *value,\n> -\t\t     const struct config_context *ctx UNUSED, void *data)\n> -{\n> -\tstruct string_list *list = data;\n> -\n> -\tif (skip_prefix(var, \"alias.\", &var)) {\n> -\t\tif (!value)\n> -\t\t\treturn config_error_nonbool(var);\n> -\t\tstring_list_append(list, var)->util = xstrdup(value);\n> -\t}\n> -\n> -\treturn 0;\n> -}\n> -\n\nA value-less \n\n\t[alias]\n\t\tfoo\n\nused to get an configuuration error with a friendly message from\nhelp.c:get_alias(), which was removed.  The config_alias_cb() called\nby alias.c:list_aliases() either silently ignores foo altogether\n(the posted patch) or creates an entry for 'foo' but leaves its\nexpansion to NULL (the above \"silent ignore fix\").  Either way,\nthere needs some new code to compensate for the loss of the error\ndetection somehow.\n\n> @@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)\n>  \tstruct cmdname_help *aliases;\n>  \tint i;\n>  \n> -\trepo_config(the_repository, get_alias, &alias_list);\n> +\tlist_aliases(&alias_list);\n>  \tstring_list_sort(&alias_list);\n>  \n>  \tfor (i = 0; i < alias_list.nr; i++) {\n\nOK.\n\n> @@ -586,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n>  \t\t\t\t  void *cb)\n>  {\n>  \tstruct help_unknown_cmd_config *cfg = cb;\n> -\tconst char *p;\n> +\tconst char *subsection, *key;\n> +\tsize_t subsection_len;\n>  \n>  \tif (!strcmp(var, \"help.autocorrect\")) {\n>  \t\tint v = parse_autocorrect(value);\n> @@ -601,8 +589,10 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n>  \t}\n>  \n>  \t/* Also use aliases for command lookup */\n> -\tif (skip_prefix(var, \"alias.\", &p))\n> -\t\tadd_cmdname(&cfg->aliases, p, strlen(p));\n> +\tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len, &key)) {\n> +\t\tif (!subsection)\n> +\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n> +\t}\n>  \n>  \treturn 0;\n>  }\n\nArguably, the last two hunks are about preparing for three-level\nalias.*.command support.\n\nIt is a bit unfortunate that with\n\n    [alias \"foo\"]\n\tcommand = !date\n\tbar = !echo bar\n\nin your configuration, \n\n    $ git foo.command\n    $ git foo.bar\n\nused to invoke the alias 'foo.command' and 'foo.bar' just fine, but\nnow with these two preparatory hunks, it no longer is the case and\nthey are silently ignored.  With the next patch, 'git foo' starts\nworking in place for 'git foo.command', but 'git foo.bar' has become\nforever inaccessible.  I wonder if we want to warn about foo.bar if\nnot foo.command, or if it is too much?  It is conceivable that we\nmay add variables like alias.*.help so it may not be a great idea to\nwarn on anything alias.<subsection>.<key> where <key> is not \"command\"\n\nPerhaps we can claim that we are fixing a bug that allowed aliases\nwith a dot in its name by mistake?  I dunno.  No matter what we\nclaim here, some people will be hit by this behaviour change and\ncomplain about a regression X-<.\n\n\n\n"},{"id":"535707","messageId":"xmqqv7g4xtx0.fsf@gitster.g","threadId":"64949","inReplyTo":"20260210183110.1151072-3-jonatan@jontes.page","subject":"Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-10T19:47:23Z","receivedAt":"2026-02-10T19:47:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> Git alias names are limited to alphanumeric characters and dashes\n> because config variable names are validated by iskeychar(). This\n\n\"ASCII alphanumeric\", perhaps, as accented alphabet characters are\nstill alphanumeric ;-).  \"because aliases are implemented as config\nvariable names\" is probably an explanation that talks to readers in\nterms closer to them (iskeychar() function is an implementation detail).\n\n> prevents non-English speakers from creating aliases in their native\n> languages.\n\nTrue.\n\n> Add support for arbitrary alias names by using config subsections:\n>\n>     [alias \"förgrena\"]\n>         command = branch\n>\n> The subsection name is matched as-is (case-sensitive byte comparison),\n> while the existing definition without a subsection (e.g.,\n> \"[alias] co = checkout\") remains case-insensitive for backward\n> compatibility. This uses existing config infrastructure since\n> subsections already support arbitrary bytes, and avoids introducing\n> Unicode normalization.\n>\n> Also teach the help subsystem about the new syntax so that \"git help\n> -a\" properly lists subsection aliases and the autocorrect feature can\n> suggest them. Use utf8_strwidth() instead of strlen() for column\n> alignment so that non-alphanumeric alias names display correctly.\n\nEither move the last two hunks from [1/2] to this step, or make it a\nseparate patch [1.5/2] between this and the other steps, and explain\nit as a change that breaks compatibility in a way that hopefully\nwould not affect anybody in practice.  The alias configuration\nparser used to be overly loose and took \"alias.<subsection>.<key>\"\nas defining an alias \"<subsection>.<key>\"; that change tightens the\nparser and alias.<subsection>.<key> are silently ignored.  This step\nthen take alias.<subsection>.command to be defining a new-style\nalias that can be invoked as \"<subsection>\", which is case sensitive\nand is not limited to ASCII alphanumeric and dashes.\n\n> Suggested-by: Jeff King <peff@peff.net>\n> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n> ---\n>  Documentation/config/alias.adoc | 44 +++++++++++++++++++++-----\n>  alias.c                         | 45 ++++++++++++++++++++++-----\n>  help.c                          | 12 +++++--\n>  t/t0014-alias.sh                | 55 +++++++++++++++++++++++++++++++++\n>  4 files changed, 137 insertions(+), 19 deletions(-)\n>\n> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\n> index 80ce17d2de..17a548cd64 100644\n> --- a/Documentation/config/alias.adoc\n> +++ b/Documentation/config/alias.adoc\n> @@ -1,12 +1,40 @@\n>  alias.*::\n> +alias.*.command::\n> +\tCommand aliases for the linkgit:git[1] command wrapper. Aliases\n> +\tcan be defined using two syntaxes:\n> ++\n> +--\n> +1. Without a subsection, e.g., `[alias] co = checkout`. The alias\n> +   name is limited to alphanumeric characters and `-` (the same\n> +   limitation as configuration variable names), and is matched\n> +   case-insensitively.\n> +2. With a subsection, e.g., `[alias \"name\"] command = value`. The\n> +   alias name can contain any characters including UTF-8, and is\n> +   matched case-sensitively as raw bytes.\n> +--\n> ++\n> +Examples:\n> ++\n> +----\n> +# Without subsection\n> +[alias]\n> +    co = checkout\n> +    st = status\n> +\n> +# With subsection (allows UTF-8 and special characters)\n> +[alias \"hämta\"]\n> +    command = fetch\n> +[alias \"gömma\"]\n> +    command = stash\n> +----\n> ++\n> +E.g. after defining `alias.last = cat-file commit HEAD`, the invocation\n> +`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n> +confusion and troubles with script usage, aliases that\n> +hide existing Git commands are ignored except for deprecated\n> +commands.  Arguments are split by\n> +spaces, the usual shell quoting and escaping are supported.\n> +A quote pair or a backslash can be used to quote them.\n>  +\n>  Note that the first word of an alias does not necessarily have to be a\n>  command. It can be a command-line option that will be passed into the\n> diff --git a/alias.c b/alias.c\n> index c66a6095bb..cfd313ce5d 100644\n> --- a/alias.c\n> +++ b/alias.c\n> @@ -17,21 +17,50 @@ static int config_alias_cb(const char *key, const char *value,\n>  \t\t\t   const struct config_context *ctx UNUSED, void *d)\n>  {\n>  \tstruct config_alias_data *data = d;\n> -\tconst char *p;\n> +\tconst char *subsection, *subkey;\n> +\tsize_t subsection_len;\n>  \n> -\tif (!skip_prefix(key, \"alias.\", &p))\n> +\tif (parse_config_key(key, \"alias\", &subsection, &subsection_len,\n> +\t\t\t     &subkey) < 0)\n>  \t\treturn 0;\n>  \n> +\t/*\n> +\t * Two config syntaxes:\n> +\t * - alias.name = value        (without subsection, case-insensitive)\n> +\t * - [alias \"name\"]\n> +\t *       command = value       (with subsection, case-sensitive)\n> +\t */\n> +\tif (subsection) {\n> +\t\tif (strcmp(subkey, \"command\"))\n> +\t\t\treturn 0;\n\nThis silently ignores\n\n\t[alias \"foo\"]\n\t\tbar = !date\n\nwhich may or may not be a feature.  If the variable name is \"help\"\ninstead of \"bar\", it certainly is a feature to silently skip it, as\nit is not inconceivable that we would add such a variable name in\nthe future, and because we won't be able to predict the future, not\nlimiting us to \"help\" but ignoring anything we do not understand\nlike this code does may probably be a good thing.  I dunno.\n\n> +\t}\n> +\n>  \tif (data->alias) {\n> -\t\tif (!strcasecmp(p, data->alias)) {\n> +\t\tint match;\n> +\n> +\t\tif (subsection)\n> +\t\t\tmatch = (strlen(data->alias) == subsection_len &&\n> +\t\t\t\t !strncmp(data->alias, subsection,\n> +\t\t\t\t\t  subsection_len));\n> +\t\telse\n> +\t\t\tmatch = !strcasecmp(data->alias, subkey);\n> +\n> +\t\tif (match) {\n>  \t\t\tFREE_AND_NULL(data->v);\n> -\t\t\treturn git_config_string(&data->v,\n> -\t\t\t\t\t\t key, value);\n> +\t\t\treturn git_config_string(&data->v, key, value);\n>  \t\t}\n>  \t} else if (data->list) {\n> -\t\tif (value)\n> -\t\t\tstring_list_append(data->list, p)->util =\n> -\t\t\t\txstrdup(value);\n> +\t\tstruct string_list_item *item;\n> +\n> +\t\tif (!value)\n> +\t\t\treturn 0;\n> +\n> +\t\tif (subsection)\n> +\t\t\titem = string_list_append_nodup(data->list,\n> +\t\t\t\txmemdupz(subsection, subsection_len));\n> +\t\telse\n> +\t\t\titem = string_list_append(data->list, subkey);\n> +\t\titem->util = xstrdup(value);\n\nThis still silently ignores\n\n\t[alias \"foo\"]\n\t\tcommand\n\nwhich is a much more grave problem than ignoring alias.foo.bar in\nthe earlier part of this function.  We would want to preserve the\nexisting diagnosis on broken configuration.\n\n> @@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,\n>  \n>  \tfor (i = 0; cmds[i].name; i++) {\n>  \t\tif (cmds[i].category & mask) {\n> -\t\t\tsize_t len = strlen(cmds[i].name);\n> +\t\t\tsize_t len = utf8_strwidth(cmds[i].name);\n>  \t\t\tprintf(\"   %s   \", cmds[i].name);\n>  \t\t\tif (longest > len)\n>  \t\t\t\tmput_char(' ', longest - len);\n> @@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)\n>  \tstring_list_sort(&alias_list);\n>  \n>  \tfor (i = 0; i < alias_list.nr; i++) {\n> -\t\tsize_t len = strlen(alias_list.items[i].string);\n> +\t\tsize_t len = utf8_strwidth(alias_list.items[i].string);\n>  \t\tif (longest < len)\n>  \t\t\tlongest = len;\n>  \t}\n> @@ -590,8 +591,13 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n>  \n>  \t/* Also use aliases for command lookup */\n>  \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len, &key)) {\n> -\t\tif (!subsection)\n> +\t\tif (subsection) {\n> +\t\t\tif (!strcmp(key, \"command\"))\n> +\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n> +\t\t\t\t\t    subsection_len);\n> +\t\t} else {\n>  \t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n> +\t\t}\n>  \t}\n\nOK.  Alternatively, out of\n\n\t[alias \"foo\"]\n\t\tcommand = !echo foo\n\t\tbar = !echo bar\n\nwe _could_ list \"foo\" (a new style alias) and \"foo.bar\" (an old\nstyle alias that we have been accepting forever by mistake) for\nmaximum backward compatibility.  I am still undecided if it is a\ngood idea.\n\nThanks.\n"},{"id":"535716","messageId":"20260210222745.78575-1-jonatan@jontes.page","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"[PATCH 0/3] support UTF-8 in alias names","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-10T22:27:42Z","receivedAt":"2026-02-10T22:28:18Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Hi all,\n\nThis v3 addresses all feedback from Junio on v2. Changes since v2:\n\n- Split into three commits: a preparatory refactoring, a compatibility-\n  breaking change separated explicitly, and the main feature\n- Preserved error checking for value-less alias config (config_error_nonbool)\n- Fixed behavior when !value to preserve adding items to list while keeping\n  util as NULL\n- Improved commit messages to say \"ASCII alphanumeric\" and better explain\n  that aliases are implemented as config variable names\n- Tests now properly verify value-less command key handling\n\nThe implementation follows Peff's suggestion to use config subsections\nrather than modifying the config key syntax. This allows arbitrary bytes\nin alias names while maintaining backward compatibility.\n\nSuggested release note blurb:\n\n * Git aliases now support UTF-8 characters and special characters\n   in alias names through subsection syntax: `[alias \"name\"] command = value`.\n   This enables aliases in non-English languages. The traditional syntax\n   (without subsection, e.g., `[alias] co = checkout`) continues to work.\n\nJonatan Holmgren (3):\n  help: use list_aliases() for alias listing\n  alias: prepare for subsection aliases\n  alias: support non-alphanumeric names via subsection syntax\n\n Documentation/config/alias.adoc | 43 ++++++++++++++++++-----\n alias.c                         | 40 +++++++++++++++++++---\n help.c                          | 39 +++++++++++----------\n t/t0014-alias.sh                | 60 +++++++++++++++++++++++++++++++++\n 4 files changed, 150 insertions(+), 32 deletions(-)\n\n-- \n2.53.0\n"},{"id":"535717","messageId":"20260210222745.78575-2-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260210222745.78575-1-jonatan@jontes.page","subject":"[PATCH 1/3] help: use list_aliases() for alias listing","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-10T22:27:43Z","receivedAt":"2026-02-10T22:28:19Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"help.c has its own get_alias() config callback that duplicates the\nparsing logic in alias.c. Consolidate by teaching list_aliases() to\nalso store the alias values (via the string_list util field), then\nuse it in list_all_cmds_help_aliases() instead of the private\ncallback.\n\nThis preserves the existing error checking for value-less alias\ndefinitions by checking in alias.c rather than help.c.\n\nNo functional change intended.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n alias.c |  9 ++++++++-\n help.c  | 17 ++---------------\n 2 files changed, 10 insertions(+), 16 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 1a1a141a0a..1f8e13610c 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -24,12 +24,19 @@ static int config_alias_cb(const char *key, const char *value,\n \n \tif (data->alias) {\n \t\tif (!strcasecmp(p, data->alias)) {\n+\t\t\tif (!value)\n+\t\t\t\treturn config_error_nonbool(key);\n \t\t\tFREE_AND_NULL(data->v);\n \t\t\treturn git_config_string(&data->v,\n \t\t\t\t\t\t key, value);\n \t\t}\n \t} else if (data->list) {\n-\t\tstring_list_append(data->list, p);\n+\t\tstruct string_list_item *item;\n+\n+\t\titem = string_list_append(data->list, p);\n+\t\tif (value)\n+\t\t\titem->util = xstrdup(value);\n+\t\t/* if !value, item->util remains NULL but item is still added */\n \t}\n \n \treturn 0;\ndiff --git a/help.c b/help.c\nindex fefd811f7a..0bdb7ca10f 100644\n--- a/help.c\n+++ b/help.c\n@@ -20,6 +20,7 @@\n #include \"prompt.h\"\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n+#include \"alias.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)\n \tputchar('\\n');\n }\n \n-static int get_alias(const char *var, const char *value,\n-\t\t     const struct config_context *ctx UNUSED, void *data)\n-{\n-\tstruct string_list *list = data;\n-\n-\tif (skip_prefix(var, \"alias.\", &var)) {\n-\t\tif (!value)\n-\t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(list, var)->util = xstrdup(value);\n-\t}\n-\n-\treturn 0;\n-}\n-\n static void list_all_cmds_help_external_commands(void)\n {\n \tstruct string_list others = STRING_LIST_INIT_DUP;\n@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstruct cmdname_help *aliases;\n \tint i;\n \n-\trepo_config(the_repository, get_alias, &alias_list);\n+\tlist_aliases(&alias_list);\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\n-- \n2.53.0\n\n"},{"id":"535718","messageId":"20260210222745.78575-3-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260210222745.78575-1-jonatan@jontes.page","subject":"[PATCH 2/3] alias: prepare for subsection aliases","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-10T22:27:44Z","receivedAt":"2026-02-10T22:28:20Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Switch git_unknown_cmd_config() from skip_prefix() to\nparse_config_key() for alias parsing. This properly handles the\nthree-level config key structure and prepares for the new\nalias.*.command subsection syntax in the next commit.\n\nThis is a compatibility break: the alias configuration parser used\nto be overly permissive and accepted \"alias.<subsection>.<key>\" as\ndefining an alias \"<subsection>.<key>\". With this change,\nalias.<subsection>.<key> entries are silently ignored (unless <key>\nis \"command\", which will be given meaning in the next commit).\n\nThis behavior was arguably a bug, since config subsections were never\nintended to work this way for aliases, and aliases with dots in their\nnames have never been documented or intentionally supported.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n help.c | 10 +++++++---\n 1 file changed, 7 insertions(+), 3 deletions(-)\n\ndiff --git a/help.c b/help.c\nindex 0bdb7ca10f..eccd0c22f8 100644\n--- a/help.c\n+++ b/help.c\n@@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t\t\t\t  void *cb)\n {\n \tstruct help_unknown_cmd_config *cfg = cb;\n-\tconst char *p;\n+\tconst char *subsection, *key;\n+\tsize_t subsection_len;\n \n \tif (!strcmp(var, \"help.autocorrect\")) {\n \t\tint v = parse_autocorrect(value);\n@@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t}\n \n \t/* Also use aliases for command lookup */\n-\tif (skip_prefix(var, \"alias.\", &p))\n-\t\tadd_cmdname(&cfg->aliases, p, strlen(p));\n+\tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n+\t\t\t      &key)) {\n+\t\tif (!subsection)\n+\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t}\n \n \treturn 0;\n }\n-- \n2.53.0\n\n"},{"id":"535719","messageId":"20260210222745.78575-4-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260210222745.78575-1-jonatan@jontes.page","subject":"[PATCH 3/3] alias: support non-alphanumeric names via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-10T22:27:45Z","receivedAt":"2026-02-10T22:28:21Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Git alias names are limited to ASCII alphanumeric characters and\ndashes because aliases are implemented as config variable names, which\nare validated by iskeychar(). This prevents non-English speakers from\ncreating aliases in their native languages.\n\nAdd support for arbitrary alias names by using config subsections:\n\n    [alias \"förgrena\"]\n        command = branch\n\nThe subsection name is matched as-is (case-sensitive byte comparison),\nwhile the existing definition without a subsection (e.g.,\n\"[alias] co = checkout\") remains case-insensitive for backward\ncompatibility. This uses existing config infrastructure since\nsubsections already support arbitrary bytes, and avoids introducing\nUnicode normalization.\n\nAlso teach the help subsystem about the new syntax so that \"git help\n-a\" properly lists subsection aliases and the autocorrect feature can\nsuggest them. Use utf8_strwidth() instead of strlen() for column\nalignment so that non-ASCII alias names display correctly.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n Documentation/config/alias.adoc | 43 ++++++++++++++++++-----\n alias.c                         | 39 +++++++++++++++++----\n help.c                          | 14 ++++++--\n t/t0014-alias.sh                | 60 +++++++++++++++++++++++++++++++++\n 4 files changed, 138 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\nindex 80ce17d2de..9184f7cb37 100644\n--- a/Documentation/config/alias.adoc\n+++ b/Documentation/config/alias.adoc\n@@ -1,12 +1,39 @@\n alias.*::\n-\tCommand aliases for the linkgit:git[1] command wrapper - e.g.\n-\tafter defining `alias.last = cat-file commit HEAD`, the invocation\n-\t`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n-\tconfusion and troubles with script usage, aliases that\n-\thide existing Git commands are ignored except for deprecated\n-\tcommands.  Arguments are split by\n-\tspaces, the usual shell quoting and escaping are supported.\n-\tA quote pair or a backslash can be used to quote them.\n+alias.*.command::\n+\tCommand aliases for the linkgit:git[1] command wrapper. Aliases\n+\tcan be defined using two syntaxes:\n++\n+--\n+1. Without a subsection, e.g., `[alias] co = checkout`. The alias\n+   name is limited to ASCII alphanumeric characters and `-`,\n+   and is matched case-insensitively.\n+2. With a subsection, e.g., `[alias \"name\"] command = value`. The\n+   alias name can contain any characters including UTF-8, and is\n+   matched case-sensitively as raw bytes.\n+--\n++\n+Examples:\n++\n+----\n+# Without subsection (ASCII alphanumeric and dash only)\n+[alias]\n+    co = checkout\n+    st = status\n+\n+# With subsection (allows any characters, including UTF-8)\n+[alias \"hämta\"]\n+    command = fetch\n+[alias \"gömma\"]\n+    command = stash\n+----\n++\n+E.g. after defining `alias.last = cat-file commit HEAD`, the invocation\n+`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n+confusion and troubles with script usage, aliases that\n+hide existing Git commands are ignored except for deprecated\n+commands.  Arguments are split by\n+spaces, the usual shell quoting and escaping are supported.\n+A quote pair or a backslash can be used to quote them.\n +\n Note that the first word of an alias does not necessarily have to be a\n command. It can be a command-line option that will be passed into the\ndiff --git a/alias.c b/alias.c\nindex 1f8e13610c..ae1fe1efc6 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -17,15 +17,37 @@ static int config_alias_cb(const char *key, const char *value,\n \t\t\t   const struct config_context *ctx UNUSED, void *d)\n {\n \tstruct config_alias_data *data = d;\n-\tconst char *p;\n+\tconst char *subsection, *subkey;\n+\tsize_t subsection_len;\n \n-\tif (!skip_prefix(key, \"alias.\", &p))\n+\tif (parse_config_key(key, \"alias\", &subsection, &subsection_len,\n+\t\t\t     &subkey) < 0)\n \t\treturn 0;\n \n+\t/*\n+\t * Two config syntaxes:\n+\t * - alias.name = value   (without subsection, case-insensitive)\n+\t * - [alias \"name\"]\n+\t *       command = value  (with subsection, case-sensitive)\n+\t */\n+\tif (subsection) {\n+\t\tif (strcmp(subkey, \"command\"))\n+\t\t\treturn 0;\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(key);\n+\t}\n+\n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias)) {\n-\t\t\tif (!value)\n-\t\t\t\treturn config_error_nonbool(key);\n+\t\tint match;\n+\n+\t\tif (subsection)\n+\t\t\tmatch = (strlen(data->alias) == subsection_len &&\n+\t\t\t\t !strncmp(data->alias, subsection,\n+\t\t\t\t\t  subsection_len));\n+\t\telse\n+\t\t\tmatch = !strcasecmp(data->alias, subkey);\n+\n+\t\tif (match) {\n \t\t\tFREE_AND_NULL(data->v);\n \t\t\treturn git_config_string(&data->v,\n \t\t\t\t\t\t key, value);\n@@ -33,10 +55,13 @@ static int config_alias_cb(const char *key, const char *value,\n \t} else if (data->list) {\n \t\tstruct string_list_item *item;\n \n-\t\titem = string_list_append(data->list, p);\n+\t\tif (subsection)\n+\t\t\titem = string_list_append_nodup(data->list,\n+\t\t\t\txmemdupz(subsection, subsection_len));\n+\t\telse\n+\t\t\titem = string_list_append(data->list, subkey);\n \t\tif (value)\n \t\t\titem->util = xstrdup(value);\n-\t\t/* if !value, item->util remains NULL but item is still added */\n \t}\n \n \treturn 0;\ndiff --git a/help.c b/help.c\nindex eccd0c22f8..d7c6011780 100644\n--- a/help.c\n+++ b/help.c\n@@ -21,6 +21,7 @@\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n #include \"alias.h\"\n+#include \"utf8.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,\n \n \tfor (i = 0; cmds[i].name; i++) {\n \t\tif (cmds[i].category & mask) {\n-\t\t\tsize_t len = strlen(cmds[i].name);\n+\t\t\tsize_t len = utf8_strwidth(cmds[i].name);\n \t\t\tprintf(\"   %s   \", cmds[i].name);\n \t\t\tif (longest > len)\n \t\t\t\tmput_char(' ', longest - len);\n@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\n-\t\tsize_t len = strlen(alias_list.items[i].string);\n+\t\tsize_t len = utf8_strwidth(alias_list.items[i].string);\n \t\tif (longest < len)\n \t\t\tlongest = len;\n \t}\n@@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n-\t\tif (!subsection)\n+\t\tif (subsection) {\n+\t\t\t/* [alias \"name\"] command = value */\n+\t\t\tif (!strcmp(key, \"command\"))\n+\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\t\t    subsection_len);\n+\t\t} else {\n+\t\t\t/* alias.name = value */\n \t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t}\n \t}\n \n \treturn 0;\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex 07a53e7366..5d1f7730e6 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -112,4 +112,64 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '\n \tdone\n '\n \n+test_expect_success 'subsection syntax works' '\n+\ttest_config alias.testnew.command \"!echo ran-subsection\" &&\n+\tgit testnew >output &&\n+\ttest_grep \"ran-subsection\" output\n+'\n+\n+test_expect_success 'subsection syntax only accepts command key' '\n+\ttest_config alias.invalid.notcommand value &&\n+\ttest_must_fail git invalid 2>error &&\n+\ttest_grep -i \"not a git command\" error\n+'\n+\n+test_expect_success 'subsection syntax requires value for command' '\n+\ttest_when_finished \"git config --remove-section alias.noval\" &&\n+\tcat >>.git/config <<-\\EOF &&\n+\t[alias \"noval\"]\n+\t\tcommand\n+\tEOF\n+\ttest_must_fail git noval 2>error &&\n+\ttest_grep \"alias.noval.command\" error\n+'\n+\n+test_expect_success 'simple syntax is case-insensitive' '\n+\ttest_config alias.LegacyCase \"!echo ran-legacy\" &&\n+\tgit legacycase >output &&\n+\ttest_grep \"ran-legacy\" output\n+'\n+\n+test_expect_success 'subsection syntax is case-sensitive' '\n+\ttest_config alias.SubCase.command \"!echo ran-upper\" &&\n+\ttest_config alias.subcase.command \"!echo ran-lower\" &&\n+\tgit SubCase >upper.out &&\n+\tgit subcase >lower.out &&\n+\ttest_grep \"ran-upper\" upper.out &&\n+\ttest_grep \"ran-lower\" lower.out\n+'\n+\n+test_expect_success 'UTF-8 alias with Swedish characters' '\n+\ttest_config alias.\"förgrena\".command \"!echo ran-swedish\" &&\n+\tgit förgrena >output &&\n+\ttest_grep \"ran-swedish\" output\n+'\n+\n+test_expect_success 'UTF-8 alias with CJK characters' '\n+\ttest_config alias.\"分支\".command \"!echo ran-cjk\" &&\n+\tgit 分支 >output &&\n+\ttest_grep \"ran-cjk\" output\n+'\n+\n+test_expect_success 'alias with spaces in name' '\n+\ttest_config alias.\"test name\".command \"!echo ran-spaces\" &&\n+\tgit \"test name\" >output &&\n+\ttest_grep \"ran-spaces\" output\n+'\n+\n+test_expect_success 'subsection aliases listed in help -a' '\n+\ttest_config alias.\"förgrena\".command \"!echo test\" &&\n+\tgit help -a | grep \"förgrena\"\n+'\n+\n test_done\n-- \n2.53.0\n\n"},{"id":"535720","messageId":"38bf9422-c239-4398-ba9a-ebb1e62b7e86@jontes.page","threadId":"64949","inReplyTo":"xmqqv7g4xtx0.fsf@gitster.g","subject":"Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-10T22:29:26Z","receivedAt":"2026-02-10T22:29:36Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"I broke it out into 3 commits for my version 3 of the patches and think \nI've caught all the bugs in my implementation. Thanks for the patience!\n\nI also took the liberty of breaking\n     [alias \"foo\"]\n\tbar = !date\nas dotted aliases were never a feature (XKCD 1172 workflow hopefully not \nincoming) and it might be useful to be able to define more stuff under \nthis new alias syntax other than \"command\".\n\nPatch incoming.\n\nBest,\nJonatan\n\nOn 2026-02-10 20:47, Junio C Hamano wrote:\n> Jonatan Holmgren <jonatan@jontes.page> writes:\n> \n>> Git alias names are limited to alphanumeric characters and dashes\n>> because config variable names are validated by iskeychar(). This\n> \n> \"ASCII alphanumeric\", perhaps, as accented alphabet characters are\n> still alphanumeric ;-).  \"because aliases are implemented as config\n> variable names\" is probably an explanation that talks to readers in\n> terms closer to them (iskeychar() function is an implementation detail).\n> \n>> prevents non-English speakers from creating aliases in their native\n>> languages.\n> \n> True.\n> \n>> Add support for arbitrary alias names by using config subsections:\n>>\n>>      [alias \"förgrena\"]\n>>          command = branch\n>>\n>> The subsection name is matched as-is (case-sensitive byte comparison),\n>> while the existing definition without a subsection (e.g.,\n>> \"[alias] co = checkout\") remains case-insensitive for backward\n>> compatibility. This uses existing config infrastructure since\n>> subsections already support arbitrary bytes, and avoids introducing\n>> Unicode normalization.\n>>\n>> Also teach the help subsystem about the new syntax so that \"git help\n>> -a\" properly lists subsection aliases and the autocorrect feature can\n>> suggest them. Use utf8_strwidth() instead of strlen() for column\n>> alignment so that non-alphanumeric alias names display correctly.\n> \n> Either move the last two hunks from [1/2] to this step, or make it a\n> separate patch [1.5/2] between this and the other steps, and explain\n> it as a change that breaks compatibility in a way that hopefully\n> would not affect anybody in practice.  The alias configuration\n> parser used to be overly loose and took \"alias.<subsection>.<key>\"\n> as defining an alias \"<subsection>.<key>\"; that change tightens the\n> parser and alias.<subsection>.<key> are silently ignored.  This step\n> then take alias.<subsection>.command to be defining a new-style\n> alias that can be invoked as \"<subsection>\", which is case sensitive\n> and is not limited to ASCII alphanumeric and dashes.\n> \n>> Suggested-by: Jeff King <peff@peff.net>\n>> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n>> ---\n>>   Documentation/config/alias.adoc | 44 +++++++++++++++++++++-----\n>>   alias.c                         | 45 ++++++++++++++++++++++-----\n>>   help.c                          | 12 +++++--\n>>   t/t0014-alias.sh                | 55 +++++++++++++++++++++++++++++++++\n>>   4 files changed, 137 insertions(+), 19 deletions(-)\n>>\n>> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\n>> index 80ce17d2de..17a548cd64 100644\n>> --- a/Documentation/config/alias.adoc\n>> +++ b/Documentation/config/alias.adoc\n>> @@ -1,12 +1,40 @@\n>>   alias.*::\n>> +alias.*.command::\n>> +\tCommand aliases for the linkgit:git[1] command wrapper. Aliases\n>> +\tcan be defined using two syntaxes:\n>> ++\n>> +--\n>> +1. Without a subsection, e.g., `[alias] co = checkout`. The alias\n>> +   name is limited to alphanumeric characters and `-` (the same\n>> +   limitation as configuration variable names), and is matched\n>> +   case-insensitively.\n>> +2. With a subsection, e.g., `[alias \"name\"] command = value`. The\n>> +   alias name can contain any characters including UTF-8, and is\n>> +   matched case-sensitively as raw bytes.\n>> +--\n>> ++\n>> +Examples:\n>> ++\n>> +----\n>> +# Without subsection\n>> +[alias]\n>> +    co = checkout\n>> +    st = status\n>> +\n>> +# With subsection (allows UTF-8 and special characters)\n>> +[alias \"hämta\"]\n>> +    command = fetch\n>> +[alias \"gömma\"]\n>> +    command = stash\n>> +----\n>> ++\n>> +E.g. after defining `alias.last = cat-file commit HEAD`, the invocation\n>> +`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n>> +confusion and troubles with script usage, aliases that\n>> +hide existing Git commands are ignored except for deprecated\n>> +commands.  Arguments are split by\n>> +spaces, the usual shell quoting and escaping are supported.\n>> +A quote pair or a backslash can be used to quote them.\n>>   +\n>>   Note that the first word of an alias does not necessarily have to be a\n>>   command. It can be a command-line option that will be passed into the\n>> diff --git a/alias.c b/alias.c\n>> index c66a6095bb..cfd313ce5d 100644\n>> --- a/alias.c\n>> +++ b/alias.c\n>> @@ -17,21 +17,50 @@ static int config_alias_cb(const char *key, const char *value,\n>>   \t\t\t   const struct config_context *ctx UNUSED, void *d)\n>>   {\n>>   \tstruct config_alias_data *data = d;\n>> -\tconst char *p;\n>> +\tconst char *subsection, *subkey;\n>> +\tsize_t subsection_len;\n>>   \n>> -\tif (!skip_prefix(key, \"alias.\", &p))\n>> +\tif (parse_config_key(key, \"alias\", &subsection, &subsection_len,\n>> +\t\t\t     &subkey) < 0)\n>>   \t\treturn 0;\n>>   \n>> +\t/*\n>> +\t * Two config syntaxes:\n>> +\t * - alias.name = value        (without subsection, case-insensitive)\n>> +\t * - [alias \"name\"]\n>> +\t *       command = value       (with subsection, case-sensitive)\n>> +\t */\n>> +\tif (subsection) {\n>> +\t\tif (strcmp(subkey, \"command\"))\n>> +\t\t\treturn 0;\n> \n> This silently ignores\n> \n> \t[alias \"foo\"]\n> \t\tbar = !date\n> \n> which may or may not be a feature.  If the variable name is \"help\"\n> instead of \"bar\", it certainly is a feature to silently skip it, as\n> it is not inconceivable that we would add such a variable name in\n> the future, and because we won't be able to predict the future, not\n> limiting us to \"help\" but ignoring anything we do not understand\n> like this code does may probably be a good thing.  I dunno.\n> \n>> +\t}\n>> +\n>>   \tif (data->alias) {\n>> -\t\tif (!strcasecmp(p, data->alias)) {\n>> +\t\tint match;\n>> +\n>> +\t\tif (subsection)\n>> +\t\t\tmatch = (strlen(data->alias) == subsection_len &&\n>> +\t\t\t\t !strncmp(data->alias, subsection,\n>> +\t\t\t\t\t  subsection_len));\n>> +\t\telse\n>> +\t\t\tmatch = !strcasecmp(data->alias, subkey);\n>> +\n>> +\t\tif (match) {\n>>   \t\t\tFREE_AND_NULL(data->v);\n>> -\t\t\treturn git_config_string(&data->v,\n>> -\t\t\t\t\t\t key, value);\n>> +\t\t\treturn git_config_string(&data->v, key, value);\n>>   \t\t}\n>>   \t} else if (data->list) {\n>> -\t\tif (value)\n>> -\t\t\tstring_list_append(data->list, p)->util =\n>> -\t\t\t\txstrdup(value);\n>> +\t\tstruct string_list_item *item;\n>> +\n>> +\t\tif (!value)\n>> +\t\t\treturn 0;\n>> +\n>> +\t\tif (subsection)\n>> +\t\t\titem = string_list_append_nodup(data->list,\n>> +\t\t\t\txmemdupz(subsection, subsection_len));\n>> +\t\telse\n>> +\t\t\titem = string_list_append(data->list, subkey);\n>> +\t\titem->util = xstrdup(value);\n> \n> This still silently ignores\n> \n> \t[alias \"foo\"]\n> \t\tcommand\n> \n> which is a much more grave problem than ignoring alias.foo.bar in\n> the earlier part of this function.  We would want to preserve the\n> existing diagnosis on broken configuration.\n> \n>> @@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,\n>>   \n>>   \tfor (i = 0; cmds[i].name; i++) {\n>>   \t\tif (cmds[i].category & mask) {\n>> -\t\t\tsize_t len = strlen(cmds[i].name);\n>> +\t\t\tsize_t len = utf8_strwidth(cmds[i].name);\n>>   \t\t\tprintf(\"   %s   \", cmds[i].name);\n>>   \t\t\tif (longest > len)\n>>   \t\t\t\tmput_char(' ', longest - len);\n>> @@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)\n>>   \tstring_list_sort(&alias_list);\n>>   \n>>   \tfor (i = 0; i < alias_list.nr; i++) {\n>> -\t\tsize_t len = strlen(alias_list.items[i].string);\n>> +\t\tsize_t len = utf8_strwidth(alias_list.items[i].string);\n>>   \t\tif (longest < len)\n>>   \t\t\tlongest = len;\n>>   \t}\n>> @@ -590,8 +591,13 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n>>   \n>>   \t/* Also use aliases for command lookup */\n>>   \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len, &key)) {\n>> -\t\tif (!subsection)\n>> +\t\tif (subsection) {\n>> +\t\t\tif (!strcmp(key, \"command\"))\n>> +\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n>> +\t\t\t\t\t    subsection_len);\n>> +\t\t} else {\n>>   \t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n>> +\t\t}\n>>   \t}\n> \n> OK.  Alternatively, out of\n> \n> \t[alias \"foo\"]\n> \t\tcommand = !echo foo\n> \t\tbar = !echo bar\n> \n> we _could_ list \"foo\" (a new style alias) and \"foo.bar\" (an old\n> style alias that we have been accepting forever by mistake) for\n> maximum backward compatibility.  I am still undecided if it is a\n> good idea.\n> \n> Thanks.\n\n"},{"id":"535725","messageId":"xmqqv7g4w5mk.fsf@gitster.g","threadId":"64949","inReplyTo":"20260210222745.78575-2-jonatan@jontes.page","subject":"Re: [PATCH 1/3] help: use list_aliases() for alias listing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-10T23:17:23Z","receivedAt":"2026-02-10T23:17:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n>  \tif (data->alias) {\n>  \t\tif (!strcasecmp(p, data->alias)) {\n> +\t\t\tif (!value)\n> +\t\t\t\treturn config_error_nonbool(key);\n>  \t\t\tFREE_AND_NULL(data->v);\n>  \t\t\treturn git_config_string(&data->v,\n>  \t\t\t\t\t\t key, value);\n>  \t\t}\n\nHmph, git_config_string() would trigger config_error_nonbool()\nanyway if your feed value==NULL, so this change looks a noop.\n\n>  \t} else if (data->list) {\n> -\t\tstring_list_append(data->list, p);\n> +\t\tstruct string_list_item *item;\n> +\n> +\t\titem = string_list_append(data->list, p);\n> +\t\tif (value)\n> +\t\t\titem->util = xstrdup(value);\n> +\t\t/* if !value, item->util remains NULL but item is still added */\n\nThis side is silent.  We hold onto the value, when available, and\notherwise we just remember the fact that there is a (misconfigured)\nalias by keeping the NULL in the .util member.  Presumably it is now\nthe responsibility of the caller to deal with these entries with\nNULL in their .util member?\n\n>  \t}\n>  \n>  \treturn 0;\n> diff --git a/help.c b/help.c\n> index fefd811f7a..0bdb7ca10f 100644\n> --- a/help.c\n> +++ b/help.c\n> @@ -20,6 +20,7 @@\n>  #include \"prompt.h\"\n>  #include \"fsmonitor-ipc.h\"\n>  #include \"repository.h\"\n> +#include \"alias.h\"\n>  \n>  #ifndef NO_CURL\n>  #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n> @@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)\n>  \tputchar('\\n');\n>  }\n>  \n> -static int get_alias(const char *var, const char *value,\n> -\t\t     const struct config_context *ctx UNUSED, void *data)\n> -{\n> -\tstruct string_list *list = data;\n> -\n> -\tif (skip_prefix(var, \"alias.\", &var)) {\n> -\t\tif (!value)\n> -\t\t\treturn config_error_nonbool(var);\n> -\t\tstring_list_append(list, var)->util = xstrdup(value);\n> -\t}\n> -\n> -\treturn 0;\n> -}\n\nWe used to use this callback when listing aliases, which (1) added\nan alias with proper value to the list, and (2) reported a\nmisconfigured variable without adding it to the list.  So the net\neffect was that the user got diagnosis necessary to fix their\nconfiguration file, while the caller did not have to worry about\ngetting a broken entry appended to the list.\n\n>  static void list_all_cmds_help_external_commands(void)\n>  {\n>  \tstruct string_list others = STRING_LIST_INIT_DUP;\n> @@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)\n>  \tstruct cmdname_help *aliases;\n>  \tint i;\n>  \n> -\trepo_config(the_repository, get_alias, &alias_list);\n> +\tlist_aliases(&alias_list);\n\nWe call exactly the same alias.c:list_aliases(), which does not feed\ndata->alias at all, so we will take the \"else if (data->list)\"\ncodepath there.  Now we have these broken entries in the returned\nlist.  Because the shared callback did not give any diagnosis message,\nit is on up to us to do so, right?\n\nPerhaps in the code that begins in the post-context of this hunk, here...\n\n\tfor (i = 0; i < alias_list.nr; i++) {\n\t\tif (alias_list.items[i].util)\n\t\t\tcontinue;\n\t\tgive error equivanent to config_error_nonbool();\n\t\trelease resources held by alias_list.items[i];\n\t\tshift alias_list.items[i+1..alias_list.nr] by one;\n\t\ti-- to compensate for the shift of the array;\n\t}\n\nor something?\n\nHave you considered doing the config_error_nonbool(key) on the\ndata->list side of the if/else inside alias.c:config_alias_cb(),\njust like help.c:get_alias() callback used to do?\n\nI haven't stared at this code as long as you have, so it is very\npossible I am missing the reason why that code path wants to be\nsilent, though.  But if we can do so, then this caller does not have\nto worry about having to handle broken entries at all.\n\nThanks.\n\n>  \tstring_list_sort(&alias_list);\n>  \n>  \tfor (i = 0; i < alias_list.nr; i++) {\n\n"},{"id":"535801","messageId":"20260211211810.278806-1-jonatan@jontes.page","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"[PATCH v4 0/3] support UTF-8 in alias names","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-11T21:18:07Z","receivedAt":"2026-02-11T21:18:48Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"This series enables UTF-8 and special characters in Git alias names by\nusing config subsection syntax: `[alias \"name\"] command = value`. This\nallows non-English speakers to create aliases in their native languages\nwhile maintaining backward compatibility with the traditional syntax.\n\nChanges since v3:\n\n- Removed redundant !value check in data->alias branch, as Junio noted\n  that git_config_string() already handles this case\n\n- Added config_error_nonbool() in data->list branch so misconfigured\n  aliases (without values) are caught immediately instead of silently\n  adding broken entries that callers must handle\n\n- Simplified code: consolidated error checking, removed conditional\n  item->util assignment in favor of early error return\n\n- Added test case for value-less alias error handling\n\n- Improved test robustness by using intermediate file + test_grep\n  instead of piping to grep\n\nJonatan Holmgren (3):\n  help: use list_aliases() for alias listing\n  alias: prepare for subsection aliases\n  alias: support non-alphanumeric names via subsection syntax\n\n Documentation/config/alias.adoc | 43 ++++++++++++++++----\n alias.c                         | 38 ++++++++++++++++--\n help.c                          | 39 +++++++++---------\n t/t0014-alias.sh                | 71 +++++++++++++++++++++++++++++++++\n 4 files changed, 159 insertions(+), 32 deletions(-)\n\n-- \n2.53.0\n\n"},{"id":"535802","messageId":"20260211211810.278806-3-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260211211810.278806-1-jonatan@jontes.page","subject":"[PATCH v4 2/3] alias: prepare for subsection aliases","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-11T21:18:09Z","receivedAt":"2026-02-11T21:18:52Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Switch git_unknown_cmd_config() from skip_prefix() to\nparse_config_key() for alias parsing. This properly handles the\nthree-level config key structure and prepares for the new\nalias.*.command subsection syntax in the next commit.\n\nThis is a compatibility break: the alias configuration parser used\nto be overly permissive and accepted \"alias.<subsection>.<key>\" as\ndefining an alias \"<subsection>.<key>\". With this change,\nalias.<subsection>.<key> entries are silently ignored (unless <key>\nis \"command\", which will be given meaning in the next commit).\n\nThis behavior was arguably a bug, since config subsections were never\nintended to work this way for aliases, and aliases with dots in their\nnames have never been documented or intentionally supported.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n help.c | 10 +++++++---\n 1 file changed, 7 insertions(+), 3 deletions(-)\n\ndiff --git a/help.c b/help.c\nindex 0bdb7ca10f..eccd0c22f8 100644\n--- a/help.c\n+++ b/help.c\n@@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t\t\t\t  void *cb)\n {\n \tstruct help_unknown_cmd_config *cfg = cb;\n-\tconst char *p;\n+\tconst char *subsection, *key;\n+\tsize_t subsection_len;\n \n \tif (!strcmp(var, \"help.autocorrect\")) {\n \t\tint v = parse_autocorrect(value);\n@@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t}\n \n \t/* Also use aliases for command lookup */\n-\tif (skip_prefix(var, \"alias.\", &p))\n-\t\tadd_cmdname(&cfg->aliases, p, strlen(p));\n+\tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n+\t\t\t      &key)) {\n+\t\tif (!subsection)\n+\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t}\n \n \treturn 0;\n }\n-- \n2.53.0\n\n"},{"id":"535803","messageId":"20260211211810.278806-2-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260211211810.278806-1-jonatan@jontes.page","subject":"[PATCH v4 1/3] help: use list_aliases() for alias listing","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-11T21:18:08Z","receivedAt":"2026-02-11T21:18:56Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"help.c has its own get_alias() config callback that duplicates the\nparsing logic in alias.c. Consolidate by teaching list_aliases() to\nalso store the alias values (via the string_list util field), then\nuse it in list_all_cmds_help_aliases() instead of the private\ncallback.\n\nThis preserves the existing error checking for value-less alias\ndefinitions by checking in alias.c rather than help.c.\n\nNo functional change intended.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n alias.c          |  8 +++++++-\n help.c           | 17 ++---------------\n t/t0014-alias.sh | 10 ++++++++++\n 3 files changed, 19 insertions(+), 16 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 1a1a141a0a..271acb9bf1 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -29,7 +29,13 @@ static int config_alias_cb(const char *key, const char *value,\n \t\t\t\t\t\t key, value);\n \t\t}\n \t} else if (data->list) {\n-\t\tstring_list_append(data->list, p);\n+\t\tstruct string_list_item *item;\n+\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(key);\n+\n+\t\titem = string_list_append(data->list, p);\n+\t\titem->util = xstrdup(value);\n \t}\n \n \treturn 0;\ndiff --git a/help.c b/help.c\nindex fefd811f7a..0bdb7ca10f 100644\n--- a/help.c\n+++ b/help.c\n@@ -20,6 +20,7 @@\n #include \"prompt.h\"\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n+#include \"alias.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)\n \tputchar('\\n');\n }\n \n-static int get_alias(const char *var, const char *value,\n-\t\t     const struct config_context *ctx UNUSED, void *data)\n-{\n-\tstruct string_list *list = data;\n-\n-\tif (skip_prefix(var, \"alias.\", &var)) {\n-\t\tif (!value)\n-\t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(list, var)->util = xstrdup(value);\n-\t}\n-\n-\treturn 0;\n-}\n-\n static void list_all_cmds_help_external_commands(void)\n {\n \tstruct string_list others = STRING_LIST_INIT_DUP;\n@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstruct cmdname_help *aliases;\n \tint i;\n \n-\trepo_config(the_repository, get_alias, &alias_list);\n+\tlist_aliases(&alias_list);\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex 07a53e7366..a13d2be8ca 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -112,4 +112,14 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '\n \tdone\n '\n \n+test_expect_success 'alias without value reports error' '\n+\ttest_when_finished \"git config --unset alias.noval\" &&\n+\tcat >>.git/config <<-\\EOF &&\n+\t[alias]\n+\t\tnoval\n+\tEOF\n+\ttest_must_fail git noval 2>error &&\n+\ttest_grep \"alias.noval\" error\n+'\n+\n test_done\n-- \n2.53.0\n\n"},{"id":"535804","messageId":"20260211211810.278806-4-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260211211810.278806-1-jonatan@jontes.page","subject":"[PATCH v4 3/3] alias: support non-alphanumeric names via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-11T21:18:10Z","receivedAt":"2026-02-11T21:19:01Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Git alias names are limited to ASCII alphanumeric characters and\ndashes because aliases are implemented as config variable names.\nThis prevents non-English speakers from creating aliases in their\nnative languages.\n\nAdd support for arbitrary alias names by using config subsections:\n\n    [alias \"förgrena\"]\n        command = branch\n\nThe subsection name is matched as-is (case-sensitive byte comparison),\nwhile the existing definition without a subsection (e.g.,\n\"[alias] co = checkout\") remains case-insensitive for backward\ncompatibility. This uses existing config infrastructure since\nsubsections already support arbitrary bytes, and avoids introducing\nUnicode normalization.\n\nAlso teach the help subsystem about the new syntax so that \"git help\n-a\" properly lists subsection aliases and the autocorrect feature can\nsuggest them. Use utf8_strwidth() instead of strlen() for column\nalignment so that non-ASCII alias names display correctly.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n Documentation/config/alias.adoc | 43 ++++++++++++++++++-----\n alias.c                         | 32 ++++++++++++++---\n help.c                          | 14 ++++++--\n t/t0014-alias.sh                | 61 +++++++++++++++++++++++++++++++++\n 4 files changed, 135 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\nindex 80ce17d2de..9184f7cb37 100644\n--- a/Documentation/config/alias.adoc\n+++ b/Documentation/config/alias.adoc\n@@ -1,12 +1,39 @@\n alias.*::\n-\tCommand aliases for the linkgit:git[1] command wrapper - e.g.\n-\tafter defining `alias.last = cat-file commit HEAD`, the invocation\n-\t`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n-\tconfusion and troubles with script usage, aliases that\n-\thide existing Git commands are ignored except for deprecated\n-\tcommands.  Arguments are split by\n-\tspaces, the usual shell quoting and escaping are supported.\n-\tA quote pair or a backslash can be used to quote them.\n+alias.*.command::\n+\tCommand aliases for the linkgit:git[1] command wrapper. Aliases\n+\tcan be defined using two syntaxes:\n++\n+--\n+1. Without a subsection, e.g., `[alias] co = checkout`. The alias\n+   name is limited to ASCII alphanumeric characters and `-`,\n+   and is matched case-insensitively.\n+2. With a subsection, e.g., `[alias \"name\"] command = value`. The\n+   alias name can contain any characters including UTF-8, and is\n+   matched case-sensitively as raw bytes.\n+--\n++\n+Examples:\n++\n+----\n+# Without subsection (ASCII alphanumeric and dash only)\n+[alias]\n+    co = checkout\n+    st = status\n+\n+# With subsection (allows any characters, including UTF-8)\n+[alias \"hämta\"]\n+    command = fetch\n+[alias \"gömma\"]\n+    command = stash\n+----\n++\n+E.g. after defining `alias.last = cat-file commit HEAD`, the invocation\n+`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n+confusion and troubles with script usage, aliases that\n+hide existing Git commands are ignored except for deprecated\n+commands.  Arguments are split by\n+spaces, the usual shell quoting and escaping are supported.\n+A quote pair or a backslash can be used to quote them.\n +\n Note that the first word of an alias does not necessarily have to be a\n command. It can be a command-line option that will be passed into the\ndiff --git a/alias.c b/alias.c\nindex 271acb9bf1..896d0f80a4 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -17,13 +17,33 @@ static int config_alias_cb(const char *key, const char *value,\n \t\t\t   const struct config_context *ctx UNUSED, void *d)\n {\n \tstruct config_alias_data *data = d;\n-\tconst char *p;\n+\tconst char *subsection, *subkey;\n+\tsize_t subsection_len;\n \n-\tif (!skip_prefix(key, \"alias.\", &p))\n+\tif (parse_config_key(key, \"alias\", &subsection, &subsection_len,\n+\t\t\t     &subkey) < 0)\n+\t\treturn 0;\n+\n+\t/*\n+\t * Two config syntaxes:\n+\t * - alias.name = value   (without subsection, case-insensitive)\n+\t * - [alias \"name\"]\n+\t *       command = value  (with subsection, case-sensitive)\n+\t */\n+\tif (subsection && strcmp(subkey, \"command\"))\n \t\treturn 0;\n \n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias)) {\n+\t\tint match;\n+\n+\t\tif (subsection)\n+\t\t\tmatch = (strlen(data->alias) == subsection_len &&\n+\t\t\t\t !strncmp(data->alias, subsection,\n+\t\t\t\t\t  subsection_len));\n+\t\telse\n+\t\t\tmatch = !strcasecmp(data->alias, subkey);\n+\n+\t\tif (match) {\n \t\t\tFREE_AND_NULL(data->v);\n \t\t\treturn git_config_string(&data->v,\n \t\t\t\t\t\t key, value);\n@@ -34,7 +54,11 @@ static int config_alias_cb(const char *key, const char *value,\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(key);\n \n-\t\titem = string_list_append(data->list, p);\n+\t\tif (subsection)\n+\t\t\titem = string_list_append_nodup(data->list,\n+\t\t\t\txmemdupz(subsection, subsection_len));\n+\t\telse\n+\t\t\titem = string_list_append(data->list, subkey);\n \t\titem->util = xstrdup(value);\n \t}\n \ndiff --git a/help.c b/help.c\nindex eccd0c22f8..d7c6011780 100644\n--- a/help.c\n+++ b/help.c\n@@ -21,6 +21,7 @@\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n #include \"alias.h\"\n+#include \"utf8.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,\n \n \tfor (i = 0; cmds[i].name; i++) {\n \t\tif (cmds[i].category & mask) {\n-\t\t\tsize_t len = strlen(cmds[i].name);\n+\t\t\tsize_t len = utf8_strwidth(cmds[i].name);\n \t\t\tprintf(\"   %s   \", cmds[i].name);\n \t\t\tif (longest > len)\n \t\t\t\tmput_char(' ', longest - len);\n@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\n-\t\tsize_t len = strlen(alias_list.items[i].string);\n+\t\tsize_t len = utf8_strwidth(alias_list.items[i].string);\n \t\tif (longest < len)\n \t\t\tlongest = len;\n \t}\n@@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n-\t\tif (!subsection)\n+\t\tif (subsection) {\n+\t\t\t/* [alias \"name\"] command = value */\n+\t\t\tif (!strcmp(key, \"command\"))\n+\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\t\t    subsection_len);\n+\t\t} else {\n+\t\t\t/* alias.name = value */\n \t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t}\n \t}\n \n \treturn 0;\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex a13d2be8ca..34bbdb51c5 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -122,4 +122,65 @@ test_expect_success 'alias without value reports error' '\n \ttest_grep \"alias.noval\" error\n '\n \n+test_expect_success 'subsection syntax works' '\n+\ttest_config alias.testnew.command \"!echo ran-subsection\" &&\n+\tgit testnew >output &&\n+\ttest_grep \"ran-subsection\" output\n+'\n+\n+test_expect_success 'subsection syntax only accepts command key' '\n+\ttest_config alias.invalid.notcommand value &&\n+\ttest_must_fail git invalid 2>error &&\n+\ttest_grep -i \"not a git command\" error\n+'\n+\n+test_expect_success 'subsection syntax requires value for command' '\n+\ttest_when_finished \"git config --remove-section alias.noval\" &&\n+\tcat >>.git/config <<-\\EOF &&\n+\t[alias \"noval\"]\n+\t\tcommand\n+\tEOF\n+\ttest_must_fail git noval 2>error &&\n+\ttest_grep \"alias.noval.command\" error\n+'\n+\n+test_expect_success 'simple syntax is case-insensitive' '\n+\ttest_config alias.LegacyCase \"!echo ran-legacy\" &&\n+\tgit legacycase >output &&\n+\ttest_grep \"ran-legacy\" output\n+'\n+\n+test_expect_success 'subsection syntax is case-sensitive' '\n+\ttest_config alias.SubCase.command \"!echo ran-upper\" &&\n+\ttest_config alias.subcase.command \"!echo ran-lower\" &&\n+\tgit SubCase >upper.out &&\n+\tgit subcase >lower.out &&\n+\ttest_grep \"ran-upper\" upper.out &&\n+\ttest_grep \"ran-lower\" lower.out\n+'\n+\n+test_expect_success 'UTF-8 alias with Swedish characters' '\n+\ttest_config alias.\"förgrena\".command \"!echo ran-swedish\" &&\n+\tgit förgrena >output &&\n+\ttest_grep \"ran-swedish\" output\n+'\n+\n+test_expect_success 'UTF-8 alias with CJK characters' '\n+\ttest_config alias.\"分支\".command \"!echo ran-cjk\" &&\n+\tgit 分支 >output &&\n+\ttest_grep \"ran-cjk\" output\n+'\n+\n+test_expect_success 'alias with spaces in name' '\n+\ttest_config alias.\"test name\".command \"!echo ran-spaces\" &&\n+\tgit \"test name\" >output &&\n+\ttest_grep \"ran-spaces\" output\n+'\n+\n+test_expect_success 'subsection aliases listed in help -a' '\n+\ttest_config alias.\"förgrena\".command \"!echo test\" &&\n+\tgit help -a >output &&\n+\ttest_grep \"förgrena\" output\n+'\n+\n test_done\n-- \n2.53.0\n\n"},{"id":"535806","messageId":"xmqq5x83rlp2.fsf@gitster.g","threadId":"64949","inReplyTo":"20260211211810.278806-3-jonatan@jontes.page","subject":"Re: [PATCH v4 2/3] alias: prepare for subsection aliases","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-11T21:53:45Z","receivedAt":"2026-02-11T21:53:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> Switch git_unknown_cmd_config() from skip_prefix() to\n> parse_config_key() for alias parsing. This properly handles the\n> three-level config key structure and prepares for the new\n> alias.*.command subsection syntax in the next commit.\n>\n> This is a compatibility break: the alias configuration parser used\n> to be overly permissive and accepted \"alias.<subsection>.<key>\" as\n> defining an alias \"<subsection>.<key>\". With this change,\n> alias.<subsection>.<key> entries are silently ignored (unless <key>\n> is \"command\", which will be given meaning in the next commit).\n>\n> This behavior was arguably a bug, since config subsections were never\n> intended to work this way for aliases, and aliases with dots in their\n> names have never been documented or intentionally supported.\n>\n> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n> ---\n>  help.c | 10 +++++++---\n>  1 file changed, 7 insertions(+), 3 deletions(-)\n\nVery well explained.\n\n>\n> diff --git a/help.c b/help.c\n> index 0bdb7ca10f..eccd0c22f8 100644\n> --- a/help.c\n> +++ b/help.c\n> @@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n>  \t\t\t\t  void *cb)\n>  {\n>  \tstruct help_unknown_cmd_config *cfg = cb;\n> -\tconst char *p;\n> +\tconst char *subsection, *key;\n> +\tsize_t subsection_len;\n>  \n>  \tif (!strcmp(var, \"help.autocorrect\")) {\n>  \t\tint v = parse_autocorrect(value);\n> @@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n>  \t}\n>  \n>  \t/* Also use aliases for command lookup */\n> -\tif (skip_prefix(var, \"alias.\", &p))\n> -\t\tadd_cmdname(&cfg->aliases, p, strlen(p));\n> +\tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n> +\t\t\t      &key)) {\n> +\t\tif (!subsection)\n> +\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n> +\t}\n>  \n>  \treturn 0;\n>  }\n"},{"id":"535808","messageId":"xmqqqzqqrk2h.fsf@gitster.g","threadId":"64949","inReplyTo":"20260211211810.278806-4-jonatan@jontes.page","subject":"Re: [PATCH v4 3/3] alias: support non-alphanumeric names via subsection syntax","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-11T22:28:54Z","receivedAt":"2026-02-11T22:28:57Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n>  alias.*::\n> +alias.*.command::\n> +\tCommand aliases for the linkgit:git[1] command wrapper. Aliases\n> +\tcan be defined using two syntaxes:\n> ++\n> +--\n> +1. Without a subsection, e.g., `[alias] co = checkout`. The alias\n> +   name is limited to ASCII alphanumeric characters and `-`,\n> +   and is matched case-insensitively.\n\nOK.  It is obvious to us that the \"alias name\" in the example is \"co\";\nis it obvious enough for our first-time readers, or would we want to\ndo something like\n\n\tThe alias name (\"co\", in this example) is limited to ...\n\nto be extra clear, I wonder.\n\n> +2. With a subsection, e.g., `[alias \"name\"] command = value`. The\n> +   alias name can contain any characters including UTF-8, and is\n> +   matched case-sensitively as raw bytes.\n\nUnlike the previous example that is more realistic, this uses <name>\nand <value> placeholders, with `command` that MUST be given verbatim\nby the users.  Is that obvious enough to our first-time readers?\n\nThinking aloud.  How does it look with placeholder filled with\nconcrete values?\n\n\t... e.g., `[alias \"co\"] command = checkout`.  The alias name\n\t(\"co\", in this example) can contain any characters ...\n\nThis does not look too bad to me.\n\nWe do not allow newlines or NULs in the subsection.  NULs may be too\nobvious, but newlines might be worth mentioning.  I dunno.\n\n> +--\n> ++\n> +Examples:\n> ++\n> +----\n> +# Without subsection (ASCII alphanumeric and dash only)\n> +[alias]\n> +    co = checkout\n> +    st = status\n> +\n> +# With subsection (allows any characters, including UTF-8)\n> +[alias \"hämta\"]\n> +    command = fetch\n> +[alias \"gömma\"]\n> +    command = stash\n> +----\n\nGood examples, even though I do not read Swedish ;-).\n\n> +E.g. after defining `alias.last = cat-file commit HEAD`, the invocation\n> +`git last` is equivalent to `git cat-file commit HEAD`.\n\nThis is not a new problem (it is an inherited text from before your\nchange), but I've always found this\n\n\talias.last = cat-file commit HEAD\n\na poor thing to give to our users, as it does not match anything\nthey practically can use.  It is different from the valid command\nline arguments to define the alias, which is\n\n\t$ git config set alias.last \"cat-file commit HEAD\"\n\nand it is different from the way the result appears in the\nconfiguration file, which is\n\n\t[alias] last = cat-file commit HEAD\n\nAlso, since the sentences are moved around, I am not sure that the\nbeginning \"E.g.\" still fits there very well.  Taking them all\ntogether, how about\n\n    With a Git alias defined, e.g.,\n\n\t$ git config set alias.last \"cat-file commit HEAD\"\n\n    you can run `git last` and it invokes `git cat-file commit\n    HEAD`.\n\n> diff --git a/alias.c b/alias.c\n> index 271acb9bf1..896d0f80a4 100644\n> --- a/alias.c\n> +++ b/alias.c\n> @@ -17,13 +17,33 @@ static int config_alias_cb(const char *key, const char *value,\n>  \t\t\t   const struct config_context *ctx UNUSED, void *d)\n>  {\n>  \tstruct config_alias_data *data = d;\n> -\tconst char *p;\n> +\tconst char *subsection, *subkey;\n> +\tsize_t subsection_len;\n\n\"subkey\" is a confusing name for a variable.\n\nThe Synatx section in \"git config --help\" documentation says that a\nconfiguration file consists of \"sections and variables\", and a\nsection can further be divided into subsections.\n\nconfig.c seems to use \"key\" as a synonym for \"variable\" above, and\nthat is very understandable, because \"section.subsection.variable\"\nor \"section.variable\" as a whole is what the users and documentation\ncalls a \"configuration variable\", and to avoid overloading the two\nmeanings on the same word \"variable\", we'd better use a different\nname for that last-level thing.\n\nTaken together, in\n\n\t[alias] co = checkout\n\t[alias \"ci\"] command = commit\n\nit would be the best to call the parts like so:\n\n\tsection: \"alias\"\n\tsubsection: \"ci\"\n\tkey: \"co\" and \"command\"\n\n> -\tif (!skip_prefix(key, \"alias.\", &p))\n> +\tif (parse_config_key(key, \"alias\", &subsection, &subsection_len,\n> +\t\t\t     &subkey) < 0)\n> +\t\treturn 0;\n> +\n> +\t/*\n> +\t * Two config syntaxes:\n> +\t * - alias.name = value   (without subsection, case-insensitive)\n> +\t * - [alias \"name\"]\n> +\t *       command = value  (with subsection, case-sensitive)\n> +\t */\n> +\tif (subsection && strcmp(subkey, \"command\"))\n>  \t\treturn 0;\n\nOK.  We ignore alias.*.variable where variable is not \"command\".\n\n>  \tif (data->alias) {\n\nWhen the caller is querying one specific alias ...\n\n> +\t\tint match;\n> +\n> +\t\tif (subsection)\n> +\t\t\tmatch = (strlen(data->alias) == subsection_len &&\n> +\t\t\t\t !strncmp(data->alias, subsection,\n> +\t\t\t\t\t  subsection_len));\n\n... we pick either the one that literally matches the subsection\npart (we have already verified that the key is \"command\"), or ...\n\n> +\t\telse\n> +\t\t\tmatch = !strcasecmp(data->alias, subkey);\n\n... for a two-level variable, the one that matches variable name\ncase insensitively.  And when we see hit, ...\n\n> +\t\tif (match) {\n>  \t\t\tFREE_AND_NULL(data->v);\n>  \t\t\treturn git_config_string(&data->v,\n>  \t\t\t\t\t\t key, value);\n\n... we report it to the caller.  Otherwise, when we are listing ...\n\n> @@ -34,7 +54,11 @@ static int config_alias_cb(const char *key, const char *value,\n>  \t\tif (!value)\n>  \t\t\treturn config_error_nonbool(key);\n>  \n> -\t\titem = string_list_append(data->list, p);\n> +\t\tif (subsection)\n> +\t\t\titem = string_list_append_nodup(data->list,\n> +\t\t\t\txmemdupz(subsection, subsection_len));\n> +\t\telse\n> +\t\t\titem = string_list_append(data->list, subkey);\n\n... the alias name we create differs between the two- and\nthree-level names, but otherwise the handling is the same between\nthe two kinds.\n\n>  \t\titem->util = xstrdup(value);\n>  \t}\n\nAll makes sense.\n\n> diff --git a/help.c b/help.c\n> index eccd0c22f8..d7c6011780 100644\n> --- a/help.c\n> +++ b/help.c\n> @@ -21,6 +21,7 @@\n>  #include \"fsmonitor-ipc.h\"\n>  #include \"repository.h\"\n>  #include \"alias.h\"\n> +#include \"utf8.h\"\n>  \n>  #ifndef NO_CURL\n>  #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n> @@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,\n>  \n>  \tfor (i = 0; cmds[i].name; i++) {\n>  \t\tif (cmds[i].category & mask) {\n> -\t\t\tsize_t len = strlen(cmds[i].name);\n> +\t\t\tsize_t len = utf8_strwidth(cmds[i].name);\n>  \t\t\tprintf(\"   %s   \", cmds[i].name);\n>  \t\t\tif (longest > len)\n>  \t\t\t\tmput_char(' ', longest - len);\n> @@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)\n>  \tstring_list_sort(&alias_list);\n>  \n>  \tfor (i = 0; i < alias_list.nr; i++) {\n> -\t\tsize_t len = strlen(alias_list.items[i].string);\n> +\t\tsize_t len = utf8_strwidth(alias_list.items[i].string);\n>  \t\tif (longest < len)\n>  \t\t\tlongest = len;\n>  \t}\n> @@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n>  \t/* Also use aliases for command lookup */\n>  \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n>  \t\t\t      &key)) {\n> -\t\tif (!subsection)\n> +\t\tif (subsection) {\n> +\t\t\t/* [alias \"name\"] command = value */\n> +\t\t\tif (!strcmp(key, \"command\"))\n> +\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n> +\t\t\t\t\t    subsection_len);\n> +\t\t} else {\n> +\t\t\t/* alias.name = value */\n>  \t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n> +\t\t}\n>  \t}\n>  \n>  \treturn 0;\n\nLooks very good.\n\n"},{"id":"535809","messageId":"xmqqldgyrk24.fsf@gitster.g","threadId":"64949","inReplyTo":"20260211211810.278806-2-jonatan@jontes.page","subject":"Re: [PATCH v4 1/3] help: use list_aliases() for alias listing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-11T22:29:07Z","receivedAt":"2026-02-11T22:29:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> help.c has its own get_alias() config callback that duplicates the\n> parsing logic in alias.c. Consolidate by teaching list_aliases() to\n> also store the alias values (via the string_list util field), then\n> use it in list_all_cmds_help_aliases() instead of the private\n> callback.\n>\n> This preserves the existing error checking for value-less alias\n> definitions by checking in alias.c rather than help.c.\n>\n> No functional change intended.\n>\n> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n> ---\n>  alias.c          |  8 +++++++-\n>  help.c           | 17 ++---------------\n>  t/t0014-alias.sh | 10 ++++++++++\n>  3 files changed, 19 insertions(+), 16 deletions(-)\n\nLooks good.  There is a small functional change not on the\nhelp.c:get_alias() side (i.e., \"git help --all\") but on the\nalias.c:list_aliases() side.  \"git --list-cmds=alias\", used by\ncommand line completion, used to include such a broken alias, but it\nno longer does (and gets an error).\n\nI think it is fine to call it a bugfix ;-)\n\n\n> diff --git a/alias.c b/alias.c\n> index 1a1a141a0a..271acb9bf1 100644\n> --- a/alias.c\n> +++ b/alias.c\n> @@ -29,7 +29,13 @@ static int config_alias_cb(const char *key, const char *value,\n>  \t\t\t\t\t\t key, value);\n>  \t\t}\n>  \t} else if (data->list) {\n> -\t\tstring_list_append(data->list, p);\n> +\t\tstruct string_list_item *item;\n> +\n> +\t\tif (!value)\n> +\t\t\treturn config_error_nonbool(key);\n> +\n> +\t\titem = string_list_append(data->list, p);\n> +\t\titem->util = xstrdup(value);\n>  \t}\n>  \n>  \treturn 0;\n> diff --git a/help.c b/help.c\n> index fefd811f7a..0bdb7ca10f 100644\n> --- a/help.c\n> +++ b/help.c\n> @@ -20,6 +20,7 @@\n>  #include \"prompt.h\"\n>  #include \"fsmonitor-ipc.h\"\n>  #include \"repository.h\"\n> +#include \"alias.h\"\n>  \n>  #ifndef NO_CURL\n>  #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n> @@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)\n>  \tputchar('\\n');\n>  }\n>  \n> -static int get_alias(const char *var, const char *value,\n> -\t\t     const struct config_context *ctx UNUSED, void *data)\n> -{\n> -\tstruct string_list *list = data;\n> -\n> -\tif (skip_prefix(var, \"alias.\", &var)) {\n> -\t\tif (!value)\n> -\t\t\treturn config_error_nonbool(var);\n> -\t\tstring_list_append(list, var)->util = xstrdup(value);\n> -\t}\n> -\n> -\treturn 0;\n> -}\n> -\n>  static void list_all_cmds_help_external_commands(void)\n>  {\n>  \tstruct string_list others = STRING_LIST_INIT_DUP;\n> @@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)\n>  \tstruct cmdname_help *aliases;\n>  \tint i;\n>  \n> -\trepo_config(the_repository, get_alias, &alias_list);\n> +\tlist_aliases(&alias_list);\n>  \tstring_list_sort(&alias_list);\n>  \n>  \tfor (i = 0; i < alias_list.nr; i++) {\n> diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\n> index 07a53e7366..a13d2be8ca 100755\n> --- a/t/t0014-alias.sh\n> +++ b/t/t0014-alias.sh\n> @@ -112,4 +112,14 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '\n>  \tdone\n>  '\n>  \n> +test_expect_success 'alias without value reports error' '\n> +\ttest_when_finished \"git config --unset alias.noval\" &&\n> +\tcat >>.git/config <<-\\EOF &&\n> +\t[alias]\n> +\t\tnoval\n> +\tEOF\n> +\ttest_must_fail git noval 2>error &&\n> +\ttest_grep \"alias.noval\" error\n> +'\n> +\n>  test_done\n"},{"id":"535852","messageId":"20260212102711.GA17587@tb-raspi4","threadId":"64949","inReplyTo":"20260211211810.278806-1-jonatan@jontes.page","subject":"Re: [PATCH v4 0/3] support UTF-8 in alias names","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2026-02-12T10:27:12Z","receivedAt":"2026-02-12T10:27:33Z","isPatch":true,"sender":{"key":"tboegi@web.de","avatar":"https://avatars.githubusercontent.com/u/7138363?v=4"},"body":"On Wed, Feb 11, 2026 at 10:18:07PM +0100, Jonatan Holmgren wrote:\n[]\nIs it only me who only sees the header message, the 0/3, and not\nthe patches themselves, the 1/3..3/3 ?\n"},{"id":"535857","messageId":"GV2PPF54105D07420C30078862ADC8F64109C60A@GV2PPF54105D074.eurprd02.prod.outlook.com","threadId":"64949","inReplyTo":"20260211211810.278806-4-jonatan@jontes.page","subject":"RE: [PATCH v4 3/3] alias: support non-alphanumeric names via subsection syntax","fromName":"Richard Kerry","fromEmail":"richard.kerry@atos.net","sentAt":"2026-02-12T11:16:00Z","receivedAt":"2026-02-12T11:21:13Z","isPatch":true,"sender":{"key":"richard.kerry@atos.net","avatar":null},"body":"> -----Original Message-----\n> From: Jonatan Holmgren <jonatan@jontes.page>\n> Sent: 11 February 2026 21:18\n> \n> Git alias names are limited to ASCII alphanumeric characters and dashes because\n> aliases are implemented as config variable names.\n> This prevents non-English speakers from creating aliases in their native languages.\n\nI think that is overly specific as a use case and should probably be more like:\n\nThis prevents aliases being created in languages using characters outside that range.\n\n1.  It isn't specific to non-English speakers - as an English speaker I may wish to create an alias in a foreign language requiring characters outside that range.\n2.  There are other languages which can be expressed entirely using characters in this range, so their speakers are not inconvenienced by the current implementation (I believe Dutch is one such language).\n\nRegards,\nRichard.\n\n\n"},{"id":"535869","messageId":"d25db846-c92a-4ace-acb2-0b77fc994b4d@jontes.page","threadId":"64949","inReplyTo":"20260212102711.GA17587@tb-raspi4","subject":"Re: [PATCH v4 0/3] support UTF-8 in alias names","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-12T15:35:44Z","receivedAt":"2026-02-12T15:35:50Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Hello!\n\nYou should to see the patch from the public inbox at\nhttps://lore.kernel.org/git/20260209135917.GD27241@macsyma.lan/T/#mfb25a7f945ceebb6c8619680cac46c990e4ea112\n\nBest,\nJonatan\n\nOn 2026-02-12 11:27, Torsten Bögershausen wrote:\n> On Wed, Feb 11, 2026 at 10:18:07PM +0100, Jonatan Holmgren wrote:\n> []\n> Is it only me who only sees the header message, the 0/3, and not\n> the patches themselves, the 1/3..3/3 ?\n\n"},{"id":"535870","messageId":"27be3e00-ec44-46d8-a9d7-44160c1cf1b0@jontes.page","threadId":"64949","inReplyTo":"GV2PPF54105D07420C30078862ADC8F64109C60A@GV2PPF54105D074.eurprd02.prod.outlook.com","subject":"Re: [PATCH v4 3/3] alias: support non-alphanumeric names via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-12T15:34:16Z","receivedAt":"2026-02-12T15:40:50Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Hi Richard,\n\nGood point regarding the distinction between the human and the character \nset.\n\nI did actually consider that distinction when writing it, but I \nstruggled to find a phrasing that didn't feel clunky, which is why I \nsettled on the version I sent. However, I agree that your suggestion is \nmore technically accurate.\n\nI'll adopt your wording in the next version. Thanks for the review!\n\nBest,\nJonatan\n"},{"id":"535879","messageId":"ed066558-a846-4c72-894f-f29b4f911589@jontes.page","threadId":"64949","inReplyTo":"20260211211810.278806-4-jonatan@jontes.page","subject":"Re: [PATCH v4 3/3] alias: support non-alphanumeric names via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-12T18:52:45Z","receivedAt":"2026-02-12T18:52:57Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"I have discovered that shell completion suggests the \"<alias>.command\" \nwhich is a bug. Can't believe I didn't test that. Fixing in revision 5.\n\nJonatan\n"},{"id":"536119","messageId":"20260216161513.2533141-1-jonatan@jontes.page","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"[PATCH v5 0/4] support uTF-8 in alias names","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-16T16:15:09Z","receivedAt":"2026-02-16T16:15:42Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Heya!\n\nThis series enables UTF-8 and special characters in Git alias names\nby using config subsection syntax: [alias \"name\"] command = value.\n\nI have since last time:\n* Changed \"native speakers\" to \"languages outside English range\" (thanks Richard!)\n* Fixed shell completion in zsh\n* Improved docs slightly with feedback from Junio. Also changed out one of the examples with one with spaces to demonstrate that feature.\n\nThanks for your time.\n\nJonatan Holmgren (4):\n  help: use list_aliases() for alias listing\n  alias: prepare for subsection aliases\n  alias: support non-alphanumeric names via subsection syntax\n  completion: fix zsh alias listing for subsection aliases\n\n Documentation/config/alias.adoc       | 50 ++++++++++++++++---\n alias.c                               | 42 +++++++++++++---\n builtin/help.c                        | 15 ++++++\n contrib/completion/git-completion.zsh |  2 +-\n help.c                                | 39 +++++++--------\n t/t0014-alias.sh                      | 71 +++++++++++++++++++++++++++\n 6 files changed, 184 insertions(+), 35 deletions(-)\n\n-- \n2.53.0.83.g660bbd62ee.dirty\n\n"},{"id":"536120","messageId":"20260216161513.2533141-5-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260216161513.2533141-1-jonatan@jontes.page","subject":"[PATCH v5 4/4] completion: fix zsh alias listing for subsection aliases","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-16T16:15:13Z","receivedAt":"2026-02-16T16:15:42Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"The zsh completion function __git_zsh_cmd_alias() uses 'git config\n--get-regexp' to enumerate aliases and then strips the \"alias.\" prefix\nfrom each key. For subsection-style aliases (alias.name.command), this\nleaves \"name.command\" as the completion candidate instead of just\n\"name\".\n\nThe bash completion does not have this problem because it goes through\n'git --list-cmds=alias', which calls list_aliases() in C and already\nhandles both alias syntaxes correctly. However, zsh needs both the\nalias name and its value for descriptive completion, which\n--list-cmds=alias does not provide.\n\nAdd a hidden --aliases-for-completion option to 'git help', following\nthe existing --config-for-completion pattern. It outputs NUL-separated\n\"name\\nvalue\" pairs using list_aliases(), which correctly resolves both\nthe traditional (alias.name) and subsection (alias.name.command)\nformats. Update __git_zsh_cmd_alias() to use it.\n---\n builtin/help.c                        | 15 +++++++++++++++\n contrib/completion/git-completion.zsh |  2 +-\n 2 files changed, 16 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/help.c b/builtin/help.c\nindex c09cbc8912..f02308a391 100644\n--- a/builtin/help.c\n+++ b/builtin/help.c\n@@ -54,6 +54,7 @@ static enum help_action {\n \tHELP_ACTION_DEVELOPER_INTERFACES,\n \tHELP_ACTION_CONFIG_FOR_COMPLETION,\n \tHELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION,\n+\tHELP_ACTION_ALIASES_FOR_COMPLETION,\n } cmd_mode;\n \n static char *html_path;\n@@ -90,6 +91,8 @@ static struct option builtin_help_options[] = {\n \t\t    HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n \tOPT_CMDMODE_F(0, \"config-sections-for-completion\", &cmd_mode, \"\",\n \t\t    HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"aliases-for-completion\", &cmd_mode, \"\",\n+\t\t    HELP_ACTION_ALIASES_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n \n \tOPT_END(),\n };\n@@ -691,6 +694,18 @@ int cmd_help(int argc,\n \t\t\t       help_format);\n \t\tlist_config_help(SHOW_CONFIG_SECTIONS);\n \t\treturn 0;\n+\tcase HELP_ACTION_ALIASES_FOR_COMPLETION: {\n+\t\tstruct string_list alias_list = STRING_LIST_INIT_DUP;\n+\t\topt_mode_usage(argc, \"--aliases-for-completion\",\n+\t\t\t       help_format);\n+\t\tlist_aliases(&alias_list);\n+\t\tstring_list_sort(&alias_list);\n+\t\tfor (size_t i = 0; i < alias_list.nr; i++)\n+\t\t\tprintf(\"%s%c%s%c\", alias_list.items[i].string, '\\n',\n+\t\t\t       (char *)alias_list.items[i].util, '\\0');\n+\t\tstring_list_clear(&alias_list, 1);\n+\t\treturn 0;\n+\t}\n \tcase HELP_ACTION_CONFIG:\n \t\topt_mode_usage(argc, \"--config\", help_format);\n \t\tsetup_pager(the_repository);\ndiff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh\nindex f5877bd7a1..c32186a977 100644\n--- a/contrib/completion/git-completion.zsh\n+++ b/contrib/completion/git-completion.zsh\n@@ -202,7 +202,7 @@ __git_zsh_cmd_common ()\n __git_zsh_cmd_alias ()\n {\n \tlocal -a list\n-\tlist=(${${(0)\"$(git config -z --get-regexp '^alias\\.*')\"}#alias.})\n+\tlist=(${(0)\"$(git help --aliases-for-completion)\"})\n \tlist=(${(f)\"$(printf \"%s:alias for '%s'\\n\" ${(f@)list})\"})\n \t_describe -t alias-commands 'aliases' list && _ret=0\n }\n-- \n2.53.0.83.g660bbd62ee.dirty\n\n"},{"id":"536121","messageId":"20260216161513.2533141-3-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260216161513.2533141-1-jonatan@jontes.page","subject":"[PATCH v5 2/4] alias: prepare for subsection aliases","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-16T16:15:11Z","receivedAt":"2026-02-16T16:15:43Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Switch git_unknown_cmd_config() from skip_prefix() to\nparse_config_key() for alias parsing. This properly handles the\nthree-level config key structure and prepares for the new\nalias.*.command subsection syntax in the next commit.\n\nThis is a compatibility break: the alias configuration parser used\nto be overly permissive and accepted \"alias.<subsection>.<key>\" as\ndefining an alias \"<subsection>.<key>\". With this change,\nalias.<subsection>.<key> entries are silently ignored (unless <key>\nis \"command\", which will be given meaning in the next commit).\n\nThis behavior was arguably a bug, since config subsections were never\nintended to work this way for aliases, and aliases with dots in their\nnames have never been documented or intentionally supported.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n help.c | 10 +++++++---\n 1 file changed, 7 insertions(+), 3 deletions(-)\n\ndiff --git a/help.c b/help.c\nindex 5b1b320d02..691af219bf 100644\n--- a/help.c\n+++ b/help.c\n@@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t\t\t\t  void *cb)\n {\n \tstruct help_unknown_cmd_config *cfg = cb;\n-\tconst char *p;\n+\tconst char *subsection, *key;\n+\tsize_t subsection_len;\n \n \tif (!strcmp(var, \"help.autocorrect\")) {\n \t\tint v = parse_autocorrect(value);\n@@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t}\n \n \t/* Also use aliases for command lookup */\n-\tif (skip_prefix(var, \"alias.\", &p))\n-\t\tadd_cmdname(&cfg->aliases, p, strlen(p));\n+\tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n+\t\t\t      &key)) {\n+\t\tif (!subsection)\n+\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t}\n \n \treturn 0;\n }\n-- \n2.53.0.83.g660bbd62ee.dirty\n\n"},{"id":"536122","messageId":"20260216161513.2533141-2-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260216161513.2533141-1-jonatan@jontes.page","subject":"[PATCH v5 1/4] help: use list_aliases() for alias listing","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-16T16:15:10Z","receivedAt":"2026-02-16T16:15:43Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"help.c has its own get_alias() config callback that duplicates the\nparsing logic in alias.c. Consolidate by teaching list_aliases() to\nalso store the alias values (via the string_list util field), then\nuse it in list_all_cmds_help_aliases() instead of the private\ncallback.\n\nThis preserves the existing error checking for value-less alias\ndefinitions by checking in alias.c rather than help.c.\n\nNo functional change intended.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n alias.c          |  8 +++++++-\n help.c           | 17 ++---------------\n t/t0014-alias.sh | 10 ++++++++++\n 3 files changed, 19 insertions(+), 16 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 1a1a141a0a..271acb9bf1 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -29,7 +29,13 @@ static int config_alias_cb(const char *key, const char *value,\n \t\t\t\t\t\t key, value);\n \t\t}\n \t} else if (data->list) {\n-\t\tstring_list_append(data->list, p);\n+\t\tstruct string_list_item *item;\n+\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(key);\n+\n+\t\titem = string_list_append(data->list, p);\n+\t\titem->util = xstrdup(value);\n \t}\n \n \treturn 0;\ndiff --git a/help.c b/help.c\nindex 08b5c60204..5b1b320d02 100644\n--- a/help.c\n+++ b/help.c\n@@ -20,6 +20,7 @@\n #include \"prompt.h\"\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n+#include \"alias.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)\n \tputchar('\\n');\n }\n \n-static int get_alias(const char *var, const char *value,\n-\t\t     const struct config_context *ctx UNUSED, void *data)\n-{\n-\tstruct string_list *list = data;\n-\n-\tif (skip_prefix(var, \"alias.\", &var)) {\n-\t\tif (!value)\n-\t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(list, var)->util = xstrdup(value);\n-\t}\n-\n-\treturn 0;\n-}\n-\n static void list_all_cmds_help_external_commands(void)\n {\n \tstruct string_list others = STRING_LIST_INIT_DUP;\n@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstruct cmdname_help *aliases;\n \tint i;\n \n-\trepo_config(the_repository, get_alias, &alias_list);\n+\tlist_aliases(&alias_list);\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex 07a53e7366..a13d2be8ca 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -112,4 +112,14 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '\n \tdone\n '\n \n+test_expect_success 'alias without value reports error' '\n+\ttest_when_finished \"git config --unset alias.noval\" &&\n+\tcat >>.git/config <<-\\EOF &&\n+\t[alias]\n+\t\tnoval\n+\tEOF\n+\ttest_must_fail git noval 2>error &&\n+\ttest_grep \"alias.noval\" error\n+'\n+\n test_done\n-- \n2.53.0.83.g660bbd62ee.dirty\n\n"},{"id":"536123","messageId":"20260216161513.2533141-4-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260216161513.2533141-1-jonatan@jontes.page","subject":"[PATCH v5 3/4] alias: support non-alphanumeric names via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-16T16:15:12Z","receivedAt":"2026-02-16T16:15:45Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Git alias names are limited to ASCII alphanumeric characters and\ndashes because aliases are implemented as config variable names.\nThis prevents aliases being created in languages using characters outside that range.\n\nAdd support for arbitrary alias names by using config subsections:\n\n    [alias \"förgrena\"]\n        command = branch\n\nThe subsection name is matched as-is (case-sensitive byte comparison),\nwhile the existing definition without a subsection (e.g.,\n\"[alias] co = checkout\") remains case-insensitive for backward\ncompatibility. This uses existing config infrastructure since\nsubsections already support arbitrary bytes, and avoids introducing\nUnicode normalization.\n\nAlso teach the help subsystem about the new syntax so that \"git help\n-a\" properly lists subsection aliases and the autocorrect feature can\nsuggest them. Use utf8_strwidth() instead of strlen() for column\nalignment so that non-ASCII alias names display correctly.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n Documentation/config/alias.adoc | 50 ++++++++++++++++++++++-----\n alias.c                         | 38 ++++++++++++++++----\n help.c                          | 14 ++++++--\n t/t0014-alias.sh                | 61 +++++++++++++++++++++++++++++++++\n 4 files changed, 145 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\nindex 80ce17d2de..09a6499249 100644\n--- a/Documentation/config/alias.adoc\n+++ b/Documentation/config/alias.adoc\n@@ -1,12 +1,46 @@\n alias.*::\n-\tCommand aliases for the linkgit:git[1] command wrapper - e.g.\n-\tafter defining `alias.last = cat-file commit HEAD`, the invocation\n-\t`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n-\tconfusion and troubles with script usage, aliases that\n-\thide existing Git commands are ignored except for deprecated\n-\tcommands.  Arguments are split by\n-\tspaces, the usual shell quoting and escaping are supported.\n-\tA quote pair or a backslash can be used to quote them.\n+alias.*.command::\n+\tCommand aliases for the linkgit:git[1] command wrapper. Aliases\n+\tcan be defined using two syntaxes:\n++\n+--\n+1. Without a subsection, e.g., `[alias] co = checkout`. The alias\n+   name (\"co\" in this example) is \n+   limited to ASCII alphanumeric characters and `-`,\n+   and is matched case-insensitively.\n+2. With a subsection, e.g., `[alias \"co\"] command = checkout`. The\n+   alias name can contain any characters (except for newlines and NUL bytes), \n+   including UTF-8, and is matched case-sensitively as raw bytes.\n+   You define the action of the alias in the `command`.\n+--\n++\n+Examples:\n++\n+----\n+# Without subsection (ASCII alphanumeric and dash only)\n+[alias]\n+    co = checkout\n+    st = status\n+\n+# With subsection (allows any characters, including UTF-8)\n+[alias \"hämta\"]\n+    command = fetch\n+[alias \"rätta till\"]\n+    command = commit --amend\n+----\n++\n+With a Git alias defined, e.g.,\n+\n+    $ git config --global alias.last \"cat-file commit HEAD\"\n+    # Which is equivalent to\n+    $ git config --global alias.last.command \"cat-file commit HEAD\"\n+\n+`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n+confusion and troubles with script usage, aliases that\n+hide existing Git commands are ignored except for deprecated\n+commands.  Arguments are split by\n+spaces, the usual shell quoting and escaping are supported.\n+A quote pair or a backslash can be used to quote them.\n +\n Note that the first word of an alias does not necessarily have to be a\n command. It can be a command-line option that will be passed into the\ndiff --git a/alias.c b/alias.c\nindex 271acb9bf1..0d636278bc 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -13,28 +13,52 @@ struct config_alias_data {\n \tstruct string_list *list;\n };\n \n-static int config_alias_cb(const char *key, const char *value,\n+static int config_alias_cb(const char *var, const char *value,\n \t\t\t   const struct config_context *ctx UNUSED, void *d)\n {\n \tstruct config_alias_data *data = d;\n-\tconst char *p;\n+\tconst char *subsection, *key;\n+\tsize_t subsection_len;\n \n-\tif (!skip_prefix(key, \"alias.\", &p))\n+\tif (parse_config_key(var, \"alias\", &subsection, &subsection_len,\n+\t\t\t     &key) < 0)\n+\t\treturn 0;\n+\n+\t/*\n+\t * Two config syntaxes:\n+\t * - alias.name = value   (without subsection, case-insensitive)\n+\t * - [alias \"name\"]\n+\t *       command = value  (with subsection, case-sensitive)\n+\t */\n+\tif (subsection && strcmp(key, \"command\"))\n \t\treturn 0;\n \n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias)) {\n+\t\tint match;\n+\n+\t\tif (subsection)\n+\t\t\tmatch = (strlen(data->alias) == subsection_len &&\n+\t\t\t\t !strncmp(data->alias, subsection,\n+\t\t\t\t\t  subsection_len));\n+\t\telse\n+\t\t\tmatch = !strcasecmp(data->alias, key);\n+\n+\t\tif (match) {\n \t\t\tFREE_AND_NULL(data->v);\n \t\t\treturn git_config_string(&data->v,\n-\t\t\t\t\t\t key, value);\n+\t\t\t\t\t\t var, value);\n \t\t}\n \t} else if (data->list) {\n \t\tstruct string_list_item *item;\n \n \t\tif (!value)\n-\t\t\treturn config_error_nonbool(key);\n+\t\t\treturn config_error_nonbool(var);\n \n-\t\titem = string_list_append(data->list, p);\n+\t\tif (subsection)\n+\t\t\titem = string_list_append_nodup(data->list,\n+\t\t\t\txmemdupz(subsection, subsection_len));\n+\t\telse\n+\t\t\titem = string_list_append(data->list, key);\n \t\titem->util = xstrdup(value);\n \t}\n \ndiff --git a/help.c b/help.c\nindex 691af219bf..95f576c5c8 100644\n--- a/help.c\n+++ b/help.c\n@@ -21,6 +21,7 @@\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n #include \"alias.h\"\n+#include \"utf8.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,\n \n \tfor (i = 0; cmds[i].name; i++) {\n \t\tif (cmds[i].category & mask) {\n-\t\t\tsize_t len = strlen(cmds[i].name);\n+\t\t\tsize_t len = utf8_strwidth(cmds[i].name);\n \t\t\tprintf(\"   %s   \", cmds[i].name);\n \t\t\tif (longest > len)\n \t\t\t\tmput_char(' ', longest - len);\n@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\n-\t\tsize_t len = strlen(alias_list.items[i].string);\n+\t\tsize_t len = utf8_strwidth(alias_list.items[i].string);\n \t\tif (longest < len)\n \t\t\tlongest = len;\n \t}\n@@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n-\t\tif (!subsection)\n+\t\tif (subsection) {\n+\t\t\t/* [alias \"name\"] command = value */\n+\t\t\tif (!strcmp(key, \"command\"))\n+\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\t\t    subsection_len);\n+\t\t} else {\n+\t\t\t/* alias.name = value */\n \t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t}\n \t}\n \n \treturn 0;\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex a13d2be8ca..34bbdb51c5 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -122,4 +122,65 @@ test_expect_success 'alias without value reports error' '\n \ttest_grep \"alias.noval\" error\n '\n \n+test_expect_success 'subsection syntax works' '\n+\ttest_config alias.testnew.command \"!echo ran-subsection\" &&\n+\tgit testnew >output &&\n+\ttest_grep \"ran-subsection\" output\n+'\n+\n+test_expect_success 'subsection syntax only accepts command key' '\n+\ttest_config alias.invalid.notcommand value &&\n+\ttest_must_fail git invalid 2>error &&\n+\ttest_grep -i \"not a git command\" error\n+'\n+\n+test_expect_success 'subsection syntax requires value for command' '\n+\ttest_when_finished \"git config --remove-section alias.noval\" &&\n+\tcat >>.git/config <<-\\EOF &&\n+\t[alias \"noval\"]\n+\t\tcommand\n+\tEOF\n+\ttest_must_fail git noval 2>error &&\n+\ttest_grep \"alias.noval.command\" error\n+'\n+\n+test_expect_success 'simple syntax is case-insensitive' '\n+\ttest_config alias.LegacyCase \"!echo ran-legacy\" &&\n+\tgit legacycase >output &&\n+\ttest_grep \"ran-legacy\" output\n+'\n+\n+test_expect_success 'subsection syntax is case-sensitive' '\n+\ttest_config alias.SubCase.command \"!echo ran-upper\" &&\n+\ttest_config alias.subcase.command \"!echo ran-lower\" &&\n+\tgit SubCase >upper.out &&\n+\tgit subcase >lower.out &&\n+\ttest_grep \"ran-upper\" upper.out &&\n+\ttest_grep \"ran-lower\" lower.out\n+'\n+\n+test_expect_success 'UTF-8 alias with Swedish characters' '\n+\ttest_config alias.\"förgrena\".command \"!echo ran-swedish\" &&\n+\tgit förgrena >output &&\n+\ttest_grep \"ran-swedish\" output\n+'\n+\n+test_expect_success 'UTF-8 alias with CJK characters' '\n+\ttest_config alias.\"分支\".command \"!echo ran-cjk\" &&\n+\tgit 分支 >output &&\n+\ttest_grep \"ran-cjk\" output\n+'\n+\n+test_expect_success 'alias with spaces in name' '\n+\ttest_config alias.\"test name\".command \"!echo ran-spaces\" &&\n+\tgit \"test name\" >output &&\n+\ttest_grep \"ran-spaces\" output\n+'\n+\n+test_expect_success 'subsection aliases listed in help -a' '\n+\ttest_config alias.\"förgrena\".command \"!echo test\" &&\n+\tgit help -a >output &&\n+\ttest_grep \"förgrena\" output\n+'\n+\n test_done\n-- \n2.53.0.83.g660bbd62ee.dirty\n\n"},{"id":"536133","messageId":"CALnO6CD3QpEZ=689w3mQpc5Hx0bj3+E8gU-JhstYJ9XOvunxiQ@mail.gmail.com","threadId":"64949","inReplyTo":"20260216161513.2533141-5-jonatan@jontes.page","subject":"Re: [PATCH v5 4/4] completion: fix zsh alias listing for subsection aliases","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-02-16T18:32:43Z","receivedAt":"2026-02-16T18:32:54Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Mon, Feb 16, 2026 at 11:15 AM Jonatan Holmgren <jonatan@jontes.page> wrote:\n>\n> The zsh completion function __git_zsh_cmd_alias() uses 'git config\n> --get-regexp' to enumerate aliases and then strips the \"alias.\" prefix\n> from each key. For subsection-style aliases (alias.name.command), this\n> leaves \"name.command\" as the completion candidate instead of just\n> \"name\".\n>\n> The bash completion does not have this problem because it goes through\n> 'git --list-cmds=alias', which calls list_aliases() in C and already\n> handles both alias syntaxes correctly. However, zsh needs both the\n> alias name and its value for descriptive completion, which\n> --list-cmds=alias does not provide.\n>\n> Add a hidden --aliases-for-completion option to 'git help', following\n> the existing --config-for-completion pattern. It outputs NUL-separated\n> \"name\\nvalue\" pairs using list_aliases(), which correctly resolves both\n> the traditional (alias.name) and subsection (alias.name.command)\n> formats. Update __git_zsh_cmd_alias() to use it.\n\nAn alternative would to be to post-process and turn\n\"alias.name.command\" into \"name\", but this solution is easier to parse\n(won't mix 2 formats).\n\n> ---\n>  builtin/help.c                        | 15 +++++++++++++++\n>  contrib/completion/git-completion.zsh |  2 +-\n>  2 files changed, 16 insertions(+), 1 deletion(-)\n>\n> diff --git a/builtin/help.c b/builtin/help.c\n> index c09cbc8912..f02308a391 100644\n> --- a/builtin/help.c\n> +++ b/builtin/help.c\n> @@ -54,6 +54,7 @@ static enum help_action {\n>         HELP_ACTION_DEVELOPER_INTERFACES,\n>         HELP_ACTION_CONFIG_FOR_COMPLETION,\n>         HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION,\n> +       HELP_ACTION_ALIASES_FOR_COMPLETION,\n>  } cmd_mode;\n>\n>  static char *html_path;\n> @@ -90,6 +91,8 @@ static struct option builtin_help_options[] = {\n>                     HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n>         OPT_CMDMODE_F(0, \"config-sections-for-completion\", &cmd_mode, \"\",\n>                     HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n> +       OPT_CMDMODE_F(0, \"aliases-for-completion\", &cmd_mode, \"\",\n> +                   HELP_ACTION_ALIASES_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n>\n>         OPT_END(),\n>  };\n> @@ -691,6 +694,18 @@ int cmd_help(int argc,\n>                                help_format);\n>                 list_config_help(SHOW_CONFIG_SECTIONS);\n>                 return 0;\n> +       case HELP_ACTION_ALIASES_FOR_COMPLETION: {\n> +               struct string_list alias_list = STRING_LIST_INIT_DUP;\n> +               opt_mode_usage(argc, \"--aliases-for-completion\",\n> +                              help_format);\n\nstyle nit: I _think_ this line could be unwrapped?\n\n> +               list_aliases(&alias_list);\n> +               string_list_sort(&alias_list);\n\nIt may not matter much since lists of aliases are probably not\nhumongous, but do we need to sort? I don't _think_ \"git\n--list-cmds=alias\" does, for example, though I'm not sure about \"git\nconfig --get-regexp\" that's being replaced. Leaving it in the config\norder could even be a feature, so I can put easier-to-complete aliases\nearlier in my config?\n\n> +               for (size_t i = 0; i < alias_list.nr; i++)\n> +                       printf(\"%s%c%s%c\", alias_list.items[i].string, '\\n',\n> +                              (char *)alias_list.items[i].util, '\\0');\n> +               string_list_clear(&alias_list, 1);\n> +               return 0;\n> +       }\n>         case HELP_ACTION_CONFIG:\n>                 opt_mode_usage(argc, \"--config\", help_format);\n>                 setup_pager(the_repository);\n\n\n> diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh\n> index f5877bd7a1..c32186a977 100644\n> --- a/contrib/completion/git-completion.zsh\n> +++ b/contrib/completion/git-completion.zsh\n> @@ -202,7 +202,7 @@ __git_zsh_cmd_common ()\n>  __git_zsh_cmd_alias ()\n>  {\n>         local -a list\n> -       list=(${${(0)\"$(git config -z --get-regexp '^alias\\.*')\"}#alias.})\n> +       list=(${(0)\"$(git help --aliases-for-completion)\"})\n>         list=(${(f)\"$(printf \"%s:alias for '%s'\\n\" ${(f@)list})\"})\n>         _describe -t alias-commands 'aliases' list && _ret=0\n>  }\n> --\n> 2.53.0.83.g660bbd62ee.dirty\n\nThe Zsh hunk looks straightforwardly correct, especially since the\noutput format of the new help-mode is unchanged, thanks.\n\n-- \nD. Ben Knoble\n"},{"id":"536221","messageId":"xmqqecmjb0mx.fsf@gitster.g","threadId":"64949","inReplyTo":"20260216161513.2533141-5-jonatan@jontes.page","subject":"Re: [PATCH v5 4/4] completion: fix zsh alias listing for subsection aliases","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-17T20:01:10Z","receivedAt":"2026-02-17T20:01:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> The zsh completion function __git_zsh_cmd_alias() uses 'git config\n> --get-regexp' to enumerate aliases and then strips the \"alias.\" prefix\n> from each key. For subsection-style aliases (alias.name.command), this\n> leaves \"name.command\" as the completion candidate instead of just\n> \"name\".\n>\n> The bash completion does not have this problem because it goes through\n> 'git --list-cmds=alias', which calls list_aliases() in C and already\n> handles both alias syntaxes correctly. However, zsh needs both the\n> alias name and its value for descriptive completion, which\n> --list-cmds=alias does not provide.\n\nOK.  It is a natural question to ask why it is insufficient to use\nwhat bash side happily uses, and the above gives a good explanation.\n\n> Add a hidden --aliases-for-completion option to 'git help', following\n> the existing --config-for-completion pattern. It outputs NUL-separated\n> \"name\\nvalue\" pairs using list_aliases(), which correctly resolves both\n> the traditional (alias.name) and subsection (alias.name.command)\n> formats. Update __git_zsh_cmd_alias() to use it.\n\nSeeing that \"--config-for-completion\" is used for bash, I wonder if\nit would be a good follow-up topic to rewrite the bash completion to\nalso use \"--aliases-for-completion\" (and possibly drop the need to\nsupport \"git --list-cmds=alias\" command).  But that is clearly\noutside the scope of this topic.\n\n> ---\n\nMissing sign-off.\n\n>  builtin/help.c                        | 15 +++++++++++++++\n>  contrib/completion/git-completion.zsh |  2 +-\n>  2 files changed, 16 insertions(+), 1 deletion(-)\n>\n> diff --git a/builtin/help.c b/builtin/help.c\n> index c09cbc8912..f02308a391 100644\n> --- a/builtin/help.c\n> +++ b/builtin/help.c\n> @@ -54,6 +54,7 @@ static enum help_action {\n>  \tHELP_ACTION_DEVELOPER_INTERFACES,\n>  \tHELP_ACTION_CONFIG_FOR_COMPLETION,\n>  \tHELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION,\n> +\tHELP_ACTION_ALIASES_FOR_COMPLETION,\n>  } cmd_mode;\n>  \n>  static char *html_path;\n> @@ -90,6 +91,8 @@ static struct option builtin_help_options[] = {\n>  \t\t    HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n>  \tOPT_CMDMODE_F(0, \"config-sections-for-completion\", &cmd_mode, \"\",\n>  \t\t    HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n> +\tOPT_CMDMODE_F(0, \"aliases-for-completion\", &cmd_mode, \"\",\n> +\t\t    HELP_ACTION_ALIASES_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n>  \n>  \tOPT_END(),\n>  };\n> @@ -691,6 +694,18 @@ int cmd_help(int argc,\n>  \t\t\t       help_format);\n>  \t\tlist_config_help(SHOW_CONFIG_SECTIONS);\n>  \t\treturn 0;\n> +\tcase HELP_ACTION_ALIASES_FOR_COMPLETION: {\n> +\t\tstruct string_list alias_list = STRING_LIST_INIT_DUP;\n> +\t\topt_mode_usage(argc, \"--aliases-for-completion\",\n> +\t\t\t       help_format);\n> +\t\tlist_aliases(&alias_list);\n> +\t\tstring_list_sort(&alias_list);\n> +\t\tfor (size_t i = 0; i < alias_list.nr; i++)\n> +\t\t\tprintf(\"%s%c%s%c\", alias_list.items[i].string, '\\n',\n> +\t\t\t       (char *)alias_list.items[i].util, '\\0');\n> +\t\tstring_list_clear(&alias_list, 1);\n> +\t\treturn 0;\n> +\t}\n>  \tcase HELP_ACTION_CONFIG:\n>  \t\topt_mode_usage(argc, \"--config\", help_format);\n>  \t\tsetup_pager(the_repository);\n> diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh\n> index f5877bd7a1..c32186a977 100644\n> --- a/contrib/completion/git-completion.zsh\n> +++ b/contrib/completion/git-completion.zsh\n> @@ -202,7 +202,7 @@ __git_zsh_cmd_common ()\n>  __git_zsh_cmd_alias ()\n>  {\n>  \tlocal -a list\n> -\tlist=(${${(0)\"$(git config -z --get-regexp '^alias\\.*')\"}#alias.})\n> +\tlist=(${(0)\"$(git help --aliases-for-completion)\"})\n>  \tlist=(${(f)\"$(printf \"%s:alias for '%s'\\n\" ${(f@)list})\"})\n>  \t_describe -t alias-commands 'aliases' list && _ret=0\n>  }\n"},{"id":"536295","messageId":"20260218145214.581460-1-jonatan@jontes.page","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"[PATCH v6 0/4] support UTF-8 in alias names","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-18T14:52:10Z","receivedAt":"2026-02-18T14:52:47Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"This series enables UTF-8 and special characters in Git alias names\nby using config subsection syntax: [alias \"name\"] command = value.\n\nI have since last time:\n- Unwrapped opt_mode_usage() call in builtin/help.c for consistency\n- Removed string_list_sort() to preserve config order as suggested by Ben\n- Added missing Signed-off-by trailer to patch 4 (oops)\n- Fixed --aliases-for-completion output format to use NUL\n  seperators instead of newlines (as per my own commit message and intention)\n\nThanks again.\n\nJonatan Holmgren (4):\n  help: use list_aliases() for alias listing\n  alias: prepare for subsection aliases\n  alias: support non-alphanumeric names via subsection syntax\n  completion: fix zsh alias listing for subsection aliases\n\n Documentation/config/alias.adoc       | 50 ++++++++++++++++---\n alias.c                               | 42 +++++++++++++---\n builtin/help.c                        | 13 +++++\n contrib/completion/git-completion.zsh |  2 +-\n help.c                                | 39 +++++++--------\n t/t0014-alias.sh                      | 71 +++++++++++++++++++++++++++\n 6 files changed, 182 insertions(+), 35 deletions(-)\n\n-- \n2.53.0.122.g591c997fb5.dirty\n\n"},{"id":"536296","messageId":"20260218145214.581460-3-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260218145214.581460-1-jonatan@jontes.page","subject":"[PATCH v6 2/4] alias: prepare for subsection aliases","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-18T14:52:12Z","receivedAt":"2026-02-18T14:52:48Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Switch git_unknown_cmd_config() from skip_prefix() to\nparse_config_key() for alias parsing. This properly handles the\nthree-level config key structure and prepares for the new\nalias.*.command subsection syntax in the next commit.\n\nThis is a compatibility break: the alias configuration parser used\nto be overly permissive and accepted \"alias.<subsection>.<key>\" as\ndefining an alias \"<subsection>.<key>\". With this change,\nalias.<subsection>.<key> entries are silently ignored (unless <key>\nis \"command\", which will be given meaning in the next commit).\n\nThis behavior was arguably a bug, since config subsections were never\nintended to work this way for aliases, and aliases with dots in their\nnames have never been documented or intentionally supported.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n help.c | 10 +++++++---\n 1 file changed, 7 insertions(+), 3 deletions(-)\n\ndiff --git a/help.c b/help.c\nindex 5b1b320d02..691af219bf 100644\n--- a/help.c\n+++ b/help.c\n@@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t\t\t\t  void *cb)\n {\n \tstruct help_unknown_cmd_config *cfg = cb;\n-\tconst char *p;\n+\tconst char *subsection, *key;\n+\tsize_t subsection_len;\n \n \tif (!strcmp(var, \"help.autocorrect\")) {\n \t\tint v = parse_autocorrect(value);\n@@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t}\n \n \t/* Also use aliases for command lookup */\n-\tif (skip_prefix(var, \"alias.\", &p))\n-\t\tadd_cmdname(&cfg->aliases, p, strlen(p));\n+\tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n+\t\t\t      &key)) {\n+\t\tif (!subsection)\n+\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t}\n \n \treturn 0;\n }\n-- \n2.53.0.122.g591c997fb5.dirty\n\n"},{"id":"536299","messageId":"20260218145214.581460-2-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260218145214.581460-1-jonatan@jontes.page","subject":"[PATCH v6 1/4] help: use list_aliases() for alias listing","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-18T14:52:11Z","receivedAt":"2026-02-18T14:52:48Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"help.c has its own get_alias() config callback that duplicates the\nparsing logic in alias.c. Consolidate by teaching list_aliases() to\nalso store the alias values (via the string_list util field), then\nuse it in list_all_cmds_help_aliases() instead of the private\ncallback.\n\nThis preserves the existing error checking for value-less alias\ndefinitions by checking in alias.c rather than help.c.\n\nNo functional change intended.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n alias.c          |  8 +++++++-\n help.c           | 17 ++---------------\n t/t0014-alias.sh | 10 ++++++++++\n 3 files changed, 19 insertions(+), 16 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 1a1a141a0a..271acb9bf1 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -29,7 +29,13 @@ static int config_alias_cb(const char *key, const char *value,\n \t\t\t\t\t\t key, value);\n \t\t}\n \t} else if (data->list) {\n-\t\tstring_list_append(data->list, p);\n+\t\tstruct string_list_item *item;\n+\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(key);\n+\n+\t\titem = string_list_append(data->list, p);\n+\t\titem->util = xstrdup(value);\n \t}\n \n \treturn 0;\ndiff --git a/help.c b/help.c\nindex 08b5c60204..5b1b320d02 100644\n--- a/help.c\n+++ b/help.c\n@@ -20,6 +20,7 @@\n #include \"prompt.h\"\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n+#include \"alias.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)\n \tputchar('\\n');\n }\n \n-static int get_alias(const char *var, const char *value,\n-\t\t     const struct config_context *ctx UNUSED, void *data)\n-{\n-\tstruct string_list *list = data;\n-\n-\tif (skip_prefix(var, \"alias.\", &var)) {\n-\t\tif (!value)\n-\t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(list, var)->util = xstrdup(value);\n-\t}\n-\n-\treturn 0;\n-}\n-\n static void list_all_cmds_help_external_commands(void)\n {\n \tstruct string_list others = STRING_LIST_INIT_DUP;\n@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstruct cmdname_help *aliases;\n \tint i;\n \n-\trepo_config(the_repository, get_alias, &alias_list);\n+\tlist_aliases(&alias_list);\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex 07a53e7366..a13d2be8ca 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -112,4 +112,14 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '\n \tdone\n '\n \n+test_expect_success 'alias without value reports error' '\n+\ttest_when_finished \"git config --unset alias.noval\" &&\n+\tcat >>.git/config <<-\\EOF &&\n+\t[alias]\n+\t\tnoval\n+\tEOF\n+\ttest_must_fail git noval 2>error &&\n+\ttest_grep \"alias.noval\" error\n+'\n+\n test_done\n-- \n2.53.0.122.g591c997fb5.dirty\n\n"},{"id":"536297","messageId":"20260218145214.581460-5-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260218145214.581460-1-jonatan@jontes.page","subject":"[PATCH v6 4/4] completion: fix zsh alias listing for subsection aliases","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-18T14:52:14Z","receivedAt":"2026-02-18T14:52:49Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"The zsh completion function __git_zsh_cmd_alias() uses 'git config\n--get-regexp' to enumerate aliases and then strips the \"alias.\" prefix\nfrom each key. For subsection-style aliases (alias.name.command), this\nleaves \"name.command\" as the completion candidate instead of just\n\"name\".\n\nThe bash completion does not have this problem because it goes through\n'git --list-cmds=alias', which calls list_aliases() in C and already\nhandles both alias syntaxes correctly. However, zsh needs both the\nalias name and its value for descriptive completion, which\n--list-cmds=alias does not provide.\n\nAdd a hidden --aliases-for-completion option to 'git help', following\nthe existing --config-for-completion pattern. It outputs NUL-separated\n\"name\\nvalue\" pairs using list_aliases(), which correctly resolves both\nthe traditional (alias.name) and subsection (alias.name.command)\nformats. Update __git_zsh_cmd_alias() to use it.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n builtin/help.c                        | 13 +++++++++++++\n contrib/completion/git-completion.zsh |  2 +-\n 2 files changed, 14 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/help.c b/builtin/help.c\nindex c09cbc8912..2db41d0b55 100644\n--- a/builtin/help.c\n+++ b/builtin/help.c\n@@ -54,6 +54,7 @@ static enum help_action {\n \tHELP_ACTION_DEVELOPER_INTERFACES,\n \tHELP_ACTION_CONFIG_FOR_COMPLETION,\n \tHELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION,\n+\tHELP_ACTION_ALIASES_FOR_COMPLETION,\n } cmd_mode;\n \n static char *html_path;\n@@ -90,6 +91,8 @@ static struct option builtin_help_options[] = {\n \t\t    HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n \tOPT_CMDMODE_F(0, \"config-sections-for-completion\", &cmd_mode, \"\",\n \t\t    HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"aliases-for-completion\", &cmd_mode, \"\",\n+\t\t    HELP_ACTION_ALIASES_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n \n \tOPT_END(),\n };\n@@ -691,6 +694,16 @@ int cmd_help(int argc,\n \t\t\t       help_format);\n \t\tlist_config_help(SHOW_CONFIG_SECTIONS);\n \t\treturn 0;\n+\tcase HELP_ACTION_ALIASES_FOR_COMPLETION: {\n+\t\tstruct string_list alias_list = STRING_LIST_INIT_DUP;\n+\t\topt_mode_usage(argc, \"--aliases-for-completion\", help_format);\n+\t\tlist_aliases(&alias_list);\n+\t\tfor (size_t i = 0; i < alias_list.nr; i++)\n+\t\t\tprintf(\"%s%c%s%c\", alias_list.items[i].string, '\\0',\n+\t\t\t       (char *)alias_list.items[i].util, '\\0');\n+\t\tstring_list_clear(&alias_list, 1);\n+\t\treturn 0;\n+\t}\n \tcase HELP_ACTION_CONFIG:\n \t\topt_mode_usage(argc, \"--config\", help_format);\n \t\tsetup_pager(the_repository);\ndiff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh\nindex f5877bd7a1..c32186a977 100644\n--- a/contrib/completion/git-completion.zsh\n+++ b/contrib/completion/git-completion.zsh\n@@ -202,7 +202,7 @@ __git_zsh_cmd_common ()\n __git_zsh_cmd_alias ()\n {\n \tlocal -a list\n-\tlist=(${${(0)\"$(git config -z --get-regexp '^alias\\.*')\"}#alias.})\n+\tlist=(${(0)\"$(git help --aliases-for-completion)\"})\n \tlist=(${(f)\"$(printf \"%s:alias for '%s'\\n\" ${(f@)list})\"})\n \t_describe -t alias-commands 'aliases' list && _ret=0\n }\n-- \n2.53.0.122.g591c997fb5.dirty\n\n"},{"id":"536298","messageId":"20260218145214.581460-4-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260218145214.581460-1-jonatan@jontes.page","subject":"[PATCH v6 3/4] alias: support non-alphanumeric names via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-18T14:52:13Z","receivedAt":"2026-02-18T14:52:49Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Git alias names are limited to ASCII alphanumeric characters and\ndashes because aliases are implemented as config variable names.\nThis prevents aliases being created in languages using characters outside that range.\n\nAdd support for arbitrary alias names by using config subsections:\n\n    [alias \"förgrena\"]\n        command = branch\n\nThe subsection name is matched as-is (case-sensitive byte comparison),\nwhile the existing definition without a subsection (e.g.,\n\"[alias] co = checkout\") remains case-insensitive for backward\ncompatibility. This uses existing config infrastructure since\nsubsections already support arbitrary bytes, and avoids introducing\nUnicode normalization.\n\nAlso teach the help subsystem about the new syntax so that \"git help\n-a\" properly lists subsection aliases and the autocorrect feature can\nsuggest them. Use utf8_strwidth() instead of strlen() for column\nalignment so that non-ASCII alias names display correctly.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n Documentation/config/alias.adoc | 50 ++++++++++++++++++++++-----\n alias.c                         | 38 ++++++++++++++++----\n help.c                          | 14 ++++++--\n t/t0014-alias.sh                | 61 +++++++++++++++++++++++++++++++++\n 4 files changed, 145 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\nindex 80ce17d2de..09a6499249 100644\n--- a/Documentation/config/alias.adoc\n+++ b/Documentation/config/alias.adoc\n@@ -1,12 +1,46 @@\n alias.*::\n-\tCommand aliases for the linkgit:git[1] command wrapper - e.g.\n-\tafter defining `alias.last = cat-file commit HEAD`, the invocation\n-\t`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n-\tconfusion and troubles with script usage, aliases that\n-\thide existing Git commands are ignored except for deprecated\n-\tcommands.  Arguments are split by\n-\tspaces, the usual shell quoting and escaping are supported.\n-\tA quote pair or a backslash can be used to quote them.\n+alias.*.command::\n+\tCommand aliases for the linkgit:git[1] command wrapper. Aliases\n+\tcan be defined using two syntaxes:\n++\n+--\n+1. Without a subsection, e.g., `[alias] co = checkout`. The alias\n+   name (\"co\" in this example) is \n+   limited to ASCII alphanumeric characters and `-`,\n+   and is matched case-insensitively.\n+2. With a subsection, e.g., `[alias \"co\"] command = checkout`. The\n+   alias name can contain any characters (except for newlines and NUL bytes), \n+   including UTF-8, and is matched case-sensitively as raw bytes.\n+   You define the action of the alias in the `command`.\n+--\n++\n+Examples:\n++\n+----\n+# Without subsection (ASCII alphanumeric and dash only)\n+[alias]\n+    co = checkout\n+    st = status\n+\n+# With subsection (allows any characters, including UTF-8)\n+[alias \"hämta\"]\n+    command = fetch\n+[alias \"rätta till\"]\n+    command = commit --amend\n+----\n++\n+With a Git alias defined, e.g.,\n+\n+    $ git config --global alias.last \"cat-file commit HEAD\"\n+    # Which is equivalent to\n+    $ git config --global alias.last.command \"cat-file commit HEAD\"\n+\n+`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n+confusion and troubles with script usage, aliases that\n+hide existing Git commands are ignored except for deprecated\n+commands.  Arguments are split by\n+spaces, the usual shell quoting and escaping are supported.\n+A quote pair or a backslash can be used to quote them.\n +\n Note that the first word of an alias does not necessarily have to be a\n command. It can be a command-line option that will be passed into the\ndiff --git a/alias.c b/alias.c\nindex 271acb9bf1..0d636278bc 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -13,28 +13,52 @@ struct config_alias_data {\n \tstruct string_list *list;\n };\n \n-static int config_alias_cb(const char *key, const char *value,\n+static int config_alias_cb(const char *var, const char *value,\n \t\t\t   const struct config_context *ctx UNUSED, void *d)\n {\n \tstruct config_alias_data *data = d;\n-\tconst char *p;\n+\tconst char *subsection, *key;\n+\tsize_t subsection_len;\n \n-\tif (!skip_prefix(key, \"alias.\", &p))\n+\tif (parse_config_key(var, \"alias\", &subsection, &subsection_len,\n+\t\t\t     &key) < 0)\n+\t\treturn 0;\n+\n+\t/*\n+\t * Two config syntaxes:\n+\t * - alias.name = value   (without subsection, case-insensitive)\n+\t * - [alias \"name\"]\n+\t *       command = value  (with subsection, case-sensitive)\n+\t */\n+\tif (subsection && strcmp(key, \"command\"))\n \t\treturn 0;\n \n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias)) {\n+\t\tint match;\n+\n+\t\tif (subsection)\n+\t\t\tmatch = (strlen(data->alias) == subsection_len &&\n+\t\t\t\t !strncmp(data->alias, subsection,\n+\t\t\t\t\t  subsection_len));\n+\t\telse\n+\t\t\tmatch = !strcasecmp(data->alias, key);\n+\n+\t\tif (match) {\n \t\t\tFREE_AND_NULL(data->v);\n \t\t\treturn git_config_string(&data->v,\n-\t\t\t\t\t\t key, value);\n+\t\t\t\t\t\t var, value);\n \t\t}\n \t} else if (data->list) {\n \t\tstruct string_list_item *item;\n \n \t\tif (!value)\n-\t\t\treturn config_error_nonbool(key);\n+\t\t\treturn config_error_nonbool(var);\n \n-\t\titem = string_list_append(data->list, p);\n+\t\tif (subsection)\n+\t\t\titem = string_list_append_nodup(data->list,\n+\t\t\t\txmemdupz(subsection, subsection_len));\n+\t\telse\n+\t\t\titem = string_list_append(data->list, key);\n \t\titem->util = xstrdup(value);\n \t}\n \ndiff --git a/help.c b/help.c\nindex 691af219bf..95f576c5c8 100644\n--- a/help.c\n+++ b/help.c\n@@ -21,6 +21,7 @@\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n #include \"alias.h\"\n+#include \"utf8.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,\n \n \tfor (i = 0; cmds[i].name; i++) {\n \t\tif (cmds[i].category & mask) {\n-\t\t\tsize_t len = strlen(cmds[i].name);\n+\t\t\tsize_t len = utf8_strwidth(cmds[i].name);\n \t\t\tprintf(\"   %s   \", cmds[i].name);\n \t\t\tif (longest > len)\n \t\t\t\tmput_char(' ', longest - len);\n@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\n-\t\tsize_t len = strlen(alias_list.items[i].string);\n+\t\tsize_t len = utf8_strwidth(alias_list.items[i].string);\n \t\tif (longest < len)\n \t\t\tlongest = len;\n \t}\n@@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n-\t\tif (!subsection)\n+\t\tif (subsection) {\n+\t\t\t/* [alias \"name\"] command = value */\n+\t\t\tif (!strcmp(key, \"command\"))\n+\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\t\t    subsection_len);\n+\t\t} else {\n+\t\t\t/* alias.name = value */\n \t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t}\n \t}\n \n \treturn 0;\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex a13d2be8ca..34bbdb51c5 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -122,4 +122,65 @@ test_expect_success 'alias without value reports error' '\n \ttest_grep \"alias.noval\" error\n '\n \n+test_expect_success 'subsection syntax works' '\n+\ttest_config alias.testnew.command \"!echo ran-subsection\" &&\n+\tgit testnew >output &&\n+\ttest_grep \"ran-subsection\" output\n+'\n+\n+test_expect_success 'subsection syntax only accepts command key' '\n+\ttest_config alias.invalid.notcommand value &&\n+\ttest_must_fail git invalid 2>error &&\n+\ttest_grep -i \"not a git command\" error\n+'\n+\n+test_expect_success 'subsection syntax requires value for command' '\n+\ttest_when_finished \"git config --remove-section alias.noval\" &&\n+\tcat >>.git/config <<-\\EOF &&\n+\t[alias \"noval\"]\n+\t\tcommand\n+\tEOF\n+\ttest_must_fail git noval 2>error &&\n+\ttest_grep \"alias.noval.command\" error\n+'\n+\n+test_expect_success 'simple syntax is case-insensitive' '\n+\ttest_config alias.LegacyCase \"!echo ran-legacy\" &&\n+\tgit legacycase >output &&\n+\ttest_grep \"ran-legacy\" output\n+'\n+\n+test_expect_success 'subsection syntax is case-sensitive' '\n+\ttest_config alias.SubCase.command \"!echo ran-upper\" &&\n+\ttest_config alias.subcase.command \"!echo ran-lower\" &&\n+\tgit SubCase >upper.out &&\n+\tgit subcase >lower.out &&\n+\ttest_grep \"ran-upper\" upper.out &&\n+\ttest_grep \"ran-lower\" lower.out\n+'\n+\n+test_expect_success 'UTF-8 alias with Swedish characters' '\n+\ttest_config alias.\"förgrena\".command \"!echo ran-swedish\" &&\n+\tgit förgrena >output &&\n+\ttest_grep \"ran-swedish\" output\n+'\n+\n+test_expect_success 'UTF-8 alias with CJK characters' '\n+\ttest_config alias.\"分支\".command \"!echo ran-cjk\" &&\n+\tgit 分支 >output &&\n+\ttest_grep \"ran-cjk\" output\n+'\n+\n+test_expect_success 'alias with spaces in name' '\n+\ttest_config alias.\"test name\".command \"!echo ran-spaces\" &&\n+\tgit \"test name\" >output &&\n+\ttest_grep \"ran-spaces\" output\n+'\n+\n+test_expect_success 'subsection aliases listed in help -a' '\n+\ttest_config alias.\"förgrena\".command \"!echo test\" &&\n+\tgit help -a >output &&\n+\ttest_grep \"förgrena\" output\n+'\n+\n test_done\n-- \n2.53.0.122.g591c997fb5.dirty\n\n"},{"id":"536304","messageId":"81e6846c-457b-438d-99b5-6412007049dd@app.fastmail.com","threadId":"64949","inReplyTo":"20260218145214.581460-3-jonatan@jontes.page","subject":"Re: [PATCH v6 2/4] alias: prepare for subsection aliases","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-18T16:21:31Z","receivedAt":"2026-02-18T16:21:52Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Feb 18, 2026, at 15:52, Jonatan Holmgren wrote:\n> Switch git_unknown_cmd_config() from skip_prefix() to\n> parse_config_key() for alias parsing. This properly handles the\n> three-level config key structure and prepares for the new\n> alias.*.command subsection syntax in the next commit.\n>\n> This is a compatibility break: the alias configuration parser used\n> to be overly permissive and accepted \"alias.<subsection>.<key>\" as\n> defining an alias \"<subsection>.<key>\". With this change,\n> alias.<subsection>.<key> entries are silently ignored (unless <key>\n> is \"command\", which will be given meaning in the next commit).\n\nUnrelated to this change. I was wondering if it makes sense to use a\ntrailer for commits that break compatibility? However unrealistic it\nmight be that the break ends up mattering in practice.\n\nAuthors might use different terms and phrases in their commit messages,\nlike\n\n• Breaking change\n• Compatibility break\n• Hysterical raisins\n\nAnd the commit message might be using these terms to describe how it\nbreaks compatibility or how it *avoids* doing so.\n\n>\n> This behavior was arguably a bug, since config subsections were never\n> intended to work this way for aliases, and aliases with dots in their\n> names have never been documented or intentionally supported.\n>\n> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n>[snip]\n"},{"id":"536344","messageId":"20260218215737.1181147-2-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260218215737.1181147-1-jonatan@jontes.page","subject":"[PATCH v7 1/4] help: use list_aliases() for alias listing","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-18T21:57:34Z","receivedAt":"2026-02-18T21:58:44Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"help.c has its own get_alias() config callback that duplicates the\nparsing logic in alias.c. Consolidate by teaching list_aliases() to\nalso store the alias values (via the string_list util field), then\nuse it in list_all_cmds_help_aliases() instead of the private\ncallback.\n\nThis preserves the existing error checking for value-less alias\ndefinitions by checking in alias.c rather than help.c.\n\nNo functional change intended.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n alias.c          |  8 +++++++-\n help.c           | 17 ++---------------\n t/t0014-alias.sh | 10 ++++++++++\n 3 files changed, 19 insertions(+), 16 deletions(-)\n\ndiff --git a/alias.c b/alias.c\nindex 1a1a141a0a..271acb9bf1 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -29,7 +29,13 @@ static int config_alias_cb(const char *key, const char *value,\n \t\t\t\t\t\t key, value);\n \t\t}\n \t} else if (data->list) {\n-\t\tstring_list_append(data->list, p);\n+\t\tstruct string_list_item *item;\n+\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(key);\n+\n+\t\titem = string_list_append(data->list, p);\n+\t\titem->util = xstrdup(value);\n \t}\n \n \treturn 0;\ndiff --git a/help.c b/help.c\nindex 08b5c60204..5b1b320d02 100644\n--- a/help.c\n+++ b/help.c\n@@ -20,6 +20,7 @@\n #include \"prompt.h\"\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n+#include \"alias.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)\n \tputchar('\\n');\n }\n \n-static int get_alias(const char *var, const char *value,\n-\t\t     const struct config_context *ctx UNUSED, void *data)\n-{\n-\tstruct string_list *list = data;\n-\n-\tif (skip_prefix(var, \"alias.\", &var)) {\n-\t\tif (!value)\n-\t\t\treturn config_error_nonbool(var);\n-\t\tstring_list_append(list, var)->util = xstrdup(value);\n-\t}\n-\n-\treturn 0;\n-}\n-\n static void list_all_cmds_help_external_commands(void)\n {\n \tstruct string_list others = STRING_LIST_INIT_DUP;\n@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstruct cmdname_help *aliases;\n \tint i;\n \n-\trepo_config(the_repository, get_alias, &alias_list);\n+\tlist_aliases(&alias_list);\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex 07a53e7366..a13d2be8ca 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -112,4 +112,14 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '\n \tdone\n '\n \n+test_expect_success 'alias without value reports error' '\n+\ttest_when_finished \"git config --unset alias.noval\" &&\n+\tcat >>.git/config <<-\\EOF &&\n+\t[alias]\n+\t\tnoval\n+\tEOF\n+\ttest_must_fail git noval 2>error &&\n+\ttest_grep \"alias.noval\" error\n+'\n+\n test_done\n-- \n2.53.0.122.g3abf75d576\n\n"},{"id":"536345","messageId":"20260218215737.1181147-1-jonatan@jontes.page","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"[PATCH v7 0/4] support UTF-8 in alias names","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-18T21:57:33Z","receivedAt":"2026-02-18T21:58:49Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Hi all!\n\nThis series adds support for non-alphanumeric (including UTF-8)\ncharacters in alias names by allowing aliases to be defined using\ngit-config's subsection syntax.\n\nChanges since v6:\nReverted the --aliases-for-completion output format to\nuse a newline as the field separator between name and value within\neach NUL-terminated record (name\\nvalue\\0). In v6 I changed this\nto use NUL for both separators, but this diverges from the\nconvention established by git config -z, which outputs\nkey\\nvalue\\0. Matching that convention is the right call here.\n\nThanks!\n\nJonatan Holmgren (4):\n  help: use list_aliases() for alias listing\n  alias: prepare for subsection aliases\n  alias: support non-alphanumeric names via subsection syntax\n  completion: fix zsh alias listing for subsection aliases\n\n Documentation/config/alias.adoc       | 50 ++++++++++++++++---\n alias.c                               | 42 +++++++++++++---\n builtin/help.c                        | 13 +++++\n contrib/completion/git-completion.zsh |  2 +-\n help.c                                | 39 +++++++--------\n t/t0014-alias.sh                      | 71 +++++++++++++++++++++++++++\n 6 files changed, 182 insertions(+), 35 deletions(-)\n\n-- \n2.53.0.122.g3abf75d576\n\n"},{"id":"536346","messageId":"20260218215737.1181147-3-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260218215737.1181147-1-jonatan@jontes.page","subject":"[PATCH v7 2/4] alias: prepare for subsection aliases","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-18T21:57:35Z","receivedAt":"2026-02-18T21:58:50Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Switch git_unknown_cmd_config() from skip_prefix() to\nparse_config_key() for alias parsing. This properly handles the\nthree-level config key structure and prepares for the new\nalias.*.command subsection syntax in the next commit.\n\nThis is a compatibility break: the alias configuration parser used\nto be overly permissive and accepted \"alias.<subsection>.<key>\" as\ndefining an alias \"<subsection>.<key>\". With this change,\nalias.<subsection>.<key> entries are silently ignored (unless <key>\nis \"command\", which will be given meaning in the next commit).\n\nThis behavior was arguably a bug, since config subsections were never\nintended to work this way for aliases, and aliases with dots in their\nnames have never been documented or intentionally supported.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n help.c | 10 +++++++---\n 1 file changed, 7 insertions(+), 3 deletions(-)\n\ndiff --git a/help.c b/help.c\nindex 5b1b320d02..691af219bf 100644\n--- a/help.c\n+++ b/help.c\n@@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t\t\t\t  void *cb)\n {\n \tstruct help_unknown_cmd_config *cfg = cb;\n-\tconst char *p;\n+\tconst char *subsection, *key;\n+\tsize_t subsection_len;\n \n \tif (!strcmp(var, \"help.autocorrect\")) {\n \t\tint v = parse_autocorrect(value);\n@@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t}\n \n \t/* Also use aliases for command lookup */\n-\tif (skip_prefix(var, \"alias.\", &p))\n-\t\tadd_cmdname(&cfg->aliases, p, strlen(p));\n+\tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n+\t\t\t      &key)) {\n+\t\tif (!subsection)\n+\t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t}\n \n \treturn 0;\n }\n-- \n2.53.0.122.g3abf75d576\n\n"},{"id":"536347","messageId":"20260218215737.1181147-4-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260218215737.1181147-1-jonatan@jontes.page","subject":"[PATCH v7 3/4] alias: support non-alphanumeric names via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-18T21:57:36Z","receivedAt":"2026-02-18T21:58:51Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Git alias names are limited to ASCII alphanumeric characters and\ndashes because aliases are implemented as config variable names.\nThis prevents aliases being created in languages using characters outside that range.\n\nAdd support for arbitrary alias names by using config subsections:\n\n    [alias \"förgrena\"]\n        command = branch\n\nThe subsection name is matched as-is (case-sensitive byte comparison),\nwhile the existing definition without a subsection (e.g.,\n\"[alias] co = checkout\") remains case-insensitive for backward\ncompatibility. This uses existing config infrastructure since\nsubsections already support arbitrary bytes, and avoids introducing\nUnicode normalization.\n\nAlso teach the help subsystem about the new syntax so that \"git help\n-a\" properly lists subsection aliases and the autocorrect feature can\nsuggest them. Use utf8_strwidth() instead of strlen() for column\nalignment so that non-ASCII alias names display correctly.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n Documentation/config/alias.adoc | 50 ++++++++++++++++++++++-----\n alias.c                         | 38 ++++++++++++++++----\n help.c                          | 14 ++++++--\n t/t0014-alias.sh                | 61 +++++++++++++++++++++++++++++++++\n 4 files changed, 145 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\nindex 80ce17d2de..09a6499249 100644\n--- a/Documentation/config/alias.adoc\n+++ b/Documentation/config/alias.adoc\n@@ -1,12 +1,46 @@\n alias.*::\n-\tCommand aliases for the linkgit:git[1] command wrapper - e.g.\n-\tafter defining `alias.last = cat-file commit HEAD`, the invocation\n-\t`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n-\tconfusion and troubles with script usage, aliases that\n-\thide existing Git commands are ignored except for deprecated\n-\tcommands.  Arguments are split by\n-\tspaces, the usual shell quoting and escaping are supported.\n-\tA quote pair or a backslash can be used to quote them.\n+alias.*.command::\n+\tCommand aliases for the linkgit:git[1] command wrapper. Aliases\n+\tcan be defined using two syntaxes:\n++\n+--\n+1. Without a subsection, e.g., `[alias] co = checkout`. The alias\n+   name (\"co\" in this example) is \n+   limited to ASCII alphanumeric characters and `-`,\n+   and is matched case-insensitively.\n+2. With a subsection, e.g., `[alias \"co\"] command = checkout`. The\n+   alias name can contain any characters (except for newlines and NUL bytes), \n+   including UTF-8, and is matched case-sensitively as raw bytes.\n+   You define the action of the alias in the `command`.\n+--\n++\n+Examples:\n++\n+----\n+# Without subsection (ASCII alphanumeric and dash only)\n+[alias]\n+    co = checkout\n+    st = status\n+\n+# With subsection (allows any characters, including UTF-8)\n+[alias \"hämta\"]\n+    command = fetch\n+[alias \"rätta till\"]\n+    command = commit --amend\n+----\n++\n+With a Git alias defined, e.g.,\n+\n+    $ git config --global alias.last \"cat-file commit HEAD\"\n+    # Which is equivalent to\n+    $ git config --global alias.last.command \"cat-file commit HEAD\"\n+\n+`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n+confusion and troubles with script usage, aliases that\n+hide existing Git commands are ignored except for deprecated\n+commands.  Arguments are split by\n+spaces, the usual shell quoting and escaping are supported.\n+A quote pair or a backslash can be used to quote them.\n +\n Note that the first word of an alias does not necessarily have to be a\n command. It can be a command-line option that will be passed into the\ndiff --git a/alias.c b/alias.c\nindex 271acb9bf1..0d636278bc 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -13,28 +13,52 @@ struct config_alias_data {\n \tstruct string_list *list;\n };\n \n-static int config_alias_cb(const char *key, const char *value,\n+static int config_alias_cb(const char *var, const char *value,\n \t\t\t   const struct config_context *ctx UNUSED, void *d)\n {\n \tstruct config_alias_data *data = d;\n-\tconst char *p;\n+\tconst char *subsection, *key;\n+\tsize_t subsection_len;\n \n-\tif (!skip_prefix(key, \"alias.\", &p))\n+\tif (parse_config_key(var, \"alias\", &subsection, &subsection_len,\n+\t\t\t     &key) < 0)\n+\t\treturn 0;\n+\n+\t/*\n+\t * Two config syntaxes:\n+\t * - alias.name = value   (without subsection, case-insensitive)\n+\t * - [alias \"name\"]\n+\t *       command = value  (with subsection, case-sensitive)\n+\t */\n+\tif (subsection && strcmp(key, \"command\"))\n \t\treturn 0;\n \n \tif (data->alias) {\n-\t\tif (!strcasecmp(p, data->alias)) {\n+\t\tint match;\n+\n+\t\tif (subsection)\n+\t\t\tmatch = (strlen(data->alias) == subsection_len &&\n+\t\t\t\t !strncmp(data->alias, subsection,\n+\t\t\t\t\t  subsection_len));\n+\t\telse\n+\t\t\tmatch = !strcasecmp(data->alias, key);\n+\n+\t\tif (match) {\n \t\t\tFREE_AND_NULL(data->v);\n \t\t\treturn git_config_string(&data->v,\n-\t\t\t\t\t\t key, value);\n+\t\t\t\t\t\t var, value);\n \t\t}\n \t} else if (data->list) {\n \t\tstruct string_list_item *item;\n \n \t\tif (!value)\n-\t\t\treturn config_error_nonbool(key);\n+\t\t\treturn config_error_nonbool(var);\n \n-\t\titem = string_list_append(data->list, p);\n+\t\tif (subsection)\n+\t\t\titem = string_list_append_nodup(data->list,\n+\t\t\t\txmemdupz(subsection, subsection_len));\n+\t\telse\n+\t\t\titem = string_list_append(data->list, key);\n \t\titem->util = xstrdup(value);\n \t}\n \ndiff --git a/help.c b/help.c\nindex 691af219bf..95f576c5c8 100644\n--- a/help.c\n+++ b/help.c\n@@ -21,6 +21,7 @@\n #include \"fsmonitor-ipc.h\"\n #include \"repository.h\"\n #include \"alias.h\"\n+#include \"utf8.h\"\n \n #ifndef NO_CURL\n #include \"git-curl-compat.h\" /* For LIBCURL_VERSION only */\n@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,\n \n \tfor (i = 0; cmds[i].name; i++) {\n \t\tif (cmds[i].category & mask) {\n-\t\t\tsize_t len = strlen(cmds[i].name);\n+\t\t\tsize_t len = utf8_strwidth(cmds[i].name);\n \t\t\tprintf(\"   %s   \", cmds[i].name);\n \t\t\tif (longest > len)\n \t\t\t\tmput_char(' ', longest - len);\n@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)\n \tstring_list_sort(&alias_list);\n \n \tfor (i = 0; i < alias_list.nr; i++) {\n-\t\tsize_t len = strlen(alias_list.items[i].string);\n+\t\tsize_t len = utf8_strwidth(alias_list.items[i].string);\n \t\tif (longest < len)\n \t\t\tlongest = len;\n \t}\n@@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,\n \t/* Also use aliases for command lookup */\n \tif (!parse_config_key(var, \"alias\", &subsection, &subsection_len,\n \t\t\t      &key)) {\n-\t\tif (!subsection)\n+\t\tif (subsection) {\n+\t\t\t/* [alias \"name\"] command = value */\n+\t\t\tif (!strcmp(key, \"command\"))\n+\t\t\t\tadd_cmdname(&cfg->aliases, subsection,\n+\t\t\t\t\t    subsection_len);\n+\t\t} else {\n+\t\t\t/* alias.name = value */\n \t\t\tadd_cmdname(&cfg->aliases, key, strlen(key));\n+\t\t}\n \t}\n \n \treturn 0;\ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex a13d2be8ca..34bbdb51c5 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -122,4 +122,65 @@ test_expect_success 'alias without value reports error' '\n \ttest_grep \"alias.noval\" error\n '\n \n+test_expect_success 'subsection syntax works' '\n+\ttest_config alias.testnew.command \"!echo ran-subsection\" &&\n+\tgit testnew >output &&\n+\ttest_grep \"ran-subsection\" output\n+'\n+\n+test_expect_success 'subsection syntax only accepts command key' '\n+\ttest_config alias.invalid.notcommand value &&\n+\ttest_must_fail git invalid 2>error &&\n+\ttest_grep -i \"not a git command\" error\n+'\n+\n+test_expect_success 'subsection syntax requires value for command' '\n+\ttest_when_finished \"git config --remove-section alias.noval\" &&\n+\tcat >>.git/config <<-\\EOF &&\n+\t[alias \"noval\"]\n+\t\tcommand\n+\tEOF\n+\ttest_must_fail git noval 2>error &&\n+\ttest_grep \"alias.noval.command\" error\n+'\n+\n+test_expect_success 'simple syntax is case-insensitive' '\n+\ttest_config alias.LegacyCase \"!echo ran-legacy\" &&\n+\tgit legacycase >output &&\n+\ttest_grep \"ran-legacy\" output\n+'\n+\n+test_expect_success 'subsection syntax is case-sensitive' '\n+\ttest_config alias.SubCase.command \"!echo ran-upper\" &&\n+\ttest_config alias.subcase.command \"!echo ran-lower\" &&\n+\tgit SubCase >upper.out &&\n+\tgit subcase >lower.out &&\n+\ttest_grep \"ran-upper\" upper.out &&\n+\ttest_grep \"ran-lower\" lower.out\n+'\n+\n+test_expect_success 'UTF-8 alias with Swedish characters' '\n+\ttest_config alias.\"förgrena\".command \"!echo ran-swedish\" &&\n+\tgit förgrena >output &&\n+\ttest_grep \"ran-swedish\" output\n+'\n+\n+test_expect_success 'UTF-8 alias with CJK characters' '\n+\ttest_config alias.\"分支\".command \"!echo ran-cjk\" &&\n+\tgit 分支 >output &&\n+\ttest_grep \"ran-cjk\" output\n+'\n+\n+test_expect_success 'alias with spaces in name' '\n+\ttest_config alias.\"test name\".command \"!echo ran-spaces\" &&\n+\tgit \"test name\" >output &&\n+\ttest_grep \"ran-spaces\" output\n+'\n+\n+test_expect_success 'subsection aliases listed in help -a' '\n+\ttest_config alias.\"förgrena\".command \"!echo test\" &&\n+\tgit help -a >output &&\n+\ttest_grep \"förgrena\" output\n+'\n+\n test_done\n-- \n2.53.0.122.g3abf75d576\n\n"},{"id":"536348","messageId":"20260218215737.1181147-5-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260218215737.1181147-1-jonatan@jontes.page","subject":"[PATCH v7 4/4] completion: fix zsh alias listing for subsection aliases","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-18T21:57:37Z","receivedAt":"2026-02-18T21:58:51Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"The zsh completion function __git_zsh_cmd_alias() uses 'git config\n--get-regexp' to enumerate aliases and then strips the \"alias.\" prefix\nfrom each key. For subsection-style aliases (alias.name.command), this\nleaves \"name.command\" as the completion candidate instead of just\n\"name\".\n\nThe bash completion does not have this problem because it goes through\n'git --list-cmds=alias', which calls list_aliases() in C and already\nhandles both alias syntaxes correctly. However, zsh needs both the\nalias name and its value for descriptive completion, which\n--list-cmds=alias does not provide.\n\nAdd a hidden --aliases-for-completion option to 'git help', following\nthe existing --config-for-completion pattern. It outputs NUL-separated\n\"name\\nvalue\" pairs using list_aliases(), which correctly resolves both\nthe traditional (alias.name) and subsection (alias.name.command)\nformats. Update __git_zsh_cmd_alias() to use it.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n builtin/help.c                        | 13 +++++++++++++\n contrib/completion/git-completion.zsh |  2 +-\n 2 files changed, 14 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/help.c b/builtin/help.c\nindex c09cbc8912..86a3d03a9b 100644\n--- a/builtin/help.c\n+++ b/builtin/help.c\n@@ -54,6 +54,7 @@ static enum help_action {\n \tHELP_ACTION_DEVELOPER_INTERFACES,\n \tHELP_ACTION_CONFIG_FOR_COMPLETION,\n \tHELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION,\n+\tHELP_ACTION_ALIASES_FOR_COMPLETION,\n } cmd_mode;\n \n static char *html_path;\n@@ -90,6 +91,8 @@ static struct option builtin_help_options[] = {\n \t\t    HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n \tOPT_CMDMODE_F(0, \"config-sections-for-completion\", &cmd_mode, \"\",\n \t\t    HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n+\tOPT_CMDMODE_F(0, \"aliases-for-completion\", &cmd_mode, \"\",\n+\t\t    HELP_ACTION_ALIASES_FOR_COMPLETION, PARSE_OPT_HIDDEN),\n \n \tOPT_END(),\n };\n@@ -691,6 +694,16 @@ int cmd_help(int argc,\n \t\t\t       help_format);\n \t\tlist_config_help(SHOW_CONFIG_SECTIONS);\n \t\treturn 0;\n+\tcase HELP_ACTION_ALIASES_FOR_COMPLETION: {\n+\t\tstruct string_list alias_list = STRING_LIST_INIT_DUP;\n+\t\topt_mode_usage(argc, \"--aliases-for-completion\", help_format);\n+\t\tlist_aliases(&alias_list);\n+\t\tfor (size_t i = 0; i < alias_list.nr; i++)\n+\t\t\tprintf(\"%s%c%s%c\", alias_list.items[i].string, '\\n',\n+\t\t\t       (char *)alias_list.items[i].util, '\\0');\n+\t\tstring_list_clear(&alias_list, 1);\n+\t\treturn 0;\n+\t}\n \tcase HELP_ACTION_CONFIG:\n \t\topt_mode_usage(argc, \"--config\", help_format);\n \t\tsetup_pager(the_repository);\ndiff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh\nindex f5877bd7a1..c32186a977 100644\n--- a/contrib/completion/git-completion.zsh\n+++ b/contrib/completion/git-completion.zsh\n@@ -202,7 +202,7 @@ __git_zsh_cmd_common ()\n __git_zsh_cmd_alias ()\n {\n \tlocal -a list\n-\tlist=(${${(0)\"$(git config -z --get-regexp '^alias\\.*')\"}#alias.})\n+\tlist=(${(0)\"$(git help --aliases-for-completion)\"})\n \tlist=(${(f)\"$(printf \"%s:alias for '%s'\\n\" ${(f@)list})\"})\n \t_describe -t alias-commands 'aliases' list && _ret=0\n }\n-- \n2.53.0.122.g3abf75d576\n\n"},{"id":"536427","messageId":"xmqqpl6061k2.fsf@gitster.g","threadId":"64949","inReplyTo":"20260218215737.1181147-1-jonatan@jontes.page","subject":"Re: [PATCH v7 0/4] support UTF-8 in alias names","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-19T18:17:01Z","receivedAt":"2026-02-19T18:17:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> Hi all!\n>\n> This series adds support for non-alphanumeric (including UTF-8)\n> characters in alias names by allowing aliases to be defined using\n> git-config's subsection syntax.\n>\n> Changes since v6:\n> Reverted the --aliases-for-completion output format to\n> use a newline as the field separator between name and value within\n> each NUL-terminated record (name\\nvalue\\0). In v6 I changed this\n> to use NUL for both separators, but this diverges from the\n> convention established by git config -z, which outputs\n> key\\nvalue\\0. Matching that convention is the right call here.\n\nOK.  The key cannot have NUL nor LF in it, so either _should_ work,\nand using LF in the context of -z somehow makes our eyes feel dirty,\nbut it is how \"git config -z\" chooses to work, so I agree that this\nchange makes sense.\n\nShall we mark this topic ready for 'next' now?\n\nWill queue.  Thanks.\n"},{"id":"536431","messageId":"d59aa674-70b6-47ad-bac5-e247fa5ac21c@jontes.page","threadId":"64949","inReplyTo":"xmqqpl6061k2.fsf@gitster.g","subject":"Re: [PATCH v7 0/4] support UTF-8 in alias names","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-19T18:54:13Z","receivedAt":"2026-02-19T18:54:20Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Thanks for the review and feedback everyone!  Excited to have my first \npatch land in Git.\n\nJonatan\n"},{"id":"536724","messageId":"112cccfd-ee04-4c23-bc5e-fd269ebc6c1f@app.fastmail.com","threadId":"64949","inReplyTo":"20260210183110.1151072-3-jonatan@jontes.page","subject":"Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-23T09:29:26Z","receivedAt":"2026-02-23T09:29:54Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Feb 10, 2026, at 19:31, Jonatan Holmgren wrote:\n> [snip]\n>\n> Suggested-by: Jeff King <peff@peff.net>\n> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n> ---\n>  Documentation/config/alias.adoc | 44 +++++++++++++++++++++-----\n>  alias.c                         | 45 ++++++++++++++++++++++-----\n>  help.c                          | 12 +++++--\n>  t/t0014-alias.sh                | 55 +++++++++++++++++++++++++++++++++\n>  4 files changed, 137 insertions(+), 19 deletions(-)\n>\n> diff --git a/Documentation/config/alias.adoc\n> b/Documentation/config/alias.adoc\n> index 80ce17d2de..17a548cd64 100644\n> --- a/Documentation/config/alias.adoc\n> +++ b/Documentation/config/alias.adoc\n>[snip]\n> +# With subsection (allows UTF-8 and special characters)\n> +[alias \"hämta\"]\n> +    command = fetch\n> +[alias \"gömma\"]\n> +    command = stash\n> +----\n> ++\n> +E.g. after defining `alias.last = cat-file commit HEAD`, the invocation\n> +`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n> +confusion and troubles with script usage, aliases that\n> +hide existing Git commands are ignored except for deprecated\n> +commands.  Arguments are split by\n> +spaces, the usual shell quoting and escaping are supported.\n> +A quote pair or a backslash can be used to quote them.\n>  +\n>  Note that the first word of an alias does not necessarily have to be a\n\nThe HTML output shows the list continuation character (+).\n\n    + Note that the first word of an alias does not ...\n\n>  command. It can be a command-line option that will be passed into the\n\nAnd there are two more outside this context which I suspect are knock-ons?\n\n    + If the alias expansion is prefixed ..\n\n    ...\n\n    + * Shell commands will be executed ...\n\nSee `Documentation/doc-diff master next`.\n\n• master: 7c02d39f (The 6th batch, 2026-02-20)\n• next: 4a7958ca (Sync with 'master', 2026-02-20)\n\n> diff --git a/alias.c b/alias.c\n>[snip]\n"},{"id":"536827","messageId":"6706edc2-04c7-4bd0-81ec-33710088f0bc@app.fastmail.com","threadId":"64949","inReplyTo":"112cccfd-ee04-4c23-bc5e-fd269ebc6c1f@app.fastmail.com","subject":"Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-23T16:07:10Z","receivedAt":"2026-02-23T16:07:31Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Feb 23, 2026, at 10:29, Kristoffer Haugsbakk wrote:\n>>[snip]\n>\n> The HTML output shows the list continuation character (+).\n>\n>     + Note that the first word of an alias does not ...\n>\n>>  command. It can be a command-line option that will be passed into the\n>\n> And there are two more outside this context which I suspect are knock-ons?\n>\n>     + If the alias expansion is prefixed ..\n>\n>     ...\n>\n>     + * Shell commands will be executed ...\n>\n> See `Documentation/doc-diff master next`.\n>\n> • master: 7c02d39f (The 6th batch, 2026-02-20)\n> • next: 4a7958ca (Sync with 'master', 2026-02-20)\n>\n>> diff --git a/alias.c b/alias.c\n>>[snip]\n\nSorry that I just replied to the first hit that I found in my\ninbox. This applies to the latest version which is in `next`.\n"},{"id":"536882","messageId":"xmqqzf4zky6e.fsf@gitster.g","threadId":"64949","inReplyTo":"6706edc2-04c7-4bd0-81ec-33710088f0bc@app.fastmail.com","subject":"Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-23T20:22:17Z","receivedAt":"2026-02-23T20:22:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n\n> On Mon, Feb 23, 2026, at 10:29, Kristoffer Haugsbakk wrote:\n>>>[snip]\n>>\n>> The HTML output shows the list continuation character (+).\n>>\n>>     + Note that the first word of an alias does not ...\n>>\n>>>  command. It can be a command-line option that will be passed into the\n>>\n>> And there are two more outside this context which I suspect are knock-ons?\n>>\n>>     + If the alias expansion is prefixed ..\n>>\n>>     ...\n>>\n>>     + * Shell commands will be executed ...\n>>\n>> See `Documentation/doc-diff master next`.\n>>\n>> • master: 7c02d39f (The 6th batch, 2026-02-20)\n>> • next: 4a7958ca (Sync with 'master', 2026-02-20)\n>>\n>>> diff --git a/alias.c b/alias.c\n>>>[snip]\n>\n> Sorry that I just replied to the first hit that I found in my\n> inbox. This applies to the latest version which is in `next`.\n\nAnd your fix is the [v7 5/4] <followup-alias-i18n.3e0@msgid.xyz>?\n\nThanks for being careful.\n"},{"id":"536883","messageId":"2951646f-f1be-4ad9-b995-6ce73c3ec85b@app.fastmail.com","threadId":"64949","inReplyTo":"xmqqzf4zky6e.fsf@gitster.g","subject":"Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-23T20:25:11Z","receivedAt":"2026-02-23T20:25:32Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Feb 23, 2026, at 21:22, Junio C Hamano wrote:\n>>[snip]\n>> Sorry that I just replied to the first hit that I found in my\n>> inbox. This applies to the latest version which is in `next`.\n>\n> And your fix is the [v7 5/4] <followup-alias-i18n.3e0@msgid.xyz>?\n>\n> Thanks for being careful.\n\nYep, I was wondering whether to use In-Reply-To the cover letter but I\ndidn’t know which approach would be less confusing (freestanding or\nreply).\n"},{"id":"536962","messageId":"aZ19E2Bs0iIt2TN2@pks.im","threadId":"64949","inReplyTo":"20260210183110.1151072-3-jonatan@jontes.page","subject":"Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-24T10:27:31Z","receivedAt":"2026-02-24T10:27:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Feb 10, 2026 at 07:31:10PM +0100, Jonatan Holmgren wrote:\n> Git alias names are limited to alphanumeric characters and dashes\n> because config variable names are validated by iskeychar(). This\n> prevents non-English speakers from creating aliases in their native\n> languages.\n> \n> Add support for arbitrary alias names by using config subsections:\n> \n>     [alias \"förgrena\"]\n>         command = branch\n> \n> The subsection name is matched as-is (case-sensitive byte comparison),\n> while the existing definition without a subsection (e.g.,\n> \"[alias] co = checkout\") remains case-insensitive for backward\n> compatibility. This uses existing config infrastructure since\n> subsections already support arbitrary bytes, and avoids introducing\n> Unicode normalization.\n> \n> Also teach the help subsystem about the new syntax so that \"git help\n> -a\" properly lists subsection aliases and the autocorrect feature can\n> suggest them. Use utf8_strwidth() instead of strlen() for column\n> alignment so that non-alphanumeric alias names display correctly.\n\nThis patch has caused a regression in a somewhat esoteric use case.\nBefore this patch, you could do the following:\n\n    $ git config set \"alias..foobar\" \"!echo barfoo\"\n    $ git .foobar\n    barfoo\n\nOr, phrased as a test case:\n\n    diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\n    index a13d2be8ca..dca50e87e2 100755\n    --- a/t/t0014-alias.sh\n    +++ b/t/t0014-alias.sh\n    @@ -4,6 +4,13 @@ test_description='git command aliasing'\n\n     . ./test-lib.sh\n\n    +test_expect_success 'alias with leading dot' '\n    +\ttest_config_global alias..something \"!echo foobar\" &&\n    +\tgit .something >actual &&\n    +\techo foobar >expect &&\n    +\ttest_cmp expect actual\n    +'\n    +\n     test_expect_success 'nested aliases - internal execution' '\n     \tgit config alias.nested-internal-1 nested-internal-2 &&\n     \tgit config alias.nested-internal-2 status &&\n\nI kind of doubt that this was intentional design, but I know that it is\nused e.g. by Dscho in his shears scripts [1]. What this script does is\nto create a temporary alias \"alias..r\" that then gets executed via the\nsequencer, and this patch broke this. I happened to discover the\nregression as I use shears myself.\n\nChances are that there are other users out there that rely on the\ncurrent behaviour.\n\nThanks!\n\nPatrick\n\n[1]: https://github.com/git-for-windows/build-extra/blob/a82c8fcb0b8f165c1379c12b0cf914741b8dc8d5/shears.sh\n"},{"id":"536963","messageId":"f1f37010-2e93-4e11-8298-420cd8931d62@app.fastmail.com","threadId":"64949","inReplyTo":"20260218215737.1181147-4-jonatan@jontes.page","subject":"Re: [PATCH v7 3/4] alias: support non-alphanumeric names via subsection syntax","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-24T10:55:16Z","receivedAt":"2026-02-24T10:55:37Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Feb 18, 2026, at 22:57, Jonatan Holmgren wrote:\n> Git alias names are limited to ASCII alphanumeric characters and\n> dashes because aliases are implemented as config variable names.\n> This prevents aliases being created in languages using characters\n> outside that range.\n>\n> Add support for arbitrary alias names by using config subsections:\n>\n>     [alias \"förgrena\"]\n>         command = branch\n>\n> The subsection name is matched as-is (case-sensitive byte comparison),\n> while the existing definition without a subsection (e.g.,\n> \"[alias] co = checkout\") remains case-insensitive for backward\n> compatibility. This uses existing config infrastructure since\n> subsections already support arbitrary bytes, and avoids introducing\n> Unicode normalization.\n>\n> Also teach the help subsystem about the new syntax so that \"git help\n> -a\" properly lists subsection aliases and the autocorrect feature can\n> suggest them. Use utf8_strwidth() instead of strlen() for column\n> alignment so that non-ASCII alias names display correctly.\n>\n> Suggested-by: Jeff King <peff@peff.net>\n> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n> ---\n>  Documentation/config/alias.adoc | 50 ++++++++++++++++++++++-----\n>  alias.c                         | 38 ++++++++++++++++----\n>  help.c                          | 14 ++++++--\n>  t/t0014-alias.sh                | 61 +++++++++++++++++++++++++++++++++\n>  4 files changed, 145 insertions(+), 18 deletions(-)\n>\n> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\n> index 80ce17d2de..09a6499249 100644\n> --- a/Documentation/config/alias.adoc\n> +++ b/Documentation/config/alias.adoc\n>[snip]\n> +# With subsection (allows any characters, including UTF-8)\n> +[alias \"hämta\"]\n> +    command = fetch\n> +[alias \"rätta till\"]\n> +    command = commit --amend\n\nThis is in `next` now so this is a question or note for later.\n\nIs this `rätta till` supposed to have a space in it? Or is there\nsupposed to be a hyphen? I couldn’t get it to work.\n\nIs the intent to use quotes for the command (in e.g. Bash)?\n\n    ./bin-wrappers/git \"rätta till\"\n\n> +----\n> ++\n> +With a Git alias defined, e.g.,\n> +\n> +    $ git config --global alias.last \"cat-file commit HEAD\"\n> +    # Which is equivalent to\n> +    $ git config --global alias.last.command \"cat-file commit HEAD\"\n> +\n> +`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n\nThis is also a note for later. I think this “To avoid” should now be\nmoved to a separate paragraph since it’s a different topic.\n\nSorry that all my feedback is this late! These are just notes for the\nlist in any case.\n\n> +confusion and troubles with script usage, aliases that\n> +hide existing Git commands are ignored except for deprecated\n> +commands.  Arguments are split by\n> +spaces, the usual shell quoting and escaping are supported.\n> +A quote pair or a backslash can be used to quote them.\n>[snip]\n"},{"id":"536977","messageId":"4b80f709-d883-4565-9fe0-d92c80a7ef0f@jontes.page","threadId":"64949","inReplyTo":"f1f37010-2e93-4e11-8298-420cd8931d62@app.fastmail.com","subject":"Re: [PATCH v7 3/4] alias: support non-alphanumeric names via subsection syntax","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-24T14:48:50Z","receivedAt":"2026-02-24T14:49:05Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"> Is this `rätta till` supposed to have a space in it? \nAbsolutely, as a demo of space working fine\n\n> Is the intent to use quotes for the command (in e.g. Bash)?\nYup, or \"\\ \" which is what my shell defaults to. So the user would type \n\"git rä<TAB>\". Does that not work on your machine?\n\nJonatan\n"},{"id":"536982","messageId":"20260224171245.458377-1-jonatan@jontes.page","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"[PATCH 0/2] Fix small issues in alias subsection handling","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-24T17:12:34Z","receivedAt":"2026-02-24T17:13:34Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Hello!\n\nI have two small patches related to the handling of alias subsections. \nThe first one is a documentation fix for the example showing the equivalence\nbetween alias.last and alias.last.command, which was missing list continuation marks. \nThe second patch addresses a compatibility issue where an empty subsection ([alias \"\"])\nwas not treated as a plain [alias], \ncausing existing entries stored this way to be ignored.\n\nThanks for considering these patches!\n\nJonatan Holmgren (2):\n  doc: fix list continuation in alias subsection example\n  alias: treat empty subsection [alias \"\"] as plain [alias]\n\n Documentation/config/alias.adoc |  7 ++++---\n alias.c                         |  4 ++++\n t/t0014-alias.sh                | 14 ++++++++++++++\n 3 files changed, 22 insertions(+), 3 deletions(-)\n\n-- \n2.53.0\n\n"},{"id":"536983","messageId":"20260224171245.458377-2-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260224171245.458377-1-jonatan@jontes.page","subject":"[PATCH 1/2] doc: fix list continuation in alias subsection example","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-24T17:12:35Z","receivedAt":"2026-02-24T17:13:35Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"The example showing the equivalence between alias.last and\nalias.last.command was missing the list continuation marks (+\nbetween the shell session block and the following prose, leaving\nthe paragraph detached from the list item in the rendered output.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n Documentation/config/alias.adoc | 7 ++++---\n 1 file changed, 4 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\nindex 115fdbb1e3..7830379f58 100644\n--- a/Documentation/config/alias.adoc\n+++ b/Documentation/config/alias.adoc\n@@ -30,13 +30,14 @@ Examples:\n ----\n +\n With a Git alias defined, e.g.,\n-\n++\n     $ git config --global alias.last \"cat-file commit HEAD\"\n     # Which is equivalent to\n     $ git config --global alias.last.command \"cat-file commit HEAD\"\n++\n+`git last` is equivalent to `git cat-file commit HEAD`. \n \n-`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n-confusion and troubles with script usage, aliases that\n+To avoid confusion and troubles with script usage, aliases that\n hide existing Git commands are ignored except for deprecated\n commands.  Arguments are split by\n spaces, the usual shell quoting and escaping are supported.\n-- \n2.53.0\n\n"},{"id":"536984","messageId":"20260224171245.458377-3-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260224171245.458377-1-jonatan@jontes.page","subject":"[PATCH 2/2] alias: treat empty subsection [alias \"\"] as plain [alias]","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-24T17:12:36Z","receivedAt":"2026-02-24T17:13:37Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"When git-config stores a key of the form alias..name, it records\nit under an empty subsection ([alias \"\"]). The new subsection-aware\nalias lookup would see a non-NULL but zero-length subsection and\nfall into the subsection code path, where it required a \"command\"\nkey and thus silently ignored the entry.\n\nNormalize an empty subsection to NULL before any further processing\nso that entries stored this way continue to work as plain\ncase-insensitive aliases, matching the pre-subsection behaviour.\n\nUsers who relied on alias..name to create an alias literally named\n\".name\" may want to migrate to subsection syntax, which looks less confusing:\n\n    [alias \".name\"]\n        command = <value>\n\nAdd tests covering both the empty-subsection compatibility case and\nthe leading-dot alias via the new syntax.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n alias.c          |  4 ++++\n t/t0014-alias.sh | 14 ++++++++++++++\n 2 files changed, 18 insertions(+)\n\ndiff --git a/alias.c b/alias.c\nindex 0d636278bc..ec9833dd30 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -30,6 +30,10 @@ static int config_alias_cb(const char *var, const char *value,\n \t * - [alias \"name\"]\n \t *       command = value  (with subsection, case-sensitive)\n \t */\n+\t/* Treat [alias \"\"] (empty subsection) the same as plain [alias]. */\n+\tif (subsection && !subsection_len)\n+\t\tsubsection = NULL;\n+\n \tif (subsection && strcmp(key, \"command\"))\n \t\treturn 0;\n \ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex 34bbdb51c5..68b4903cbf 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -183,4 +183,18 @@ test_expect_success 'subsection aliases listed in help -a' '\n \ttest_grep \"förgrena\" output\n '\n \n+test_expect_success 'empty subsection treated as no subsection' '\n+\ttest_config \"alias..something\" \"!echo foobar\" &&\n+\tgit something >actual &&\n+\techo foobar >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'alias with leading dot via subsection syntax' '\n+\ttest_config alias.\".something\".command \"!echo foobar\" &&\n+\tgit .something >actual &&\n+\techo foobar >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.53.0\n\n"},{"id":"537009","messageId":"xmqqwm02ez3c.fsf@gitster.g","threadId":"64949","inReplyTo":"20260224171245.458377-2-jonatan@jontes.page","subject":"Re: [PATCH 1/2] doc: fix list continuation in alias subsection example","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-24T19:11:19Z","receivedAt":"2026-02-24T19:11:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> The example showing the equivalence between alias.last and\n> alias.last.command was missing the list continuation marks (+\n> between the shell session block and the following prose, leaving\n> the paragraph detached from the list item in the rendered output.\n>\n> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n> ---\n>  Documentation/config/alias.adoc | 7 ++++---\n>  1 file changed, 4 insertions(+), 3 deletions(-)\n\nThis seems to address the same issue as 0ed2275f (doc: config: fix\nlist continuation in alias section, 2026-02-23).  Should we treat\nthis one as a replacement of that one?\n\n> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\n> index 115fdbb1e3..7830379f58 100644\n> --- a/Documentation/config/alias.adoc\n> +++ b/Documentation/config/alias.adoc\n> @@ -30,13 +30,14 @@ Examples:\n>  ----\n>  +\n>  With a Git alias defined, e.g.,\n> -\n> ++\n>      $ git config --global alias.last \"cat-file commit HEAD\"\n>      # Which is equivalent to\n>      $ git config --global alias.last.command \"cat-file commit HEAD\"\n> ++\n> +`git last` is equivalent to `git cat-file commit HEAD`. \n>  \n> -`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n> -confusion and troubles with script usage, aliases that\n> +To avoid confusion and troubles with script usage, aliases that\n>  hide existing Git commands are ignored except for deprecated\n>  commands.  Arguments are split by\n>  spaces, the usual shell quoting and escaping are supported.\n"},{"id":"537010","messageId":"1178ac42-0d8a-430a-9ca2-a170e4e6932e@app.fastmail.com","threadId":"64949","inReplyTo":"xmqqwm02ez3c.fsf@gitster.g","subject":"Re: [PATCH 1/2] doc: fix list continuation in alias subsection example","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-24T19:14:09Z","receivedAt":"2026-02-24T19:14:31Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Feb 24, 2026, at 20:11, Junio C Hamano wrote:\n> Jonatan Holmgren <jonatan@jontes.page> writes:\n>\n>> The example showing the equivalence between alias.last and\n>> alias.last.command was missing the list continuation marks (+\n>> between the shell session block and the following prose, leaving\n>> the paragraph detached from the list item in the rendered output.\n>>\n>> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n>> ---\n>>  Documentation/config/alias.adoc | 7 ++++---\n>>  1 file changed, 4 insertions(+), 3 deletions(-)\n>\n> This seems to address the same issue as 0ed2275f (doc: config: fix\n> list continuation in alias section, 2026-02-23).  Should we treat\n> this one as a replacement of that one?\n\nYeah I think so.\n"},{"id":"537013","messageId":"xmqqseapgabz.fsf@gitster.g","threadId":"64949","inReplyTo":"1178ac42-0d8a-430a-9ca2-a170e4e6932e@app.fastmail.com","subject":"Re: [PATCH 1/2] doc: fix list continuation in alias subsection example","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-24T20:23:12Z","receivedAt":"2026-02-24T20:23:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n\n> On Tue, Feb 24, 2026, at 20:11, Junio C Hamano wrote:\n>> Jonatan Holmgren <jonatan@jontes.page> writes:\n>>\n>>> The example showing the equivalence between alias.last and\n>>> alias.last.command was missing the list continuation marks (+\n>>> between the shell session block and the following prose, leaving\n>>> the paragraph detached from the list item in the rendered output.\n>>>\n>>> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n>>> ---\n>>>  Documentation/config/alias.adoc | 7 ++++---\n>>>  1 file changed, 4 insertions(+), 3 deletions(-)\n>>\n>> This seems to address the same issue as 0ed2275f (doc: config: fix\n>> list continuation in alias section, 2026-02-23).  Should we treat\n>> this one as a replacement of that one?\n>\n> Yeah I think so.\n\nThanks.  Will discard 0ed2275f then.\n"},{"id":"537028","messageId":"6953f6f2-22e8-4efb-8169-395e1c52634f@intel.com","threadId":"64949","inReplyTo":"20260218215737.1181147-2-jonatan@jontes.page","subject":"Re: [PATCH v7 1/4] help: use list_aliases() for alias listing","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2026-02-24T22:19:36Z","receivedAt":"2026-02-24T22:19:42Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"\n\nOn 2/18/2026 1:57 PM, Jonatan Holmgren wrote:\n> help.c has its own get_alias() config callback that duplicates the\n> parsing logic in alias.c. Consolidate by teaching list_aliases() to\n> also store the alias values (via the string_list util field), then\n> use it in list_all_cmds_help_aliases() instead of the private\n> callback.\n> \n> This preserves the existing error checking for value-less alias\n> definitions by checking in alias.c rather than help.c.\n> \n> No functional change intended.\n> \nThis results in a memory leak with git --list-cmds=alias:\n\n==2244105==ERROR: LeakSanitizer: detected memory leaks\n\nDirect leak of 1453 byte(s) in 37 object(s) allocated from:\n     #0 0x7f9e268e0ca0 in strdup (/lib64/libasan.so.8+0xe0ca0) (BuildId: \n25975f766867e9e604dc5a71a8befeaed3301942)\n     #1 0x00000115997b in xstrdup ../wrapper.c:43\n     #2 0x000000841299 in config_alias_cb ../alias.c:62\n     #3 0x00000098df73 in git_config_include ../config.c:429\n     #4 0x000000973616 in get_value ../config.c:919\n     #5 0x000000973616 in git_parse_source ../config.c:1114\n     #6 0x000000973616 in do_config_from ../config.c:1341\n     #7 0x000000975f5a in do_config_from_file ../config.c:1370\n     #8 0x000000980c9b in git_config_from_file_with_options ../config.c:1393\n     #9 0x0000009827fe in do_git_config_sequence ../config.c:1556\n     #10 0x0000009827fe in config_with_options ../config.c:1615\n     #11 0x00000098313d in read_early_config ../config.c:1670\n     #12 0x000000841935 in list_aliases ../alias.c:81\n     #13 0x00000080b12f in list_cmds ../git.c:102\n     #14 0x00000080b12f in handle_options ../git.c:336\n     #15 0x00000080d570 in cmd_main ../git.c:955\n     #16 0x00000044a54f in main ../common-main.c:9\n     #17 0x7f9e25e105b4 in __libc_start_call_main \n(/lib64/libc.so.6+0x35b4) (BuildId: \n2b5beec0fd24fe9c9f43eddfdd5facf0b8a1b805)\n     #18 0x7f9e25e10667 in __libc_start_main@@GLIBC_2.34 \n(/lib64/libc.so.6+0x3667) (BuildId: \n2b5beec0fd24fe9c9f43eddfdd5facf0b8a1b805)\n     #19 0x00000044c944 in _start \n(/home/jekeller/libexec/git-core/git+0x44c944) (BuildId: \n674cf04ebc1da782eede3c3be79a0c15f372df4c)\n\nSUMMARY: AddressSanitizer: 1453 byte(s) leaked in 37 allocation(s).\n\nThis leak occurs because you now copy and store the value of the alias \nin the util element, but the call of list_aliases() in list_cmd() \ndoesn't clean these up, since its string_list_clear passes 0 to the \nfree_util argument.\n\nThe following fixed it for me:\n\ndiff --git c/git.c i/git.c\nindex 744cb6527e06..aeb099ab1162 100644\n--- c/git.c\n+++ i/git.c\n@@ -119,7 +119,7 @@ static int list_cmds(const char *spec)\n         }\n         for (size_t i = 0; i < list.nr; i++)\n                 puts(list.items[i].string);\n-       string_list_clear(&list, 0);\n+       string_list_clear(&list, 1);\n         return 0;\n  }\n\n\n"},{"id":"537030","messageId":"00ce69ee-0f1a-4569-a19b-b426a5e475e4@intel.com","threadId":"64949","inReplyTo":"20260218215737.1181147-2-jonatan@jontes.page","subject":"Re: [PATCH v7 1/4] help: use list_aliases() for alias listing","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2026-02-24T22:21:54Z","receivedAt":"2026-02-24T22:22:18Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"On 2/18/2026 1:57 PM, Jonatan Holmgren wrote:\n> help.c has its own get_alias() config callback that duplicates the\n> parsing logic in alias.c. Consolidate by teaching list_aliases() to\n> also store the alias values (via the string_list util field), then\n> use it in list_all_cmds_help_aliases() instead of the private\n> callback.\n> \n> This preserves the existing error checking for value-less alias\n> definitions by checking in alias.c rather than help.c.\n> \n> No functional change intended.\n> \nThis results in a memory leak with git --list-cmds=alias:\n\n==2244105==ERROR: LeakSanitizer: detected memory leaks\n\nDirect leak of 1453 byte(s) in 37 object(s) allocated from:\n     #0 0x7f9e268e0ca0 in strdup (/lib64/libasan.so.8+0xe0ca0) (BuildId: \n25975f766867e9e604dc5a71a8befeaed3301942)\n     #1 0x00000115997b in xstrdup ../wrapper.c:43\n     #2 0x000000841299 in config_alias_cb ../alias.c:62\n     #3 0x00000098df73 in git_config_include ../config.c:429\n     #4 0x000000973616 in get_value ../config.c:919\n     #5 0x000000973616 in git_parse_source ../config.c:1114\n     #6 0x000000973616 in do_config_from ../config.c:1341\n     #7 0x000000975f5a in do_config_from_file ../config.c:1370\n     #8 0x000000980c9b in git_config_from_file_with_options ../config.c:1393\n     #9 0x0000009827fe in do_git_config_sequence ../config.c:1556\n     #10 0x0000009827fe in config_with_options ../config.c:1615\n     #11 0x00000098313d in read_early_config ../config.c:1670\n     #12 0x000000841935 in list_aliases ../alias.c:81\n     #13 0x00000080b12f in list_cmds ../git.c:102\n     #14 0x00000080b12f in handle_options ../git.c:336\n     #15 0x00000080d570 in cmd_main ../git.c:955\n     #16 0x00000044a54f in main ../common-main.c:9\n     #17 0x7f9e25e105b4 in __libc_start_call_main \n(/lib64/libc.so.6+0x35b4) (BuildId: \n2b5beec0fd24fe9c9f43eddfdd5facf0b8a1b805)\n     #18 0x7f9e25e10667 in __libc_start_main@@GLIBC_2.34 \n(/lib64/libc.so.6+0x3667) (BuildId: \n2b5beec0fd24fe9c9f43eddfdd5facf0b8a1b805)\n     #19 0x00000044c944 in _start \n(/home/jekeller/libexec/git-core/git+0x44c944) (BuildId: \n674cf04ebc1da782eede3c3be79a0c15f372df4c)\n\nSUMMARY: AddressSanitizer: 1453 byte(s) leaked in 37 allocation(s).\n\nThis leak occurs because you now copy and store the value of the alias \nin the util element, but the call of list_aliases() in list_cmd() \ndoesn't clean these up, since its string_list_clear passes 0 to the \nfree_util argument.\n\nThe following fixed it for me:\n\ndiff --git c/git.c i/git.c\nindex 744cb6527e06..aeb099ab1162 100644\n--- c/git.c\n+++ i/git.c\n@@ -119,7 +119,7 @@ static int list_cmds(const char *spec)\n         }\n         for (size_t i = 0; i < list.nr; i++)\n                 puts(list.items[i].string);\n-       string_list_clear(&list, 0);\n+       string_list_clear(&list, 1);\n         return 0;\n  }\n\n\n"},{"id":"537036","messageId":"xmqq1pi9epc9.fsf@gitster.g","threadId":"64949","inReplyTo":"6953f6f2-22e8-4efb-8169-395e1c52634f@intel.com","subject":"Re: [PATCH v7 1/4] help: use list_aliases() for alias listing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-24T22:41:58Z","receivedAt":"2026-02-24T22:42:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jacob Keller <jacob.e.keller@intel.com> writes:\n\n> SUMMARY: AddressSanitizer: 1453 byte(s) leaked in 37 allocation(s).\n>\n> This leak occurs because you now copy and store the value of the alias \n> in the util element, but the call of list_aliases() in list_cmd() \n> doesn't clean these up, since its string_list_clear passes 0 to the \n> free_util argument.\n>\n> The following fixed it for me:\n>\n> diff --git c/git.c i/git.c\n> index 744cb6527e06..aeb099ab1162 100644\n> --- c/git.c\n> +++ i/git.c\n> @@ -119,7 +119,7 @@ static int list_cmds(const char *spec)\n>          }\n>          for (size_t i = 0; i < list.nr; i++)\n>                  puts(list.items[i].string);\n> -       string_list_clear(&list, 0);\n> +       string_list_clear(&list, 1);\n>          return 0;\n>   }\n\nThanks. This looks like one of the right things to do.  I checked\nall list_*() that are called from the loop in this list_cmds(), and\nlist_aliases() is the only thing that uses .util member of the\nstring_list_item instances.\n\nHowever, we need to be a bit careful with list_cmds_by_config().  It\nsorts the list accumulated so far, uses remove_duplicates() on it\nwithout passing free_util=1, so there is also the same kind of leak\nthere, I suspect, until we adjust the call there.\n"},{"id":"537039","messageId":"2cf27229-3d4d-4578-b11f-db34701beeb7@app.fastmail.com","threadId":"64949","inReplyTo":"4b80f709-d883-4565-9fe0-d92c80a7ef0f@jontes.page","subject":"Re: [PATCH v7 3/4] alias: support non-alphanumeric names via subsection syntax","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-24T23:23:50Z","receivedAt":"2026-02-24T23:24:13Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Feb 24, 2026, at 15:48, Jonatan Holmgren wrote:\n>> Is this `rätta till` supposed to have a space in it?\n> Absolutely, as a demo of space working fine\n\nAha, okay. Thanks.\n"},{"id":"537143","messageId":"xmqq8qcga6ya.fsf@gitster.g","threadId":"64949","inReplyTo":"xmqq1pi9epc9.fsf@gitster.g","subject":"Re: [PATCH v7 1/4] help: use list_aliases() for alias listing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T20:45:01Z","receivedAt":"2026-02-25T20:45:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Jacob Keller <jacob.e.keller@intel.com> writes:\n>\n>> SUMMARY: AddressSanitizer: 1453 byte(s) leaked in 37 allocation(s).\n>>\n>> This leak occurs because you now copy and store the value of the alias \n>> in the util element, but the call of list_aliases() in list_cmd() \n>> doesn't clean these up, since its string_list_clear passes 0 to the \n>> free_util argument.\n>>\n>> The following fixed it for me:\n>>\n>> diff --git c/git.c i/git.c\n>> index 744cb6527e06..aeb099ab1162 100644\n>> --- c/git.c\n>> +++ i/git.c\n>> @@ -119,7 +119,7 @@ static int list_cmds(const char *spec)\n>>          }\n>>          for (size_t i = 0; i < list.nr; i++)\n>>                  puts(list.items[i].string);\n>> -       string_list_clear(&list, 0);\n>> +       string_list_clear(&list, 1);\n>>          return 0;\n>>   }\n>\n> Thanks. This looks like one of the right things to do.  I checked\n> all list_*() that are called from the loop in this list_cmds(), and\n> list_aliases() is the only thing that uses .util member of the\n> string_list_item instances.\n>\n> However, we need to be a bit careful with list_cmds_by_config().  It\n> sorts the list accumulated so far, uses remove_duplicates() on it\n> without passing free_util=1, so there is also the same kind of leak\n> there, I suspect, until we adjust the call there.\n\nFWIW, here is what I tentatively queued on top of these four\npatches.  Hopefully we can have a small and final reroll for these\n\"Fix small issues in alias\" patches and merge them down to 'next'\nsoonish?\n\nThanks.\n\n\ndiff --git a/git.c b/git.c\nindex c5fad56813..b5eb740e83 100644\n--- a/git.c\n+++ b/git.c\n@@ -119,7 +119,7 @@ static int list_cmds(const char *spec)\n \t}\n \tfor (size_t i = 0; i < list.nr; i++)\n \t\tputs(list.items[i].string);\n-\tstring_list_clear(&list, 0);\n+\tstring_list_clear(&list, 1);\n \treturn 0;\n }\n \ndiff --git a/help.c b/help.c\nindex 82fb2eaa3f..725e92a195 100644\n--- a/help.c\n+++ b/help.c\n@@ -423,7 +423,7 @@ void list_cmds_by_config(struct string_list *list)\n \t\treturn;\n \n \tstring_list_sort(list);\n-\tstring_list_remove_duplicates(list, 0);\n+\tstring_list_remove_duplicates(list, 1);\n \n \twhile (*cmd_list) {\n \t\tstruct strbuf sb = STRBUF_INIT;\n-- \n2.53.0-514-g5fc6f9e594\n\n"},{"id":"537209","messageId":"xmqqa4wv4ezb.fsf@gitster.g","threadId":"64949","inReplyTo":"20260224171245.458377-1-jonatan@jontes.page","subject":"Re: [PATCH 0/2] Fix small issues in alias subsection handling","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-26T17:00:24Z","receivedAt":"2026-02-26T17:00:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> Hello!\n>\n> I have two small patches related to the handling of alias subsections. \n> The first one is a documentation fix for the example showing the equivalence\n> between alias.last and alias.last.command, which was missing list continuation marks. \n> The second patch addresses a compatibility issue where an empty subsection ([alias \"\"])\n> was not treated as a plain [alias], \n> causing existing entries stored this way to be ignored.\n>\n> Thanks for considering these patches!\n>\n> Jonatan Holmgren (2):\n>   doc: fix list continuation in alias subsection example\n>   alias: treat empty subsection [alias \"\"] as plain [alias]\n>\n>  Documentation/config/alias.adoc |  7 ++++---\n>  alias.c                         |  4 ++++\n>  t/t0014-alias.sh                | 14 ++++++++++++++\n>  3 files changed, 22 insertions(+), 3 deletions(-)\n\nMemory leaks have been reported for these patches, e.g.,\n\n  https://lore.kernel.org/git/6953f6f2-22e8-4efb-8169-395e1c52634f@intel.com/\n\nI _think_ the following would be sufficient to plug it, but please\ndouble check and then send in updated version of these two patches\nincorporating them.\n\nThanks.\n\n\n\n git.c  | 2 +-\n help.c | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/git.c b/git.c\nindex c5fad56813..b5eb740e83 100644\n--- a/git.c\n+++ b/git.c\n@@ -119,7 +119,7 @@ static int list_cmds(const char *spec)\n \t}\n \tfor (size_t i = 0; i < list.nr; i++)\n \t\tputs(list.items[i].string);\n-\tstring_list_clear(&list, 0);\n+\tstring_list_clear(&list, 1);\n \treturn 0;\n }\n \ndiff --git a/help.c b/help.c\nindex 82fb2eaa3f..725e92a195 100644\n--- a/help.c\n+++ b/help.c\n@@ -423,7 +423,7 @@ void list_cmds_by_config(struct string_list *list)\n \t\treturn;\n \n \tstring_list_sort(list);\n-\tstring_list_remove_duplicates(list, 0);\n+\tstring_list_remove_duplicates(list, 1);\n \n \twhile (*cmd_list) {\n \t\tstruct strbuf sb = STRBUF_INIT;\n-- \n2.53.0-517-g7bf7f89b4a\n\n\n"},{"id":"537226","messageId":"20260226205339.1535482-1-jonatan@jontes.page","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"[PATCH v2 0/3] Fix small issues in alias subsection handling","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-26T20:53:25Z","receivedAt":"2026-02-26T20:54:11Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"v2: Add patch 3 to fix memory leaks in alias listing reported by\n    Jacob Keller.  The leaks were introduced by the jh/alias-i18n\n    series (specifically \"help: use list_aliases() for alias listing\"):\n    list_aliases() allocates util pointers but two callers cleared\n    the list without freeing them.\n\n    Also fix a stray trailing whitespace in patch 1.\n\n    No changes in patch 2.\n\nJonatan Holmgren (3):\n  doc: fix list continuation in alias subsection example\n  alias: treat empty subsection [alias \"\"] as plain [alias]\n  git, help: fix memory leaks in alias listing\n\n Documentation/config/alias.adoc |  7 ++++---\n alias.c                         |  4 ++++\n git.c                           |  2 +-\n help.c                          |  2 +-\n t/t0014-alias.sh                | 14 ++++++++++++++\n 5 files changed, 24 insertions(+), 5 deletions(-)\n\n-- \n2.53.0\n\n"},{"id":"537227","messageId":"20260226205339.1535482-3-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260226205339.1535482-1-jonatan@jontes.page","subject":"[PATCH v2 2/3] alias: treat empty subsection [alias \"\"] as plain [alias]","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-26T20:53:27Z","receivedAt":"2026-02-26T20:54:11Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"When git-config stores a key of the form alias..name, it records\nit under an empty subsection ([alias \"\"]). The new subsection-aware\nalias lookup would see a non-NULL but zero-length subsection and\nfall into the subsection code path, where it required a \"command\"\nkey and thus silently ignored the entry.\n\nNormalize an empty subsection to NULL before any further processing\nso that entries stored this way continue to work as plain\ncase-insensitive aliases, matching the pre-subsection behaviour.\n\nUsers who relied on alias..name to create an alias literally named\n\".name\" may want to migrate to subsection syntax, which looks less confusing:\n\n    [alias \".name\"]\n        command = <value>\n\nAdd tests covering both the empty-subsection compatibility case and\nthe leading-dot alias via the new syntax.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n alias.c          |  4 ++++\n t/t0014-alias.sh | 14 ++++++++++++++\n 2 files changed, 18 insertions(+)\n\ndiff --git a/alias.c b/alias.c\nindex 0d636278bc..ec9833dd30 100644\n--- a/alias.c\n+++ b/alias.c\n@@ -30,6 +30,10 @@ static int config_alias_cb(const char *var, const char *value,\n \t * - [alias \"name\"]\n \t *       command = value  (with subsection, case-sensitive)\n \t */\n+\t/* Treat [alias \"\"] (empty subsection) the same as plain [alias]. */\n+\tif (subsection && !subsection_len)\n+\t\tsubsection = NULL;\n+\n \tif (subsection && strcmp(key, \"command\"))\n \t\treturn 0;\n \ndiff --git a/t/t0014-alias.sh b/t/t0014-alias.sh\nindex 34bbdb51c5..68b4903cbf 100755\n--- a/t/t0014-alias.sh\n+++ b/t/t0014-alias.sh\n@@ -183,4 +183,18 @@ test_expect_success 'subsection aliases listed in help -a' '\n \ttest_grep \"förgrena\" output\n '\n \n+test_expect_success 'empty subsection treated as no subsection' '\n+\ttest_config \"alias..something\" \"!echo foobar\" &&\n+\tgit something >actual &&\n+\techo foobar >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'alias with leading dot via subsection syntax' '\n+\ttest_config alias.\".something\".command \"!echo foobar\" &&\n+\tgit .something >actual &&\n+\techo foobar >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.53.0\n\n"},{"id":"537228","messageId":"20260226205339.1535482-2-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260226205339.1535482-1-jonatan@jontes.page","subject":"[PATCH v2 1/3] doc: fix list continuation in alias subsection example","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-26T20:53:26Z","receivedAt":"2026-02-26T20:54:11Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"The example showing the equivalence between alias.last and\nalias.last.command was missing the list continuation marks (+\nbetween the shell session block and the following prose, leaving\nthe paragraph detached from the list item in the rendered output.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n Documentation/config/alias.adoc | 7 ++++---\n 1 file changed, 4 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\nindex 115fdbb1e3..26949a0ccb 100644\n--- a/Documentation/config/alias.adoc\n+++ b/Documentation/config/alias.adoc\n@@ -30,13 +30,14 @@ Examples:\n ----\n +\n With a Git alias defined, e.g.,\n-\n++\n     $ git config --global alias.last \"cat-file commit HEAD\"\n     # Which is equivalent to\n     $ git config --global alias.last.command \"cat-file commit HEAD\"\n++\n+`git last` is equivalent to `git cat-file commit HEAD`.\n \n-`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n-confusion and troubles with script usage, aliases that\n+To avoid confusion and troubles with script usage, aliases that\n hide existing Git commands are ignored except for deprecated\n commands.  Arguments are split by\n spaces, the usual shell quoting and escaping are supported.\n-- \n2.53.0\n\n"},{"id":"537229","messageId":"20260226205339.1535482-4-jonatan@jontes.page","threadId":"64949","inReplyTo":"20260226205339.1535482-1-jonatan@jontes.page","subject":"[PATCH v2 3/3] git, help: fix memory leaks in alias listing","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-02-26T20:53:28Z","receivedAt":"2026-02-26T20:54:13Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"The list_aliases() function sets the util pointer of each list item to\na heap-allocated copy of the alias command value.  Two callers failed\nto free these util pointers:\n\n - list_cmds() in git.c collects a string list with STRING_LIST_INIT_DUP\n   and clears it with string_list_clear(&list, 0), which frees the\n   duplicated strings (strdup_strings=1) but not the util pointers.\n   Pass free_util=1 to free them.\n\n - list_cmds_by_config() in help.c calls string_list_sort_u(list, 0) to\n   deduplicate the list before processing completion.commands overrides.\n   When duplicate entries are removed, the util pointer of each discarded\n   item is leaked because free_util=0.  Pass free_util=1 to free them.\n\nReported-by: Jacob Keller <jacob.e.keller@intel.com>\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n git.c  | 2 +-\n help.c | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/git.c b/git.c\nindex 744cb6527e..aeb099ab11 100644\n--- a/git.c\n+++ b/git.c\n@@ -119,7 +119,7 @@ static int list_cmds(const char *spec)\n \t}\n \tfor (size_t i = 0; i < list.nr; i++)\n \t\tputs(list.items[i].string);\n-\tstring_list_clear(&list, 0);\n+\tstring_list_clear(&list, 1);\n \treturn 0;\n }\n \ndiff --git a/help.c b/help.c\nindex 95f576c5c8..3e59d07c37 100644\n--- a/help.c\n+++ b/help.c\n@@ -422,7 +422,7 @@ void list_cmds_by_config(struct string_list *list)\n \tif (repo_config_get_string_tmp(the_repository, \"completion.commands\", &cmd_list))\n \t\treturn;\n \n-\tstring_list_sort_u(list, 0);\n+\tstring_list_sort_u(list, 1);\n \n \twhile (*cmd_list) {\n \t\tstruct strbuf sb = STRBUF_INIT;\n-- \n2.53.0\n\n"},{"id":"537234","messageId":"xmqqqzq7w6ve.fsf@gitster.g","threadId":"64949","inReplyTo":"20260226205339.1535482-1-jonatan@jontes.page","subject":"Re: [PATCH v2 0/3] Fix small issues in alias subsection handling","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-26T21:08:05Z","receivedAt":"2026-02-26T21:08:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonatan Holmgren <jonatan@jontes.page> writes:\n\n> v2: Add patch 3 to fix memory leaks in alias listing reported by\n>     Jacob Keller.  The leaks were introduced by the jh/alias-i18n\n>     series (specifically \"help: use list_aliases() for alias listing\"):\n>     list_aliases() allocates util pointers but two callers cleared\n>     the list without freeing them.\n>\n>     Also fix a stray trailing whitespace in patch 1.\n>\n>     No changes in patch 2.\n\nThanks.  Looking good.\n\nLet's mark the topic for 'next'.\n"},{"id":"537258","messageId":"71a2c638-9dc4-4add-9306-b4110e6c499f@intel.com","threadId":"64949","inReplyTo":"xmqq8qcga6ya.fsf@gitster.g","subject":"Re: [PATCH v7 1/4] help: use list_aliases() for alias listing","fromName":"Jacob Keller","fromEmail":"jacob.e.keller@intel.com","sentAt":"2026-02-26T23:33:48Z","receivedAt":"2026-02-26T23:33:51Z","isPatch":true,"sender":{"key":"jacob.e.keller@intel.com","avatar":"https://avatars.githubusercontent.com/u/874719?v=4"},"body":"\n\nOn 2/25/2026 12:45 PM, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n>> Jacob Keller <jacob.e.keller@intel.com> writes:\n>>\n>>> SUMMARY: AddressSanitizer: 1453 byte(s) leaked in 37 allocation(s).\n>>>\n>>> This leak occurs because you now copy and store the value of the alias\n>>> in the util element, but the call of list_aliases() in list_cmd()\n>>> doesn't clean these up, since its string_list_clear passes 0 to the\n>>> free_util argument.\n>>>\n>>> The following fixed it for me:\n>>>\n>>> diff --git c/git.c i/git.c\n>>> index 744cb6527e06..aeb099ab1162 100644\n>>> --- c/git.c\n>>> +++ i/git.c\n>>> @@ -119,7 +119,7 @@ static int list_cmds(const char *spec)\n>>>           }\n>>>           for (size_t i = 0; i < list.nr; i++)\n>>>                   puts(list.items[i].string);\n>>> -       string_list_clear(&list, 0);\n>>> +       string_list_clear(&list, 1);\n>>>           return 0;\n>>>    }\n>>\n>> Thanks. This looks like one of the right things to do.  I checked\n>> all list_*() that are called from the loop in this list_cmds(), and\n>> list_aliases() is the only thing that uses .util member of the\n>> string_list_item instances.\n>>\n>> However, we need to be a bit careful with list_cmds_by_config().  It\n>> sorts the list accumulated so far, uses remove_duplicates() on it\n>> without passing free_util=1, so there is also the same kind of leak\n>> there, I suspect, until we adjust the call there.\n> \n> FWIW, here is what I tentatively queued on top of these four\n> patches.  Hopefully we can have a small and final reroll for these\n> \"Fix small issues in alias\" patches and merge them down to 'next'\n> soonish?\n> \n> Thanks.\n> \n> \n> diff --git a/git.c b/git.c\n> index c5fad56813..b5eb740e83 100644\n> --- a/git.c\n> +++ b/git.c\n> @@ -119,7 +119,7 @@ static int list_cmds(const char *spec)\n>   \t}\n>   \tfor (size_t i = 0; i < list.nr; i++)\n>   \t\tputs(list.items[i].string);\n> -\tstring_list_clear(&list, 0);\n> +\tstring_list_clear(&list, 1);\n>   \treturn 0;\n>   }\n>   \n> diff --git a/help.c b/help.c\n> index 82fb2eaa3f..725e92a195 100644\n> --- a/help.c\n> +++ b/help.c\n> @@ -423,7 +423,7 @@ void list_cmds_by_config(struct string_list *list)\n>   \t\treturn;\n>   \n>   \tstring_list_sort(list);\n> -\tstring_list_remove_duplicates(list, 0);\n> +\tstring_list_remove_duplicates(list, 1);\n>   \n>   \twhile (*cmd_list) {\n>   \t\tstruct strbuf sb = STRBUF_INIT;\n\nThis looks correct to me, and I didn't see anything missing. Thanks!\n"},{"id":"537653","messageId":"7dc9d03d-61e1-4bd9-8998-0423214d1ffe@app.fastmail.com","threadId":"64949","inReplyTo":"20260226205339.1535482-2-jonatan@jontes.page","subject":"Re: [PATCH v2 1/3] doc: fix list continuation in alias subsection example","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-03T09:41:57Z","receivedAt":"2026-03-03T09:44:44Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Thu, Feb 26, 2026, at 21:53, Jonatan Holmgren wrote:\n> The example showing the equivalence between alias.last and\n> alias.last.command was missing the list continuation marks (+\n> between the shell session block and the following prose, leaving\n> the paragraph detached from the list item in the rendered output.\n>\n> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>\n> ---\n>  Documentation/config/alias.adoc | 7 ++++---\n>  1 file changed, 4 insertions(+), 3 deletions(-)\n>\n> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\n> index 115fdbb1e3..26949a0ccb 100644\n> --- a/Documentation/config/alias.adoc\n> +++ b/Documentation/config/alias.adoc\n> @@ -30,13 +30,14 @@ Examples:\n>  ----\n>  +\n>  With a Git alias defined, e.g.,\n> -\n> ++\n>      $ git config --global alias.last \"cat-file commit HEAD\"\n>      # Which is equivalent to\n>      $ git config --global alias.last.command \"cat-file commit HEAD\"\n> ++\n> +`git last` is equivalent to `git cat-file commit HEAD`.\n>\n\nMissing list continuation.\n\n> -`git last` is equivalent to `git cat-file commit HEAD`. To avoid\n> -confusion and troubles with script usage, aliases that\n> +To avoid confusion and troubles with script usage, aliases that\n>  hide existing Git commands are ignored except for deprecated\n>  commands.  Arguments are split by\n>  spaces, the usual shell quoting and escaping are supported.\n> --\n> 2.53.0\n"},{"id":"537684","messageId":"20260303151238.1000191-1-jonatan@jontes.page","threadId":"64949","inReplyTo":"3124b359-2929-4f3f-9ac6-793277fe422b@jontes.page","subject":"[PATCH] doc: fix list continuation in alias.adoc","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-03-03T15:12:38Z","receivedAt":"2026-03-03T15:13:03Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"Add missing list continuation marks ('+') after code blocks and shell examples\nso paragraphs render correctly as part of the preceding list item.\n\nSigned-off-by: Jonatan Holmgren <jonatan@jontes.page>\n---\n Documentation/config/alias.adoc | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc\nindex 26949a0ccb..dc6ca0ee08 100644\n--- a/Documentation/config/alias.adoc\n+++ b/Documentation/config/alias.adoc\n@@ -36,7 +36,7 @@ With a Git alias defined, e.g.,\n     $ git config --global alias.last.command \"cat-file commit HEAD\"\n +\n `git last` is equivalent to `git cat-file commit HEAD`.\n-\n++\n To avoid confusion and troubles with script usage, aliases that\n hide existing Git commands are ignored except for deprecated\n commands.  Arguments are split by\n-- \n2.53.0\n\n"},{"id":"537685","messageId":"c16d5045-d557-4336-b4dc-962034faa65b@jontes.page","threadId":"64949","inReplyTo":"7dc9d03d-61e1-4bd9-8998-0423214d1ffe@app.fastmail.com","subject":"Re: [PATCH v2 1/3] doc: fix list continuation in alias subsection example!","fromName":"Jonatan Holmgren","fromEmail":"jonatan@jontes.page","sentAt":"2026-03-03T15:13:44Z","receivedAt":"2026-03-03T15:20:19Z","isPatch":true,"sender":{"key":"jonatan@jontes.page","avatar":"https://avatars.githubusercontent.com/u/84264740?v=4"},"body":"> Missing list continuation.\n\nCan't believe I missed that! Good catch.\n\nThanks!\n"}]}