threads / rfc / 64949

[RFC] Support UTF-8 characters in Git alias names

Subject: [RFC] Support UTF-8 characters in Git alias names

## tl;dr

88 messages between Feb 8, 2026 and Mar 3, 2026.

replies: 87people: 11as markdown or json

Jonatan Holmgren· Feb 8, 2026, 15:30 UTC · lore
Hi Git developers,

I'd like to propose adding support for UTF-8 characters in Git alias names to better support non-English speaking users as these are currently restricted to [a-zA-Z0-9-] which prevents users from creating aliases in languages with other characters. Shell aliases do not solve this as the best you could do is "git-förgrena".

This would allow users to set aliases such as:
     git config alias.förgrena branch # Swedish
     git config alias.分支 branch # Chinese
etc.
The restriction comes from iskeychar() in config.c:526-529:
     static inline int iskeychar(int c)
     {
	return isalnum(c) || c == '-';
     }

The function in question validates all Git config keys, not just aliases. The git_config_parse_key() function (same file, 543-598) enforces this validation and uses tolower(), which only works for ASCII.

I think the best approach is to support UTF-8 specifically for alias.* variables, which would mean modifying the git_config_parse_key() fn to allow UTF-8 bytes and make non-ascii aliases case-sensitive to avoid complex locale-dependent case folding.

The main pain point would be making sure all platforms handle this nicely, esp since mac uses NFD and not NFC Unicode.

Before implementing this, I'd like to hear:
1. Is this a feature the project would like?
2. Is my implementation approach reasonable?
3. What concerns should be addressed in said design?
4. Any compat requirements I should be aware of?
Thank you so much for your consideration!
Jonatan Holmgren
D. Ben Knoble· Feb 8, 2026, 16:07 UTC · re: Jonatan Holmgren · lore

Re: [RFC] Support UTF-8 characters in Git alias names

On Sun, Feb 8, 2026 at 10:37 AM Jonatan Holmgren <jonatan@jontes.page> wrote:
Show 27 quoted lines
>
> Hi Git developers,
>
> I'd like to propose adding support for UTF-8 characters in Git alias
> names to better support non-English speaking users as these are
> currently restricted to [a-zA-Z0-9-] which prevents users from creating
> aliases in languages with other characters. Shell aliases do not solve
> this as the best you could do is "git-förgrena".
>
>
> This would allow users to set aliases such as:
>
>      git config alias.förgrena branch # Swedish
>      git config alias.分支 branch # Chinese
>
> etc.
>
> The restriction comes from iskeychar() in config.c:526-529:
>
>      static inline int iskeychar(int c)
>      {
>         return isalnum(c) || c == '-';
>      }
>
> The function in question validates all Git config keys, not just
> aliases. The git_config_parse_key() function (same file, 543-598)
> enforces this validation and uses tolower(), which only works for ASCII.

In particular, this comes from sane-ctype.h and ctype.c, which I could not easily rip out of the build :/

It looks like Git's ctype definitions are ASCII-specific even though isalnum(3) on my system is documented as using the locale, so I was hoping that if I could build Git without it's own ctype.c I could test your aliases. Bummer.

It looks like this goes back to 4546738b58 (Unlocalized isspace and friends, 2005-10-13). I don't have an amlog note for it, but searching all of lore turned up <https://lore.kernel.org/all/Pine.LNX.4.64.0510130838240.15297@g5.osdl.org/>. In the same thread, [1] suggests we'd have to use more complex logic to parse UTF-8 config, right?

[1]: https://lore.kernel.org/all/434E8650.7060604@zytor.com/

Anyway, my opinion (not that it holds much weight) is that user-facing parts of Git ought to support international languages. (If the `diff` line for git-apply(1) has to be ASCII, that's probably fine [2]; it should definitely be parsed as a byte-stream.)

[2]: https://lore.kernel.org/all/7vachd6hdx.fsf@assigned-by-dhcp.cox.net/
-- 
D. Ben Knoble
brian m. carlson· Feb 8, 2026, 23:21 UTC · re: Jonatan Holmgren · lore

Re: [RFC] Support UTF-8 characters in Git alias names

On 2026-02-08 at 15:30:02, Jonatan Holmgren wrote:
> I think the best approach is to support UTF-8 specifically for alias.*
> variables, which would mean modifying the git_config_parse_key() fn to allow
> UTF-8 bytes and make non-ascii aliases case-sensitive to avoid complex
> locale-dependent case folding.

Yes, I don't think anyone should be relying on case folding for aliases. Not doing case folding also avoids locale problems with Turkic languages.

Our mailmap also doesn't do case-folding for non-ASCII characters, so there is some precedent, although I wish we were consistent by not doing case folding at all.

> The main pain point would be making sure all platforms handle this nicely,
> esp since mac uses NFD and not NFC Unicode.

I don't think this affects command-line arguments, though, which are just bytes. Since running `git paramétrer` would just be passing `paramétrer` as a command-line argument, then it should pass that value as an unnormalized UTF-8 byte string. It would only be normalized if Git invoked `git-paramétrer` as a binary or script.

I don't think we have any Unicode normalization code at all in Git, though, so if you want a quality implementation, that may be a thing we need.

> Before implementing this, I'd like to hear:
> 
> 1. Is this a feature the project would like?

I think this would be useful. I don't personally plan to use it, but I can imagine a lot of other people would, and in general I'm in favour of better i18n and l10n support.

> 2. Is my implementation approach reasonable?
> 3. What concerns should be addressed in said design?

As I said, Unicode normalization may be a thing you want to support here. Not having it isn't a complete dealbreaker, but it would prevent hard-to-debug breakage.

-- 
brian m. carlson (they/them)
Toronto, Ontario, CA
Junio C Hamano· Feb 9, 2026, 14:55 UTC · re: brian m. carlson · lore

Re: [RFC] Support UTF-8 characters in Git alias names

"brian m. carlson" <sandals@crustytoothpaste.net> writes:
> I don't think we have any Unicode normalization code at all in Git,
> though, so if you want a quality implementation, that may be a thing we
> need.

Isn't NKC/NKD a macOS-only issue in practice? Anything on the command line "git" potty and "git-blah" built-in commands receive goes through precompose_argv_prefix() to be normalized on that platform.

Show 7 quoted lines
>> Before implementing this, I'd like to hear:
>> 
>> 1. Is this a feature the project would like?
>
> I think this would be useful.  I don't personally plan to use it, but I
> can imagine a lot of other people would, and in general I'm in favour of
> better i18n and l10n support.

I am not fundamentally against this, as long as such an addition does not introduce unnecessary bugs and ambiguities. IOW, do not force me to read bug reports in this area after it is done.

Show 6 quoted lines
>> 2. Is my implementation approach reasonable?
>> 3. What concerns should be addressed in said design?
>
> As I said, Unicode normalization may be a thing you want to support
> here.  Not having it isn't a complete dealbreaker, but it would prevent
> hard-to-debug breakage.

A buggy normalization implementation would be also a source of unnecessary bugs. We cannot have and eat that cake so easily ;-).

Jonatan Holmgren· Feb 9, 2026, 15:19 UTC · re: Junio C Hamano · lore

Re: [RFC] Support UTF-8 characters in Git alias names

Thanks for chiming in!
 > Isn't NKC/NKD a macOS-only issue in practice?  Anything on the
 > command line "git" potty and "git-blah" built-in commands receive
 > goes through precompose_argv_prefix() to be normalized on that
 > platform.

If we use Jeff's proposed alias.*.{keyname} approach with literal byte matching macOS should already handle the normalization at the argv level before Git even sees it, correct? I'm not very familiar with how macOS handles this.

 > I am not fundamentally against this, as long as such an addition
 > does not introduce unnecessary bugs and ambiguities.  IOW, do not
 > force me to read bug reports in this area after it is done.

Understood. Jeff's subsection approach seems safest, it uses existing config infrastructure that already supports arbitrary bytes in subsections. This would enable

     [alias "förgrena"]
         command = branch
as an alternative to (not a replacement for) the current ASCII-only:
     [alias]
         forgrena = branch
Is this sound?
Jonatan
On 2026-02-09 15:55, Junio C Hamano wrote:
Show 27 quoted lines
> "brian m. carlson"<sandals@crustytoothpaste.net> writes:
> 
>> I don't think we have any Unicode normalization code at all in Git,
>> though, so if you want a quality implementation, that may be a thing we
>> need.
> Isn't NKC/NKD a macOS-only issue in practice?  Anything on the
> command line "git" potty and "git-blah" built-in commands receive
> goes through precompose_argv_prefix() to be normalized on that
> platform.
> 
>>> Before implementing this, I'd like to hear:
>>>
>>> 1. Is this a feature the project would like?
>> I think this would be useful.  I don't personally plan to use it, but I
>> can imagine a lot of other people would, and in general I'm in favour of
>> better i18n and l10n support.
> I am not fundamentally against this, as long as such an addition
> does not introduce unnecessary bugs and ambiguities.  IOW, do not
> force me to read bug reports in this area after it is done.
> 
>>> 2. Is my implementation approach reasonable?
>>> 3. What concerns should be addressed in said design?
>> As I said, Unicode normalization may be a thing you want to support
>> here.  Not having it isn't a complete dealbreaker, but it would prevent
>> hard-to-debug breakage.
> A buggy normalization implementation would be also a source of
> unnecessary bugs.  We cannot have and eat that cake so easily 😉.
Junio C Hamano· Feb 9, 2026, 17:59 UTC · re: Jonatan Holmgren · lore

Re: [RFC] Support UTF-8 characters in Git alias names

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 11 quoted lines
> Thanks for chiming in!
>
>  > Isn't NKC/NKD a macOS-only issue in practice?  Anything on the
>  > command line "git" potty and "git-blah" built-in commands receive
>  > goes through precompose_argv_prefix() to be normalized on that
>  > platform.
>
> If we use Jeff's proposed alias.*.{keyname} approach with literal byte
> matching macOS should already handle the normalization at the argv level 
> before Git even sees it, correct? I'm not very familiar with how macOS 
> handles this.

I may have phrased it poorly, but the above (with Peff's syntax change, which I think is independently a good thing to do) is exactly what I meant. That way, we do not have to worry about normalization ourselves at all in this area.

Thanks.
brian m. carlson· Feb 9, 2026, 22:40 UTC · re: Junio C Hamano · lore

Re: [RFC] Support UTF-8 characters in Git alias names

On 2026-02-09 at 14:55:51, Junio C Hamano wrote:
Show 10 quoted lines
> "brian m. carlson" <sandals@crustytoothpaste.net> writes:
> 
> > I don't think we have any Unicode normalization code at all in Git,
> > though, so if you want a quality implementation, that may be a thing we
> > need.
> 
> Isn't NKC/NKD a macOS-only issue in practice?  Anything on the
> command line "git" potty and "git-blah" built-in commands receive
> goes through precompose_argv_prefix() to be normalized on that
> platform.

Normalization is not a macOS-only issue. Many accented characters can be written in multiple ways, one composed and one decomposed. If the alias in the file is composed and what's on the command line is decomposed, they will not match bytewise even though they are logically and graphically identical.

For instance, here is the word for "where" in French, first composed, then decomposed:

où où

The former is U+006F U+00F9 and the latter is U+006F U+0075 U+0300. Obviously, if I write one of those in my config file and the other on the command line, I intended to execute the same alias, but they are not bytewise identical unless both are normalized identically.

This is why many websites don't accept Unicode in passwords: because logging in on different systems can produce different sequences and they must be properly normalized to avoid hard-to-reproduce problems.

There are also canonical (NFC and NFD) and compatibility (NFKC and NFKD) normalizations. For instance, a Greek question mark looks like an English semicolon. Canonical normalizations preserve this distinction, but compatibility ones do not.

I'll note that the Mac-native normalizations do not match any standard Unicode normalizations for any version, so we'd need separate normalization code. I also don't think UTF-8-MAC is available on all versions of libiconv, either.

-- 
brian m. carlson (they/them)
Toronto, Ontario, CA
Junio C Hamano· Feb 9, 2026, 23:14 UTC · re: brian m. carlson · lore

Re: [RFC] Support UTF-8 characters in Git alias names

"brian m. carlson" <sandals@crustytoothpaste.net> writes:
Show 14 quoted lines
> On 2026-02-09 at 14:55:51, Junio C Hamano wrote:
>> "brian m. carlson" <sandals@crustytoothpaste.net> writes:
>> 
>> > I don't think we have any Unicode normalization code at all in Git,
>> > though, so if you want a quality implementation, that may be a thing we
>> > need.
>> 
>> Isn't NKC/NKD a macOS-only issue in practice?  Anything on the
>> command line "git" potty and "git-blah" built-in commands receive
>> goes through precompose_argv_prefix() to be normalized on that
>> platform.
>
> Normalization is not a macOS-only issue.  Many accented characters can
> be written in multiple ways,...

Yup, but that wasn't what I brought up macOS for. No sane person would write the same string in multiple ways on purpose and everybody would want to stick to one, so that byte-for-byte comparison can decide paths they created in the filesystem can be matched with a list of paths they added in .gitignore, for example. And for that everybody uses normalization form C, no?

But the macOS makes it harder to stick to a single way when it involves filesystem entities; the pathname you gave to a new file with your creat/open(2) may be normalized in macOS specific way when it comes back from readdir(2). Ahd for that glitch, we massage the strings we got from the command line and readdir(), which are in the normalization form D, into normalization form C.

I think the suggestion here is to assume that the users are doing the right thing and treat alias names (which eventually end up being pathname components) as bytes, and everything should be happy, even on macOS.

Ben Knoble· Feb 10, 2026, 00:45 UTC · re: Junio C Hamano · lore

Re: [RFC] Support UTF-8 characters in Git alias names

Show 38 quoted lines
> 
> Le 9 févr. 2026 à 18:14, Junio C Hamano <gitster@pobox.com> a écrit :
> 
> "brian m. carlson" <sandals@crustytoothpaste.net> writes:
> 
>>> On 2026-02-09 at 14:55:51, Junio C Hamano wrote:
>>> "brian m. carlson" <sandals@crustytoothpaste.net> writes:
>>> 
>>>> I don't think we have any Unicode normalization code at all in Git,
>>>> though, so if you want a quality implementation, that may be a thing we
>>>> need.
>>> 
>>> Isn't NKC/NKD a macOS-only issue in practice?  Anything on the
>>> command line "git" potty and "git-blah" built-in commands receive
>>> goes through precompose_argv_prefix() to be normalized on that
>>> platform.
>> 
>> Normalization is not a macOS-only issue.  Many accented characters can
>> be written in multiple ways,...
> 
> Yup, but that wasn't what I brought up macOS for.  No sane person
> would write the same string in multiple ways on purpose and
> everybody would want to stick to one, so that byte-for-byte
> comparison can decide paths they created in the filesystem can be
> matched with a list of paths they added in .gitignore, for example.
> And for that everybody uses normalization form C, no?
> 
> But the macOS makes it harder to stick to a single way when it
> involves filesystem entities; the pathname you gave to a new file
> with your creat/open(2) may be normalized in macOS specific way when
> it comes back from readdir(2).  Ahd for that glitch, we massage the
> strings we got from the command line and readdir(), which are in the
> normalization form D, into normalization form C.
> 
> I think the suggestion here is to assume that the users are doing
> the right thing and treat alias names (which eventually end up being
> pathname components) as bytes, and everything should be happy, even
> on macOS.
In what way do alias names end up being pathname components? Or did you mean to insert « treated like » (as in, normalized as command arguments)?
Junio C Hamano· Feb 10, 2026, 01:04 UTC · re: Ben Knoble · lore

Re: [RFC] Support UTF-8 characters in Git alias names

Ben Knoble <ben.knoble@gmail.com> writes:
> In what way do alias names end up being pathname components? Or
> did you mean to insert « treated like » (as in, normalized as
> command arguments)?

Ah, they don't. I somehow was confusing those custom commands you would throw into your ~/bin as "git-something" ;-).

But you're correct to point out that for a command line "git something", "something" would go through the same "undo macOS NFD for all argv[] elements".

Jeff King· Feb 10, 2026, 06:59 UTC · re: Junio C Hamano · lore

Re: [RFC] Support UTF-8 characters in Git alias names

On Mon, Feb 09, 2026 at 05:04:18PM -0800, Junio C Hamano wrote:
Show 12 quoted lines
> Ben Knoble <ben.knoble@gmail.com> writes:
> 
> > In what way do alias names end up being pathname components? Or
> > did you mean to insert « treated like » (as in, normalized as
> > command arguments)?
> 
> Ah, they don't.  I somehow was confusing those custom commands you
> would throw into your ~/bin as "git-something" ;-).
> 
> But you're correct to point out that for a command line "git
> something", "something" would go through the same "undo macOS NFD
> for all argv[] elements".

It's not an issue for aliases, but pager.* (and a hypothetical pager.*.command) does care about the on-disk representation of program names (and that it matches what is fed from the command-line).

-Peff
Jeff King· Feb 9, 2026, 07:36 UTC · re: Jonatan Holmgren · lore

Re: [RFC] Support UTF-8 characters in Git alias names

On Sun, Feb 08, 2026 at 04:30:02PM +0100, Jonatan Holmgren wrote:
Show 14 quoted lines
> I think the best approach is to support UTF-8 specifically for alias.*
> variables, which would mean modifying the git_config_parse_key() fn to allow
> UTF-8 bytes and make non-ascii aliases case-sensitive to avoid complex
> locale-dependent case folding.
> 
> The main pain point would be making sure all platforms handle this nicely,
> esp since mac uses NFD and not NFC Unicode.
> 
> Before implementing this, I'd like to hear:
> 
> 1. Is this a feature the project would like?
> 2. Is my implementation approach reasonable?
> 3. What concerns should be addressed in said design?
> 4. Any compat requirements I should be aware of?
I think supporting non-ascii aliases is a good goal.

However, I'm not sure that special-casing the parsing of alias config keys is the best direction. Since it's a syntactic change, the special case would have to be understand by all code that reads or writes config, not just git_config_parse_key(). And then you'd potentially run into problems with older versions of Git, or alternate implementations (of which there are several).

Plus it doesn't solve all of the issues. E.g., should we allow new characters like "_" (for a potential "git foo_bar")? That is doable, but what about "." (for "git foo.bar")? I think that introduces new ambiguities into the syntax.

Taking a step back, I think the root of the issue is that the schema for alias keys is poorly designed. Git's config syntax allows for three levels: section, subsection, and key. The section and key fields are restricted to alnum and dash, but the subsection is designed to be unrestricted (modulo NUL bytes).

And that's why we have:
  [branch "foo/bar"]
  remote = origin

for example, because branch names don't follow the same syntax rules as config keys. And it's the same issue here: the alias.* schema is trying to use one syntax (alnum config keys) to store another (command names). They _usually_ overlap, but not always. The pager.* config has the same problem.

We've discussed this before, e.g., in:
  https://lore.kernel.org/git/20150206124528.GA18859@inner.h.apk.li/

There the immediate problem was that "git foo_bar" caused an error message. We hacked around it by suppressing the error, but it was still impossible to add an alias or pager config. We knew that was a limitation, but punted until somebody came along who actually cared about making it work. Now you get to be that somebody. ;)

So what I'd propose instead is introducing a new schema like:
  - setting "alias.foo.command" to "bar" would alias "git foo" to "bar";
    this should work for any command name, as it is just a byte stream
  - a given command subsection is matched verbatim. So alias.foo.command
    matches "git foo" but not "git Foo". Likewise, we do not do any
    normalization. You put what you want into your config, and it should
    match the command you invoke. This is perhaps less friendly, but it
    punts on any normalization or case-folding that we have to do, and
    matches how the rest of Git works (paths are likewise streams of
    bytes, and it is mostly up to the user to use them consistently).
  - leave "alias.foo" as a historical synonym for "alias.foo.command",
    so that existing config continues working
  - optionally add new keys within alias.foo.* sections. For example, we
    could allow alias.foo.help to provide text shown during "git help
    foo". For the most part that could come later, so I'm just
    illustrating possible eventual directions that the new schema would
    allow. But it might be worth pondering a little now to avoid
    painting ourselves into a corner. E.g., you could imagine a schema
    where alias.foo.shell is set to "true" instead of sticking a "!" at
    the front of the value of alias.foo.command. I don't know if that's
    a good idea or not, but if we were going to do stuff like that, we'd
    want to decide now before setting the alias.foo.command behavior in
    stone.
  - likewise, optionally do the same for pager.*

I hacked together some illustrative code below. Note that we do use strcasecmp() currently to match command names (which kind of makes sense, since if you had "alias.Foo" in your config, the parser would downcase it to "alias.foo"). So probably that historical code should continue to behave like that, but the new "alias.Foo.command" should be more verbatim (the patch below just feeds them both to strcasecmp).

-Peff
---
diff --git a/alias.c b/alias.c
index 1a1a141a0a..44bdde58af 100644
--- a/alias.c
+++ b/alias.c
@@ -17,19 +17,30 @@ static int config_alias_cb(const char *key, const char *value,
 			   const struct config_context *ctx UNUSED, void *d)
 {
 	struct config_alias_data *data = d;
-	const char *p;
+	const char *cmd, *p;
+	size_t cmd_len;
 
-	if (!skip_prefix(key, "alias.", &p))
+	if (parse_config_key(key, "alias", &cmd, &cmd_len, &p) < 0)
 		return 0;
 
+	if (cmd) {
+		/* The only 3-level key we understand is alias.*.command */
+		if (strcmp(p, "command"))
+			return 0;
+	} else {
+		/* alias.foo is the same as alias.foo.command */
+		cmd = p;
+		cmd_len = strlen(p);
+	}
+
 	if (data->alias) {
-		if (!strcasecmp(p, data->alias)) {
+		if (!strncasecmp(cmd, data->alias, cmd_len)) {
 			FREE_AND_NULL(data->v);
 			return git_config_string(&data->v,
 						 key, value);
 		}
 	} else if (data->list) {
-		string_list_append(data->list, p);
+		string_list_append_nodup(data->list, xmemdupz(cmd, cmd_len));
 	}
 
 	return 0;
Theodore Tso· Feb 9, 2026, 13:59 UTC · re: Jeff King · lore

Re: [RFC] Support UTF-8 characters in Git alias names

I would just note that Unicode normalization can be *tricky*. The fact that ext4 case folding would do Unicode normaliation was sufficient to trigger a CVE that resulted a security vulnerability in git. Of course, who would be insane enough to use git on a case folded file system (aside from someone running git instead Termux on an Android device, that is....)?

So I'd suggest that git folks think very carefully before trying to dive into the insanity which is Unicode. It might be a very strong nice-to-have for certain user communities, but it's also a mess.

	     	 	      		   - Ted
Jonatan Holmgren· Feb 9, 2026, 22:01 UTC · re: Jonatan Holmgren · lore

[PATCH v1] alias: support UTF-8 characters via subsection syntax

Git aliases are currently restricted to ASCII characters due to config key syntax limitations. This prevents non-English speakers from creating aliases in their native languages.

Add support for UTF-8 alias names using config subsections:
    [alias "förgrena"]
        command = branch

The subsection name is matched verbatim (case-sensitive), while the existing flat syntax (alias.name) remains case-insensitive for backward compatibility. This approach uses existing config infrastructure and avoids complex Unicode normalization.

Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 Documentation/RelNotes/2.54.0.adoc |  8 +++++
 Documentation/config/alias.adoc    | 53 +++++++++++++++++++++++++-----
 alias.c                            | 38 +++++++++++++++++----
 t/t0014-alias-utf8.sh              | 44 +++++++++++++++++++++++++
 t/t0014-alias.sh                   | 32 ++++++++++++++++++
 5 files changed, 161 insertions(+), 14 deletions(-)
 create mode 100755 t/t0014-alias-utf8.sh
diff --git a/Documentation/RelNotes/2.54.0.adoc b/Documentation/RelNotes/2.54.0.adoc
index 20c660d82a..8cb00a21bb 100644
--- a/Documentation/RelNotes/2.54.0.adoc
+++ b/Documentation/RelNotes/2.54.0.adoc
@@ -7,6 +7,14 @@ UI, Workflows & Features
  * "git add -p" and friends note what the current status of the hunk
    being shown is.
 
+ * Git aliases now support UTF-8 characters in alias names through
+   subsection syntax: `[alias "name"] command = value`. This enables
+   aliases in non-English languages. The flat syntax continues
+   to work for backward compatibility.
+
+ * The new subsection syntax uses case-sensitive matching and
+   the flat syntax remains case-insensitive for backward compatibility.
+
 
 Performance, Internal Implementation, Development Support etc.
 --------------------------------------------------------------
diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
index 80ce17d2de..feba1e2022 100644
--- a/Documentation/config/alias.adoc
+++ b/Documentation/config/alias.adoc
@@ -1,12 +1,49 @@
 alias.*::
-	Command aliases for the linkgit:git[1] command wrapper - e.g.
-	after defining `alias.last = cat-file commit HEAD`, the invocation
-	`git last` is equivalent to `git cat-file commit HEAD`. To avoid
-	confusion and troubles with script usage, aliases that
-	hide existing Git commands are ignored except for deprecated
-	commands.  Arguments are split by
-	spaces, the usual shell quoting and escaping are supported.
-	A quote pair or a backslash can be used to quote them.
+alias.*.command::
+	Command aliases for the linkgit:git[1] command wrapper. Aliases
+	can be defined using two syntaxes:
++
+--
+1. **Simple syntax** (case-insensitive): `alias.name = value`
+2. **Subsection syntax** (recommended for UTF-8/special characters):
+   `[alias "name"]` with `command = value`
+
+The subsection syntax allows alias names containing UTF-8 characters,
+spaces, or special characters, as the subsection preserves the exact
+bytes including case.
+--
++
+Examples:
++
+----
+# Simple syntax (ASCII names)
+[alias]
+    co = checkout
+    st = status
+
+# Subsection syntax (UTF-8 and special characters)
+[alias "hämta"]
+    command = fetch
+[alias "gömma"]
+    command = stash
+[alias "my alias with whitespace"]
+    command = "status --short"
+----
++
+After defining these, you can run `git co`, `git hämta`, `git gömma`,
+and `git "my alias with whitespace"` (note the quotes for aliases with spaces).
++
+**Note:** The flat syntax `alias.name` remains case-insensitive for
+backward compatibility. The new subsection syntax is case-sensitive:
+`[alias "Foo"]` and `[alias "foo"]` are different aliases.
++
+E.g. after defining `alias.last = cat-file commit HEAD`, the invocation
+`git last` is equivalent to `git cat-file commit HEAD`. To avoid
+confusion and troubles with script usage, aliases that
+hide existing Git commands are ignored except for deprecated
+commands.  Arguments are split by
+spaces, the usual shell quoting and escaping are supported.
+A quote pair or a backslash can be used to quote them.
 +
 Note that the first word of an alias does not necessarily have to be a
 command. It can be a command-line option that will be passed into the
diff --git a/alias.c b/alias.c
index 1a1a141a0a..f0c5f12fdd 100644
--- a/alias.c
+++ b/alias.c
@@ -17,19 +17,45 @@ static int config_alias_cb(const char *key, const char *value,
 			   const struct config_context *ctx UNUSED, void *d)
 {
 	struct config_alias_data *data = d;
-	const char *p;
+	const char *cmd, *subkey;
+	size_t cmd_len;
+	int is_subsection;
 
-	if (!skip_prefix(key, "alias.", &p))
+	/* Use parse_config_key() to handle both 2-level and 3-level keys */
+	if (parse_config_key(key, "alias", &cmd, &cmd_len, &subkey) < 0)
 		return 0;
 
+	/*
+	 * Support two syntaxes:
+	 * 1. alias.name = value (simple, 2-level key)
+	 * 2. [alias "name"] command = value (new, 3-level key)
+	 */
+	if (cmd) {
+		if (strcmp(subkey, "command"))
+			return 0;
+		is_subsection = 1;
+	} else {
+		cmd = subkey;
+		cmd_len = strlen(cmd);
+		is_subsection = 0;
+	}
+
 	if (data->alias) {
-		if (!strcasecmp(p, data->alias)) {
+		int match;
+		if (is_subsection) {
+			match = (strlen(data->alias) == cmd_len &&
+				 !strncmp(data->alias, cmd, cmd_len));
+		} else {
+			match = (strlen(data->alias) == cmd_len &&
+				 !strncasecmp(data->alias, cmd, cmd_len));
+		}
+
+		if (match) {
 			FREE_AND_NULL(data->v);
-			return git_config_string(&data->v,
-						 key, value);
+			return git_config_string(&data->v, key, value);
 		}
 	} else if (data->list) {
-		string_list_append(data->list, p);
+		string_list_append_nodup(data->list, xmemdupz(cmd, cmd_len));
 	}
 
 	return 0;
diff --git a/t/t0014-alias-utf8.sh b/t/t0014-alias-utf8.sh
new file mode 100755
index 0000000000..d55b5a7213
--- /dev/null
+++ b/t/t0014-alias-utf8.sh
@@ -0,0 +1,44 @@
+#!/bin/sh
+
+test_description='UTF-8 support in git aliases via subsection syntax'
+
+. ./test-lib.sh
+
+# Skip if filesystem/locale doesn't support UTF-8
+test_lazy_prereq UTF8_LOCALE '
+	test_have_prereq !MINGW &&
+	test_set_prereq UTF8_LOCALE
+'
+
+test_expect_success 'setup test repository' '
+	git init &&
+	test_commit initial
+'
+
+test_expect_success UTF8_LOCALE 'setup UTF-8 aliases' '
+	git config alias."förgrena".command branch &&
+	git config alias."分支".command "branch --list" &&
+	git config alias."test name".command status
+'
+
+test_expect_success UTF8_LOCALE 'UTF-8 alias with Swedish characters' '
+	git förgrena >output &&
+	test_grep -E "^(\* )?(main|master)" output
+'
+
+test_expect_success UTF8_LOCALE 'UTF-8 alias with CJK characters' '
+	git 分支 >output &&
+	test_grep -E "^(\* )?(main|master)" output
+'
+
+test_expect_success UTF8_LOCALE 'alias with spaces in name' '
+	git "test name" >output &&
+	test_grep "On branch" output
+'
+
+test_expect_success 'list UTF-8 aliases' '
+	git config --get-regexp "^alias\\..*\\.command" >output &&
+	test_line_count -ge 3 output
+'
+
+test_done
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index 07a53e7366..b19a8a5061 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -111,5 +111,37 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '
 		cannot_alias_regular_builtin "$cmd" || return 1
 	done
 '
+test_expect_success 'flat syntax still works' '
+	git config alias.testlegacy status &&
+	git testlegacy >output &&
+	test_grep "On branch" output
+'
+
+test_expect_success 'new subsection syntax works' '
+	git config alias.testnew.command status &&
+	git testnew >output &&
+	test_grep "On branch" output
+'
+
+test_expect_success 'subsection syntax only accepts command key' '
+	git config alias.invalid.notcommand "value" &&
+	test_must_fail git invalid 2>error &&
+	test_grep -i "not a git command" error
+'
+
+test_expect_success 'simple syntax is case-insensitive' '
+	git config alias.LegacyCase status &&
+	git legacycase >output 2>&1 &&
+	test_grep "On branch" output
+'
+
+test_expect_success 'subsection syntax is case-sensitive' '
+	test_commit case-test &&
+	git config alias.SubCase.command "log --oneline" &&
+	git config alias.subcase.command status &&
+	git SubCase >upper.out 2>&1 &&
+	git subcase >lower.out 2>&1 &&
+	! test_cmp upper.out lower.out
+'
 
 test_done
-- 
2.53.0
Jeff King· Feb 10, 2026, 07:44 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v1] alias: support UTF-8 characters via subsection syntax

On Mon, Feb 09, 2026 at 11:01:15PM +0100, Jonatan Holmgren wrote:
Show 13 quoted lines
> Git aliases are currently restricted to ASCII characters due to
> config key syntax limitations. This prevents non-English speakers
> from creating aliases in their native languages.
> 
> Add support for UTF-8 alias names using config subsections:
> 
>     [alias "förgrena"]
>         command = branch
> 
> The subsection name is matched verbatim (case-sensitive), while the
> existing flat syntax (alias.name) remains case-insensitive for
> backward compatibility. This approach uses existing config
> infrastructure and avoids complex Unicode normalization.

Thanks, this mostly looks quite good. I have a few comments below that range from a few small bugs to project-specific gotchas to style/readability suggestions.

> diff --git a/Documentation/RelNotes/2.54.0.adoc b/Documentation/RelNotes/2.54.0.adoc
> [...]

Usually patches do not fill out their own release notes, as it would create many conflicts when merging topics in arbitrary order. Instead, the maintainer generally writes up a blurb that goes in the merge commits and is eventually placed in the release notes by script. Suggesting a blurb does save work for the maintainer, but it should go in the cover letter (so for a single patch, after the "---" lines).

There's a bit on this in Documentation/SubmittingPatches, under the section "the-topic-summary".

> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
> index 80ce17d2de..feba1e2022 100644
> --- a/Documentation/config/alias.adoc
> +++ b/Documentation/config/alias.adoca

Yay, I was happy to see nice comprehensive documentation here. And you do not seem to have gotten caught by any of the (many) formatting gotchas.

> +After defining these, you can run `git co`, `git hämta`, `git gömma`,
> +and `git "my alias with whitespace"` (note the quotes for aliases with spaces).

The use of literal backticks here makes sense, and if you haven't disabled it with a build-time knob, they should be shown in bold. Curiously in my build of the docs this doesn't quite work, and I get "git h" in bold, but "ämta" unbolded. Weird, and presumably a bug in asciidoc (the generated docbook xml shows the same thing, as does html). I get the same with asciidoctor, too. I'm not sure if it is worth working around it (or even how we would do so).

Show 32 quoted lines
> diff --git a/alias.c b/alias.c
> index 1a1a141a0a..f0c5f12fdd 100644
> --- a/alias.c
> +++ b/alias.c
> @@ -17,19 +17,45 @@ static int config_alias_cb(const char *key, const char *value,
>  			   const struct config_context *ctx UNUSED, void *d)
>  {
>  	struct config_alias_data *data = d;
> -	const char *p;
> +	const char *cmd, *subkey;
> +	size_t cmd_len;
> +	int is_subsection;
>  
> -	if (!skip_prefix(key, "alias.", &p))
> +	/* Use parse_config_key() to handle both 2-level and 3-level keys */
> +	if (parse_config_key(key, "alias", &cmd, &cmd_len, &subkey) < 0)
>  		return 0;
>  
> +	/*
> +	 * Support two syntaxes:
> +	 * 1. alias.name = value (simple, 2-level key)
> +	 * 2. [alias "name"] command = value (new, 3-level key)
> +	 */
> +	if (cmd) {
> +		if (strcmp(subkey, "command"))
> +			return 0;
> +		is_subsection = 1;
> +	} else {
> +		cmd = subkey;
> +		cmd_len = strlen(cmd);
> +		is_subsection = 0;
> +	}

OK, so versus what I showed earlier, we are keeping an is_subsection flag in order to differentiate the cases. And then we use that below...

Show 10 quoted lines
>  	if (data->alias) {
> -		if (!strcasecmp(p, data->alias)) {
> +		int match;
> +		if (is_subsection) {
> +			match = (strlen(data->alias) == cmd_len &&
> +				 !strncmp(data->alias, cmd, cmd_len));
> +		} else {
> +			match = (strlen(data->alias) == cmd_len &&
> +				 !strncasecmp(data->alias, cmd, cmd_len));
> +		}
...to decide whether to be case-insensitive or not. Makes sense.

I was going to suggest some simplifications here: if is_subsection is false, then we know that cmd is a NUL-terminated string, and we could just use strcasecmp() as before. And in fact, with that conditional we do not need the "cmd" aliasing at all. Which also means we don't need a separate flag to distinguish the cases.

You could just write:
  if (cmd) {
	match = (strlen(data->alias) == cmd_len &&
	         !strncmp(data->alias, cmd, cmd_len));
  } else {
	match = !strcasecmp(data->alias, subkey);
  }
But I guess we do use the redirection in the listing path later anyway:
>  	} else if (data->list) {
> -		string_list_append(data->list, p);
> +		string_list_append_nodup(data->list, xmemdupz(cmd, cmd_len));
>  	}

So maybe it is better as-is. I dunno. I have a feeling this would all need even more refactoring even if we ever added any more alias.foo.* config entries. So we can probably just leave it until then for more rearranging.

One thing I did find a little funny is the use of the word "subkey" for one variable. Usually we just call this the "key", but that name is annoyingly already taken by the function parameter (well before your patch). Usually we call that "var" instead. I don't know if it's worth changing or not. It's probably only confusing to me because I've stared at so many other Git config callback functions.

> diff --git a/t/t0014-alias-utf8.sh b/t/t0014-alias-utf8.sh

Is there any reason to add a new script here, and not just put these in the existing t0014? If you were bailing from the script when the prereq was not met, we'd need a separate script. But since you are using per-test prereqs, they can just exist alongside the other alias tests.

And also, you can't have two scripts with the same number. ;) Running "make test" should complain about that (I'll assume you ran your tests, but manually as ./t0014).

Show 5 quoted lines
> +# Skip if filesystem/locale doesn't support UTF-8
> +test_lazy_prereq UTF8_LOCALE '
> +	test_have_prereq !MINGW &&
> +	test_set_prereq UTF8_LOCALE
> +'

This is a slight mis-use of set_prereq. You should just need to return success from the lazy-prereq function, and it will be set automatically. So more like:

  test_lazy_prereq UTF8_LOCALE '
	test_have_prereq !MINGW
  '

At which point I wonder if there is much value in having UTF8_LOCALE at all, and not just putting "!MINGW" in each of the tests.

Beyond that, my larger question is: what are we asking of the platform and why does MINGW not work? We do not care about actual utf8 filesystem support, since these are just aliases. As long as the config file can store them and we can pass them on the command-line, that should be enough.

Show 5 quoted lines
> +test_expect_success UTF8_LOCALE 'setup UTF-8 aliases' '
> +	git config alias."förgrena".command branch &&
> +	git config alias."分支".command "branch --list" &&
> +	git config alias."test name".command status
> +'

Usually we'd just define these in the tests that use them, which keeps each individual test a bit more self-contained. So more like:

  test_expect_success 'UTF-8 alias with Swedish characters' '
	test_config alias."förgrena".command branch &&
	git förgrena >output &&
	...
  '

But we use them in the listing test, too, so they are each used twice. I dunno. It might make sense for the listing test to just define its own expected set.

> +test_expect_success UTF8_LOCALE 'UTF-8 alias with Swedish characters' '
> +	git förgrena >output &&
> +	test_grep -E "^(\* )?(main|master)" output
> +'

So I see we're aliasing "branch" here, which gives us this complicated regex for checking the output. Would something like "!echo ran my alias" make the test easier to understand?

> +test_expect_success 'list UTF-8 aliases' '
> +	git config --get-regexp "^alias\\..*\\.command" >output &&
> +	test_line_count -ge 3 output
> +'

This one needs the UTF8_LOCALE prereq, too (since we'd only have set up those aliases if we had it).

Is doing --get-regexp here all that interesting? It's not testing your new code at all, and would have passed already. I expected us to look at the output of "git help -a" to see that the entries are listed.

And trying that with your patch:
  ./git -c alias.föo.command=branch help -a
shows some possible issues:
  - we show it as "föo.command"
  - the alignment with other aliases is not quite right. Probably it is
    using a byte-count instead of utf8_strwidth()

I was puzzled that we would not parse it as föo correctly, since your patch modified the listing code. It looks like help.c has its own separate parser for this. Yuck. :( I think this also means that "git help föo" will fail.

So probably we want a preparatory patch to teach help.c to rely on list_aliases() and alias_lookup() from alias.[ch].

Show 5 quoted lines
> +test_expect_success 'flat syntax still works' '
> +	git config alias.testlegacy status &&
> +	git testlegacy >output &&
> +	test_grep "On branch" output
> +'
This probably is covered elsewhere already, but OK. :)
Show 5 quoted lines
> +test_expect_success 'new subsection syntax works' '
> +	git config alias.testnew.command status &&
> +	git testnew >output &&
> +	test_grep "On branch" output
> +'
Nice basic test without utf8. Good.
Show 5 quoted lines
> +test_expect_success 'subsection syntax only accepts command key' '
> +	git config alias.invalid.notcommand "value" &&
> +	test_must_fail git invalid 2>error &&
> +	test_grep -i "not a git command" error
> +'
Good thinking.
Show 14 quoted lines
> +test_expect_success 'simple syntax is case-insensitive' '
> +	git config alias.LegacyCase status &&
> +	git legacycase >output 2>&1 &&
> +	test_grep "On branch" output
> +'
> +
> +test_expect_success 'subsection syntax is case-sensitive' '
> +	test_commit case-test &&
> +	git config alias.SubCase.command "log --oneline" &&
> +	git config alias.subcase.command status &&
> +	git SubCase >upper.out 2>&1 &&
> +	git subcase >lower.out 2>&1 &&
> +	! test_cmp upper.out lower.out
> +'
And these are both very nice to see.
-Peff
Torsten Bögershausen· Feb 10, 2026, 08:30 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v1] alias: support UTF-8 characters via subsection syntax

[] Thanks for working on this. Trying to review it: Is there a chance to split it into 2 workpackages/commits/ ?

For me it seems as if the introduction of the new subsection is one big thing. With documentation, code review, tests.

And handling UTF-8 is another big thing, opening questions like Do we need to set a locale ? Does mingw handle UTF-8 on the command line, or why is it exlcuded ?

Junio C Hamano· Feb 10, 2026, 16:35 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v1] alias: support UTF-8 characters via subsection syntax

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 15 quoted lines
> diff --git a/Documentation/RelNotes/2.54.0.adoc b/Documentation/RelNotes/2.54.0.adoc
> index 20c660d82a..8cb00a21bb 100644
> --- a/Documentation/RelNotes/2.54.0.adoc
> +++ b/Documentation/RelNotes/2.54.0.adoc
> @@ -7,6 +7,14 @@ UI, Workflows & Features
>   * "git add -p" and friends note what the current status of the hunk
>     being shown is.
>  
> + * Git aliases now support UTF-8 characters in alias names through
> +   subsection syntax: `[alias "name"] command = value`. This enables
> +   aliases in non-English languages. The flat syntax continues
> +   to work for backward compatibility.
> +
> + * The new subsection syntax uses case-sensitive matching and
> +   the flat syntax remains case-insensitive for backward compatibility.

Thanks for summrizing the topic well. "The flat syntax" is a new phrase to us, I think. Do we have a term to call two-level configuration variables in contrast to threee-level ones defined in the glossary? I've seen phrases like two- and three- level names to distinguish them in the past, but that is no way "official".

"git config --help" has Syntax section that calls "alias" a "section", and "foo" a "subsection", and "command" a "variable name" in "alias.foo.command". Perhaps "the alias definition without subsection, e.g., "[alias] co = checkout", continues to work", or something, perhaps?

In addition to what Peff already mentioned, we would not want the second bullet point; the fact that the traditional two-level config is still supported is very much worth mentioning, which is already done in the previous point. So all it remains in this bullet point is that this topic did not change anything in the three-level configuration case, which is not noteworty.

> +# Simple syntax (ASCII names)
> +[alias]
> +    co = checkout
> +    st = status

It is somewhat misleading to call this "ASCII" as it is stricter than that (e.g., there are '.' and other ASCII characters that you cannot have in the name).

    "Limited to alphanumeric and '-' letters (the same limitation as
    configuration variable names), case insensitive"
or something.
Jonatan Holmgren· Feb 10, 2026, 18:31 UTC · re: Jonatan Holmgren · lore

[PATCH v2 0/2] support UTF-8 in alias names

Hi all,

Thanks for the feedback on my first patch! This iteration (to my understanding) addresses all the points raised by Jeff, Junio, and Brian.

Relevant since RFC/v1:
- Split into two commits: a preparatory refactoring and the main feature
- Removed release notes from commits 
  (suggested blurb below, first time doing this, bear with me :))
- Fixed documentation to use "without subsection" terminology instead of
  "flat syntax"
- Consolidated help.c to use list_aliases() from alias.c, eliminating
  duplicate parsing logic
- Added utf8_strwidth() for proper column alignment in help output
- Improved test coverage with case-sensitivity tests, subsection validation,
  and help listing verification
- Tests now use test_config helper and simpler output verification

The implementation follows Peff's suggestion to use config subsections rather than modifying the config key syntax. This allows arbitrary bytes in alias names while maintaining backward compatibility.

Suggested release note blurb:
 * Git aliases now support UTF-8 characters and special characters
   in alias names through subsection syntax: `[alias "name"] command = value`.
   This enables aliases in non-English languages. The traditional syntax
   (without subsection, e.g., `[alias] co = checkout`) continues to work.
Jonatan Holmgren (2):
  help: use list_aliases() for alias listing and lookup
  alias: support non-alphanumeric names via subsection syntax
 Documentation/config/alias.adoc | 44 +++++++++++++++++++++-----
 alias.c                         | 43 ++++++++++++++++++++++----
 help.c                          | 36 ++++++++++-----------
 t/t0014-alias.sh                | 55 +++++++++++++++++++++++++++++++++
 4 files changed, 144 insertions(+), 34 deletions(-)
-- 
2.53.0
Jonatan Holmgren· Feb 10, 2026, 18:31 UTC · re: Jonatan Holmgren · lore

[PATCH v2 1/2] help: use list_aliases() for alias listing and lookup

help.c has its own get_alias() config callback that duplicates the parsing logic in alias.c. Consolidate by teaching list_aliases() to also store the alias values (via the string_list util field), then use it in list_all_cmds_help_aliases() instead of the private callback.

While at it, switch git_unknown_cmd_config() from skip_prefix() to parse_config_key() for alias parsing, which properly handles the config key structure and prepares for multi-level alias config keys in a subsequent commit.

No functional change intended.
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 alias.c |  4 +++-
 help.c  | 26 ++++++++------------------
 2 files changed, 11 insertions(+), 19 deletions(-)
diff --git a/alias.c b/alias.c
index 1a1a141a0a..c66a6095bb 100644
--- a/alias.c
+++ b/alias.c
@@ -29,7 +29,9 @@ static int config_alias_cb(const char *key, const char *value,
 						 key, value);
 		}
 	} else if (data->list) {
-		string_list_append(data->list, p);
+		if (value)
+			string_list_append(data->list, p)->util =
+				xstrdup(value);
 	}
 
 	return 0;
diff --git a/help.c b/help.c
index fefd811f7a..a450d57987 100644
--- a/help.c
+++ b/help.c
@@ -20,6 +20,7 @@
 #include "prompt.h"
 #include "fsmonitor-ipc.h"
 #include "repository.h"
+#include "alias.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)
 	putchar('\n');
 }
 
-static int get_alias(const char *var, const char *value,
-		     const struct config_context *ctx UNUSED, void *data)
-{
-	struct string_list *list = data;
-
-	if (skip_prefix(var, "alias.", &var)) {
-		if (!value)
-			return config_error_nonbool(var);
-		string_list_append(list, var)->util = xstrdup(value);
-	}
-
-	return 0;
-}
-
 static void list_all_cmds_help_external_commands(void)
 {
 	struct string_list others = STRING_LIST_INIT_DUP;
@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)
 	struct cmdname_help *aliases;
 	int i;
 
-	repo_config(the_repository, get_alias, &alias_list);
+	list_aliases(&alias_list);
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
@@ -586,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 				  void *cb)
 {
 	struct help_unknown_cmd_config *cfg = cb;
-	const char *p;
+	const char *subsection, *key;
+	size_t subsection_len;
 
 	if (!strcmp(var, "help.autocorrect")) {
 		int v = parse_autocorrect(value);
@@ -601,8 +589,10 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	}
 
 	/* Also use aliases for command lookup */
-	if (skip_prefix(var, "alias.", &p))
-		add_cmdname(&cfg->aliases, p, strlen(p));
+	if (!parse_config_key(var, "alias", &subsection, &subsection_len, &key)) {
+		if (!subsection)
+			add_cmdname(&cfg->aliases, key, strlen(key));
+	}
 
 	return 0;
 }
-- 
2.53.0
Junio C Hamano· Feb 10, 2026, 19:27 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v2 1/2] help: use list_aliases() for alias listing and lookup

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 32 quoted lines
> help.c has its own get_alias() config callback that duplicates the
> parsing logic in alias.c. Consolidate by teaching list_aliases() to
> also store the alias values (via the string_list util field), then
> use it in list_all_cmds_help_aliases() instead of the private
> callback.
>
> While at it, switch git_unknown_cmd_config() from skip_prefix() to
> parse_config_key() for alias parsing, which properly handles the
> config key structure and prepares for multi-level alias config keys
> in a subsequent commit.
>
> No functional change intended.
>
> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
> ---
>  alias.c |  4 +++-
>  help.c  | 26 ++++++++------------------
>  2 files changed, 11 insertions(+), 19 deletions(-)
>
> diff --git a/alias.c b/alias.c
> index 1a1a141a0a..c66a6095bb 100644
> --- a/alias.c
> +++ b/alias.c
> @@ -29,7 +29,9 @@ static int config_alias_cb(const char *key, const char *value,
>  						 key, value);
>  		}
>  	} else if (data->list) {
> -		string_list_append(data->list, p);
> +		if (value)
> +			string_list_append(data->list, p)->util =
> +				xstrdup(value);
>  	}

If !value, the original still added p to data->list, but the updated code discards p when value is not there. Is that an intended change?

If not,
	} else if (data->list) {
		struct string_list_item *item;
                item = string_list_append(data->list, p);
		if (value)
			item->util = xstrdup(value);
	}
perhaps.
Show 14 quoted lines
> -static int get_alias(const char *var, const char *value,
> -		     const struct config_context *ctx UNUSED, void *data)
> -{
> -	struct string_list *list = data;
> -
> -	if (skip_prefix(var, "alias.", &var)) {
> -		if (!value)
> -			return config_error_nonbool(var);
> -		string_list_append(list, var)->util = xstrdup(value);
> -	}
> -
> -	return 0;
> -}
> -
A value-less 
	[alias]
		foo

used to get an configuuration error with a friendly message from help.c:get_alias(), which was removed. The config_alias_cb() called by alias.c:list_aliases() either silently ignores foo altogether (the posted patch) or creates an entry for 'foo' but leaves its expansion to NULL (the above "silent ignore fix"). Either way, there needs some new code to compensate for the loss of the error detection somehow.

Show 9 quoted lines
> @@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)
>  	struct cmdname_help *aliases;
>  	int i;
>  
> -	repo_config(the_repository, get_alias, &alias_list);
> +	list_aliases(&alias_list);
>  	string_list_sort(&alias_list);
>  
>  	for (i = 0; i < alias_list.nr; i++) {
OK.
Show 23 quoted lines
> @@ -586,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,
>  				  void *cb)
>  {
>  	struct help_unknown_cmd_config *cfg = cb;
> -	const char *p;
> +	const char *subsection, *key;
> +	size_t subsection_len;
>  
>  	if (!strcmp(var, "help.autocorrect")) {
>  		int v = parse_autocorrect(value);
> @@ -601,8 +589,10 @@ static int git_unknown_cmd_config(const char *var, const char *value,
>  	}
>  
>  	/* Also use aliases for command lookup */
> -	if (skip_prefix(var, "alias.", &p))
> -		add_cmdname(&cfg->aliases, p, strlen(p));
> +	if (!parse_config_key(var, "alias", &subsection, &subsection_len, &key)) {
> +		if (!subsection)
> +			add_cmdname(&cfg->aliases, key, strlen(key));
> +	}
>  
>  	return 0;
>  }

Arguably, the last two hunks are about preparing for three-level alias.*.command support.

It is a bit unfortunate that with
    [alias "foo"]
	command = !date
	bar = !echo bar
in your configuration, 
    $ git foo.command
    $ git foo.bar

used to invoke the alias 'foo.command' and 'foo.bar' just fine, but now with these two preparatory hunks, it no longer is the case and they are silently ignored. With the next patch, 'git foo' starts working in place for 'git foo.command', but 'git foo.bar' has become forever inaccessible. I wonder if we want to warn about foo.bar if not foo.command, or if it is too much? It is conceivable that we may add variables like alias.*.help so it may not be a great idea to warn on anything alias.<subsection>.<key> where <key> is not "command"

Perhaps we can claim that we are fixing a bug that allowed aliases with a dot in its name by mistake? I dunno. No matter what we claim here, some people will be hit by this behaviour change and complain about a regression X-<.

Jonatan Holmgren· Feb 10, 2026, 18:31 UTC · re: Jonatan Holmgren · lore

[PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax

Git alias names are limited to alphanumeric characters and dashes because config variable names are validated by iskeychar(). This prevents non-English speakers from creating aliases in their native languages.

Add support for arbitrary alias names by using config subsections:
    [alias "förgrena"]
        command = branch

The subsection name is matched as-is (case-sensitive byte comparison), while the existing definition without a subsection (e.g., "[alias] co = checkout") remains case-insensitive for backward compatibility. This uses existing config infrastructure since subsections already support arbitrary bytes, and avoids introducing Unicode normalization.

Also teach the help subsystem about the new syntax so that "git help -a" properly lists subsection aliases and the autocorrect feature can suggest them. Use utf8_strwidth() instead of strlen() for column alignment so that non-alphanumeric alias names display correctly.

Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 Documentation/config/alias.adoc | 44 +++++++++++++++++++++-----
 alias.c                         | 45 ++++++++++++++++++++++-----
 help.c                          | 12 +++++--
 t/t0014-alias.sh                | 55 +++++++++++++++++++++++++++++++++
 4 files changed, 137 insertions(+), 19 deletions(-)
diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
index 80ce17d2de..17a548cd64 100644
--- a/Documentation/config/alias.adoc
+++ b/Documentation/config/alias.adoc
@@ -1,12 +1,40 @@
 alias.*::
-	Command aliases for the linkgit:git[1] command wrapper - e.g.
-	after defining `alias.last = cat-file commit HEAD`, the invocation
-	`git last` is equivalent to `git cat-file commit HEAD`. To avoid
-	confusion and troubles with script usage, aliases that
-	hide existing Git commands are ignored except for deprecated
-	commands.  Arguments are split by
-	spaces, the usual shell quoting and escaping are supported.
-	A quote pair or a backslash can be used to quote them.
+alias.*.command::
+	Command aliases for the linkgit:git[1] command wrapper. Aliases
+	can be defined using two syntaxes:
++
+--
+1. Without a subsection, e.g., `[alias] co = checkout`. The alias
+   name is limited to alphanumeric characters and `-` (the same
+   limitation as configuration variable names), and is matched
+   case-insensitively.
+2. With a subsection, e.g., `[alias "name"] command = value`. The
+   alias name can contain any characters including UTF-8, and is
+   matched case-sensitively as raw bytes.
+--
++
+Examples:
++
+----
+# Without subsection
+[alias]
+    co = checkout
+    st = status
+
+# With subsection (allows UTF-8 and special characters)
+[alias "hämta"]
+    command = fetch
+[alias "gömma"]
+    command = stash
+----
++
+E.g. after defining `alias.last = cat-file commit HEAD`, the invocation
+`git last` is equivalent to `git cat-file commit HEAD`. To avoid
+confusion and troubles with script usage, aliases that
+hide existing Git commands are ignored except for deprecated
+commands.  Arguments are split by
+spaces, the usual shell quoting and escaping are supported.
+A quote pair or a backslash can be used to quote them.
 +
 Note that the first word of an alias does not necessarily have to be a
 command. It can be a command-line option that will be passed into the
diff --git a/alias.c b/alias.c
index c66a6095bb..cfd313ce5d 100644
--- a/alias.c
+++ b/alias.c
@@ -17,21 +17,50 @@ static int config_alias_cb(const char *key, const char *value,
 			   const struct config_context *ctx UNUSED, void *d)
 {
 	struct config_alias_data *data = d;
-	const char *p;
+	const char *subsection, *subkey;
+	size_t subsection_len;
 
-	if (!skip_prefix(key, "alias.", &p))
+	if (parse_config_key(key, "alias", &subsection, &subsection_len,
+			     &subkey) < 0)
 		return 0;
 
+	/*
+	 * Two config syntaxes:
+	 * - alias.name = value        (without subsection, case-insensitive)
+	 * - [alias "name"]
+	 *       command = value       (with subsection, case-sensitive)
+	 */
+	if (subsection) {
+		if (strcmp(subkey, "command"))
+			return 0;
+	}
+
 	if (data->alias) {
-		if (!strcasecmp(p, data->alias)) {
+		int match;
+
+		if (subsection)
+			match = (strlen(data->alias) == subsection_len &&
+				 !strncmp(data->alias, subsection,
+					  subsection_len));
+		else
+			match = !strcasecmp(data->alias, subkey);
+
+		if (match) {
 			FREE_AND_NULL(data->v);
-			return git_config_string(&data->v,
-						 key, value);
+			return git_config_string(&data->v, key, value);
 		}
 	} else if (data->list) {
-		if (value)
-			string_list_append(data->list, p)->util =
-				xstrdup(value);
+		struct string_list_item *item;
+
+		if (!value)
+			return 0;
+
+		if (subsection)
+			item = string_list_append_nodup(data->list,
+				xmemdupz(subsection, subsection_len));
+		else
+			item = string_list_append(data->list, subkey);
+		item->util = xstrdup(value);
 	}
 
 	return 0;
diff --git a/help.c b/help.c
index a450d57987..16abfe7bf3 100644
--- a/help.c
+++ b/help.c
@@ -21,6 +21,7 @@
 #include "fsmonitor-ipc.h"
 #include "repository.h"
 #include "alias.h"
+#include "utf8.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
 
 	for (i = 0; cmds[i].name; i++) {
 		if (cmds[i].category & mask) {
-			size_t len = strlen(cmds[i].name);
+			size_t len = utf8_strwidth(cmds[i].name);
 			printf("   %s   ", cmds[i].name);
 			if (longest > len)
 				mput_char(' ', longest - len);
@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
-		size_t len = strlen(alias_list.items[i].string);
+		size_t len = utf8_strwidth(alias_list.items[i].string);
 		if (longest < len)
 			longest = len;
 	}
@@ -590,8 +591,13 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 
 	/* Also use aliases for command lookup */
 	if (!parse_config_key(var, "alias", &subsection, &subsection_len, &key)) {
-		if (!subsection)
+		if (subsection) {
+			if (!strcmp(key, "command"))
+				add_cmdname(&cfg->aliases, subsection,
+					    subsection_len);
+		} else {
 			add_cmdname(&cfg->aliases, key, strlen(key));
+		}
 	}
 
 	return 0;
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index 07a53e7366..66631ad40f 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -112,4 +112,59 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '
 	done
 '
 
+test_expect_success 'subsection syntax works' '
+	test_config alias.testnew.command "!echo ran-subsection" &&
+	git testnew >output &&
+	test_grep "ran-subsection" output
+'
+
+test_expect_success 'subsection syntax only accepts command key' '
+	test_config alias.invalid.notcommand value &&
+	test_must_fail git invalid 2>error &&
+	test_grep -i "not a git command" error
+'
+
+test_expect_success 'simple syntax is case-insensitive' '
+	test_config alias.LegacyCase "!echo ran-legacy" &&
+	git legacycase >output &&
+	test_grep "ran-legacy" output
+'
+
+test_expect_success 'subsection syntax is case-sensitive' '
+	test_config alias.SubCase.command "!echo ran-upper" &&
+	test_config alias.subcase.command "!echo ran-lower" &&
+	git SubCase >upper.out &&
+	git subcase >lower.out &&
+	test_grep "ran-upper" upper.out &&
+	test_grep "ran-lower" lower.out
+'
+
+test_expect_success 'UTF-8 alias with Swedish characters' '
+	test_config alias."förgrena".command "!echo ran-swedish" &&
+	git förgrena >output &&
+	test_grep "ran-swedish" output
+'
+
+test_expect_success 'UTF-8 alias with CJK characters' '
+	test_config alias."分支".command "!echo ran-cjk" &&
+	git 分支 >output &&
+	test_grep "ran-cjk" output
+'
+
+test_expect_success 'alias with spaces in name' '
+	test_config alias."test name".command "!echo ran-spaces" &&
+	git "test name" >output &&
+	test_grep "ran-spaces" output
+'
+
+test_expect_success 'subsection aliases listed in help -a' '
+	test_config alias."förgrena".command "!echo test" &&
+	test_config alias."分支".command "!echo test" &&
+	test_config alias.regular "!echo test" &&
+	git help -a >output &&
+	test_grep "förgrena" output &&
+	test_grep "分支" output &&
+	test_grep "regular" output
+'
+
 test_done
-- 
2.53.0
Junio C Hamano· Feb 10, 2026, 19:47 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax

Jonatan Holmgren <jonatan@jontes.page> writes:
> Git alias names are limited to alphanumeric characters and dashes
> because config variable names are validated by iskeychar(). This

"ASCII alphanumeric", perhaps, as accented alphabet characters are still alphanumeric ;-). "because aliases are implemented as config variable names" is probably an explanation that talks to readers in terms closer to them (iskeychar() function is an implementation detail).

> prevents non-English speakers from creating aliases in their native
> languages.
True.
Show 16 quoted lines
> Add support for arbitrary alias names by using config subsections:
>
>     [alias "förgrena"]
>         command = branch
>
> The subsection name is matched as-is (case-sensitive byte comparison),
> while the existing definition without a subsection (e.g.,
> "[alias] co = checkout") remains case-insensitive for backward
> compatibility. This uses existing config infrastructure since
> subsections already support arbitrary bytes, and avoids introducing
> Unicode normalization.
>
> Also teach the help subsystem about the new syntax so that "git help
> -a" properly lists subsection aliases and the autocorrect feature can
> suggest them. Use utf8_strwidth() instead of strlen() for column
> alignment so that non-alphanumeric alias names display correctly.

Either move the last two hunks from [1/2] to this step, or make it a separate patch [1.5/2] between this and the other steps, and explain it as a change that breaks compatibility in a way that hopefully would not affect anybody in practice. The alias configuration parser used to be overly loose and took "alias.<subsection>.<key>" as defining an alias "<subsection>.<key>"; that change tightens the parser and alias.<subsection>.<key> are silently ignored. This step then take alias.<subsection>.command to be defining a new-style alias that can be invoked as "<subsection>", which is case sensitive and is not limited to ASCII alphanumeric and dashes.

Show 80 quoted lines
> Suggested-by: Jeff King <peff@peff.net>
> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
> ---
>  Documentation/config/alias.adoc | 44 +++++++++++++++++++++-----
>  alias.c                         | 45 ++++++++++++++++++++++-----
>  help.c                          | 12 +++++--
>  t/t0014-alias.sh                | 55 +++++++++++++++++++++++++++++++++
>  4 files changed, 137 insertions(+), 19 deletions(-)
>
> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
> index 80ce17d2de..17a548cd64 100644
> --- a/Documentation/config/alias.adoc
> +++ b/Documentation/config/alias.adoc
> @@ -1,12 +1,40 @@
>  alias.*::
> +alias.*.command::
> +	Command aliases for the linkgit:git[1] command wrapper. Aliases
> +	can be defined using two syntaxes:
> ++
> +--
> +1. Without a subsection, e.g., `[alias] co = checkout`. The alias
> +   name is limited to alphanumeric characters and `-` (the same
> +   limitation as configuration variable names), and is matched
> +   case-insensitively.
> +2. With a subsection, e.g., `[alias "name"] command = value`. The
> +   alias name can contain any characters including UTF-8, and is
> +   matched case-sensitively as raw bytes.
> +--
> ++
> +Examples:
> ++
> +----
> +# Without subsection
> +[alias]
> +    co = checkout
> +    st = status
> +
> +# With subsection (allows UTF-8 and special characters)
> +[alias "hämta"]
> +    command = fetch
> +[alias "gömma"]
> +    command = stash
> +----
> ++
> +E.g. after defining `alias.last = cat-file commit HEAD`, the invocation
> +`git last` is equivalent to `git cat-file commit HEAD`. To avoid
> +confusion and troubles with script usage, aliases that
> +hide existing Git commands are ignored except for deprecated
> +commands.  Arguments are split by
> +spaces, the usual shell quoting and escaping are supported.
> +A quote pair or a backslash can be used to quote them.
>  +
>  Note that the first word of an alias does not necessarily have to be a
>  command. It can be a command-line option that will be passed into the
> diff --git a/alias.c b/alias.c
> index c66a6095bb..cfd313ce5d 100644
> --- a/alias.c
> +++ b/alias.c
> @@ -17,21 +17,50 @@ static int config_alias_cb(const char *key, const char *value,
>  			   const struct config_context *ctx UNUSED, void *d)
>  {
>  	struct config_alias_data *data = d;
> -	const char *p;
> +	const char *subsection, *subkey;
> +	size_t subsection_len;
>  
> -	if (!skip_prefix(key, "alias.", &p))
> +	if (parse_config_key(key, "alias", &subsection, &subsection_len,
> +			     &subkey) < 0)
>  		return 0;
>  
> +	/*
> +	 * Two config syntaxes:
> +	 * - alias.name = value        (without subsection, case-insensitive)
> +	 * - [alias "name"]
> +	 *       command = value       (with subsection, case-sensitive)
> +	 */
> +	if (subsection) {
> +		if (strcmp(subkey, "command"))
> +			return 0;
This silently ignores
	[alias "foo"]
		bar = !date

which may or may not be a feature. If the variable name is "help" instead of "bar", it certainly is a feature to silently skip it, as it is not inconceivable that we would add such a variable name in the future, and because we won't be able to predict the future, not limiting us to "help" but ignoring anything we do not understand like this code does may probably be a good thing. I dunno.

Show 34 quoted lines
> +	}
> +
>  	if (data->alias) {
> -		if (!strcasecmp(p, data->alias)) {
> +		int match;
> +
> +		if (subsection)
> +			match = (strlen(data->alias) == subsection_len &&
> +				 !strncmp(data->alias, subsection,
> +					  subsection_len));
> +		else
> +			match = !strcasecmp(data->alias, subkey);
> +
> +		if (match) {
>  			FREE_AND_NULL(data->v);
> -			return git_config_string(&data->v,
> -						 key, value);
> +			return git_config_string(&data->v, key, value);
>  		}
>  	} else if (data->list) {
> -		if (value)
> -			string_list_append(data->list, p)->util =
> -				xstrdup(value);
> +		struct string_list_item *item;
> +
> +		if (!value)
> +			return 0;
> +
> +		if (subsection)
> +			item = string_list_append_nodup(data->list,
> +				xmemdupz(subsection, subsection_len));
> +		else
> +			item = string_list_append(data->list, subkey);
> +		item->util = xstrdup(value);
This still silently ignores
	[alias "foo"]
		command

which is a much more grave problem than ignoring alias.foo.bar in the earlier part of this function. We would want to preserve the existing diagnosis on broken configuration.

Show 31 quoted lines
> @@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
>  
>  	for (i = 0; cmds[i].name; i++) {
>  		if (cmds[i].category & mask) {
> -			size_t len = strlen(cmds[i].name);
> +			size_t len = utf8_strwidth(cmds[i].name);
>  			printf("   %s   ", cmds[i].name);
>  			if (longest > len)
>  				mput_char(' ', longest - len);
> @@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)
>  	string_list_sort(&alias_list);
>  
>  	for (i = 0; i < alias_list.nr; i++) {
> -		size_t len = strlen(alias_list.items[i].string);
> +		size_t len = utf8_strwidth(alias_list.items[i].string);
>  		if (longest < len)
>  			longest = len;
>  	}
> @@ -590,8 +591,13 @@ static int git_unknown_cmd_config(const char *var, const char *value,
>  
>  	/* Also use aliases for command lookup */
>  	if (!parse_config_key(var, "alias", &subsection, &subsection_len, &key)) {
> -		if (!subsection)
> +		if (subsection) {
> +			if (!strcmp(key, "command"))
> +				add_cmdname(&cfg->aliases, subsection,
> +					    subsection_len);
> +		} else {
>  			add_cmdname(&cfg->aliases, key, strlen(key));
> +		}
>  	}
OK.  Alternatively, out of
	[alias "foo"]
		command = !echo foo
		bar = !echo bar

we _could_ list "foo" (a new style alias) and "foo.bar" (an old style alias that we have been accepting forever by mistake) for maximum backward compatibility. I am still undecided if it is a good idea.

Thanks.
Jonatan Holmgren· Feb 10, 2026, 22:29 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax

I broke it out into 3 commits for my version 3 of the patches and think I've caught all the bugs in my implementation. Thanks for the patience!

I also took the liberty of breaking
     [alias "foo"]
	bar = !date
as dotted aliases were never a feature (XKCD 1172 workflow hopefully not 
incoming) and it might be useful to be able to define more stuff under 
this new alias syntax other than "command".
Patch incoming.

Best, Jonatan

On 2026-02-10 20:47, Junio C Hamano wrote:
Show 224 quoted lines
> Jonatan Holmgren <jonatan@jontes.page> writes:
> 
>> Git alias names are limited to alphanumeric characters and dashes
>> because config variable names are validated by iskeychar(). This
> 
> "ASCII alphanumeric", perhaps, as accented alphabet characters are
> still alphanumeric ;-).  "because aliases are implemented as config
> variable names" is probably an explanation that talks to readers in
> terms closer to them (iskeychar() function is an implementation detail).
> 
>> prevents non-English speakers from creating aliases in their native
>> languages.
> 
> True.
> 
>> Add support for arbitrary alias names by using config subsections:
>>
>>      [alias "förgrena"]
>>          command = branch
>>
>> The subsection name is matched as-is (case-sensitive byte comparison),
>> while the existing definition without a subsection (e.g.,
>> "[alias] co = checkout") remains case-insensitive for backward
>> compatibility. This uses existing config infrastructure since
>> subsections already support arbitrary bytes, and avoids introducing
>> Unicode normalization.
>>
>> Also teach the help subsystem about the new syntax so that "git help
>> -a" properly lists subsection aliases and the autocorrect feature can
>> suggest them. Use utf8_strwidth() instead of strlen() for column
>> alignment so that non-alphanumeric alias names display correctly.
> 
> Either move the last two hunks from [1/2] to this step, or make it a
> separate patch [1.5/2] between this and the other steps, and explain
> it as a change that breaks compatibility in a way that hopefully
> would not affect anybody in practice.  The alias configuration
> parser used to be overly loose and took "alias.<subsection>.<key>"
> as defining an alias "<subsection>.<key>"; that change tightens the
> parser and alias.<subsection>.<key> are silently ignored.  This step
> then take alias.<subsection>.command to be defining a new-style
> alias that can be invoked as "<subsection>", which is case sensitive
> and is not limited to ASCII alphanumeric and dashes.
> 
>> Suggested-by: Jeff King <peff@peff.net>
>> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
>> ---
>>   Documentation/config/alias.adoc | 44 +++++++++++++++++++++-----
>>   alias.c                         | 45 ++++++++++++++++++++++-----
>>   help.c                          | 12 +++++--
>>   t/t0014-alias.sh                | 55 +++++++++++++++++++++++++++++++++
>>   4 files changed, 137 insertions(+), 19 deletions(-)
>>
>> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
>> index 80ce17d2de..17a548cd64 100644
>> --- a/Documentation/config/alias.adoc
>> +++ b/Documentation/config/alias.adoc
>> @@ -1,12 +1,40 @@
>>   alias.*::
>> +alias.*.command::
>> +	Command aliases for the linkgit:git[1] command wrapper. Aliases
>> +	can be defined using two syntaxes:
>> ++
>> +--
>> +1. Without a subsection, e.g., `[alias] co = checkout`. The alias
>> +   name is limited to alphanumeric characters and `-` (the same
>> +   limitation as configuration variable names), and is matched
>> +   case-insensitively.
>> +2. With a subsection, e.g., `[alias "name"] command = value`. The
>> +   alias name can contain any characters including UTF-8, and is
>> +   matched case-sensitively as raw bytes.
>> +--
>> ++
>> +Examples:
>> ++
>> +----
>> +# Without subsection
>> +[alias]
>> +    co = checkout
>> +    st = status
>> +
>> +# With subsection (allows UTF-8 and special characters)
>> +[alias "hämta"]
>> +    command = fetch
>> +[alias "gömma"]
>> +    command = stash
>> +----
>> ++
>> +E.g. after defining `alias.last = cat-file commit HEAD`, the invocation
>> +`git last` is equivalent to `git cat-file commit HEAD`. To avoid
>> +confusion and troubles with script usage, aliases that
>> +hide existing Git commands are ignored except for deprecated
>> +commands.  Arguments are split by
>> +spaces, the usual shell quoting and escaping are supported.
>> +A quote pair or a backslash can be used to quote them.
>>   +
>>   Note that the first word of an alias does not necessarily have to be a
>>   command. It can be a command-line option that will be passed into the
>> diff --git a/alias.c b/alias.c
>> index c66a6095bb..cfd313ce5d 100644
>> --- a/alias.c
>> +++ b/alias.c
>> @@ -17,21 +17,50 @@ static int config_alias_cb(const char *key, const char *value,
>>   			   const struct config_context *ctx UNUSED, void *d)
>>   {
>>   	struct config_alias_data *data = d;
>> -	const char *p;
>> +	const char *subsection, *subkey;
>> +	size_t subsection_len;
>>   
>> -	if (!skip_prefix(key, "alias.", &p))
>> +	if (parse_config_key(key, "alias", &subsection, &subsection_len,
>> +			     &subkey) < 0)
>>   		return 0;
>>   
>> +	/*
>> +	 * Two config syntaxes:
>> +	 * - alias.name = value        (without subsection, case-insensitive)
>> +	 * - [alias "name"]
>> +	 *       command = value       (with subsection, case-sensitive)
>> +	 */
>> +	if (subsection) {
>> +		if (strcmp(subkey, "command"))
>> +			return 0;
> 
> This silently ignores
> 
> 	[alias "foo"]
> 		bar = !date
> 
> which may or may not be a feature.  If the variable name is "help"
> instead of "bar", it certainly is a feature to silently skip it, as
> it is not inconceivable that we would add such a variable name in
> the future, and because we won't be able to predict the future, not
> limiting us to "help" but ignoring anything we do not understand
> like this code does may probably be a good thing.  I dunno.
> 
>> +	}
>> +
>>   	if (data->alias) {
>> -		if (!strcasecmp(p, data->alias)) {
>> +		int match;
>> +
>> +		if (subsection)
>> +			match = (strlen(data->alias) == subsection_len &&
>> +				 !strncmp(data->alias, subsection,
>> +					  subsection_len));
>> +		else
>> +			match = !strcasecmp(data->alias, subkey);
>> +
>> +		if (match) {
>>   			FREE_AND_NULL(data->v);
>> -			return git_config_string(&data->v,
>> -						 key, value);
>> +			return git_config_string(&data->v, key, value);
>>   		}
>>   	} else if (data->list) {
>> -		if (value)
>> -			string_list_append(data->list, p)->util =
>> -				xstrdup(value);
>> +		struct string_list_item *item;
>> +
>> +		if (!value)
>> +			return 0;
>> +
>> +		if (subsection)
>> +			item = string_list_append_nodup(data->list,
>> +				xmemdupz(subsection, subsection_len));
>> +		else
>> +			item = string_list_append(data->list, subkey);
>> +		item->util = xstrdup(value);
> 
> This still silently ignores
> 
> 	[alias "foo"]
> 		command
> 
> which is a much more grave problem than ignoring alias.foo.bar in
> the earlier part of this function.  We would want to preserve the
> existing diagnosis on broken configuration.
> 
>> @@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
>>   
>>   	for (i = 0; cmds[i].name; i++) {
>>   		if (cmds[i].category & mask) {
>> -			size_t len = strlen(cmds[i].name);
>> +			size_t len = utf8_strwidth(cmds[i].name);
>>   			printf("   %s   ", cmds[i].name);
>>   			if (longest > len)
>>   				mput_char(' ', longest - len);
>> @@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)
>>   	string_list_sort(&alias_list);
>>   
>>   	for (i = 0; i < alias_list.nr; i++) {
>> -		size_t len = strlen(alias_list.items[i].string);
>> +		size_t len = utf8_strwidth(alias_list.items[i].string);
>>   		if (longest < len)
>>   			longest = len;
>>   	}
>> @@ -590,8 +591,13 @@ static int git_unknown_cmd_config(const char *var, const char *value,
>>   
>>   	/* Also use aliases for command lookup */
>>   	if (!parse_config_key(var, "alias", &subsection, &subsection_len, &key)) {
>> -		if (!subsection)
>> +		if (subsection) {
>> +			if (!strcmp(key, "command"))
>> +				add_cmdname(&cfg->aliases, subsection,
>> +					    subsection_len);
>> +		} else {
>>   			add_cmdname(&cfg->aliases, key, strlen(key));
>> +		}
>>   	}
> 
> OK.  Alternatively, out of
> 
> 	[alias "foo"]
> 		command = !echo foo
> 		bar = !echo bar
> 
> we _could_ list "foo" (a new style alias) and "foo.bar" (an old
> style alias that we have been accepting forever by mistake) for
> maximum backward compatibility.  I am still undecided if it is a
> good idea.
> 
> Thanks.
Kristoffer Haugsbakk· Feb 23, 2026, 09:29 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax

On Tue, Feb 10, 2026, at 19:31, Jonatan Holmgren wrote:
Show 33 quoted lines
> [snip]
>
> Suggested-by: Jeff King <peff@peff.net>
> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
> ---
>  Documentation/config/alias.adoc | 44 +++++++++++++++++++++-----
>  alias.c                         | 45 ++++++++++++++++++++++-----
>  help.c                          | 12 +++++--
>  t/t0014-alias.sh                | 55 +++++++++++++++++++++++++++++++++
>  4 files changed, 137 insertions(+), 19 deletions(-)
>
> diff --git a/Documentation/config/alias.adoc
> b/Documentation/config/alias.adoc
> index 80ce17d2de..17a548cd64 100644
> --- a/Documentation/config/alias.adoc
> +++ b/Documentation/config/alias.adoc
>[snip]
> +# With subsection (allows UTF-8 and special characters)
> +[alias "hämta"]
> +    command = fetch
> +[alias "gömma"]
> +    command = stash
> +----
> ++
> +E.g. after defining `alias.last = cat-file commit HEAD`, the invocation
> +`git last` is equivalent to `git cat-file commit HEAD`. To avoid
> +confusion and troubles with script usage, aliases that
> +hide existing Git commands are ignored except for deprecated
> +commands.  Arguments are split by
> +spaces, the usual shell quoting and escaping are supported.
> +A quote pair or a backslash can be used to quote them.
>  +
>  Note that the first word of an alias does not necessarily have to be a
The HTML output shows the list continuation character (+).
    + Note that the first word of an alias does not ...
>  command. It can be a command-line option that will be passed into the
And there are two more outside this context which I suspect are knock-ons?
    + If the alias expansion is prefixed ..
    ...
    + * Shell commands will be executed ...
See `Documentation/doc-diff master next`.

• master: 7c02d39f (The 6th batch, 2026-02-20) • next: 4a7958ca (Sync with 'master', 2026-02-20)

> diff --git a/alias.c b/alias.c
>[snip]
Kristoffer Haugsbakk· Feb 23, 2026, 16:07 UTC · re: Kristoffer Haugsbakk · lore

Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax

On Mon, Feb 23, 2026, at 10:29, Kristoffer Haugsbakk wrote:
Show 23 quoted lines
>>[snip]
>
> The HTML output shows the list continuation character (+).
>
>     + Note that the first word of an alias does not ...
>
>>  command. It can be a command-line option that will be passed into the
>
> And there are two more outside this context which I suspect are knock-ons?
>
>     + If the alias expansion is prefixed ..
>
>     ...
>
>     + * Shell commands will be executed ...
>
> See `Documentation/doc-diff master next`.
>
> • master: 7c02d39f (The 6th batch, 2026-02-20)
> • next: 4a7958ca (Sync with 'master', 2026-02-20)
>
>> diff --git a/alias.c b/alias.c
>>[snip]

Sorry that I just replied to the first hit that I found in my inbox. This applies to the latest version which is in `next`.

Junio C Hamano· Feb 23, 2026, 20:22 UTC · re: Kristoffer Haugsbakk · lore

Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax

"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
Show 27 quoted lines
> On Mon, Feb 23, 2026, at 10:29, Kristoffer Haugsbakk wrote:
>>>[snip]
>>
>> The HTML output shows the list continuation character (+).
>>
>>     + Note that the first word of an alias does not ...
>>
>>>  command. It can be a command-line option that will be passed into the
>>
>> And there are two more outside this context which I suspect are knock-ons?
>>
>>     + If the alias expansion is prefixed ..
>>
>>     ...
>>
>>     + * Shell commands will be executed ...
>>
>> See `Documentation/doc-diff master next`.
>>
>> • master: 7c02d39f (The 6th batch, 2026-02-20)
>> • next: 4a7958ca (Sync with 'master', 2026-02-20)
>>
>>> diff --git a/alias.c b/alias.c
>>>[snip]
>
> Sorry that I just replied to the first hit that I found in my
> inbox. This applies to the latest version which is in `next`.
And your fix is the [v7 5/4] <followup-alias-i18n.3e0@msgid.xyz>?
Thanks for being careful.
Kristoffer Haugsbakk· Feb 23, 2026, 20:25 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax

On Mon, Feb 23, 2026, at 21:22, Junio C Hamano wrote:
Show 7 quoted lines
>>[snip]
>> Sorry that I just replied to the first hit that I found in my
>> inbox. This applies to the latest version which is in `next`.
>
> And your fix is the [v7 5/4] <followup-alias-i18n.3e0@msgid.xyz>?
>
> Thanks for being careful.

Yep, I was wondering whether to use In-Reply-To the cover letter but I didn’t know which approach would be less confusing (freestanding or reply).

Patrick Steinhardt· Feb 24, 2026, 10:27 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v2 2/2] alias: support non-alphanumeric names via subsection syntax

On Tue, Feb 10, 2026 at 07:31:10PM +0100, Jonatan Holmgren wrote:
Show 21 quoted lines
> Git alias names are limited to alphanumeric characters and dashes
> because config variable names are validated by iskeychar(). This
> prevents non-English speakers from creating aliases in their native
> languages.
> 
> Add support for arbitrary alias names by using config subsections:
> 
>     [alias "förgrena"]
>         command = branch
> 
> The subsection name is matched as-is (case-sensitive byte comparison),
> while the existing definition without a subsection (e.g.,
> "[alias] co = checkout") remains case-insensitive for backward
> compatibility. This uses existing config infrastructure since
> subsections already support arbitrary bytes, and avoids introducing
> Unicode normalization.
> 
> Also teach the help subsystem about the new syntax so that "git help
> -a" properly lists subsection aliases and the autocorrect feature can
> suggest them. Use utf8_strwidth() instead of strlen() for column
> alignment so that non-alphanumeric alias names display correctly.

This patch has caused a regression in a somewhat esoteric use case. Before this patch, you could do the following:

    $ git config set "alias..foobar" "!echo barfoo"
    $ git .foobar
    barfoo
Or, phrased as a test case:
    diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
    index a13d2be8ca..dca50e87e2 100755
    --- a/t/t0014-alias.sh
    +++ b/t/t0014-alias.sh
    @@ -4,6 +4,13 @@ test_description='git command aliasing'
     . ./test-lib.sh
    +test_expect_success 'alias with leading dot' '
    +	test_config_global alias..something "!echo foobar" &&
    +	git .something >actual &&
    +	echo foobar >expect &&
    +	test_cmp expect actual
    +'
    +
     test_expect_success 'nested aliases - internal execution' '
     	git config alias.nested-internal-1 nested-internal-2 &&
     	git config alias.nested-internal-2 status &&

I kind of doubt that this was intentional design, but I know that it is used e.g. by Dscho in his shears scripts [1]. What this script does is to create a temporary alias "alias..r" that then gets executed via the sequencer, and this patch broke this. I happened to discover the regression as I use shears myself.

Chances are that there are other users out there that rely on the current behaviour.

Thanks!
Patrick
[1]: https://github.com/git-for-windows/build-extra/blob/a82c8fcb0b8f165c1379c12b0cf914741b8dc8d5/shears.sh
Jonatan Holmgren· Feb 10, 2026, 22:27 UTC · re: Jonatan Holmgren · lore

[PATCH 0/3] support UTF-8 in alias names

Hi all,
This v3 addresses all feedback from Junio on v2. Changes since v2:
- Split into three commits: a preparatory refactoring, a compatibility-
  breaking change separated explicitly, and the main feature
- Preserved error checking for value-less alias config (config_error_nonbool)
- Fixed behavior when !value to preserve adding items to list while keeping
  util as NULL
- Improved commit messages to say "ASCII alphanumeric" and better explain
  that aliases are implemented as config variable names
- Tests now properly verify value-less command key handling

The implementation follows Peff's suggestion to use config subsections rather than modifying the config key syntax. This allows arbitrary bytes in alias names while maintaining backward compatibility.

Suggested release note blurb:
 * Git aliases now support UTF-8 characters and special characters
   in alias names through subsection syntax: `[alias "name"] command = value`.
   This enables aliases in non-English languages. The traditional syntax
   (without subsection, e.g., `[alias] co = checkout`) continues to work.
Jonatan Holmgren (3):
  help: use list_aliases() for alias listing
  alias: prepare for subsection aliases
  alias: support non-alphanumeric names via subsection syntax
 Documentation/config/alias.adoc | 43 ++++++++++++++++++-----
 alias.c                         | 40 +++++++++++++++++++---
 help.c                          | 39 +++++++++++----------
 t/t0014-alias.sh                | 60 +++++++++++++++++++++++++++++++++
 4 files changed, 150 insertions(+), 32 deletions(-)
-- 
2.53.0
Jonatan Holmgren· Feb 10, 2026, 22:27 UTC · re: Jonatan Holmgren · lore

[PATCH 1/3] help: use list_aliases() for alias listing

help.c has its own get_alias() config callback that duplicates the parsing logic in alias.c. Consolidate by teaching list_aliases() to also store the alias values (via the string_list util field), then use it in list_all_cmds_help_aliases() instead of the private callback.

This preserves the existing error checking for value-less alias definitions by checking in alias.c rather than help.c.

No functional change intended.
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 alias.c |  9 ++++++++-
 help.c  | 17 ++---------------
 2 files changed, 10 insertions(+), 16 deletions(-)
diff --git a/alias.c b/alias.c
index 1a1a141a0a..1f8e13610c 100644
--- a/alias.c
+++ b/alias.c
@@ -24,12 +24,19 @@ static int config_alias_cb(const char *key, const char *value,
 
 	if (data->alias) {
 		if (!strcasecmp(p, data->alias)) {
+			if (!value)
+				return config_error_nonbool(key);
 			FREE_AND_NULL(data->v);
 			return git_config_string(&data->v,
 						 key, value);
 		}
 	} else if (data->list) {
-		string_list_append(data->list, p);
+		struct string_list_item *item;
+
+		item = string_list_append(data->list, p);
+		if (value)
+			item->util = xstrdup(value);
+		/* if !value, item->util remains NULL but item is still added */
 	}
 
 	return 0;
diff --git a/help.c b/help.c
index fefd811f7a..0bdb7ca10f 100644
--- a/help.c
+++ b/help.c
@@ -20,6 +20,7 @@
 #include "prompt.h"
 #include "fsmonitor-ipc.h"
 #include "repository.h"
+#include "alias.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)
 	putchar('\n');
 }
 
-static int get_alias(const char *var, const char *value,
-		     const struct config_context *ctx UNUSED, void *data)
-{
-	struct string_list *list = data;
-
-	if (skip_prefix(var, "alias.", &var)) {
-		if (!value)
-			return config_error_nonbool(var);
-		string_list_append(list, var)->util = xstrdup(value);
-	}
-
-	return 0;
-}
-
 static void list_all_cmds_help_external_commands(void)
 {
 	struct string_list others = STRING_LIST_INIT_DUP;
@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)
 	struct cmdname_help *aliases;
 	int i;
 
-	repo_config(the_repository, get_alias, &alias_list);
+	list_aliases(&alias_list);
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
-- 
2.53.0
Junio C Hamano· Feb 10, 2026, 23:17 UTC · re: Jonatan Holmgren · lore

Re: [PATCH 1/3] help: use list_aliases() for alias listing

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 8 quoted lines
>  	if (data->alias) {
>  		if (!strcasecmp(p, data->alias)) {
> +			if (!value)
> +				return config_error_nonbool(key);
>  			FREE_AND_NULL(data->v);
>  			return git_config_string(&data->v,
>  						 key, value);
>  		}

Hmph, git_config_string() would trigger config_error_nonbool() anyway if your feed value==NULL, so this change looks a noop.

Show 8 quoted lines
>  	} else if (data->list) {
> -		string_list_append(data->list, p);
> +		struct string_list_item *item;
> +
> +		item = string_list_append(data->list, p);
> +		if (value)
> +			item->util = xstrdup(value);
> +		/* if !value, item->util remains NULL but item is still added */

This side is silent. We hold onto the value, when available, and otherwise we just remember the fact that there is a (misconfigured) alias by keeping the NULL in the .util member. Presumably it is now the responsibility of the caller to deal with these entries with NULL in their .util member?

Show 32 quoted lines
>  	}
>  
>  	return 0;
> diff --git a/help.c b/help.c
> index fefd811f7a..0bdb7ca10f 100644
> --- a/help.c
> +++ b/help.c
> @@ -20,6 +20,7 @@
>  #include "prompt.h"
>  #include "fsmonitor-ipc.h"
>  #include "repository.h"
> +#include "alias.h"
>  
>  #ifndef NO_CURL
>  #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
> @@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)
>  	putchar('\n');
>  }
>  
> -static int get_alias(const char *var, const char *value,
> -		     const struct config_context *ctx UNUSED, void *data)
> -{
> -	struct string_list *list = data;
> -
> -	if (skip_prefix(var, "alias.", &var)) {
> -		if (!value)
> -			return config_error_nonbool(var);
> -		string_list_append(list, var)->util = xstrdup(value);
> -	}
> -
> -	return 0;
> -}

We used to use this callback when listing aliases, which (1) added an alias with proper value to the list, and (2) reported a misconfigured variable without adding it to the list. So the net effect was that the user got diagnosis necessary to fix their configuration file, while the caller did not have to worry about getting a broken entry appended to the list.

Show 9 quoted lines
>  static void list_all_cmds_help_external_commands(void)
>  {
>  	struct string_list others = STRING_LIST_INIT_DUP;
> @@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)
>  	struct cmdname_help *aliases;
>  	int i;
>  
> -	repo_config(the_repository, get_alias, &alias_list);
> +	list_aliases(&alias_list);

We call exactly the same alias.c:list_aliases(), which does not feed data->alias at all, so we will take the "else if (data->list)" codepath there. Now we have these broken entries in the returned list. Because the shared callback did not give any diagnosis message, it is on up to us to do so, right?

Perhaps in the code that begins in the post-context of this hunk, here...
	for (i = 0; i < alias_list.nr; i++) {
		if (alias_list.items[i].util)
			continue;
		give error equivanent to config_error_nonbool();
		release resources held by alias_list.items[i];
		shift alias_list.items[i+1..alias_list.nr] by one;
		i-- to compensate for the shift of the array;
	}
or something?

Have you considered doing the config_error_nonbool(key) on the data->list side of the if/else inside alias.c:config_alias_cb(), just like help.c:get_alias() callback used to do?

I haven't stared at this code as long as you have, so it is very possible I am missing the reason why that code path wants to be silent, though. But if we can do so, then this caller does not have to worry about having to handle broken entries at all.

Thanks.
>  	string_list_sort(&alias_list);
>  
>  	for (i = 0; i < alias_list.nr; i++) {
Jonatan Holmgren· Feb 10, 2026, 22:27 UTC · re: Jonatan Holmgren · lore

[PATCH 2/3] alias: prepare for subsection aliases

Switch git_unknown_cmd_config() from skip_prefix() to parse_config_key() for alias parsing. This properly handles the three-level config key structure and prepares for the new alias.*.command subsection syntax in the next commit.

This is a compatibility break: the alias configuration parser used to be overly permissive and accepted "alias.<subsection>.<key>" as defining an alias "<subsection>.<key>". With this change, alias.<subsection>.<key> entries are silently ignored (unless <key> is "command", which will be given meaning in the next commit).

This behavior was arguably a bug, since config subsections were never intended to work this way for aliases, and aliases with dots in their names have never been documented or intentionally supported.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 help.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/help.c b/help.c
index 0bdb7ca10f..eccd0c22f8 100644
--- a/help.c
+++ b/help.c
@@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 				  void *cb)
 {
 	struct help_unknown_cmd_config *cfg = cb;
-	const char *p;
+	const char *subsection, *key;
+	size_t subsection_len;
 
 	if (!strcmp(var, "help.autocorrect")) {
 		int v = parse_autocorrect(value);
@@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	}
 
 	/* Also use aliases for command lookup */
-	if (skip_prefix(var, "alias.", &p))
-		add_cmdname(&cfg->aliases, p, strlen(p));
+	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
+			      &key)) {
+		if (!subsection)
+			add_cmdname(&cfg->aliases, key, strlen(key));
+	}
 
 	return 0;
 }
-- 
2.53.0
Jonatan Holmgren· Feb 10, 2026, 22:27 UTC · re: Jonatan Holmgren · lore

[PATCH 3/3] alias: support non-alphanumeric names via subsection syntax

Git alias names are limited to ASCII alphanumeric characters and dashes because aliases are implemented as config variable names, which are validated by iskeychar(). This prevents non-English speakers from creating aliases in their native languages.

Add support for arbitrary alias names by using config subsections:
    [alias "förgrena"]
        command = branch

The subsection name is matched as-is (case-sensitive byte comparison), while the existing definition without a subsection (e.g., "[alias] co = checkout") remains case-insensitive for backward compatibility. This uses existing config infrastructure since subsections already support arbitrary bytes, and avoids introducing Unicode normalization.

Also teach the help subsystem about the new syntax so that "git help -a" properly lists subsection aliases and the autocorrect feature can suggest them. Use utf8_strwidth() instead of strlen() for column alignment so that non-ASCII alias names display correctly.

Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 Documentation/config/alias.adoc | 43 ++++++++++++++++++-----
 alias.c                         | 39 +++++++++++++++++----
 help.c                          | 14 ++++++--
 t/t0014-alias.sh                | 60 +++++++++++++++++++++++++++++++++
 4 files changed, 138 insertions(+), 18 deletions(-)
diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
index 80ce17d2de..9184f7cb37 100644
--- a/Documentation/config/alias.adoc
+++ b/Documentation/config/alias.adoc
@@ -1,12 +1,39 @@
 alias.*::
-	Command aliases for the linkgit:git[1] command wrapper - e.g.
-	after defining `alias.last = cat-file commit HEAD`, the invocation
-	`git last` is equivalent to `git cat-file commit HEAD`. To avoid
-	confusion and troubles with script usage, aliases that
-	hide existing Git commands are ignored except for deprecated
-	commands.  Arguments are split by
-	spaces, the usual shell quoting and escaping are supported.
-	A quote pair or a backslash can be used to quote them.
+alias.*.command::
+	Command aliases for the linkgit:git[1] command wrapper. Aliases
+	can be defined using two syntaxes:
++
+--
+1. Without a subsection, e.g., `[alias] co = checkout`. The alias
+   name is limited to ASCII alphanumeric characters and `-`,
+   and is matched case-insensitively.
+2. With a subsection, e.g., `[alias "name"] command = value`. The
+   alias name can contain any characters including UTF-8, and is
+   matched case-sensitively as raw bytes.
+--
++
+Examples:
++
+----
+# Without subsection (ASCII alphanumeric and dash only)
+[alias]
+    co = checkout
+    st = status
+
+# With subsection (allows any characters, including UTF-8)
+[alias "hämta"]
+    command = fetch
+[alias "gömma"]
+    command = stash
+----
++
+E.g. after defining `alias.last = cat-file commit HEAD`, the invocation
+`git last` is equivalent to `git cat-file commit HEAD`. To avoid
+confusion and troubles with script usage, aliases that
+hide existing Git commands are ignored except for deprecated
+commands.  Arguments are split by
+spaces, the usual shell quoting and escaping are supported.
+A quote pair or a backslash can be used to quote them.
 +
 Note that the first word of an alias does not necessarily have to be a
 command. It can be a command-line option that will be passed into the
diff --git a/alias.c b/alias.c
index 1f8e13610c..ae1fe1efc6 100644
--- a/alias.c
+++ b/alias.c
@@ -17,15 +17,37 @@ static int config_alias_cb(const char *key, const char *value,
 			   const struct config_context *ctx UNUSED, void *d)
 {
 	struct config_alias_data *data = d;
-	const char *p;
+	const char *subsection, *subkey;
+	size_t subsection_len;
 
-	if (!skip_prefix(key, "alias.", &p))
+	if (parse_config_key(key, "alias", &subsection, &subsection_len,
+			     &subkey) < 0)
 		return 0;
 
+	/*
+	 * Two config syntaxes:
+	 * - alias.name = value   (without subsection, case-insensitive)
+	 * - [alias "name"]
+	 *       command = value  (with subsection, case-sensitive)
+	 */
+	if (subsection) {
+		if (strcmp(subkey, "command"))
+			return 0;
+		if (!value)
+			return config_error_nonbool(key);
+	}
+
 	if (data->alias) {
-		if (!strcasecmp(p, data->alias)) {
-			if (!value)
-				return config_error_nonbool(key);
+		int match;
+
+		if (subsection)
+			match = (strlen(data->alias) == subsection_len &&
+				 !strncmp(data->alias, subsection,
+					  subsection_len));
+		else
+			match = !strcasecmp(data->alias, subkey);
+
+		if (match) {
 			FREE_AND_NULL(data->v);
 			return git_config_string(&data->v,
 						 key, value);
@@ -33,10 +55,13 @@ static int config_alias_cb(const char *key, const char *value,
 	} else if (data->list) {
 		struct string_list_item *item;
 
-		item = string_list_append(data->list, p);
+		if (subsection)
+			item = string_list_append_nodup(data->list,
+				xmemdupz(subsection, subsection_len));
+		else
+			item = string_list_append(data->list, subkey);
 		if (value)
 			item->util = xstrdup(value);
-		/* if !value, item->util remains NULL but item is still added */
 	}
 
 	return 0;
diff --git a/help.c b/help.c
index eccd0c22f8..d7c6011780 100644
--- a/help.c
+++ b/help.c
@@ -21,6 +21,7 @@
 #include "fsmonitor-ipc.h"
 #include "repository.h"
 #include "alias.h"
+#include "utf8.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
 
 	for (i = 0; cmds[i].name; i++) {
 		if (cmds[i].category & mask) {
-			size_t len = strlen(cmds[i].name);
+			size_t len = utf8_strwidth(cmds[i].name);
 			printf("   %s   ", cmds[i].name);
 			if (longest > len)
 				mput_char(' ', longest - len);
@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
-		size_t len = strlen(alias_list.items[i].string);
+		size_t len = utf8_strwidth(alias_list.items[i].string);
 		if (longest < len)
 			longest = len;
 	}
@@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	/* Also use aliases for command lookup */
 	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
 			      &key)) {
-		if (!subsection)
+		if (subsection) {
+			/* [alias "name"] command = value */
+			if (!strcmp(key, "command"))
+				add_cmdname(&cfg->aliases, subsection,
+					    subsection_len);
+		} else {
+			/* alias.name = value */
 			add_cmdname(&cfg->aliases, key, strlen(key));
+		}
 	}
 
 	return 0;
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index 07a53e7366..5d1f7730e6 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -112,4 +112,64 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '
 	done
 '
 
+test_expect_success 'subsection syntax works' '
+	test_config alias.testnew.command "!echo ran-subsection" &&
+	git testnew >output &&
+	test_grep "ran-subsection" output
+'
+
+test_expect_success 'subsection syntax only accepts command key' '
+	test_config alias.invalid.notcommand value &&
+	test_must_fail git invalid 2>error &&
+	test_grep -i "not a git command" error
+'
+
+test_expect_success 'subsection syntax requires value for command' '
+	test_when_finished "git config --remove-section alias.noval" &&
+	cat >>.git/config <<-\EOF &&
+	[alias "noval"]
+		command
+	EOF
+	test_must_fail git noval 2>error &&
+	test_grep "alias.noval.command" error
+'
+
+test_expect_success 'simple syntax is case-insensitive' '
+	test_config alias.LegacyCase "!echo ran-legacy" &&
+	git legacycase >output &&
+	test_grep "ran-legacy" output
+'
+
+test_expect_success 'subsection syntax is case-sensitive' '
+	test_config alias.SubCase.command "!echo ran-upper" &&
+	test_config alias.subcase.command "!echo ran-lower" &&
+	git SubCase >upper.out &&
+	git subcase >lower.out &&
+	test_grep "ran-upper" upper.out &&
+	test_grep "ran-lower" lower.out
+'
+
+test_expect_success 'UTF-8 alias with Swedish characters' '
+	test_config alias."förgrena".command "!echo ran-swedish" &&
+	git förgrena >output &&
+	test_grep "ran-swedish" output
+'
+
+test_expect_success 'UTF-8 alias with CJK characters' '
+	test_config alias."分支".command "!echo ran-cjk" &&
+	git 分支 >output &&
+	test_grep "ran-cjk" output
+'
+
+test_expect_success 'alias with spaces in name' '
+	test_config alias."test name".command "!echo ran-spaces" &&
+	git "test name" >output &&
+	test_grep "ran-spaces" output
+'
+
+test_expect_success 'subsection aliases listed in help -a' '
+	test_config alias."förgrena".command "!echo test" &&
+	git help -a | grep "förgrena"
+'
+
 test_done
-- 
2.53.0
Jonatan Holmgren· Feb 11, 2026, 21:18 UTC · re: Jonatan Holmgren · lore

[PATCH v4 0/3] support UTF-8 in alias names

This series enables UTF-8 and special characters in Git alias names by using config subsection syntax: `[alias "name"] command = value`. This allows non-English speakers to create aliases in their native languages while maintaining backward compatibility with the traditional syntax.

Changes since v3:
- Removed redundant !value check in data->alias branch, as Junio noted
  that git_config_string() already handles this case
- Added config_error_nonbool() in data->list branch so misconfigured
  aliases (without values) are caught immediately instead of silently
  adding broken entries that callers must handle
- Simplified code: consolidated error checking, removed conditional
  item->util assignment in favor of early error return
- Added test case for value-less alias error handling
- Improved test robustness by using intermediate file + test_grep
  instead of piping to grep
Jonatan Holmgren (3):
  help: use list_aliases() for alias listing
  alias: prepare for subsection aliases
  alias: support non-alphanumeric names via subsection syntax
 Documentation/config/alias.adoc | 43 ++++++++++++++++----
 alias.c                         | 38 ++++++++++++++++--
 help.c                          | 39 +++++++++---------
 t/t0014-alias.sh                | 71 +++++++++++++++++++++++++++++++++
 4 files changed, 159 insertions(+), 32 deletions(-)
-- 
2.53.0
Jonatan Holmgren· Feb 11, 2026, 21:18 UTC · re: Jonatan Holmgren · lore

[PATCH v4 2/3] alias: prepare for subsection aliases

Switch git_unknown_cmd_config() from skip_prefix() to parse_config_key() for alias parsing. This properly handles the three-level config key structure and prepares for the new alias.*.command subsection syntax in the next commit.

This is a compatibility break: the alias configuration parser used to be overly permissive and accepted "alias.<subsection>.<key>" as defining an alias "<subsection>.<key>". With this change, alias.<subsection>.<key> entries are silently ignored (unless <key> is "command", which will be given meaning in the next commit).

This behavior was arguably a bug, since config subsections were never intended to work this way for aliases, and aliases with dots in their names have never been documented or intentionally supported.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 help.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/help.c b/help.c
index 0bdb7ca10f..eccd0c22f8 100644
--- a/help.c
+++ b/help.c
@@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 				  void *cb)
 {
 	struct help_unknown_cmd_config *cfg = cb;
-	const char *p;
+	const char *subsection, *key;
+	size_t subsection_len;
 
 	if (!strcmp(var, "help.autocorrect")) {
 		int v = parse_autocorrect(value);
@@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	}
 
 	/* Also use aliases for command lookup */
-	if (skip_prefix(var, "alias.", &p))
-		add_cmdname(&cfg->aliases, p, strlen(p));
+	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
+			      &key)) {
+		if (!subsection)
+			add_cmdname(&cfg->aliases, key, strlen(key));
+	}
 
 	return 0;
 }
-- 
2.53.0
Junio C Hamano· Feb 11, 2026, 21:53 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v4 2/3] alias: prepare for subsection aliases

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 19 quoted lines
> Switch git_unknown_cmd_config() from skip_prefix() to
> parse_config_key() for alias parsing. This properly handles the
> three-level config key structure and prepares for the new
> alias.*.command subsection syntax in the next commit.
>
> This is a compatibility break: the alias configuration parser used
> to be overly permissive and accepted "alias.<subsection>.<key>" as
> defining an alias "<subsection>.<key>". With this change,
> alias.<subsection>.<key> entries are silently ignored (unless <key>
> is "command", which will be given meaning in the next commit).
>
> This behavior was arguably a bug, since config subsections were never
> intended to work this way for aliases, and aliases with dots in their
> names have never been documented or intentionally supported.
>
> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
> ---
>  help.c | 10 +++++++---
>  1 file changed, 7 insertions(+), 3 deletions(-)
Very well explained.
Show 29 quoted lines
>
> diff --git a/help.c b/help.c
> index 0bdb7ca10f..eccd0c22f8 100644
> --- a/help.c
> +++ b/help.c
> @@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,
>  				  void *cb)
>  {
>  	struct help_unknown_cmd_config *cfg = cb;
> -	const char *p;
> +	const char *subsection, *key;
> +	size_t subsection_len;
>  
>  	if (!strcmp(var, "help.autocorrect")) {
>  		int v = parse_autocorrect(value);
> @@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,
>  	}
>  
>  	/* Also use aliases for command lookup */
> -	if (skip_prefix(var, "alias.", &p))
> -		add_cmdname(&cfg->aliases, p, strlen(p));
> +	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
> +			      &key)) {
> +		if (!subsection)
> +			add_cmdname(&cfg->aliases, key, strlen(key));
> +	}
>  
>  	return 0;
>  }
Jonatan Holmgren· Feb 11, 2026, 21:18 UTC · re: Jonatan Holmgren · lore

[PATCH v4 1/3] help: use list_aliases() for alias listing

help.c has its own get_alias() config callback that duplicates the parsing logic in alias.c. Consolidate by teaching list_aliases() to also store the alias values (via the string_list util field), then use it in list_all_cmds_help_aliases() instead of the private callback.

This preserves the existing error checking for value-less alias definitions by checking in alias.c rather than help.c.

No functional change intended.
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 alias.c          |  8 +++++++-
 help.c           | 17 ++---------------
 t/t0014-alias.sh | 10 ++++++++++
 3 files changed, 19 insertions(+), 16 deletions(-)
diff --git a/alias.c b/alias.c
index 1a1a141a0a..271acb9bf1 100644
--- a/alias.c
+++ b/alias.c
@@ -29,7 +29,13 @@ static int config_alias_cb(const char *key, const char *value,
 						 key, value);
 		}
 	} else if (data->list) {
-		string_list_append(data->list, p);
+		struct string_list_item *item;
+
+		if (!value)
+			return config_error_nonbool(key);
+
+		item = string_list_append(data->list, p);
+		item->util = xstrdup(value);
 	}
 
 	return 0;
diff --git a/help.c b/help.c
index fefd811f7a..0bdb7ca10f 100644
--- a/help.c
+++ b/help.c
@@ -20,6 +20,7 @@
 #include "prompt.h"
 #include "fsmonitor-ipc.h"
 #include "repository.h"
+#include "alias.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)
 	putchar('\n');
 }
 
-static int get_alias(const char *var, const char *value,
-		     const struct config_context *ctx UNUSED, void *data)
-{
-	struct string_list *list = data;
-
-	if (skip_prefix(var, "alias.", &var)) {
-		if (!value)
-			return config_error_nonbool(var);
-		string_list_append(list, var)->util = xstrdup(value);
-	}
-
-	return 0;
-}
-
 static void list_all_cmds_help_external_commands(void)
 {
 	struct string_list others = STRING_LIST_INIT_DUP;
@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)
 	struct cmdname_help *aliases;
 	int i;
 
-	repo_config(the_repository, get_alias, &alias_list);
+	list_aliases(&alias_list);
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index 07a53e7366..a13d2be8ca 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -112,4 +112,14 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '
 	done
 '
 
+test_expect_success 'alias without value reports error' '
+	test_when_finished "git config --unset alias.noval" &&
+	cat >>.git/config <<-\EOF &&
+	[alias]
+		noval
+	EOF
+	test_must_fail git noval 2>error &&
+	test_grep "alias.noval" error
+'
+
 test_done
-- 
2.53.0
Junio C Hamano· Feb 11, 2026, 22:29 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v4 1/3] help: use list_aliases() for alias listing

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 17 quoted lines
> help.c has its own get_alias() config callback that duplicates the
> parsing logic in alias.c. Consolidate by teaching list_aliases() to
> also store the alias values (via the string_list util field), then
> use it in list_all_cmds_help_aliases() instead of the private
> callback.
>
> This preserves the existing error checking for value-less alias
> definitions by checking in alias.c rather than help.c.
>
> No functional change intended.
>
> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
> ---
>  alias.c          |  8 +++++++-
>  help.c           | 17 ++---------------
>  t/t0014-alias.sh | 10 ++++++++++
>  3 files changed, 19 insertions(+), 16 deletions(-)

Looks good. There is a small functional change not on the help.c:get_alias() side (i.e., "git help --all") but on the alias.c:list_aliases() side. "git --list-cmds=alias", used by command line completion, used to include such a broken alias, but it no longer does (and gets an error).

I think it is fine to call it a bugfix ;-)
Show 80 quoted lines
> diff --git a/alias.c b/alias.c
> index 1a1a141a0a..271acb9bf1 100644
> --- a/alias.c
> +++ b/alias.c
> @@ -29,7 +29,13 @@ static int config_alias_cb(const char *key, const char *value,
>  						 key, value);
>  		}
>  	} else if (data->list) {
> -		string_list_append(data->list, p);
> +		struct string_list_item *item;
> +
> +		if (!value)
> +			return config_error_nonbool(key);
> +
> +		item = string_list_append(data->list, p);
> +		item->util = xstrdup(value);
>  	}
>  
>  	return 0;
> diff --git a/help.c b/help.c
> index fefd811f7a..0bdb7ca10f 100644
> --- a/help.c
> +++ b/help.c
> @@ -20,6 +20,7 @@
>  #include "prompt.h"
>  #include "fsmonitor-ipc.h"
>  #include "repository.h"
> +#include "alias.h"
>  
>  #ifndef NO_CURL
>  #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
> @@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)
>  	putchar('\n');
>  }
>  
> -static int get_alias(const char *var, const char *value,
> -		     const struct config_context *ctx UNUSED, void *data)
> -{
> -	struct string_list *list = data;
> -
> -	if (skip_prefix(var, "alias.", &var)) {
> -		if (!value)
> -			return config_error_nonbool(var);
> -		string_list_append(list, var)->util = xstrdup(value);
> -	}
> -
> -	return 0;
> -}
> -
>  static void list_all_cmds_help_external_commands(void)
>  {
>  	struct string_list others = STRING_LIST_INIT_DUP;
> @@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)
>  	struct cmdname_help *aliases;
>  	int i;
>  
> -	repo_config(the_repository, get_alias, &alias_list);
> +	list_aliases(&alias_list);
>  	string_list_sort(&alias_list);
>  
>  	for (i = 0; i < alias_list.nr; i++) {
> diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
> index 07a53e7366..a13d2be8ca 100755
> --- a/t/t0014-alias.sh
> +++ b/t/t0014-alias.sh
> @@ -112,4 +112,14 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '
>  	done
>  '
>  
> +test_expect_success 'alias without value reports error' '
> +	test_when_finished "git config --unset alias.noval" &&
> +	cat >>.git/config <<-\EOF &&
> +	[alias]
> +		noval
> +	EOF
> +	test_must_fail git noval 2>error &&
> +	test_grep "alias.noval" error
> +'
> +
>  test_done
Jonatan Holmgren· Feb 11, 2026, 21:18 UTC · re: Jonatan Holmgren · lore

[PATCH v4 3/3] alias: support non-alphanumeric names via subsection syntax

Git alias names are limited to ASCII alphanumeric characters and dashes because aliases are implemented as config variable names. This prevents non-English speakers from creating aliases in their native languages.

Add support for arbitrary alias names by using config subsections:
    [alias "förgrena"]
        command = branch

The subsection name is matched as-is (case-sensitive byte comparison), while the existing definition without a subsection (e.g., "[alias] co = checkout") remains case-insensitive for backward compatibility. This uses existing config infrastructure since subsections already support arbitrary bytes, and avoids introducing Unicode normalization.

Also teach the help subsystem about the new syntax so that "git help -a" properly lists subsection aliases and the autocorrect feature can suggest them. Use utf8_strwidth() instead of strlen() for column alignment so that non-ASCII alias names display correctly.

Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 Documentation/config/alias.adoc | 43 ++++++++++++++++++-----
 alias.c                         | 32 ++++++++++++++---
 help.c                          | 14 ++++++--
 t/t0014-alias.sh                | 61 +++++++++++++++++++++++++++++++++
 4 files changed, 135 insertions(+), 15 deletions(-)
diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
index 80ce17d2de..9184f7cb37 100644
--- a/Documentation/config/alias.adoc
+++ b/Documentation/config/alias.adoc
@@ -1,12 +1,39 @@
 alias.*::
-	Command aliases for the linkgit:git[1] command wrapper - e.g.
-	after defining `alias.last = cat-file commit HEAD`, the invocation
-	`git last` is equivalent to `git cat-file commit HEAD`. To avoid
-	confusion and troubles with script usage, aliases that
-	hide existing Git commands are ignored except for deprecated
-	commands.  Arguments are split by
-	spaces, the usual shell quoting and escaping are supported.
-	A quote pair or a backslash can be used to quote them.
+alias.*.command::
+	Command aliases for the linkgit:git[1] command wrapper. Aliases
+	can be defined using two syntaxes:
++
+--
+1. Without a subsection, e.g., `[alias] co = checkout`. The alias
+   name is limited to ASCII alphanumeric characters and `-`,
+   and is matched case-insensitively.
+2. With a subsection, e.g., `[alias "name"] command = value`. The
+   alias name can contain any characters including UTF-8, and is
+   matched case-sensitively as raw bytes.
+--
++
+Examples:
++
+----
+# Without subsection (ASCII alphanumeric and dash only)
+[alias]
+    co = checkout
+    st = status
+
+# With subsection (allows any characters, including UTF-8)
+[alias "hämta"]
+    command = fetch
+[alias "gömma"]
+    command = stash
+----
++
+E.g. after defining `alias.last = cat-file commit HEAD`, the invocation
+`git last` is equivalent to `git cat-file commit HEAD`. To avoid
+confusion and troubles with script usage, aliases that
+hide existing Git commands are ignored except for deprecated
+commands.  Arguments are split by
+spaces, the usual shell quoting and escaping are supported.
+A quote pair or a backslash can be used to quote them.
 +
 Note that the first word of an alias does not necessarily have to be a
 command. It can be a command-line option that will be passed into the
diff --git a/alias.c b/alias.c
index 271acb9bf1..896d0f80a4 100644
--- a/alias.c
+++ b/alias.c
@@ -17,13 +17,33 @@ static int config_alias_cb(const char *key, const char *value,
 			   const struct config_context *ctx UNUSED, void *d)
 {
 	struct config_alias_data *data = d;
-	const char *p;
+	const char *subsection, *subkey;
+	size_t subsection_len;
 
-	if (!skip_prefix(key, "alias.", &p))
+	if (parse_config_key(key, "alias", &subsection, &subsection_len,
+			     &subkey) < 0)
+		return 0;
+
+	/*
+	 * Two config syntaxes:
+	 * - alias.name = value   (without subsection, case-insensitive)
+	 * - [alias "name"]
+	 *       command = value  (with subsection, case-sensitive)
+	 */
+	if (subsection && strcmp(subkey, "command"))
 		return 0;
 
 	if (data->alias) {
-		if (!strcasecmp(p, data->alias)) {
+		int match;
+
+		if (subsection)
+			match = (strlen(data->alias) == subsection_len &&
+				 !strncmp(data->alias, subsection,
+					  subsection_len));
+		else
+			match = !strcasecmp(data->alias, subkey);
+
+		if (match) {
 			FREE_AND_NULL(data->v);
 			return git_config_string(&data->v,
 						 key, value);
@@ -34,7 +54,11 @@ static int config_alias_cb(const char *key, const char *value,
 		if (!value)
 			return config_error_nonbool(key);
 
-		item = string_list_append(data->list, p);
+		if (subsection)
+			item = string_list_append_nodup(data->list,
+				xmemdupz(subsection, subsection_len));
+		else
+			item = string_list_append(data->list, subkey);
 		item->util = xstrdup(value);
 	}
 
diff --git a/help.c b/help.c
index eccd0c22f8..d7c6011780 100644
--- a/help.c
+++ b/help.c
@@ -21,6 +21,7 @@
 #include "fsmonitor-ipc.h"
 #include "repository.h"
 #include "alias.h"
+#include "utf8.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
 
 	for (i = 0; cmds[i].name; i++) {
 		if (cmds[i].category & mask) {
-			size_t len = strlen(cmds[i].name);
+			size_t len = utf8_strwidth(cmds[i].name);
 			printf("   %s   ", cmds[i].name);
 			if (longest > len)
 				mput_char(' ', longest - len);
@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
-		size_t len = strlen(alias_list.items[i].string);
+		size_t len = utf8_strwidth(alias_list.items[i].string);
 		if (longest < len)
 			longest = len;
 	}
@@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	/* Also use aliases for command lookup */
 	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
 			      &key)) {
-		if (!subsection)
+		if (subsection) {
+			/* [alias "name"] command = value */
+			if (!strcmp(key, "command"))
+				add_cmdname(&cfg->aliases, subsection,
+					    subsection_len);
+		} else {
+			/* alias.name = value */
 			add_cmdname(&cfg->aliases, key, strlen(key));
+		}
 	}
 
 	return 0;
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index a13d2be8ca..34bbdb51c5 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -122,4 +122,65 @@ test_expect_success 'alias without value reports error' '
 	test_grep "alias.noval" error
 '
 
+test_expect_success 'subsection syntax works' '
+	test_config alias.testnew.command "!echo ran-subsection" &&
+	git testnew >output &&
+	test_grep "ran-subsection" output
+'
+
+test_expect_success 'subsection syntax only accepts command key' '
+	test_config alias.invalid.notcommand value &&
+	test_must_fail git invalid 2>error &&
+	test_grep -i "not a git command" error
+'
+
+test_expect_success 'subsection syntax requires value for command' '
+	test_when_finished "git config --remove-section alias.noval" &&
+	cat >>.git/config <<-\EOF &&
+	[alias "noval"]
+		command
+	EOF
+	test_must_fail git noval 2>error &&
+	test_grep "alias.noval.command" error
+'
+
+test_expect_success 'simple syntax is case-insensitive' '
+	test_config alias.LegacyCase "!echo ran-legacy" &&
+	git legacycase >output &&
+	test_grep "ran-legacy" output
+'
+
+test_expect_success 'subsection syntax is case-sensitive' '
+	test_config alias.SubCase.command "!echo ran-upper" &&
+	test_config alias.subcase.command "!echo ran-lower" &&
+	git SubCase >upper.out &&
+	git subcase >lower.out &&
+	test_grep "ran-upper" upper.out &&
+	test_grep "ran-lower" lower.out
+'
+
+test_expect_success 'UTF-8 alias with Swedish characters' '
+	test_config alias."förgrena".command "!echo ran-swedish" &&
+	git förgrena >output &&
+	test_grep "ran-swedish" output
+'
+
+test_expect_success 'UTF-8 alias with CJK characters' '
+	test_config alias."分支".command "!echo ran-cjk" &&
+	git 分支 >output &&
+	test_grep "ran-cjk" output
+'
+
+test_expect_success 'alias with spaces in name' '
+	test_config alias."test name".command "!echo ran-spaces" &&
+	git "test name" >output &&
+	test_grep "ran-spaces" output
+'
+
+test_expect_success 'subsection aliases listed in help -a' '
+	test_config alias."förgrena".command "!echo test" &&
+	git help -a >output &&
+	test_grep "förgrena" output
+'
+
 test_done
-- 
2.53.0
Junio C Hamano· Feb 11, 2026, 22:28 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v4 3/3] alias: support non-alphanumeric names via subsection syntax

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 9 quoted lines
>  alias.*::
> +alias.*.command::
> +	Command aliases for the linkgit:git[1] command wrapper. Aliases
> +	can be defined using two syntaxes:
> ++
> +--
> +1. Without a subsection, e.g., `[alias] co = checkout`. The alias
> +   name is limited to ASCII alphanumeric characters and `-`,
> +   and is matched case-insensitively.

OK. It is obvious to us that the "alias name" in the example is "co"; is it obvious enough for our first-time readers, or would we want to do something like

	The alias name ("co", in this example) is limited to ...
to be extra clear, I wonder.
> +2. With a subsection, e.g., `[alias "name"] command = value`. The
> +   alias name can contain any characters including UTF-8, and is
> +   matched case-sensitively as raw bytes.

Unlike the previous example that is more realistic, this uses <name> and <value> placeholders, with `command` that MUST be given verbatim by the users. Is that obvious enough to our first-time readers?

Thinking aloud. How does it look with placeholder filled with concrete values?

	... e.g., `[alias "co"] command = checkout`.  The alias name
	("co", in this example) can contain any characters ...
This does not look too bad to me.

We do not allow newlines or NULs in the subsection. NULs may be too obvious, but newlines might be worth mentioning. I dunno.

Show 16 quoted lines
> +--
> ++
> +Examples:
> ++
> +----
> +# Without subsection (ASCII alphanumeric and dash only)
> +[alias]
> +    co = checkout
> +    st = status
> +
> +# With subsection (allows any characters, including UTF-8)
> +[alias "hämta"]
> +    command = fetch
> +[alias "gömma"]
> +    command = stash
> +----
Good examples, even though I do not read Swedish ;-).
> +E.g. after defining `alias.last = cat-file commit HEAD`, the invocation
> +`git last` is equivalent to `git cat-file commit HEAD`.

This is not a new problem (it is an inherited text from before your change), but I've always found this

	alias.last = cat-file commit HEAD

a poor thing to give to our users, as it does not match anything they practically can use. It is different from the valid command line arguments to define the alias, which is

	$ git config set alias.last "cat-file commit HEAD"

and it is different from the way the result appears in the configuration file, which is

	[alias] last = cat-file commit HEAD

Also, since the sentences are moved around, I am not sure that the beginning "E.g." still fits there very well. Taking them all together, how about

    With a Git alias defined, e.g.,
	$ git config set alias.last "cat-file commit HEAD"
    you can run `git last` and it invokes `git cat-file commit
    HEAD`.
Show 11 quoted lines
> diff --git a/alias.c b/alias.c
> index 271acb9bf1..896d0f80a4 100644
> --- a/alias.c
> +++ b/alias.c
> @@ -17,13 +17,33 @@ static int config_alias_cb(const char *key, const char *value,
>  			   const struct config_context *ctx UNUSED, void *d)
>  {
>  	struct config_alias_data *data = d;
> -	const char *p;
> +	const char *subsection, *subkey;
> +	size_t subsection_len;
"subkey" is a confusing name for a variable.

The Synatx section in "git config --help" documentation says that a configuration file consists of "sections and variables", and a section can further be divided into subsections.

config.c seems to use "key" as a synonym for "variable" above, and that is very understandable, because "section.subsection.variable" or "section.variable" as a whole is what the users and documentation calls a "configuration variable", and to avoid overloading the two meanings on the same word "variable", we'd better use a different name for that last-level thing.

Taken together, in
	[alias] co = checkout
	[alias "ci"] command = commit
it would be the best to call the parts like so:
	section: "alias"
	subsection: "ci"
	key: "co" and "command"
Show 13 quoted lines
> -	if (!skip_prefix(key, "alias.", &p))
> +	if (parse_config_key(key, "alias", &subsection, &subsection_len,
> +			     &subkey) < 0)
> +		return 0;
> +
> +	/*
> +	 * Two config syntaxes:
> +	 * - alias.name = value   (without subsection, case-insensitive)
> +	 * - [alias "name"]
> +	 *       command = value  (with subsection, case-sensitive)
> +	 */
> +	if (subsection && strcmp(subkey, "command"))
>  		return 0;
OK.  We ignore alias.*.variable where variable is not "command".
>  	if (data->alias) {
When the caller is querying one specific alias ...
Show 6 quoted lines
> +		int match;
> +
> +		if (subsection)
> +			match = (strlen(data->alias) == subsection_len &&
> +				 !strncmp(data->alias, subsection,
> +					  subsection_len));

... we pick either the one that literally matches the subsection part (we have already verified that the key is "command"), or ...

> +		else
> +			match = !strcasecmp(data->alias, subkey);

... for a two-level variable, the one that matches variable name case insensitively. And when we see hit, ...

> +		if (match) {
>  			FREE_AND_NULL(data->v);
>  			return git_config_string(&data->v,
>  						 key, value);
... we report it to the caller.  Otherwise, when we are listing ...
Show 10 quoted lines
> @@ -34,7 +54,11 @@ static int config_alias_cb(const char *key, const char *value,
>  		if (!value)
>  			return config_error_nonbool(key);
>  
> -		item = string_list_append(data->list, p);
> +		if (subsection)
> +			item = string_list_append_nodup(data->list,
> +				xmemdupz(subsection, subsection_len));
> +		else
> +			item = string_list_append(data->list, subkey);

... the alias name we create differs between the two- and three-level names, but otherwise the handling is the same between the two kinds.

>  		item->util = xstrdup(value);
>  	}
All makes sense.
Show 47 quoted lines
> diff --git a/help.c b/help.c
> index eccd0c22f8..d7c6011780 100644
> --- a/help.c
> +++ b/help.c
> @@ -21,6 +21,7 @@
>  #include "fsmonitor-ipc.h"
>  #include "repository.h"
>  #include "alias.h"
> +#include "utf8.h"
>  
>  #ifndef NO_CURL
>  #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
> @@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
>  
>  	for (i = 0; cmds[i].name; i++) {
>  		if (cmds[i].category & mask) {
> -			size_t len = strlen(cmds[i].name);
> +			size_t len = utf8_strwidth(cmds[i].name);
>  			printf("   %s   ", cmds[i].name);
>  			if (longest > len)
>  				mput_char(' ', longest - len);
> @@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)
>  	string_list_sort(&alias_list);
>  
>  	for (i = 0; i < alias_list.nr; i++) {
> -		size_t len = strlen(alias_list.items[i].string);
> +		size_t len = utf8_strwidth(alias_list.items[i].string);
>  		if (longest < len)
>  			longest = len;
>  	}
> @@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,
>  	/* Also use aliases for command lookup */
>  	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
>  			      &key)) {
> -		if (!subsection)
> +		if (subsection) {
> +			/* [alias "name"] command = value */
> +			if (!strcmp(key, "command"))
> +				add_cmdname(&cfg->aliases, subsection,
> +					    subsection_len);
> +		} else {
> +			/* alias.name = value */
>  			add_cmdname(&cfg->aliases, key, strlen(key));
> +		}
>  	}
>  
>  	return 0;
Looks very good.
Richard Kerry· Feb 12, 2026, 11:16 UTC · re: Jonatan Holmgren · lore

RE: [PATCH v4 3/3] alias: support non-alphanumeric names via subsection syntax

Show 7 quoted lines
> -----Original Message-----
> From: Jonatan Holmgren <jonatan@jontes.page>
> Sent: 11 February 2026 21:18
> 
> Git alias names are limited to ASCII alphanumeric characters and dashes because
> aliases are implemented as config variable names.
> This prevents non-English speakers from creating aliases in their native languages.
I think that is overly specific as a use case and should probably be more like:
This prevents aliases being created in languages using characters outside that range.
1.  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.
2.  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).

Regards, Richard.

Jonatan Holmgren· Feb 12, 2026, 15:34 UTC · re: Richard Kerry · lore

Re: [PATCH v4 3/3] alias: support non-alphanumeric names via subsection syntax

Hi Richard,

Good point regarding the distinction between the human and the character set.

I did actually consider that distinction when writing it, but I struggled to find a phrasing that didn't feel clunky, which is why I settled on the version I sent. However, I agree that your suggestion is more technically accurate.

I'll adopt your wording in the next version. Thanks for the review!

Best, Jonatan

Jonatan Holmgren· Feb 12, 2026, 18:52 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v4 3/3] alias: support non-alphanumeric names via subsection syntax

I have discovered that shell completion suggests the "<alias>.command" which is a bug. Can't believe I didn't test that. Fixing in revision 5.

Jonatan
Jonatan Holmgren· Feb 12, 2026, 15:35 UTC · re: Torsten Bögershausen · lore

Re: [PATCH v4 0/3] support UTF-8 in alias names

Hello!

You should to see the patch from the public inbox at https://lore.kernel.org/git/20260209135917.GD27241@macsyma.lan/T/#mfb25a7f945ceebb6c8619680cac46c990e4ea112

Best, Jonatan

On 2026-02-12 11:27, Torsten Bögershausen wrote:
> On Wed, Feb 11, 2026 at 10:18:07PM +0100, Jonatan Holmgren wrote:
> []
> Is it only me who only sees the header message, the 0/3, and not
> the patches themselves, the 1/3..3/3 ?
Jonatan Holmgren· Feb 16, 2026, 16:15 UTC · re: Jonatan Holmgren · lore

[PATCH v5 0/4] support uTF-8 in alias names

Heya!

This series enables UTF-8 and special characters in Git alias names by using config subsection syntax: [alias "name"] command = value.

I have since last time:
* Changed "native speakers" to "languages outside English range" (thanks Richard!)
* Fixed shell completion in zsh
* Improved docs slightly with feedback from Junio. Also changed out one of the examples with one with spaces to demonstrate that feature.
Thanks for your time.
Jonatan Holmgren (4):
  help: use list_aliases() for alias listing
  alias: prepare for subsection aliases
  alias: support non-alphanumeric names via subsection syntax
  completion: fix zsh alias listing for subsection aliases
 Documentation/config/alias.adoc       | 50 ++++++++++++++++---
 alias.c                               | 42 +++++++++++++---
 builtin/help.c                        | 15 ++++++
 contrib/completion/git-completion.zsh |  2 +-
 help.c                                | 39 +++++++--------
 t/t0014-alias.sh                      | 71 +++++++++++++++++++++++++++
 6 files changed, 184 insertions(+), 35 deletions(-)
-- 
2.53.0.83.g660bbd62ee.dirty
Jonatan Holmgren· Feb 16, 2026, 16:15 UTC · re: Jonatan Holmgren · lore

[PATCH v5 4/4] completion: fix zsh alias listing for subsection aliases

The zsh completion function __git_zsh_cmd_alias() uses 'git config --get-regexp' to enumerate aliases and then strips the "alias." prefix from each key. For subsection-style aliases (alias.name.command), this leaves "name.command" as the completion candidate instead of just "name".

The bash completion does not have this problem because it goes through 'git --list-cmds=alias', which calls list_aliases() in C and already handles both alias syntaxes correctly. However, zsh needs both the alias name and its value for descriptive completion, which --list-cmds=alias does not provide.

Add a hidden --aliases-for-completion option to 'git help', following
the existing --config-for-completion pattern. It outputs NUL-separated
"name\nvalue" pairs using list_aliases(), which correctly resolves both
the traditional (alias.name) and subsection (alias.name.command)
formats. Update __git_zsh_cmd_alias() to use it.
---
 builtin/help.c                        | 15 +++++++++++++++
 contrib/completion/git-completion.zsh |  2 +-
 2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/builtin/help.c b/builtin/help.c
index c09cbc8912..f02308a391 100644
--- a/builtin/help.c
+++ b/builtin/help.c
@@ -54,6 +54,7 @@ static enum help_action {
 	HELP_ACTION_DEVELOPER_INTERFACES,
 	HELP_ACTION_CONFIG_FOR_COMPLETION,
 	HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION,
+	HELP_ACTION_ALIASES_FOR_COMPLETION,
 } cmd_mode;
 
 static char *html_path;
@@ -90,6 +91,8 @@ static struct option builtin_help_options[] = {
 		    HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN),
 	OPT_CMDMODE_F(0, "config-sections-for-completion", &cmd_mode, "",
 		    HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "aliases-for-completion", &cmd_mode, "",
+		    HELP_ACTION_ALIASES_FOR_COMPLETION, PARSE_OPT_HIDDEN),
 
 	OPT_END(),
 };
@@ -691,6 +694,18 @@ int cmd_help(int argc,
 			       help_format);
 		list_config_help(SHOW_CONFIG_SECTIONS);
 		return 0;
+	case HELP_ACTION_ALIASES_FOR_COMPLETION: {
+		struct string_list alias_list = STRING_LIST_INIT_DUP;
+		opt_mode_usage(argc, "--aliases-for-completion",
+			       help_format);
+		list_aliases(&alias_list);
+		string_list_sort(&alias_list);
+		for (size_t i = 0; i < alias_list.nr; i++)
+			printf("%s%c%s%c", alias_list.items[i].string, '\n',
+			       (char *)alias_list.items[i].util, '\0');
+		string_list_clear(&alias_list, 1);
+		return 0;
+	}
 	case HELP_ACTION_CONFIG:
 		opt_mode_usage(argc, "--config", help_format);
 		setup_pager(the_repository);
diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
index f5877bd7a1..c32186a977 100644
--- a/contrib/completion/git-completion.zsh
+++ b/contrib/completion/git-completion.zsh
@@ -202,7 +202,7 @@ __git_zsh_cmd_common ()
 __git_zsh_cmd_alias ()
 {
 	local -a list
-	list=(${${(0)"$(git config -z --get-regexp '^alias\.*')"}#alias.})
+	list=(${(0)"$(git help --aliases-for-completion)"})
 	list=(${(f)"$(printf "%s:alias for '%s'\n" ${(f@)list})"})
 	_describe -t alias-commands 'aliases' list && _ret=0
 }
-- 
2.53.0.83.g660bbd62ee.dirty
D. Ben Knoble· Feb 16, 2026, 18:32 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v5 4/4] completion: fix zsh alias listing for subsection aliases

On Mon, Feb 16, 2026 at 11:15 AM Jonatan Holmgren <jonatan@jontes.page> wrote:
Show 18 quoted lines
>
> The zsh completion function __git_zsh_cmd_alias() uses 'git config
> --get-regexp' to enumerate aliases and then strips the "alias." prefix
> from each key. For subsection-style aliases (alias.name.command), this
> leaves "name.command" as the completion candidate instead of just
> "name".
>
> The bash completion does not have this problem because it goes through
> 'git --list-cmds=alias', which calls list_aliases() in C and already
> handles both alias syntaxes correctly. However, zsh needs both the
> alias name and its value for descriptive completion, which
> --list-cmds=alias does not provide.
>
> Add a hidden --aliases-for-completion option to 'git help', following
> the existing --config-for-completion pattern. It outputs NUL-separated
> "name\nvalue" pairs using list_aliases(), which correctly resolves both
> the traditional (alias.name) and subsection (alias.name.command)
> formats. Update __git_zsh_cmd_alias() to use it.

An alternative would to be to post-process and turn "alias.name.command" into "name", but this solution is easier to parse (won't mix 2 formats).

Show 34 quoted lines
> ---
>  builtin/help.c                        | 15 +++++++++++++++
>  contrib/completion/git-completion.zsh |  2 +-
>  2 files changed, 16 insertions(+), 1 deletion(-)
>
> diff --git a/builtin/help.c b/builtin/help.c
> index c09cbc8912..f02308a391 100644
> --- a/builtin/help.c
> +++ b/builtin/help.c
> @@ -54,6 +54,7 @@ static enum help_action {
>         HELP_ACTION_DEVELOPER_INTERFACES,
>         HELP_ACTION_CONFIG_FOR_COMPLETION,
>         HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION,
> +       HELP_ACTION_ALIASES_FOR_COMPLETION,
>  } cmd_mode;
>
>  static char *html_path;
> @@ -90,6 +91,8 @@ static struct option builtin_help_options[] = {
>                     HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN),
>         OPT_CMDMODE_F(0, "config-sections-for-completion", &cmd_mode, "",
>                     HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN),
> +       OPT_CMDMODE_F(0, "aliases-for-completion", &cmd_mode, "",
> +                   HELP_ACTION_ALIASES_FOR_COMPLETION, PARSE_OPT_HIDDEN),
>
>         OPT_END(),
>  };
> @@ -691,6 +694,18 @@ int cmd_help(int argc,
>                                help_format);
>                 list_config_help(SHOW_CONFIG_SECTIONS);
>                 return 0;
> +       case HELP_ACTION_ALIASES_FOR_COMPLETION: {
> +               struct string_list alias_list = STRING_LIST_INIT_DUP;
> +               opt_mode_usage(argc, "--aliases-for-completion",
> +                              help_format);
style nit: I _think_ this line could be unwrapped?
> +               list_aliases(&alias_list);
> +               string_list_sort(&alias_list);

It may not matter much since lists of aliases are probably not humongous, but do we need to sort? I don't _think_ "git --list-cmds=alias" does, for example, though I'm not sure about "git config --get-regexp" that's being replaced. Leaving it in the config order could even be a feature, so I can put easier-to-complete aliases earlier in my config?

Show 9 quoted lines
> +               for (size_t i = 0; i < alias_list.nr; i++)
> +                       printf("%s%c%s%c", alias_list.items[i].string, '\n',
> +                              (char *)alias_list.items[i].util, '\0');
> +               string_list_clear(&alias_list, 1);
> +               return 0;
> +       }
>         case HELP_ACTION_CONFIG:
>                 opt_mode_usage(argc, "--config", help_format);
>                 setup_pager(the_repository);
Show 15 quoted lines
> diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
> index f5877bd7a1..c32186a977 100644
> --- a/contrib/completion/git-completion.zsh
> +++ b/contrib/completion/git-completion.zsh
> @@ -202,7 +202,7 @@ __git_zsh_cmd_common ()
>  __git_zsh_cmd_alias ()
>  {
>         local -a list
> -       list=(${${(0)"$(git config -z --get-regexp '^alias\.*')"}#alias.})
> +       list=(${(0)"$(git help --aliases-for-completion)"})
>         list=(${(f)"$(printf "%s:alias for '%s'\n" ${(f@)list})"})
>         _describe -t alias-commands 'aliases' list && _ret=0
>  }
> --
> 2.53.0.83.g660bbd62ee.dirty

The Zsh hunk looks straightforwardly correct, especially since the output format of the new help-mode is unchanged, thanks.

-- 
D. Ben Knoble
Junio C Hamano· Feb 17, 2026, 20:01 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v5 4/4] completion: fix zsh alias listing for subsection aliases

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 11 quoted lines
> The zsh completion function __git_zsh_cmd_alias() uses 'git config
> --get-regexp' to enumerate aliases and then strips the "alias." prefix
> from each key. For subsection-style aliases (alias.name.command), this
> leaves "name.command" as the completion candidate instead of just
> "name".
>
> The bash completion does not have this problem because it goes through
> 'git --list-cmds=alias', which calls list_aliases() in C and already
> handles both alias syntaxes correctly. However, zsh needs both the
> alias name and its value for descriptive completion, which
> --list-cmds=alias does not provide.

OK. It is a natural question to ask why it is insufficient to use what bash side happily uses, and the above gives a good explanation.

Show 5 quoted lines
> Add a hidden --aliases-for-completion option to 'git help', following
> the existing --config-for-completion pattern. It outputs NUL-separated
> "name\nvalue" pairs using list_aliases(), which correctly resolves both
> the traditional (alias.name) and subsection (alias.name.command)
> formats. Update __git_zsh_cmd_alias() to use it.

Seeing that "--config-for-completion" is used for bash, I wonder if it would be a good follow-up topic to rewrite the bash completion to also use "--aliases-for-completion" (and possibly drop the need to support "git --list-cmds=alias" command). But that is clearly outside the scope of this topic.

> ---
Missing sign-off.
Show 57 quoted lines
>  builtin/help.c                        | 15 +++++++++++++++
>  contrib/completion/git-completion.zsh |  2 +-
>  2 files changed, 16 insertions(+), 1 deletion(-)
>
> diff --git a/builtin/help.c b/builtin/help.c
> index c09cbc8912..f02308a391 100644
> --- a/builtin/help.c
> +++ b/builtin/help.c
> @@ -54,6 +54,7 @@ static enum help_action {
>  	HELP_ACTION_DEVELOPER_INTERFACES,
>  	HELP_ACTION_CONFIG_FOR_COMPLETION,
>  	HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION,
> +	HELP_ACTION_ALIASES_FOR_COMPLETION,
>  } cmd_mode;
>  
>  static char *html_path;
> @@ -90,6 +91,8 @@ static struct option builtin_help_options[] = {
>  		    HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN),
>  	OPT_CMDMODE_F(0, "config-sections-for-completion", &cmd_mode, "",
>  		    HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN),
> +	OPT_CMDMODE_F(0, "aliases-for-completion", &cmd_mode, "",
> +		    HELP_ACTION_ALIASES_FOR_COMPLETION, PARSE_OPT_HIDDEN),
>  
>  	OPT_END(),
>  };
> @@ -691,6 +694,18 @@ int cmd_help(int argc,
>  			       help_format);
>  		list_config_help(SHOW_CONFIG_SECTIONS);
>  		return 0;
> +	case HELP_ACTION_ALIASES_FOR_COMPLETION: {
> +		struct string_list alias_list = STRING_LIST_INIT_DUP;
> +		opt_mode_usage(argc, "--aliases-for-completion",
> +			       help_format);
> +		list_aliases(&alias_list);
> +		string_list_sort(&alias_list);
> +		for (size_t i = 0; i < alias_list.nr; i++)
> +			printf("%s%c%s%c", alias_list.items[i].string, '\n',
> +			       (char *)alias_list.items[i].util, '\0');
> +		string_list_clear(&alias_list, 1);
> +		return 0;
> +	}
>  	case HELP_ACTION_CONFIG:
>  		opt_mode_usage(argc, "--config", help_format);
>  		setup_pager(the_repository);
> diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
> index f5877bd7a1..c32186a977 100644
> --- a/contrib/completion/git-completion.zsh
> +++ b/contrib/completion/git-completion.zsh
> @@ -202,7 +202,7 @@ __git_zsh_cmd_common ()
>  __git_zsh_cmd_alias ()
>  {
>  	local -a list
> -	list=(${${(0)"$(git config -z --get-regexp '^alias\.*')"}#alias.})
> +	list=(${(0)"$(git help --aliases-for-completion)"})
>  	list=(${(f)"$(printf "%s:alias for '%s'\n" ${(f@)list})"})
>  	_describe -t alias-commands 'aliases' list && _ret=0
>  }
Jonatan Holmgren· Feb 16, 2026, 16:15 UTC · re: Jonatan Holmgren · lore

[PATCH v5 2/4] alias: prepare for subsection aliases

Switch git_unknown_cmd_config() from skip_prefix() to parse_config_key() for alias parsing. This properly handles the three-level config key structure and prepares for the new alias.*.command subsection syntax in the next commit.

This is a compatibility break: the alias configuration parser used to be overly permissive and accepted "alias.<subsection>.<key>" as defining an alias "<subsection>.<key>". With this change, alias.<subsection>.<key> entries are silently ignored (unless <key> is "command", which will be given meaning in the next commit).

This behavior was arguably a bug, since config subsections were never intended to work this way for aliases, and aliases with dots in their names have never been documented or intentionally supported.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 help.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/help.c b/help.c
index 5b1b320d02..691af219bf 100644
--- a/help.c
+++ b/help.c
@@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 				  void *cb)
 {
 	struct help_unknown_cmd_config *cfg = cb;
-	const char *p;
+	const char *subsection, *key;
+	size_t subsection_len;
 
 	if (!strcmp(var, "help.autocorrect")) {
 		int v = parse_autocorrect(value);
@@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	}
 
 	/* Also use aliases for command lookup */
-	if (skip_prefix(var, "alias.", &p))
-		add_cmdname(&cfg->aliases, p, strlen(p));
+	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
+			      &key)) {
+		if (!subsection)
+			add_cmdname(&cfg->aliases, key, strlen(key));
+	}
 
 	return 0;
 }
-- 
2.53.0.83.g660bbd62ee.dirty
Jonatan Holmgren· Feb 16, 2026, 16:15 UTC · re: Jonatan Holmgren · lore

[PATCH v5 1/4] help: use list_aliases() for alias listing

help.c has its own get_alias() config callback that duplicates the parsing logic in alias.c. Consolidate by teaching list_aliases() to also store the alias values (via the string_list util field), then use it in list_all_cmds_help_aliases() instead of the private callback.

This preserves the existing error checking for value-less alias definitions by checking in alias.c rather than help.c.

No functional change intended.
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 alias.c          |  8 +++++++-
 help.c           | 17 ++---------------
 t/t0014-alias.sh | 10 ++++++++++
 3 files changed, 19 insertions(+), 16 deletions(-)
diff --git a/alias.c b/alias.c
index 1a1a141a0a..271acb9bf1 100644
--- a/alias.c
+++ b/alias.c
@@ -29,7 +29,13 @@ static int config_alias_cb(const char *key, const char *value,
 						 key, value);
 		}
 	} else if (data->list) {
-		string_list_append(data->list, p);
+		struct string_list_item *item;
+
+		if (!value)
+			return config_error_nonbool(key);
+
+		item = string_list_append(data->list, p);
+		item->util = xstrdup(value);
 	}
 
 	return 0;
diff --git a/help.c b/help.c
index 08b5c60204..5b1b320d02 100644
--- a/help.c
+++ b/help.c
@@ -20,6 +20,7 @@
 #include "prompt.h"
 #include "fsmonitor-ipc.h"
 #include "repository.h"
+#include "alias.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)
 	putchar('\n');
 }
 
-static int get_alias(const char *var, const char *value,
-		     const struct config_context *ctx UNUSED, void *data)
-{
-	struct string_list *list = data;
-
-	if (skip_prefix(var, "alias.", &var)) {
-		if (!value)
-			return config_error_nonbool(var);
-		string_list_append(list, var)->util = xstrdup(value);
-	}
-
-	return 0;
-}
-
 static void list_all_cmds_help_external_commands(void)
 {
 	struct string_list others = STRING_LIST_INIT_DUP;
@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)
 	struct cmdname_help *aliases;
 	int i;
 
-	repo_config(the_repository, get_alias, &alias_list);
+	list_aliases(&alias_list);
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index 07a53e7366..a13d2be8ca 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -112,4 +112,14 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '
 	done
 '
 
+test_expect_success 'alias without value reports error' '
+	test_when_finished "git config --unset alias.noval" &&
+	cat >>.git/config <<-\EOF &&
+	[alias]
+		noval
+	EOF
+	test_must_fail git noval 2>error &&
+	test_grep "alias.noval" error
+'
+
 test_done
-- 
2.53.0.83.g660bbd62ee.dirty
Jonatan Holmgren· Feb 16, 2026, 16:15 UTC · re: Jonatan Holmgren · lore

[PATCH v5 3/4] alias: support non-alphanumeric names via subsection syntax

Git alias names are limited to ASCII alphanumeric characters and dashes because aliases are implemented as config variable names. This prevents aliases being created in languages using characters outside that range.

Add support for arbitrary alias names by using config subsections:
    [alias "förgrena"]
        command = branch

The subsection name is matched as-is (case-sensitive byte comparison), while the existing definition without a subsection (e.g., "[alias] co = checkout") remains case-insensitive for backward compatibility. This uses existing config infrastructure since subsections already support arbitrary bytes, and avoids introducing Unicode normalization.

Also teach the help subsystem about the new syntax so that "git help -a" properly lists subsection aliases and the autocorrect feature can suggest them. Use utf8_strwidth() instead of strlen() for column alignment so that non-ASCII alias names display correctly.

Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 Documentation/config/alias.adoc | 50 ++++++++++++++++++++++-----
 alias.c                         | 38 ++++++++++++++++----
 help.c                          | 14 ++++++--
 t/t0014-alias.sh                | 61 +++++++++++++++++++++++++++++++++
 4 files changed, 145 insertions(+), 18 deletions(-)
diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
index 80ce17d2de..09a6499249 100644
--- a/Documentation/config/alias.adoc
+++ b/Documentation/config/alias.adoc
@@ -1,12 +1,46 @@
 alias.*::
-	Command aliases for the linkgit:git[1] command wrapper - e.g.
-	after defining `alias.last = cat-file commit HEAD`, the invocation
-	`git last` is equivalent to `git cat-file commit HEAD`. To avoid
-	confusion and troubles with script usage, aliases that
-	hide existing Git commands are ignored except for deprecated
-	commands.  Arguments are split by
-	spaces, the usual shell quoting and escaping are supported.
-	A quote pair or a backslash can be used to quote them.
+alias.*.command::
+	Command aliases for the linkgit:git[1] command wrapper. Aliases
+	can be defined using two syntaxes:
++
+--
+1. Without a subsection, e.g., `[alias] co = checkout`. The alias
+   name ("co" in this example) is 
+   limited to ASCII alphanumeric characters and `-`,
+   and is matched case-insensitively.
+2. With a subsection, e.g., `[alias "co"] command = checkout`. The
+   alias name can contain any characters (except for newlines and NUL bytes), 
+   including UTF-8, and is matched case-sensitively as raw bytes.
+   You define the action of the alias in the `command`.
+--
++
+Examples:
++
+----
+# Without subsection (ASCII alphanumeric and dash only)
+[alias]
+    co = checkout
+    st = status
+
+# With subsection (allows any characters, including UTF-8)
+[alias "hämta"]
+    command = fetch
+[alias "rätta till"]
+    command = commit --amend
+----
++
+With a Git alias defined, e.g.,
+
+    $ git config --global alias.last "cat-file commit HEAD"
+    # Which is equivalent to
+    $ git config --global alias.last.command "cat-file commit HEAD"
+
+`git last` is equivalent to `git cat-file commit HEAD`. To avoid
+confusion and troubles with script usage, aliases that
+hide existing Git commands are ignored except for deprecated
+commands.  Arguments are split by
+spaces, the usual shell quoting and escaping are supported.
+A quote pair or a backslash can be used to quote them.
 +
 Note that the first word of an alias does not necessarily have to be a
 command. It can be a command-line option that will be passed into the
diff --git a/alias.c b/alias.c
index 271acb9bf1..0d636278bc 100644
--- a/alias.c
+++ b/alias.c
@@ -13,28 +13,52 @@ struct config_alias_data {
 	struct string_list *list;
 };
 
-static int config_alias_cb(const char *key, const char *value,
+static int config_alias_cb(const char *var, const char *value,
 			   const struct config_context *ctx UNUSED, void *d)
 {
 	struct config_alias_data *data = d;
-	const char *p;
+	const char *subsection, *key;
+	size_t subsection_len;
 
-	if (!skip_prefix(key, "alias.", &p))
+	if (parse_config_key(var, "alias", &subsection, &subsection_len,
+			     &key) < 0)
+		return 0;
+
+	/*
+	 * Two config syntaxes:
+	 * - alias.name = value   (without subsection, case-insensitive)
+	 * - [alias "name"]
+	 *       command = value  (with subsection, case-sensitive)
+	 */
+	if (subsection && strcmp(key, "command"))
 		return 0;
 
 	if (data->alias) {
-		if (!strcasecmp(p, data->alias)) {
+		int match;
+
+		if (subsection)
+			match = (strlen(data->alias) == subsection_len &&
+				 !strncmp(data->alias, subsection,
+					  subsection_len));
+		else
+			match = !strcasecmp(data->alias, key);
+
+		if (match) {
 			FREE_AND_NULL(data->v);
 			return git_config_string(&data->v,
-						 key, value);
+						 var, value);
 		}
 	} else if (data->list) {
 		struct string_list_item *item;
 
 		if (!value)
-			return config_error_nonbool(key);
+			return config_error_nonbool(var);
 
-		item = string_list_append(data->list, p);
+		if (subsection)
+			item = string_list_append_nodup(data->list,
+				xmemdupz(subsection, subsection_len));
+		else
+			item = string_list_append(data->list, key);
 		item->util = xstrdup(value);
 	}
 
diff --git a/help.c b/help.c
index 691af219bf..95f576c5c8 100644
--- a/help.c
+++ b/help.c
@@ -21,6 +21,7 @@
 #include "fsmonitor-ipc.h"
 #include "repository.h"
 #include "alias.h"
+#include "utf8.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
 
 	for (i = 0; cmds[i].name; i++) {
 		if (cmds[i].category & mask) {
-			size_t len = strlen(cmds[i].name);
+			size_t len = utf8_strwidth(cmds[i].name);
 			printf("   %s   ", cmds[i].name);
 			if (longest > len)
 				mput_char(' ', longest - len);
@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
-		size_t len = strlen(alias_list.items[i].string);
+		size_t len = utf8_strwidth(alias_list.items[i].string);
 		if (longest < len)
 			longest = len;
 	}
@@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	/* Also use aliases for command lookup */
 	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
 			      &key)) {
-		if (!subsection)
+		if (subsection) {
+			/* [alias "name"] command = value */
+			if (!strcmp(key, "command"))
+				add_cmdname(&cfg->aliases, subsection,
+					    subsection_len);
+		} else {
+			/* alias.name = value */
 			add_cmdname(&cfg->aliases, key, strlen(key));
+		}
 	}
 
 	return 0;
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index a13d2be8ca..34bbdb51c5 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -122,4 +122,65 @@ test_expect_success 'alias without value reports error' '
 	test_grep "alias.noval" error
 '
 
+test_expect_success 'subsection syntax works' '
+	test_config alias.testnew.command "!echo ran-subsection" &&
+	git testnew >output &&
+	test_grep "ran-subsection" output
+'
+
+test_expect_success 'subsection syntax only accepts command key' '
+	test_config alias.invalid.notcommand value &&
+	test_must_fail git invalid 2>error &&
+	test_grep -i "not a git command" error
+'
+
+test_expect_success 'subsection syntax requires value for command' '
+	test_when_finished "git config --remove-section alias.noval" &&
+	cat >>.git/config <<-\EOF &&
+	[alias "noval"]
+		command
+	EOF
+	test_must_fail git noval 2>error &&
+	test_grep "alias.noval.command" error
+'
+
+test_expect_success 'simple syntax is case-insensitive' '
+	test_config alias.LegacyCase "!echo ran-legacy" &&
+	git legacycase >output &&
+	test_grep "ran-legacy" output
+'
+
+test_expect_success 'subsection syntax is case-sensitive' '
+	test_config alias.SubCase.command "!echo ran-upper" &&
+	test_config alias.subcase.command "!echo ran-lower" &&
+	git SubCase >upper.out &&
+	git subcase >lower.out &&
+	test_grep "ran-upper" upper.out &&
+	test_grep "ran-lower" lower.out
+'
+
+test_expect_success 'UTF-8 alias with Swedish characters' '
+	test_config alias."förgrena".command "!echo ran-swedish" &&
+	git förgrena >output &&
+	test_grep "ran-swedish" output
+'
+
+test_expect_success 'UTF-8 alias with CJK characters' '
+	test_config alias."分支".command "!echo ran-cjk" &&
+	git 分支 >output &&
+	test_grep "ran-cjk" output
+'
+
+test_expect_success 'alias with spaces in name' '
+	test_config alias."test name".command "!echo ran-spaces" &&
+	git "test name" >output &&
+	test_grep "ran-spaces" output
+'
+
+test_expect_success 'subsection aliases listed in help -a' '
+	test_config alias."förgrena".command "!echo test" &&
+	git help -a >output &&
+	test_grep "förgrena" output
+'
+
 test_done
-- 
2.53.0.83.g660bbd62ee.dirty
Jonatan Holmgren· Feb 18, 2026, 14:52 UTC · re: Jonatan Holmgren · lore

[PATCH v6 0/4] support UTF-8 in alias names

This series enables UTF-8 and special characters in Git alias names by using config subsection syntax: [alias "name"] command = value.

I have since last time:
- Unwrapped opt_mode_usage() call in builtin/help.c for consistency
- Removed string_list_sort() to preserve config order as suggested by Ben
- Added missing Signed-off-by trailer to patch 4 (oops)
- Fixed --aliases-for-completion output format to use NUL
  seperators instead of newlines (as per my own commit message and intention)
Thanks again.
Jonatan Holmgren (4):
  help: use list_aliases() for alias listing
  alias: prepare for subsection aliases
  alias: support non-alphanumeric names via subsection syntax
  completion: fix zsh alias listing for subsection aliases
 Documentation/config/alias.adoc       | 50 ++++++++++++++++---
 alias.c                               | 42 +++++++++++++---
 builtin/help.c                        | 13 +++++
 contrib/completion/git-completion.zsh |  2 +-
 help.c                                | 39 +++++++--------
 t/t0014-alias.sh                      | 71 +++++++++++++++++++++++++++
 6 files changed, 182 insertions(+), 35 deletions(-)
-- 
2.53.0.122.g591c997fb5.dirty
Jonatan Holmgren· Feb 18, 2026, 14:52 UTC · re: Jonatan Holmgren · lore

[PATCH v6 2/4] alias: prepare for subsection aliases

Switch git_unknown_cmd_config() from skip_prefix() to parse_config_key() for alias parsing. This properly handles the three-level config key structure and prepares for the new alias.*.command subsection syntax in the next commit.

This is a compatibility break: the alias configuration parser used to be overly permissive and accepted "alias.<subsection>.<key>" as defining an alias "<subsection>.<key>". With this change, alias.<subsection>.<key> entries are silently ignored (unless <key> is "command", which will be given meaning in the next commit).

This behavior was arguably a bug, since config subsections were never intended to work this way for aliases, and aliases with dots in their names have never been documented or intentionally supported.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 help.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/help.c b/help.c
index 5b1b320d02..691af219bf 100644
--- a/help.c
+++ b/help.c
@@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 				  void *cb)
 {
 	struct help_unknown_cmd_config *cfg = cb;
-	const char *p;
+	const char *subsection, *key;
+	size_t subsection_len;
 
 	if (!strcmp(var, "help.autocorrect")) {
 		int v = parse_autocorrect(value);
@@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	}
 
 	/* Also use aliases for command lookup */
-	if (skip_prefix(var, "alias.", &p))
-		add_cmdname(&cfg->aliases, p, strlen(p));
+	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
+			      &key)) {
+		if (!subsection)
+			add_cmdname(&cfg->aliases, key, strlen(key));
+	}
 
 	return 0;
 }
-- 
2.53.0.122.g591c997fb5.dirty
Kristoffer Haugsbakk· Feb 18, 2026, 16:21 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v6 2/4] alias: prepare for subsection aliases

On Wed, Feb 18, 2026, at 15:52, Jonatan Holmgren wrote:
Show 10 quoted lines
> Switch git_unknown_cmd_config() from skip_prefix() to
> parse_config_key() for alias parsing. This properly handles the
> three-level config key structure and prepares for the new
> alias.*.command subsection syntax in the next commit.
>
> This is a compatibility break: the alias configuration parser used
> to be overly permissive and accepted "alias.<subsection>.<key>" as
> defining an alias "<subsection>.<key>". With this change,
> alias.<subsection>.<key> entries are silently ignored (unless <key>
> is "command", which will be given meaning in the next commit).

Unrelated to this change. I was wondering if it makes sense to use a trailer for commits that break compatibility? However unrealistic it might be that the break ends up mattering in practice.

Authors might use different terms and phrases in their commit messages, like

• Breaking change • Compatibility break • Hysterical raisins

And the commit message might be using these terms to describe how it breaks compatibility or how it *avoids* doing so.

Show 7 quoted lines
>
> This behavior was arguably a bug, since config subsections were never
> intended to work this way for aliases, and aliases with dots in their
> names have never been documented or intentionally supported.
>
> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
>[snip]
Jonatan Holmgren· Feb 18, 2026, 14:52 UTC · re: Jonatan Holmgren · lore

[PATCH v6 1/4] help: use list_aliases() for alias listing

help.c has its own get_alias() config callback that duplicates the parsing logic in alias.c. Consolidate by teaching list_aliases() to also store the alias values (via the string_list util field), then use it in list_all_cmds_help_aliases() instead of the private callback.

This preserves the existing error checking for value-less alias definitions by checking in alias.c rather than help.c.

No functional change intended.
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 alias.c          |  8 +++++++-
 help.c           | 17 ++---------------
 t/t0014-alias.sh | 10 ++++++++++
 3 files changed, 19 insertions(+), 16 deletions(-)
diff --git a/alias.c b/alias.c
index 1a1a141a0a..271acb9bf1 100644
--- a/alias.c
+++ b/alias.c
@@ -29,7 +29,13 @@ static int config_alias_cb(const char *key, const char *value,
 						 key, value);
 		}
 	} else if (data->list) {
-		string_list_append(data->list, p);
+		struct string_list_item *item;
+
+		if (!value)
+			return config_error_nonbool(key);
+
+		item = string_list_append(data->list, p);
+		item->util = xstrdup(value);
 	}
 
 	return 0;
diff --git a/help.c b/help.c
index 08b5c60204..5b1b320d02 100644
--- a/help.c
+++ b/help.c
@@ -20,6 +20,7 @@
 #include "prompt.h"
 #include "fsmonitor-ipc.h"
 #include "repository.h"
+#include "alias.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)
 	putchar('\n');
 }
 
-static int get_alias(const char *var, const char *value,
-		     const struct config_context *ctx UNUSED, void *data)
-{
-	struct string_list *list = data;
-
-	if (skip_prefix(var, "alias.", &var)) {
-		if (!value)
-			return config_error_nonbool(var);
-		string_list_append(list, var)->util = xstrdup(value);
-	}
-
-	return 0;
-}
-
 static void list_all_cmds_help_external_commands(void)
 {
 	struct string_list others = STRING_LIST_INIT_DUP;
@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)
 	struct cmdname_help *aliases;
 	int i;
 
-	repo_config(the_repository, get_alias, &alias_list);
+	list_aliases(&alias_list);
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index 07a53e7366..a13d2be8ca 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -112,4 +112,14 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '
 	done
 '
 
+test_expect_success 'alias without value reports error' '
+	test_when_finished "git config --unset alias.noval" &&
+	cat >>.git/config <<-\EOF &&
+	[alias]
+		noval
+	EOF
+	test_must_fail git noval 2>error &&
+	test_grep "alias.noval" error
+'
+
 test_done
-- 
2.53.0.122.g591c997fb5.dirty
Jonatan Holmgren· Feb 18, 2026, 14:52 UTC · re: Jonatan Holmgren · lore

[PATCH v6 4/4] completion: fix zsh alias listing for subsection aliases

The zsh completion function __git_zsh_cmd_alias() uses 'git config --get-regexp' to enumerate aliases and then strips the "alias." prefix from each key. For subsection-style aliases (alias.name.command), this leaves "name.command" as the completion candidate instead of just "name".

The bash completion does not have this problem because it goes through 'git --list-cmds=alias', which calls list_aliases() in C and already handles both alias syntaxes correctly. However, zsh needs both the alias name and its value for descriptive completion, which --list-cmds=alias does not provide.

Add a hidden --aliases-for-completion option to 'git help', following the existing --config-for-completion pattern. It outputs NUL-separated "name\nvalue" pairs using list_aliases(), which correctly resolves both the traditional (alias.name) and subsection (alias.name.command) formats. Update __git_zsh_cmd_alias() to use it.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 builtin/help.c                        | 13 +++++++++++++
 contrib/completion/git-completion.zsh |  2 +-
 2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/builtin/help.c b/builtin/help.c
index c09cbc8912..2db41d0b55 100644
--- a/builtin/help.c
+++ b/builtin/help.c
@@ -54,6 +54,7 @@ static enum help_action {
 	HELP_ACTION_DEVELOPER_INTERFACES,
 	HELP_ACTION_CONFIG_FOR_COMPLETION,
 	HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION,
+	HELP_ACTION_ALIASES_FOR_COMPLETION,
 } cmd_mode;
 
 static char *html_path;
@@ -90,6 +91,8 @@ static struct option builtin_help_options[] = {
 		    HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN),
 	OPT_CMDMODE_F(0, "config-sections-for-completion", &cmd_mode, "",
 		    HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "aliases-for-completion", &cmd_mode, "",
+		    HELP_ACTION_ALIASES_FOR_COMPLETION, PARSE_OPT_HIDDEN),
 
 	OPT_END(),
 };
@@ -691,6 +694,16 @@ int cmd_help(int argc,
 			       help_format);
 		list_config_help(SHOW_CONFIG_SECTIONS);
 		return 0;
+	case HELP_ACTION_ALIASES_FOR_COMPLETION: {
+		struct string_list alias_list = STRING_LIST_INIT_DUP;
+		opt_mode_usage(argc, "--aliases-for-completion", help_format);
+		list_aliases(&alias_list);
+		for (size_t i = 0; i < alias_list.nr; i++)
+			printf("%s%c%s%c", alias_list.items[i].string, '\0',
+			       (char *)alias_list.items[i].util, '\0');
+		string_list_clear(&alias_list, 1);
+		return 0;
+	}
 	case HELP_ACTION_CONFIG:
 		opt_mode_usage(argc, "--config", help_format);
 		setup_pager(the_repository);
diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
index f5877bd7a1..c32186a977 100644
--- a/contrib/completion/git-completion.zsh
+++ b/contrib/completion/git-completion.zsh
@@ -202,7 +202,7 @@ __git_zsh_cmd_common ()
 __git_zsh_cmd_alias ()
 {
 	local -a list
-	list=(${${(0)"$(git config -z --get-regexp '^alias\.*')"}#alias.})
+	list=(${(0)"$(git help --aliases-for-completion)"})
 	list=(${(f)"$(printf "%s:alias for '%s'\n" ${(f@)list})"})
 	_describe -t alias-commands 'aliases' list && _ret=0
 }
-- 
2.53.0.122.g591c997fb5.dirty
Jonatan Holmgren· Feb 18, 2026, 14:52 UTC · re: Jonatan Holmgren · lore

[PATCH v6 3/4] alias: support non-alphanumeric names via subsection syntax

Git alias names are limited to ASCII alphanumeric characters and dashes because aliases are implemented as config variable names. This prevents aliases being created in languages using characters outside that range.

Add support for arbitrary alias names by using config subsections:
    [alias "förgrena"]
        command = branch

The subsection name is matched as-is (case-sensitive byte comparison), while the existing definition without a subsection (e.g., "[alias] co = checkout") remains case-insensitive for backward compatibility. This uses existing config infrastructure since subsections already support arbitrary bytes, and avoids introducing Unicode normalization.

Also teach the help subsystem about the new syntax so that "git help -a" properly lists subsection aliases and the autocorrect feature can suggest them. Use utf8_strwidth() instead of strlen() for column alignment so that non-ASCII alias names display correctly.

Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 Documentation/config/alias.adoc | 50 ++++++++++++++++++++++-----
 alias.c                         | 38 ++++++++++++++++----
 help.c                          | 14 ++++++--
 t/t0014-alias.sh                | 61 +++++++++++++++++++++++++++++++++
 4 files changed, 145 insertions(+), 18 deletions(-)
diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
index 80ce17d2de..09a6499249 100644
--- a/Documentation/config/alias.adoc
+++ b/Documentation/config/alias.adoc
@@ -1,12 +1,46 @@
 alias.*::
-	Command aliases for the linkgit:git[1] command wrapper - e.g.
-	after defining `alias.last = cat-file commit HEAD`, the invocation
-	`git last` is equivalent to `git cat-file commit HEAD`. To avoid
-	confusion and troubles with script usage, aliases that
-	hide existing Git commands are ignored except for deprecated
-	commands.  Arguments are split by
-	spaces, the usual shell quoting and escaping are supported.
-	A quote pair or a backslash can be used to quote them.
+alias.*.command::
+	Command aliases for the linkgit:git[1] command wrapper. Aliases
+	can be defined using two syntaxes:
++
+--
+1. Without a subsection, e.g., `[alias] co = checkout`. The alias
+   name ("co" in this example) is 
+   limited to ASCII alphanumeric characters and `-`,
+   and is matched case-insensitively.
+2. With a subsection, e.g., `[alias "co"] command = checkout`. The
+   alias name can contain any characters (except for newlines and NUL bytes), 
+   including UTF-8, and is matched case-sensitively as raw bytes.
+   You define the action of the alias in the `command`.
+--
++
+Examples:
++
+----
+# Without subsection (ASCII alphanumeric and dash only)
+[alias]
+    co = checkout
+    st = status
+
+# With subsection (allows any characters, including UTF-8)
+[alias "hämta"]
+    command = fetch
+[alias "rätta till"]
+    command = commit --amend
+----
++
+With a Git alias defined, e.g.,
+
+    $ git config --global alias.last "cat-file commit HEAD"
+    # Which is equivalent to
+    $ git config --global alias.last.command "cat-file commit HEAD"
+
+`git last` is equivalent to `git cat-file commit HEAD`. To avoid
+confusion and troubles with script usage, aliases that
+hide existing Git commands are ignored except for deprecated
+commands.  Arguments are split by
+spaces, the usual shell quoting and escaping are supported.
+A quote pair or a backslash can be used to quote them.
 +
 Note that the first word of an alias does not necessarily have to be a
 command. It can be a command-line option that will be passed into the
diff --git a/alias.c b/alias.c
index 271acb9bf1..0d636278bc 100644
--- a/alias.c
+++ b/alias.c
@@ -13,28 +13,52 @@ struct config_alias_data {
 	struct string_list *list;
 };
 
-static int config_alias_cb(const char *key, const char *value,
+static int config_alias_cb(const char *var, const char *value,
 			   const struct config_context *ctx UNUSED, void *d)
 {
 	struct config_alias_data *data = d;
-	const char *p;
+	const char *subsection, *key;
+	size_t subsection_len;
 
-	if (!skip_prefix(key, "alias.", &p))
+	if (parse_config_key(var, "alias", &subsection, &subsection_len,
+			     &key) < 0)
+		return 0;
+
+	/*
+	 * Two config syntaxes:
+	 * - alias.name = value   (without subsection, case-insensitive)
+	 * - [alias "name"]
+	 *       command = value  (with subsection, case-sensitive)
+	 */
+	if (subsection && strcmp(key, "command"))
 		return 0;
 
 	if (data->alias) {
-		if (!strcasecmp(p, data->alias)) {
+		int match;
+
+		if (subsection)
+			match = (strlen(data->alias) == subsection_len &&
+				 !strncmp(data->alias, subsection,
+					  subsection_len));
+		else
+			match = !strcasecmp(data->alias, key);
+
+		if (match) {
 			FREE_AND_NULL(data->v);
 			return git_config_string(&data->v,
-						 key, value);
+						 var, value);
 		}
 	} else if (data->list) {
 		struct string_list_item *item;
 
 		if (!value)
-			return config_error_nonbool(key);
+			return config_error_nonbool(var);
 
-		item = string_list_append(data->list, p);
+		if (subsection)
+			item = string_list_append_nodup(data->list,
+				xmemdupz(subsection, subsection_len));
+		else
+			item = string_list_append(data->list, key);
 		item->util = xstrdup(value);
 	}
 
diff --git a/help.c b/help.c
index 691af219bf..95f576c5c8 100644
--- a/help.c
+++ b/help.c
@@ -21,6 +21,7 @@
 #include "fsmonitor-ipc.h"
 #include "repository.h"
 #include "alias.h"
+#include "utf8.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
 
 	for (i = 0; cmds[i].name; i++) {
 		if (cmds[i].category & mask) {
-			size_t len = strlen(cmds[i].name);
+			size_t len = utf8_strwidth(cmds[i].name);
 			printf("   %s   ", cmds[i].name);
 			if (longest > len)
 				mput_char(' ', longest - len);
@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
-		size_t len = strlen(alias_list.items[i].string);
+		size_t len = utf8_strwidth(alias_list.items[i].string);
 		if (longest < len)
 			longest = len;
 	}
@@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	/* Also use aliases for command lookup */
 	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
 			      &key)) {
-		if (!subsection)
+		if (subsection) {
+			/* [alias "name"] command = value */
+			if (!strcmp(key, "command"))
+				add_cmdname(&cfg->aliases, subsection,
+					    subsection_len);
+		} else {
+			/* alias.name = value */
 			add_cmdname(&cfg->aliases, key, strlen(key));
+		}
 	}
 
 	return 0;
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index a13d2be8ca..34bbdb51c5 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -122,4 +122,65 @@ test_expect_success 'alias without value reports error' '
 	test_grep "alias.noval" error
 '
 
+test_expect_success 'subsection syntax works' '
+	test_config alias.testnew.command "!echo ran-subsection" &&
+	git testnew >output &&
+	test_grep "ran-subsection" output
+'
+
+test_expect_success 'subsection syntax only accepts command key' '
+	test_config alias.invalid.notcommand value &&
+	test_must_fail git invalid 2>error &&
+	test_grep -i "not a git command" error
+'
+
+test_expect_success 'subsection syntax requires value for command' '
+	test_when_finished "git config --remove-section alias.noval" &&
+	cat >>.git/config <<-\EOF &&
+	[alias "noval"]
+		command
+	EOF
+	test_must_fail git noval 2>error &&
+	test_grep "alias.noval.command" error
+'
+
+test_expect_success 'simple syntax is case-insensitive' '
+	test_config alias.LegacyCase "!echo ran-legacy" &&
+	git legacycase >output &&
+	test_grep "ran-legacy" output
+'
+
+test_expect_success 'subsection syntax is case-sensitive' '
+	test_config alias.SubCase.command "!echo ran-upper" &&
+	test_config alias.subcase.command "!echo ran-lower" &&
+	git SubCase >upper.out &&
+	git subcase >lower.out &&
+	test_grep "ran-upper" upper.out &&
+	test_grep "ran-lower" lower.out
+'
+
+test_expect_success 'UTF-8 alias with Swedish characters' '
+	test_config alias."förgrena".command "!echo ran-swedish" &&
+	git förgrena >output &&
+	test_grep "ran-swedish" output
+'
+
+test_expect_success 'UTF-8 alias with CJK characters' '
+	test_config alias."分支".command "!echo ran-cjk" &&
+	git 分支 >output &&
+	test_grep "ran-cjk" output
+'
+
+test_expect_success 'alias with spaces in name' '
+	test_config alias."test name".command "!echo ran-spaces" &&
+	git "test name" >output &&
+	test_grep "ran-spaces" output
+'
+
+test_expect_success 'subsection aliases listed in help -a' '
+	test_config alias."förgrena".command "!echo test" &&
+	git help -a >output &&
+	test_grep "förgrena" output
+'
+
 test_done
-- 
2.53.0.122.g591c997fb5.dirty
Jonatan Holmgren· Feb 18, 2026, 21:57 UTC · re: Jonatan Holmgren · lore

[PATCH v7 0/4] support UTF-8 in alias names

Hi all!

This series adds support for non-alphanumeric (including UTF-8) characters in alias names by allowing aliases to be defined using git-config's subsection syntax.

Changes since v6: Reverted the --aliases-for-completion output format to use a newline as the field separator between name and value within each NUL-terminated record (name\nvalue\0). In v6 I changed this to use NUL for both separators, but this diverges from the convention established by git config -z, which outputs key\nvalue\0. Matching that convention is the right call here.

Thanks!
Jonatan Holmgren (4):
  help: use list_aliases() for alias listing
  alias: prepare for subsection aliases
  alias: support non-alphanumeric names via subsection syntax
  completion: fix zsh alias listing for subsection aliases
 Documentation/config/alias.adoc       | 50 ++++++++++++++++---
 alias.c                               | 42 +++++++++++++---
 builtin/help.c                        | 13 +++++
 contrib/completion/git-completion.zsh |  2 +-
 help.c                                | 39 +++++++--------
 t/t0014-alias.sh                      | 71 +++++++++++++++++++++++++++
 6 files changed, 182 insertions(+), 35 deletions(-)
-- 
2.53.0.122.g3abf75d576
Jonatan Holmgren· Feb 18, 2026, 21:57 UTC · re: Jonatan Holmgren · lore

[PATCH v7 1/4] help: use list_aliases() for alias listing

help.c has its own get_alias() config callback that duplicates the parsing logic in alias.c. Consolidate by teaching list_aliases() to also store the alias values (via the string_list util field), then use it in list_all_cmds_help_aliases() instead of the private callback.

This preserves the existing error checking for value-less alias definitions by checking in alias.c rather than help.c.

No functional change intended.
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 alias.c          |  8 +++++++-
 help.c           | 17 ++---------------
 t/t0014-alias.sh | 10 ++++++++++
 3 files changed, 19 insertions(+), 16 deletions(-)
diff --git a/alias.c b/alias.c
index 1a1a141a0a..271acb9bf1 100644
--- a/alias.c
+++ b/alias.c
@@ -29,7 +29,13 @@ static int config_alias_cb(const char *key, const char *value,
 						 key, value);
 		}
 	} else if (data->list) {
-		string_list_append(data->list, p);
+		struct string_list_item *item;
+
+		if (!value)
+			return config_error_nonbool(key);
+
+		item = string_list_append(data->list, p);
+		item->util = xstrdup(value);
 	}
 
 	return 0;
diff --git a/help.c b/help.c
index 08b5c60204..5b1b320d02 100644
--- a/help.c
+++ b/help.c
@@ -20,6 +20,7 @@
 #include "prompt.h"
 #include "fsmonitor-ipc.h"
 #include "repository.h"
+#include "alias.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -468,20 +469,6 @@ void list_developer_interfaces_help(void)
 	putchar('\n');
 }
 
-static int get_alias(const char *var, const char *value,
-		     const struct config_context *ctx UNUSED, void *data)
-{
-	struct string_list *list = data;
-
-	if (skip_prefix(var, "alias.", &var)) {
-		if (!value)
-			return config_error_nonbool(var);
-		string_list_append(list, var)->util = xstrdup(value);
-	}
-
-	return 0;
-}
-
 static void list_all_cmds_help_external_commands(void)
 {
 	struct string_list others = STRING_LIST_INIT_DUP;
@@ -501,7 +488,7 @@ static void list_all_cmds_help_aliases(int longest)
 	struct cmdname_help *aliases;
 	int i;
 
-	repo_config(the_repository, get_alias, &alias_list);
+	list_aliases(&alias_list);
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index 07a53e7366..a13d2be8ca 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -112,4 +112,14 @@ test_expect_success 'cannot alias-shadow a sample of regular builtins' '
 	done
 '
 
+test_expect_success 'alias without value reports error' '
+	test_when_finished "git config --unset alias.noval" &&
+	cat >>.git/config <<-\EOF &&
+	[alias]
+		noval
+	EOF
+	test_must_fail git noval 2>error &&
+	test_grep "alias.noval" error
+'
+
 test_done
-- 
2.53.0.122.g3abf75d576
Jacob Keller· Feb 24, 2026, 22:19 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v7 1/4] help: use list_aliases() for alias listing

On 2/18/2026 1:57 PM, Jonatan Holmgren wrote:
Show 11 quoted lines
> help.c has its own get_alias() config callback that duplicates the
> parsing logic in alias.c. Consolidate by teaching list_aliases() to
> also store the alias values (via the string_list util field), then
> use it in list_all_cmds_help_aliases() instead of the private
> callback.
> 
> This preserves the existing error checking for value-less alias
> definitions by checking in alias.c rather than help.c.
> 
> No functional change intended.
> 
This results in a memory leak with git --list-cmds=alias:
==2244105==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 1453 byte(s) in 37 object(s) allocated from:
     #0 0x7f9e268e0ca0 in strdup (/lib64/libasan.so.8+0xe0ca0) (BuildId: 
25975f766867e9e604dc5a71a8befeaed3301942)
     #1 0x00000115997b in xstrdup ../wrapper.c:43
     #2 0x000000841299 in config_alias_cb ../alias.c:62
     #3 0x00000098df73 in git_config_include ../config.c:429
     #4 0x000000973616 in get_value ../config.c:919
     #5 0x000000973616 in git_parse_source ../config.c:1114
     #6 0x000000973616 in do_config_from ../config.c:1341
     #7 0x000000975f5a in do_config_from_file ../config.c:1370
     #8 0x000000980c9b in git_config_from_file_with_options ../config.c:1393
     #9 0x0000009827fe in do_git_config_sequence ../config.c:1556
     #10 0x0000009827fe in config_with_options ../config.c:1615
     #11 0x00000098313d in read_early_config ../config.c:1670
     #12 0x000000841935 in list_aliases ../alias.c:81
     #13 0x00000080b12f in list_cmds ../git.c:102
     #14 0x00000080b12f in handle_options ../git.c:336
     #15 0x00000080d570 in cmd_main ../git.c:955
     #16 0x00000044a54f in main ../common-main.c:9
     #17 0x7f9e25e105b4 in __libc_start_call_main 
(/lib64/libc.so.6+0x35b4) (BuildId: 
2b5beec0fd24fe9c9f43eddfdd5facf0b8a1b805)
     #18 0x7f9e25e10667 in __libc_start_main@@GLIBC_2.34 
(/lib64/libc.so.6+0x3667) (BuildId: 
2b5beec0fd24fe9c9f43eddfdd5facf0b8a1b805)
     #19 0x00000044c944 in _start 
(/home/jekeller/libexec/git-core/git+0x44c944) (BuildId: 
674cf04ebc1da782eede3c3be79a0c15f372df4c)
SUMMARY: AddressSanitizer: 1453 byte(s) leaked in 37 allocation(s).

This leak occurs because you now copy and store the value of the alias in the util element, but the call of list_aliases() in list_cmd() doesn't clean these up, since its string_list_clear passes 0 to the free_util argument.

The following fixed it for me:
diff --git c/git.c i/git.c
index 744cb6527e06..aeb099ab1162 100644
--- c/git.c
+++ i/git.c
@@ -119,7 +119,7 @@ static int list_cmds(const char *spec)
         }
         for (size_t i = 0; i < list.nr; i++)
                 puts(list.items[i].string);
-       string_list_clear(&list, 0);
+       string_list_clear(&list, 1);
         return 0;
  }
Junio C Hamano· Feb 24, 2026, 22:41 UTC · re: Jacob Keller · lore

Re: [PATCH v7 1/4] help: use list_aliases() for alias listing

Jacob Keller <jacob.e.keller@intel.com> writes:
Show 21 quoted lines
> SUMMARY: AddressSanitizer: 1453 byte(s) leaked in 37 allocation(s).
>
> This leak occurs because you now copy and store the value of the alias 
> in the util element, but the call of list_aliases() in list_cmd() 
> doesn't clean these up, since its string_list_clear passes 0 to the 
> free_util argument.
>
> The following fixed it for me:
>
> diff --git c/git.c i/git.c
> index 744cb6527e06..aeb099ab1162 100644
> --- c/git.c
> +++ i/git.c
> @@ -119,7 +119,7 @@ static int list_cmds(const char *spec)
>          }
>          for (size_t i = 0; i < list.nr; i++)
>                  puts(list.items[i].string);
> -       string_list_clear(&list, 0);
> +       string_list_clear(&list, 1);
>          return 0;
>   }

Thanks. This looks like one of the right things to do. I checked all list_*() that are called from the loop in this list_cmds(), and list_aliases() is the only thing that uses .util member of the string_list_item instances.

However, we need to be a bit careful with list_cmds_by_config(). It sorts the list accumulated so far, uses remove_duplicates() on it without passing free_util=1, so there is also the same kind of leak there, I suspect, until we adjust the call there.

Junio C Hamano· Feb 25, 2026, 20:45 UTC · re: Junio C Hamano · lore

Re: [PATCH v7 1/4] help: use list_aliases() for alias listing

Junio C Hamano <gitster@pobox.com> writes:
Show 33 quoted lines
> Jacob Keller <jacob.e.keller@intel.com> writes:
>
>> SUMMARY: AddressSanitizer: 1453 byte(s) leaked in 37 allocation(s).
>>
>> This leak occurs because you now copy and store the value of the alias 
>> in the util element, but the call of list_aliases() in list_cmd() 
>> doesn't clean these up, since its string_list_clear passes 0 to the 
>> free_util argument.
>>
>> The following fixed it for me:
>>
>> diff --git c/git.c i/git.c
>> index 744cb6527e06..aeb099ab1162 100644
>> --- c/git.c
>> +++ i/git.c
>> @@ -119,7 +119,7 @@ static int list_cmds(const char *spec)
>>          }
>>          for (size_t i = 0; i < list.nr; i++)
>>                  puts(list.items[i].string);
>> -       string_list_clear(&list, 0);
>> +       string_list_clear(&list, 1);
>>          return 0;
>>   }
>
> Thanks. This looks like one of the right things to do.  I checked
> all list_*() that are called from the loop in this list_cmds(), and
> list_aliases() is the only thing that uses .util member of the
> string_list_item instances.
>
> However, we need to be a bit careful with list_cmds_by_config().  It
> sorts the list accumulated so far, uses remove_duplicates() on it
> without passing free_util=1, so there is also the same kind of leak
> there, I suspect, until we adjust the call there.

FWIW, here is what I tentatively queued on top of these four patches. Hopefully we can have a small and final reroll for these "Fix small issues in alias" patches and merge them down to 'next' soonish?

Thanks.
diff --git a/git.c b/git.c
index c5fad56813..b5eb740e83 100644
--- a/git.c
+++ b/git.c
@@ -119,7 +119,7 @@ static int list_cmds(const char *spec)
 	}
 	for (size_t i = 0; i < list.nr; i++)
 		puts(list.items[i].string);
-	string_list_clear(&list, 0);
+	string_list_clear(&list, 1);
 	return 0;
 }
 
diff --git a/help.c b/help.c
index 82fb2eaa3f..725e92a195 100644
--- a/help.c
+++ b/help.c
@@ -423,7 +423,7 @@ void list_cmds_by_config(struct string_list *list)
 		return;
 
 	string_list_sort(list);
-	string_list_remove_duplicates(list, 0);
+	string_list_remove_duplicates(list, 1);
 
 	while (*cmd_list) {
 		struct strbuf sb = STRBUF_INIT;
-- 
2.53.0-514-g5fc6f9e594
Jacob Keller· Feb 26, 2026, 23:33 UTC · re: Junio C Hamano · lore

Re: [PATCH v7 1/4] help: use list_aliases() for alias listing

On 2/25/2026 12:45 PM, Junio C Hamano wrote:
Show 70 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
> 
>> Jacob Keller <jacob.e.keller@intel.com> writes:
>>
>>> SUMMARY: AddressSanitizer: 1453 byte(s) leaked in 37 allocation(s).
>>>
>>> This leak occurs because you now copy and store the value of the alias
>>> in the util element, but the call of list_aliases() in list_cmd()
>>> doesn't clean these up, since its string_list_clear passes 0 to the
>>> free_util argument.
>>>
>>> The following fixed it for me:
>>>
>>> diff --git c/git.c i/git.c
>>> index 744cb6527e06..aeb099ab1162 100644
>>> --- c/git.c
>>> +++ i/git.c
>>> @@ -119,7 +119,7 @@ static int list_cmds(const char *spec)
>>>           }
>>>           for (size_t i = 0; i < list.nr; i++)
>>>                   puts(list.items[i].string);
>>> -       string_list_clear(&list, 0);
>>> +       string_list_clear(&list, 1);
>>>           return 0;
>>>    }
>>
>> Thanks. This looks like one of the right things to do.  I checked
>> all list_*() that are called from the loop in this list_cmds(), and
>> list_aliases() is the only thing that uses .util member of the
>> string_list_item instances.
>>
>> However, we need to be a bit careful with list_cmds_by_config().  It
>> sorts the list accumulated so far, uses remove_duplicates() on it
>> without passing free_util=1, so there is also the same kind of leak
>> there, I suspect, until we adjust the call there.
> 
> FWIW, here is what I tentatively queued on top of these four
> patches.  Hopefully we can have a small and final reroll for these
> "Fix small issues in alias" patches and merge them down to 'next'
> soonish?
> 
> Thanks.
> 
> 
> diff --git a/git.c b/git.c
> index c5fad56813..b5eb740e83 100644
> --- a/git.c
> +++ b/git.c
> @@ -119,7 +119,7 @@ static int list_cmds(const char *spec)
>   	}
>   	for (size_t i = 0; i < list.nr; i++)
>   		puts(list.items[i].string);
> -	string_list_clear(&list, 0);
> +	string_list_clear(&list, 1);
>   	return 0;
>   }
>   
> diff --git a/help.c b/help.c
> index 82fb2eaa3f..725e92a195 100644
> --- a/help.c
> +++ b/help.c
> @@ -423,7 +423,7 @@ void list_cmds_by_config(struct string_list *list)
>   		return;
>   
>   	string_list_sort(list);
> -	string_list_remove_duplicates(list, 0);
> +	string_list_remove_duplicates(list, 1);
>   
>   	while (*cmd_list) {
>   		struct strbuf sb = STRBUF_INIT;
This looks correct to me, and I didn't see anything missing. Thanks!
Jacob Keller· Feb 24, 2026, 22:21 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v7 1/4] help: use list_aliases() for alias listing

On 2/18/2026 1:57 PM, Jonatan Holmgren wrote:
Show 11 quoted lines
> help.c has its own get_alias() config callback that duplicates the
> parsing logic in alias.c. Consolidate by teaching list_aliases() to
> also store the alias values (via the string_list util field), then
> use it in list_all_cmds_help_aliases() instead of the private
> callback.
> 
> This preserves the existing error checking for value-less alias
> definitions by checking in alias.c rather than help.c.
> 
> No functional change intended.
> 
This results in a memory leak with git --list-cmds=alias:
==2244105==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 1453 byte(s) in 37 object(s) allocated from:
     #0 0x7f9e268e0ca0 in strdup (/lib64/libasan.so.8+0xe0ca0) (BuildId: 
25975f766867e9e604dc5a71a8befeaed3301942)
     #1 0x00000115997b in xstrdup ../wrapper.c:43
     #2 0x000000841299 in config_alias_cb ../alias.c:62
     #3 0x00000098df73 in git_config_include ../config.c:429
     #4 0x000000973616 in get_value ../config.c:919
     #5 0x000000973616 in git_parse_source ../config.c:1114
     #6 0x000000973616 in do_config_from ../config.c:1341
     #7 0x000000975f5a in do_config_from_file ../config.c:1370
     #8 0x000000980c9b in git_config_from_file_with_options ../config.c:1393
     #9 0x0000009827fe in do_git_config_sequence ../config.c:1556
     #10 0x0000009827fe in config_with_options ../config.c:1615
     #11 0x00000098313d in read_early_config ../config.c:1670
     #12 0x000000841935 in list_aliases ../alias.c:81
     #13 0x00000080b12f in list_cmds ../git.c:102
     #14 0x00000080b12f in handle_options ../git.c:336
     #15 0x00000080d570 in cmd_main ../git.c:955
     #16 0x00000044a54f in main ../common-main.c:9
     #17 0x7f9e25e105b4 in __libc_start_call_main 
(/lib64/libc.so.6+0x35b4) (BuildId: 
2b5beec0fd24fe9c9f43eddfdd5facf0b8a1b805)
     #18 0x7f9e25e10667 in __libc_start_main@@GLIBC_2.34 
(/lib64/libc.so.6+0x3667) (BuildId: 
2b5beec0fd24fe9c9f43eddfdd5facf0b8a1b805)
     #19 0x00000044c944 in _start 
(/home/jekeller/libexec/git-core/git+0x44c944) (BuildId: 
674cf04ebc1da782eede3c3be79a0c15f372df4c)
SUMMARY: AddressSanitizer: 1453 byte(s) leaked in 37 allocation(s).

This leak occurs because you now copy and store the value of the alias in the util element, but the call of list_aliases() in list_cmd() doesn't clean these up, since its string_list_clear passes 0 to the free_util argument.

The following fixed it for me:
diff --git c/git.c i/git.c
index 744cb6527e06..aeb099ab1162 100644
--- c/git.c
+++ i/git.c
@@ -119,7 +119,7 @@ static int list_cmds(const char *spec)
         }
         for (size_t i = 0; i < list.nr; i++)
                 puts(list.items[i].string);
-       string_list_clear(&list, 0);
+       string_list_clear(&list, 1);
         return 0;
  }
Jonatan Holmgren· Feb 18, 2026, 21:57 UTC · re: Jonatan Holmgren · lore

[PATCH v7 2/4] alias: prepare for subsection aliases

Switch git_unknown_cmd_config() from skip_prefix() to parse_config_key() for alias parsing. This properly handles the three-level config key structure and prepares for the new alias.*.command subsection syntax in the next commit.

This is a compatibility break: the alias configuration parser used to be overly permissive and accepted "alias.<subsection>.<key>" as defining an alias "<subsection>.<key>". With this change, alias.<subsection>.<key> entries are silently ignored (unless <key> is "command", which will be given meaning in the next commit).

This behavior was arguably a bug, since config subsections were never intended to work this way for aliases, and aliases with dots in their names have never been documented or intentionally supported.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 help.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/help.c b/help.c
index 5b1b320d02..691af219bf 100644
--- a/help.c
+++ b/help.c
@@ -573,7 +573,8 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 				  void *cb)
 {
 	struct help_unknown_cmd_config *cfg = cb;
-	const char *p;
+	const char *subsection, *key;
+	size_t subsection_len;
 
 	if (!strcmp(var, "help.autocorrect")) {
 		int v = parse_autocorrect(value);
@@ -588,8 +589,11 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	}
 
 	/* Also use aliases for command lookup */
-	if (skip_prefix(var, "alias.", &p))
-		add_cmdname(&cfg->aliases, p, strlen(p));
+	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
+			      &key)) {
+		if (!subsection)
+			add_cmdname(&cfg->aliases, key, strlen(key));
+	}
 
 	return 0;
 }
-- 
2.53.0.122.g3abf75d576
Jonatan Holmgren· Feb 18, 2026, 21:57 UTC · re: Jonatan Holmgren · lore

[PATCH v7 3/4] alias: support non-alphanumeric names via subsection syntax

Git alias names are limited to ASCII alphanumeric characters and dashes because aliases are implemented as config variable names. This prevents aliases being created in languages using characters outside that range.

Add support for arbitrary alias names by using config subsections:
    [alias "förgrena"]
        command = branch

The subsection name is matched as-is (case-sensitive byte comparison), while the existing definition without a subsection (e.g., "[alias] co = checkout") remains case-insensitive for backward compatibility. This uses existing config infrastructure since subsections already support arbitrary bytes, and avoids introducing Unicode normalization.

Also teach the help subsystem about the new syntax so that "git help -a" properly lists subsection aliases and the autocorrect feature can suggest them. Use utf8_strwidth() instead of strlen() for column alignment so that non-ASCII alias names display correctly.

Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 Documentation/config/alias.adoc | 50 ++++++++++++++++++++++-----
 alias.c                         | 38 ++++++++++++++++----
 help.c                          | 14 ++++++--
 t/t0014-alias.sh                | 61 +++++++++++++++++++++++++++++++++
 4 files changed, 145 insertions(+), 18 deletions(-)
diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
index 80ce17d2de..09a6499249 100644
--- a/Documentation/config/alias.adoc
+++ b/Documentation/config/alias.adoc
@@ -1,12 +1,46 @@
 alias.*::
-	Command aliases for the linkgit:git[1] command wrapper - e.g.
-	after defining `alias.last = cat-file commit HEAD`, the invocation
-	`git last` is equivalent to `git cat-file commit HEAD`. To avoid
-	confusion and troubles with script usage, aliases that
-	hide existing Git commands are ignored except for deprecated
-	commands.  Arguments are split by
-	spaces, the usual shell quoting and escaping are supported.
-	A quote pair or a backslash can be used to quote them.
+alias.*.command::
+	Command aliases for the linkgit:git[1] command wrapper. Aliases
+	can be defined using two syntaxes:
++
+--
+1. Without a subsection, e.g., `[alias] co = checkout`. The alias
+   name ("co" in this example) is 
+   limited to ASCII alphanumeric characters and `-`,
+   and is matched case-insensitively.
+2. With a subsection, e.g., `[alias "co"] command = checkout`. The
+   alias name can contain any characters (except for newlines and NUL bytes), 
+   including UTF-8, and is matched case-sensitively as raw bytes.
+   You define the action of the alias in the `command`.
+--
++
+Examples:
++
+----
+# Without subsection (ASCII alphanumeric and dash only)
+[alias]
+    co = checkout
+    st = status
+
+# With subsection (allows any characters, including UTF-8)
+[alias "hämta"]
+    command = fetch
+[alias "rätta till"]
+    command = commit --amend
+----
++
+With a Git alias defined, e.g.,
+
+    $ git config --global alias.last "cat-file commit HEAD"
+    # Which is equivalent to
+    $ git config --global alias.last.command "cat-file commit HEAD"
+
+`git last` is equivalent to `git cat-file commit HEAD`. To avoid
+confusion and troubles with script usage, aliases that
+hide existing Git commands are ignored except for deprecated
+commands.  Arguments are split by
+spaces, the usual shell quoting and escaping are supported.
+A quote pair or a backslash can be used to quote them.
 +
 Note that the first word of an alias does not necessarily have to be a
 command. It can be a command-line option that will be passed into the
diff --git a/alias.c b/alias.c
index 271acb9bf1..0d636278bc 100644
--- a/alias.c
+++ b/alias.c
@@ -13,28 +13,52 @@ struct config_alias_data {
 	struct string_list *list;
 };
 
-static int config_alias_cb(const char *key, const char *value,
+static int config_alias_cb(const char *var, const char *value,
 			   const struct config_context *ctx UNUSED, void *d)
 {
 	struct config_alias_data *data = d;
-	const char *p;
+	const char *subsection, *key;
+	size_t subsection_len;
 
-	if (!skip_prefix(key, "alias.", &p))
+	if (parse_config_key(var, "alias", &subsection, &subsection_len,
+			     &key) < 0)
+		return 0;
+
+	/*
+	 * Two config syntaxes:
+	 * - alias.name = value   (without subsection, case-insensitive)
+	 * - [alias "name"]
+	 *       command = value  (with subsection, case-sensitive)
+	 */
+	if (subsection && strcmp(key, "command"))
 		return 0;
 
 	if (data->alias) {
-		if (!strcasecmp(p, data->alias)) {
+		int match;
+
+		if (subsection)
+			match = (strlen(data->alias) == subsection_len &&
+				 !strncmp(data->alias, subsection,
+					  subsection_len));
+		else
+			match = !strcasecmp(data->alias, key);
+
+		if (match) {
 			FREE_AND_NULL(data->v);
 			return git_config_string(&data->v,
-						 key, value);
+						 var, value);
 		}
 	} else if (data->list) {
 		struct string_list_item *item;
 
 		if (!value)
-			return config_error_nonbool(key);
+			return config_error_nonbool(var);
 
-		item = string_list_append(data->list, p);
+		if (subsection)
+			item = string_list_append_nodup(data->list,
+				xmemdupz(subsection, subsection_len));
+		else
+			item = string_list_append(data->list, key);
 		item->util = xstrdup(value);
 	}
 
diff --git a/help.c b/help.c
index 691af219bf..95f576c5c8 100644
--- a/help.c
+++ b/help.c
@@ -21,6 +21,7 @@
 #include "fsmonitor-ipc.h"
 #include "repository.h"
 #include "alias.h"
+#include "utf8.h"
 
 #ifndef NO_CURL
 #include "git-curl-compat.h" /* For LIBCURL_VERSION only */
@@ -108,7 +109,7 @@ static void print_command_list(const struct cmdname_help *cmds,
 
 	for (i = 0; cmds[i].name; i++) {
 		if (cmds[i].category & mask) {
-			size_t len = strlen(cmds[i].name);
+			size_t len = utf8_strwidth(cmds[i].name);
 			printf("   %s   ", cmds[i].name);
 			if (longest > len)
 				mput_char(' ', longest - len);
@@ -492,7 +493,7 @@ static void list_all_cmds_help_aliases(int longest)
 	string_list_sort(&alias_list);
 
 	for (i = 0; i < alias_list.nr; i++) {
-		size_t len = strlen(alias_list.items[i].string);
+		size_t len = utf8_strwidth(alias_list.items[i].string);
 		if (longest < len)
 			longest = len;
 	}
@@ -591,8 +592,15 @@ static int git_unknown_cmd_config(const char *var, const char *value,
 	/* Also use aliases for command lookup */
 	if (!parse_config_key(var, "alias", &subsection, &subsection_len,
 			      &key)) {
-		if (!subsection)
+		if (subsection) {
+			/* [alias "name"] command = value */
+			if (!strcmp(key, "command"))
+				add_cmdname(&cfg->aliases, subsection,
+					    subsection_len);
+		} else {
+			/* alias.name = value */
 			add_cmdname(&cfg->aliases, key, strlen(key));
+		}
 	}
 
 	return 0;
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index a13d2be8ca..34bbdb51c5 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -122,4 +122,65 @@ test_expect_success 'alias without value reports error' '
 	test_grep "alias.noval" error
 '
 
+test_expect_success 'subsection syntax works' '
+	test_config alias.testnew.command "!echo ran-subsection" &&
+	git testnew >output &&
+	test_grep "ran-subsection" output
+'
+
+test_expect_success 'subsection syntax only accepts command key' '
+	test_config alias.invalid.notcommand value &&
+	test_must_fail git invalid 2>error &&
+	test_grep -i "not a git command" error
+'
+
+test_expect_success 'subsection syntax requires value for command' '
+	test_when_finished "git config --remove-section alias.noval" &&
+	cat >>.git/config <<-\EOF &&
+	[alias "noval"]
+		command
+	EOF
+	test_must_fail git noval 2>error &&
+	test_grep "alias.noval.command" error
+'
+
+test_expect_success 'simple syntax is case-insensitive' '
+	test_config alias.LegacyCase "!echo ran-legacy" &&
+	git legacycase >output &&
+	test_grep "ran-legacy" output
+'
+
+test_expect_success 'subsection syntax is case-sensitive' '
+	test_config alias.SubCase.command "!echo ran-upper" &&
+	test_config alias.subcase.command "!echo ran-lower" &&
+	git SubCase >upper.out &&
+	git subcase >lower.out &&
+	test_grep "ran-upper" upper.out &&
+	test_grep "ran-lower" lower.out
+'
+
+test_expect_success 'UTF-8 alias with Swedish characters' '
+	test_config alias."förgrena".command "!echo ran-swedish" &&
+	git förgrena >output &&
+	test_grep "ran-swedish" output
+'
+
+test_expect_success 'UTF-8 alias with CJK characters' '
+	test_config alias."分支".command "!echo ran-cjk" &&
+	git 分支 >output &&
+	test_grep "ran-cjk" output
+'
+
+test_expect_success 'alias with spaces in name' '
+	test_config alias."test name".command "!echo ran-spaces" &&
+	git "test name" >output &&
+	test_grep "ran-spaces" output
+'
+
+test_expect_success 'subsection aliases listed in help -a' '
+	test_config alias."förgrena".command "!echo test" &&
+	git help -a >output &&
+	test_grep "förgrena" output
+'
+
 test_done
-- 
2.53.0.122.g3abf75d576
Kristoffer Haugsbakk· Feb 24, 2026, 10:55 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v7 3/4] alias: support non-alphanumeric names via subsection syntax

On Wed, Feb 18, 2026, at 22:57, Jonatan Holmgren wrote:
Show 41 quoted lines
> Git alias names are limited to ASCII alphanumeric characters and
> dashes because aliases are implemented as config variable names.
> This prevents aliases being created in languages using characters
> outside that range.
>
> Add support for arbitrary alias names by using config subsections:
>
>     [alias "förgrena"]
>         command = branch
>
> The subsection name is matched as-is (case-sensitive byte comparison),
> while the existing definition without a subsection (e.g.,
> "[alias] co = checkout") remains case-insensitive for backward
> compatibility. This uses existing config infrastructure since
> subsections already support arbitrary bytes, and avoids introducing
> Unicode normalization.
>
> Also teach the help subsystem about the new syntax so that "git help
> -a" properly lists subsection aliases and the autocorrect feature can
> suggest them. Use utf8_strwidth() instead of strlen() for column
> alignment so that non-ASCII alias names display correctly.
>
> Suggested-by: Jeff King <peff@peff.net>
> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
> ---
>  Documentation/config/alias.adoc | 50 ++++++++++++++++++++++-----
>  alias.c                         | 38 ++++++++++++++++----
>  help.c                          | 14 ++++++--
>  t/t0014-alias.sh                | 61 +++++++++++++++++++++++++++++++++
>  4 files changed, 145 insertions(+), 18 deletions(-)
>
> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
> index 80ce17d2de..09a6499249 100644
> --- a/Documentation/config/alias.adoc
> +++ b/Documentation/config/alias.adoc
>[snip]
> +# With subsection (allows any characters, including UTF-8)
> +[alias "hämta"]
> +    command = fetch
> +[alias "rätta till"]
> +    command = commit --amend
This is in `next` now so this is a question or note for later.

Is this `rätta till` supposed to have a space in it? Or is there supposed to be a hyphen? I couldn’t get it to work.

Is the intent to use quotes for the command (in e.g. Bash)?
    ./bin-wrappers/git "rätta till"
Show 9 quoted lines
> +----
> ++
> +With a Git alias defined, e.g.,
> +
> +    $ git config --global alias.last "cat-file commit HEAD"
> +    # Which is equivalent to
> +    $ git config --global alias.last.command "cat-file commit HEAD"
> +
> +`git last` is equivalent to `git cat-file commit HEAD`. To avoid

This is also a note for later. I think this “To avoid” should now be moved to a separate paragraph since it’s a different topic.

Sorry that all my feedback is this late! These are just notes for the list in any case.

Show 6 quoted lines
> +confusion and troubles with script usage, aliases that
> +hide existing Git commands are ignored except for deprecated
> +commands.  Arguments are split by
> +spaces, the usual shell quoting and escaping are supported.
> +A quote pair or a backslash can be used to quote them.
>[snip]
Jonatan Holmgren· Feb 24, 2026, 14:48 UTC · re: Kristoffer Haugsbakk · lore

Re: [PATCH v7 3/4] alias: support non-alphanumeric names via subsection syntax

> Is this `rätta till` supposed to have a space in it? 
Absolutely, as a demo of space working fine
> Is the intent to use quotes for the command (in e.g. Bash)?

Yup, or "\ " which is what my shell defaults to. So the user would type "git rä<TAB>". Does that not work on your machine?

Jonatan
Kristoffer Haugsbakk· Feb 24, 2026, 23:23 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v7 3/4] alias: support non-alphanumeric names via subsection syntax

On Tue, Feb 24, 2026, at 15:48, Jonatan Holmgren wrote:
>> Is this `rätta till` supposed to have a space in it?
> Absolutely, as a demo of space working fine
Aha, okay. Thanks.
Jonatan Holmgren· Feb 18, 2026, 21:57 UTC · re: Jonatan Holmgren · lore

[PATCH v7 4/4] completion: fix zsh alias listing for subsection aliases

The zsh completion function __git_zsh_cmd_alias() uses 'git config --get-regexp' to enumerate aliases and then strips the "alias." prefix from each key. For subsection-style aliases (alias.name.command), this leaves "name.command" as the completion candidate instead of just "name".

The bash completion does not have this problem because it goes through 'git --list-cmds=alias', which calls list_aliases() in C and already handles both alias syntaxes correctly. However, zsh needs both the alias name and its value for descriptive completion, which --list-cmds=alias does not provide.

Add a hidden --aliases-for-completion option to 'git help', following the existing --config-for-completion pattern. It outputs NUL-separated "name\nvalue" pairs using list_aliases(), which correctly resolves both the traditional (alias.name) and subsection (alias.name.command) formats. Update __git_zsh_cmd_alias() to use it.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 builtin/help.c                        | 13 +++++++++++++
 contrib/completion/git-completion.zsh |  2 +-
 2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/builtin/help.c b/builtin/help.c
index c09cbc8912..86a3d03a9b 100644
--- a/builtin/help.c
+++ b/builtin/help.c
@@ -54,6 +54,7 @@ static enum help_action {
 	HELP_ACTION_DEVELOPER_INTERFACES,
 	HELP_ACTION_CONFIG_FOR_COMPLETION,
 	HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION,
+	HELP_ACTION_ALIASES_FOR_COMPLETION,
 } cmd_mode;
 
 static char *html_path;
@@ -90,6 +91,8 @@ static struct option builtin_help_options[] = {
 		    HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN),
 	OPT_CMDMODE_F(0, "config-sections-for-completion", &cmd_mode, "",
 		    HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN),
+	OPT_CMDMODE_F(0, "aliases-for-completion", &cmd_mode, "",
+		    HELP_ACTION_ALIASES_FOR_COMPLETION, PARSE_OPT_HIDDEN),
 
 	OPT_END(),
 };
@@ -691,6 +694,16 @@ int cmd_help(int argc,
 			       help_format);
 		list_config_help(SHOW_CONFIG_SECTIONS);
 		return 0;
+	case HELP_ACTION_ALIASES_FOR_COMPLETION: {
+		struct string_list alias_list = STRING_LIST_INIT_DUP;
+		opt_mode_usage(argc, "--aliases-for-completion", help_format);
+		list_aliases(&alias_list);
+		for (size_t i = 0; i < alias_list.nr; i++)
+			printf("%s%c%s%c", alias_list.items[i].string, '\n',
+			       (char *)alias_list.items[i].util, '\0');
+		string_list_clear(&alias_list, 1);
+		return 0;
+	}
 	case HELP_ACTION_CONFIG:
 		opt_mode_usage(argc, "--config", help_format);
 		setup_pager(the_repository);
diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
index f5877bd7a1..c32186a977 100644
--- a/contrib/completion/git-completion.zsh
+++ b/contrib/completion/git-completion.zsh
@@ -202,7 +202,7 @@ __git_zsh_cmd_common ()
 __git_zsh_cmd_alias ()
 {
 	local -a list
-	list=(${${(0)"$(git config -z --get-regexp '^alias\.*')"}#alias.})
+	list=(${(0)"$(git help --aliases-for-completion)"})
 	list=(${(f)"$(printf "%s:alias for '%s'\n" ${(f@)list})"})
 	_describe -t alias-commands 'aliases' list && _ret=0
 }
-- 
2.53.0.122.g3abf75d576
Junio C Hamano· Feb 19, 2026, 18:17 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v7 0/4] support UTF-8 in alias names

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 13 quoted lines
> Hi all!
>
> This series adds support for non-alphanumeric (including UTF-8)
> characters in alias names by allowing aliases to be defined using
> git-config's subsection syntax.
>
> Changes since v6:
> Reverted the --aliases-for-completion output format to
> use a newline as the field separator between name and value within
> each NUL-terminated record (name\nvalue\0). In v6 I changed this
> to use NUL for both separators, but this diverges from the
> convention established by git config -z, which outputs
> key\nvalue\0. Matching that convention is the right call here.

OK. The key cannot have NUL nor LF in it, so either _should_ work, and using LF in the context of -z somehow makes our eyes feel dirty, but it is how "git config -z" chooses to work, so I agree that this change makes sense.

Shall we mark this topic ready for 'next' now?
Will queue.  Thanks.
Jonatan Holmgren· Feb 24, 2026, 17:12 UTC · re: Jonatan Holmgren · lore

[PATCH 0/2] Fix small issues in alias subsection handling

Hello!

I have two small patches related to the handling of alias subsections. The first one is a documentation fix for the example showing the equivalence between alias.last and alias.last.command, which was missing list continuation marks. The second patch addresses a compatibility issue where an empty subsection ([alias ""]) was not treated as a plain [alias], causing existing entries stored this way to be ignored.

Thanks for considering these patches!
Jonatan Holmgren (2):
  doc: fix list continuation in alias subsection example
  alias: treat empty subsection [alias ""] as plain [alias]
 Documentation/config/alias.adoc |  7 ++++---
 alias.c                         |  4 ++++
 t/t0014-alias.sh                | 14 ++++++++++++++
 3 files changed, 22 insertions(+), 3 deletions(-)
-- 
2.53.0
Jonatan Holmgren· Feb 24, 2026, 17:12 UTC · re: Jonatan Holmgren · lore

[PATCH 1/2] doc: fix list continuation in alias subsection example

The example showing the equivalence between alias.last and alias.last.command was missing the list continuation marks (+ between the shell session block and the following prose, leaving the paragraph detached from the list item in the rendered output.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 Documentation/config/alias.adoc | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
index 115fdbb1e3..7830379f58 100644
--- a/Documentation/config/alias.adoc
+++ b/Documentation/config/alias.adoc
@@ -30,13 +30,14 @@ Examples:
 ----
 +
 With a Git alias defined, e.g.,
-
++
     $ git config --global alias.last "cat-file commit HEAD"
     # Which is equivalent to
     $ git config --global alias.last.command "cat-file commit HEAD"
++
+`git last` is equivalent to `git cat-file commit HEAD`. 
 
-`git last` is equivalent to `git cat-file commit HEAD`. To avoid
-confusion and troubles with script usage, aliases that
+To avoid confusion and troubles with script usage, aliases that
 hide existing Git commands are ignored except for deprecated
 commands.  Arguments are split by
 spaces, the usual shell quoting and escaping are supported.
-- 
2.53.0
Junio C Hamano· Feb 24, 2026, 19:11 UTC · re: Jonatan Holmgren · lore

Re: [PATCH 1/2] doc: fix list continuation in alias subsection example

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 9 quoted lines
> The example showing the equivalence between alias.last and
> alias.last.command was missing the list continuation marks (+
> between the shell session block and the following prose, leaving
> the paragraph detached from the list item in the rendered output.
>
> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
> ---
>  Documentation/config/alias.adoc | 7 ++++---
>  1 file changed, 4 insertions(+), 3 deletions(-)

This seems to address the same issue as 0ed2275f (doc: config: fix list continuation in alias section, 2026-02-23). Should we treat this one as a replacement of that one?

Show 22 quoted lines
> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
> index 115fdbb1e3..7830379f58 100644
> --- a/Documentation/config/alias.adoc
> +++ b/Documentation/config/alias.adoc
> @@ -30,13 +30,14 @@ Examples:
>  ----
>  +
>  With a Git alias defined, e.g.,
> -
> ++
>      $ git config --global alias.last "cat-file commit HEAD"
>      # Which is equivalent to
>      $ git config --global alias.last.command "cat-file commit HEAD"
> ++
> +`git last` is equivalent to `git cat-file commit HEAD`. 
>  
> -`git last` is equivalent to `git cat-file commit HEAD`. To avoid
> -confusion and troubles with script usage, aliases that
> +To avoid confusion and troubles with script usage, aliases that
>  hide existing Git commands are ignored except for deprecated
>  commands.  Arguments are split by
>  spaces, the usual shell quoting and escaping are supported.
Kristoffer Haugsbakk· Feb 24, 2026, 19:14 UTC · re: Junio C Hamano · lore

Re: [PATCH 1/2] doc: fix list continuation in alias subsection example

On Tue, Feb 24, 2026, at 20:11, Junio C Hamano wrote:
Show 15 quoted lines
> Jonatan Holmgren <jonatan@jontes.page> writes:
>
>> The example showing the equivalence between alias.last and
>> alias.last.command was missing the list continuation marks (+
>> between the shell session block and the following prose, leaving
>> the paragraph detached from the list item in the rendered output.
>>
>> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
>> ---
>>  Documentation/config/alias.adoc | 7 ++++---
>>  1 file changed, 4 insertions(+), 3 deletions(-)
>
> This seems to address the same issue as 0ed2275f (doc: config: fix
> list continuation in alias section, 2026-02-23).  Should we treat
> this one as a replacement of that one?
Yeah I think so.
Junio C Hamano· Feb 24, 2026, 20:23 UTC · re: Kristoffer Haugsbakk · lore

Re: [PATCH 1/2] doc: fix list continuation in alias subsection example

"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
Show 18 quoted lines
> On Tue, Feb 24, 2026, at 20:11, Junio C Hamano wrote:
>> Jonatan Holmgren <jonatan@jontes.page> writes:
>>
>>> The example showing the equivalence between alias.last and
>>> alias.last.command was missing the list continuation marks (+
>>> between the shell session block and the following prose, leaving
>>> the paragraph detached from the list item in the rendered output.
>>>
>>> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
>>> ---
>>>  Documentation/config/alias.adoc | 7 ++++---
>>>  1 file changed, 4 insertions(+), 3 deletions(-)
>>
>> This seems to address the same issue as 0ed2275f (doc: config: fix
>> list continuation in alias section, 2026-02-23).  Should we treat
>> this one as a replacement of that one?
>
> Yeah I think so.
Thanks.  Will discard 0ed2275f then.
Jonatan Holmgren· Feb 24, 2026, 17:12 UTC · re: Jonatan Holmgren · lore

[PATCH 2/2] alias: treat empty subsection [alias ""] as plain [alias]

When git-config stores a key of the form alias..name, it records it under an empty subsection ([alias ""]). The new subsection-aware alias lookup would see a non-NULL but zero-length subsection and fall into the subsection code path, where it required a "command" key and thus silently ignored the entry.

Normalize an empty subsection to NULL before any further processing so that entries stored this way continue to work as plain case-insensitive aliases, matching the pre-subsection behaviour.

Users who relied on alias..name to create an alias literally named ".name" may want to migrate to subsection syntax, which looks less confusing:

    [alias ".name"]
        command = <value>

Add tests covering both the empty-subsection compatibility case and the leading-dot alias via the new syntax.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 alias.c          |  4 ++++
 t/t0014-alias.sh | 14 ++++++++++++++
 2 files changed, 18 insertions(+)
diff --git a/alias.c b/alias.c
index 0d636278bc..ec9833dd30 100644
--- a/alias.c
+++ b/alias.c
@@ -30,6 +30,10 @@ static int config_alias_cb(const char *var, const char *value,
 	 * - [alias "name"]
 	 *       command = value  (with subsection, case-sensitive)
 	 */
+	/* Treat [alias ""] (empty subsection) the same as plain [alias]. */
+	if (subsection && !subsection_len)
+		subsection = NULL;
+
 	if (subsection && strcmp(key, "command"))
 		return 0;
 
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index 34bbdb51c5..68b4903cbf 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -183,4 +183,18 @@ test_expect_success 'subsection aliases listed in help -a' '
 	test_grep "förgrena" output
 '
 
+test_expect_success 'empty subsection treated as no subsection' '
+	test_config "alias..something" "!echo foobar" &&
+	git something >actual &&
+	echo foobar >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'alias with leading dot via subsection syntax' '
+	test_config alias.".something".command "!echo foobar" &&
+	git .something >actual &&
+	echo foobar >expect &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.53.0
Junio C Hamano· Feb 26, 2026, 17:00 UTC · re: Jonatan Holmgren · lore

Re: [PATCH 0/2] Fix small issues in alias subsection handling

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 19 quoted lines
> Hello!
>
> I have two small patches related to the handling of alias subsections. 
> The first one is a documentation fix for the example showing the equivalence
> between alias.last and alias.last.command, which was missing list continuation marks. 
> The second patch addresses a compatibility issue where an empty subsection ([alias ""])
> was not treated as a plain [alias], 
> causing existing entries stored this way to be ignored.
>
> Thanks for considering these patches!
>
> Jonatan Holmgren (2):
>   doc: fix list continuation in alias subsection example
>   alias: treat empty subsection [alias ""] as plain [alias]
>
>  Documentation/config/alias.adoc |  7 ++++---
>  alias.c                         |  4 ++++
>  t/t0014-alias.sh                | 14 ++++++++++++++
>  3 files changed, 22 insertions(+), 3 deletions(-)
Memory leaks have been reported for these patches, e.g.,
  https://lore.kernel.org/git/6953f6f2-22e8-4efb-8169-395e1c52634f@intel.com/

I _think_ the following would be sufficient to plug it, but please double check and then send in updated version of these two patches incorporating them.

Thanks.
 git.c  | 2 +-
 help.c | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/git.c b/git.c
index c5fad56813..b5eb740e83 100644
--- a/git.c
+++ b/git.c
@@ -119,7 +119,7 @@ static int list_cmds(const char *spec)
 	}
 	for (size_t i = 0; i < list.nr; i++)
 		puts(list.items[i].string);
-	string_list_clear(&list, 0);
+	string_list_clear(&list, 1);
 	return 0;
 }
 
diff --git a/help.c b/help.c
index 82fb2eaa3f..725e92a195 100644
--- a/help.c
+++ b/help.c
@@ -423,7 +423,7 @@ void list_cmds_by_config(struct string_list *list)
 		return;
 
 	string_list_sort(list);
-	string_list_remove_duplicates(list, 0);
+	string_list_remove_duplicates(list, 1);
 
 	while (*cmd_list) {
 		struct strbuf sb = STRBUF_INIT;
-- 
2.53.0-517-g7bf7f89b4a
Jonatan Holmgren· Feb 26, 2026, 20:53 UTC · re: Jonatan Holmgren · lore

[PATCH v2 0/3] Fix small issues in alias subsection handling

v2: Add patch 3 to fix memory leaks in alias listing reported by
    Jacob Keller.  The leaks were introduced by the jh/alias-i18n
    series (specifically "help: use list_aliases() for alias listing"):
    list_aliases() allocates util pointers but two callers cleared
    the list without freeing them.
    Also fix a stray trailing whitespace in patch 1.
    No changes in patch 2.
Jonatan Holmgren (3):
  doc: fix list continuation in alias subsection example
  alias: treat empty subsection [alias ""] as plain [alias]
  git, help: fix memory leaks in alias listing
 Documentation/config/alias.adoc |  7 ++++---
 alias.c                         |  4 ++++
 git.c                           |  2 +-
 help.c                          |  2 +-
 t/t0014-alias.sh                | 14 ++++++++++++++
 5 files changed, 24 insertions(+), 5 deletions(-)
-- 
2.53.0
Jonatan Holmgren· Feb 26, 2026, 20:53 UTC · re: Jonatan Holmgren · lore

[PATCH v2 2/3] alias: treat empty subsection [alias ""] as plain [alias]

When git-config stores a key of the form alias..name, it records it under an empty subsection ([alias ""]). The new subsection-aware alias lookup would see a non-NULL but zero-length subsection and fall into the subsection code path, where it required a "command" key and thus silently ignored the entry.

Normalize an empty subsection to NULL before any further processing so that entries stored this way continue to work as plain case-insensitive aliases, matching the pre-subsection behaviour.

Users who relied on alias..name to create an alias literally named ".name" may want to migrate to subsection syntax, which looks less confusing:

    [alias ".name"]
        command = <value>

Add tests covering both the empty-subsection compatibility case and the leading-dot alias via the new syntax.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 alias.c          |  4 ++++
 t/t0014-alias.sh | 14 ++++++++++++++
 2 files changed, 18 insertions(+)
diff --git a/alias.c b/alias.c
index 0d636278bc..ec9833dd30 100644
--- a/alias.c
+++ b/alias.c
@@ -30,6 +30,10 @@ static int config_alias_cb(const char *var, const char *value,
 	 * - [alias "name"]
 	 *       command = value  (with subsection, case-sensitive)
 	 */
+	/* Treat [alias ""] (empty subsection) the same as plain [alias]. */
+	if (subsection && !subsection_len)
+		subsection = NULL;
+
 	if (subsection && strcmp(key, "command"))
 		return 0;
 
diff --git a/t/t0014-alias.sh b/t/t0014-alias.sh
index 34bbdb51c5..68b4903cbf 100755
--- a/t/t0014-alias.sh
+++ b/t/t0014-alias.sh
@@ -183,4 +183,18 @@ test_expect_success 'subsection aliases listed in help -a' '
 	test_grep "förgrena" output
 '
 
+test_expect_success 'empty subsection treated as no subsection' '
+	test_config "alias..something" "!echo foobar" &&
+	git something >actual &&
+	echo foobar >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'alias with leading dot via subsection syntax' '
+	test_config alias.".something".command "!echo foobar" &&
+	git .something >actual &&
+	echo foobar >expect &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.53.0
Jonatan Holmgren· Feb 26, 2026, 20:53 UTC · re: Jonatan Holmgren · lore

[PATCH v2 1/3] doc: fix list continuation in alias subsection example

The example showing the equivalence between alias.last and alias.last.command was missing the list continuation marks (+ between the shell session block and the following prose, leaving the paragraph detached from the list item in the rendered output.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 Documentation/config/alias.adoc | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
index 115fdbb1e3..26949a0ccb 100644
--- a/Documentation/config/alias.adoc
+++ b/Documentation/config/alias.adoc
@@ -30,13 +30,14 @@ Examples:
 ----
 +
 With a Git alias defined, e.g.,
-
++
     $ git config --global alias.last "cat-file commit HEAD"
     # Which is equivalent to
     $ git config --global alias.last.command "cat-file commit HEAD"
++
+`git last` is equivalent to `git cat-file commit HEAD`.
 
-`git last` is equivalent to `git cat-file commit HEAD`. To avoid
-confusion and troubles with script usage, aliases that
+To avoid confusion and troubles with script usage, aliases that
 hide existing Git commands are ignored except for deprecated
 commands.  Arguments are split by
 spaces, the usual shell quoting and escaping are supported.
-- 
2.53.0
Kristoffer Haugsbakk· Mar 3, 2026, 09:41 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v2 1/3] doc: fix list continuation in alias subsection example

On Thu, Feb 26, 2026, at 21:53, Jonatan Holmgren wrote:
Show 26 quoted lines
> The example showing the equivalence between alias.last and
> alias.last.command was missing the list continuation marks (+
> between the shell session block and the following prose, leaving
> the paragraph detached from the list item in the rendered output.
>
> Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
> ---
>  Documentation/config/alias.adoc | 7 ++++---
>  1 file changed, 4 insertions(+), 3 deletions(-)
>
> diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
> index 115fdbb1e3..26949a0ccb 100644
> --- a/Documentation/config/alias.adoc
> +++ b/Documentation/config/alias.adoc
> @@ -30,13 +30,14 @@ Examples:
>  ----
>  +
>  With a Git alias defined, e.g.,
> -
> ++
>      $ git config --global alias.last "cat-file commit HEAD"
>      # Which is equivalent to
>      $ git config --global alias.last.command "cat-file commit HEAD"
> ++
> +`git last` is equivalent to `git cat-file commit HEAD`.
>
Missing list continuation.
Show 8 quoted lines
> -`git last` is equivalent to `git cat-file commit HEAD`. To avoid
> -confusion and troubles with script usage, aliases that
> +To avoid confusion and troubles with script usage, aliases that
>  hide existing Git commands are ignored except for deprecated
>  commands.  Arguments are split by
>  spaces, the usual shell quoting and escaping are supported.
> --
> 2.53.0
Jonatan Holmgren· Feb 26, 2026, 20:53 UTC · re: Jonatan Holmgren · lore

[PATCH v2 3/3] git, help: fix memory leaks in alias listing

The list_aliases() function sets the util pointer of each list item to a heap-allocated copy of the alias command value. Two callers failed to free these util pointers:

 - list_cmds() in git.c collects a string list with STRING_LIST_INIT_DUP
   and clears it with string_list_clear(&list, 0), which frees the
   duplicated strings (strdup_strings=1) but not the util pointers.
   Pass free_util=1 to free them.
 - list_cmds_by_config() in help.c calls string_list_sort_u(list, 0) to
   deduplicate the list before processing completion.commands overrides.
   When duplicate entries are removed, the util pointer of each discarded
   item is leaked because free_util=0.  Pass free_util=1 to free them.
Reported-by: Jacob Keller <jacob.e.keller@intel.com>
Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 git.c  | 2 +-
 help.c | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/git.c b/git.c
index 744cb6527e..aeb099ab11 100644
--- a/git.c
+++ b/git.c
@@ -119,7 +119,7 @@ static int list_cmds(const char *spec)
 	}
 	for (size_t i = 0; i < list.nr; i++)
 		puts(list.items[i].string);
-	string_list_clear(&list, 0);
+	string_list_clear(&list, 1);
 	return 0;
 }
 
diff --git a/help.c b/help.c
index 95f576c5c8..3e59d07c37 100644
--- a/help.c
+++ b/help.c
@@ -422,7 +422,7 @@ void list_cmds_by_config(struct string_list *list)
 	if (repo_config_get_string_tmp(the_repository, "completion.commands", &cmd_list))
 		return;
 
-	string_list_sort_u(list, 0);
+	string_list_sort_u(list, 1);
 
 	while (*cmd_list) {
 		struct strbuf sb = STRBUF_INIT;
-- 
2.53.0
Junio C Hamano· Feb 26, 2026, 21:08 UTC · re: Jonatan Holmgren · lore

Re: [PATCH v2 0/3] Fix small issues in alias subsection handling

Jonatan Holmgren <jonatan@jontes.page> writes:
Show 9 quoted lines
> v2: Add patch 3 to fix memory leaks in alias listing reported by
>     Jacob Keller.  The leaks were introduced by the jh/alias-i18n
>     series (specifically "help: use list_aliases() for alias listing"):
>     list_aliases() allocates util pointers but two callers cleared
>     the list without freeing them.
>
>     Also fix a stray trailing whitespace in patch 1.
>
>     No changes in patch 2.
Thanks.  Looking good.
Let's mark the topic for 'next'.
Jonatan Holmgren· Mar 3, 2026, 15:12 UTC · re: Jonatan Holmgren · lore

[PATCH] doc: fix list continuation in alias.adoc

Add missing list continuation marks ('+') after code blocks and shell examples so paragraphs render correctly as part of the preceding list item.

Signed-off-by: Jonatan Holmgren <jonatan@jontes.page>
---
 Documentation/config/alias.adoc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Documentation/config/alias.adoc b/Documentation/config/alias.adoc
index 26949a0ccb..dc6ca0ee08 100644
--- a/Documentation/config/alias.adoc
+++ b/Documentation/config/alias.adoc
@@ -36,7 +36,7 @@ With a Git alias defined, e.g.,
     $ git config --global alias.last.command "cat-file commit HEAD"
 +
 `git last` is equivalent to `git cat-file commit HEAD`.
-
++
 To avoid confusion and troubles with script usage, aliases that
 hide existing Git commands are ignored except for deprecated
 commands.  Arguments are split by
-- 
2.53.0

← back to recent threads